mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
让 WechatExplorer 既能用图形界面浏览聊天记录,也能作为本机 MCP 数据源被
Claude / Codex 等客户端通过 127.0.0.1:6131 直接拉取。
本机 HTTP API
- 新增 http-server: 8 个端点,覆盖 health / current_time / contact /
chatroom / recent_chat / chatlog / group_snapshot / resolve / report
- 时间参数支持 YYYY-MM-DD / YYYY-MM-DD/HH:mm / Unix 秒级;日期单独使用
时自动补到 00:00:00~23:59:59,避免漏消息
- apiServer 单例支持动态启停,启动失败返回 friendlyMessage(把
EADDRINUSE 翻译成"端口已被占用"的中文错误并附 4 次重试)
数据库根目录与自服务入口
- Wcdb4Client 接受 accountRoot 时会自动解析:父目录下找最新含
db_storage 的 wxid 子目录;设置面板"测试连接"成功后回写解析后的
精确路径
- 抽 chat-service.ts:IPC 和 HTTP 共享 listContacts / listMessages /
getGroupSnapshot / searchMessages / getSelfAccountInfo / testConnection
/ reopenWithRoot
- WechatDb 接受可选 accountRoot;设置面板新增"应用并重新初始化"
按钮,改完 dbRoot 立即生效
- 新增 settings-store.ts,dbRoot / apiEnabled / apiHost / apiPort 落到
userData/settings.json
主进程健壮性
- 新增 safe-log.ts 包一层 console.log/warn/error,electron-vite 关闭
子进程 stderr 后写 EPIPE 不再炸 IPC handler(原 main build 启动时
即 installSafeConsole)
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
import { Wcdb4Client } from './wcdb4-client'
|
|
|
|
export interface UserContact {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
avatar?: string
|
|
}
|
|
|
|
export interface WechatMessage {
|
|
mesLocalID: string
|
|
mesDes: number
|
|
messageType: string
|
|
msgCreateTime: string
|
|
msgContent: string
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[key: string]: any
|
|
}
|
|
|
|
export interface Contact {
|
|
m_nsUsrName: string
|
|
m_nsNickName: string
|
|
md5: string
|
|
type: 'user' | 'group'
|
|
avatar?: string
|
|
}
|
|
|
|
export interface GroupMemberInfo {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
m_nsHeadImgUrl: string
|
|
}
|
|
|
|
export class WechatDb {
|
|
private wcdb4Client: Wcdb4Client
|
|
private chatMd5ToUsername = new Map<string, string>()
|
|
|
|
constructor(rawKey: string, accountRoot?: string) {
|
|
console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`)
|
|
const client = new Wcdb4Client(rawKey, accountRoot)
|
|
client.open()
|
|
this.wcdb4Client = client
|
|
for (const table of client.getChatTables()) {
|
|
if (table.name.startsWith('Chat_')) {
|
|
this.chatMd5ToUsername.set(table.name.substring(5), table.db_number)
|
|
}
|
|
}
|
|
}
|
|
|
|
public getUserList(nicknameFilter?: string): UserContact[] {
|
|
const keyword = (nicknameFilter || '').trim().toLowerCase()
|
|
return this.wcdb4Client
|
|
.getSessions()
|
|
.map((session) => ({
|
|
m_nsUsrName: session.username,
|
|
nickname: session.nickname || session.username,
|
|
avatar: session.avatar
|
|
}))
|
|
.filter((contact) => {
|
|
if (!keyword) return true
|
|
return (
|
|
contact.m_nsUsrName.toLowerCase().includes(keyword) ||
|
|
contact.nickname.toLowerCase().includes(keyword)
|
|
)
|
|
})
|
|
}
|
|
|
|
public getAllGroupContacts(): Record<string, string> {
|
|
const groupContacts: Record<string, string> = {}
|
|
for (const session of this.wcdb4Client.getSessions()) {
|
|
if (session.username.endsWith('@chatroom')) {
|
|
groupContacts[this.md5(session.username)] = session.nickname || session.username
|
|
}
|
|
}
|
|
return groupContacts
|
|
}
|
|
|
|
public getAllGroupMembers(): Record<string, string> {
|
|
const members: Record<string, string> = {}
|
|
for (const session of this.wcdb4Client.getSessions()) {
|
|
if (!session.username.endsWith('@chatroom')) continue
|
|
for (const member of this.wcdb4Client.getGroupMembers(session.username)) {
|
|
if (member.m_nsUsrName) {
|
|
members[member.m_nsUsrName] = member.nickname || member.m_nsUsrName
|
|
}
|
|
}
|
|
}
|
|
return members
|
|
}
|
|
|
|
public getGroupMembersForChat(userMd5: string): Record<string, string> {
|
|
const username = this.chatMd5ToUsername.get(userMd5)
|
|
if (!username || !username.endsWith('@chatroom')) return {}
|
|
|
|
const members: Record<string, string> = {}
|
|
for (const member of this.wcdb4Client.getGroupMembers(username)) {
|
|
if (member.m_nsUsrName) {
|
|
members[member.m_nsUsrName] = member.nickname || member.m_nsUsrName
|
|
}
|
|
}
|
|
return members
|
|
}
|
|
|
|
public getGroupMember(wxid: string, chatroomId?: string): GroupMemberInfo | null {
|
|
if (!chatroomId) return null
|
|
return (
|
|
this.wcdb4Client
|
|
.getGroupMembers(chatroomId)
|
|
.find((member) => member.m_nsUsrName === wxid) || null
|
|
)
|
|
}
|
|
|
|
public getAllChatTables(): { name: string; db_number: string }[] {
|
|
return this.wcdb4Client.getChatTables()
|
|
}
|
|
|
|
public getMyAvatarUrl(): string | undefined {
|
|
return this.wcdb4Client.getMyAvatarUrl()
|
|
}
|
|
|
|
public getWcdb4Client(): Wcdb4Client {
|
|
return this.wcdb4Client
|
|
}
|
|
|
|
public close(): void {
|
|
this.wcdb4Client.close()
|
|
}
|
|
|
|
public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] {
|
|
const username = this.chatMd5ToUsername.get(userMd5)
|
|
if (!username) return []
|
|
return this.wcdb4Client.getMessages(username, startTime, endTime).map((message) => ({
|
|
...message,
|
|
...message.raw
|
|
}))
|
|
}
|
|
|
|
public searchAllMessages(keyword: string): string | null {
|
|
const lowerKeyword = keyword.trim().toLowerCase()
|
|
if (!lowerKeyword) return null
|
|
for (const session of this.wcdb4Client.getSessions()) {
|
|
const found = this.wcdb4Client
|
|
.getMessages(session.username)
|
|
.some((message) => message.msgContent.toLowerCase().includes(lowerKeyword))
|
|
if (found) return `Chat_${this.md5(session.username)}`
|
|
}
|
|
return null
|
|
}
|
|
|
|
public md5(str: string): string {
|
|
return this.wcdb4Client.md5(str)
|
|
}
|
|
}
|