android-wechat-tracker-sqlite.js
3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* 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 };