wechat-tracker.js 5.39 KB
/**
 * 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<Array<Message>>}
     */
    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 };