@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 powercess
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# qq-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that reads **QQ (Windows NTQQ)** local chat records, contacts and
|
|
4
|
+
database schema **offline** — the outcome of a reverse-engineering session
|
|
5
|
+
against NTQQ **9.9.35.52892** (full findings: [docs/reverse-engineering.md](docs/reverse-engineering.md)).
|
|
6
|
+
|
|
7
|
+
TypeScript + Node.js (`@modelcontextprotocol/sdk`), stdio transport, no native
|
|
8
|
+
dependencies: the SQLCipher page cipher is implemented in this repo.
|
|
9
|
+
|
|
10
|
+
## What works
|
|
11
|
+
|
|
12
|
+
| Tool | Mode | What it does |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `list_qq_databases` | offline | Enumerates `nt_qq/nt_db` databases (pages, salt, format version, `.material` presence) and running QQ PIDs |
|
|
15
|
+
| `capture_offline_key` | online¹ | Recovers the device passphrase from QQ's process memory, verified against a `.material` file or database page |
|
|
16
|
+
| `verify_offline_key` | offline | Checks a passphrase against one database, a `.material` file, or every local database at once |
|
|
17
|
+
| `decrypt_qq_database` | offline | Decrypts a database (WAL replayed) into a cached plaintext SQLite file under `~/.qq-mcp/plain`, with row counts |
|
|
18
|
+
| `query_chat_messages` | offline | Queries c2c/group messages by peer, group, time range and keyword; returns typed segments |
|
|
19
|
+
| `list_contacts` | offline | Friends (QQ, NT uid, name, last activity), groups (number, name, members) and the QQ↔uid map |
|
|
20
|
+
| `list_group_members` | offline | Group roster from `group_info.db`: member QQ, uid, nickname, card, last speak time |
|
|
21
|
+
| `get_db_schema` | offline | Decrypts a `.material` DDL blob and returns the full schema (87 tables for `nt_msg.db`) |
|
|
22
|
+
|
|
23
|
+
¹ online = QQ must be logged in for memory scraping. Everything else works with
|
|
24
|
+
QQ closed, straight off the files on disk. Windows only.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @powercess/qq-mcp # or: npx @powercess/qq-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
From a source checkout:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
npm run build
|
|
37
|
+
npm test # 23 tests, incl. real-database integration
|
|
38
|
+
node scripts/smoke-client.mjs # list tools
|
|
39
|
+
node scripts/smoke-client.mjs query_chat_messages '{ "chat": "group", "groupQq": 123456, "limit": 20 }'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Configure as an MCP server (installed globally):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"qq-mcp": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["-y", "@powercess/qq-mcp"],
|
|
50
|
+
"env": { "QQ_MCP_PASSPHRASE": "<16-char device passphrase>" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The device passphrase
|
|
57
|
+
|
|
58
|
+
NTQQ derives database keys in-process; the passphrase is not stored on disk.
|
|
59
|
+
It is a short ASCII string (16 or 20 bytes) inside the QQ process, and the same
|
|
60
|
+
passphrase unlocks **every** database on the device.
|
|
61
|
+
|
|
62
|
+
Acquire it once, either way:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. automatic: scan a logged-in QQ and verify candidates against known artefacts
|
|
66
|
+
node scripts/smoke-client.mjs capture_offline_key '{ "save": true }' # writes ~/.qq-mcp/qqkey (0600)
|
|
67
|
+
|
|
68
|
+
# 2. manual: debugger route documented in docs/reverse-engineering.md
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Provide it to the server via the `passphrase` argument, `QQ_MCP_PASSPHRASE`, or
|
|
72
|
+
`~/.qq-mcp/qqkey` (written by `capture_offline_key { "save": true }`). Other
|
|
73
|
+
knobs: `QQ_MCP_HOME` (runtime directory, default `~/.qq-mcp`), `QQ_MCP_CACHE_DIR`
|
|
74
|
+
(plaintext cache, default `$QQ_MCP_HOME/plain`), `QQ_MCP_DATA_DIR` (extra `nt_db`
|
|
75
|
+
directories, `;`-separated), `QQ_MCP_PYTHON` (Python for the memory scanner).
|
|
76
|
+
|
|
77
|
+
## How the decryption works
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
file = [1024-byte NTQQ header][page 1][page 2]...
|
|
81
|
+
page1 = [16-byte salt][ciphertext][16-byte IV][32-byte tag]
|
|
82
|
+
pageN = [ciphertext][16-byte IV][32-byte tag]
|
|
83
|
+
key = PBKDF2-HMAC-SHA512(passphrase, salt, 4000, 32)
|
|
84
|
+
data = AES-256-CBC, IV read from the page tail
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Verified byte-exact against SQLCipher 4.12 (same row counts, same
|
|
88
|
+
`quick_check` verdict) on a 2.4 GB `nt_msg.db`. Decryption runs as a streaming
|
|
89
|
+
transform: ~7 s for 2.4 GB, bounded memory. Committed `-wal` frames are replayed
|
|
90
|
+
after their checksum chain is validated; frames past a torn write are dropped.
|
|
91
|
+
|
|
92
|
+
See [docs/reverse-engineering.md](docs/reverse-engineering.md) for how each
|
|
93
|
+
constant was pinned down and what is still open.
|
|
94
|
+
|
|
95
|
+
## Limitations (honest)
|
|
96
|
+
|
|
97
|
+
- The 32-byte page tag is **not** authenticated: no standard HMAC layout matched,
|
|
98
|
+
and decryption does not need it. Forged pages would not be detected.
|
|
99
|
+
- Some QQ databases ship damaged **index** pages (`database disk image is
|
|
100
|
+
malformed` on indexed queries); table b-trees are intact. Queries page by
|
|
101
|
+
primary key, and keyword search decodes protobuf text in JS, so
|
|
102
|
+
`query_chat_messages` takes `maxScan` as a depth guard.
|
|
103
|
+
- Message content coverage is empirical: text, image, video, file, sticker,
|
|
104
|
+
contact card, interactive card (`ark`), nudge (戳一戳), reply, forward,
|
|
105
|
+
legacy forward, call and system messages are typed. Unmodelled protobuf field
|
|
106
|
+
ids are reported per segment instead of being dropped — see the open-questions
|
|
107
|
+
list in the docs.
|
|
108
|
+
- Windows-only; memory scanning requires Python 3 (stdlib only).
|
|
109
|
+
|
|
110
|
+
## Layout
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/sqlcipher.ts page cipher, key derivation, WAL replay, reconstruction
|
|
114
|
+
src/messages.ts protobuf decoding of message bodies into typed segments
|
|
115
|
+
src/offline.ts database discovery, key resolution, plaintext cache, queries
|
|
116
|
+
src/keyscan.ts passphrase capture (memory scrape + oracle verification)
|
|
117
|
+
src/schema.ts offline .material DDL decryption
|
|
118
|
+
src/index.ts MCP tool surface
|
|
119
|
+
scripts/ scan_memory.py (key candidates), smoke-client.mjs
|
|
120
|
+
test/ node:test suites + hermetic fixture builders
|
|
121
|
+
docs/ reverse-engineering findings
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Privacy
|
|
125
|
+
|
|
126
|
+
Decrypted databases and the captured key live under `~/.qq-mcp` (override with
|
|
127
|
+
`QQ_MCP_HOME`), never in the package directory; in a source checkout `exports/`
|
|
128
|
+
and `.qqkey` are gitignored. Captured chat data and keys are never committed —
|
|
129
|
+
do not publish them.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { decryptMaterial, extractSchema, DEFAULT_KDF_ITER } from "./schema.js";
|
|
5
|
+
import { chatStats, ensurePlaintext, findQQDataDirs, listContacts, listDataSources, listGroupMembers, openOffline, plaintextCachePath, queryMessages, requirePassphrase, } from "./offline.js";
|
|
6
|
+
import { captureOfflineKey, findQQPids, pickOracle, verifyPassphraseEverywhere } from "./keyscan.js";
|
|
7
|
+
import { verifyPassphrase } from "./sqlcipher.js";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
function ok(payload) {
|
|
10
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11
|
+
}
|
|
12
|
+
function fail(error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }] };
|
|
15
|
+
}
|
|
16
|
+
/** Finds a local database by name, preferring the most recently written copy. */
|
|
17
|
+
function resolveSource(name, explicit) {
|
|
18
|
+
if (explicit) {
|
|
19
|
+
const wanted = resolve(explicit);
|
|
20
|
+
const dir = wanted.replace(/[\\/][^\\/]*$/, "");
|
|
21
|
+
const match = listDataSources([dir]).find((s) => resolve(s.dbPath) === wanted);
|
|
22
|
+
if (match)
|
|
23
|
+
return match;
|
|
24
|
+
throw new Error(`${explicit}: no NTQQ database found at that path`);
|
|
25
|
+
}
|
|
26
|
+
const candidates = listDataSources().filter((s) => s.name === name || s.name.startsWith(name));
|
|
27
|
+
const usable = candidates.filter((s) => s.pages > 0).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
28
|
+
if (usable.length === 0) {
|
|
29
|
+
const dirs = findQQDataDirs();
|
|
30
|
+
throw new Error(`no local ${name} found (searched: ${dirs.join(", ") || "no NTQQ directories"})`);
|
|
31
|
+
}
|
|
32
|
+
return usable[0];
|
|
33
|
+
}
|
|
34
|
+
/** Sibling database in the same `nt_db` directory as `source`. */
|
|
35
|
+
function sibling(source, name) {
|
|
36
|
+
const dir = source.dbPath.replace(/[\\/][^\\/]*$/, "");
|
|
37
|
+
const found = listDataSources([dir]).find((s) => s.name === name && s.pages > 0);
|
|
38
|
+
return found?.dbPath;
|
|
39
|
+
}
|
|
40
|
+
const server = new McpServer({
|
|
41
|
+
name: "qq-mcp",
|
|
42
|
+
version: "0.2.0",
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Offline: list locally installed NTQQ databases plus running QQ PIDs.
|
|
46
|
+
* First step of every offline workflow — pick a database, then decrypt it.
|
|
47
|
+
*/
|
|
48
|
+
server.registerTool("list_qq_databases", {
|
|
49
|
+
description: "Lists NTQQ (Windows QQ NT) databases found under Documents/Tencent Files/*/nt_qq/nt_db, " +
|
|
50
|
+
"with page count, salt, format version and whether a .material key blob exists. Also " +
|
|
51
|
+
"reports running QQ PIDs. Works fully offline.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
dirs: z.array(z.string()).optional().describe("Extra nt_db directories to scan"),
|
|
54
|
+
},
|
|
55
|
+
}, async ({ dirs }) => {
|
|
56
|
+
try {
|
|
57
|
+
const searchDirs = [...(dirs ?? []), ...findQQDataDirs()];
|
|
58
|
+
const databases = listDataSources(searchDirs).map((s) => ({
|
|
59
|
+
name: s.name,
|
|
60
|
+
path: s.dbPath,
|
|
61
|
+
bytes: s.bytes,
|
|
62
|
+
pages: s.pages,
|
|
63
|
+
saltHex: s.saltHex,
|
|
64
|
+
formatVersion: s.formatVersion,
|
|
65
|
+
material: s.materialPath ?? null,
|
|
66
|
+
}));
|
|
67
|
+
const pids = await findQQPids().catch(() => []);
|
|
68
|
+
return ok({ searchedDirs: searchDirs, databases, qqPids: pids });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return fail(error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Online (Windows): recover the device passphrase by scanning QQ's own memory
|
|
76
|
+
* and accepting the first candidate that decrypts a known artefact. The
|
|
77
|
+
* passphrase is never written to disk unless `save` is set.
|
|
78
|
+
*/
|
|
79
|
+
server.registerTool("capture_offline_key", {
|
|
80
|
+
description: "Recovers the NTQQ device passphrase: scrapes short ASCII tokens from a running QQ " +
|
|
81
|
+
"process and verifies each against a .material file or database page. Requires a " +
|
|
82
|
+
"logged-in QQ and Python 3. Pass save=true to persist it to .qqkey (gitignored).",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
pids: z.array(z.number().int().positive()).optional().describe("QQ PIDs; defaults to all running QQ.exe"),
|
|
85
|
+
materialPath: z.string().optional().describe("Any <db>-first.material file to verify against"),
|
|
86
|
+
dbPath: z.string().optional().describe("Database to verify against when no material is available"),
|
|
87
|
+
maxCandidates: z.number().int().positive().optional().default(200_000),
|
|
88
|
+
save: z.boolean().optional().default(false),
|
|
89
|
+
},
|
|
90
|
+
}, async (args) => {
|
|
91
|
+
try {
|
|
92
|
+
const result = await captureOfflineKey(args);
|
|
93
|
+
if (!result.passphrase) {
|
|
94
|
+
return ok({
|
|
95
|
+
found: false,
|
|
96
|
+
...result,
|
|
97
|
+
note: "no candidate decrypted the oracle; make sure QQ is logged in and the target database belongs to this account",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return ok({ found: true, ...result });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return fail(error);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
/** Offline: check a passphrase against a database (single target or every local database). */
|
|
107
|
+
server.registerTool("verify_offline_key", {
|
|
108
|
+
description: "Verifies an NTQQ device passphrase against a database's page 1 or a .material file. " +
|
|
109
|
+
"Use all=true to test every database found locally — one passphrase unlocks all of them.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
passphrase: z.string().optional().describe("Defaults to QQ_MCP_PASSPHRASE or .qqkey"),
|
|
112
|
+
dbPath: z.string().optional(),
|
|
113
|
+
materialPath: z.string().optional(),
|
|
114
|
+
all: z.boolean().optional().default(false),
|
|
115
|
+
},
|
|
116
|
+
}, async ({ passphrase, dbPath, materialPath, all }) => {
|
|
117
|
+
try {
|
|
118
|
+
const key = requirePassphrase(passphrase);
|
|
119
|
+
if (all)
|
|
120
|
+
return ok({ results: verifyPassphraseEverywhere(key) });
|
|
121
|
+
if (materialPath) {
|
|
122
|
+
const { plaintext } = decryptMaterial(materialPath, key);
|
|
123
|
+
return ok({ materialPath, ok: plaintext.includes(Buffer.from("CREATE TABLE", "utf8")), byteLen: plaintext.length });
|
|
124
|
+
}
|
|
125
|
+
const oracle = pickOracle({ dbPath, sources: dbPath ? undefined : listDataSources() });
|
|
126
|
+
if (!oracle)
|
|
127
|
+
return ok({ ok: false, note: "no database found to verify against" });
|
|
128
|
+
if (oracle.kind === "material") {
|
|
129
|
+
const { plaintext } = decryptMaterial(oracle.materialPath, key);
|
|
130
|
+
return ok({ oracle: oracle.materialPath, ok: plaintext.includes(Buffer.from("CREATE TABLE", "utf8")) });
|
|
131
|
+
}
|
|
132
|
+
return ok({ oracle: oracle.dbPath, ok: verifyPassphrase(oracle.dbPath, key) });
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return fail(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* Offline: decrypt a database (WAL replayed) into the plaintext cache and
|
|
140
|
+
* report its shape. This is the "offline key" payoff — no QQ required.
|
|
141
|
+
*/
|
|
142
|
+
server.registerTool("decrypt_qq_database", {
|
|
143
|
+
description: "Decrypts an NTQQ SQLCipher database (AES-256-CBC page cipher, PBKDF2-HMAC-SHA512) " +
|
|
144
|
+
"into a plaintext SQLite file under exports/plain, replaying committed WAL frames. " +
|
|
145
|
+
"Returns the plaintext path plus table counts. Cached until the source changes.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
dbPath: z.string().optional().describe("Defaults to the newest local nt_msg.db"),
|
|
148
|
+
passphrase: z.string().optional().describe("Defaults to QQ_MCP_PASSPHRASE or .qqkey"),
|
|
149
|
+
force: z.boolean().optional().default(false),
|
|
150
|
+
},
|
|
151
|
+
}, async ({ dbPath, passphrase, force }) => {
|
|
152
|
+
try {
|
|
153
|
+
const source = resolveSource("nt_msg.db", dbPath);
|
|
154
|
+
const key = requirePassphrase(passphrase);
|
|
155
|
+
const result = ensurePlaintext(source.dbPath, key, { force });
|
|
156
|
+
const stats = chatStats(openOffline(source.dbPath, { passphrase: key, plaintextPath: result.path }));
|
|
157
|
+
return ok({
|
|
158
|
+
source: source.dbPath,
|
|
159
|
+
plaintext: result.path,
|
|
160
|
+
reusedCache: result.reused,
|
|
161
|
+
decryptMs: result.durationMs,
|
|
162
|
+
cachePath: plaintextCachePath(source.dbPath),
|
|
163
|
+
stats,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return fail(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
/** Offline: query chat messages with decoded protobuf content. */
|
|
171
|
+
server.registerTool("query_chat_messages", {
|
|
172
|
+
description: "Queries decrypted chat records offline. Filters by peer QQ (c2c) or group number, " +
|
|
173
|
+
"time range and keyword (matched on decoded text). Each message carries typed " +
|
|
174
|
+
"segments (text/image/file/video/sticker/reply/forward/call/system).",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
dbPath: z.string().optional().describe("Defaults to the newest local nt_msg.db"),
|
|
177
|
+
passphrase: z.string().optional(),
|
|
178
|
+
chat: z.enum(["c2c", "group", "all"]).optional().default("all"),
|
|
179
|
+
peerQq: z.number().int().positive().optional(),
|
|
180
|
+
groupQq: z.number().int().positive().optional(),
|
|
181
|
+
since: z.number().int().optional().describe("Unix seconds, inclusive"),
|
|
182
|
+
until: z.number().int().optional().describe("Unix seconds, inclusive"),
|
|
183
|
+
keyword: z.string().optional(),
|
|
184
|
+
limit: z.number().int().positive().max(5000).optional().default(100),
|
|
185
|
+
order: z.enum(["asc", "desc"]).optional().default("desc"),
|
|
186
|
+
maxScan: z.number().int().positive().optional().default(200_000),
|
|
187
|
+
},
|
|
188
|
+
}, async (args) => {
|
|
189
|
+
try {
|
|
190
|
+
const source = resolveSource("nt_msg.db", args.dbPath);
|
|
191
|
+
const key = requirePassphrase(args.passphrase);
|
|
192
|
+
const offline = openOffline(source.dbPath, { passphrase: key });
|
|
193
|
+
const result = queryMessages(offline, args);
|
|
194
|
+
return ok({ source: source.dbPath, plaintext: offline.plaintextPath, ...result });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return fail(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
/** Offline: real contact book — friends, groups and the QQ↔UID mapping table. */
|
|
201
|
+
server.registerTool("list_contacts", {
|
|
202
|
+
description: "Lists contacts offline from recent_contact_v3_table (friends with QQ number, uid, " +
|
|
203
|
+
"remark/nickname and last-activity time), group_list in group_info.db (group number, " +
|
|
204
|
+
"name, member count) and the nt_uid_mapping_table QQ↔NT-UID map.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
dbPath: z.string().optional(),
|
|
207
|
+
passphrase: z.string().optional(),
|
|
208
|
+
limit: z.number().int().positive().max(5000).optional().default(500),
|
|
209
|
+
},
|
|
210
|
+
}, async ({ dbPath, passphrase, limit }) => {
|
|
211
|
+
try {
|
|
212
|
+
const source = resolveSource("nt_msg.db", dbPath);
|
|
213
|
+
const key = requirePassphrase(passphrase);
|
|
214
|
+
const offline = openOffline(source.dbPath, { passphrase: key });
|
|
215
|
+
const groupInfoPath = sibling(source, "group_info.db");
|
|
216
|
+
const contacts = listContacts(offline, { groupInfoPath, passphrase: key, limit });
|
|
217
|
+
return ok({
|
|
218
|
+
source: source.dbPath,
|
|
219
|
+
groupInfo: groupInfoPath ?? null,
|
|
220
|
+
friends: contacts.friends,
|
|
221
|
+
groups: contacts.groups,
|
|
222
|
+
uidMappings: contacts.uidMappings,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return fail(error);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
/** Offline: group member roster from group_info.db. */
|
|
230
|
+
server.registerTool("list_group_members", {
|
|
231
|
+
description: "Lists the offline group member roster from group_info.db.group_member3: member QQ, " +
|
|
232
|
+
"NT uid, nickname, group card and last-speak time. Filter by groupQq for one group.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
groupQq: z.number().int().positive().optional(),
|
|
235
|
+
dbPath: z.string().optional().describe("Path to nt_msg.db, used to locate group_info.db"),
|
|
236
|
+
passphrase: z.string().optional(),
|
|
237
|
+
limit: z.number().int().positive().max(5000).optional().default(200),
|
|
238
|
+
},
|
|
239
|
+
}, async ({ groupQq, dbPath, passphrase, limit }) => {
|
|
240
|
+
try {
|
|
241
|
+
const source = resolveSource("nt_msg.db", dbPath);
|
|
242
|
+
const key = requirePassphrase(passphrase);
|
|
243
|
+
const groupInfoPath = sibling(source, "group_info.db");
|
|
244
|
+
if (!groupInfoPath)
|
|
245
|
+
throw new Error("group_info.db not found next to the selected database");
|
|
246
|
+
const members = listGroupMembers(groupInfoPath, { passphrase: key, groupQq, limit });
|
|
247
|
+
return ok({ groupInfo: groupInfoPath, count: members.length, members });
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return fail(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* Offline: decrypt a QQNT `<db>.material` file (PBKDF2-HMAC-SHA512 + AES-256-CBC,
|
|
255
|
+
* IV=0) and list the database schema it embeds. Independent of any database.
|
|
256
|
+
*/
|
|
257
|
+
server.registerTool("get_db_schema", {
|
|
258
|
+
description: "Decrypts a QQNT .material key-material file and returns the database schema " +
|
|
259
|
+
"(tables/indexes of nt_msg.db etc). Needs only the passphrase — no database access.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
materialPath: z.string().describe("Path to e.g. nt_msg.db-first.material"),
|
|
262
|
+
passphrase: z.string().optional().describe("Defaults to QQ_MCP_PASSPHRASE or .qqkey"),
|
|
263
|
+
kdfIter: z.number().int().positive().optional().default(DEFAULT_KDF_ITER),
|
|
264
|
+
},
|
|
265
|
+
}, async ({ materialPath, passphrase, kdfIter }) => {
|
|
266
|
+
try {
|
|
267
|
+
const key = requirePassphrase(passphrase);
|
|
268
|
+
const { salt, plaintext } = decryptMaterial(materialPath, key, kdfIter);
|
|
269
|
+
const tables = extractSchema(plaintext);
|
|
270
|
+
return ok({
|
|
271
|
+
saltHex: salt.toString("hex"),
|
|
272
|
+
byteLen: plaintext.length,
|
|
273
|
+
tableCount: tables.length,
|
|
274
|
+
tables: tables.map((t) => ({
|
|
275
|
+
name: t.name,
|
|
276
|
+
columns: t.columns ?? [],
|
|
277
|
+
statement: t.statement?.slice(0, 400) ?? null,
|
|
278
|
+
})),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return fail(error);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
const transport = new StdioServerTransport();
|
|
286
|
+
await server.connect(transport);
|
|
287
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EACL,SAAS,EACT,eAAe,EACf,cAAc,EACd,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,aAAa,EACb,iBAAiB,GAElB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,SAAS,EAAE,CAAC,OAAgB;IAC1B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC1B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACpH,CAAC;AAED,iFAAiF;AACjF,SAAS,aAAa,CAAC,IAAY,EAAE,QAAiB;IACpD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,CAAC;QAC/E,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,uCAAuC,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,UAAU,GAAG,eAAe,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3F,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,qBAAqB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,qBAAqB,GAAG,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;AACnB,CAAC;AAED,kEAAkE;AAClE,SAAS,OAAO,CAAC,MAAkB,EAAE,IAAY;IAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACjF,OAAO,KAAK,EAAE,MAAM,CAAC;AACvB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,YAAY,CACjB,mBAAmB,EACnB;IACE,WAAW,EACT,0FAA0F;QAC1F,sFAAsF;QACtF,+CAA+C;IACjD,WAAW,EAAE;QACX,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;KACjF;CACF,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,GAAG,cAAc,EAAE,CAAC,CAAC;QAC1D,MAAM,SAAS,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxD,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,MAAM;YACd,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,QAAQ,EAAE,CAAC,CAAC,YAAY,IAAI,IAAI;SACjC,CAAC,CAAC,CAAC;QACJ,MAAM,IAAI,GAAG,MAAM,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAChD,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;IACE,WAAW,EACT,oFAAoF;QACpF,kFAAkF;QAClF,iFAAiF;IACnF,WAAW,EAAE;QACX,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACzG,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;QAC9F,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0DAA0D,CAAC;QAClG,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;QACtE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;KAC5C;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;gBACR,KAAK,EAAE,KAAK;gBACZ,GAAG,MAAM;gBACT,IAAI,EAAE,8GAA8G;aACrH,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;IACE,WAAW,EACT,sFAAsF;QACtF,yFAAyF;IAC3F,WAAW,EAAE;QACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACrF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACnC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;KAC3C;CACF,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,EAAE,EAAE;IAClD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,GAAG;YAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,0BAA0B,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjE,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;YACzD,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;QACtH,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,qCAAqC,EAAE,CAAC,CAAC;QACnF,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;YAChE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,EAAE,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1G,CAAC;QACD,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACjF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;IACE,WAAW,EACT,oFAAoF;QACpF,oFAAoF;QACpF,gFAAgF;IAClF,WAAW,EAAE;QACX,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;QAChF,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACrF,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;KAC7C;CACF,EACD,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACrG,OAAO,EAAE,CAAC;YACR,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,SAAS,EAAE,MAAM,CAAC,IAAI;YACtB,WAAW,EAAE,MAAM,CAAC,MAAM;YAC1B,SAAS,EAAE,MAAM,CAAC,UAAU;YAC5B,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC;YAC5C,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,kEAAkE;AAClE,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;IACE,WAAW,EACT,oFAAoF;QACpF,+EAA+E;QAC/E,qEAAqE;IACvE,WAAW,EAAE;QACX,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;QAChF,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;QAC/D,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAC9C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QACtE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QACtE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC9B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;QACpE,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;QACzD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;KACjE;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,aAAa,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IACpF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,iFAAiF;AACjF,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;IACE,WAAW,EACT,oFAAoF;QACpF,sFAAsF;QACtF,iEAAiE;IACnE,WAAW,EAAE;QACX,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;KACrE;CACF,EACD,KAAK,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QAChE,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QAClF,OAAO,EAAE,CAAC;YACR,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,SAAS,EAAE,aAAa,IAAI,IAAI;YAChC,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,WAAW,EAAE,QAAQ,CAAC,WAAW;SAClC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,uDAAuD;AACvD,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;IACE,WAAW,EACT,qFAAqF;QACrF,oFAAoF;IACtF,WAAW,EAAE;QACX,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAC/C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;QACzF,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;KACrE;CACF,EACD,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QACvD,IAAI,CAAC,aAAa;YAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC7F,MAAM,OAAO,GAAG,gBAAgB,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACrF,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;IACE,WAAW,EACT,8EAA8E;QAC9E,oFAAoF;IACtF,WAAW,EAAE;QACX,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uCAAuC,CAAC;QAC1E,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACrF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC;KAC1E;CACF,EACD,KAAK,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,EAAE;IAC9C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;QACxC,OAAO,EAAE,CAAC;YACR,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC7B,OAAO,EAAE,SAAS,CAAC,MAAM;YACzB,UAAU,EAAE,MAAM,CAAC,MAAM;YACzB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACzB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;gBACxB,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI;aAC9C,CAAC,CAAC;SACJ,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
package/dist/keyscan.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline key (device passphrase) capture for NTQQ.
|
|
3
|
+
*
|
|
4
|
+
* The passphrase is never stored on disk by QQ: it is derived in-process and
|
|
5
|
+
* handed to SQLCipher. It is, however, a short ASCII string living in the QQ
|
|
6
|
+
* process, and it is the same for every database on the device. This module
|
|
7
|
+
* scrapes candidate tokens from process memory and accepts the first one that
|
|
8
|
+
* actually decrypts a known artefact (a `.material` DDL blob or page 1 of a
|
|
9
|
+
* database), so a candidate is only reported when it is verified.
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { availableParallelism } from "node:os";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import { Worker } from "node:worker_threads";
|
|
16
|
+
import { decryptMaterial } from "./schema.js";
|
|
17
|
+
import { verifyPassphrase } from "./sqlcipher.js";
|
|
18
|
+
import { findQQDataDirs, KEY_FILE, listDataSources, SCANNER_SCRIPT } from "./offline.js";
|
|
19
|
+
const PYTHON = process.env.QQ_MCP_PYTHON ?? "python";
|
|
20
|
+
/** Tests one candidate against the oracle. Wrong candidates fail structurally. */
|
|
21
|
+
export function verifyKeyCandidate(oracle, candidate) {
|
|
22
|
+
try {
|
|
23
|
+
if (oracle.kind === "material") {
|
|
24
|
+
const { plaintext } = decryptMaterial(oracle.materialPath, candidate);
|
|
25
|
+
return plaintext.includes(Buffer.from("CREATE TABLE", "utf8"));
|
|
26
|
+
}
|
|
27
|
+
return verifyPassphrase(oracle.dbPath, candidate);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** PIDs of running `QQ.exe` processes (Windows). */
|
|
34
|
+
export function findQQPids() {
|
|
35
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
36
|
+
execFile("powershell", ["-NoProfile", "-Command", "(Get-Process -Name 'QQ' -ErrorAction SilentlyContinue).Id"], { windowsHide: true }, (error, stdout) => {
|
|
37
|
+
if (error) {
|
|
38
|
+
reject(error);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const pids = stdout
|
|
42
|
+
.split(/\s+/)
|
|
43
|
+
.map((s) => Number.parseInt(s, 10))
|
|
44
|
+
.filter((n) => Number.isInteger(n) && n > 0);
|
|
45
|
+
resolve(pids);
|
|
46
|
+
});
|
|
47
|
+
return promise;
|
|
48
|
+
}
|
|
49
|
+
/** Picks a verification oracle from the locally installed QQ data. */
|
|
50
|
+
export function pickOracle(opts = {}) {
|
|
51
|
+
if (opts.materialPath)
|
|
52
|
+
return { kind: "material", materialPath: opts.materialPath };
|
|
53
|
+
if (opts.dbPath)
|
|
54
|
+
return { kind: "database", dbPath: opts.dbPath };
|
|
55
|
+
const sources = opts.sources ?? listDataSources();
|
|
56
|
+
const withMaterial = sources.find((s) => s.materialPath && s.name.startsWith("nt_msg"));
|
|
57
|
+
if (withMaterial?.materialPath)
|
|
58
|
+
return { kind: "material", materialPath: withMaterial.materialPath };
|
|
59
|
+
const anyMaterial = sources.find((s) => s.materialPath);
|
|
60
|
+
if (anyMaterial?.materialPath)
|
|
61
|
+
return { kind: "material", materialPath: anyMaterial.materialPath };
|
|
62
|
+
const anyUsable = sources.find((s) => s.pages > 0);
|
|
63
|
+
if (anyUsable)
|
|
64
|
+
return { kind: "database", dbPath: anyUsable.dbPath };
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Scrapes candidate tokens from QQ's memory and verifies them against a key oracle. */
|
|
68
|
+
export async function captureOfflineKey(opts = {}) {
|
|
69
|
+
const started = Date.now();
|
|
70
|
+
const sources = listDataSources();
|
|
71
|
+
const oracle = pickOracle({ materialPath: opts.materialPath, dbPath: opts.dbPath, sources });
|
|
72
|
+
if (!oracle) {
|
|
73
|
+
throw new Error("no verification oracle: pass materialPath or dbPath (no NTQQ databases found locally)");
|
|
74
|
+
}
|
|
75
|
+
const pids = opts.pids?.length ? opts.pids : await findQQPids();
|
|
76
|
+
if (pids.length === 0)
|
|
77
|
+
throw new Error("QQ is not running — log in first, then retry");
|
|
78
|
+
const scanner = opts.scannerPath ?? SCANNER_SCRIPT;
|
|
79
|
+
if (!existsSync(scanner))
|
|
80
|
+
throw new Error(`missing memory scanner: ${scanner}`);
|
|
81
|
+
opts.onProgress?.(`scanning ${pids.length} process(es) for candidates`);
|
|
82
|
+
const candidates = await runScanner(scanner, pids, opts.maxCandidates ?? 200_000);
|
|
83
|
+
opts.onProgress?.(`testing ${candidates.length} candidates against ${oracle.kind}`);
|
|
84
|
+
const found = await verifyCandidates(oracle, candidates, opts.onProgress);
|
|
85
|
+
const result = {
|
|
86
|
+
passphrase: found.passphrase,
|
|
87
|
+
oracle,
|
|
88
|
+
pidsScanned: pids,
|
|
89
|
+
candidates: candidates.length,
|
|
90
|
+
testedInParallel: found.tested,
|
|
91
|
+
elapsedMs: Date.now() - started,
|
|
92
|
+
};
|
|
93
|
+
if (found.passphrase && opts.save) {
|
|
94
|
+
mkdirSync(dirname(KEY_FILE), { recursive: true });
|
|
95
|
+
writeFileSync(KEY_FILE, `${found.passphrase}\n`, { encoding: "utf8", mode: 0o600 });
|
|
96
|
+
result.savedTo = KEY_FILE;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function runScanner(scanner, pids, maxCandidates) {
|
|
101
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
102
|
+
execFile(PYTHON, [scanner, ...pids.map(String), "--max", String(maxCandidates)], { windowsHide: true, timeout: 900_000, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
103
|
+
if (error && !stdout) {
|
|
104
|
+
reject(new Error(`memory scan failed: ${stderr || error.message}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
109
|
+
const token = line.trim();
|
|
110
|
+
if (token.length >= 12 && token.length <= 32)
|
|
111
|
+
seen.add(token);
|
|
112
|
+
}
|
|
113
|
+
resolve([...seen]);
|
|
114
|
+
});
|
|
115
|
+
return promise;
|
|
116
|
+
}
|
|
117
|
+
/** Fans candidates across workers; returns as soon as one worker verifies a key. */
|
|
118
|
+
async function verifyCandidates(oracle, candidates, onProgress) {
|
|
119
|
+
if (candidates.length === 0)
|
|
120
|
+
return { tested: 0 };
|
|
121
|
+
const workers = Math.max(1, Math.min(availableParallelism(), 16, candidates.length));
|
|
122
|
+
const chunkSize = Math.ceil(candidates.length / workers);
|
|
123
|
+
const chunks = [];
|
|
124
|
+
for (let i = 0; i < candidates.length; i += chunkSize)
|
|
125
|
+
chunks.push(candidates.slice(i, i + chunkSize));
|
|
126
|
+
const workerUrl = new URL("./verify-worker.js", import.meta.url);
|
|
127
|
+
const running = chunks.map((chunk) => new Promise((resolve, reject) => {
|
|
128
|
+
const worker = new Worker(workerUrl, { workerData: { oracle, candidates: chunk } });
|
|
129
|
+
worker.once("message", (message) => {
|
|
130
|
+
resolve({ passphrase: message.passphrase, tested: message.tested });
|
|
131
|
+
});
|
|
132
|
+
worker.once("error", reject);
|
|
133
|
+
}));
|
|
134
|
+
const outcomes = running.map(async (promise, index) => {
|
|
135
|
+
const outcome = await promise;
|
|
136
|
+
onProgress?.(`worker ${index + 1}/${running.length} tested ${outcome.tested}`);
|
|
137
|
+
return outcome;
|
|
138
|
+
});
|
|
139
|
+
const settled = await Promise.allSettled(outcomes);
|
|
140
|
+
let tested = 0;
|
|
141
|
+
let passphrase;
|
|
142
|
+
for (const entry of settled) {
|
|
143
|
+
if (entry.status !== "fulfilled")
|
|
144
|
+
continue;
|
|
145
|
+
tested += entry.value.tested;
|
|
146
|
+
if (!passphrase && entry.value.passphrase)
|
|
147
|
+
passphrase = entry.value.passphrase;
|
|
148
|
+
}
|
|
149
|
+
return { passphrase, tested };
|
|
150
|
+
}
|
|
151
|
+
/** Convenience: verifies a known passphrase against every locally found database. */
|
|
152
|
+
export function verifyPassphraseEverywhere(passphrase) {
|
|
153
|
+
return listDataSources().map((source) => ({ name: source.name, ok: verifyPassphrase(source.dbPath, passphrase) }));
|
|
154
|
+
}
|
|
155
|
+
export { findQQDataDirs };
|
|
156
|
+
//# sourceMappingURL=keyscan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyscan.js","sourceRoot":"","sources":["../src/keyscan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAmB,MAAM,cAAc,CAAC;AAE1G,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,QAAQ,CAAC;AAOrD,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,MAAiB,EAAE,SAAiB;IACrE,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;YACtE,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,UAAU;IACxB,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAY,CAAC;IACvE,QAAQ,CACN,YAAY,EACZ,CAAC,YAAY,EAAE,UAAU,EAAE,2DAA2D,CAAC,EACvF,EAAE,WAAW,EAAE,IAAI,EAAE,EACrB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QAChB,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,KAAK,CAAC,CAAC;YACd,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,MAAM;aAChB,KAAK,CAAC,KAAK,CAAC;aACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;aAClC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC,CACF,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,OAA2E,EAAE;IACtG,IAAI,IAAI,CAAC,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;IACpF,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,eAAe,EAAE,CAAC;IAClD,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxF,IAAI,YAAY,EAAE,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,CAAC,YAAY,EAAE,CAAC;IACrG,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IACxD,IAAI,WAAW,EAAE,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,CAAC,YAAY,EAAE,CAAC;IACnG,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACnD,IAAI,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;IACrE,OAAO,SAAS,CAAC;AACnB,CAAC;AAwBD,wFAAwF;AACxF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAAuB,EAAE;IAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,eAAe,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7F,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;IAC3G,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,UAAU,EAAE,CAAC;IAChE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACvF,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,IAAI,cAAc,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,EAAE,CAAC,CAAC;IAEhF,IAAI,CAAC,UAAU,EAAE,CAAC,YAAY,IAAI,CAAC,MAAM,6BAA6B,CAAC,CAAC;IACxE,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,CAAC;IAClF,IAAI,CAAC,UAAU,EAAE,CAAC,WAAW,UAAU,CAAC,MAAM,uBAAuB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAE1E,MAAM,MAAM,GAAkB;QAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM;QACN,WAAW,EAAE,IAAI;QACjB,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,gBAAgB,EAAE,KAAK,CAAC,MAAM;QAC9B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO;KAChC,CAAC;IACF,IAAI,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAClC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,aAAa,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,MAAM,CAAC,OAAO,GAAG,QAAQ,CAAC;IAC5B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,UAAU,CAAC,OAAe,EAAE,IAAc,EAAE,aAAqB;IACxE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAY,CAAC;IACvE,QAAQ,CACN,MAAM,EACN,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,EAC9D,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EACpE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;QACxB,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,KAAK,CAAC,uBAAuB,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACpE,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE;gBAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACrB,CAAC,CACF,CAAC;IACF,OAAO,OAAO,CAAC;AACjB,CAAC;AAOD,oFAAoF;AACpF,KAAK,UAAU,gBAAgB,CAC7B,MAAiB,EACjB,UAAoB,EACpB,UAAsC;IAEtC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IACrF,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;IACzD,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS;QAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;IACvG,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CACxB,CAAC,KAAK,EAAE,EAAE,CACR,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC7C,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACpF,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,OAAgD,EAAE,EAAE;YAC1E,OAAO,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CACL,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACpD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;QAC9B,UAAU,EAAE,CAAC,UAAU,KAAK,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,WAAW,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/E,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,UAA8B,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW;YAAE,SAAS;QAC3C,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU;YAAE,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;IACjF,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,0BAA0B,CAAC,UAAkB;IAC3D,OAAO,eAAe,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;AACrH,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|