@powercess/qq-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @powercess/qq-mcp might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/index.js +287 -0
- package/dist/index.js.map +1 -0
- package/dist/keyscan.js +156 -0
- package/dist/keyscan.js.map +1 -0
- package/dist/messages.js +309 -0
- package/dist/messages.js.map +1 -0
- package/dist/offline.js +316 -0
- package/dist/offline.js.map +1 -0
- package/dist/schema.js +86 -0
- package/dist/schema.js.map +1 -0
- package/dist/sqlcipher.js +273 -0
- package/dist/sqlcipher.js.map +1 -0
- package/dist/verify-worker.js +18 -0
- package/dist/verify-worker.js.map +1 -0
- package/docs/reverse-engineering.md +188 -0
- package/package.json +62 -0
- package/scripts/scan_memory.py +155 -0
- package/scripts/smoke-client.mjs +30 -0
- package/src/index.ts +341 -0
- package/src/keyscan.ts +202 -0
- package/src/messages.ts +414 -0
- package/src/offline.ts +422 -0
- package/src/schema.ts +101 -0
- package/src/sqlcipher.ts +326 -0
- package/src/verify-worker.ts +18 -0
package/dist/messages.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
export const MSG_BODY_CONTENT_FIELD = 40800;
|
|
2
|
+
/** Decodes one protobuf message level into its wire fields. */
|
|
3
|
+
export function decodeWireFields(buf, maxFields = 512) {
|
|
4
|
+
const out = [];
|
|
5
|
+
let off = 0;
|
|
6
|
+
while (off < buf.length && out.length < maxFields) {
|
|
7
|
+
const [tag, afterTag] = readVarint(buf, off);
|
|
8
|
+
off = afterTag;
|
|
9
|
+
const field = Number(tag >> 3n);
|
|
10
|
+
const wire = Number(tag & 7n);
|
|
11
|
+
if (wire === 0) {
|
|
12
|
+
const [value, next] = readVarint(buf, off);
|
|
13
|
+
off = next;
|
|
14
|
+
out.push({ field, wire, value });
|
|
15
|
+
}
|
|
16
|
+
else if (wire === 2) {
|
|
17
|
+
const [len, next] = readVarint(buf, off);
|
|
18
|
+
off = next;
|
|
19
|
+
const end = off + Number(len);
|
|
20
|
+
if (end > buf.length)
|
|
21
|
+
throw new Error(`field ${field}: length ${len} exceeds buffer`);
|
|
22
|
+
out.push({ field, wire, bytes: buf.subarray(off, end) });
|
|
23
|
+
off = end;
|
|
24
|
+
}
|
|
25
|
+
else if (wire === 5) {
|
|
26
|
+
out.push({ field, wire, value: BigInt(buf.readUInt32LE(off)) });
|
|
27
|
+
off += 4;
|
|
28
|
+
}
|
|
29
|
+
else if (wire === 1) {
|
|
30
|
+
out.push({ field, wire, value: buf.readBigUInt64LE(off) });
|
|
31
|
+
off += 8;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error(`field ${field}: unsupported wire type ${wire}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function readVarint(buf, off) {
|
|
40
|
+
let value = 0n;
|
|
41
|
+
let shift = 0n;
|
|
42
|
+
let i = off;
|
|
43
|
+
while (i < buf.length) {
|
|
44
|
+
const byte = buf[i++];
|
|
45
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
46
|
+
if ((byte & 0x80) === 0)
|
|
47
|
+
return [value, i];
|
|
48
|
+
shift += 7n;
|
|
49
|
+
if (shift > 63n)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`varint overflow at ${off}`);
|
|
53
|
+
}
|
|
54
|
+
const IMAGE_EXT = {
|
|
55
|
+
jpg: true, jpeg: true, png: true, gif: true, webp: true, bmp: true, heic: true, avif: true,
|
|
56
|
+
};
|
|
57
|
+
const VIDEO_EXT = {
|
|
58
|
+
mp4: true, mov: true, mkv: true, avi: true, webm: true, m4v: true, wmv: true,
|
|
59
|
+
};
|
|
60
|
+
/** MsgContent fields this module models; everything else is reported as unmodelled. */
|
|
61
|
+
const MODELLED_FIELDS = {
|
|
62
|
+
5: true, 40010: true, 40020: true, 40021: true, 45001: true, 45002: true, 45003: true, 45004: true,
|
|
63
|
+
45101: true, 45102: true, 45103: true, 45104: true, 45105: true, 45106: true, 45108: true, 45109: true,
|
|
64
|
+
45110: true, 45111: true, 45112: true, 45402: true, 45403: true, 45404: true, 45405: true, 45406: true,
|
|
65
|
+
45407: true, 45408: true, 45409: true, 45410: true, 45411: true, 45412: true, 45413: true, 45414: true,
|
|
66
|
+
45415: true, 45416: true, 45418: true, 45419: true, 45421: true, 45422: true, 45423: true, 45424: true,
|
|
67
|
+
45501: true, 45503: true, 45504: true, 45505: true, 45507: true, 45509: true, 45510: true, 45511: true,
|
|
68
|
+
45512: true, 45513: true, 45514: true, 45515: true, 45516: true, 45517: true, 45518: true, 45519: true,
|
|
69
|
+
45526: true, 45550: true, 45600: true, 45801: true, 45802: true, 45803: true, 45804: true, 45805: true,
|
|
70
|
+
45806: true, 45807: true, 45812: true, 45814: true, 45815: true, 45816: true, 45817: true, 45818: true,
|
|
71
|
+
45819: true, 45820: true, 45821: true, 45822: true, 45823: true, 45824: true, 45825: true, 45826: true,
|
|
72
|
+
45827: true, 45828: true, 45829: true, 45851: true, 45852: true, 45853: true, 45854: true, 45855: true,
|
|
73
|
+
45856: true, 45857: true, 45858: true, 45859: true, 45860: true, 45861: true, 45862: true, 45863: true,
|
|
74
|
+
45906: true, 45907: true, 45909: true, 45911: true, 45922: true, 45923: true, 45924: true, 45925: true,
|
|
75
|
+
45926: true, 45954: true, 45974: true, 47401: true, 47402: true, 47403: true, 47404: true, 47413: true,
|
|
76
|
+
47421: true, 47422: true, 47423: true, 47501: true, 47502: true, 47601: true, 47602: true, 47603: true,
|
|
77
|
+
47604: true, 47605: true, 47606: true, 47607: true, 47608: true, 47609: true, 47610: true, 47611: true,
|
|
78
|
+
47612: true, 47613: true, 47614: true, 47615: true, 47616: true, 47617: true, 47618: true, 47619: true,
|
|
79
|
+
47620: true, 47622: true, 47702: true, 47703: true, 47704: true, 47705: true, 47706: true, 47710: true,
|
|
80
|
+
47711: true, 47713: true, 47714: true, 47715: true, 47901: true, 47902: true, 47904: true, 48151: true,
|
|
81
|
+
48152: true, 48153: true, 48154: true, 48155: true, 48156: true, 48157: true, 48210: true, 48211: true,
|
|
82
|
+
48212: true, 48213: true, 48214: true, 48215: true, 48216: true, 48217: true, 48218: true, 48271: true,
|
|
83
|
+
48272: true, 48273: true, 48275: true, 48401: true, 48402: true, 48403: true, 48404: true, 48405: true,
|
|
84
|
+
48406: true, 48407: true, 48408: true, 48409: true, 48410: true, 48411: true, 48412: true, 48417: true,
|
|
85
|
+
48418: true, 48419: true, 48421: true, 48601: true, 48602: true, 48603: true, 49154: true, 49155: true,
|
|
86
|
+
80810: true, 80824: true, 80900: true, 80901: true, 80902: true, 80903: true, 80905: true, 80908: true,
|
|
87
|
+
80909: true, 80910: true, 80935: true, 80941: true, 80942: true, 80970: true, 80975: true, 80980: true,
|
|
88
|
+
80981: true, 80983: true, 80995: true, 95654: true, 48542: true,
|
|
89
|
+
};
|
|
90
|
+
const STICKER_SUMMARY = "[动画表情]";
|
|
91
|
+
/** Decodes the `[40800]` blob of a message row into typed segments. */
|
|
92
|
+
export function decodeMessageContent(blob, maxFields = 512) {
|
|
93
|
+
const buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
|
|
94
|
+
const fields = decodeWireFields(buf, maxFields);
|
|
95
|
+
const segments = [];
|
|
96
|
+
let innerTimestamp;
|
|
97
|
+
let protoVersion;
|
|
98
|
+
for (const field of fields) {
|
|
99
|
+
if (field.field === MSG_BODY_CONTENT_FIELD && field.bytes) {
|
|
100
|
+
segments.push(decodeSegment(field.bytes));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (field.field === 49155 && field.value !== undefined)
|
|
104
|
+
innerTimestamp = Number(field.value);
|
|
105
|
+
if (field.field === 49154 && field.bytes)
|
|
106
|
+
protoVersion = asText(field.bytes);
|
|
107
|
+
}
|
|
108
|
+
const text = segments
|
|
109
|
+
.map((s) => s.text ?? "")
|
|
110
|
+
.filter((t) => t.length > 0)
|
|
111
|
+
.join("\n");
|
|
112
|
+
const kinds = new Set(segments.map((s) => s.type));
|
|
113
|
+
const kind = kinds.size === 1 ? segments[0]?.type ?? "unknown" : kinds.size > 1 ? "mixed" : "unknown";
|
|
114
|
+
return { segments, text, kind, innerTimestamp, protoVersion };
|
|
115
|
+
}
|
|
116
|
+
function decodeSegment(buf) {
|
|
117
|
+
const fields = decodeWireFields(buf);
|
|
118
|
+
const byId = new Map();
|
|
119
|
+
for (const f of fields)
|
|
120
|
+
if (!byId.has(f.field))
|
|
121
|
+
byId.set(f.field, f);
|
|
122
|
+
const all = (id) => fields.filter((f) => f.field === id);
|
|
123
|
+
const textFallbacks = all(45815)
|
|
124
|
+
.map((f) => asText(f.bytes ?? Buffer.alloc(0)))
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
const num = (id) => {
|
|
127
|
+
const v = byId.get(id)?.value;
|
|
128
|
+
return v === undefined ? undefined : Number(v);
|
|
129
|
+
};
|
|
130
|
+
const str = (id) => {
|
|
131
|
+
const b = byId.get(id)?.bytes;
|
|
132
|
+
return b ? asText(b) : undefined;
|
|
133
|
+
};
|
|
134
|
+
const raw = (id) => byId.get(id)?.bytes;
|
|
135
|
+
const filename = str(45402);
|
|
136
|
+
const fileExt = (str(45419)?.replace(/^\./, "") ?? filename?.split(".").pop() ?? "").toLowerCase() || undefined;
|
|
137
|
+
const md5Raw = raw(45406);
|
|
138
|
+
const urls = [45802, 45803, 45804].map(str).filter((u) => Boolean(u));
|
|
139
|
+
const summary = textFallbacks[0] ?? str(45422) ?? str(45812) ?? str(80900);
|
|
140
|
+
const unmodelledFields = unmappedFields(byId);
|
|
141
|
+
const msgIdValue = byId.get(45001)?.value;
|
|
142
|
+
const segment = {
|
|
143
|
+
type: "unknown",
|
|
144
|
+
msgId: msgIdValue === undefined ? undefined : String(msgIdValue),
|
|
145
|
+
contentType: num(45002),
|
|
146
|
+
filename,
|
|
147
|
+
fileExt,
|
|
148
|
+
fileSize: num(45405),
|
|
149
|
+
fileUuid: str(45503),
|
|
150
|
+
md5: md5Raw && md5Raw.length >= 16 ? md5Raw.subarray(0, 16).toString("hex") : undefined,
|
|
151
|
+
width: num(45411),
|
|
152
|
+
height: num(45412),
|
|
153
|
+
urls,
|
|
154
|
+
cdnHost: str(45816),
|
|
155
|
+
unmodelledFields,
|
|
156
|
+
};
|
|
157
|
+
const callType = num(48151);
|
|
158
|
+
if (callType !== undefined || str(48153) !== undefined) {
|
|
159
|
+
return { ...segment, type: "call", callType, callDuration: num(48152), callDesc: str(48153), text: str(48153) ?? summary };
|
|
160
|
+
}
|
|
161
|
+
const systemContent = str(80900);
|
|
162
|
+
if (systemContent !== undefined) {
|
|
163
|
+
return { ...segment, type: "system", systemContent, text: systemContent };
|
|
164
|
+
}
|
|
165
|
+
const contactNickname = str(47705) ?? str(47714);
|
|
166
|
+
if (contactNickname !== undefined || str(47703) !== undefined) {
|
|
167
|
+
const contact = {
|
|
168
|
+
relation: num(47702),
|
|
169
|
+
uid: str(47703) ?? str(47704),
|
|
170
|
+
nickname: contactNickname,
|
|
171
|
+
remark: str(47706) ?? str(47715),
|
|
172
|
+
};
|
|
173
|
+
return { ...segment, type: "contact", contact, text: summary ?? contact.nickname ?? contact.remark ?? "[名片]" };
|
|
174
|
+
}
|
|
175
|
+
const cardAppId = str(48409);
|
|
176
|
+
if (cardAppId !== undefined || raw(48403) !== undefined) {
|
|
177
|
+
const card = { appId: cardAppId, token: str(48410) ?? str(48418), template: str(48402) };
|
|
178
|
+
return { ...segment, type: "ark", card, text: summary ?? "[卡片消息]" };
|
|
179
|
+
}
|
|
180
|
+
const tipXml = str(48214) ?? str(48271);
|
|
181
|
+
if (tipXml !== undefined) {
|
|
182
|
+
const pairs = fieldPairs(all(48217));
|
|
183
|
+
const parties = fieldPairs(all(48210)).map(([uid, nickname]) => ({ uid, nickname: nickname || undefined }));
|
|
184
|
+
const nudge = {
|
|
185
|
+
action: pairs.find(([key]) => key === "action_str")?.[1],
|
|
186
|
+
suffix: pairs.find(([key]) => key === "suffix_str")?.[1],
|
|
187
|
+
image: pairs.find(([key]) => key === "action_img_url")?.[1],
|
|
188
|
+
parties: parties.length ? parties : undefined,
|
|
189
|
+
};
|
|
190
|
+
return { ...segment, type: "nudge", nudge, text: summary ?? tipText(tipXml) ?? "[互动消息]" };
|
|
191
|
+
}
|
|
192
|
+
const isImageExt = fileExt !== undefined && IMAGE_EXT[fileExt] === true;
|
|
193
|
+
const isVideoExt = fileExt !== undefined && VIDEO_EXT[fileExt] === true;
|
|
194
|
+
if (num(47601)) {
|
|
195
|
+
return { ...segment, type: "video", text: summary ?? (filename ? `[视频] ${filename}` : "[视频]") };
|
|
196
|
+
}
|
|
197
|
+
if (raw(45600) || summary === STICKER_SUMMARY) {
|
|
198
|
+
return { ...segment, type: "sticker", text: summary ?? "[表情]" };
|
|
199
|
+
}
|
|
200
|
+
if (isVideoExt) {
|
|
201
|
+
return { ...segment, type: "video", text: summary ?? `[视频] ${filename}` };
|
|
202
|
+
}
|
|
203
|
+
if (isImageExt || (fileExt === undefined && filename !== undefined && (num(45411) ?? 0) > 0 && (num(45412) ?? 0) > 0)) {
|
|
204
|
+
return { ...segment, type: "image", text: summary ?? (filename ? `[图片] ${filename}` : "[图片]") };
|
|
205
|
+
}
|
|
206
|
+
const legacyXml = str(48602);
|
|
207
|
+
if (legacyXml !== undefined) {
|
|
208
|
+
return { ...segment, type: "legacy_forward", text: str(47901) ?? legacyXml.slice(0, 200) };
|
|
209
|
+
}
|
|
210
|
+
if (str(47901) !== undefined || str(47904) !== undefined) {
|
|
211
|
+
return { ...segment, type: "forward", text: str(47901) ?? summary ?? "[合并转发]" };
|
|
212
|
+
}
|
|
213
|
+
const replyMsgIdValue = byId.get(47401)?.value;
|
|
214
|
+
if (replyMsgIdValue !== undefined) {
|
|
215
|
+
const replySummary = str(47413);
|
|
216
|
+
return {
|
|
217
|
+
...segment,
|
|
218
|
+
type: "reply",
|
|
219
|
+
replyMsgId: String(replyMsgIdValue),
|
|
220
|
+
replySummary,
|
|
221
|
+
text: summary ?? replySummary ?? "[引用]",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (filename !== undefined || num(45405) !== undefined) {
|
|
225
|
+
return { ...segment, type: "file", text: summary ?? (filename ? `[文件] ${filename}` : "[文件]") };
|
|
226
|
+
}
|
|
227
|
+
const text = str(45101);
|
|
228
|
+
if (text !== undefined)
|
|
229
|
+
return { ...segment, type: "text", text };
|
|
230
|
+
return { ...segment, type: "unknown", text: summary };
|
|
231
|
+
}
|
|
232
|
+
/** Reads repeated sub-messages that encode `{key, value}` string pairs. */
|
|
233
|
+
function fieldPairs(fields) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const field of fields) {
|
|
236
|
+
if (!field.bytes)
|
|
237
|
+
continue;
|
|
238
|
+
const parts = decodeWireFields(field.bytes)
|
|
239
|
+
.filter((inner) => inner.bytes)
|
|
240
|
+
.map((inner) => asText(inner.bytes));
|
|
241
|
+
if (parts.length >= 2)
|
|
242
|
+
out.push([parts[0], parts[1]]);
|
|
243
|
+
else if (parts.length === 1)
|
|
244
|
+
out.push([parts[0], ""]);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
/** Renders a nudge tip payload: JSON items when parseable, else its XML text. */
|
|
249
|
+
function tipText(raw) {
|
|
250
|
+
if (raw.startsWith("{")) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(raw);
|
|
253
|
+
const parts = (parsed.items ?? []).map((item) => item.txt ?? item.nm ?? "").filter(Boolean);
|
|
254
|
+
if (parts.length)
|
|
255
|
+
return parts.join("");
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// Fall through to the XML path.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const parts = [...raw.matchAll(/(?:txt|nm)="([^"]*)"/g)].map((match) => match[1]).filter(Boolean);
|
|
262
|
+
return parts.length ? parts.join("") : undefined;
|
|
263
|
+
}
|
|
264
|
+
function unmappedFields(byId) {
|
|
265
|
+
const ids = [];
|
|
266
|
+
for (const id of byId.keys())
|
|
267
|
+
if (MODELLED_FIELDS[id] !== true)
|
|
268
|
+
ids.push(id);
|
|
269
|
+
return ids.length ? ids.sort((a, b) => a - b) : undefined;
|
|
270
|
+
}
|
|
271
|
+
function asText(buf) {
|
|
272
|
+
return buf.toString("utf8").replace(/\0+$/, "");
|
|
273
|
+
}
|
|
274
|
+
export function toMessageRecord(row, chatType) {
|
|
275
|
+
const decoded = row.body ? decodeMessageContent(row.body) : { segments: [], text: "", kind: "unknown" };
|
|
276
|
+
const timestamp = Number(row.timestamp ?? 0);
|
|
277
|
+
// Group rows sometimes carry 0 in the peer column (system messages); the
|
|
278
|
+
// conversation column holds the group number in every row.
|
|
279
|
+
const groupQq = asNumber(row.conversationId) ?? asNumber(row.peerQq);
|
|
280
|
+
return {
|
|
281
|
+
msgId: String(row.msgId ?? ""),
|
|
282
|
+
timestamp,
|
|
283
|
+
iso: timestamp ? new Date(timestamp * 1000).toISOString() : "",
|
|
284
|
+
direction: Number(row.direction ?? 0),
|
|
285
|
+
chatType,
|
|
286
|
+
senderQq: asNumber(row.senderQq),
|
|
287
|
+
senderUid: asOptionalString(row.senderUid),
|
|
288
|
+
senderName: asOptionalString(row.senderName),
|
|
289
|
+
peerQq: asNumber(row.peerQq),
|
|
290
|
+
groupQq: chatType === "group" ? groupQq : undefined,
|
|
291
|
+
msgType: asNumber(row.msgType),
|
|
292
|
+
kind: decoded.kind,
|
|
293
|
+
text: decoded.text,
|
|
294
|
+
segments: decoded.segments,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function asNumber(value) {
|
|
298
|
+
if (value === null || value === undefined)
|
|
299
|
+
return undefined;
|
|
300
|
+
const n = Number(value);
|
|
301
|
+
return Number.isFinite(n) ? n : undefined;
|
|
302
|
+
}
|
|
303
|
+
function asOptionalString(value) {
|
|
304
|
+
if (value === null || value === undefined)
|
|
305
|
+
return undefined;
|
|
306
|
+
const s = String(value);
|
|
307
|
+
return s.length ? s : undefined;
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACtG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,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"}
|
package/dist/offline.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline access to NTQQ (Windows QQ NT) databases.
|
|
3
|
+
*
|
|
4
|
+
* Finds the local `nt_qq/nt_db` directories, resolves the device passphrase,
|
|
5
|
+
* decrypts a database on demand into a cached plaintext SQLite file, and turns
|
|
6
|
+
* it into chat/contact queries. This replaces the earlier in-memory scrape:
|
|
7
|
+
* the on-disk databases yield exact, schema-accurate records.
|
|
8
|
+
*/
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { parseNtqqHeader, pageCount, readSalt, reconstructDatabase, verifyPassphrase } from "./sqlcipher.js";
|
|
15
|
+
import { toMessageRecord } from "./messages.js";
|
|
16
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
/**
|
|
18
|
+
* Per-user directory holding the device key file and the plaintext cache.
|
|
19
|
+
* Kept out of the package directory so an installed copy does not write
|
|
20
|
+
* gigabytes (or a device key) into `node_modules`.
|
|
21
|
+
*/
|
|
22
|
+
export const QQ_MCP_HOME = process.env.QQ_MCP_HOME ?? join(homedir(), ".qq-mcp");
|
|
23
|
+
/** Where decrypted plaintext databases are cached. */
|
|
24
|
+
export const DEFAULT_CACHE_DIR = process.env.QQ_MCP_CACHE_DIR ?? join(QQ_MCP_HOME, "plain");
|
|
25
|
+
/** Device key file written by `capture_offline_key` when asked to persist. */
|
|
26
|
+
export const KEY_FILE = join(QQ_MCP_HOME, "qqkey");
|
|
27
|
+
/** Bundled memory scanner, shipped next to the compiled server. */
|
|
28
|
+
export const SCANNER_SCRIPT = join(ROOT, "scripts", "scan_memory.py");
|
|
29
|
+
/** Candidate `nt_db` directories for the current machine. */
|
|
30
|
+
export function findQQDataDirs() {
|
|
31
|
+
const dirs = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const push = (dir) => {
|
|
34
|
+
if (!dir || seen.has(dir) || !existsSync(dir))
|
|
35
|
+
return;
|
|
36
|
+
seen.add(dir);
|
|
37
|
+
dirs.push(dir);
|
|
38
|
+
};
|
|
39
|
+
const override = process.env.QQ_MCP_DATA_DIR;
|
|
40
|
+
if (override)
|
|
41
|
+
for (const part of override.split(";"))
|
|
42
|
+
push(part.trim());
|
|
43
|
+
const docs = join(process.env.USERPROFILE ?? "", "Documents", "Tencent Files");
|
|
44
|
+
if (existsSync(docs)) {
|
|
45
|
+
for (const entry of safeReaddir(docs)) {
|
|
46
|
+
push(join(docs, entry, "nt_qq", "nt_db"));
|
|
47
|
+
}
|
|
48
|
+
push(join(docs, "nt_qq", "global", "nt_db"));
|
|
49
|
+
}
|
|
50
|
+
return dirs;
|
|
51
|
+
}
|
|
52
|
+
function safeReaddir(dir) {
|
|
53
|
+
try {
|
|
54
|
+
return readdirSync(dir);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Lists NTQQ databases found in the given directories (defaults to {@link findQQDataDirs}). */
|
|
61
|
+
export function listDataSources(dirs = findQQDataDirs()) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const dir of dirs) {
|
|
64
|
+
for (const entry of safeReaddir(dir)) {
|
|
65
|
+
if (!entry.endsWith(".db"))
|
|
66
|
+
continue;
|
|
67
|
+
const dbPath = join(dir, entry);
|
|
68
|
+
try {
|
|
69
|
+
const stats = statSync(dbPath);
|
|
70
|
+
const header = parseNtqqHeader(dbPath);
|
|
71
|
+
const materialPath = `${dbPath}-first.material`;
|
|
72
|
+
out.push({
|
|
73
|
+
name: entry,
|
|
74
|
+
dbPath,
|
|
75
|
+
bytes: stats.size,
|
|
76
|
+
pages: pageCount(dbPath),
|
|
77
|
+
saltHex: readSalt(dbPath).toString("hex"),
|
|
78
|
+
formatVersion: header.formatVersion,
|
|
79
|
+
materialPath: existsSync(materialPath) ? materialPath : undefined,
|
|
80
|
+
mtimeMs: stats.mtimeMs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Not an NTQQ database (stub files, unrelated sqlite) — skip.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** Passphrase from an explicit argument, `QQ_MCP_PASSPHRASE`, or the `qqkey` file in {@link QQ_MCP_HOME}. */
|
|
91
|
+
export function resolvePassphrase(explicit) {
|
|
92
|
+
if (explicit)
|
|
93
|
+
return explicit;
|
|
94
|
+
const env = process.env.QQ_MCP_PASSPHRASE;
|
|
95
|
+
if (env)
|
|
96
|
+
return env;
|
|
97
|
+
try {
|
|
98
|
+
const file = readFileSync(KEY_FILE, "utf8").trim();
|
|
99
|
+
if (file.length)
|
|
100
|
+
return file;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// No cached key file.
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
export function requirePassphrase(explicit) {
|
|
108
|
+
const passphrase = resolvePassphrase(explicit);
|
|
109
|
+
if (!passphrase) {
|
|
110
|
+
throw new Error(`no device passphrase: pass \`passphrase\`, set QQ_MCP_PASSPHRASE, write ${KEY_FILE}, ` +
|
|
111
|
+
"or run capture_offline_key while QQ is logged in");
|
|
112
|
+
}
|
|
113
|
+
return passphrase;
|
|
114
|
+
}
|
|
115
|
+
/** Cache path for the decrypted form of `dbPath`; keyed by size+mtime so QQ writes invalidate it. */
|
|
116
|
+
export function plaintextCachePath(dbPath, cacheDir = DEFAULT_CACHE_DIR) {
|
|
117
|
+
const stats = statSync(dbPath);
|
|
118
|
+
const name = dbPath.split(/[\\/]/).pop() ?? "db";
|
|
119
|
+
return join(cacheDir, `${name}.${stats.size}.${Math.floor(stats.mtimeMs)}.plain.db`);
|
|
120
|
+
}
|
|
121
|
+
/** Decrypts `dbPath` into the cache (or reuses a fresh cache entry). */
|
|
122
|
+
export function ensurePlaintext(dbPath, passphrase, opts = {}) {
|
|
123
|
+
if (!verifyPassphrase(dbPath, passphrase)) {
|
|
124
|
+
throw new Error(`passphrase does not decrypt ${dbPath} (wrong key or not an NTQQ database)`);
|
|
125
|
+
}
|
|
126
|
+
const cacheDir = opts.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
127
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
128
|
+
const target = plaintextCachePath(dbPath, cacheDir);
|
|
129
|
+
const started = Date.now();
|
|
130
|
+
if (!opts.force && existsSync(target)) {
|
|
131
|
+
return { path: target, reused: true, durationMs: 0 };
|
|
132
|
+
}
|
|
133
|
+
const result = reconstructDatabase({ dbPath, passphrase, outPath: target, onProgress: opts.onProgress });
|
|
134
|
+
pruneCache(cacheDir, dbPath);
|
|
135
|
+
return { path: result.outPath, reused: false, durationMs: Date.now() - started };
|
|
136
|
+
}
|
|
137
|
+
/** Keeps at most two plaintext copies per source database, newest first. */
|
|
138
|
+
function pruneCache(cacheDir, dbPath) {
|
|
139
|
+
const name = dbPath.split(/[\\/]/).pop() ?? "db";
|
|
140
|
+
try {
|
|
141
|
+
const entries = safeReaddir(cacheDir)
|
|
142
|
+
.filter((e) => e.startsWith(`${name}.`) && e.endsWith(".plain.db"))
|
|
143
|
+
.map((e) => ({ e, mtimeMs: statSync(join(cacheDir, e)).mtimeMs }))
|
|
144
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
145
|
+
for (const stale of entries.slice(2))
|
|
146
|
+
rmSync(join(cacheDir, stale.e), { force: true });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Cache pruning is best-effort.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Opens a decrypted NTQQ database read-only. */
|
|
153
|
+
export function openOffline(dbPath, opts = {}) {
|
|
154
|
+
const plaintextPath = opts.plaintextPath ?? ensurePlaintext(dbPath, requirePassphrase(opts.passphrase), opts).path;
|
|
155
|
+
return { db: new DatabaseSync(plaintextPath, { readOnly: true, readBigInts: true }), plaintextPath, sourcePath: dbPath };
|
|
156
|
+
}
|
|
157
|
+
const MSG_SELECT = `[40001] AS msgId, [40011] AS msgType, [40013] AS direction, [40020] AS senderUid,
|
|
158
|
+
[40021] AS peerUid, [40033] AS senderQq, [40030] AS peerQq, [40027] AS conversationId,
|
|
159
|
+
[40050] AS timestamp, [40093] AS senderName, [40800] AS body`;
|
|
160
|
+
/**
|
|
161
|
+
* Queries chat messages. Text lives inside protobuf, so keyword filtering
|
|
162
|
+
* happens after decoding; rows are paged by primary key (keyset) rather than by
|
|
163
|
+
* an index, because some QQ databases ship damaged index pages while the table
|
|
164
|
+
* b-trees stay readable. `maxScan` bounds how deep a keyword search may go.
|
|
165
|
+
*/
|
|
166
|
+
export function queryMessages(offline, query = {}) {
|
|
167
|
+
const { db } = offline;
|
|
168
|
+
const limit = Math.min(Math.max(query.limit ?? 100, 1), 5000);
|
|
169
|
+
const order = query.order === "asc" ? "ASC" : "DESC";
|
|
170
|
+
const comparator = order === "ASC" ? "<" : ">";
|
|
171
|
+
const maxScan = query.maxScan ?? 200_000;
|
|
172
|
+
const pageSize = 1000;
|
|
173
|
+
const chats = query.chat === "all" || query.chat === undefined ? ["c2c", "group"] : [query.chat];
|
|
174
|
+
const messages = [];
|
|
175
|
+
let scanned = 0;
|
|
176
|
+
for (const chatType of chats) {
|
|
177
|
+
if (messages.length >= limit || scanned >= maxScan)
|
|
178
|
+
break;
|
|
179
|
+
const table = chatType === "c2c" ? "c2c_msg_table" : "group_msg_table";
|
|
180
|
+
const filters = [];
|
|
181
|
+
if (chatType === "c2c" && query.peerQq !== undefined) {
|
|
182
|
+
filters.push(`([40030] = ${Number(query.peerQq)} OR [40033] = ${Number(query.peerQq)})`);
|
|
183
|
+
}
|
|
184
|
+
if (chatType === "group" && query.groupQq !== undefined) {
|
|
185
|
+
filters.push(`[40027] = ${Number(query.groupQq)}`);
|
|
186
|
+
}
|
|
187
|
+
if (query.since !== undefined)
|
|
188
|
+
filters.push(`[40050] >= ${Number(query.since)}`);
|
|
189
|
+
if (query.until !== undefined)
|
|
190
|
+
filters.push(`[40050] <= ${Number(query.until)}`);
|
|
191
|
+
let cursor;
|
|
192
|
+
for (;;) {
|
|
193
|
+
const where = [...filters];
|
|
194
|
+
if (cursor)
|
|
195
|
+
where.push(`[40001] ${comparator} '${cursor}'`);
|
|
196
|
+
const sql = `SELECT ${MSG_SELECT} FROM ${table} ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY [40001] ${order} LIMIT ${pageSize}`;
|
|
197
|
+
let rows;
|
|
198
|
+
try {
|
|
199
|
+
rows = db.prepare(sql).all();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new Error(`query failed on ${table}: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
if (rows.length === 0)
|
|
205
|
+
break;
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
scanned++;
|
|
208
|
+
cursor = String(row.msgId);
|
|
209
|
+
const record = toMessageRecord(row, chatType);
|
|
210
|
+
if (query.keyword && !record.text.includes(query.keyword))
|
|
211
|
+
continue;
|
|
212
|
+
if (messages.length >= limit) {
|
|
213
|
+
return finish(messages, scanned, order);
|
|
214
|
+
}
|
|
215
|
+
messages.push(record);
|
|
216
|
+
}
|
|
217
|
+
if (rows.length < pageSize || scanned >= maxScan)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const truncated = scanned >= maxScan && messages.length < limit;
|
|
222
|
+
return finish(messages, scanned, order, truncated);
|
|
223
|
+
}
|
|
224
|
+
function finish(messages, scanned, order, truncated = false) {
|
|
225
|
+
messages.sort((a, b) => (order === "DESC" ? b.timestamp - a.timestamp : a.timestamp - b.timestamp));
|
|
226
|
+
return { messages, scanned, truncated };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Contact book: friends from `recent_contact_v3_table`, groups from
|
|
230
|
+
* `group_info.db.group_list` when available, plus the QQ↔NT-UID mapping table.
|
|
231
|
+
*/
|
|
232
|
+
export function listContacts(offline, opts = {}) {
|
|
233
|
+
const limit = Math.min(Math.max(opts.limit ?? 500, 1), 5000);
|
|
234
|
+
const friends = readFriends(offline.db, limit);
|
|
235
|
+
const uidMappings = readUidMappings(offline.db, limit);
|
|
236
|
+
const groups = opts.groupInfoPath ? readGroups(opts.groupInfoPath, opts.passphrase, limit) : [];
|
|
237
|
+
return { friends, groups, uidMappings };
|
|
238
|
+
}
|
|
239
|
+
function readFriends(db, limit) {
|
|
240
|
+
const rows = db
|
|
241
|
+
.prepare(`SELECT [40020] AS uid, [40033] AS qq, [40090] AS name1, [40093] AS name2, [40050] AS lastActive
|
|
242
|
+
FROM recent_contact_v3_table
|
|
243
|
+
WHERE cast([40020] AS TEXT) GLOB 'u_*'
|
|
244
|
+
ORDER BY [40050] DESC LIMIT ${limit}`)
|
|
245
|
+
.all();
|
|
246
|
+
return rows.map((row) => ({
|
|
247
|
+
kind: "friend",
|
|
248
|
+
qq: Number(row.qq) || undefined,
|
|
249
|
+
uid: String(row.uid ?? "") || undefined,
|
|
250
|
+
name: String(row.name1 ?? "") || String(row.name2 ?? "") || undefined,
|
|
251
|
+
lastActive: Number(row.lastActive) || undefined,
|
|
252
|
+
source: "recent_contact",
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
function readUidMappings(db, limit) {
|
|
256
|
+
const rows = db
|
|
257
|
+
.prepare(`SELECT [1002] AS qq, [48902] AS uid FROM nt_uid_mapping_table LIMIT ${limit}`)
|
|
258
|
+
.all();
|
|
259
|
+
return rows
|
|
260
|
+
.map((row) => ({ qq: Number(row.qq), uid: String(row.uid ?? "") }))
|
|
261
|
+
.filter((r) => Number.isFinite(r.qq) && r.uid.length > 0);
|
|
262
|
+
}
|
|
263
|
+
function readGroups(groupInfoPath, passphrase, limit) {
|
|
264
|
+
const { db } = openOffline(groupInfoPath, { passphrase });
|
|
265
|
+
const rows = db
|
|
266
|
+
.prepare(`SELECT [60001] AS qq, [60007] AS name, [60006] AS members, [60004] AS owner, [60008] AS notice
|
|
267
|
+
FROM group_list WHERE cast([60001] AS TEXT) GLOB '[0-9]*' ORDER BY [60006] DESC LIMIT ${limit}`)
|
|
268
|
+
.all();
|
|
269
|
+
return rows.map((row) => ({
|
|
270
|
+
kind: "group",
|
|
271
|
+
qq: Number(row.qq) || undefined,
|
|
272
|
+
name: String(row.name ?? "") || undefined,
|
|
273
|
+
remark: String(row.notice ?? "") || undefined,
|
|
274
|
+
source: "group_info",
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
/** Group member roster from `group_info.db.group_member3`. */
|
|
278
|
+
export function listGroupMembers(groupInfoPath, opts = {}) {
|
|
279
|
+
const limit = Math.min(Math.max(opts.limit ?? 200, 1), 5000);
|
|
280
|
+
const { db } = openOffline(groupInfoPath, { passphrase: opts.passphrase });
|
|
281
|
+
const where = opts.groupQq !== undefined ? `WHERE [60001] = ${Number(opts.groupQq)}` : "";
|
|
282
|
+
const rows = db
|
|
283
|
+
.prepare(`SELECT [60001] AS groupQq, [1002] AS qq, [1000] AS uid, [20002] AS nickname, [64003] AS card,
|
|
284
|
+
[64008] AS lastSpeakTs FROM group_member3 ${where} ORDER BY [64008] DESC LIMIT ${limit}`)
|
|
285
|
+
.all();
|
|
286
|
+
return rows.map((row) => ({
|
|
287
|
+
groupQq: Number(row.groupQq),
|
|
288
|
+
qq: Number(row.qq) || undefined,
|
|
289
|
+
uid: String(row.uid ?? "") || undefined,
|
|
290
|
+
nickname: String(row.nickname ?? "") || undefined,
|
|
291
|
+
card: String(row.card ?? "") || undefined,
|
|
292
|
+
lastSpeakTs: Number(row.lastSpeakTs) || undefined,
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
/** Row counts and time span of a decrypted `nt_msg.db`. */
|
|
296
|
+
export function chatStats(offline) {
|
|
297
|
+
const { db } = offline;
|
|
298
|
+
const one = (sql) => {
|
|
299
|
+
try {
|
|
300
|
+
const row = db.prepare(sql).get();
|
|
301
|
+
return Number(row?.v ?? 0);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
c2cMessages: one("SELECT count(*) AS v FROM c2c_msg_table"),
|
|
309
|
+
groupMessages: one("SELECT count(*) AS v FROM group_msg_table"),
|
|
310
|
+
contacts: one("SELECT count(*) AS v FROM recent_contact_v3_table"),
|
|
311
|
+
groups: one("SELECT count(DISTINCT [40027]) AS v FROM group_msg_table"),
|
|
312
|
+
firstTimestamp: one("SELECT min([40050]) AS v FROM c2c_msg_table") || undefined,
|
|
313
|
+
lastTimestamp: one("SELECT max([40050]) AS v FROM c2c_msg_table") || undefined,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=offline.js.map
|