import DatabaseConstructor from 'better-sqlite3-multiple-ciphers' import fs from 'fs-extra' import path from 'path' import crypto from 'crypto' import os from 'os' type Database = import('better-sqlite3-multiple-ciphers').Database export interface UserContact { m_nsUsrName: string nickname: 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' } export interface GroupMemberInfo { m_nsUsrName: string nickname: string m_nsHeadImgUrl: string } export class WechatDb { private static WECHAT_DIR = path.join( os.homedir(), 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9' ) private rawKey: string private correctUserId: string | null = null private chatDb: { name: string; db_number: string }[] | null = null private groupMemberCache = new Map() constructor(rawKey: string) { this.rawKey = rawKey console.log(`Initializing WechatDb with key: ${rawKey}`) if (!fs.existsSync(WechatDb.WECHAT_DIR)) { throw new Error(`WeChat directory not found at ${WechatDb.WECHAT_DIR}`) } if (this.getUser()) { console.log('User found, getting chat DB number') this.chatDb = this.getChatDbNumber() } else { throw new Error('No valid user found or invalid key') } } private connectDb(dbPath: string): Database | null { const targetPath = path.resolve(dbPath) if (!fs.existsSync(targetPath)) { console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`) return null } try { const db = new DatabaseConstructor(targetPath, { // verbose: console.log }) // 配置加密方案 (Cipher Scheme) db.pragma("cipher='sqlcipher'") db.pragma('cipher_page_size = 1024') db.pragma('legacy=3') db.pragma('kdf_iter = 64000') db.pragma('cipher_hmac_algorithm = HMAC_SHA1') db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1') const processedKey = this.processRawKey(this.rawKey) // console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`) db.pragma(`key = "x'${processedKey}'"`) // 验证连接 db.prepare('SELECT count(*) as count FROM sqlite_master').get() return db } catch (error) { console.error(`❌ 连接失败:`, error) const errString = String(error) if (errString.includes('file is not a database')) { console.error( "💡 诊断建议:\n1. 请确认是否已安装 'better-sqlite3-multiple-ciphers'。\n2. 检查密钥是否正确(WeChat密钥与设备强绑定)。\n3. 尝试将页大小改为 4096 (db.pragma('cipher_page_size = 4096'))。" ) } else if (errString.includes('HMAC')) { console.error( '💡 诊断建议:密钥可能正确,但HMAC算法或页大小不匹配。尝试调整 cipher_hmac_algorithm。' ) } return null } } private processRawKey(key: string): string { // 移除可能的 '0x' 前缀并修剪空格 return key.replace(/^0x/i, '').trim() } private getUser(): boolean { const keyValuePath = path.join(WechatDb.WECHAT_DIR, 'KeyValue') if (!fs.existsSync(keyValuePath)) return false const contents = fs.readdirSync(keyValuePath) const potentialUsers = contents.filter((name) => !name.startsWith('.')) for (const user of potentialUsers) { const dbPath = path.join(keyValuePath, user, 'KeyValue.db') if (fs.existsSync(dbPath)) { const db = this.connectDb(dbPath) if (db) { this.correctUserId = user console.log(`Valid user found: ${user}`) db.close() return true } } } return false } private getChatDbNumber(): { name: string; db_number: string }[] { const chatDb: { name: string; db_number: string }[] = [] if (!this.correctUserId) return [] for (let i = 0; i < 10; i++) { const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', `msg_${i}.db`) if (!fs.existsSync(dbPath)) continue const db = this.connectDb(dbPath) if (db) { const tables = db .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'") .all() as { name: string }[] for (const table of tables) { chatDb.push({ name: table.name, db_number: `msg_${i}.db` }) } db.close() } } return chatDb } public getUserList(nicknameFilter?: string): UserContact[] { if (!this.correctUserId) return [] const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Contact/wccontact_new2.db') const db = this.connectDb(dbPath) if (!db) return [] let query = "SELECT m_nsUsrName, nickname FROM WCContact WHERE m_nsUsrName NOT LIKE 'gh_%'" if (nicknameFilter) { query += ` AND nickname LIKE '%${nicknameFilter}%'` } const results = db.prepare(query).all() as unknown as UserContact[] db.close() return results } public getAllGroupContacts(): Record { if (!this.correctUserId) return {} const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') const db = this.connectDb(dbPath) if (!db) return {} const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupContact').all() as { m_nsUsrName: string nickname: string }[] db.close() const groupContacts: Record = {} for (const row of results) { if (row.m_nsUsrName && row.nickname) { const md5 = this.md5(row.m_nsUsrName) groupContacts[md5] = row.nickname } } return groupContacts } public getAllGroupMembers(): Record { if (!this.correctUserId) return {} const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') const db = this.connectDb(dbPath) if (!db) return {} // 查询 GroupMember 表 const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupMember').all() as { m_nsUsrName: string nickname: string }[] db.close() const groupMembers: Record = {} for (const row of results) { if (row.m_nsUsrName && row.nickname) { // 将 wxid (m_nsUsrName) 映射到昵称 groupMembers[row.m_nsUsrName] = row.nickname } } return groupMembers } public getGroupMember(wxid: string): GroupMemberInfo | null { if (!this.correctUserId) return null // 检查缓存 if (this.groupMemberCache.has(wxid)) { return this.groupMemberCache.get(wxid) || null } const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db') const db = this.connectDb(dbPath) if (!db) { this.groupMemberCache.set(wxid, null) return null } const result = db .prepare( 'SELECT m_nsUsrName, nickname, m_nsHeadImgUrl FROM GroupMember WHERE m_nsUsrName = ?' ) .get(wxid) as GroupMemberInfo db.close() // 写入缓存 this.groupMemberCache.set(wxid, result || null) return result || null } public getAllChatTables(): { name: string; db_number: string }[] { return this.chatDb || [] } public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] { if (!this.chatDb || !this.correctUserId) return [] const tableName = `Chat_${userMd5}` const tableInfo = this.chatDb.find((t) => t.name === tableName) if (!tableInfo) { console.error(`No table found for ${tableName}`) return [] } const dbPath = path.join( WechatDb.WECHAT_DIR, this.correctUserId, 'Message', tableInfo.db_number ) const db = this.connectDb(dbPath) if (!db) return [] try { let query = `SELECT * FROM ${tableName}` const conditions: string[] = [] // console.log(startTime, endTime, 'time range') if (startTime) { conditions.push(`msgCreateTime >= ${startTime}`) } if (endTime) { conditions.push(`msgCreateTime <= ${endTime}`) } if (conditions.length > 0) { query += ` WHERE ${conditions.join(' AND ')}` } // 添加排序以确保存储正确的顺序 query += ` ORDER BY msgCreateTime ASC` const results = db.prepare(query).all() as unknown as WechatMessage[] db.close() return results } catch (e) { console.error(`Error querying messages for ${tableName}:`, e) db.close() return [] } } public searchAllMessages(keyword: string): string | null { if (!this.correctUserId) return null for (let i = 0; i < 10; i++) { const dbName = `msg_${i}.db` const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', dbName) if (!fs.existsSync(dbPath)) continue const db = this.connectDb(dbPath) if (!db) continue const tables = db .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'") .all() as { name: string }[] for (const table of tables) { try { // 首先检查 msgContent 列是否存在以避免错误? // 或者像 Swift 代码中那样使用 try/catch "抑制错误" const results = db .prepare(`SELECT 1 FROM ${table.name} WHERE msgContent LIKE '%${keyword}%' LIMIT 1`) .get() if (results) { console.log(`Found keyword in ${table.name} (${dbName})`) db.close() return table.name } } catch { // 忽略没有 msgContent 的表 } } db.close() } return null } public md5(str: string): string { return crypto.createHash('md5').update(str).digest('hex') } }