android-wechat-tracker-sqlite.js 3.88 KB
/**
 * 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 };