/** * wechat-tracker.js — 实时跟踪微信收发消息 * * 通过 adb shell su -c 读取 LSPosed/Vector 日志,解析 HermesWX 标签的日志行。 * 支持:轮询模式(默认)和流式监听模式。 */ const { spawn } = require('child_process'); const path = require('path'); const EventEmitter = require('events'); const ADB = process.env.ADB_PATH || (() => { try { const projectAdb = path.resolve(__dirname, '../../../../platform-tools/adb'); require('fs').accessSync(projectAdb, require('fs').constants.X_OK); return projectAdb; } catch { return 'adb'; } })(); const LOG_PATTERN = /\[HermesWX\].*?MSG\s+(SEND|RECV)\s+talker=([^\s]+)\s+type=(\d+)\s+msgId=(\d+)\s+content=(.+)/; const SEND_RESULT_PATTERN = /\[HermesWX\].*?Send result:\s+(true|false)/; const BROADCAST_PATTERN = /\[HermesWX\].*?Broadcast received:\s+to=(.+)/; class WechatTracker extends EventEmitter { constructor() { super(); this.polling = false; this.lastLogTime = null; this.streamProc = null; } /** * 一次性拉取最近 N 条消息记录 * @param {number} n — 最近几条,默认 50 * @returns {Promise>} */ async fetchRecent(n = 50) { const cmd = `/debug_ramdisk/su -c "cat /data/adb/lspd/log/verbose_*.log 2>/dev/null | grep 'HermesWX' | tail -${n}"`; try { const out = await adbShell(cmd, 10000); return this._parseLines(out); } catch (e) { return []; } } /** * 开始轮询监听(轻量级,不需要持续 adb 连接) * @param {number} intervalMs — 轮询间隔,默认 3000ms */ startPolling(intervalMs = 3000) { if (this.polling) return; this.polling = true; const tick = async () => { if (!this.polling) return; try { const msgs = await this.fetchRecent(20); for (const msg of msgs) { if (this.lastLogTime && msg.timestamp <= this.lastLogTime) continue; this.emit('message', msg); this.lastLogTime = msg.timestamp; } } catch (e) { this.emit('error', e); } setTimeout(tick, intervalMs); }; tick(); } stopPolling() { this.polling = false; } /** * 开始流式监听(通过 adb shell 持续 tail -f 日志,更实时但占用 adb 连接) */ startStream() { if (this.streamProc) return; const cmd = `shell "/debug_ramdisk/su" "-c" "tail -f /data/adb/lspd/log/verbose_*.log"`; const args = cmd.split(' '); this.streamProc = spawn(ADB, args); const rl = require('readline').createInterface({ input: this.streamProc.stdout }); rl.on('line', line => { const msgs = this._parseLines(line); for (const msg of msgs) { this.emit('message', msg); } const sendResult = this._parseSendResult(line); if (sendResult) this.emit('sendResult', sendResult); const broadcast = this._parseBroadcast(line); if (broadcast) this.emit('sendRequest', broadcast); }); this.streamProc.stderr.on('data', d => { const err = d.toString().trim(); if (err) this.emit('error', new Error(err)); }); this.streamProc.on('close', () => { this.streamProc = null; this.emit('close'); }); } stopStream() { if (this.streamProc) { this.streamProc.kill(); this.streamProc = null; } } /** * 解析单行或多行日志 */ _parseLines(text) { const messages = []; if (!text) return messages; for (const line of text.split('\n')) { const m = line.match(LOG_PATTERN); if (m) { messages.push({ type: m[1].toLowerCase(), // 'send' | 'recv' talker: m[2], msgType: parseInt(m[3], 10), msgId: parseInt(m[4], 10), content: m[5], raw: line.trim(), timestamp: Date.now(), }); } } return messages; } _parseSendResult(line) { const m = line.match(SEND_RESULT_PATTERN); if (m) return { success: m[1] === 'true', raw: line.trim() }; return null; } _parseBroadcast(line) { const m = line.match(BROADCAST_PATTERN); if (m) return { toUser: m[1], raw: line.trim() }; return null; } } function adbShell(cmd, timeoutMs = 15000) { return new Promise((resolve, reject) => { const args = cmd.startsWith('shell ') ? cmd.split(' ') : ['shell', ...cmd.split(' ')]; const proc = spawn(ADB, 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(`adb exit ${code}: ${stderr.trim() || stdout.trim()}`)); } else { resolve(stdout.trim()); } }); proc.on('error', reject); }); } module.exports = { WechatTracker, adbShell };