import { WechatDb, WechatMessage } from '../wechat-db' import { parseImageDatNameFromRow, parseMessageContent, parseStickerMessageFromRow } from '../message-parser' import type { DatabaseKeyValidationCode, DatabaseKeyValidationResult } from '../../shared/database-key' import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service' export function getCurrentKey(): string { if (!dbRef) return '' try { return dbRef.getWcdb4Client().getKey() } catch { return '' } } export function getCurrentAccountRoot(): string { if (!dbRef) return '' try { return dbRef.getWcdb4Client().getAccountRoot() } catch { return '' } } export interface FormattedContact { m_nsUsrName: string m_nsNickName: string md5: string type: 'user' | 'group' avatar?: string } export interface FormattedMessage { id: string from: string type: string datetime: string content: string isSender: boolean img?: string name?: string senderId?: string contentData?: ReturnType voiceDataUrl?: string voiceDuration?: number localId?: number serverId?: string createTime?: number sessionId?: string recalled?: boolean recalledBy?: string } export interface GroupSnapshot { roomId: string memberCount: number members: { wxid: string nickname: string groupNickname: string wechatNickname: string remark: string avatar: string }[] } const MSG_TYPE_DICT: Record = { 1: '普通文本', 3: '图片', 34: '语音', 42: '名片', 43: '视频', 47: '表情包', 48: '位置', 49: '分享消息', 50: '通话', 10000: '系统消息' } function normalizeMsgType(value: string | number | undefined): number { const raw = String(value ?? '').trim() if (!raw) return 0 try { const parsed = BigInt(raw) const low32 = Number(parsed & 0xffffffffn) return low32 || Number(parsed) } catch { const parsed = Number(raw) if (!Number.isFinite(parsed)) return 0 return parsed > 0xffffffff ? parsed >>> 0 : parsed } } let dbRef: WechatDb | null = null export function setChatDb(db: WechatDb | null): void { dbRef?.close() dbRef = db } export function getChatDb(): WechatDb | null { return dbRef } export function isReady(): boolean { return dbRef !== null } export function listContacts(filter?: string): FormattedContact[] { if (!dbRef) return [] const contacts: FormattedContact[] = [] const groupContacts = dbRef.getAllGroupContacts() const userList = dbRef.getUserList(filter) const existingMd5s = new Set() for (const user of userList) { const md5 = dbRef.md5(user.m_nsUsrName) const isGroup = user.m_nsUsrName.endsWith('@chatroom') existingMd5s.add(md5) contacts.push({ m_nsUsrName: user.m_nsUsrName, m_nsNickName: user.nickname || '未知用户', md5, type: isGroup ? 'group' : 'user', avatar: typeof user.avatar === 'string' ? user.avatar : undefined }) } const chatTables = dbRef.getAllChatTables() for (const table of chatTables) { if (!table.name.startsWith('Chat_')) continue const md5 = table.name.substring(5) if (existingMd5s.has(md5)) continue if (groupContacts[md5]) { contacts.push({ m_nsUsrName: `Group_${md5}`, m_nsNickName: groupContacts[md5], md5, type: 'group' }) } else { contacts.push({ m_nsUsrName: `Unknown_${md5}`, m_nsNickName: `Chat_${md5}`, md5, type: 'user' }) } } return contacts } export function getContactAvatars(usernames: string[]): Record { if (!dbRef) return {} const normalized = Array.from( new Set((usernames || []).map((username) => String(username || '').trim()).filter(Boolean)) ) if (normalized.length === 0) return {} return dbRef.getWcdb4Client().getAvatarUrls(normalized) } function listSourceMessages( userMd5: string, startTime?: number, endTime?: number, options?: { limit?: number } ): FormattedMessage[] { if (!dbRef) return [] const startedAt = Date.now() const wcdb4Client = dbRef.getWcdb4Client() const username = wcdb4Client.getUsernameByMd5(userMd5) const isGroupChat = Boolean(username?.endsWith('@chatroom')) console.log( `[ChatService] listMessages begin md5=${userMd5} username=${username || ''} start=${startTime || 0} end=${endTime || 0} limit=${options?.limit || 0}` ) const rawMessages = dbRef.getUserMessages(userMd5, startTime, endTime, options) console.log( `[ChatService] listMessages native done md5=${userMd5} raw=${rawMessages.length} cost=${Date.now() - startedAt}ms` ) const formatted = rawMessages.map((msg: WechatMessage) => { const rawMsgType = parseInt(msg.messageType) const msgType = normalizeMsgType(msg.messageType) const createTime = parseInt(msg.msgCreateTime) const date = new Date(createTime * 1000) const isMine = msg.mesDes !== 1 const localId = parseInt(msg.mesLocalID) || 0 let content = msg.msgContent let img = '' let name = '' let senderId = typeof msg.sender === 'string' ? msg.sender : '' if (isMine) { name = typeof msg.senderNickname === 'string' ? msg.senderNickname : '' } else { if (typeof msg.senderAvatar === 'string') img = msg.senderAvatar if (typeof msg.senderNickname === 'string') name = msg.senderNickname } if (isGroupChat && content && typeof content === 'string') { const colonIndex = content.indexOf(':') if (colonIndex > 0) { const potentialSenderId = content.substring(0, colonIndex).trim() if (/^[a-zA-Z0-9_@.-]{3,64}$/.test(potentialSenderId)) { senderId = senderId || potentialSenderId name = name || potentialSenderId content = content.substring(colonIndex + 1).replace(/^\s+/, '') } } } if (!isMine && !name && senderId) name = senderId let contentData: ReturnType | undefined let displayType = MSG_TYPE_DICT[msgType] || msg.messageType const inferredMsgType = typeof content === 'string' && / member?.m_nsUsrName) .map((member) => ({ wxid: member.m_nsUsrName, nickname: member.nickname || '', groupNickname: member.groupNickname || '', wechatNickname: member.wechatNickname || '', remark: member.remark || '', avatar: member.m_nsHeadImgUrl || '' })) return { roomId, memberCount: members.length, members } } export function searchMessages(keyword: string): string | null { if (!dbRef) return null return dbRef.searchAllMessages(keyword) } export function listRecentChat(limit = 50): FormattedContact[] { const contacts = listContacts() return contacts.slice(0, limit) } export function resolveMd5(query: string): FormattedContact | null { const trimmed = query.trim() if (!trimmed) return null const lower = trimmed.toLowerCase() const contacts = listContacts() const exact = contacts.find( (c) => c.md5 === trimmed || c.m_nsUsrName.toLowerCase() === lower || c.m_nsNickName.toLowerCase() === lower ) if (exact) return exact const partial = contacts.find( (c) => c.m_nsNickName.toLowerCase().includes(lower) || c.m_nsUsrName.toLowerCase().includes(lower) ) return partial || null } export interface SelfAccountInfo { wxid: string nickname: string avatar?: string accountRoot: string } export function getSelfAccountInfo(): SelfAccountInfo | null { if (!dbRef) return null const wcdb = dbRef.getWcdb4Client() const accountRoot = wcdb.getAccountRoot() const usernameCandidates = wcdb.getMyUsernameCandidates() const primaryUsername = usernameCandidates[0] ?? '' const wxid = primaryUsername && primaryUsername.toLowerCase().startsWith('wxid_') ? primaryUsername : wcdb.getUsernameByMd5(wcdb.md5(accountRoot.split('/').pop() || '')) || primaryUsername let nickname = '' let avatar: string | undefined try { avatar = wcdb.getMyAvatarUrl() } catch { avatar = undefined } if (usernameCandidates.length) { const contacts = listContacts() const self = contacts.find( (c) => usernameCandidates.includes(c.m_nsUsrName) || (c.type === 'user' && usernameCandidates.some((u) => c.m_nsUsrName.includes(u))) ) if (self) { nickname = self.m_nsNickName avatar = avatar || self.avatar } } return { wxid: wxid || primaryUsername || '', nickname: nickname || wxid || '我', avatar, accountRoot } } export function testConnection(key: string, accountRoot?: string): DatabaseKeyValidationResult { const probeKey = key.replace(/^0x/i, '').trim() if (!/^[0-9a-f]{64}$/i.test(probeKey)) { return { success: false, code: 'INVALID_FORMAT', error: '密钥格式不正确' } } let probe: WechatDb | null = null let ownsProbe = false try { const current = dbRef const canReuseCurrent = current && getCurrentKey().replace(/^0x/i, '').trim() === probeKey && (!accountRoot || getCurrentAccountRoot() === accountRoot) const validationDb = canReuseCurrent ? current : accountRoot ? new WechatDb(probeKey, accountRoot) : new WechatDb(probeKey) probe = validationDb ownsProbe = validationDb !== current const client = validationDb.getWcdb4Client() const contacts = client.getSessions() const messages = client.getChatTables() if (contacts[0]) client.getMessages(contacts[0].username, undefined, undefined, { limit: 1 }) return { success: true, accountRoot: client.getAccountRoot(), wxid: (client.getMyUsernameCandidates?.() ?? [])[0] || '', contacts: { available: true, count: contacts.length }, messages: { available: true, count: messages.length } } } catch (error) { const detail = error instanceof Error ? error.message : String(error) const code = mapConnectionError(detail) return { success: false, code, error: DATABASE_KEY_ERROR_MESSAGES[code] } } finally { try { // macOS native shutdown is process-wide. Do not tear down the active reader // when validating a replacement key; the new runtime connection takes over on save. if (ownsProbe && !(process.platform === 'darwin' && dbRef)) probe?.close() } catch { // Validation probes are best-effort closed without exposing native details. } } } const DATABASE_KEY_ERROR_MESSAGES: Record = { INVALID_FORMAT: '密钥格式不正确', DATABASE_OPEN_FAILED: '无法打开数据库', ACCOUNT_MISMATCH: '密钥与当前账号不匹配', ROOT_UNAVAILABLE: '当前数据库目录不可用', DATABASE_FILE_MISSING: '数据库文件缺失', UNKNOWN_VALIDATION_ERROR: '未知验证错误' } function mapConnectionError(detail: string): DatabaseKeyValidationCode { const normalized = detail.toLowerCase() if (normalized.includes('-1005') || normalized.includes('不匹配')) return 'ACCOUNT_MISMATCH' if (normalized.includes('session.db') || normalized.includes('数据库文件')) { return 'DATABASE_FILE_MISSING' } if ( normalized.includes('数据目录') || normalized.includes('账号目录') || normalized.includes('db_storage') ) { return 'ROOT_UNAVAILABLE' } if (normalized.includes('wcdb_open_account') || normalized.includes('open')) { return 'DATABASE_OPEN_FAILED' } return 'UNKNOWN_VALIDATION_ERROR' } export function reopenWithRoot(accountRoot: string): boolean { if (!dbRef) return false const key = getCurrentKey() if (!key) return false try { const next = new WechatDb(key, accountRoot) setChatDb(next) return true } catch (error) { console.error('[ChatService] reopen with root failed:', error) return false } }