Files
WechatExplorer/src/main/wechat-db.ts
T
Wxw-Gu c70e49bf16 fix: 完善聊天解析与导出体验
- 修复引用消息名称和图片布局
- 明确单会话图片测试日志范围
- 支持导出文件附件
- 完善图片批测、会话刷新和安全退出
2026-08-04 12:03:07 +08:00

207 lines
6.1 KiB
TypeScript

import { Wcdb4Client, Wcdb4MessageQueryOptions } from './wcdb4-client'
export interface UserContact {
m_nsUsrName: string
nickname: string
avatar?: string
wechatNickname?: string
remark?: string
isFolded?: boolean
isMuted?: boolean
}
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>()
private chatTableMappingLoaded = false
static async create(rawKey: string, accountRoot?: string): Promise<WechatDb> {
const client = new Wcdb4Client(rawKey, accountRoot)
await client.openAsync()
return new WechatDb(rawKey, accountRoot, client)
}
constructor(
rawKey: string,
accountRoot?: string,
clientOverride?: Wcdb4Client,
initialChatTables?: { name: string; db_number: string }[]
) {
console.log(`Initializing WechatDb with key length: ${rawKey.trim().length}`)
const client = clientOverride || new Wcdb4Client(rawKey, accountRoot)
if (!clientOverride) client.open()
this.wcdb4Client = client
for (const table of initialChatTables || []) {
if (table.name.startsWith('Chat_')) {
this.chatMd5ToUsername.set(table.name.substring(5), table.db_number)
}
}
this.chatTableMappingLoaded = Boolean(initialChatTables)
}
private ensureChatTableMapping(): void {
if (this.chatTableMappingLoaded) return
for (const table of this.wcdb4Client.getChatTables()) {
if (table.name.startsWith('Chat_')) {
this.chatMd5ToUsername.set(table.name.substring(5), table.db_number)
}
}
this.chatTableMappingLoaded = true
}
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,
wechatNickname: session.wechatNickname,
remark: session.remark,
isFolded: session.isFolded,
isMuted: session.isMuted
}))
.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> {
this.ensureChatTableMapping()
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 closeAsync(timeoutMs?: number): Promise<boolean> {
return this.wcdb4Client.closeAsync(timeoutMs)
}
public getUserMessages(
userMd5: string,
startTime?: number,
endTime?: number,
options?: Wcdb4MessageQueryOptions
): WechatMessage[] {
this.ensureChatTableMapping()
const username = this.chatMd5ToUsername.get(userMd5)
if (!username) return []
return this.wcdb4Client.getMessages(username, startTime, endTime, options).map((message) => ({
...message,
...message.raw
}))
}
public async getUserMessagesAsync(
userMd5: string,
startTime?: number,
endTime?: number,
options?: Wcdb4MessageQueryOptions
): Promise<WechatMessage[]> {
this.ensureChatTableMapping()
const username = this.chatMd5ToUsername.get(userMd5)
if (!username) return []
const messages = await this.wcdb4Client.getMessagesAsync(username, startTime, endTime, options)
return messages.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)
}
}