@napuketto/kernel 0.0.7 → 0.0.9

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,4120 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_fs = require("node:fs");
25
+ let node_fs_promises = require("node:fs/promises");
26
+ let node_path = require("node:path");
27
+ let smol_toml = require("smol-toml");
28
+ let node_process = require("node:process");
29
+ node_process = __toESM(node_process, 1);
30
+ let pino = require("pino");
31
+ pino = __toESM(pino, 1);
32
+ let pino_pretty = require("pino-pretty");
33
+ pino_pretty = __toESM(pino_pretty, 1);
34
+ let node_os = require("node:os");
35
+ let node_url = require("node:url");
36
+ let node_crypto = require("node:crypto");
37
+ let node_https = require("node:https");
38
+ let node_events = require("node:events");
39
+ //#region src/infra/errors.ts
40
+ /**
41
+ * 全部错误码集合。协议层映射表可据此推导:
42
+ * `Record<(typeof KERNEL_ERROR_CODES)[number], number>`。
43
+ */
44
+ const KERNEL_ERROR_CODES = [
45
+ "SEND_FAILED",
46
+ "PERMISSION_DENIED",
47
+ "NOT_FOUND",
48
+ "TIMEOUT",
49
+ "NOT_LOGIN",
50
+ "INVALID_PARAM",
51
+ "INVALID_STATE",
52
+ "UNKNOWN"
53
+ ];
54
+ /** kernel 类型化错误:apis / 基础设施层抛出的统一错误形态。 */
55
+ var KernelError = class extends Error {
56
+ code;
57
+ constructor(message, code = "UNKNOWN", options) {
58
+ super(message, options);
59
+ this.name = "KernelError";
60
+ this.code = code;
61
+ }
62
+ };
63
+ /** 便捷构造:`kernelError('消息不存在', 'NOT_FOUND', { cause })`。 */
64
+ function kernelError(message, code = "UNKNOWN", options) {
65
+ return new KernelError(message, code, options);
66
+ }
67
+ /** 类型守卫:协议层 catch 统一映射时区分 KernelError 与意外错误。 */
68
+ function isKernelError(err) {
69
+ return err instanceof KernelError;
70
+ }
71
+ //#endregion
72
+ //#region src/infra/config.ts
73
+ /**
74
+ * 配置基类(ADR-012)
75
+ *
76
+ * 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
77
+ * (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
78
+ *
79
+ * kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
80
+ * 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
81
+ * 协议配置 schema 在各自协议包,见各包 docs/design.md。
82
+ */
83
+ /** 按扩展名推断格式(.toml → toml,其余 → json)。 */
84
+ function inferFormat(path) {
85
+ if ((0, node_path.extname)(path).toLowerCase() === ".toml") return "toml";
86
+ return "json";
87
+ }
88
+ function isMissingFileError(err) {
89
+ if (err instanceof Error) return err.code === "ENOENT";
90
+ return false;
91
+ }
92
+ /**
93
+ * 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
94
+ * 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
95
+ * - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
96
+ * - `save()`:按格式序列化(TOML/JSON)原子写。
97
+ */
98
+ var ConfigBase = class {
99
+ path;
100
+ defaults;
101
+ schema;
102
+ format;
103
+ seed;
104
+ value;
105
+ constructor(opts) {
106
+ this.path = opts.path;
107
+ this.defaults = opts.defaults;
108
+ this.schema = opts.schema;
109
+ this.format = opts.format ?? inferFormat(opts.path);
110
+ this.seed = opts.seed;
111
+ this.value = opts.seed ?? opts.defaults;
112
+ }
113
+ /** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
114
+ get() {
115
+ return this.value;
116
+ }
117
+ /**
118
+ * 加载配置:
119
+ * - seed 存在 → 直接用 seed(已校验,不读文件;全局 TOML 分段装配方式);
120
+ * - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
121
+ * - 文件存在 → 读取 + 校验,更新内存,返回。
122
+ */
123
+ async load() {
124
+ if (this.seed !== void 0) {
125
+ this.value = this.seed;
126
+ return this.value;
127
+ }
128
+ let raw;
129
+ try {
130
+ raw = await (0, node_fs_promises.readFile)(this.path, "utf8");
131
+ } catch (err) {
132
+ if (isMissingFileError(err)) {
133
+ await this.save(this.defaults);
134
+ return this.value;
135
+ }
136
+ throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
137
+ }
138
+ this.value = this.parse(raw);
139
+ return this.value;
140
+ }
141
+ /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
142
+ reload() {
143
+ return this.load();
144
+ }
145
+ /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
146
+ async save(next) {
147
+ const parsed = this.parse(this.serialize(next));
148
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(this.path), { recursive: true });
149
+ const tmpPath = `${this.path}.tmp`;
150
+ await (0, node_fs_promises.writeFile)(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
151
+ await (0, node_fs_promises.rename)(tmpPath, this.path);
152
+ this.value = parsed;
153
+ }
154
+ /** 对象 → 文本(TOML/JSON)。 */
155
+ serialize(value) {
156
+ if (this.format === "toml") return (0, smol_toml.stringify)(value);
157
+ return JSON.stringify(value, null, 4);
158
+ }
159
+ /** 文本 → 对象(TOML/JSON)。 */
160
+ deserialize(raw) {
161
+ if (this.format === "toml") return (0, smol_toml.parse)(raw);
162
+ return JSON.parse(raw);
163
+ }
164
+ /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
165
+ parse(raw) {
166
+ let input;
167
+ try {
168
+ input = this.deserialize(raw);
169
+ } catch (err) {
170
+ throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
171
+ }
172
+ try {
173
+ return this.schema.parse(input);
174
+ } catch (err) {
175
+ throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
176
+ }
177
+ }
178
+ };
179
+ /** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
180
+ function parseToml(text) {
181
+ return (0, smol_toml.parse)(text);
182
+ }
183
+ /** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
184
+ function stringifyToml(value) {
185
+ return (0, smol_toml.stringify)(value);
186
+ }
187
+ //#endregion
188
+ //#region src/infra/logger.ts
189
+ /**
190
+ * pino 日志封装(ADR-007)
191
+ *
192
+ * - console(pino-pretty 可读输出)+ 可选文件双目的地
193
+ * - 统一 level 控制
194
+ * - 内置 redact(token / 票据等敏感字段不打日志),可追加
195
+ * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
196
+ */
197
+ /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
198
+ const DEFAULT_REDACT_PATHS = [
199
+ "token",
200
+ "ticket",
201
+ "cookie",
202
+ "password",
203
+ "secret"
204
+ ];
205
+ function toSingle(streams) {
206
+ const [only] = streams;
207
+ if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
208
+ return only.stream;
209
+ }
210
+ /**
211
+ * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
212
+ *
213
+ * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
214
+ * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
215
+ */
216
+ function createLogger(opts = {}) {
217
+ const level = opts.level ?? "info";
218
+ const consoleEnabled = opts.console ?? true;
219
+ const { file, base } = opts;
220
+ const streams = [];
221
+ if (consoleEnabled) streams.push({ stream: (0, pino_pretty.default)({
222
+ colorize: true,
223
+ translateTime: "SYS:HH:MM:ss.l",
224
+ singleLine: false,
225
+ destination: node_process.default.stdout
226
+ }) });
227
+ if (file) {
228
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(file), { recursive: true });
229
+ streams.push({ stream: pino.default.destination({
230
+ dest: file,
231
+ sync: true
232
+ }) });
233
+ }
234
+ if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
235
+ const pinoOptions = {
236
+ level,
237
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
238
+ };
239
+ if (base) pinoOptions.base = {
240
+ pid: node_process.default.pid,
241
+ ...base
242
+ };
243
+ let stream;
244
+ if (streams.length === 1) stream = toSingle(streams);
245
+ else stream = pino.default.multistream(streams);
246
+ return (0, pino.default)(pinoOptions, stream);
247
+ }
248
+ //#endregion
249
+ //#region src/infra/paths.ts
250
+ /**
251
+ * 数据目录布局(ADR-016,2026-08-08 修订:数据根默认放项目根)
252
+ *
253
+ * ```
254
+ * <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
255
+ * ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
256
+ * ├── logs/ # pino 文件日志
257
+ * └── cache/ # 临时文件、媒体缓存
258
+ *
259
+ * <项目根>/napuketto.toml # 全局配置文件(放项目根目录,不入库)
260
+ * <项目根>/.napuketto/ # 数据根(默认值,2026-08-08 用户拍板:直接放项目主目录)
261
+ * ```
262
+ *
263
+ * 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 >
264
+ * `<项目根>/.napuketto`(默认,部署原因不放用户目录)。
265
+ * 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
266
+ */
267
+ /** 默认数据根目录名(项目根下,2026-08-08 用户拍板:不放用户目录)。 */
268
+ const DEFAULT_DATA_ROOT_NAME = ".napuketto";
269
+ /** 全局配置文件名。 */
270
+ const MAIN_CONFIG_FILE = "napuketto.toml";
271
+ /** `~` 前缀展开为用户主目录(跨平台配置文件写法 `dataDir = "~/.napuketto"`)。 */
272
+ function expandTilde(p) {
273
+ if (p === "~") return (0, node_os.homedir)();
274
+ if (p.startsWith("~/") || p.startsWith("~\\")) return (0, node_path.join)((0, node_os.homedir)(), p.slice(2));
275
+ return p;
276
+ }
277
+ /**
278
+ * 项目根查找:入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即仓库根)→
279
+ * cwd 向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)→ undefined(兜底)。
280
+ */
281
+ function resolveProjectRoot() {
282
+ const moduleRoot = findProjectRoot((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "pnpm-workspace.yaml");
283
+ if (moduleRoot !== void 0) return moduleRoot;
284
+ return findProjectRoot(node_process.default.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(node_process.default.cwd(), "package.json");
285
+ }
286
+ /**
287
+ * 解析数据根:显式参数 > 环境变量 > `<项目根>/.napuketto`(默认)> 用户目录兜底(裸脚本)。
288
+ * 支持 `~` 前缀(展开为用户主目录)与相对路径(相对 cwd 解析)。
289
+ * 返回绝对路径。独立导出便于 cli / 探测脚本复用。
290
+ */
291
+ function resolveDataRoot(dataRoot) {
292
+ const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
293
+ if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(expandTilde(explicit));
294
+ const projectRoot = resolveProjectRoot();
295
+ if (projectRoot !== void 0) return (0, node_path.join)(projectRoot, DEFAULT_DATA_ROOT_NAME);
296
+ return (0, node_path.join)((0, node_os.homedir)(), DEFAULT_DATA_ROOT_NAME);
297
+ }
298
+ /** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
299
+ function findProjectRoot(startDir, marker) {
300
+ let dir = startDir;
301
+ for (;;) {
302
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, marker))) return dir;
303
+ const parent = (0, node_path.dirname)(dir);
304
+ if (parent === dir) return;
305
+ dir = parent;
306
+ }
307
+ }
308
+ /**
309
+ * 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
310
+ * 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
311
+ *
312
+ * 优先级:
313
+ * 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
314
+ * 2. 项目根探测(入口模块向上找 pnpm-workspace.yaml → cwd 向上找,见 resolveProjectRoot)
315
+ * 3. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
316
+ *
317
+ * 返回绝对路径。独立导出便于 cli / loader 装配链复用。
318
+ */
319
+ function resolveConfigPath(opts = {}) {
320
+ const explicit = node_process.default.env["NAPKETTO_CONFIG"];
321
+ if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
322
+ const projectRoot = resolveProjectRoot();
323
+ if (projectRoot !== void 0) return (0, node_path.join)(projectRoot, MAIN_CONFIG_FILE);
324
+ return (0, node_path.join)(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
325
+ }
326
+ /**
327
+ * 路径装配器:持有账号数据目录下的全部子目录。
328
+ * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
329
+ */
330
+ var PathWrapper = class {
331
+ /** 数据根(绝对路径)。 */
332
+ root;
333
+ /** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
334
+ accountDir;
335
+ /** config 目录(napuketto.json + 各协议配置)。 */
336
+ configDir;
337
+ /** logs 目录(pino 文件日志)。 */
338
+ logsDir;
339
+ /** cache 目录(临时文件、媒体缓存)。 */
340
+ cacheDir;
341
+ constructor(opts = {}) {
342
+ const root = resolveDataRoot(opts.dataRoot);
343
+ this.root = root;
344
+ if (opts.account) this.accountDir = (0, node_path.join)(root, opts.account);
345
+ else this.accountDir = root;
346
+ this.configDir = (0, node_path.join)(this.accountDir, "config");
347
+ this.logsDir = (0, node_path.join)(this.accountDir, "logs");
348
+ this.cacheDir = (0, node_path.join)(this.accountDir, "cache");
349
+ }
350
+ /** 递归创建全部目录,启动装配时调用一次。 */
351
+ ensure() {
352
+ for (const dir of [
353
+ this.configDir,
354
+ this.logsDir,
355
+ this.cacheDir
356
+ ]) (0, node_fs.mkdirSync)(dir, { recursive: true });
357
+ }
358
+ /** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
359
+ file(...segments) {
360
+ return (0, node_path.join)(this.accountDir, ...segments);
361
+ }
362
+ /**
363
+ * 清空缓存目录(clean_cache 动作经注入回调消费)。
364
+ * 只删除 cacheDir 下的条目,保留目录本身。
365
+ */
366
+ clearCache() {
367
+ let entries;
368
+ try {
369
+ entries = (0, node_fs.readdirSync)(this.cacheDir);
370
+ } catch {
371
+ return;
372
+ }
373
+ for (const entry of entries) (0, node_fs.rmSync)((0, node_path.join)(this.cacheDir, entry), {
374
+ recursive: true,
375
+ force: true
376
+ });
377
+ }
378
+ };
379
+ //#endregion
380
+ //#region src/apis/result.ts
381
+ /**
382
+ * 原生调用结果解包(ADR-009 统一错误语义)
383
+ *
384
+ * wrapper.node 的 service 方法大多返回 `{ result, errMsg }`:
385
+ * - unwrap:result 为 number 且非 0 → 抛 KernelError
386
+ * - checkLooseResult:宽松形状(result 可能缺失/非数字)→ 仅当 result 为
387
+ * 数字且非 0 时抛错(探测期兼容:某些 API 成功时 result 缺省或为对象)
388
+ * - unwrapResult:result !== 0 → 按 errMsg 内容映射语义错误码(msg 系专用)
389
+ */
390
+ /** errMsg 关键词 → 错误码规则(顺序即优先级,命中任一组关键词即映射)。 */
391
+ const RESULT_CODE_RULES = [
392
+ {
393
+ code: "NOT_LOGIN",
394
+ keywords: ["未登录", "login"]
395
+ },
396
+ {
397
+ code: "PERMISSION_DENIED",
398
+ keywords: ["无权限", "permission"]
399
+ },
400
+ {
401
+ code: "NOT_FOUND",
402
+ keywords: ["不存在", "not found"]
403
+ }
404
+ ];
405
+ /** 按 errMsg 关键词映射错误码(未命中 → UNKNOWN)。 */
406
+ function mapResultCode(msg) {
407
+ for (const rule of RESULT_CODE_RULES) if (rule.keywords.some((k) => msg.includes(k))) return rule.code;
408
+ return "UNKNOWN";
409
+ }
410
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
411
+ function unwrap(label, result, errMsg) {
412
+ if (result === 0) return;
413
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
414
+ }
415
+ /** 宽松 result 校验(result 为数字非 0 视为失败;缺失/非数字视为成功)。 */
416
+ function checkLooseResult(label, res) {
417
+ if (res === void 0 || res === null || typeof res.result !== "number" || res.result === 0) return;
418
+ throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
419
+ }
420
+ /** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
421
+ function unwrapResult(label, raw) {
422
+ if (raw.result === 0) return;
423
+ const msg = raw.errMsg || "无错误详情";
424
+ let code = mapResultCode(msg);
425
+ if (code === "UNKNOWN" && label === "sendMsg") code = "SEND_FAILED";
426
+ throw kernelError(`${label} 失败: ${msg}`, code);
427
+ }
428
+ //#endregion
429
+ //#region src/apis/friend.ts
430
+ /**
431
+ * FriendApi:好友语义化 API(ADR-009 统一错误语义)
432
+ *
433
+ * 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
434
+ * 方法面(P2-4 + P2-11):好友列表(uid + 昵称/备注)+ 好友请求
435
+ * (getBuddyReq / approvalFriendRequest / setBuddyRemark / delBuddy /
436
+ * getDoubtBuddyReq / approvalDoubtBuddyReq)。
437
+ */
438
+ /** 好友 API:从 session 拿 buddy service,包装成语义化方法。 */
439
+ var FriendApi = class {
440
+ service;
441
+ uidToUin;
442
+ constructor(session, opts = {}) {
443
+ const service = session.getBuddyService();
444
+ if (service === null || service === void 0) throw kernelError("getBuddyService() 返回空(session 未 init)", "INVALID_STATE");
445
+ this.service = service;
446
+ this.uidToUin = opts.uidToUin;
447
+ }
448
+ /** 好友列表(分类拍平为 Friend[];昵称/备注从 service 缓存读取)。 */
449
+ async getFriendList() {
450
+ let categories;
451
+ try {
452
+ categories = await this.service.getBuddyListFromCache(0);
453
+ } catch {
454
+ const raw = await this.service.getBuddyListV2("cli", true, 0);
455
+ if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
456
+ categories = raw.data;
457
+ }
458
+ const uids = categories.flatMap((cat) => cat.buddyUids);
459
+ let uinMap = /* @__PURE__ */ new Map();
460
+ if (this.uidToUin !== void 0) uinMap = await this.uidToUin(uids);
461
+ const out = [];
462
+ for (const uid of uids) out.push({
463
+ uid,
464
+ uin: uinMap.get(uid) ?? uid,
465
+ nickname: this.service.getBuddyNick(uid),
466
+ remark: this.service.getBuddyRemark(uid)
467
+ });
468
+ return out;
469
+ }
470
+ /** 带分类的好友列表(get_friends_with_category,分类保留)。 */
471
+ async getFriendCategories() {
472
+ const categories = await this.getBuddyCategories();
473
+ const friendMap = /* @__PURE__ */ new Map();
474
+ for (const friend of await this.getFriendList()) friendMap.set(friend.uid, friend);
475
+ return categories.map((cat) => {
476
+ const buddies = [];
477
+ for (const uid of cat.buddyUids) {
478
+ const friend = friendMap.get(uid);
479
+ if (friend !== void 0) buddies.push(friend);
480
+ }
481
+ return {
482
+ categoryId: cat.categoryId,
483
+ categorySortId: cat.categorySortId,
484
+ categoryName: cat.categroyName,
485
+ categoryMbCount: cat.categroyMbCount,
486
+ onlineCount: cat.onlineCount,
487
+ buddyList: buddies
488
+ };
489
+ });
490
+ }
491
+ /** 原始分类(getBuddyListV2 / 缓存)。 */
492
+ async getBuddyCategories() {
493
+ try {
494
+ return await this.service.getBuddyListFromCache(0);
495
+ } catch {
496
+ const raw = await this.service.getBuddyListV2("cli", true, 0);
497
+ if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
498
+ return raw.data;
499
+ }
500
+ }
501
+ /** 判断是否好友。 */
502
+ isBuddy(uid) {
503
+ return this.service.isBuddy(uid);
504
+ }
505
+ /** 好友申请列表(set_friend_add_request 应答前查找匹配项)。 */
506
+ async getBuddyReqList() {
507
+ const raw = await this.service.getBuddyReq();
508
+ unwrap("getBuddyReq", raw.result, raw.errMsg);
509
+ return raw.buddyReqs ?? [];
510
+ }
511
+ /** 应答好友申请(accept=true 同意;remark 由调用方另行 setFriendRemark)。 */
512
+ async handleFriendRequest(notify, accept) {
513
+ await this.service.approvalFriendRequest({
514
+ friendUid: notify.friendUid,
515
+ reqTime: notify.reqTime,
516
+ accept
517
+ });
518
+ }
519
+ /** 设置好友备注(void 语义乐观处理)。 */
520
+ setFriendRemark(uid, remark) {
521
+ this.service.setBuddyRemark({
522
+ uid,
523
+ remark
524
+ });
525
+ }
526
+ /** 删除好友(tempBlock=拉黑,tempBothDel=双向删除)。 */
527
+ async deleteFriend(uid, tempBlock = false, tempBothDel = false) {
528
+ checkLooseResult("delBuddy", await this.service.delBuddy({
529
+ friendUid: uid,
530
+ tempBlock,
531
+ tempBothDel
532
+ }));
533
+ }
534
+ /** 可疑好友申请列表(get_doubt_friends_add_request;uin 经 uidToUin 转换)。 */
535
+ async getDoubtFriendRequest(count) {
536
+ const reqId = String(Date.now());
537
+ const raw = await this.service.getDoubtBuddyReq(reqId, count, "");
538
+ unwrap("getDoubtBuddyReq", raw.result, raw.errMsg);
539
+ const list = raw.doubtList ?? [];
540
+ let uinMap = /* @__PURE__ */ new Map();
541
+ if (this.uidToUin !== void 0 && list.length > 0) uinMap = await this.uidToUin(list.map((item) => item.uid));
542
+ const out = [];
543
+ for (const item of list) out.push(this.toDoubtFriendRequestInfo(item, uinMap));
544
+ return out;
545
+ }
546
+ /** 单项转换:DoubtBuddyReq → OB11 结构(可选字段条件赋值,exactOptionalPropertyTypes)。 */
547
+ toDoubtFriendRequestInfo(item, uinMap) {
548
+ return toDoubtFriendRequestInfo(item, uinMap);
549
+ }
550
+ /** 处理可疑好友申请(set_doubt_friends_add_request;uid=flag)。 */
551
+ handleDoubtFriendRequest(uid) {
552
+ this.service.approvalDoubtBuddyReq(uid, "", "");
553
+ }
554
+ };
555
+ /** 可选字段条件赋值(undefined 跳过,兼容 exactOptionalPropertyTypes)。 */
556
+ function assignOptional(info, key, value) {
557
+ if (value !== void 0) info[key] = value;
558
+ }
559
+ /** 单项转换:DoubtBuddyReq → OB11 结构(导出供单测,friend.test.ts)。 */
560
+ function toDoubtFriendRequestInfo(item, uinMap) {
561
+ const info = {
562
+ flag: item.uid,
563
+ uin: Number(uinMap.get(item.uid) ?? item.uid),
564
+ type: "doubt"
565
+ };
566
+ assignOptional(info, "nick", item.nick);
567
+ assignOptional(info, "source", item.source);
568
+ assignOptional(info, "reason", item.reason);
569
+ assignOptional(info, "msg", item.msg);
570
+ assignOptional(info, "group_code", item.groupCode);
571
+ assignOptional(info, "time", item.reqTime);
572
+ return info;
573
+ }
574
+ //#endregion
575
+ //#region src/types/entities.ts
576
+ /**
577
+ * 实体类型层(运行时探测产物 + 公开资料作说明书,ADR-006)
578
+ *
579
+ * ⚠️ 部分字段按公开资料(wrapper 外部契约)自研描述,待下一次进程内
580
+ * 探测(probe.ts)校准——接口签名是外部系统事实,自研描述其形状。
581
+ */
582
+ /** 会话类型(QQ wrapper 外部契约)。 */
583
+ const ChatType = {
584
+ C2C: 1,
585
+ GROUP: 2,
586
+ DISC: 3,
587
+ GUILD: 4,
588
+ C2C_TEMP: 100
589
+ };
590
+ /** 灰色提示子类型(系统事件类型,QQ wrapper 契约)。 */
591
+ const GrayTipSubType = {
592
+ REVOKE: 1,
593
+ PROCLAMATION: 2,
594
+ GROUP: 4,
595
+ BUDDY: 5,
596
+ FEED: 6,
597
+ ESSENCE: 7,
598
+ GROUP_NOTIFY: 8,
599
+ BUDDY_NOTIFY: 9,
600
+ FILE: 10,
601
+ JSON: 17
602
+ };
603
+ /** 群成员变动类型(TipGroupElement.type)。 */
604
+ const TipGroupElementType = {
605
+ MEMBER_ADD: 1,
606
+ DISBANDED: 2,
607
+ QUIT: 3,
608
+ CREATED: 4,
609
+ GROUP_NAME_MODIFIED: 5,
610
+ BLOCK: 6,
611
+ UNBLOCK: 7,
612
+ SHUT_UP: 8
613
+ };
614
+ //#endregion
615
+ //#region src/types/listeners/group.ts
616
+ /** 群列表更新类型(说明书参考,值待探测校准)。 */
617
+ const GroupListUpdateType = {
618
+ /** 全量更新(groupList 为完整列表)。 */
619
+ ALL: 0,
620
+ /** 增量更新(合并到现有缓存)。 */
621
+ PARTIAL: 1
622
+ };
623
+ //#endregion
624
+ //#region src/types/services/msg-service.ts
625
+ /** 消息元素类型枚举(QQ wrapper 外部契约,自研描述)。 */
626
+ const ElementType = {
627
+ UNKNOWN: 0,
628
+ TEXT: 1,
629
+ PIC: 2,
630
+ FILE: 3,
631
+ PTT: 4,
632
+ VIDEO: 5,
633
+ FACE: 6,
634
+ REPLY: 7,
635
+ GRAY_TIP: 8,
636
+ WALLET: 9,
637
+ ARK: 10,
638
+ MFACE: 11,
639
+ LIVE_GIFT: 12,
640
+ STRUCT_LONG_MSG: 13,
641
+ MARKDOWN: 14,
642
+ GIPHY: 15,
643
+ MULTI_FORWARD: 16,
644
+ INLINE_KEYBOARD: 17,
645
+ INTEXT_GIFT: 18,
646
+ CALENDAR: 19
647
+ };
648
+ //#endregion
649
+ //#region src/types/message-element.ts
650
+ /**
651
+ * canonical 消息元素模型(协议无关,ADR-008 延伸)
652
+ *
653
+ * 描述 QQ 消息的事实结构,是各协议翻译层的公共中间表示:
654
+ * kernel 只做一次 NT ↔ canonical(toCanonicalElements / toSendElements),
655
+ * 各协议(onebot11/satori)只写薄映射 canonical ↔ 协议格式。
656
+ */
657
+ /** 文本 at 类型(QQ wrapper 契约,自研描述)。 */
658
+ const AtType = {
659
+ ALL: 1,
660
+ ONE: 2,
661
+ ME: 4
662
+ };
663
+ /** TEXT 元素 → canonical(at 或纯文本)。 */
664
+ function textToCanonical(textEl) {
665
+ if (textEl === void 0) return {
666
+ type: "text",
667
+ text: ""
668
+ };
669
+ const { atType, atUid, content } = textEl;
670
+ if (atType === AtType.ALL) return {
671
+ type: "at",
672
+ target: "all"
673
+ };
674
+ if (atType === AtType.ONE || atType === AtType.ME) {
675
+ const base = {
676
+ type: "at",
677
+ target: atUid ?? content
678
+ };
679
+ const display = content.startsWith("@") ? content.slice(1) : content;
680
+ if (display === "") return base;
681
+ return {
682
+ ...base,
683
+ display
684
+ };
685
+ }
686
+ return {
687
+ type: "text",
688
+ text: content
689
+ };
690
+ }
691
+ /** PIC 元素 → canonical image。 */
692
+ function picToCanonical(pic) {
693
+ if (pic === void 0) return {
694
+ type: "image",
695
+ path: ""
696
+ };
697
+ const path = pic.picPath ?? pic.picUrl ?? "";
698
+ const url = pic.picUrl;
699
+ if (url === void 0) return {
700
+ type: "image",
701
+ path
702
+ };
703
+ return {
704
+ type: "image",
705
+ path,
706
+ url
707
+ };
708
+ }
709
+ /** FACE 元素 → canonical face。 */
710
+ function faceToCanonical(face) {
711
+ if (face === void 0) return {
712
+ type: "face",
713
+ id: "0"
714
+ };
715
+ return {
716
+ type: "face",
717
+ id: String(face.faceIndex)
718
+ };
719
+ }
720
+ /** PTT 元素 → canonical voice。 */
721
+ function pttToCanonical(ptt) {
722
+ if (ptt === void 0) return {
723
+ type: "voice",
724
+ path: ""
725
+ };
726
+ return {
727
+ type: "voice",
728
+ path: ptt.filePath ?? ""
729
+ };
730
+ }
731
+ /** VIDEO 元素 → canonical video。 */
732
+ function videoToCanonical(video) {
733
+ if (video === void 0) return {
734
+ type: "video",
735
+ path: ""
736
+ };
737
+ const path = video.filePath ?? video.videoUrl ?? "";
738
+ const url = video.videoUrl;
739
+ if (url === void 0) return {
740
+ type: "video",
741
+ path
742
+ };
743
+ return {
744
+ type: "video",
745
+ path,
746
+ url
747
+ };
748
+ }
749
+ /** FILE 元素 → canonical file。 */
750
+ function fileToCanonical(file) {
751
+ if (file === void 0) return {
752
+ type: "file",
753
+ path: ""
754
+ };
755
+ const path = file.filePath ?? "";
756
+ const name = file.fileName;
757
+ if (name === void 0) return {
758
+ type: "file",
759
+ path
760
+ };
761
+ return {
762
+ type: "file",
763
+ path,
764
+ name
765
+ };
766
+ }
767
+ /** REPLY 元素 → canonical reply。 */
768
+ function replyToCanonical(reply) {
769
+ if (reply === void 0) return {
770
+ type: "reply",
771
+ messageId: ""
772
+ };
773
+ return {
774
+ type: "reply",
775
+ messageId: reply.replayMsgId
776
+ };
777
+ }
778
+ /** 单个 NT 元素 → canonical(接收方向,宽容:无法表达回 unknown,不抛错)。 */
779
+ function toCanonicalElement(el) {
780
+ switch (el.elementType) {
781
+ case ElementType.TEXT: return textToCanonical(el.textElement);
782
+ case ElementType.PIC: return picToCanonical(el.picElement);
783
+ case ElementType.FACE: return faceToCanonical(el.faceElement);
784
+ case ElementType.PTT: return pttToCanonical(el.pttElement);
785
+ case ElementType.VIDEO: return videoToCanonical(el.videoElement);
786
+ case ElementType.FILE: return fileToCanonical(el.fileElement);
787
+ case ElementType.REPLY: return replyToCanonical(el.replyElement);
788
+ default: return {
789
+ type: "unknown",
790
+ raw: el
791
+ };
792
+ }
793
+ }
794
+ /** NT RawMessage → canonical 元素(与 toSendElements 对称,接收方向)。 */
795
+ function toCanonicalElements(msg) {
796
+ return msg.elements.map(toCanonicalElement);
797
+ }
798
+ /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心五类)。 */
799
+ function toSendElements(elements) {
800
+ const out = [];
801
+ for (const el of elements) switch (el.type) {
802
+ case "text":
803
+ out.push({
804
+ elementType: ElementType.TEXT,
805
+ textElement: { content: el.text }
806
+ });
807
+ break;
808
+ case "at":
809
+ if (el.target === "all") out.push({
810
+ elementType: ElementType.TEXT,
811
+ textElement: {
812
+ content: el.display ?? "@",
813
+ atType: 1
814
+ }
815
+ });
816
+ else out.push({
817
+ elementType: ElementType.TEXT,
818
+ textElement: {
819
+ content: el.display ?? "@",
820
+ atType: 2,
821
+ atUid: el.target
822
+ }
823
+ });
824
+ break;
825
+ case "face":
826
+ out.push({
827
+ elementType: ElementType.FACE,
828
+ faceElement: { faceIndex: Number(el.id) }
829
+ });
830
+ break;
831
+ case "image":
832
+ out.push({
833
+ elementType: ElementType.PIC,
834
+ picElement: { picPath: el.path }
835
+ });
836
+ break;
837
+ case "voice":
838
+ out.push({
839
+ elementType: ElementType.PTT,
840
+ pttElement: { filePath: el.path }
841
+ });
842
+ break;
843
+ case "reply":
844
+ out.push({
845
+ elementType: ElementType.REPLY,
846
+ replyElement: {
847
+ replayMsgId: el.messageId,
848
+ replayMsgSeq: el.messageId,
849
+ replayMsgTime: "0"
850
+ }
851
+ });
852
+ break;
853
+ case "video":
854
+ case "file":
855
+ case "forward":
856
+ case "json":
857
+ case "xml":
858
+ case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
859
+ }
860
+ return out;
861
+ }
862
+ //#endregion
863
+ //#region src/types/services/group-service.ts
864
+ /** 群成员角色(QQ wrapper 契约)。 */
865
+ const NTGroupMemberRole = {
866
+ UNSPECIFIED: 0,
867
+ STRANGER: 1,
868
+ MEMBER: 2,
869
+ ADMIN: 3,
870
+ OWNER: 4
871
+ };
872
+ /** 群通知类型(GroupNotifyMsgType,说明书参考)。 */
873
+ const GroupNotifyMsgType = {
874
+ UN_SPECIFIED: 0,
875
+ INVITED_BY_MEMBER: 1,
876
+ REFUSE_INVITED: 2,
877
+ REFUSED_BY_ADMINI_STRATOR: 3,
878
+ AGREED_TOJOIN_DIRECT: 4,
879
+ INVITED_NEED_ADMINI_STRATOR_PASS: 5,
880
+ AGREED_TO_JOIN_BY_ADMINI_STRATOR: 6,
881
+ REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS: 7,
882
+ SET_ADMIN: 8,
883
+ KICK_MEMBER_NOTIFY_ADMIN: 9,
884
+ KICK_MEMBER_NOTIFY_KICKED: 10,
885
+ MEMBER_LEAVE_NOTIFY_ADMIN: 11,
886
+ CANCEL_ADMIN_NOTIFY_CANCELED: 12,
887
+ CANCEL_ADMIN_NOTIFY_ADMIN: 13,
888
+ TRANSFER_GROUP_NOTIFY_OLDOWNER: 14,
889
+ TRANSFER_GROUP_NOTIFY_ADMIN: 15
890
+ };
891
+ /** 群通知处理状态(GroupNotifyMsgStatus,说明书参考)。 */
892
+ const GroupNotifyMsgStatus = {
893
+ KINIT: 0,
894
+ KUNHANDLE: 1,
895
+ KAGREED: 2,
896
+ KREFUSED: 3,
897
+ KIGNORED: 4
898
+ };
899
+ /** 群请求操作类型(NTGroupRequestOperateTypes,说明书参考)。 */
900
+ const NTGroupRequestOperateTypes = {
901
+ KUNSPECIFIED: 0,
902
+ KAGREE: 1,
903
+ KREFUSE: 2,
904
+ KIGNORE: 3,
905
+ KDELETE: 4
906
+ };
907
+ //#endregion
908
+ //#region src/types/wrapper.ts
909
+ /**
910
+ * wrapper.node 的 NAPI 接口面(自研描述,非移植)
911
+ *
912
+ * 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
913
+ * 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
914
+ * 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
915
+ *
916
+ * 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
917
+ */
918
+ /** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
919
+ const PlatformType = {
920
+ KUNKNOWN: 0,
921
+ KANDROID: 1,
922
+ KIOS: 2,
923
+ KWINDOWS: 3,
924
+ KMAC: 4,
925
+ KLINUX: 5
926
+ };
927
+ /** 厂商类型(deviceInfo.vendorType)。 */
928
+ const VendorType = {
929
+ KNOSETONIOS: 0,
930
+ KUNSUPPORTANDROIDPUSH: 1,
931
+ KSUPPORTTPNS: 2,
932
+ KSUPPORTHMS: 3,
933
+ KSUPPORTOPPOPUSH: 4,
934
+ KSUPPORTVIVOPUSH: 5,
935
+ KSUPPORTGOOGLEPUSH: 99
936
+ };
937
+ //#endregion
938
+ //#region src/apis/group.ts
939
+ /**
940
+ * GroupApi:群语义化 API(ADR-009 统一错误语义)
941
+ *
942
+ * 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
943
+ * 方法面(P2-4 + P2-10):群列表 / 群详情 / 成员列表 / 成员详情 / uin↔uid +
944
+ * 群管操作(踢人 / 禁言 / 管理员 / 名片 / 群名 / 退群 / 精华消息 / @all 剩余)。
945
+ */
946
+ /** 群 API:从 session 拿 group service,包装成语义化方法。 */
947
+ var GroupApi = class {
948
+ service;
949
+ /** 精华消息需要 msg service(按 msgId 拉消息取 msgSeq/msgRandom)。 */
950
+ msgService;
951
+ constructor(session) {
952
+ const service = session.getGroupService();
953
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
954
+ this.service = service;
955
+ const msgService = session.getMsgService();
956
+ if (msgService === null || msgService === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
957
+ this.msgService = msgService;
958
+ }
959
+ /**
960
+ * 群列表(force=true 触发原生刷新)。
961
+ *
962
+ * ⚠️ 实测校准(2026-08-08 端到端联调):原生 getGroupList 返回仅
963
+ * `{ result, errMsg }`——**列表数据经 onGroupListUpdate 事件推送**,
964
+ * 不走返回值。消费群列表一律从 `GroupCache.listGroups()` 读(缓存由
965
+ * 事件主动维护);本方法仅作「触发刷新」用,返回值恒为 []。
966
+ */
967
+ async getGroupList(force = false) {
968
+ const raw = await this.service.getGroupList(force);
969
+ if (raw.result !== 0) unwrap("getGroupList", raw.result, raw.errMsg);
970
+ return [];
971
+ }
972
+ /** 群详情(groupCode 为群号字符串)。
973
+ * getGroupDetailInfo 返回形状:失败 result=错误码;成功详情可能直接在 result
974
+ * 对象里,也可能在 groupInfo/detailInfo 字段——三种形状都兼容(探测校准)。
975
+ * 2026-08-07 修复:成功(result=0)时不再把 {result:0,errMsg} 原样当详情返回,
976
+ * 而是提取详情字段;形状异常抛 UNKNOWN 而非静默返回垃圾数据。 */
977
+ async getGroupInfo(groupCode) {
978
+ const raw = await this.service.getGroupDetailInfo(groupCode, 0);
979
+ const { result, errMsg } = raw;
980
+ if (typeof result === "number" && result !== 0) unwrap("getGroupDetailInfo", result, errMsg);
981
+ const detail = extractGroupDetail(result, raw);
982
+ if (detail === null) throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
983
+ return detail;
984
+ }
985
+ /** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
986
+ async getGroupMemberList(groupCode, forceFetch = false) {
987
+ const raw = await this.service.getAllMemberList(groupCode, forceFetch);
988
+ if (raw.errCode !== 0) throw kernelError(`getAllMemberList 失败: ${raw.errMsg}`, "UNKNOWN");
989
+ return [...raw.result.infos.values()];
990
+ }
991
+ /** 群成员详情(uids 为空返回空数组)。 */
992
+ async getGroupMemberInfo(groupCode, uids) {
993
+ if (uids.length === 0) return [];
994
+ const raw = await this.service.getMemberInfo(groupCode, uids, true);
995
+ unwrap("getMemberInfo", raw.result, raw.errMsg);
996
+ const { infos } = raw;
997
+ if (infos instanceof Map) return [...infos.values()];
998
+ return [];
999
+ }
1000
+ /** uin → uid(私聊发送等需要)。 */
1001
+ async uinToUid(uins) {
1002
+ if (uins.length === 0) return /* @__PURE__ */ new Map();
1003
+ const raw = await this.service.getUidByUins(uins);
1004
+ if (raw.errCode !== 0) throw kernelError(`getUidByUins 失败: ${raw.errMsg}`, "UNKNOWN");
1005
+ return raw.uids;
1006
+ }
1007
+ /** uid → uin。 */
1008
+ async uidToUin(uids) {
1009
+ if (uids.length === 0) return /* @__PURE__ */ new Map();
1010
+ const raw = await this.service.getUinByUids(uids);
1011
+ if (raw.errCode !== 0) throw kernelError(`getUinByUids 失败: ${raw.errMsg}`, "UNKNOWN");
1012
+ return raw.uins;
1013
+ }
1014
+ /** 群成员角色 → OB11 role 字符串。 */
1015
+ static roleToString(role) {
1016
+ if (role === NTGroupMemberRole.OWNER) return "owner";
1017
+ if (role === NTGroupMemberRole.ADMIN) return "admin";
1018
+ return "member";
1019
+ }
1020
+ /**
1021
+ * 踢人(set_group_kick)。memberUids 为成员 uid;refuseForever=true 拉黑拒绝再次入群。
1022
+ * kickMemberV2 的 kickFlag/kickList 字段值待探测校准(TODO P3 联调)。
1023
+ */
1024
+ async kickMember(groupCode, memberUids, refuseForever, reason = "") {
1025
+ if (memberUids.length === 0) throw kernelError("kickMember 需要至少一个成员 uid", "INVALID_PARAM");
1026
+ let optFlag = 1;
1027
+ if (refuseForever) optFlag = 0;
1028
+ const req = {
1029
+ groupCode,
1030
+ kickFlag: 0,
1031
+ kickList: memberUids.map((uid) => ({
1032
+ optFlag,
1033
+ optOperate: 0,
1034
+ optMemberUid: uid,
1035
+ optBytesMsg: ""
1036
+ })),
1037
+ kickListUids: memberUids,
1038
+ kickMsg: reason
1039
+ };
1040
+ const raw = await this.service.kickMemberV2(req);
1041
+ unwrap("kickMemberV2", raw.result, raw.errMsg);
1042
+ }
1043
+ /** 成员禁言(duration 秒,0 解除禁言)。 */
1044
+ async setMemberShutUp(groupCode, members) {
1045
+ if (members.length === 0) throw kernelError("setMemberShutUp 需要至少一个成员", "INVALID_PARAM");
1046
+ const raw = await this.service.setMemberShutUp(groupCode, members.map((m) => ({
1047
+ uid: m.uid,
1048
+ timeStamp: m.duration
1049
+ })));
1050
+ unwrap("setMemberShutUp", raw.result, raw.errMsg);
1051
+ }
1052
+ /** 全员禁言(enable=true 开启全群禁言)。 */
1053
+ async setGroupShutUp(groupCode, enable) {
1054
+ const raw = await this.service.setGroupShutUp(groupCode, enable);
1055
+ unwrap("setGroupShutUp", raw.result, raw.errMsg);
1056
+ }
1057
+ /** 设置管理员(role=ADMIN 设管理,MEMBER 取消;void 语义乐观处理)。 */
1058
+ setMemberRole(groupCode, uid, role) {
1059
+ this.service.modifyMemberRole(groupCode, uid, role);
1060
+ }
1061
+ /** 设置群名片(void 语义乐观处理)。 */
1062
+ setMemberCardName(groupCode, uid, cardName) {
1063
+ this.service.modifyMemberCardName(groupCode, uid, cardName);
1064
+ }
1065
+ /** 修改群名。 */
1066
+ async modifyGroupName(groupCode, groupName) {
1067
+ const raw = await this.service.modifyGroupName(groupCode, groupName, false);
1068
+ unwrap("modifyGroupName", raw.result, raw.errMsg);
1069
+ }
1070
+ /** 退群(needDeleteLocalMsg=is_dismiss:是否解散群,仅群主)。 */
1071
+ async quitGroup(groupCode, needDeleteLocalMsg = false) {
1072
+ const raw = await this.service.quitGroupV2({
1073
+ groupCode,
1074
+ needDeleteLocalMsg
1075
+ });
1076
+ unwrap("quitGroupV2", raw.result, raw.errMsg);
1077
+ }
1078
+ /** 设置精华消息(msgId 经 msgService 取 msgSeq/msgRandom)。 */
1079
+ async addGroupEssence(groupCode, msgId) {
1080
+ await this.setEssence(groupCode, msgId, true);
1081
+ }
1082
+ /** 取消精华消息。 */
1083
+ async removeGroupEssence(groupCode, msgId) {
1084
+ await this.setEssence(groupCode, msgId, false);
1085
+ }
1086
+ /** 精华消息公共流程:按 msgId 拉消息 → add/removeGroupEssence。 */
1087
+ async setEssence(groupCode, msgId, add) {
1088
+ const peer = {
1089
+ chatType: ChatType.GROUP,
1090
+ peerUid: groupCode
1091
+ };
1092
+ const raw = await this.msgService.getMsgsByMsgId(peer, [msgId]);
1093
+ unwrap("getMsgsByMsgId", raw.result, raw.errMsg);
1094
+ const msg = raw.msgList?.[0];
1095
+ if (msg === void 0) throw kernelError(`消息 ${msgId} 不存在`, "NOT_FOUND");
1096
+ const param = {
1097
+ groupCode,
1098
+ msgRandom: Number(msg.msgRandom ?? 0),
1099
+ msgSeq: Number(msg.msgSeq)
1100
+ };
1101
+ let label = "removeGroupEssence";
1102
+ let res;
1103
+ if (add) {
1104
+ label = "addGroupEssence";
1105
+ res = await this.service.addGroupEssence(param);
1106
+ } else res = await this.service.removeGroupEssence(param);
1107
+ checkLooseResult(label, res);
1108
+ }
1109
+ /** @all 剩余次数(get_group_at_all_remain)。 */
1110
+ async getGroupRemainAtTimes(groupCode) {
1111
+ const raw = await this.service.getGroupRemainAtTimes(groupCode);
1112
+ if (raw.errCode !== 0) throw kernelError(`getGroupRemainAtTimes 失败: ${raw.errMsg}`, "UNKNOWN");
1113
+ return {
1114
+ canAtAll: raw.atInfo.canAtAll,
1115
+ remainAtAllCountForUin: raw.atInfo.remainAtAllCountForUin,
1116
+ remainAtAllCountForGroup: raw.atInfo.remainAtAllCountForGroup
1117
+ };
1118
+ }
1119
+ };
1120
+ /**
1121
+ * 从 getGroupDetailInfo 返回提取群详情(兼容三种形状):
1122
+ * - result 为对象 → 详情直接在其中(最常见)
1123
+ * - result 为 0/空 → 从 raw.groupInfo / detailInfo / info 提取
1124
+ * 提取不到返回 null(调用方抛 UNKNOWN)。导出供单测。
1125
+ */
1126
+ function extractGroupDetail(result, raw) {
1127
+ if (result !== null && typeof result === "object") return result;
1128
+ const detail = raw;
1129
+ const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
1130
+ if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
1131
+ return null;
1132
+ }
1133
+ //#endregion
1134
+ //#region src/apis/group-notify.ts
1135
+ /**
1136
+ * GroupNotifyApi:群通知/禁言列表语义化 API(ADR-009 统一错误语义,P2-13)
1137
+ *
1138
+ * get_group_system_msg / set_group_add_request / get_group_shut_list 共用。
1139
+ * getSingleScreenNotifies / getGroupShutUpMemberList 的列表可能走 listener 回调,
1140
+ * 直接调用返回形状待探测校准:兼容 result 数组 / 字段 / 直接数组。
1141
+ */
1142
+ /** 群通知 API:从 session 拿 group service,包装成语义化方法。 */
1143
+ var GroupNotifyApi = class {
1144
+ service;
1145
+ constructor(session) {
1146
+ const service = session.getGroupService();
1147
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
1148
+ this.service = service;
1149
+ }
1150
+ /** 单屏通知列表(get_group_system_msg / set_group_add_request 共用)。 */
1151
+ async getSingleScreenNotifies(doubt, count) {
1152
+ return extractNotifyList((await this.service.getSingleScreenNotifies(doubt, "", count)).result);
1153
+ }
1154
+ /** 禁言成员列表(get_group_shut_list)。 */
1155
+ async getGroupShutUpMemberList(groupCode) {
1156
+ const raw = await this.service.getGroupShutUpMemberList(groupCode);
1157
+ if (raw.result !== 0) unwrap("getGroupShutUpMemberList", raw.result, raw.errMsg);
1158
+ return extractMemberList(raw.result);
1159
+ }
1160
+ /** 处理群请求(set_group_add_request 应答;approve=true 同意 / false 拒绝)。 */
1161
+ async handleGroupRequest(doubt, notify, operateType, reason) {
1162
+ const { group, seq, type } = notify;
1163
+ let groupCode = "";
1164
+ if (group !== void 0) ({groupCode} = group);
1165
+ let postscript = reason;
1166
+ if (postscript === "") postscript = " ";
1167
+ await this.service.operateSysNotify(doubt, {
1168
+ operateType,
1169
+ targetMsg: {
1170
+ seq,
1171
+ type,
1172
+ groupCode,
1173
+ postscript
1174
+ }
1175
+ });
1176
+ }
1177
+ };
1178
+ /** 从 getSingleScreenNotifies 返回提取通知数组(兼容 result 数组 / notifies 字段)。 */
1179
+ function extractNotifyList(result) {
1180
+ if (Array.isArray(result)) return result;
1181
+ if (result !== null && typeof result === "object") {
1182
+ const obj = result;
1183
+ if (Array.isArray(obj.notifies)) return obj.notifies;
1184
+ if (Array.isArray(obj.groupNotifies)) return obj.groupNotifies;
1185
+ }
1186
+ return [];
1187
+ }
1188
+ /** 从 getGroupShutUpMemberList 返回提取成员数组(兼容 result 数组 / memberList 字段)。 */
1189
+ function extractMemberList(result) {
1190
+ if (Array.isArray(result)) return result;
1191
+ if (result !== null && typeof result === "object") {
1192
+ const obj = result;
1193
+ if (Array.isArray(obj.memberList)) return obj.memberList;
1194
+ if (Array.isArray(obj.list)) return obj.list;
1195
+ }
1196
+ return [];
1197
+ }
1198
+ //#endregion
1199
+ //#region src/apis/msg.ts
1200
+ /**
1201
+ * MsgApi:消息语义化 API(ADR-009 统一错误语义)
1202
+ *
1203
+ * 内部解包原生 `{ result, errMsg }`:成功返回纯业务值,失败抛 KernelError。
1204
+ * 协议层只维护 `KernelErrorCode → 协议错误码` 映射表,不解析错误逻辑。
1205
+ *
1206
+ * 方法面(P2-1):发送 / 撤回 / 拉历史 / 标记已读。group/friend 等后续 apis 同构。
1207
+ */
1208
+ /** 发送状态(onMsgInfoListUpdate 事件 msg.sendStatus)。 */
1209
+ const SEND_STATUS = {
1210
+ FAILED: 0,
1211
+ SENDING: 1,
1212
+ SUCCESS: 2,
1213
+ SUCCESS_NO_SEQ: 3
1214
+ };
1215
+ /** 发送确认超时(毫秒)。 */
1216
+ const SEND_CONFIRM_TIMEOUT_MS = 15e3;
1217
+ /** 读文件 stat(富媒体发送预处理;文件不存在抛 INVALID_PARAM)。 */
1218
+ async function statFile(path) {
1219
+ try {
1220
+ return await (0, node_fs_promises.stat)(path);
1221
+ } catch {
1222
+ throw kernelError(`图片文件不存在: ${path}`, "INVALID_PARAM");
1223
+ }
1224
+ }
1225
+ /** 计算文件 md5(十六进制小写)。 */
1226
+ async function hashFile(path) {
1227
+ try {
1228
+ const buf = await (0, node_fs_promises.readFile)(path);
1229
+ return (0, node_crypto.createHash)("md5").update(buf).digest("hex");
1230
+ } catch {
1231
+ throw kernelError(`图片文件读取失败: ${path}`, "INVALID_PARAM");
1232
+ }
1233
+ }
1234
+ /** 消息 API:从 session 拿 msg service,包装成语义化方法。 */
1235
+ var MsgApi = class {
1236
+ service;
1237
+ /** 消息事件通道(sendMsg 后等 onMsgInfoListUpdate 确认发送结果)。 */
1238
+ channel;
1239
+ /** NodeQQNTWrapperUtil(富媒体发送 copyFile 用;session 拿不到时可为 null)。 */
1240
+ util;
1241
+ /** 上次生成 msgId 的时间(单调递增,2026-08-07 防同毫秒并发碰撞)。 */
1242
+ lastMsgTime = 0;
1243
+ constructor(session, channel, util) {
1244
+ const service = session.getMsgService();
1245
+ if (service === null || service === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
1246
+ this.service = service;
1247
+ this.channel = channel ?? null;
1248
+ this.util = util !== null && util !== void 0 ? util : null;
1249
+ }
1250
+ /**
1251
+ * 生成 msgId 时间戳(单调递增)。
1252
+ * generateMsgUniqueId(chatType, time) 以 time 区分消息——同毫秒并发发送
1253
+ * (机器人群发/多会话同时回复)Date.now() 会碰撞,msgId 相同导致 wrapper
1254
+ * 拒绝或覆盖。严格单调递增保证进程内唯一。
1255
+ */
1256
+ nextMsgTime() {
1257
+ const now = Date.now();
1258
+ if (now > this.lastMsgTime) this.lastMsgTime = now;
1259
+ else this.lastMsgTime += 1;
1260
+ return String(this.lastMsgTime);
1261
+ }
1262
+ /**
1263
+ * 发送消息:canonical 元素 → NT 发送元素 → sendMsg(NapCat 式)。
1264
+ * 返回 NT msgId(雪花 ID)。
1265
+ *
1266
+ * 2026-08-11 修复(NapCat 式,实测):
1267
+ * - sendMsg 第一参传 '0'(固定),msgId 塞 peer.guildId —— 传 msgId 作第一参
1268
+ * 时 wrapper 返回 result=5(失败),NapCat 同款调用返回 result=0。
1269
+ * - 发送结果以 onMsgInfoListUpdate 事件确认(sendStatus===2 成功):sendMsg
1270
+ * 返回值 result 可能为 5 但事件仍成功(异步确认),反之亦然。
1271
+ * - ⚠️ 必须先注册事件监听再调 sendMsg(事件可能在 sendMsg 返回前就触发)。
1272
+ */
1273
+ async sendMessage(target, elements) {
1274
+ const sendElements = await this.prepareSendElements(elements);
1275
+ const msgId = this.service.generateMsgUniqueId(target.chatType, this.nextMsgTime());
1276
+ const sendPeer = {
1277
+ ...target,
1278
+ guildId: msgId
1279
+ };
1280
+ if (this.channel === null) {
1281
+ unwrapResult("sendMsg", await this.service.sendMsg("0", sendPeer, sendElements, /* @__PURE__ */ new Map()));
1282
+ return { msgId };
1283
+ }
1284
+ const confirm = this.confirmSend(msgId, target);
1285
+ await this.service.sendMsg("0", sendPeer, sendElements, /* @__PURE__ */ new Map());
1286
+ await confirm;
1287
+ return { msgId };
1288
+ }
1289
+ /**
1290
+ * canonical 元素 → 发送元素,富媒体(PIC/PTT)做 NapCat 式预处理。
1291
+ * 2026-08-11 修复(实测):图片发送必须 elementType=2(PIC)+ 完整 picElement
1292
+ * (md5HexStr/fileSize/fileName/sourcePath),且文件须经 getRichMediaFilePathForGuild
1293
+ * 计算目标路径 + util.copyFile 放置——缺任一环节 sendMsg 返回 rich media transfer failed。
1294
+ * 2026-08-12(语音):PTT 同理——只给 filePath 触发 wrapper 内部
1295
+ * "Cannot convert undefined or null to object"(缺字段转换失败)。
1296
+ */
1297
+ async prepareSendElements(elements) {
1298
+ const out = [];
1299
+ for (const el of elements) switch (el.type) {
1300
+ case "image":
1301
+ out.push(await this.prepareImageElement(el.path));
1302
+ break;
1303
+ case "voice":
1304
+ out.push(await this.preparePttElement(el.path));
1305
+ break;
1306
+ default: out.push(...toSendElements([el]));
1307
+ }
1308
+ return out;
1309
+ }
1310
+ /** PIC 元素 NapCat 式预处理:md5 → 目标路径 → copyFile → 完整 picElement。 */
1311
+ async prepareImageElement(path) {
1312
+ const file = await statFile(path);
1313
+ const md5 = await hashFile(path);
1314
+ const fileName = (0, node_path.basename)(path);
1315
+ const relPath = await this.placeMediaFile(path, ElementType.PIC, md5, fileName);
1316
+ return {
1317
+ elementType: ElementType.PIC,
1318
+ elementId: "",
1319
+ picElement: {
1320
+ md5HexStr: md5,
1321
+ fileSize: String(file.size),
1322
+ picWidth: 0,
1323
+ picHeight: 0,
1324
+ fileName,
1325
+ sourcePath: relPath,
1326
+ original: true,
1327
+ picType: 1e3,
1328
+ picSubType: 0,
1329
+ fileUuid: "",
1330
+ fileSubId: "",
1331
+ thumbFileSize: 0,
1332
+ summary: ""
1333
+ }
1334
+ };
1335
+ }
1336
+ /**
1337
+ * PTT 元素 NapCat 式预处理:md5 → 目标路径 → copyFile → 完整 pttElement。
1338
+ *
1339
+ * 2026-08-12(实测依据):只给 filePath 时 wrapper 内部抛
1340
+ * "Cannot convert undefined or null to object"(缺 md5HexStr/fileSize 等字段),
1341
+ * 且发送后进程崩溃重启(supervisor 自动拉起)。完整字段(NapCat 同款,
1342
+ * formatType/voiceType/canConvert2Text/waveAmplitudes 等)消除该错误。
1343
+ *
1344
+ * ⚠️ silk 格式:QQ 语音协议为 silk v3。传入非 silk(ogg/mp3 等)时发送器
1345
+ * 可能拒绝上传(待实测)。转换点预留:此处直接放置原文件,后续接入
1346
+ * ogg → PCM → silk 编码器(外部 ffmpeg + silk-wasm,见 docs/design.md)。
1347
+ */
1348
+ async preparePttElement(path) {
1349
+ const file = await statFile(path);
1350
+ const md5 = await hashFile(path);
1351
+ const fileName = (0, node_path.basename)(path);
1352
+ const relPath = await this.placeMediaFile(path, ElementType.PTT, md5, fileName);
1353
+ const duration = Math.max(1, Math.floor(file.size / 1024 / 3));
1354
+ return {
1355
+ elementType: ElementType.PTT,
1356
+ elementId: "",
1357
+ pttElement: {
1358
+ fileName,
1359
+ filePath: relPath,
1360
+ md5HexStr: md5,
1361
+ fileSize: String(file.size),
1362
+ duration,
1363
+ formatType: 1,
1364
+ voiceType: 1,
1365
+ voiceChangeType: 0,
1366
+ canConvert2Text: true,
1367
+ waveAmplitudes: [
1368
+ 0,
1369
+ 18,
1370
+ 9,
1371
+ 23,
1372
+ 16,
1373
+ 17,
1374
+ 16,
1375
+ 15,
1376
+ 44,
1377
+ 17,
1378
+ 24,
1379
+ 20,
1380
+ 14,
1381
+ 15,
1382
+ 17
1383
+ ],
1384
+ fileSubId: "",
1385
+ playState: 1,
1386
+ autoConvertText: 0,
1387
+ storeID: 0,
1388
+ otherBusinessInfo: { aiVoiceType: 0 }
1389
+ }
1390
+ };
1391
+ }
1392
+ /**
1393
+ * 富媒体文件放置(PIC/PTT 共用):md5 + 文件名 → QQ 内部目标路径
1394
+ * (getRichMediaFilePathForGuild)→ util.copyFile 放置 → 返回相对数据根路径。
1395
+ */
1396
+ async placeMediaFile(path, elementType, md5, fileName) {
1397
+ const service = this.service;
1398
+ const util = this.util;
1399
+ const relPath = service.getRichMediaFilePathForGuild({
1400
+ md5HexStr: md5,
1401
+ fileName,
1402
+ elementType,
1403
+ elementSubType: 0,
1404
+ thumbSize: 0,
1405
+ needCreate: true,
1406
+ downloadType: 1,
1407
+ file_uuid: ""
1408
+ });
1409
+ if (util !== null) {
1410
+ const instance = typeof util.get === "function" ? util.get() : util;
1411
+ const copy = instance["copyFile"];
1412
+ if (typeof copy === "function") await copy.call(instance, path, relPath);
1413
+ }
1414
+ return relPath;
1415
+ }
1416
+ /** 注册 onMsgInfoListUpdate 确认监听(返回 Promise,resolve 时发送成功)。 */
1417
+ confirmSend(msgId, target) {
1418
+ const channel = this.channel;
1419
+ if (channel === null) return Promise.resolve();
1420
+ return new Promise((resolve, reject) => {
1421
+ const off = channel.on("Msg/onMsgInfoListUpdate", (list) => {
1422
+ const mine = list.find((m) => m.guildId === msgId);
1423
+ if (mine === void 0) return;
1424
+ if (mine.sendStatus === SEND_STATUS.SUCCESS || mine.sendStatus === SEND_STATUS.SUCCESS_NO_SEQ) {
1425
+ off();
1426
+ resolve();
1427
+ } else if (mine.sendStatus === SEND_STATUS.FAILED) {
1428
+ off();
1429
+ reject(kernelError(`sendMsg 失败: sendStatus=${mine.sendStatus}`, "SEND_FAILED"));
1430
+ }
1431
+ });
1432
+ setTimeout(() => {
1433
+ off();
1434
+ reject(kernelError(`sendMsg 等待确认超时(msgId=${msgId}, target=${JSON.stringify(target)})`, "SEND_FAILED"));
1435
+ }, SEND_CONFIRM_TIMEOUT_MS);
1436
+ });
1437
+ }
1438
+ /** 撤回消息(群聊管理员 / 私聊 2 分钟内)。 */
1439
+ async recallMessage(target, msgIds) {
1440
+ if (msgIds.length === 0) throw kernelError("recallMessage 需要至少一个 msgId", "INVALID_PARAM");
1441
+ unwrapResult("recallMsg", await this.service.recallMsg(target, msgIds));
1442
+ }
1443
+ /** 拉取历史消息(msgId 为空从最新拉;count 条,时间倒序)。 */
1444
+ async fetchMessages(target, opts) {
1445
+ const raw = await this.service.getMsgs(target, opts.msgId ?? "", opts.count, false);
1446
+ unwrapResult("getMsgs", raw);
1447
+ return raw.msgList ?? [];
1448
+ }
1449
+ /** 按 msgId 批量拉取消息(get_msg / 精华消息 / ptt 转文字共用)。 */
1450
+ async fetchMsgsByMsgId(target, ids) {
1451
+ if (ids.length === 0) return [];
1452
+ const raw = await this.service.getMsgsByMsgId(target, ids);
1453
+ unwrapResult("getMsgsByMsgId", raw);
1454
+ return raw.msgList ?? [];
1455
+ }
1456
+ /** 消息表情表态(set_msg_emoji_like;like=true 点赞,false 取消)。 */
1457
+ async setMsgEmojiLike(target, opts) {
1458
+ unwrapResult("setMsgEmojiLikes", await this.service.setMsgEmojiLikes(target, opts.msgSeq, opts.emojiId, opts.emojiType, opts.like));
1459
+ }
1460
+ /**
1461
+ * 语音转文字(fetch_ptt_text)。
1462
+ * 流程:按 msgId 拉消息 → 找 PTT 元素 → translatePtt2Text(异步转写)
1463
+ * → 再拉一次消息读 pttElement.text。
1464
+ */
1465
+ async fetchPttText(msgId, target) {
1466
+ const ptt = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]));
1467
+ if (ptt === null) throw kernelError("消息中不包含语音", "NOT_FOUND");
1468
+ unwrapResult("translatePtt2Text", await this.service.translatePtt2Text(msgId, target, ptt));
1469
+ const text = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]))?.pttElement?.text;
1470
+ if (text === void 0 || text === "") throw kernelError("获取语音转文字结果失败", "UNKNOWN");
1471
+ return text;
1472
+ }
1473
+ /** 标记会话已读。 */
1474
+ async markRead(target) {
1475
+ unwrapResult("setMsgRead", await this.service.setMsgRead(target));
1476
+ }
1477
+ /** 发送输入状态(set_input_status;eventType=1 输入中,0 停止)。 */
1478
+ async setInputStatus(target, eventType) {
1479
+ await this.service.sendShowInputStatusReq(target.chatType, eventType, target.peerUid);
1480
+ }
1481
+ /**
1482
+ * 发送合并转发(send_group/private_forward_msg)。
1483
+ * buildMultiForwardMsg 组装 MULTI_FORWARD 元素 → 直接作 sendMsg 元素发送。
1484
+ */
1485
+ async sendForwardMessage(target, sourcePeer, srcMsgIds) {
1486
+ if (srcMsgIds.length === 0) throw kernelError("sendForwardMessage 需要至少一条源消息", "INVALID_PARAM");
1487
+ const built = await this.service.buildMultiForwardMsg({
1488
+ srcMsgIds,
1489
+ srcContact: sourcePeer
1490
+ });
1491
+ unwrapResult("buildMultiForwardMsg", built);
1492
+ const elements = built.rspInfo?.elements;
1493
+ if (elements === void 0 || elements.length === 0) throw kernelError("合并转发组装失败:无元素", "UNKNOWN");
1494
+ const msgId = this.service.generateMsgUniqueId(target.chatType, String(Date.now()));
1495
+ unwrapResult("sendMsg", await this.service.sendMsg(msgId, target, elements, /* @__PURE__ */ new Map()));
1496
+ return { msgId };
1497
+ }
1498
+ /** 获取合并转发内容(get_forward_msg;resId 取自 multiForwardMsgElement)。 */
1499
+ async fetchForwardMessage(peer, msgId) {
1500
+ const forward = findForwardElement(await this.fetchMsgsByMsgId(peer, [msgId]));
1501
+ if (forward === null || forward.resId === "") throw kernelError("消息不包含合并转发内容", "NOT_FOUND");
1502
+ const raw = await this.service.getMultiMsg(peer, msgId, forward.resId);
1503
+ unwrapResult("getMultiMsg", raw);
1504
+ return raw.msgList ?? [];
1505
+ }
1506
+ /** 单条转发(forward_group/friend_single_msg;srcMsgIds 源、dstPeer 目标)。 */
1507
+ async forwardSingleMessage(sourcePeer, srcMsgIds, dstPeer) {
1508
+ if (srcMsgIds.length === 0) throw kernelError("forwardSingleMessage 需要至少一条源消息", "INVALID_PARAM");
1509
+ unwrapResult("forwardMsg", await this.service.forwardMsg(srcMsgIds, sourcePeer, [dstPeer], void 0));
1510
+ }
1511
+ /** 设置在线状态(set_online_status;customStatus 为自定义状态)。 */
1512
+ async setOnlineStatus(opts) {
1513
+ unwrapResult("setStatus", await this.service.setStatus(opts));
1514
+ }
1515
+ };
1516
+ /** 在消息列表中找含 pttElement 的元素(找不到返回 null)。 */
1517
+ function findPttElement(msgs) {
1518
+ const [first] = msgs;
1519
+ if (first === void 0) return null;
1520
+ return first.elements.find((el) => el.pttElement !== void 0) ?? null;
1521
+ }
1522
+ /** 在消息列表中找含 multiForwardMsgElement 的元素(找不到返回 null)。 */
1523
+ function findForwardElement(msgs) {
1524
+ const [first] = msgs;
1525
+ if (first === void 0) return null;
1526
+ return first.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement ?? null;
1527
+ }
1528
+ //#endregion
1529
+ //#region src/apis/stranger-info.ts
1530
+ /** 宽松取值:undefined 时给默认值(null 也归默认)。 */
1531
+ function orDefault(value, fallback) {
1532
+ return value ?? fallback;
1533
+ }
1534
+ /** 合并两份详情:优先 info(uid 完整详情),字段缺失回退 byUin(uin 初查详情)。 */
1535
+ function mergeDetail(info, byUin) {
1536
+ const infoSimple = info.detail?.simpleInfo;
1537
+ const byUinSimple = byUin.detail?.simpleInfo;
1538
+ return {
1539
+ coreInfo: pickField(infoSimple, byUinSimple, (s) => s?.coreInfo),
1540
+ baseInfo: pickField(infoSimple, byUinSimple, (s) => s?.baseInfo),
1541
+ vasInfo: pickField(infoSimple, byUinSimple, (s) => s?.vasInfo),
1542
+ commonExt: pickField(info.detail, byUin.detail, (d) => d?.commonExt),
1543
+ status: pickField(infoSimple, byUinSimple, (s) => s?.status?.status)
1544
+ };
1545
+ }
1546
+ /** 字段级回退:primary 的字段取不到回退 fallback 的同名字段(宽松取值)。 */
1547
+ function pickField(primary, fallback, get) {
1548
+ const fromPrimary = primary !== void 0 ? get(primary) : void 0;
1549
+ if (fromPrimary !== void 0) return fromPrimary;
1550
+ return fallback !== void 0 ? get(fallback) : void 0;
1551
+ }
1552
+ /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1553
+ function flattenStrangerInfo(uin, uid, byUin, info) {
1554
+ const d = mergeDetail(info, byUin);
1555
+ return {
1556
+ user_id: Number(uin),
1557
+ uid,
1558
+ nickname: orDefault(d.coreInfo?.nick, ""),
1559
+ age: orDefault(d.baseInfo?.age, 0),
1560
+ qid: orDefault(d.baseInfo?.qid, ""),
1561
+ qq_level: orDefault(d.commonExt?.qqLevel, 0),
1562
+ sex: mapSex(d.baseInfo?.sex),
1563
+ long_nick: orDefault(d.baseInfo?.longNick, ""),
1564
+ reg_time: orDefault(d.commonExt?.regTime, 0),
1565
+ is_vip: orDefault(d.vasInfo?.svipFlag, false),
1566
+ is_years_vip: orDefault(d.vasInfo?.yearVipFlag, false),
1567
+ vip_level: orDefault(d.vasInfo?.vipLevel, 0),
1568
+ remark: orDefault(d.coreInfo?.remark, ""),
1569
+ status: orDefault(d.status, 0),
1570
+ login_days: 0
1571
+ };
1572
+ }
1573
+ /** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
1574
+ function mapSex(raw) {
1575
+ if (raw === 1) return "male";
1576
+ if (raw === 2) return "female";
1577
+ return "unknown";
1578
+ }
1579
+ //#endregion
1580
+ //#region src/apis/profile.ts
1581
+ /**
1582
+ * ProfileApi:资料语义化 API(ADR-009 统一错误语义,P2-14/P2-15)
1583
+ *
1584
+ * - setLongNick:set_self_longnick
1585
+ * - setNickName:set_qq_profile
1586
+ * - setHeader:set_qq_avatar
1587
+ * - getStrangerInfo:get_stranger_info(uin → uid → 详情扁平化)
1588
+ */
1589
+ /** 资料 API:从 session 拿 profile service,包装成语义化方法。 */
1590
+ var ProfileApi = class {
1591
+ service;
1592
+ constructor(session) {
1593
+ const service = session.getProfileService();
1594
+ if (service === null || service === void 0) throw kernelError("getProfileService() 返回空(session 未 init)", "INVALID_STATE");
1595
+ this.service = service;
1596
+ }
1597
+ /** 设置个性签名(set_self_longnick)。 */
1598
+ async setLongNick(longNick) {
1599
+ checkLooseResult("setLongNick", await this.service.setLongNick(longNick));
1600
+ }
1601
+ /** 设置昵称(set_qq_profile)。 */
1602
+ async setNickName(nickName) {
1603
+ checkLooseResult("setNickName", await this.service.setNickName(nickName));
1604
+ }
1605
+ /** 设置头像(set_qq_avatar;filePath 为本地路径)。 */
1606
+ async setHeader(filePath) {
1607
+ const raw = await this.service.setHeader(filePath);
1608
+ if (raw.result !== 0) throw kernelError(`setHeader 失败: ${raw.errMsg}`, "UNKNOWN");
1609
+ }
1610
+ /** 获取陌生人信息(get_stranger_info;uin 为用户 QQ 号)。 */
1611
+ async getStrangerInfo(uin) {
1612
+ const byUin = await this.service.getUserDetailInfoByUin(uin);
1613
+ const detailUid = byUin.detail?.uid;
1614
+ let uid = uin;
1615
+ if (detailUid !== void 0 && detailUid !== "") uid = detailUid;
1616
+ const info = await this.service.getUserDetailInfo(uid);
1617
+ return flattenStrangerInfo(uin, uid, byUin, info);
1618
+ }
1619
+ };
1620
+ //#endregion
1621
+ //#region src/apis/profile-like.ts
1622
+ /**
1623
+ * ProfileLikeApi:资料点赞语义化 API(ADR-009 统一错误语义,P2-14)
1624
+ *
1625
+ * sendLike:send_like(sourceId=71 赞来源,doLikeCount 次数)。
1626
+ */
1627
+ /** 点赞 API:从 session 拿 profile like service,包装成语义化方法。 */
1628
+ var ProfileLikeApi = class {
1629
+ service;
1630
+ constructor(session) {
1631
+ const service = session.getProfileLikeService();
1632
+ if (service === null || service === void 0) throw kernelError("getProfileLikeService() 返回空(session 未 init)", "INVALID_STATE");
1633
+ this.service = service;
1634
+ }
1635
+ /** 点赞(send_like;uid 为目标 uid,count 次数)。 */
1636
+ async sendLike(uid, count = 1) {
1637
+ const raw = await this.service.setBuddyProfileLike({
1638
+ friendUid: uid,
1639
+ sourceId: 71,
1640
+ doLikeCount: count,
1641
+ doLikeTollCount: 0
1642
+ });
1643
+ if (raw.result !== 0) throw kernelError(`点赞失败: ${raw.errMsg}`, "UNKNOWN");
1644
+ }
1645
+ };
1646
+ //#endregion
1647
+ //#region src/apis/richmedia.ts
1648
+ /**
1649
+ * RichMediaApi:富媒体语义化 API(ADR-009 统一错误语义,P2-14)
1650
+ *
1651
+ * - translateWords:translate_en2zh
1652
+ * - 群文件:列表 / 空间 / 建夹 / 删除文件 / 删除夹 / 重命名 / 移动 / 转存
1653
+ */
1654
+ /** 群文件业务类型(deleteGroupFile/moveGroupFile/renameGroupFile 用,说明书参考)。 */
1655
+ const GROUP_FILE_BIZ_TYPE = 102;
1656
+ /** 排序方式(按时间)。 */
1657
+ const SORT_TYPE_TIME = 1;
1658
+ /** 排序方向(倒序)。 */
1659
+ const SORT_ORDER_DESC = 2;
1660
+ /** 起始下标(根目录)。 */
1661
+ const START_INDEX_ROOT = 0;
1662
+ /** 不显示在线文档文件夹。 */
1663
+ const HIDE_ONLINEDOC_FOLDER = 0;
1664
+ /** 默认文件数量。 */
1665
+ const DEFAULT_FILE_COUNT = 50;
1666
+ /** 空间查询文件数量(单条即可)。 */
1667
+ const SPACE_QUERY_FILE_COUNT = 1;
1668
+ /** 富媒体 API:从 session 拿 rich media service,包装成语义化方法。 */
1669
+ var RichMediaApi = class {
1670
+ service;
1671
+ constructor(session) {
1672
+ const service = session.getRichMediaService();
1673
+ if (service === null || service === void 0) throw kernelError("getRichMediaService() 返回空(session 未 init)", "INVALID_STATE");
1674
+ this.service = service;
1675
+ }
1676
+ /** 英文单词翻译(translate_en2zh)。 */
1677
+ async translateWords(words) {
1678
+ const raw = await this.service.translateEnWordToZn(words);
1679
+ unwrap("translateEnWordToZn", raw.result, raw.errMsg);
1680
+ return raw.words ?? [];
1681
+ }
1682
+ /** 群文件列表(get_group_root_files;parent 缺省根目录)。 */
1683
+ async getGroupFileList(groupCode, parentFolderId = "", fileCount = DEFAULT_FILE_COUNT) {
1684
+ const params = {
1685
+ sortType: SORT_TYPE_TIME,
1686
+ fileCount,
1687
+ startIndex: START_INDEX_ROOT,
1688
+ sortOrder: SORT_ORDER_DESC,
1689
+ showOnlinedocFolder: HIDE_ONLINEDOC_FOLDER
1690
+ };
1691
+ const raw = await this.service.getGroupFileList(groupCode, params);
1692
+ if (raw.result !== 0) unwrap("getGroupFileList", raw.result, raw.errMsg);
1693
+ const list = extractFileList(raw.result);
1694
+ if (parentFolderId === "") return list;
1695
+ return list.filter((item) => item.fileInfo !== void 0 && item.fileInfo.parent === parentFolderId || item.folderInfo !== void 0 && item.folderInfo.folderUid === parentFolderId);
1696
+ }
1697
+ /** 群文件系统信息(get_group_file_system_info;getGroupFileList 的 groupSpaceResult)。 */
1698
+ async getGroupFileSystemInfo(groupCode) {
1699
+ const params = {
1700
+ sortType: SORT_TYPE_TIME,
1701
+ fileCount: SPACE_QUERY_FILE_COUNT,
1702
+ startIndex: START_INDEX_ROOT,
1703
+ sortOrder: SORT_ORDER_DESC,
1704
+ showOnlinedocFolder: HIDE_ONLINEDOC_FOLDER
1705
+ };
1706
+ const raw = await this.service.getGroupFileList(groupCode, params);
1707
+ if (raw.result !== 0) unwrap("getGroupFileList", raw.result, raw.errMsg);
1708
+ const space = raw.groupSpaceResult;
1709
+ if (space === void 0) throw kernelError("getGroupFileList 无 groupSpaceResult", "UNKNOWN");
1710
+ return {
1711
+ fileCount: countGroupFiles(raw.result),
1712
+ limitCount: 0,
1713
+ usedSpace: space.usedSpace,
1714
+ totalSpace: space.totalSpace
1715
+ };
1716
+ }
1717
+ /** 创建群文件夹(create_group_file_folder)。 */
1718
+ async createGroupFolder(groupCode, folderName) {
1719
+ const raw = await this.service.createGroupFolder(groupCode, folderName);
1720
+ if (raw.result !== 0) unwrap("createGroupFolder", raw.result, raw.errMsg);
1721
+ const { result } = raw;
1722
+ if (typeof result === "string") return result;
1723
+ return "";
1724
+ }
1725
+ /** 删除群文件(delete_group_file)。 */
1726
+ async deleteGroupFile(groupCode, fileIds) {
1727
+ const raw = await this.service.deleteGroupFile(groupCode, [GROUP_FILE_BIZ_TYPE], fileIds);
1728
+ if (raw.result !== 0) unwrap("deleteGroupFile", raw.result, raw.errMsg);
1729
+ }
1730
+ /** 删除群文件夹(delete_group_folder)。 */
1731
+ async deleteGroupFolder(groupCode, folderId) {
1732
+ const raw = await this.service.deleteGroupFolder(groupCode, folderId);
1733
+ if (raw.result !== 0) unwrap("deleteGroupFolder", raw.result, raw.errMsg);
1734
+ }
1735
+ /** 重命名群文件(rename_group_file;参数语义待探测校准)。 */
1736
+ async renameGroupFile(groupCode, fileId, newName) {
1737
+ await this.service.renameGroupFile(groupCode, GROUP_FILE_BIZ_TYPE, fileId, newName, "");
1738
+ }
1739
+ /** 移动群文件(move_group_file;targetFolder 空为根目录)。 */
1740
+ async moveGroupFile(groupCode, fileIds, targetFolderId) {
1741
+ const raw = await this.service.moveGroupFile(groupCode, [GROUP_FILE_BIZ_TYPE], fileIds, "", targetFolderId);
1742
+ if (raw.result !== 0) unwrap("moveGroupFile", raw.result, raw.errMsg);
1743
+ }
1744
+ /** 转存群文件(trans_group_file)。 */
1745
+ async transGroupFile(groupCode, fileId) {
1746
+ const raw = await this.service.transGroupFile(groupCode, fileId);
1747
+ if (raw.result !== 0) unwrap("transGroupFile", raw.result, raw.errMsg);
1748
+ }
1749
+ };
1750
+ /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。导出供单测。 */
1751
+ function extractFileList(result) {
1752
+ if (Array.isArray(result)) return result;
1753
+ if (result === null || typeof result !== "object") return [];
1754
+ const obj = result;
1755
+ for (const key of [
1756
+ "items",
1757
+ "fileList",
1758
+ "list"
1759
+ ]) {
1760
+ const value = obj[key];
1761
+ if (Array.isArray(value)) return value;
1762
+ }
1763
+ return [];
1764
+ }
1765
+ /** 统计文件数量(兼容数组 / { items })。 */
1766
+ function countGroupFiles(result) {
1767
+ if (Array.isArray(result)) return result.filter((item) => item.fileInfo !== void 0).length;
1768
+ return 0;
1769
+ }
1770
+ //#endregion
1771
+ //#region src/apis/ticket.ts
1772
+ /**
1773
+ * TicketApi:票据语义化 API(ADR-009 统一错误语义,P2-13)
1774
+ *
1775
+ * - getClientKey:TicketService.forceFetchClientKey 解包(get_clientkey)
1776
+ * - getCookies:clientKey → ssl.ptlogin2.qq.com/jump 跳转 → 解析 set-cookie 头
1777
+ * (fetch 会吞掉 Set-Cookie 头,故用 node:https 手动请求)
1778
+ */
1779
+ /** 跳转请求超时(毫秒)。 */
1780
+ const JUMP_TIMEOUT_MS = 1e4;
1781
+ /** 票据 API:从 session 拿 ticket service,包装成语义化方法。 */
1782
+ var TicketApi = class {
1783
+ service;
1784
+ constructor(session) {
1785
+ const service = session.getTicketService();
1786
+ if (service === null || service === void 0) throw kernelError("getTicketService() 返回空(session 未 init)", "INVALID_STATE");
1787
+ this.service = service;
1788
+ }
1789
+ /** 获取 clientKey(get_clientkey)。 */
1790
+ async getClientKey() {
1791
+ const raw = await this.service.forceFetchClientKey("");
1792
+ if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
1793
+ return raw;
1794
+ }
1795
+ /** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
1796
+ async getCookies(domain, uin) {
1797
+ const { clientKey } = await this.getClientKey();
1798
+ const u1 = `https://${domain}/${uin}/infocenter`;
1799
+ return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
1800
+ }
1801
+ /** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
1802
+ static getBkn(skey) {
1803
+ let hash = 5381;
1804
+ for (let i = 0; i < skey.length; i += 1) {
1805
+ const code = skey.charCodeAt(i);
1806
+ hash = hash + (hash << 5) + code;
1807
+ }
1808
+ return (hash & 2147483647).toString();
1809
+ }
1810
+ };
1811
+ /** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
1812
+ function fetchJumpCookies(url) {
1813
+ return new Promise((resolve, reject) => {
1814
+ const req = (0, node_https.get)(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
1815
+ const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
1816
+ res.resume();
1817
+ res.on("end", () => resolve(cookies));
1818
+ });
1819
+ req.setTimeout(JUMP_TIMEOUT_MS, () => {
1820
+ req.destroy();
1821
+ reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
1822
+ });
1823
+ req.on("error", (err) => reject(err));
1824
+ });
1825
+ }
1826
+ /** 解析 set-cookie 头数组 → { name: value } dict。 */
1827
+ function parseSetCookieHeaders(raw) {
1828
+ const cookies = {};
1829
+ if (raw === void 0) return cookies;
1830
+ for (const line of raw) {
1831
+ const [pair] = line.split(";");
1832
+ if (pair !== void 0) {
1833
+ const eq = pair.indexOf("=");
1834
+ if (eq > 0) {
1835
+ const name = pair.slice(0, eq).trim();
1836
+ const value = pair.slice(eq + 1);
1837
+ if (name !== "") cookies[name] = value;
1838
+ }
1839
+ }
1840
+ }
1841
+ return cookies;
1842
+ }
1843
+ //#endregion
1844
+ //#region src/apis/webapi.ts
1845
+ /**
1846
+ * WebApi:群空间 web 接口(P2-15)
1847
+ *
1848
+ * qun.qq.com 群空间接口需 Cookie(p_skey/skey)+ bkn(skey 哈希)。
1849
+ * 复用 TicketApi.getCookies('qun.qq.com') 取 Cookie,TicketApi.getBkn 算 bkn。
1850
+ * 零 NapCat 代码:接口契约(URL/参数/返回形状)是 qun.qq.com 外部系统的事实,
1851
+ * 自研描述。
1852
+ */
1853
+ /** 群空间域名(Cookie 归属)。 */
1854
+ const QUN_DOMAIN = "qun.qq.com";
1855
+ /** 精华消息页大小(分页拉取)。 */
1856
+ const ESSENCE_PAGE_LIMIT = 50;
1857
+ /** 精华消息最大分页数。 */
1858
+ const ESSENCE_MAX_PAGES = 20;
1859
+ /** 群荣誉类型(1=龙王 2=群聊之火 3=群聊炽热 6=快乐源泉)。 */
1860
+ const WebHonorType = {
1861
+ ALL: 0,
1862
+ TALKATIVE: 1,
1863
+ PERFORMER: 2,
1864
+ LEGEND: 3,
1865
+ EMOTION: 6
1866
+ };
1867
+ /** 群空间 web API(Cookie 经 TicketApi 注入)。 */
1868
+ var WebApi = class {
1869
+ getCookies;
1870
+ constructor(opts) {
1871
+ this.getCookies = opts.getCookies;
1872
+ }
1873
+ /** 拉取单页精华消息(失败/retcode 非 0 返回 null)。 */
1874
+ async fetchEssencePage(groupCode, cookie, bkn, page) {
1875
+ const ret = await fetchQunJson("https://qun.qq.com/cgi-bin/group_digest/digest_list?" + new URLSearchParams({
1876
+ bkn,
1877
+ page_start: String(page * ESSENCE_PAGE_LIMIT),
1878
+ page_limit: String(ESSENCE_PAGE_LIMIT),
1879
+ group_code: groupCode
1880
+ }).toString(), cookie);
1881
+ if (ret === null || ret.retcode !== 0) return null;
1882
+ const list = ret.data?.msg_list;
1883
+ return {
1884
+ items: Array.isArray(list) ? list.filter((item) => item !== null) : [],
1885
+ isEnd: ret.data?.is_end === true
1886
+ };
1887
+ }
1888
+ /** 获取群精华消息列表(get_essence_msg_list;分页拉取至 is_end)。 */
1889
+ async getEssenceMsgList(groupCode) {
1890
+ const cookie = await this.getCookies(QUN_DOMAIN);
1891
+ const bkn = TicketApi.getBkn(cookie["skey"] ?? "");
1892
+ const out = [];
1893
+ for (let page = 0; page < ESSENCE_MAX_PAGES; page += 1) {
1894
+ const result = await this.fetchEssencePage(groupCode, cookie, bkn, page);
1895
+ if (result === null) break;
1896
+ out.push(...result.items);
1897
+ if (result.isEnd) break;
1898
+ }
1899
+ return out;
1900
+ }
1901
+ /** 获取群荣誉信息(get_group_honor_info;type 0=全部)。 */
1902
+ async getGroupHonorInfo(groupCode, type) {
1903
+ const cookie = await this.getCookies(QUN_DOMAIN);
1904
+ const result = {
1905
+ group_id: Number(groupCode),
1906
+ current_talkative: null,
1907
+ talkative_list: [],
1908
+ performer_list: [],
1909
+ legend_list: [],
1910
+ emotion_list: [],
1911
+ strong_newbie_list: []
1912
+ };
1913
+ await this.fillHonorLists(result, groupCode, cookie, type);
1914
+ return result;
1915
+ }
1916
+ /** 按 type 填充荣誉列表(ALL 拉全部,单类型只拉对应类)。 */
1917
+ async fillHonorLists(result, groupCode, cookie, type) {
1918
+ for (const target of honorTargets(type)) {
1919
+ const list = await this.fetchHonorList(groupCode, cookie, target.kind);
1920
+ result[target.listKey] = list;
1921
+ if (target.kind === WebHonorType.TALKATIVE) {
1922
+ const [first] = list;
1923
+ if (first !== void 0) result.current_talkative = first;
1924
+ }
1925
+ }
1926
+ }
1927
+ /** 拉取单类荣誉列表(honorlist 接口,正则提取 __INITIAL_STATE__)。 */
1928
+ async fetchHonorList(groupCode, cookie, type) {
1929
+ const text = await fetchQunText("https://qun.qq.com/interactive/honorlist?" + new URLSearchParams({
1930
+ gc: groupCode,
1931
+ type: String(type)
1932
+ }).toString(), cookie);
1933
+ if (text === null) return [];
1934
+ return parseHonorList(text, type);
1935
+ }
1936
+ };
1937
+ /** honorlist 页面 __INITIAL_STATE__ 提取。 */
1938
+ const INITIAL_STATE_RE = /window\.__INITIAL_STATE__=(.*?);/;
1939
+ /** 从 honorlist 页面提取 __INITIAL_STATE__ → 荣誉列表(解析失败返回空)。 */
1940
+ function parseHonorList(text, type) {
1941
+ const match = INITIAL_STATE_RE.exec(text);
1942
+ if (match === null || match[1] === void 0) return [];
1943
+ let state;
1944
+ try {
1945
+ state = JSON.parse(match[1]);
1946
+ } catch {
1947
+ return [];
1948
+ }
1949
+ const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
1950
+ if (!Array.isArray(raw)) return [];
1951
+ const out = [];
1952
+ for (const item of raw) if (item !== null && typeof item === "object") {
1953
+ const obj = item;
1954
+ out.push({
1955
+ uin: String(obj.uin ?? ""),
1956
+ name: String(obj.name ?? ""),
1957
+ avatar: String(obj.avatar ?? ""),
1958
+ desc: String(obj.desc ?? "")
1959
+ });
1960
+ }
1961
+ return out;
1962
+ }
1963
+ /** 请求 qun.qq.com JSON 接口(带 Cookie;失败返回 null)。 */
1964
+ async function fetchQunJson(url, cookie) {
1965
+ try {
1966
+ const res = await fetch(url, { headers: { Cookie: cookieToString(cookie) } });
1967
+ if (!res.ok) return null;
1968
+ const json = await res.json();
1969
+ return {
1970
+ retcode: json.retcode ?? -1,
1971
+ data: json.data
1972
+ };
1973
+ } catch (err) {
1974
+ throw kernelError(`qun.qq.com 请求失败: ${String(err)}`, "UNKNOWN");
1975
+ }
1976
+ }
1977
+ /** 请求 qun.qq.com 文本接口(带 Cookie;失败返回 null)。 */
1978
+ async function fetchQunText(url, cookie) {
1979
+ try {
1980
+ const res = await fetch(url, { headers: { Cookie: cookieToString(cookie) } });
1981
+ if (!res.ok) return null;
1982
+ return await res.text();
1983
+ } catch (err) {
1984
+ throw kernelError(`qun.qq.com 请求失败: ${String(err)}`, "UNKNOWN");
1985
+ }
1986
+ }
1987
+ /** Cookie dict → 头字符串。 */
1988
+ function cookieToString(cookie) {
1989
+ const parts = [];
1990
+ for (const [key, value] of Object.entries(cookie)) parts.push(`${key}=${value}`);
1991
+ return parts.join("; ");
1992
+ }
1993
+ /** 荣誉类型 → 目标列表(ALL 拉全部,单类型只拉对应类)。 */
1994
+ function honorTargets(type) {
1995
+ const all = [
1996
+ {
1997
+ kind: WebHonorType.TALKATIVE,
1998
+ listKey: "talkative_list"
1999
+ },
2000
+ {
2001
+ kind: WebHonorType.PERFORMER,
2002
+ listKey: "performer_list"
2003
+ },
2004
+ {
2005
+ kind: WebHonorType.LEGEND,
2006
+ listKey: "legend_list"
2007
+ },
2008
+ {
2009
+ kind: WebHonorType.EMOTION,
2010
+ listKey: "emotion_list"
2011
+ }
2012
+ ];
2013
+ if (type === WebHonorType.ALL) return [...all];
2014
+ return all.filter((target) => target.kind === type);
2015
+ }
2016
+ //#endregion
2017
+ //#region src/bridge/group-bridge.ts
2018
+ /**
2019
+ * 群事件桥:注册原生监听 → 回调 emit 到 channel。
2020
+ * 无全局单例(ADR-015 推论)——每进程每 session 实例化一份。
2021
+ */
2022
+ var GroupBridge = class {
2023
+ service;
2024
+ channel;
2025
+ listenerId = null;
2026
+ constructor(session, channel) {
2027
+ const service = session.getGroupService();
2028
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
2029
+ this.service = service;
2030
+ this.channel = channel;
2031
+ }
2032
+ /** 注册原生监听(幂等)。 */
2033
+ register() {
2034
+ if (this.listenerId !== null) return;
2035
+ const listener = {
2036
+ onGroupListInited: (listEmpty) => this.channel.emit("Group/onGroupListInited", listEmpty),
2037
+ onGroupListUpdate: (updateType, groupList) => this.channel.emit("Group/onGroupListUpdate", updateType, groupList),
2038
+ onGroupDetailInfoChange: (detailInfo) => this.channel.emit("Group/onGroupDetailInfoChange", detailInfo),
2039
+ onMemberListChange: (arg) => this.channel.emit("Group/onMemberListChange", arg),
2040
+ onMemberInfoChange: (groupCode, dataSource, members) => this.channel.emit("Group/onMemberInfoChange", groupCode, dataSource, members),
2041
+ onGroupNotifiesUpdated: (doubt, notifies) => this.channel.emit("Group/onGroupNotifiesUpdated", doubt, notifies),
2042
+ onGroupSingleScreenNotifies: (doubt, seq, notifies) => this.channel.emit("Group/onGroupSingleScreenNotifies", doubt, seq, notifies),
2043
+ onShutUpMemberListChanged: (groupCode, members) => this.channel.emit("Group/onShutUpMemberListChanged", groupCode, members)
2044
+ };
2045
+ this.listenerId = this.service.addKernelGroupListener(listener);
2046
+ }
2047
+ /** 注销原生监听(幂等)。 */
2048
+ unregister() {
2049
+ if (this.listenerId === null) return;
2050
+ this.service.removeKernelGroupListener(this.listenerId);
2051
+ this.listenerId = null;
2052
+ }
2053
+ };
2054
+ //#endregion
2055
+ //#region src/bridge/msg-bridge.ts
2056
+ /**
2057
+ * 消息事件桥:注册原生监听 → 回调 emit 到 channel。
2058
+ * 无全局单例(ADR-015 推论)——每进程每 session 实例化一份。
2059
+ */
2060
+ var MsgBridge = class {
2061
+ service;
2062
+ channel;
2063
+ listenerId = null;
2064
+ constructor(session, channel) {
2065
+ const service = session.getMsgService();
2066
+ if (service === null || service === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
2067
+ this.service = service;
2068
+ this.channel = channel;
2069
+ }
2070
+ /** 注册原生监听(幂等)。 */
2071
+ register() {
2072
+ if (this.listenerId !== null) return;
2073
+ const listener = {
2074
+ onRecvMsg: (msgs) => this.channel.emit("Msg/onRecvMsg", msgs),
2075
+ onRecvMsgReadReport: (reports) => this.channel.emit("Msg/onRecvMsgReadReport", reports),
2076
+ onRecvMsgReceipt: (receipts) => this.channel.emit("Msg/onRecvMsgReceipt", receipts),
2077
+ onMsgInfoListUpdate: (msgs) => this.channel.emit("Msg/onMsgInfoListUpdate", msgs)
2078
+ };
2079
+ this.listenerId = this.service.addKernelMsgListener(listener);
2080
+ }
2081
+ /** 注销原生监听(幂等)。 */
2082
+ unregister() {
2083
+ if (this.listenerId === null) return;
2084
+ this.service.removeKernelMsgListener(this.listenerId);
2085
+ this.listenerId = null;
2086
+ }
2087
+ };
2088
+ //#endregion
2089
+ //#region src/login/wait.ts
2090
+ /**
2091
+ * 等待/延迟工具(login 模块共享,2026-08-08 克隆合并)
2092
+ */
2093
+ /** 等待条件满足(轮询,带超时)。 */
2094
+ function waitFor(predicate, timeoutMs, intervalMs = 500) {
2095
+ return new Promise((resolve) => {
2096
+ const started = Date.now();
2097
+ const tick = () => {
2098
+ if (predicate()) {
2099
+ resolve(true);
2100
+ return;
2101
+ }
2102
+ if (Date.now() - started > timeoutMs) {
2103
+ resolve(false);
2104
+ return;
2105
+ }
2106
+ setTimeout(tick, intervalMs);
2107
+ };
2108
+ tick();
2109
+ });
2110
+ }
2111
+ /** 短延迟。 */
2112
+ function sleep(ms) {
2113
+ return new Promise((resolve) => {
2114
+ setTimeout(resolve, ms);
2115
+ });
2116
+ }
2117
+ //#endregion
2118
+ //#region src/cache/group-cache.ts
2119
+ /**
2120
+ * 群/成员缓存:事件主动维护 + 查询惰性回填。
2121
+ * 只读接口 getGroupDetail / getMembers / getMember / getGroup(列表项)。
2122
+ */
2123
+ var GroupCache = class {
2124
+ channel;
2125
+ groupApi;
2126
+ /** 群列表项(onGroupListUpdate / getGroupList 回填)。 */
2127
+ groupList = /* @__PURE__ */ new Map();
2128
+ /** 群详情(onGroupDetailInfoChange / getGroupInfo 回填)。 */
2129
+ groupDetails = /* @__PURE__ */ new Map();
2130
+ /** 群成员:groupCode → (uid → GroupMember)。 */
2131
+ members = /* @__PURE__ */ new Map();
2132
+ /** 惰性回填 in-flight 去重。 */
2133
+ inFlight = /* @__PURE__ */ new Map();
2134
+ unsubscribes = null;
2135
+ constructor(opts) {
2136
+ this.channel = opts.channel;
2137
+ this.groupApi = opts.groupApi;
2138
+ }
2139
+ /** 订阅群事件通道(幂等)。 */
2140
+ register() {
2141
+ if (this.unsubscribes !== null) return;
2142
+ const unsubs = [];
2143
+ unsubs.push(this.channel.on("Group/onGroupListInited", (listEmpty) => {
2144
+ if (listEmpty) this.groupList.clear();
2145
+ }));
2146
+ unsubs.push(this.channel.on("Group/onGroupListUpdate", (updateType, groupList) => {
2147
+ if (updateType === GroupListUpdateType.ALL) this.groupList.clear();
2148
+ for (const group of groupList) this.groupList.set(group.groupCode, group);
2149
+ }));
2150
+ unsubs.push(this.channel.on("Group/onGroupDetailInfoChange", (detailInfo) => {
2151
+ this.groupDetails.set(detailInfo.groupCode, detailInfo);
2152
+ }));
2153
+ unsubs.push(this.channel.on("Group/onMemberListChange", (arg) => {
2154
+ this.upsertMembers(arg.sceneId, arg.infos);
2155
+ }));
2156
+ unsubs.push(this.channel.on("Group/onMemberInfoChange", (_groupCode, _dataSource, members) => {
2157
+ this.upsertMembers(_groupCode, members);
2158
+ }));
2159
+ this.unsubscribes = () => {
2160
+ for (const unsub of unsubs) unsub();
2161
+ };
2162
+ }
2163
+ /** 退订事件通道(幂等)。 */
2164
+ unregister() {
2165
+ this.unsubscribes?.();
2166
+ this.unsubscribes = null;
2167
+ this.groupList.clear();
2168
+ this.groupDetails.clear();
2169
+ this.members.clear();
2170
+ this.inFlight.clear();
2171
+ }
2172
+ /** 合并成员到该群缓存(uid → GroupMember)。 */
2173
+ upsertMembers(groupCode, infos) {
2174
+ let map = this.members.get(groupCode);
2175
+ if (map === void 0) {
2176
+ map = /* @__PURE__ */ new Map();
2177
+ this.members.set(groupCode, map);
2178
+ }
2179
+ for (const [uid, member] of infos) map.set(uid, member);
2180
+ }
2181
+ /** 群列表是否已有缓存(同步判断)。 */
2182
+ hasGroup(groupCode) {
2183
+ return this.groupList.has(groupCode) || this.groupDetails.has(groupCode);
2184
+ }
2185
+ /**
2186
+ * 群列表快照(只读,缓存由 onGroupListUpdate 事件主动维护)。
2187
+ * 群列表数据不走原生 getGroupList 返回值(实测仅 { result, errMsg },
2188
+ * 列表经事件推送)——协议/IPC 层消费列表一律经此方法。
2189
+ */
2190
+ listGroups() {
2191
+ return [...this.groupList.values()];
2192
+ }
2193
+ /**
2194
+ * 群列表快照(缓存为空时主动触发原生刷新,等待事件回填后返回)。
2195
+ *
2196
+ * 实测(2026-08-08 端到端联调):onGroupListUpdate 仅在登录后首轮同步
2197
+ * 推送(MMKV 已有缓存后不再重复推送)——进程内缓存为空时列表读不到。
2198
+ * 触发 getGroupList(true) 会重新拉取并推送事件回填缓存。
2199
+ * 返回空数组 = 刷新后仍无数据(超时 5s)。
2200
+ */
2201
+ async listGroupsRefreshed() {
2202
+ if (this.groupList.size > 0) return [...this.groupList.values()];
2203
+ await this.groupApi.getGroupList(true);
2204
+ await waitFor(() => this.groupList.size > 0, 5e3, 200);
2205
+ return [...this.groupList.values()];
2206
+ }
2207
+ /**
2208
+ * 群列表项(只读;缺失惰性回填 getGroupList)。
2209
+ * 返回 undefined 表示群不存在。
2210
+ */
2211
+ async getGroup(groupCode) {
2212
+ const cached = this.groupList.get(groupCode);
2213
+ if (cached !== void 0) return cached;
2214
+ await this.loadGroupList();
2215
+ return this.groupList.get(groupCode);
2216
+ }
2217
+ /**
2218
+ * 群详情(只读;缺失惰性回填 getGroupInfo)。
2219
+ * getGroupInfo 恒返回对象(失败抛 KernelError),直接缓存。
2220
+ */
2221
+ async getGroupDetail(groupCode) {
2222
+ const cached = this.groupDetails.get(groupCode);
2223
+ if (cached !== void 0) return cached;
2224
+ const detail = await this.dedupe(`detail:${groupCode}`, () => this.groupApi.getGroupInfo(groupCode));
2225
+ this.groupDetails.set(groupCode, detail);
2226
+ return detail;
2227
+ }
2228
+ /**
2229
+ * 群成员列表(只读;缺失惰性回填 getAllMemberList(forceFetch=true))。
2230
+ * 返回空数组表示该群无成员(或拉取失败已由 GroupApi 抛 KernelError)。
2231
+ */
2232
+ async getMembers(groupCode) {
2233
+ const map = this.members.get(groupCode);
2234
+ if (map !== void 0 && map.size > 0) return [...map.values()];
2235
+ const members = await this.dedupe(`members:${groupCode}`, () => this.groupApi.getGroupMemberList(groupCode, true));
2236
+ const merged = /* @__PURE__ */ new Map();
2237
+ for (const member of members) merged.set(member.uid, member);
2238
+ this.members.set(groupCode, merged);
2239
+ return members;
2240
+ }
2241
+ /**
2242
+ * 群成员(只读;缺失回填:先整群拉取,再按 uid 取;仍无则 getMemberInfo 单查)。
2243
+ * 返回 undefined 表示成员不在群。
2244
+ */
2245
+ async getMember(groupCode, uid) {
2246
+ const cached = await this.getCachedMember(groupCode, uid);
2247
+ if (cached !== void 0) return cached;
2248
+ const [member] = await this.dedupe(`member:${groupCode}:${uid}`, () => this.groupApi.getGroupMemberInfo(groupCode, [uid]));
2249
+ if (member === void 0) return;
2250
+ const target = this.members.get(groupCode);
2251
+ if (target !== void 0) target.set(member.uid, member);
2252
+ return member;
2253
+ }
2254
+ /** 读缓存成员(缺失先整群回填一次)。 */
2255
+ async getCachedMember(groupCode, uid) {
2256
+ let map = this.members.get(groupCode);
2257
+ if (map === void 0 || map.size === 0) {
2258
+ await this.getMembers(groupCode);
2259
+ map = this.members.get(groupCode);
2260
+ }
2261
+ return map?.get(uid);
2262
+ }
2263
+ /** 惰性回填全量群列表(getGroup 缺失时)。 */
2264
+ async loadGroupList() {
2265
+ const list = await this.dedupe("group-list", () => this.groupApi.getGroupList(false));
2266
+ for (const group of list) this.groupList.set(group.groupCode, group);
2267
+ }
2268
+ /** 惰性回填并发去重:同 key 并发只拉一次。 */
2269
+ async dedupe(key, loader) {
2270
+ const existing = this.inFlight.get(key);
2271
+ if (existing !== void 0) return await existing;
2272
+ const task = loader().catch((err) => {
2273
+ throw err;
2274
+ }).finally(() => {
2275
+ this.inFlight.delete(key);
2276
+ });
2277
+ this.inFlight.set(key, task);
2278
+ return await task;
2279
+ }
2280
+ };
2281
+ //#endregion
2282
+ //#region src/context.ts
2283
+ /** 创建装配根(初始 wrapper/login 为 null)。 */
2284
+ function createCoreContext(opts) {
2285
+ return {
2286
+ logger: opts.logger,
2287
+ paths: opts.paths,
2288
+ wrapper: null,
2289
+ login: null
2290
+ };
2291
+ }
2292
+ //#endregion
2293
+ //#region src/wrapper/wrapper-adapters.ts
2294
+ /** NAPI 回调占位(方法存在即可被反射调用,无副作用)。 */
2295
+ const noop = () => void 0;
2296
+ /** NodeIGlobalAdapter 实现(engine.initWithDeskTopConfig 第二参)。 */
2297
+ var GlobalAdapter = class {
2298
+ onLog = noop;
2299
+ onGetSrvCalTime = noop;
2300
+ onShowErrUITips = noop;
2301
+ fixPicImgType = noop;
2302
+ getAppSetting = noop;
2303
+ onInstallFinished = noop;
2304
+ onUpdateGeneralFlag = noop;
2305
+ onGetOfflineMsg = noop;
2306
+ };
2307
+ /** NodeIDependsAdapter 实现(session.init 第二参)。 */
2308
+ var DependsAdapter = class {
2309
+ onMSFStatusChange = noop;
2310
+ onMSFSsoError = noop;
2311
+ getGroupCode = noop;
2312
+ };
2313
+ /** NodeIDispatcherAdapter 实现(session.init 第三参)。 */
2314
+ var DispatcherAdapter = class {
2315
+ dispatchRequest = noop;
2316
+ dispatchCall = noop;
2317
+ dispatchCallWithJson = noop;
2318
+ };
2319
+ /**
2320
+ * 创建会话监听器(默认静默,boot 阶段默认;login 模块接入后可覆盖)。
2321
+ * 回调方法必须存在(NAPI 按方法名反射),但输出与否由调用方决定——
2322
+ * onNTSessionCreate / onOpentelemetryInit 等对用户无信息量,默认不打 stdout。
2323
+ */
2324
+ function createSessionListener(opts = {}) {
2325
+ const { onEvent } = opts;
2326
+ const emit = (name, ...args) => {
2327
+ onEvent?.(name, ...args);
2328
+ };
2329
+ return {
2330
+ onNTSessionCreate: (sessionId) => emit("onNTSessionCreate", sessionId),
2331
+ onGProSessionCreate: (sessionId) => emit("onGProSessionCreate", sessionId),
2332
+ onSessionInitComplete: (sessionId) => emit("onSessionInitComplete", sessionId),
2333
+ onOpentelemetryInit: (info) => emit("onOpentelemetryInit", info),
2334
+ onUserOnlineResult: (result) => emit("onUserOnlineResult", result),
2335
+ onGetSelfTinyId: (result) => emit("onGetSelfTinyId", result)
2336
+ };
2337
+ }
2338
+ /** 创建登录监听器(占位,登录器接入时填充)。 */
2339
+ function createLoginListener() {
2340
+ return {};
2341
+ }
2342
+ //#endregion
2343
+ //#region src/login/login-connect.ts
2344
+ /**
2345
+ * 登录连接与快速登录(从 lifecycle.ts 拆分,2026-08-08 FTA 优化)
2346
+ *
2347
+ * - waitForNetworkConnection:等 MSF 网络连接就绪(getMsfStatus() === 3)
2348
+ * - ensureLoginConnected:connect() → 等 onLoginConnected(自建宿主必需)
2349
+ * - quickLogin:历史账号快速登录(网络异常 1006511 等就绪后重试)
2350
+ */
2351
+ /**
2352
+ * 网络状态(MSF)常量。
2353
+ * 3 = 已连接(NapCat waitForNetworkConnection 语义,自研描述)。
2354
+ */
2355
+ const MSF_STATUS_CONNECTED = 3;
2356
+ /** 快速登录网络异常错误码(1006511,P2-1 实测:登录前网络未就绪时报此错)。 */
2357
+ const NETWORK_ERROR_CODE = "1006511";
2358
+ /** 登录连接异常错误提示(自建宿主实测:connect 后未缓冲即 quickLogin 报此错)。 */
2359
+ const CONNECTION_ERROR_HINT = "登录系统连接异常";
2360
+ /** 网络重试最大次数(每次重试前等网络就绪)。 */
2361
+ const NETWORK_RETRY_MAX = 3;
2362
+ /** 网络就绪等待超时(毫秒)。 */
2363
+ const NETWORK_READY_TIMEOUT_MS = 15e3;
2364
+ /** 网络就绪轮询间隔(毫秒)。 */
2365
+ const NETWORK_READY_POLL_MS = 1e3;
2366
+ /** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
2367
+ const CONNECTION_SETTLE_MS = 3e3;
2368
+ /**
2369
+ * 等待网络连接就绪(loginService.getMsfStatus() === 3)。
2370
+ * 参考 NapCat waitForNetworkConnection 思路(自研实现):快速登录在 QQ 刚拉起、
2371
+ * 网络栈未初始化时报 1006511 网络异常——等待 MSF 连接后再重试。
2372
+ * @returns 是否在超时前就绪。
2373
+ */
2374
+ function waitForNetworkConnection(ctx, opts = {}) {
2375
+ const raw = ctx.loginService;
2376
+ if (raw === null || typeof raw.getMsfStatus !== "function") return Promise.resolve(false);
2377
+ return waitFor(() => {
2378
+ try {
2379
+ return raw.getMsfStatus() === MSF_STATUS_CONNECTED;
2380
+ } catch {
2381
+ return false;
2382
+ }
2383
+ }, opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS, NETWORK_READY_POLL_MS);
2384
+ }
2385
+ /** 列出历史登录账号(boot.cjs 启动横幅用,「可用快速登录 of QQ」)。 */
2386
+ async function listLoginAccounts(ctx) {
2387
+ const raw = ctx.loginService;
2388
+ if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
2389
+ return (await raw.getLoginList()).LocalLoginInfoList;
2390
+ }
2391
+ /**
2392
+ * 登录服务连接(自建宿主必需,HANDOVER-V6/V10 p0-kernel-flow 实证顺序):
2393
+ * connect() → 等 onLoginConnected。不 connect 则快速登录报「登录系统连接异常」。
2394
+ *
2395
+ * 对齐 p0-kernel-flow:initConfig → getLoginList → connect + onLoginConnected →
2396
+ * quickLoginWithUin。QQ 环境(worker/主进程)下可能已连接——connect 幂等,
2397
+ * 监听器超时兜底,不影响原流程。
2398
+ */
2399
+ async function ensureLoginConnected(ctx, opts) {
2400
+ const raw = ctx.loginService;
2401
+ if (raw === null || typeof raw.connect !== "function") return;
2402
+ await waitLoginConnected(raw, opts.timeoutMs);
2403
+ await sleep(CONNECTION_SETTLE_MS);
2404
+ }
2405
+ /** 等 onLoginConnected(注册临时监听 + connect + 超时兜底)。 */
2406
+ function waitLoginConnected(raw, timeoutMs) {
2407
+ return new Promise((resolve) => {
2408
+ let settled = false;
2409
+ const finish = () => {
2410
+ if (!settled) {
2411
+ settled = true;
2412
+ resolve();
2413
+ }
2414
+ };
2415
+ tryRegisterConnectListener(raw, finish);
2416
+ setTimeout(finish, timeoutMs ?? NETWORK_READY_TIMEOUT_MS);
2417
+ try {
2418
+ raw.connect?.();
2419
+ } catch {
2420
+ finish();
2421
+ }
2422
+ });
2423
+ }
2424
+ /** 注册临时登录连接监听(失败不阻塞连接)。 */
2425
+ function tryRegisterConnectListener(raw, finish) {
2426
+ try {
2427
+ const listener = createLoginListener();
2428
+ listener.onLoginConnected = () => {
2429
+ finish();
2430
+ };
2431
+ if (typeof raw.addKernelLoginListener === "function") raw.addKernelLoginListener(listener);
2432
+ } catch {}
2433
+ }
2434
+ /**
2435
+ * 目标账号选择(2026-08-07 修复:显式 uin 不在列表时**报错**而非静默 fallback——
2436
+ * 之前会退到第一个可快速登录账号,导致 `-q <uin>` 实际登成别的号)。
2437
+ * 导出供单测(login-connect.test.ts)。
2438
+ */
2439
+ function pickLoginTarget(items, uin) {
2440
+ if (uin !== void 0) {
2441
+ const target = items.find((i) => i.uin === uin);
2442
+ if (target === void 0) throw kernelError(`账号 ${uin} 不在登录列表(可用:${items.map((i) => i.uin).join("、")})`, "NOT_FOUND");
2443
+ return target;
2444
+ }
2445
+ const quick = items.find((i) => i.isQuickLogin);
2446
+ if (quick !== void 0) return quick;
2447
+ const [first] = items;
2448
+ if (first === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
2449
+ return first;
2450
+ }
2451
+ /**
2452
+ * 带网络重试的快速登录(P2-1)。
2453
+ * 重试语义:仅当失败为网络异常(1006511)且未达上限时,等网络就绪后重试。
2454
+ */
2455
+ async function loginWithNetworkRetry(ctx, loginService, target, opts) {
2456
+ let lastErrMsg = "";
2457
+ for (let attempt = 1; attempt <= NETWORK_RETRY_MAX; attempt += 1) {
2458
+ const attemptResult = await attemptQuickLogin(loginService, target);
2459
+ if (attemptResult.ok) return attemptResult.result;
2460
+ lastErrMsg = attemptResult.errMsg;
2461
+ if (!(isNetworkError(lastErrMsg) && attempt < NETWORK_RETRY_MAX)) break;
2462
+ if (!await waitForNetworkConnection(ctx, { timeoutMs: opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS })) break;
2463
+ }
2464
+ throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
2465
+ }
2466
+ /** 单次快速登录尝试(成功带结果,失败带错误消息)。 */
2467
+ async function attemptQuickLogin(loginService, target) {
2468
+ const { errMsg } = (await loginService.quickLoginWithUin(target.uin)).loginErrorInfo;
2469
+ if (errMsg) return {
2470
+ ok: false,
2471
+ errMsg
2472
+ };
2473
+ return {
2474
+ ok: true,
2475
+ result: {
2476
+ uin: target.uin,
2477
+ uid: target.uid ?? "",
2478
+ nick: target.nickName ?? ""
2479
+ }
2480
+ };
2481
+ }
2482
+ /** 判定快速登录失败是否为网络异常(重试语义)。导出供单测。 */
2483
+ function isNetworkError(errMsg) {
2484
+ return errMsg.includes(NETWORK_ERROR_CODE) || errMsg.includes(CONNECTION_ERROR_HINT);
2485
+ }
2486
+ /**
2487
+ * 快速登录:遍历历史登录列表尝试。
2488
+ * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
2489
+ */
2490
+ async function quickLogin(ctx, opts) {
2491
+ const raw = ctx.loginService;
2492
+ if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
2493
+ const loginService = raw;
2494
+ await ensureLoginConnected(ctx, { ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} });
2495
+ const items = (await loginService.getLoginList()).LocalLoginInfoList;
2496
+ if (items.length === 0) throw kernelError("无历史登录账号", "NOT_LOGIN");
2497
+ return loginWithNetworkRetry(ctx, loginService, pickLoginTarget(items, opts.uin), opts);
2498
+ }
2499
+ //#endregion
2500
+ //#region src/login/lifecycle.ts
2501
+ /**
2502
+ * 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
2503
+ *
2504
+ * 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
2505
+ * 运行时实测确认,自研实现:
2506
+ * 1. engine.initWithDeskTopConfig(appid/qua/版本)
2507
+ * 2. loginService.initConfig + addKernelLoginListener
2508
+ * 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
2509
+ * 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
2510
+ * 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
2511
+ *
2512
+ * 关键(2026-08-05 实测修正):
2513
+ * - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
2514
+ * 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
2515
+ *
2516
+ * 模块边界(2026-08-05 解耦;2026-08-08 FTA 优化再拆):
2517
+ * - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
2518
+ * - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
2519
+ * - 登录连接 + 快速登录(waitForNetworkConnection / quickLogin 等)→ login-connect.ts
2520
+ * - 本文件只保留 session 流程编排:就绪等待(waitSessionReady)+ session 初始化(initAndStartSession)。
2521
+ */
2522
+ /** session init 默认超时(毫秒)。 */
2523
+ const DEFAULT_INIT_TIMEOUT_MS = 15e3;
2524
+ /** session 就绪等待默认超时(毫秒)。 */
2525
+ const SESSION_READY_TIMEOUT_MS = 3e4;
2526
+ /** session 就绪轮询间隔(毫秒)。 */
2527
+ const SESSION_READY_POLL_MS = 1e3;
2528
+ /**
2529
+ * 等待 QQ 自己完成 session 初始化(framework 复用模式,2026-08-05 修正)。
2530
+ *
2531
+ * 背景:QQ 9.9.31 实测——session 由 QQ 主进程自己创建并 init(登录后)。
2532
+ * 我们不自己 init(重复 init 断言 "implementation of IQQNTWrapperSession is not valid"),
2533
+ * 也不自己 create(登录前 create() 干扰 QQ,实测 QQ 退出 code=0)。
2534
+ * 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
2535
+ */
2536
+ async function waitSessionReady(ctx, opts) {
2537
+ const { session } = ctx;
2538
+ if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
2539
+ if (!await waitFor(() => {
2540
+ try {
2541
+ const svc = session.getMsgService();
2542
+ return svc !== null && svc !== void 0;
2543
+ } catch {
2544
+ return false;
2545
+ }
2546
+ }, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
2547
+ }
2548
+ /** 登录连接 + 快速登录(quickLogin 等)已拆到 login-connect.ts(见文件头 re-export)。 */
2549
+ /** session 初始化(4 参全为普通 JS 对象,等 init 完成信号)。 */
2550
+ async function initAndStartSession(ctx, config, listener, opts) {
2551
+ const { session } = ctx;
2552
+ if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
2553
+ const depends = new DependsAdapter();
2554
+ const dispatcher = new DispatcherAdapter();
2555
+ const initComplete = watchInitSignal(listener);
2556
+ session.init(config, depends, dispatcher, listener);
2557
+ startSessionBestEffort(ctx, session);
2558
+ if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2559
+ }
2560
+ /** 监听 init 完成信号:onOpentelemetryInit(is_init) 为主,onSessionInitComplete(0) 为辅。 */
2561
+ function watchInitSignal(listener) {
2562
+ return new Promise((resolve, reject) => {
2563
+ const onOpentelemetry = listener.onOpentelemetryInit;
2564
+ listener.onOpentelemetryInit = (info) => {
2565
+ if (info.is_init) resolve();
2566
+ if (typeof onOpentelemetry === "function") onOpentelemetry(info);
2567
+ };
2568
+ const onInitComplete = listener.onSessionInitComplete;
2569
+ listener.onSessionInitComplete = (r) => {
2570
+ if (r === 0 || r === "0") resolve();
2571
+ else reject(kernelError(`session init 失败: ${String(r)}`, "UNKNOWN"));
2572
+ if (typeof onInitComplete === "function") onInitComplete(r);
2573
+ };
2574
+ });
2575
+ }
2576
+ /**
2577
+ * 启动会话(best effort,失败不致命):
2578
+ * **先 init 后 start(2026-08-07 V9 决定性修正)**。
2579
+ * 自建宿主实测(HANDOVER-V9,p0-napcat-min):必须先 session.init 再
2580
+ * startupSession.start()——顺序颠倒(先 start 后 init)业务 service 不挂载
2581
+ * (getMsgService null);init 后用 startNT(非 startupSession.start)也失败。
2582
+ * NapCat initializeSession 同款:有 startupSession 用 start(),否则 startNT(0)。
2583
+ */
2584
+ function startSessionBestEffort(ctx, session) {
2585
+ if (startupSessionStart(ctx)) return;
2586
+ try {
2587
+ session.startNT(0);
2588
+ } catch {
2589
+ try {
2590
+ session.startNT();
2591
+ } catch {}
2592
+ }
2593
+ }
2594
+ /** 有 startupSession 则 start()(失败不致命),返回是否已处理。 */
2595
+ function startupSessionStart(ctx) {
2596
+ const { startupSession } = ctx;
2597
+ if (startupSession === null || typeof startupSession.start !== "function") return false;
2598
+ try {
2599
+ startupSession.start();
2600
+ } catch {}
2601
+ return true;
2602
+ }
2603
+ //#endregion
2604
+ //#region src/login/login.ts
2605
+ /**
2606
+ * login.ts:QR 登录流程编排 + 状态机 + selfInfo(ADR-010,2026-08-05)
2607
+ *
2608
+ * 流程(QQ wrapper 登录契约,自研实现):
2609
+ * loginService.initConfig(core.login 已做)→ addKernelLoginListener → connect()
2610
+ * → 指定账号:quickLoginWithUin;否则 getQRCodePicture() 触发二维码
2611
+ * → 回调驱动状态机:未登录 → 扫码中 → 已扫码 → 已登录(onQRCodeLoginSucceed)
2612
+ * → 二维码过期(errType=1 errCode=3)→ refresh()
2613
+ *
2614
+ * 不含 UI 渲染(cli 只做二维码渲染/URL 打印)。
2615
+ */
2616
+ /** 二维码过期错误码(errType=1 errCode=3,实测)。 */
2617
+ const QR_EXPIRED_ERR_TYPE = 1;
2618
+ const QR_EXPIRED_ERR_CODE = 3;
2619
+ /** 二维码过期判定。 */
2620
+ function isQrCodeExpired(errType, errCode) {
2621
+ return errType === QR_EXPIRED_ERR_TYPE && errCode === QR_EXPIRED_ERR_CODE;
2622
+ }
2623
+ /** 二维码 png data URI 前缀剥离正则。 */
2624
+ const DATA_URI_PREFIX_RE = /^data:image\/\w+;base64,/;
2625
+ /** QR 登录会话(状态机 + 回调订阅)。 */
2626
+ var QrLoginSession = class {
2627
+ loginService;
2628
+ state = "idle";
2629
+ listenerId = null;
2630
+ self = null;
2631
+ qrCodeHandlers = /* @__PURE__ */ new Set();
2632
+ stateHandlers = /* @__PURE__ */ new Set();
2633
+ constructor(loginService) {
2634
+ const svc = loginService;
2635
+ if (svc === null || typeof svc.addKernelLoginListener !== "function") throw kernelError("loginService 无效(缺 addKernelLoginListener)", "INVALID_STATE");
2636
+ this.loginService = svc;
2637
+ }
2638
+ /** 当前状态。 */
2639
+ get currentState() {
2640
+ return this.state;
2641
+ }
2642
+ /** selfInfo(登录成功后有值)。 */
2643
+ get selfInfo() {
2644
+ return this.self;
2645
+ }
2646
+ /** 订阅二维码(png base64 + url)。返回退订函数。 */
2647
+ onQrCode(cb) {
2648
+ this.qrCodeHandlers.add(cb);
2649
+ return () => this.qrCodeHandlers.delete(cb);
2650
+ }
2651
+ /** 订阅状态变化。返回退订函数。 */
2652
+ onStateChange(cb) {
2653
+ this.stateHandlers.add(cb);
2654
+ return () => this.stateHandlers.delete(cb);
2655
+ }
2656
+ /** 开始登录:注册监听 → connect → 快速登录(指定账号)或触发二维码。 */
2657
+ start(opts = {}) {
2658
+ if (this.state !== "idle") return;
2659
+ this.listenerId = this.loginService.addKernelLoginListener(this.buildListener());
2660
+ this.loginService.connect();
2661
+ if (opts.quickUin !== void 0 && opts.quickUin !== "") this.loginService.quickLoginWithUin(opts.quickUin).then((result) => {
2662
+ if (result.loginErrorInfo.errMsg !== "") this.refresh();
2663
+ }).catch(() => {
2664
+ this.refresh();
2665
+ });
2666
+ else this.refresh();
2667
+ }
2668
+ /** 刷新二维码(手动 / 过期自动)。 */
2669
+ refresh() {
2670
+ this.setState("waiting_scan");
2671
+ this.loginService.getQRCodePicture();
2672
+ }
2673
+ /** 构建登录监听(普通 JS 对象,NAPI 反射)。 */
2674
+ buildListener() {
2675
+ const listener = createLoginListener();
2676
+ listener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
2677
+ const realBase64 = pngBase64QrcodeData.replace(DATA_URI_PREFIX_RE, "");
2678
+ this.emitQrCode({
2679
+ pngBase64: realBase64,
2680
+ qrcodeUrl
2681
+ });
2682
+ };
2683
+ listener.onQRCodeLoginPollingStarted = () => {
2684
+ this.setState("waiting_scan");
2685
+ };
2686
+ listener.onQRCodeSessionUserScaned = () => {
2687
+ this.setState("scanned");
2688
+ };
2689
+ listener.onQRCodeLoginSucceed = (result) => {
2690
+ this.self = {
2691
+ uin: result.uin,
2692
+ uid: result.uid,
2693
+ nick: result.nick ?? ""
2694
+ };
2695
+ this.setState("logged_in");
2696
+ };
2697
+ listener.onQRCodeSessionFailed = (errType, errCode) => {
2698
+ if (isQrCodeExpired(errType, errCode)) {
2699
+ this.refresh();
2700
+ return;
2701
+ }
2702
+ this.setState("failed");
2703
+ };
2704
+ listener.onLoginFailed = () => {
2705
+ this.setState("failed");
2706
+ };
2707
+ return listener;
2708
+ }
2709
+ /** 停止:注销监听(登录成功后调用,防重复回调)。 */
2710
+ stop() {
2711
+ if (this.listenerId !== null) {
2712
+ this.loginService.removeKernelLoginListener(this.listenerId);
2713
+ this.listenerId = null;
2714
+ }
2715
+ }
2716
+ setState(next) {
2717
+ this.state = next;
2718
+ for (const cb of this.stateHandlers) cb(next);
2719
+ }
2720
+ emitQrCode(qr) {
2721
+ for (const cb of this.qrCodeHandlers) cb(qr);
2722
+ }
2723
+ };
2724
+ //#endregion
2725
+ //#region src/wrapper/qq-data-path.ts
2726
+ /**
2727
+ * QQ 数据根路径解析(P2-1,2026-08-06)
2728
+ *
2729
+ * 从 `getNTUserDataInfoConfig()` 的返回值中提取 QQ 真实数据根路径:
2730
+ * worker(utilityProcess)模式下 loginService 是 new 的,commonPath 必须指向
2731
+ * QQ 真实数据路径才能读到历史账号(cli 的 `.napuketto\default` 读不到,
2732
+ * getLoginList 空,P2-1 实测)。
2733
+ *
2734
+ * 返回值形态可能是 JSON 字符串(对象/数组)或纯路径字符串,此处做宽容解析。
2735
+ */
2736
+ /**
2737
+ * QQ 全局数据目录(commonPath / desktopGlobalPath = 数据根/nt_qq/global,
2738
+ * HANDOVER-V6 自建宿主三要素之三,p0-kernel-flow 实证)。
2739
+ *
2740
+ * 防御:已含 nt_qq/global 后缀(worker 环境下 getNTUserDataInfoConfig 可能
2741
+ * 返回完整路径)则不重复拼接,避免 `nt_qq/global/nt_qq/global` 错位。
2742
+ */
2743
+ function resolveQqGlobalPath(root) {
2744
+ const norm = root.replace(/\\/g, "/");
2745
+ if (norm.endsWith("/nt_qq/global")) return root;
2746
+ if (norm.endsWith("/nt_qq")) return (0, node_path.join)(root, "global");
2747
+ return (0, node_path.join)(root, "nt_qq", "global");
2748
+ }
2749
+ /** 从 getNTUserDataInfoConfig 返回值提取数据根(JSON 字符串解析或纯路径)。 */
2750
+ function extractDataRoot(raw) {
2751
+ if (raw.startsWith("{") || raw.startsWith("[")) try {
2752
+ return findPathInValue(JSON.parse(raw));
2753
+ } catch {
2754
+ return null;
2755
+ }
2756
+ return raw;
2757
+ }
2758
+ /** 递归在对象/数组里找含 QQ 数据路径特征的字符串值。 */
2759
+ function findPathInValue(value) {
2760
+ if (typeof value === "string") return isQqDataPath(value) ? value : null;
2761
+ if (Array.isArray(value)) return findInArray(value);
2762
+ if (typeof value === "object" && value !== null) return findInObject(value);
2763
+ return null;
2764
+ }
2765
+ /** 判断字符串是否含 QQ 数据路径特征。 */
2766
+ function isQqDataPath(value) {
2767
+ return value.includes("Tencent Files") || value.includes("nt_qq");
2768
+ }
2769
+ /** 在数组里递归查找。 */
2770
+ function findInArray(value) {
2771
+ for (const item of value) {
2772
+ const found = findPathInValue(item);
2773
+ if (found !== null) return found;
2774
+ }
2775
+ return null;
2776
+ }
2777
+ /** 在对象值里递归查找。 */
2778
+ function findInObject(value) {
2779
+ for (const v of Object.values(value)) {
2780
+ const found = findPathInValue(v);
2781
+ if (found !== null) return found;
2782
+ }
2783
+ return null;
2784
+ }
2785
+ //#endregion
2786
+ //#region src/wrapper/wrapper-config.ts
2787
+ /**
2788
+ * wrapper 交互配置装配(纯函数,无状态)
2789
+ *
2790
+ * 生成 QQ wrapper 交互所需的三份配置:
2791
+ * - buildEngineConfig → engine.initWithDeskTopConfig(appid / qua / 版本 / 路径)
2792
+ * - buildLoginConfig → loginService.initConfig
2793
+ * - buildSessionConfig → session.init(登录成功后调用)
2794
+ *
2795
+ * 与 lifecycle.ts(流程编排)解耦:本模块只回答「配置长什么样」,不管流程怎么走。
2796
+ * 字段为 wrapper.node 外部契约(appid/qua/版本),运行时实测确认,自研描述。
2797
+ */
2798
+ /** 系统信息(先用 fixed 值,真实环境探测后补)。 */
2799
+ function systemInfo() {
2800
+ return {
2801
+ platVer: "Windows 10.0.22631",
2802
+ osVersion: "Windows 10 Pro",
2803
+ devType: "Windows"
2804
+ };
2805
+ }
2806
+ /** 纯数字串(major.node 提取的 appid 判定)。 */
2807
+ const DIGITS_ONLY_RE = /^\d+$/;
2808
+ /**
2809
+ * 从 major.node 提取 appid(NapCat parseAppidFromMajorV2 的自研等价实现)。
2810
+ * major.node 含 `QQAppId/` 标记后跟数字(腾讯工具链产物,实测确认)。
2811
+ * 返回 null 表示解析失败(调用方回退硬编码表)。
2812
+ */
2813
+ function parseAppidFromMajor(majorPath) {
2814
+ if (!(0, node_fs.existsSync)(majorPath)) return null;
2815
+ let buf;
2816
+ try {
2817
+ buf = (0, node_fs.readFileSync)(majorPath);
2818
+ } catch {
2819
+ return null;
2820
+ }
2821
+ return scanAppidMarkers(buf);
2822
+ }
2823
+ /** 扫描 buf 中所有 QQAppId/ 标记,返回第一个纯数字串(无则 null)。 */
2824
+ function scanAppidMarkers(buf) {
2825
+ const marker = Buffer.from("QQAppId/", "utf-8");
2826
+ let pos = 0;
2827
+ while (pos < buf.length) {
2828
+ const idx = buf.indexOf(marker, pos);
2829
+ if (idx === -1) return null;
2830
+ const str = readMarkerValue(buf, idx + marker.length);
2831
+ if (DIGITS_ONLY_RE.test(str)) return str;
2832
+ pos = idx + marker.length + str.length + 1;
2833
+ }
2834
+ return null;
2835
+ }
2836
+ /** 读取标记后的 C 字符串(到首个 0 字节为止)。 */
2837
+ function readMarkerValue(buf, start) {
2838
+ let end = start;
2839
+ while (end < buf.length && buf[end] !== 0) end += 1;
2840
+ return buf.subarray(start, end).toString("utf-8");
2841
+ }
2842
+ /** Windows 兜底 appid / qua(major.node 解析失败时)。 */
2843
+ function fallbackAppidQua(fullVersion) {
2844
+ return {
2845
+ appid: "537237765",
2846
+ qua: `V1_WIN_NQ_${fullVersion}_${fullVersion.split("-")[1] ?? ""}_GW_B`
2847
+ };
2848
+ }
2849
+ /**
2850
+ * 解析 appid / qua(自研,参考 NapCat QQBasicInfoWrapper 思路但独立实现):
2851
+ * 1. 优先从 major.node 提取 appid(实测 9.9.33-51802 = 537376818)
2852
+ * 2. 回退硬编码表(旧版 537237765)
2853
+ * majorPath 传 wrapper.node 同目录 major.node;不传则跳过 major 解析。
2854
+ */
2855
+ function resolveAppidQua(fullVersion, majorPath) {
2856
+ if (majorPath !== void 0) {
2857
+ const appid = parseAppidFromMajor(majorPath);
2858
+ if (appid !== null) return {
2859
+ appid,
2860
+ qua: `V1_WIN_NQ_${fullVersion}_${fullVersion.split("-")[1] ?? ""}_GW_B`
2861
+ };
2862
+ }
2863
+ return fallbackAppidQua(fullVersion);
2864
+ }
2865
+ /** 生成 engine 桌面配置(wrapper 契约字段)。majorPath 可选(解析 appid/qua)。 */
2866
+ function buildEngineConfig(fullVersion, dataPathGlobal, majorPath) {
2867
+ const { osVersion } = systemInfo();
2868
+ const { qua } = resolveAppidQua(fullVersion, majorPath);
2869
+ return {
2870
+ base_path_prefix: "",
2871
+ platform_type: PlatformType.KWINDOWS,
2872
+ app_type: 4,
2873
+ app_version: fullVersion,
2874
+ os_version: osVersion,
2875
+ use_xlog: true,
2876
+ qua,
2877
+ global_path_config: { desktopGlobalPath: dataPathGlobal },
2878
+ thumb_config: {
2879
+ maxSide: 324,
2880
+ minSide: 48,
2881
+ longLimit: 6,
2882
+ density: 2
2883
+ }
2884
+ };
2885
+ }
2886
+ /** 生成登录初始化配置(externalVersion: false 与 NapCat 同款,扫码兼容关键)。 */
2887
+ function buildLoginConfig(appid, fullVersion, commonPath) {
2888
+ const { platVer } = systemInfo();
2889
+ return {
2890
+ machineId: "",
2891
+ appid,
2892
+ platVer,
2893
+ commonPath,
2894
+ clientVer: fullVersion,
2895
+ hostName: (0, node_os.hostname)(),
2896
+ externalVersion: false
2897
+ };
2898
+ }
2899
+ /** 生成 session 初始化配置(登录成功后调用)。 */
2900
+ function buildSessionConfig(options) {
2901
+ const { appid, fullVersion, selfUin, selfUid, accountPath, downloadPath } = options;
2902
+ const { platVer, osVersion, devType } = systemInfo();
2903
+ const deviceInfo = {
2904
+ guid: "",
2905
+ buildVer: fullVersion,
2906
+ localId: 2052,
2907
+ devName: (0, node_os.hostname)(),
2908
+ devType,
2909
+ vendorName: "",
2910
+ osVer: osVersion,
2911
+ vendorOsName: devType,
2912
+ setMute: false,
2913
+ vendorType: VendorType.KNOSETONIOS
2914
+ };
2915
+ return {
2916
+ selfUin,
2917
+ selfUid,
2918
+ desktopPathConfig: { account_path: accountPath },
2919
+ clientVer: fullVersion,
2920
+ a2: "",
2921
+ d2: "",
2922
+ d2Key: "",
2923
+ machineId: "",
2924
+ platform: PlatformType.KWINDOWS,
2925
+ platVer,
2926
+ appid,
2927
+ rdeliveryConfig: {
2928
+ appKey: "",
2929
+ systemId: 0,
2930
+ appId: "",
2931
+ logicEnvironment: "",
2932
+ platform: PlatformType.KWINDOWS,
2933
+ language: "",
2934
+ sdkVersion: "",
2935
+ userId: "",
2936
+ appVersion: "",
2937
+ osVersion: "",
2938
+ bundleId: "",
2939
+ serverUrl: "",
2940
+ fixedAfterHitKeys: [""]
2941
+ },
2942
+ defaultFileDownloadPath: downloadPath,
2943
+ deviceInfo,
2944
+ deviceConfig: "{\"appearance\":{\"isSplitViewMode\":true},\"msg\":{}}"
2945
+ };
2946
+ }
2947
+ //#endregion
2948
+ //#region src/wrapper/wrapper-loader.ts
2949
+ /**
2950
+ * wrapper 引导(NAPI 范式,2026-08-05 重构,替代旧 koffi 方案)
2951
+ *
2952
+ * 事实(实测):wrapper.node 不是标准 NAPI self-register 模块,只能在 QQ 定制版
2953
+ * Electron 主进程里由 preload 注册。@napuketto/loader 注入 hook DLL 后,boot.cjs
2954
+ * 在 QQ 主进程内截获 `process.dlopen` 的 `module.exports`,然后调用本模块初始化。
2955
+ *
2956
+ * 本模块**不再**使用 koffi / vtable / 内存偏移——所有对象由 QQ 的 NAPI 层自动构建。
2957
+ * 业务层拿到的都是真实 JS 对象:engine.initWithDeskTopConfig(config, adapter) 等。
2958
+ */
2959
+ /** 默认 engine 配置(KWINDOWS + 版本号,字段待首个真实联调确认)。 */
2960
+ function defaultEngineConfig(env, qqDataPath) {
2961
+ return {
2962
+ base_path_prefix: env.dataDir ?? "",
2963
+ platform_type: PlatformType.KWINDOWS,
2964
+ app_type: 4,
2965
+ app_version: env.qqVersion ?? "",
2966
+ os_version: node_process.default.platform,
2967
+ use_xlog: false,
2968
+ qua: "",
2969
+ global_path_config: { desktopGlobalPath: qqDataPath ?? env.dataDir ?? "" },
2970
+ thumb_config: {
2971
+ maxSide: 324,
2972
+ minSide: 48,
2973
+ longLimit: 6,
2974
+ density: 2
2975
+ }
2976
+ };
2977
+ }
2978
+ /**
2979
+ * Electron 进程类型(Node 的 process 类型无此字段)。
2980
+ * - "browser":QQ 主进程(V1 模式)
2981
+ * - "utility":utilityProcess Worker(路线 B,2026-08-06)
2982
+ */
2983
+ function electronProcessType() {
2984
+ return node_process.default.type;
2985
+ }
2986
+ /**
2987
+ * 解析 QQ 用户数据根(登录数据目录,如 `C:\Users\<user>\Documents\Tencent Files`)。
2988
+ * 优先 NodeQQNTWrapperUtil.getNTUserDataInfoConfig()(QQ 官方 API),回退默认位置。
2989
+ *
2990
+ * 背景(2026-08-06 P2-1 实测):worker(utilityProcess)模式下 loginService 是
2991
+ * `new NodeIKernelLoginService()` 新建的,其 initConfig 的 commonPath 必须指向
2992
+ * QQ 真实数据路径才能读到历史账号——cli 的 `.napuketto\default` 数据目录读不到
2993
+ * (getLoginList 返回空 → 只能 QR 登录)。V1 主进程模式 QQ 自己 initConfig 过
2994
+ * 正确路径,无需此解析。
2995
+ */
2996
+ function resolveQqUserDataRoot(exports) {
2997
+ return extractUserDataRoot(exports) ?? (0, node_path.join)((0, node_os.homedir)(), "Documents", "Tencent Files");
2998
+ }
2999
+ /** 从 QQ 官方 API 提取数据根(不可用返回 null,调用方回退默认)。 */
3000
+ function extractUserDataRoot(exports) {
3001
+ try {
3002
+ const raw = exports.NodeQQNTWrapperUtil?.get?.().getNTUserDataInfoConfig?.();
3003
+ if (typeof raw === "string" && raw.trim() !== "") return extractDataRoot(raw.trim());
3004
+ } catch {}
3005
+ return null;
3006
+ }
3007
+ /**
3008
+ * 从 loader 截获的 NAPI exports 创建 wrapper 上下文。
3009
+ * 在 QQ 主进程内调用(boot.cjs → kernel 入口)。
3010
+ */
3011
+ function createWrapper(exports, versionInfo) {
3012
+ const engineMaybe = exports.NodeIQQNTWrapperEngine;
3013
+ if (engineMaybe === null || typeof engineMaybe.get !== "function") throw kernelError("wrapper.node exports 无效(缺少 NodeIQQNTWrapperEngine)", "INVALID_PARAM");
3014
+ const engine = engineMaybe.get();
3015
+ if (!engine || typeof engine.initWithDeskTopConfig !== "function") throw kernelError("NodeIQQNTWrapperEngine.get() 返回对象无效", "INVALID_PARAM");
3016
+ return {
3017
+ exports,
3018
+ engine,
3019
+ versionInfo,
3020
+ session: null,
3021
+ startupSession: null,
3022
+ loginService: null
3023
+ };
3024
+ }
3025
+ /** engine 初始化(wrapper 契约:先 engine 后 session)。config 为普通 JS 对象。 */
3026
+ function initEngine(ctx, config) {
3027
+ ctx.engine.initWithDeskTopConfig(config, new GlobalAdapter());
3028
+ }
3029
+ /**
3030
+ * 创建会话(2026-08-06 P2-0 实测修正——NapCat 方式自研等价):
3031
+ * 1. StartupSessionWrapper.create()(启动会话包装)
3032
+ * 2. getNTWrapperSession("nt_1")(QQ 主 session,带完整 cpp_impl)
3033
+ * 3. S.create() 回退
3034
+ *
3035
+ * 背景:`new NodeIQQNTWrapperSession()` 构造对象缺 cpp_impl,session.init 断言
3036
+ * "implementation of IQQNTWrapperSession is not valid"(P2-0 实测确认)。
3037
+ * NapCat 用 StartupSessionWrapper.create() + getNTWrapperSession('nt_1') 解决——
3038
+ * 返回带完整实现的实例。worker(utilityProcess)继承 QQ env 后同样有效。
3039
+ */
3040
+ function createSession(ctx) {
3041
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3042
+ const session = createStartupSession(ctx) ?? getNTWrapperSession(S) ?? createNewSession(S) ?? new S();
3043
+ ctx.session = session;
3044
+ return session;
3045
+ }
3046
+ /** 步骤 1:StartupSessionWrapper.create()(建立启动 session,失败忽略)。 */
3047
+ function createStartupSession(ctx) {
3048
+ const Ssw = ctx.exports.NodeIQQNTStartupSessionWrapper;
3049
+ if (Ssw === void 0 || typeof Ssw.create !== "function") return null;
3050
+ try {
3051
+ ctx.startupSession = Ssw.create();
3052
+ } catch {}
3053
+ return null;
3054
+ }
3055
+ /** 步骤 2:getNTWrapperSession("nt_1")(QQ 主 session,带 cpp_impl)。 */
3056
+ function getNTWrapperSession(S) {
3057
+ if (typeof S.getNTWrapperSession !== "function") return null;
3058
+ try {
3059
+ return S.getNTWrapperSession("nt_1");
3060
+ } catch {
3061
+ return null;
3062
+ }
3063
+ }
3064
+ /** 步骤 3:S.create() 回退。 */
3065
+ function createNewSession(S) {
3066
+ if (typeof S.create !== "function") return null;
3067
+ try {
3068
+ return S.create();
3069
+ } catch {
3070
+ return null;
3071
+ }
3072
+ }
3073
+ /** session 初始化(4 参全为 JS 对象,NAPI 自动转换)。 */
3074
+ function initSession(ctx, config, listener) {
3075
+ (ctx.session ?? createSession(ctx)).init(config, new DependsAdapter(), new DispatcherAdapter(), listener);
3076
+ }
3077
+ /**
3078
+ * 启动会话(有 startupSession 用 start(),否则 startNT(0))。
3079
+ *
3080
+ * ⚠️ 2026-08-07 V9 修正:单纯 start() 只在「已 init」的 session 上有效。
3081
+ * 自建宿主必须走 initAndStartSession(先 session.init 再 startupSession.start())——
3082
+ * 先 start 后 init 业务 service 不挂载(HANDOVER-V9 实测)。本函数保留给
3083
+ * 已 init 的 session 补启动用;新路径请用 initAndStartSession。
3084
+ */
3085
+ function startSession(ctx) {
3086
+ const { session, startupSession } = ctx;
3087
+ if (session === null) throw kernelError("session 未创建,无法启动", "INVALID_STATE");
3088
+ if (startupSession !== null && typeof startupSession.start === "function") {
3089
+ startupSession.start();
3090
+ return;
3091
+ }
3092
+ session.startNT(0);
3093
+ }
3094
+ /**
3095
+ * boot 装配入口:createWrapper → initEngine → session。
3096
+ *
3097
+ * **session 来源(2026-08-05 修正)**:
3098
+ * 1. qqSession(boot.cjs 拦截 `new` 窃取的 QQ 已 init session)——尚未落地
3099
+ * 2. createSession(`new NodeIQQNTWrapperSession()`)——实测可用,默认路径
3100
+ * (getMainSession 实测为空壳:service 全 null + 缺 startNT,仅 probe 探测参考)
3101
+ *
3102
+ * 由 loader runtime/boot.cjs 在 QQ 主进程内调用(import kernel dist 后)。
3103
+ * 返回 WrapperContext;失败抛 KernelError。
3104
+ */
3105
+ function startNapuketto(options) {
3106
+ const { env, engineConfig, sessionConfig, wrapperExports, qqSession, qqLoginService } = options;
3107
+ const ctx = createWrapper(wrapperExports, {
3108
+ fullVersion: env?.qqVersion ?? "",
3109
+ buildVersion: env?.qqVersion ?? ""
3110
+ });
3111
+ const hasCapturedSession = captureSession(ctx, qqSession);
3112
+ const isSelfHost = electronProcessType() === void 0;
3113
+ prepareSession(ctx, hasCapturedSession, isSelfHost);
3114
+ initEngine(ctx, resolveEngineConfig(ctx, env, engineConfig));
3115
+ resolveLoginService(ctx, qqLoginService);
3116
+ finishSession(ctx, hasCapturedSession, isSelfHost, sessionConfig);
3117
+ return ctx;
3118
+ }
3119
+ /** engine 前 session 准备(自建宿主必须先建,顺序决定性)。 */
3120
+ function prepareSession(ctx, hasCaptured, isSelfHost) {
3121
+ if (!hasCaptured && isSelfHost) createSession(ctx);
3122
+ }
3123
+ /** engine 后收尾:非自建宿主补建 session;配置了 sessionConfig 则 init+start。 */
3124
+ function finishSession(ctx, hasCaptured, isSelfHost, sessionConfig) {
3125
+ if (!hasCaptured && !isSelfHost) createSession(ctx);
3126
+ if (sessionConfig !== void 0) {
3127
+ initSession(ctx, sessionConfig, createSessionListener());
3128
+ startSession(ctx);
3129
+ }
3130
+ }
3131
+ /** engine 配置:显式覆盖优先,否则默认(含 QQ 数据根 global 路径解析)。 */
3132
+ function resolveEngineConfig(ctx, env, engineConfig) {
3133
+ if (engineConfig !== void 0) return engineConfig;
3134
+ return defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx));
3135
+ }
3136
+ /** 捕获 QQ 已 init 的 session(boot.cjs 拦截 `new` 窃取),返回是否捕获成功。 */
3137
+ function captureSession(ctx, qqSession) {
3138
+ if (qqSession === void 0 || qqSession === null) return false;
3139
+ ctx.session = qqSession;
3140
+ return true;
3141
+ }
3142
+ /**
3143
+ * 解析 QQ 真实数据目录的 global 路径(desktopGlobalPath 三要素之三)。
3144
+ * worker(utilityProcess)模式 + 自建宿主(标准 node)下必须指向 QQ 真实
3145
+ * 数据目录(数据根/nt_qq/global,HANDOVER-V6),否则登录数据读写错位
3146
+ * (P2-1 实测 2026-08-06;getLoginList 空)。QQ 主进程(V1)QQ 自己已配置
3147
+ * engine,无需解析。
3148
+ */
3149
+ function resolveBootQqGlobalPath(ctx) {
3150
+ if (electronProcessType() === "browser") return;
3151
+ const root = resolveQqUserDataRoot(ctx.exports);
3152
+ if (root === null) return;
3153
+ return resolveQqGlobalPath(root);
3154
+ }
3155
+ /**
3156
+ * 解析 loginService:优先捕获实例,其次单例 get()(自建宿主/worker 下 QQ 环境
3157
+ * 创建的实例才完整——`new` 自建实例读不到登录列表(getLoginList 空),
3158
+ * HANDOVER-V6 p0-login3 实证),最后 new 回退。
3159
+ */
3160
+ function resolveLoginService(ctx, qqLoginService) {
3161
+ if (qqLoginService !== void 0 && qqLoginService !== null) {
3162
+ ctx.loginService = qqLoginService;
3163
+ return;
3164
+ }
3165
+ ctx.loginService = getOrCreateLoginService(ctx.exports);
3166
+ }
3167
+ /** 单例 get() 优先,缺失回退 new(都失败返回 null)。 */
3168
+ function getOrCreateLoginService(exports) {
3169
+ try {
3170
+ const loginSvcCtor = exports.NodeIKernelLoginService;
3171
+ if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") return loginSvcCtor.get();
3172
+ return new exports.NodeIKernelLoginService();
3173
+ } catch {
3174
+ return null;
3175
+ }
3176
+ }
3177
+ //#endregion
3178
+ //#region src/core.ts
3179
+ /**
3180
+ * NapukettoCore:装配 + 启动(§9 第 6 项,2026-08-05 实现)
3181
+ *
3182
+ * 装配:paths(建目录)→ logger → CoreContext。
3183
+ * 启动:attachWrapper(wrapperExports → startNapuketto)→ login(快速登录 → session.init)。
3184
+ *
3185
+ * 典型用法(boot.cjs 截获 exports 后):
3186
+ * const core = await NapukettoCore.create({ dataRoot, account });
3187
+ * core.attachWrapper(wrapperExports, { qqVersion, dataDir });
3188
+ * await core.login({ appid: "537237765" });
3189
+ *
3190
+ * 与手动拼 startNapuketto/quickLogin/initAndStartSession 等价,但把装配状态
3191
+ * (ctx)集中管理,协议层直接消费 core.ctx。
3192
+ */
3193
+ /**
3194
+ * 核心装配器:持有 CoreContext,负责启动/停止编排。
3195
+ * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
3196
+ */
3197
+ var NapukettoCore = class NapukettoCore {
3198
+ /** 装配根(协议层 / apis / cache 的消费入口)。 */
3199
+ ctx;
3200
+ /** 由 create() 装配,不直接 new。 */
3201
+ constructor(ctx) {
3202
+ this.ctx = ctx;
3203
+ }
3204
+ /**
3205
+ * 装配:paths(建目录)→ logger → CoreContext。
3206
+ * 不加载 wrapper / 不登录——启动阶段由 attachWrapper + login 完成。
3207
+ */
3208
+ static create(opts = {}) {
3209
+ const paths = new PathWrapper(opts.paths);
3210
+ paths.ensure();
3211
+ const logger = createLogger({
3212
+ level: opts.logLevel ?? "info",
3213
+ console: opts.consoleLog ?? true,
3214
+ file: opts.logFile ?? paths.file("logs", "napuketto.log"),
3215
+ base: { name: "kernel" }
3216
+ });
3217
+ return new NapukettoCore(createCoreContext({
3218
+ logger,
3219
+ paths
3220
+ }));
3221
+ }
3222
+ /**
3223
+ * 装配 wrapper:wrapperExports → startNapuketto(engine.init + session 创建),
3224
+ * 结果挂到 ctx.wrapper。在 QQ 主进程内调用(boot.cjs 截获 exports 后)。
3225
+ * captured:boot.cjs 拦截 `new` 捕获的 QQ 实例(session/loginService,优先使用)。
3226
+ */
3227
+ attachWrapper(wrapperExports, env, captured) {
3228
+ let wrapper;
3229
+ if (env === void 0) wrapper = startNapuketto({
3230
+ wrapperExports,
3231
+ ...captured
3232
+ });
3233
+ else wrapper = startNapuketto({
3234
+ wrapperExports,
3235
+ env,
3236
+ ...captured
3237
+ });
3238
+ this.ctx.wrapper = wrapper;
3239
+ this.ctx.logger.info({ version: env?.qqVersion }, "wrapper 装配完成(engine + session)");
3240
+ return wrapper;
3241
+ }
3242
+ /**
3243
+ * 登录 + session 初始化(wrapper 契约,2026-08-05 实测确认):
3244
+ * loginService.initConfig → 登录(快速/QR)→ **登录成功后** create session + init
3245
+ * → 等 onOpentelemetryInit(is_init) 完成信号。成功后填 ctx.login。
3246
+ *
3247
+ * 关键(实测):登录前 create()/init 均断言失败(session 未 init / 干扰 QQ);
3248
+ * 登录后 QQ 自己 init 并触发 listener.onOpentelemetryInit——我们只需在登录后
3249
+ * create + init 并监听该信号。
3250
+ */
3251
+ async login(opts) {
3252
+ const { wrapper } = this.ctx;
3253
+ if (wrapper === null) throw kernelError("wrapper 未装配,无法登录", "INVALID_STATE");
3254
+ this.initLoginConfig(wrapper, opts.appid);
3255
+ let loginResult;
3256
+ try {
3257
+ const quickOpts = {};
3258
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
3259
+ loginResult = await quickLogin(wrapper, quickOpts);
3260
+ this.ctx.logger.info({
3261
+ uin: loginResult.uin,
3262
+ uid: loginResult.uid
3263
+ }, "快速登录成功");
3264
+ } catch (err) {
3265
+ if (opts.qrFallback !== true) throw err;
3266
+ loginResult = await this.loginByQr(opts);
3267
+ }
3268
+ this.ctx.login = loginResult;
3269
+ return loginResult;
3270
+ }
3271
+ /**
3272
+ * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
3273
+ * worker(utilityProcess)模式 + 自建宿主(标准 node)下 loginService 是
3274
+ * new 的,commonPath 必须指向 QQ 真实数据目录(数据根/nt_qq/global,
3275
+ * HANDOVER-V6 三要素之三)才能读到历史账号——cli 的 `.napuketto\default`
3276
+ * 读不到(getLoginList 空,P2-1 实测 2026-08-06)。
3277
+ * QQ 主进程(V1)模式 QQ 自己 initConfig 过正确路径,无需解析。
3278
+ */
3279
+ initLoginConfig(wrapper, appid) {
3280
+ const loginService = wrapper.loginService;
3281
+ if (loginService === null || typeof loginService.initConfig !== "function") {
3282
+ this.ctx.logger.warn("loginService 不可用,跳过 initConfig");
3283
+ return;
3284
+ }
3285
+ const commonPath = resolveCommonPath(this.ctx, wrapper);
3286
+ const loginCfg = buildLoginConfig(appid, wrapper.versionInfo.fullVersion, commonPath);
3287
+ loginService.initConfig(loginCfg);
3288
+ this.ctx.logger.info({ commonPath }, "loginService.initConfig OK");
3289
+ }
3290
+ /**
3291
+ * 替换当前 session(登录后 Proxy 捕获的 QQ 新实例替换自己 create 的无效实例)。
3292
+ * 在 QQ 主进程内由 boot.cjs 调用(登录成功后)。
3293
+ */
3294
+ setSession(session) {
3295
+ if (this.ctx.wrapper === null) throw kernelError("wrapper 未装配,无法设置 session", "INVALID_STATE");
3296
+ this.ctx.wrapper.session = session;
3297
+ }
3298
+ /** QR 登录:QrLoginSession 状态机 + 二维码写缓存目录(无 UI,cli 可打印路径)。 */
3299
+ async loginByQr(opts) {
3300
+ const { wrapper } = this.ctx;
3301
+ if (wrapper === null || wrapper.loginService === null) throw kernelError("wrapper/loginService 不可用,无法 QR 登录", "INVALID_STATE");
3302
+ const session = new QrLoginSession(wrapper.loginService);
3303
+ const qrPath = opts.qrCodePath ?? this.ctx.paths.file("cache", "qrcode.png");
3304
+ this.subscribeQrProgress(session, opts, qrPath);
3305
+ session.start();
3306
+ await waitQrLoggedIn(session);
3307
+ const self = session.selfInfo;
3308
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
3309
+ session.stop();
3310
+ return {
3311
+ uin: self.uin,
3312
+ uid: self.uid,
3313
+ nick: self.nick
3314
+ };
3315
+ }
3316
+ /** 订阅 QR 进度:二维码写盘 + 状态机经 onLoginProgress 转发。 */
3317
+ subscribeQrProgress(session, opts, qrPath) {
3318
+ session.onQrCode((qr) => {
3319
+ this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
3320
+ if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
3321
+ opts.onLoginProgress?.({
3322
+ state: "waiting_scan",
3323
+ qr
3324
+ });
3325
+ });
3326
+ session.onStateChange((state) => {
3327
+ this.ctx.logger.info({ state }, "QR 登录状态");
3328
+ opts.onLoginProgress?.({ state });
3329
+ });
3330
+ }
3331
+ /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
3332
+ stop() {
3333
+ this.ctx.logger.info("NapukettoCore 停止");
3334
+ }
3335
+ };
3336
+ /** 写二维码 png 到磁盘(base64 解码,供 cli/用户扫码)。 */
3337
+ function writeQrCodePng(filePath, pngBase64) {
3338
+ try {
3339
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(filePath), { recursive: true });
3340
+ (0, node_fs.writeFileSync)(filePath, Buffer.from(pngBase64, "base64"));
3341
+ } catch {}
3342
+ }
3343
+ /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3344
+ function waitQrLoggedIn(session) {
3345
+ return new Promise((resolve, reject) => {
3346
+ const offState = session.onStateChange((state) => {
3347
+ if (state === "logged_in") {
3348
+ offState();
3349
+ resolve();
3350
+ } else if (state === "failed") {
3351
+ offState();
3352
+ reject(kernelError("QR 登录失败", "NOT_LOGIN"));
3353
+ }
3354
+ });
3355
+ });
3356
+ }
3357
+ /**
3358
+ * loginService.initConfig 的 commonPath:
3359
+ * worker/自建宿主(非 browser)优先 QQ 真实数据目录(数据根/nt_qq/global),
3360
+ * 否则回退账号目录。
3361
+ */
3362
+ function resolveCommonPath(ctx, wrapper) {
3363
+ if (electronProcessType() !== "browser") {
3364
+ const root = resolveQqUserDataRoot(wrapper.exports);
3365
+ if (root !== null) return resolveQqGlobalPath(root);
3366
+ }
3367
+ return ctx.paths.accountDir;
3368
+ }
3369
+ //#endregion
3370
+ //#region src/event-channel.ts
3371
+ /**
3372
+ * 类型化事件通道(ADR-003)
3373
+ *
3374
+ * - 事件名约定 `${Service}/${method}`(如 "Msg/onRecvMsg"),签名从 Listener 接口编译期推导
3375
+ * - 事件名写错 → 编译期报错(类型推导)
3376
+ * - `on` 返回 unsubscribe;`waitFor` 注册临时监听 → filter → 超时自动清理(替代魔法字符串方案)
3377
+ * - error 兜底:监听器抛异常不打断派发,统一经 `onError` 通知
3378
+ *
3379
+ * 组合实现而非 extends EventEmitter:EventEmitter.on 返回 `this`,与设计要求的
3380
+ * 返回 unsubscribe 冲突;组合更干净,且内核无全局单例(ADR-015 推论)。
3381
+ *
3382
+ * 类型推导说明:不直接 `ListenerEvents<L, Name>[E]` 索引(泛型下会扩宽到
3383
+ * string|number|symbol 索引,索引出 unknown),而是用 Extract 按事件名从
3384
+ * `{ name, fn }` 联合精确提取 handler / 参数类型,保证实例化后签名精确。
3385
+ */
3386
+ /** waitFor 默认超时(毫秒)。 */
3387
+ const DEFAULT_WAIT_TIMEOUT_MS = 3e4;
3388
+ /**
3389
+ * 类型化事件通道。
3390
+ *
3391
+ * @typeParam L - Listener 接口(探测产物,如 MsgListener)
3392
+ * @typeParam Name - Service 名(事件名前缀,如 "Msg")
3393
+ */
3394
+ var NTEventChannel = class {
3395
+ serviceName;
3396
+ emitter = new node_events.EventEmitter();
3397
+ errorHandlers = /* @__PURE__ */ new Set();
3398
+ anyHandlers = /* @__PURE__ */ new Set();
3399
+ constructor(serviceName) {
3400
+ this.serviceName = serviceName;
3401
+ this.emitter.setMaxListeners(0);
3402
+ }
3403
+ /**
3404
+ * 订阅事件:返回 unsubscribe(只注销该 handler,不影响其他订阅者)。
3405
+ * 每个 Service 只注册一次原生监听,缓存维护与协议翻译都订阅这里。
3406
+ */
3407
+ on(event, handler) {
3408
+ this.emitter.addListener(event, handler);
3409
+ return () => {
3410
+ this.emitter.removeListener(event, handler);
3411
+ };
3412
+ }
3413
+ /**
3414
+ * 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
3415
+ * 替代魔法字符串事件名方案。
3416
+ */
3417
+ waitFor(event, opts = {}) {
3418
+ const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
3419
+ return new Promise((resolve, reject) => {
3420
+ const handler = (...args) => {
3421
+ if (opts.filter && !opts.filter(...args)) return;
3422
+ cleanup();
3423
+ resolve(args);
3424
+ };
3425
+ const timer = setTimeout(() => {
3426
+ cleanup();
3427
+ reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
3428
+ }, timeoutMs);
3429
+ const cleanup = () => {
3430
+ clearTimeout(timer);
3431
+ this.emitter.removeListener(event, handler);
3432
+ };
3433
+ this.emitter.addListener(event, handler);
3434
+ });
3435
+ }
3436
+ /**
3437
+ * 类型化派发:wrapper-loader 的原生回调经此推入通道。
3438
+ * 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
3439
+ * listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
3440
+ * 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
3441
+ */
3442
+ /**
3443
+ * 订阅所有事件(IPC 子进程模式整通道转发用:事件名 `"${service}/${method}"`
3444
+ * 经 handler 首参透出,args 原样透传)。返回退订函数。
3445
+ */
3446
+ onAny(handler) {
3447
+ this.anyHandlers.add(handler);
3448
+ return () => {
3449
+ this.anyHandlers.delete(handler);
3450
+ };
3451
+ }
3452
+ emit(event, ...args) {
3453
+ for (const anyHandler of this.anyHandlers) try {
3454
+ anyHandler(event, ...args);
3455
+ } catch (err) {
3456
+ this.notifyError(err);
3457
+ }
3458
+ const listeners = this.emitter.listeners(event);
3459
+ for (const listener of listeners) try {
3460
+ listener(...args);
3461
+ } catch (err) {
3462
+ this.notifyError(err);
3463
+ }
3464
+ }
3465
+ /** 通知所有错误处理器(单个异常不打断其他处理器)。 */
3466
+ notifyError(err) {
3467
+ for (const handler of this.errorHandlers) handler(err);
3468
+ }
3469
+ /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
3470
+ onError(handler) {
3471
+ this.errorHandlers.add(handler);
3472
+ return () => {
3473
+ this.errorHandlers.delete(handler);
3474
+ };
3475
+ }
3476
+ };
3477
+ //#endregion
3478
+ //#region src/wrapper/session-resolver.ts
3479
+ /** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
3480
+ function findMainSessionId(ids) {
3481
+ return firstStringId(ids, (s) => s.startsWith("nt_")) ?? firstStringId(ids, () => true);
3482
+ }
3483
+ /** 遍历 Map 找第一个满足谓词的字符串 id(值优先,其次键)。 */
3484
+ function firstStringId(ids, pred) {
3485
+ for (const [k, v] of ids) {
3486
+ if (typeof v === "string" && pred(v)) return v;
3487
+ if (typeof k === "string" && pred(k)) return k;
3488
+ }
3489
+ return null;
3490
+ }
3491
+ /** 通过 getNTWrapperSession 拿主 session(内部辅助)。 */
3492
+ function resolveMainSession(created, getNTWrapperSession) {
3493
+ created.start?.();
3494
+ const ids = typeof created.getSessionIdList === "function" ? created.getSessionIdList() : null;
3495
+ if (!(ids instanceof Map)) return null;
3496
+ const mainId = findMainSessionId(ids);
3497
+ if (mainId === null) return null;
3498
+ const maybe = getNTWrapperSession(mainId);
3499
+ return maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function" ? maybe : null;
3500
+ }
3501
+ /**
3502
+ * 复用 QQ 已有 session(P1-4:QQ 已登录,直接拿单例避免重复 init)。
3503
+ * 优先 `NodeIQQNTWrapperSession.get()`;返回 null 时回退 createSession。
3504
+ */
3505
+ function getExistingSession(ctx) {
3506
+ const got = trySessionGet(ctx);
3507
+ if (got !== null) {
3508
+ ctx.session = got;
3509
+ return got;
3510
+ }
3511
+ return null;
3512
+ }
3513
+ /** NodeIQQNTWrapperSession.get() 单例(无效/异常返回 null)。 */
3514
+ function trySessionGet(ctx) {
3515
+ try {
3516
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3517
+ if (typeof S.get !== "function") return null;
3518
+ const got = S.get();
3519
+ return got && typeof got.getMsgService === "function" ? got : null;
3520
+ } catch {
3521
+ return null;
3522
+ }
3523
+ }
3524
+ /**
3525
+ * 复用 QQ 主 session(P1-4 实测链路):
3526
+ * `NodeIQQNTStartupSessionWrapper.create()` → `start()` → `getSessionIdList()`(Map
3527
+ * {nt:"nt_3", gpro:"gpro_3"})→ `NodeIQQNTWrapperSession.getNTWrapperSession("nt_3")`。
3528
+ * 注:实测这些 session 是空壳(核心 service null),真正可用的是 new + init(lifecycle.ts)。
3529
+ */
3530
+ function getMainSession(ctx) {
3531
+ try {
3532
+ return resolveMainFromStartup(ctx) ?? null;
3533
+ } catch {
3534
+ return null;
3535
+ }
3536
+ }
3537
+ /** startup.create → start → getSessionIdList → getNTWrapperSession 链路。 */
3538
+ function resolveMainFromStartup(ctx) {
3539
+ const startupRaw = ctx.exports.NodeIQQNTStartupSessionWrapper;
3540
+ const sessionCtor = ctx.exports.NodeIQQNTWrapperSession;
3541
+ if (typeof startupRaw.create !== "function" || typeof sessionCtor.getNTWrapperSession !== "function") return null;
3542
+ const created = startupRaw.create();
3543
+ if (!created) return null;
3544
+ const getSession = (id) => sessionCtor.getNTWrapperSession(id);
3545
+ const session = resolveMainSession(created, getSession);
3546
+ if (session !== null) ctx.session = session;
3547
+ return session;
3548
+ }
3549
+ //#endregion
3550
+ //#region src/wrapper/probe/probe-serialize.ts
3551
+ /**
3552
+ * 运行时反射探测的序列化工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
3553
+ *
3554
+ * - serialize:深度受限序列化(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)
3555
+ * - tryShape:解析 service getter 返回形状
3556
+ * - shapeKeyGetters:探测 service 关键数据 getter 返回形状
3557
+ */
3558
+ /** 序列化深度上限(循环引用/深层对象防护)。 */
3559
+ const MAX_SERIALIZE_DEPTH = 4;
3560
+ /** 数组序列化条数上限。 */
3561
+ const MAX_ARRAY_ITEMS = 20;
3562
+ /** 对象键序列化条数上限。 */
3563
+ const MAX_OBJECT_KEYS = 50;
3564
+ /** 序列化一个值(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)。 */
3565
+ function serialize(value, depth = 0) {
3566
+ if (value === null || value === void 0) return value;
3567
+ if (typeof value === "bigint") return value.toString();
3568
+ if (typeof value === "function") return `[function ${value.name ?? "anon"}]`;
3569
+ if (typeof value !== "object") return value;
3570
+ if (depth > MAX_SERIALIZE_DEPTH) return "[depth-limit]";
3571
+ try {
3572
+ return serializeContainer(value, depth);
3573
+ } catch {
3574
+ return "[unserializable]";
3575
+ }
3576
+ }
3577
+ /** 序列化容器对象(数组 / Map / Set / Promise / 普通对象)。 */
3578
+ function serializeContainer(value, depth) {
3579
+ if (Array.isArray(value)) return serializeArray(value, depth);
3580
+ if (value instanceof Map) return serializeMap(value, depth);
3581
+ if (value instanceof Set) return serializeSet(value, depth);
3582
+ if (typeof value.then === "function") return "[Promise]";
3583
+ return serializeObject(value, depth);
3584
+ }
3585
+ /** 数组序列化(条数截断)。 */
3586
+ function serializeArray(value, depth) {
3587
+ return value.slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1));
3588
+ }
3589
+ /** Map 序列化(键转字符串,条目截断)。 */
3590
+ function serializeMap(value, depth) {
3591
+ const out = {};
3592
+ let i = 0;
3593
+ for (const [k, v] of value) {
3594
+ if (i >= MAX_OBJECT_KEYS) break;
3595
+ out[String(k)] = serialize(v, depth + 1);
3596
+ i += 1;
3597
+ }
3598
+ return {
3599
+ kind: "Map",
3600
+ size: value.size,
3601
+ entries: out
3602
+ };
3603
+ }
3604
+ /** Set 序列化(值截断)。 */
3605
+ function serializeSet(value, depth) {
3606
+ return {
3607
+ kind: "Set",
3608
+ size: value.size,
3609
+ values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
3610
+ };
3611
+ }
3612
+ /** 普通对象序列化(键截断)。 */
3613
+ function serializeObject(value, depth) {
3614
+ const out = {};
3615
+ for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
3616
+ return out;
3617
+ }
3618
+ /** 尝试解析一个 service getter 的返回值形状(深度受限)。 */
3619
+ function tryShape(session, getter) {
3620
+ try {
3621
+ const fn = session[getter];
3622
+ if (typeof fn !== "function") return null;
3623
+ const value = fn.call(session);
3624
+ if (value === null || value === void 0) return null;
3625
+ return serialize(value);
3626
+ } catch {
3627
+ return "[error]";
3628
+ }
3629
+ }
3630
+ /** 数据 getter 方法名模式(探测返回形状用)。 */
3631
+ const DATA_GETTER_RE = /^(get|query|fetch|load)/i;
3632
+ /** 探测 service 的关键数据 getter 返回形状(取前几个无参调用)。 */
3633
+ function shapeKeyGetters(session, serviceMethods) {
3634
+ const out = {};
3635
+ const candidates = serviceMethods.filter((m) => DATA_GETTER_RE.test(m) && !m.endsWith("Listener"));
3636
+ for (const m of candidates.slice(0, 8)) out[m] = tryShape(session, m);
3637
+ return out;
3638
+ }
3639
+ //#endregion
3640
+ //#region src/wrapper/probe/probe-utils.ts
3641
+ /**
3642
+ * 运行时反射探测基础工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
3643
+ *
3644
+ * - listMethods:反射枚举对象原型链上的方法名(去重、去构造器)
3645
+ * - tryCall:安全调用方法拿返回值(失败记录错误,不中断探测)
3646
+ */
3647
+ /** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
3648
+ function listMethods(obj) {
3649
+ if (obj === null || obj === void 0) return [];
3650
+ const names = /* @__PURE__ */ new Set();
3651
+ collectPrototypeNames(obj, names);
3652
+ collectOwnNames(obj, names);
3653
+ return [...names].sort();
3654
+ }
3655
+ /** 收集原型链上的属性名(到 Object/Function.prototype 为止,去构造器)。 */
3656
+ function collectPrototypeNames(obj, names) {
3657
+ let proto = Object.getPrototypeOf(obj);
3658
+ while (isProbeProto(proto)) {
3659
+ for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3660
+ proto = Object.getPrototypeOf(proto);
3661
+ }
3662
+ }
3663
+ /** 是否仍需遍历的原型(非 Object/Function.prototype 且非空)。 */
3664
+ function isProbeProto(proto) {
3665
+ return proto !== null && proto !== Object.prototype && proto !== Function.prototype;
3666
+ }
3667
+ /** 收集自有属性名(去构造器)。 */
3668
+ function collectOwnNames(obj, names) {
3669
+ for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3670
+ }
3671
+ /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3672
+ function tryCall(obj, name) {
3673
+ try {
3674
+ const fn = obj[name];
3675
+ if (typeof fn !== "function") return {
3676
+ ok: false,
3677
+ error: `not a function (typeof=${typeof fn})`
3678
+ };
3679
+ return {
3680
+ ok: true,
3681
+ value: fn.call(obj)
3682
+ };
3683
+ } catch (e) {
3684
+ let message = String(e);
3685
+ if (e instanceof Error) message = e.message;
3686
+ return {
3687
+ ok: false,
3688
+ error: message
3689
+ };
3690
+ }
3691
+ }
3692
+ //#endregion
3693
+ //#region src/wrapper/probe/probe-services.ts
3694
+ /** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
3695
+ function probeExportCtors(ctx) {
3696
+ const keys = [
3697
+ "NodeIQQNTWrapperSession",
3698
+ "NodeIQQNTStartupSessionWrapper",
3699
+ "NodeQQNTWrapperUtil",
3700
+ "NodeIKernelLoginService",
3701
+ "NodeIQQNTWrapperEngine"
3702
+ ];
3703
+ const out = {};
3704
+ for (const key of keys) {
3705
+ const ctor = ctx.exports[key];
3706
+ if (ctor !== null && ctor !== void 0) out[key] = {
3707
+ methods: listMethods(ctor),
3708
+ ownKeys: Object.getOwnPropertyNames(ctor)
3709
+ };
3710
+ }
3711
+ return out;
3712
+ }
3713
+ /** 探测 engine 关键调用(getDeviceInfo / getECDHService / getThirdPartySigService)。 */
3714
+ function probeEngineCalls(ctx) {
3715
+ const out = {};
3716
+ for (const m of ENGINE_CALL_PROBES) out[m] = probeEngineCall(ctx, m);
3717
+ return out;
3718
+ }
3719
+ /** 探测单个 engine 方法调用(成功记录方法面/形状,失败记录错误)。 */
3720
+ function probeEngineCall(ctx, m) {
3721
+ const call = tryCall(ctx.engine, m);
3722
+ return call.ok ? {
3723
+ value: serialize(call.value),
3724
+ methods: call.value ? listMethods(call.value) : []
3725
+ } : { error: call.error ?? "null/undefined" };
3726
+ }
3727
+ /** engine 关键调用候选。 */
3728
+ const ENGINE_CALL_PROBES = [
3729
+ "getDeviceInfo",
3730
+ "getECDHService",
3731
+ "getThirdPartySigService",
3732
+ "readyToShow"
3733
+ ];
3734
+ /** 探测 LoginService 实例(QQ 已登录凭据入口)。 */
3735
+ function probeLoginService(ctx) {
3736
+ const ctor = ctx.exports.NodeIKernelLoginService;
3737
+ if (ctor === null || typeof ctor.get !== "function") return null;
3738
+ const inst = tryCall(ctor, "get");
3739
+ if (!(inst.ok && inst.value)) return { get: inst.error ?? "null/undefined" };
3740
+ return {
3741
+ methods: listMethods(inst.value),
3742
+ ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30),
3743
+ ...probeLoginGetters(inst.value)
3744
+ };
3745
+ }
3746
+ /** 尝试关键登录信息 getter(无参调用,探测形状)。 */
3747
+ function probeLoginGetters(inst) {
3748
+ const out = {};
3749
+ for (const m of LOGIN_GETTER_PROBES) if (typeof inst[m] === "function") out[m] = tryShape(inst, m);
3750
+ return out;
3751
+ }
3752
+ /** 登录信息 getter 候选(无参调用,探测形状)。 */
3753
+ const LOGIN_GETTER_PROBES = [
3754
+ "getAccountInfo",
3755
+ "getLoginInfo",
3756
+ "getA2",
3757
+ "getTicket",
3758
+ "getUin",
3759
+ "getUid",
3760
+ "getSelfUin"
3761
+ ];
3762
+ //#endregion
3763
+ //#region src/wrapper/probe/probe.ts
3764
+ /**
3765
+ * 运行时反射探测(ADR-006 落地,P1-4)
3766
+ *
3767
+ * 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
3768
+ * 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
3769
+ * 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
3770
+ *
3771
+ * 用法:boot.cjs 在 NAPUTO_PROBE=1 时于 startNapuketto 后调用 probeRuntime(ctx)。
3772
+ * 产物是研究工具数据,类型层据此补全(自研描述,非移植)。
3773
+ *
3774
+ * 工具拆分(2026-08-08 FTA 优化):方法枚举/安全调用 → probe-utils.ts;
3775
+ * 序列化/形状探测 → probe-serialize.ts;exports/service 探测 → probe-services.ts;
3776
+ * 本文件只留 session 探测编排(probeStartup/probeSession/probeRuntime)。
3777
+ */
3778
+ /** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
3779
+ function enumerateSessionIds(ctx) {
3780
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3781
+ const enumFn = getNTWrapperSessionFn(ctx);
3782
+ if (enumFn === null) return {};
3783
+ const out = {};
3784
+ for (let i = 0; i < 10; i += 1) {
3785
+ probeSessionId(enumFn, S, `nt_${i}`, out);
3786
+ probeSessionId(enumFn, S, `gpro_${i}`, out);
3787
+ }
3788
+ return out;
3789
+ }
3790
+ /** 探测单个 sessionId(getter 存在性 + getMsgService 调用结果)。 */
3791
+ function probeSessionId(enumFn, thisCtx, id, out) {
3792
+ try {
3793
+ const value = enumFn.call(thisCtx, id);
3794
+ if (!value) return;
3795
+ out[id] = probeSessionRecord(value);
3796
+ } catch {}
3797
+ }
3798
+ /** 探测单个 session 记录(getter 存在性 + msgService 调用结果 + 方法面)。 */
3799
+ function probeSessionRecord(value) {
3800
+ const rec = value;
3801
+ const msg = tryMsgService(rec, value);
3802
+ return {
3803
+ hasMsgServiceGetter: typeof rec["getMsgService"] === "function",
3804
+ msgServiceOk: msg.service !== null,
3805
+ msgServiceMethods: msg.service ? listMethods(msg.service).length : 0,
3806
+ msgError: msg.error,
3807
+ methods: listMethods(value)
3808
+ };
3809
+ }
3810
+ /** 尝试调用 getMsgService(失败记录错误,成功返回实例)。 */
3811
+ function tryMsgService(rec, thisCtx) {
3812
+ try {
3813
+ return {
3814
+ service: rec["getMsgService"].call(thisCtx) ?? null,
3815
+ error: null
3816
+ };
3817
+ } catch (e) {
3818
+ return {
3819
+ service: null,
3820
+ error: e instanceof Error ? e.message : String(e)
3821
+ };
3822
+ }
3823
+ }
3824
+ /** 取 NodeIQQNTWrapperSession.getNTWrapperSession 函数(无则 null)。 */
3825
+ function getNTWrapperSessionFn(ctx) {
3826
+ const fn = ctx.exports.NodeIQQNTWrapperSession["getNTWrapperSession"];
3827
+ return typeof fn === "function" ? fn : null;
3828
+ }
3829
+ /** 探测 startup 实例:start() + getSessionIdList(),提取 sessionIds(Map 值或键)。 */
3830
+ function probeStartupChain(created) {
3831
+ const started = tryCall(created, "start");
3832
+ const ids = tryCall(created, "getSessionIdList");
3833
+ return {
3834
+ start: started.ok ? { ok: true } : { error: started.error },
3835
+ createdGetSessionIdList: ids.ok ? { value: serialize(ids.value) } : { error: ids.error },
3836
+ sessionIds: extractSessionIds(ids)
3837
+ };
3838
+ }
3839
+ /** 从 getSessionIdList 结果提取 sessionIds(Map 值或键优先)。 */
3840
+ function extractSessionIds(ids) {
3841
+ if (!(ids.ok && ids.value instanceof Map)) return [];
3842
+ const out = [];
3843
+ for (const [k, v] of ids.value) out.push(typeof v === "string" ? v : String(k));
3844
+ return out;
3845
+ }
3846
+ /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
3847
+ function probeSessionById(ctx, id) {
3848
+ const fn = getNTWrapperSessionFn(ctx);
3849
+ if (fn === null) return null;
3850
+ try {
3851
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, id);
3852
+ return value ? {
3853
+ methods: listMethods(value),
3854
+ ownKeys: Object.getOwnPropertyNames(value)
3855
+ } : null;
3856
+ } catch (e) {
3857
+ return { error: e instanceof Error ? e.message : String(e) };
3858
+ }
3859
+ }
3860
+ /** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
3861
+ function probeByNameFallback(ctx) {
3862
+ const candidates = [
3863
+ "main",
3864
+ "primary",
3865
+ "session1",
3866
+ "default",
3867
+ ""
3868
+ ];
3869
+ const getResults = {};
3870
+ for (const name of candidates) getResults[name === "" ? "<empty>" : name] = probeSessionByName(ctx, name);
3871
+ return getResults;
3872
+ }
3873
+ /** 探测单个候选名(getNTWrapperSession(name),记录方法面或错误)。 */
3874
+ function probeSessionByName(ctx, name) {
3875
+ try {
3876
+ const fn = getNTWrapperSessionFn(ctx);
3877
+ if (fn === null) return null;
3878
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, name);
3879
+ return value ? {
3880
+ methods: listMethods(value),
3881
+ ownKeys: Object.getOwnPropertyNames(value)
3882
+ } : null;
3883
+ } catch (e) {
3884
+ return { error: e instanceof Error ? e.message : String(e) };
3885
+ }
3886
+ }
3887
+ /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3888
+ function probeStartup(ctx) {
3889
+ const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
3890
+ if (startup === null || typeof startup.create !== "function") return null;
3891
+ const out = { staticMethods: listMethods(startup) };
3892
+ const created = tryCall(startup, "create");
3893
+ out["create"] = created.ok ? {
3894
+ methods: created.value ? listMethods(created.value) : [],
3895
+ value: serialize(created.value)
3896
+ } : { error: created.error };
3897
+ if (created.ok && created.value) Object.assign(out, probeCreatedChain(ctx, created.value));
3898
+ out["enumSessionIds"] = enumerateSessionIds(ctx);
3899
+ if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
3900
+ return out;
3901
+ }
3902
+ /** 探测 create 后的实例链:start / getSessionIdList / 主 session。 */
3903
+ function probeCreatedChain(ctx, created) {
3904
+ const chain = probeStartupChain(created);
3905
+ const out = {
3906
+ start: chain.start,
3907
+ createdGetSessionIdList: chain.createdGetSessionIdList,
3908
+ sessionIds: chain.sessionIds
3909
+ };
3910
+ const mainId = findMainProbeId(chain.sessionIds);
3911
+ if (mainId !== void 0) {
3912
+ out["mainSessionId"] = mainId;
3913
+ out["mainSession"] = probeSessionById(ctx, mainId);
3914
+ }
3915
+ return out;
3916
+ }
3917
+ /** 选主 sessionId(nt_ 前缀优先,否则第一个)。 */
3918
+ function findMainProbeId(sessionIds) {
3919
+ return sessionIds.find((id) => id.startsWith("nt_")) ?? sessionIds[0];
3920
+ }
3921
+ /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
3922
+ function probeSession(ctx) {
3923
+ const out = {};
3924
+ const session = pickProbeSession(ctx, out);
3925
+ out["session"] = session ? {
3926
+ methods: listMethods(session),
3927
+ ownKeys: Object.getOwnPropertyNames(session)
3928
+ } : null;
3929
+ if (session !== null && session !== void 0) out["services"] = collectServiceGetters(session);
3930
+ return out;
3931
+ }
3932
+ /** 选探测目标 session:优先 QQ 主 session(getMainSession),回退 ctx.session。 */
3933
+ function pickProbeSession(ctx, out) {
3934
+ try {
3935
+ const main = getMainSession(ctx);
3936
+ if (main !== null) {
3937
+ out["sessionSource"] = "mainSession(getNTWrapperSession)";
3938
+ return main;
3939
+ }
3940
+ } catch {}
3941
+ if (ctx.session !== null) {
3942
+ out["sessionSource"] = "own-create";
3943
+ return ctx.session;
3944
+ }
3945
+ return null;
3946
+ }
3947
+ /** 收集 session 的 get*Service getter 方法面与返回形状。 */
3948
+ function collectServiceGetters(session) {
3949
+ const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
3950
+ const services = {};
3951
+ for (const getter of getters) services[getter] = probeServiceGetter(session, getter);
3952
+ return services;
3953
+ }
3954
+ /** 探测单个 getter:成功记录方法面/形状,失败记录错误。 */
3955
+ function probeServiceGetter(session, getter) {
3956
+ const call = tryCall(session, getter);
3957
+ if (!(call.ok && call.value)) return {
3958
+ error: call.error ?? "null/undefined",
3959
+ ok: call.ok
3960
+ };
3961
+ const methods = listMethods(call.value);
3962
+ return {
3963
+ methods,
3964
+ ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
3965
+ shapes: shapeKeyGetters(call.value, methods)
3966
+ };
3967
+ }
3968
+ /**
3969
+ * 探测 wrapper 运行时:dump session 方法与各 service 方法到 JSON。
3970
+ * 返回探测结果对象;同时写文件(NAPUTO_CFG_DIR/<file>,默认 napuketto-probe.json)。
3971
+ */
3972
+ function probeRuntime(ctx, file = "napuketto-probe.json") {
3973
+ const result = {
3974
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3975
+ exports: listMethods(ctx.exports),
3976
+ engineMethods: listMethods(ctx.engine),
3977
+ exportCtors: probeExportCtors(ctx),
3978
+ engineCalls: probeEngineCalls(ctx),
3979
+ loginService: probeLoginService(ctx),
3980
+ startup: probeStartup(ctx),
3981
+ ...probeSession(ctx)
3982
+ };
3983
+ try {
3984
+ const dir = node_process.default.env["NAPUTO_CFG_DIR"] ?? node_process.default.cwd();
3985
+ (0, node_fs.mkdirSync)(dir, { recursive: true });
3986
+ const full = (0, node_path.join)(dir, file);
3987
+ (0, node_fs.writeFileSync)(full, JSON.stringify(result, null, 2), "utf8");
3988
+ result["probeFile"] = full;
3989
+ } catch (e) {
3990
+ result["probeFileError"] = e instanceof Error ? e.message : String(e);
3991
+ }
3992
+ return result;
3993
+ }
3994
+ //#endregion
3995
+ //#region src/wrapper/wrapper-version.ts
3996
+ /**
3997
+ * QQ 安装目录探测(ADR-018)
3998
+ *
3999
+ * wrapper.node 路径随 QQ 版本变化:`<安装目录>/versions/<版本>/resources/app/wrapper.node`。
4000
+ * 加载前必须知道版本(登录握手参数 appid/qua 与版本强相关)。
4001
+ *
4002
+ * 已实测确认(2026-08-05,QQ 9.9.31-49919):
4003
+ * - 目录结构:versions/<9.9.31-49919>/resources/app/wrapper.node
4004
+ * - 版本信息在 versions/<版本>/resources/app/package.json(name=qq-chat / version / buildVersion)
4005
+ * - wrapper.node 是 C++ ABI 模块(非 N-API),导出 INTSessionShell 等符号
4006
+ */
4007
+ /** 从 versions 目录读取版本 package.json(拿 name/version/buildVersion)。 */
4008
+ function readVersionPackage(installDir, version) {
4009
+ const pkgPath = (0, node_path.join)(installDir, "versions", version, "resources", "app", "package.json");
4010
+ if (!(0, node_fs.existsSync)(pkgPath)) throw kernelError(`QQ 版本目录缺少 package.json: ${pkgPath}`, "NOT_FOUND");
4011
+ try {
4012
+ const raw = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf8"));
4013
+ return {
4014
+ name: raw.name ?? "",
4015
+ version: raw.version ?? "",
4016
+ ...raw.buildVersion !== void 0 ? { buildVersion: raw.buildVersion } : {}
4017
+ };
4018
+ } catch (cause) {
4019
+ throw kernelError(`QQ 版本 package.json 解析失败: ${pkgPath}`, "INVALID_PARAM", { cause });
4020
+ }
4021
+ }
4022
+ /** 解析 wrapper.node 绝对路径(按 ADR-018 目录约定)。 */
4023
+ function resolveWrapperPath(installDir, version) {
4024
+ return (0, node_path.join)(installDir, "versions", version, "resources", "app", "wrapper.node");
4025
+ }
4026
+ /**
4027
+ * 探测指定版本信息(校验 wrapper.node 存在)。
4028
+ * 版本号不匹配目录时抛 NOT_FOUND(明确失败而非静默)。
4029
+ */
4030
+ function resolveQQVersion(installDir, version) {
4031
+ const wrapperPath = resolveWrapperPath(installDir, version);
4032
+ if (!(0, node_fs.existsSync)(wrapperPath)) throw kernelError(`未找到 wrapper.node: ${wrapperPath}`, "NOT_FOUND");
4033
+ const pkg = readVersionPackage(installDir, version);
4034
+ return {
4035
+ fullVersion: pkg.version || version,
4036
+ buildVersion: pkg.buildVersion ?? "",
4037
+ wrapperPath,
4038
+ appid: "",
4039
+ qua: ""
4040
+ };
4041
+ }
4042
+ /** 扫描 versions 目录,取可加载的版本列表(按目录名倒序,最新在前)。 */
4043
+ function listQQVersions(installDir) {
4044
+ const versionsDir = (0, node_path.join)(installDir, "versions");
4045
+ if (!(0, node_fs.existsSync)(versionsDir)) return [];
4046
+ return (0, node_fs.readdirSync)(versionsDir).filter((name) => {
4047
+ const full = (0, node_path.join)(versionsDir, name);
4048
+ return (0, node_fs.statSync)(full).isDirectory() && (0, node_fs.existsSync)((0, node_path.join)(full, "resources", "app", "wrapper.node"));
4049
+ }).sort().reverse();
4050
+ }
4051
+ //#endregion
4052
+ exports.ChatType = ChatType;
4053
+ exports.ConfigBase = ConfigBase;
4054
+ exports.DEFAULT_DATA_ROOT_NAME = DEFAULT_DATA_ROOT_NAME;
4055
+ exports.ElementType = ElementType;
4056
+ exports.FriendApi = FriendApi;
4057
+ exports.GrayTipSubType = GrayTipSubType;
4058
+ exports.GroupApi = GroupApi;
4059
+ exports.GroupBridge = GroupBridge;
4060
+ exports.GroupCache = GroupCache;
4061
+ exports.GroupListUpdateType = GroupListUpdateType;
4062
+ exports.GroupNotifyApi = GroupNotifyApi;
4063
+ exports.GroupNotifyMsgStatus = GroupNotifyMsgStatus;
4064
+ exports.GroupNotifyMsgType = GroupNotifyMsgType;
4065
+ exports.KERNEL_ERROR_CODES = KERNEL_ERROR_CODES;
4066
+ exports.KernelError = KernelError;
4067
+ exports.MAIN_CONFIG_FILE = MAIN_CONFIG_FILE;
4068
+ exports.MsgApi = MsgApi;
4069
+ exports.MsgBridge = MsgBridge;
4070
+ exports.NTEventChannel = NTEventChannel;
4071
+ exports.NTGroupMemberRole = NTGroupMemberRole;
4072
+ exports.NTGroupRequestOperateTypes = NTGroupRequestOperateTypes;
4073
+ exports.NapukettoCore = NapukettoCore;
4074
+ exports.PathWrapper = PathWrapper;
4075
+ exports.ProfileApi = ProfileApi;
4076
+ exports.ProfileLikeApi = ProfileLikeApi;
4077
+ exports.QrLoginSession = QrLoginSession;
4078
+ exports.RichMediaApi = RichMediaApi;
4079
+ exports.TicketApi = TicketApi;
4080
+ exports.TipGroupElementType = TipGroupElementType;
4081
+ exports.WebApi = WebApi;
4082
+ exports.WebHonorType = WebHonorType;
4083
+ exports.buildEngineConfig = buildEngineConfig;
4084
+ exports.buildLoginConfig = buildLoginConfig;
4085
+ exports.buildSessionConfig = buildSessionConfig;
4086
+ exports.createCoreContext = createCoreContext;
4087
+ exports.createLifecycleSessionListener = createSessionListener;
4088
+ exports.createLogger = createLogger;
4089
+ exports.createLoginListener = createLoginListener;
4090
+ exports.createSession = createSession;
4091
+ exports.createSessionListener = createSessionListener;
4092
+ exports.createWrapper = createWrapper;
4093
+ exports.electronProcessType = electronProcessType;
4094
+ exports.getExistingSession = getExistingSession;
4095
+ exports.getMainSession = getMainSession;
4096
+ exports.initAndStartSession = initAndStartSession;
4097
+ exports.initEngine = initEngine;
4098
+ exports.initSession = initSession;
4099
+ exports.isKernelError = isKernelError;
4100
+ exports.kernelError = kernelError;
4101
+ exports.listLoginAccounts = listLoginAccounts;
4102
+ exports.listQQVersions = listQQVersions;
4103
+ exports.parseAppidFromMajor = parseAppidFromMajor;
4104
+ exports.parseToml = parseToml;
4105
+ exports.probeRuntime = probeRuntime;
4106
+ exports.quickLogin = quickLogin;
4107
+ exports.resolveAppidQua = resolveAppidQua;
4108
+ exports.resolveConfigPath = resolveConfigPath;
4109
+ exports.resolveDataRoot = resolveDataRoot;
4110
+ exports.resolveQQVersion = resolveQQVersion;
4111
+ exports.resolveQqGlobalPath = resolveQqGlobalPath;
4112
+ exports.resolveQqUserDataRoot = resolveQqUserDataRoot;
4113
+ exports.resolveWrapperPath = resolveWrapperPath;
4114
+ exports.startNapuketto = startNapuketto;
4115
+ exports.startSession = startSession;
4116
+ exports.stringifyToml = stringifyToml;
4117
+ exports.toCanonicalElements = toCanonicalElements;
4118
+ exports.toSendElements = toSendElements;
4119
+ exports.waitForNetworkConnection = waitForNetworkConnection;
4120
+ exports.waitSessionReady = waitSessionReady;