/** * android-wechat-tracker-sqlite.js — 通过轮询 SQLite 数据库检测新消息 * * 不依赖 LSPosed HOOK,直接查询 EnMicroMsg.db 的 message 表。 * 需要 root 权限(su -c)。 */ const { spawn } = require('child_process'); const EventEmitter = require('events'); const DB_DIR = '/data/data/com.tencent.mm/MicroMsg'; function hasRoot() { return process.getuid && process.getuid() === 0; } function wrapCmd(cmd) { if (hasRoot()) return cmd; return `su -c "${cmd.replace(/"/g, '\\"')}"`; } function shell(cmd, timeoutMs = 15000) { const fullCmd = wrapCmd(cmd); const args = ['-c', fullCmd]; const shellBin = process.env.SHELL || '/system/bin/sh'; return new Promise((resolve, reject) => { const proc = spawn(shellBin, args, { timeout: timeoutMs }); let stdout = ''; let stderr = ''; proc.stdout.on('data', d => stdout += d.toString()); proc.stderr.on('data', d => stderr += d.toString()); proc.on('close', code => { if (code !== 0) { reject(new Error(`exit ${code}: ${stderr.trim() || stdout.trim()}`)); } else { resolve(stdout.trim()); } }); proc.on('error', reject); }); } /** * 查找 EnMicroMsg.db 的实际路径 */ async function findDbPath() { const cmd = `ls -d ${DB_DIR}/*/EnMicroMsg.db 2>/dev/null | head -1`; try { const out = await shell(cmd, 5000); return out || null; } catch { return null; } } class AndroidWechatTrackerSQLite extends EventEmitter { constructor() { super(); this.polling = false; this.lastMsgId = 0; this.dbPath = null; } async init() { this.dbPath = await findDbPath(); if (!this.dbPath) { throw new Error('EnMicroMsg.db not found'); } // 获取当前最大 msgId const out = await shell(`sqlite3 "${this.dbPath}" "SELECT MAX(msgId) FROM message;"`, 5000); this.lastMsgId = parseInt(out, 10) || 0; console.log(`[TrackerSQLite] DB: ${this.dbPath}, lastMsgId: ${this.lastMsgId}`); } /** * 获取最近 N 条消息 */ async fetchRecent(n = 50) { if (!this.dbPath) await this.init(); const cmd = `sqlite3 "${this.dbPath}" "SELECT msgId, type, isSend, talker, content, createTime FROM message ORDER BY msgId DESC LIMIT ${n};"`; const out = await shell(cmd, 10000); return this._parseRows(out); } /** * 获取新消息(msgId > lastMsgId) */ async fetchNew() { if (!this.dbPath) await this.init(); const cmd = `sqlite3 "${this.dbPath}" "SELECT msgId, type, isSend, talker, content, createTime FROM message WHERE msgId > ${this.lastMsgId} ORDER BY msgId ASC;"`; const out = await shell(cmd, 10000); const msgs = this._parseRows(out); if (msgs.length > 0) { this.lastMsgId = Math.max(...msgs.map(m => m.msgId)); } return msgs; } startPolling(intervalMs = 2000) { if (this.polling) return; this.polling = true; const tick = async () => { if (!this.polling) return; try { const msgs = await this.fetchNew(); for (const msg of msgs) { this.emit('message', msg); } } catch (e) { this.emit('error', e); } setTimeout(tick, intervalMs); }; tick(); } stopPolling() { this.polling = false; } _parseRows(text) { const messages = []; if (!text) return messages; for (const line of text.split('\n')) { const parts = line.split('|'); if (parts.length >= 6) { messages.push({ msgId: parseInt(parts[0], 10), msgType: parseInt(parts[1], 10), isSend: parseInt(parts[2], 10), talker: parts[3], content: parts[4], createTime: parseInt(parts[5], 10), type: parseInt(parts[2], 10) === 1 ? 'send' : 'recv', timestamp: Date.now(), raw: line.trim(), }); } } return messages; } } module.exports = { AndroidWechatTrackerSQLite, shell };