@napuketto/kernel 0.0.1 → 0.0.2

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.mjs CHANGED
@@ -1,14 +1,14 @@
1
- import { get } from "node:https";
2
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { readFile, rename, writeFile } from "node:fs/promises";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
+ import { parse, stringify } from "smol-toml";
4
5
  import process from "node:process";
5
6
  import pino from "pino";
6
7
  import pinoPretty from "pino-pretty";
7
8
  import { homedir, hostname } from "node:os";
8
9
  import { fileURLToPath } from "node:url";
10
+ import { get } from "node:https";
9
11
  import { EventEmitter } from "node:events";
10
- import { readFile, rename, writeFile } from "node:fs/promises";
11
- import { parse, stringify } from "smol-toml";
12
12
  //#region src/infra/errors.ts
13
13
  /**
14
14
  * 全部错误码集合。协议层映射表可据此推导:
@@ -32,14 +32,371 @@ var KernelError = class extends Error {
32
32
  this.name = "KernelError";
33
33
  this.code = code;
34
34
  }
35
- };
36
- /** 便捷构造:`kernelError('消息不存在', 'NOT_FOUND', { cause })`。 */
37
- function kernelError(message, code = "UNKNOWN", options) {
38
- return new KernelError(message, code, options);
35
+ };
36
+ /** 便捷构造:`kernelError('消息不存在', 'NOT_FOUND', { cause })`。 */
37
+ function kernelError(message, code = "UNKNOWN", options) {
38
+ return new KernelError(message, code, options);
39
+ }
40
+ /** 类型守卫:协议层 catch 统一映射时区分 KernelError 与意外错误。 */
41
+ function isKernelError(err) {
42
+ return err instanceof KernelError;
43
+ }
44
+ //#endregion
45
+ //#region src/infra/config.ts
46
+ /**
47
+ * 配置基类(ADR-012)
48
+ *
49
+ * 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
50
+ * (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
51
+ *
52
+ * kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
53
+ * 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
54
+ * 协议配置 schema 在各自协议包,见各包 docs/design.md。
55
+ */
56
+ /** 按扩展名推断格式(.toml → toml,其余 → json)。 */
57
+ function inferFormat(path) {
58
+ if (extname(path).toLowerCase() === ".toml") return "toml";
59
+ return "json";
60
+ }
61
+ function isMissingFileError(err) {
62
+ if (err instanceof Error) return err.code === "ENOENT";
63
+ return false;
64
+ }
65
+ /**
66
+ * 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
67
+ * 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
68
+ * - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
69
+ * - `save()`:按格式序列化(TOML/JSON)原子写。
70
+ */
71
+ var ConfigBase = class {
72
+ path;
73
+ defaults;
74
+ schema;
75
+ format;
76
+ seed;
77
+ value;
78
+ constructor(opts) {
79
+ this.path = opts.path;
80
+ this.defaults = opts.defaults;
81
+ this.schema = opts.schema;
82
+ this.format = opts.format ?? inferFormat(opts.path);
83
+ this.seed = opts.seed;
84
+ this.value = opts.seed ?? opts.defaults;
85
+ }
86
+ /** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
87
+ get() {
88
+ return this.value;
89
+ }
90
+ /**
91
+ * 加载配置:
92
+ * - seed 存在 → 直接用 seed(已校验,不读文件;全局 TOML 分段装配方式);
93
+ * - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
94
+ * - 文件存在 → 读取 + 校验,更新内存,返回。
95
+ */
96
+ async load() {
97
+ if (this.seed !== void 0) {
98
+ this.value = this.seed;
99
+ return this.value;
100
+ }
101
+ let raw;
102
+ try {
103
+ raw = await readFile(this.path, "utf8");
104
+ } catch (err) {
105
+ if (isMissingFileError(err)) {
106
+ await this.save(this.defaults);
107
+ return this.value;
108
+ }
109
+ throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
110
+ }
111
+ this.value = this.parse(raw);
112
+ return this.value;
113
+ }
114
+ /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
115
+ reload() {
116
+ return this.load();
117
+ }
118
+ /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
119
+ async save(next) {
120
+ const parsed = this.parse(this.serialize(next));
121
+ mkdirSync(dirname(this.path), { recursive: true });
122
+ const tmpPath = `${this.path}.tmp`;
123
+ await writeFile(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
124
+ await rename(tmpPath, this.path);
125
+ this.value = parsed;
126
+ }
127
+ /** 对象 → 文本(TOML/JSON)。 */
128
+ serialize(value) {
129
+ if (this.format === "toml") return stringify(value);
130
+ return JSON.stringify(value, null, 4);
131
+ }
132
+ /** 文本 → 对象(TOML/JSON)。 */
133
+ deserialize(raw) {
134
+ if (this.format === "toml") return parse(raw);
135
+ return JSON.parse(raw);
136
+ }
137
+ /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
138
+ parse(raw) {
139
+ let input;
140
+ try {
141
+ input = this.deserialize(raw);
142
+ } catch (err) {
143
+ throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
144
+ }
145
+ try {
146
+ return this.schema.parse(input);
147
+ } catch (err) {
148
+ throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
149
+ }
150
+ }
151
+ };
152
+ /** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
153
+ function parseToml(text) {
154
+ return parse(text);
155
+ }
156
+ /** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
157
+ function stringifyToml(value) {
158
+ return stringify(value);
159
+ }
160
+ //#endregion
161
+ //#region src/infra/logger.ts
162
+ /**
163
+ * pino 日志封装(ADR-007)
164
+ *
165
+ * - console(pino-pretty 可读输出)+ 可选文件双目的地
166
+ * - 统一 level 控制
167
+ * - 内置 redact(token / 票据等敏感字段不打日志),可追加
168
+ * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
169
+ */
170
+ /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
171
+ const DEFAULT_REDACT_PATHS = [
172
+ "token",
173
+ "ticket",
174
+ "cookie",
175
+ "password",
176
+ "secret"
177
+ ];
178
+ function toSingle(streams) {
179
+ const [only] = streams;
180
+ if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
181
+ return only.stream;
182
+ }
183
+ /**
184
+ * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
185
+ *
186
+ * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
187
+ * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
188
+ */
189
+ function createLogger(opts = {}) {
190
+ const level = opts.level ?? "info";
191
+ const consoleEnabled = opts.console ?? true;
192
+ const { file, base } = opts;
193
+ const streams = [];
194
+ if (consoleEnabled) streams.push({ stream: pinoPretty({
195
+ colorize: true,
196
+ translateTime: "SYS:HH:MM:ss.l",
197
+ singleLine: false,
198
+ destination: process.stdout
199
+ }) });
200
+ if (file) {
201
+ mkdirSync(dirname(file), { recursive: true });
202
+ streams.push({ stream: pino.destination({
203
+ dest: file,
204
+ sync: true
205
+ }) });
206
+ }
207
+ if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
208
+ const pinoOptions = {
209
+ level,
210
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
211
+ };
212
+ if (base) pinoOptions.base = {
213
+ pid: process.pid,
214
+ ...base
215
+ };
216
+ let stream;
217
+ if (streams.length === 1) stream = toSingle(streams);
218
+ else stream = pino.multistream(streams);
219
+ return pino(pinoOptions, stream);
220
+ }
221
+ //#endregion
222
+ //#region src/infra/paths.ts
223
+ /**
224
+ * 数据目录布局(ADR-016,2026-08-08 修订:数据根默认放项目根)
225
+ *
226
+ * ```
227
+ * <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
228
+ * ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
229
+ * ├── logs/ # pino 文件日志
230
+ * └── cache/ # 临时文件、媒体缓存
231
+ *
232
+ * <项目根>/napuketto.toml # 全局配置文件(放项目根目录,不入库)
233
+ * <项目根>/.napuketto/ # 数据根(默认值,2026-08-08 用户拍板:直接放项目主目录)
234
+ * ```
235
+ *
236
+ * 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 >
237
+ * `<项目根>/.napuketto`(默认,部署原因不放用户目录)。
238
+ * 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
239
+ */
240
+ /** 默认数据根目录名(项目根下,2026-08-08 用户拍板:不放用户目录)。 */
241
+ const DEFAULT_DATA_ROOT_NAME = ".napuketto";
242
+ /** 全局配置文件名。 */
243
+ const MAIN_CONFIG_FILE = "napuketto.toml";
244
+ /** `~` 前缀展开为用户主目录(跨平台配置文件写法 `dataDir = "~/.napuketto"`)。 */
245
+ function expandTilde(p) {
246
+ if (p === "~") return homedir();
247
+ if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
248
+ return p;
249
+ }
250
+ /**
251
+ * 项目根查找:入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即仓库根)→
252
+ * cwd 向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)→ undefined(兜底)。
253
+ */
254
+ function resolveProjectRoot() {
255
+ const moduleRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url)), "pnpm-workspace.yaml");
256
+ if (moduleRoot !== void 0) return moduleRoot;
257
+ return findProjectRoot(process.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(process.cwd(), "package.json");
258
+ }
259
+ /**
260
+ * 解析数据根:显式参数 > 环境变量 > `<项目根>/.napuketto`(默认)> 用户目录兜底(裸脚本)。
261
+ * 支持 `~` 前缀(展开为用户主目录)与相对路径(相对 cwd 解析)。
262
+ * 返回绝对路径。独立导出便于 cli / 探测脚本复用。
263
+ */
264
+ function resolveDataRoot(dataRoot) {
265
+ const explicit = dataRoot ?? process.env["NAPKETTO_DATA"];
266
+ if (explicit !== void 0 && explicit !== "") return resolve(expandTilde(explicit));
267
+ const projectRoot = resolveProjectRoot();
268
+ if (projectRoot !== void 0) return join(projectRoot, DEFAULT_DATA_ROOT_NAME);
269
+ return join(homedir(), DEFAULT_DATA_ROOT_NAME);
270
+ }
271
+ /** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
272
+ function findProjectRoot(startDir, marker) {
273
+ let dir = startDir;
274
+ for (;;) {
275
+ if (existsSync(join(dir, marker))) return dir;
276
+ const parent = dirname(dir);
277
+ if (parent === dir) return;
278
+ dir = parent;
279
+ }
280
+ }
281
+ /**
282
+ * 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
283
+ * 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
284
+ *
285
+ * 优先级:
286
+ * 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
287
+ * 2. 项目根探测(入口模块向上找 pnpm-workspace.yaml → cwd 向上找,见 resolveProjectRoot)
288
+ * 3. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
289
+ *
290
+ * 返回绝对路径。独立导出便于 cli / loader 装配链复用。
291
+ */
292
+ function resolveConfigPath(opts = {}) {
293
+ const explicit = process.env["NAPKETTO_CONFIG"];
294
+ if (explicit !== void 0 && explicit !== "") return resolve(explicit);
295
+ const projectRoot = resolveProjectRoot();
296
+ if (projectRoot !== void 0) return join(projectRoot, MAIN_CONFIG_FILE);
297
+ return join(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
298
+ }
299
+ /**
300
+ * 路径装配器:持有账号数据目录下的全部子目录。
301
+ * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
302
+ */
303
+ var PathWrapper = class {
304
+ /** 数据根(绝对路径)。 */
305
+ root;
306
+ /** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
307
+ accountDir;
308
+ /** config 目录(napuketto.json + 各协议配置)。 */
309
+ configDir;
310
+ /** logs 目录(pino 文件日志)。 */
311
+ logsDir;
312
+ /** cache 目录(临时文件、媒体缓存)。 */
313
+ cacheDir;
314
+ constructor(opts = {}) {
315
+ const root = resolveDataRoot(opts.dataRoot);
316
+ this.root = root;
317
+ if (opts.account) this.accountDir = join(root, opts.account);
318
+ else this.accountDir = root;
319
+ this.configDir = join(this.accountDir, "config");
320
+ this.logsDir = join(this.accountDir, "logs");
321
+ this.cacheDir = join(this.accountDir, "cache");
322
+ }
323
+ /** 递归创建全部目录,启动装配时调用一次。 */
324
+ ensure() {
325
+ for (const dir of [
326
+ this.configDir,
327
+ this.logsDir,
328
+ this.cacheDir
329
+ ]) mkdirSync(dir, { recursive: true });
330
+ }
331
+ /** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
332
+ file(...segments) {
333
+ return join(this.accountDir, ...segments);
334
+ }
335
+ /**
336
+ * 清空缓存目录(clean_cache 动作经注入回调消费)。
337
+ * 只删除 cacheDir 下的条目,保留目录本身。
338
+ */
339
+ clearCache() {
340
+ let entries;
341
+ try {
342
+ entries = readdirSync(this.cacheDir);
343
+ } catch {
344
+ return;
345
+ }
346
+ for (const entry of entries) rmSync(join(this.cacheDir, entry), {
347
+ recursive: true,
348
+ force: true
349
+ });
350
+ }
351
+ };
352
+ //#endregion
353
+ //#region src/apis/result.ts
354
+ /**
355
+ * 原生调用结果解包(ADR-009 统一错误语义)
356
+ *
357
+ * wrapper.node 的 service 方法大多返回 `{ result, errMsg }`:
358
+ * - unwrap:result 为 number 且非 0 → 抛 KernelError
359
+ * - checkLooseResult:宽松形状(result 可能缺失/非数字)→ 仅当 result 为
360
+ * 数字且非 0 时抛错(探测期兼容:某些 API 成功时 result 缺省或为对象)
361
+ * - unwrapResult:result !== 0 → 按 errMsg 内容映射语义错误码(msg 系专用)
362
+ */
363
+ /** errMsg 关键词 → 错误码规则(顺序即优先级,命中任一组关键词即映射)。 */
364
+ const RESULT_CODE_RULES = [
365
+ {
366
+ code: "NOT_LOGIN",
367
+ keywords: ["未登录", "login"]
368
+ },
369
+ {
370
+ code: "PERMISSION_DENIED",
371
+ keywords: ["无权限", "permission"]
372
+ },
373
+ {
374
+ code: "NOT_FOUND",
375
+ keywords: ["不存在", "not found"]
376
+ }
377
+ ];
378
+ /** 按 errMsg 关键词映射错误码(未命中 → UNKNOWN)。 */
379
+ function mapResultCode(msg) {
380
+ for (const rule of RESULT_CODE_RULES) if (rule.keywords.some((k) => msg.includes(k))) return rule.code;
381
+ return "UNKNOWN";
39
382
  }
40
- /** 类型守卫:协议层 catch 统一映射时区分 KernelError 与意外错误。 */
41
- function isKernelError(err) {
42
- return err instanceof KernelError;
383
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
384
+ function unwrap(label, result, errMsg) {
385
+ if (result === 0) return;
386
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
387
+ }
388
+ /** 宽松 result 校验(result 为数字非 0 视为失败;缺失/非数字视为成功)。 */
389
+ function checkLooseResult(label, res) {
390
+ if (res === void 0 || res === null || typeof res.result !== "number" || res.result === 0) return;
391
+ throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
392
+ }
393
+ /** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
394
+ function unwrapResult(label, raw) {
395
+ if (raw.result === 0) return;
396
+ const msg = raw.errMsg || "无错误详情";
397
+ let code = mapResultCode(msg);
398
+ if (code === "UNKNOWN" && label === "sendMsg") code = "SEND_FAILED";
399
+ throw kernelError(`${label} 失败: ${msg}`, code);
43
400
  }
44
401
  //#endregion
45
402
  //#region src/apis/friend.ts
@@ -51,11 +408,6 @@ function isKernelError(err) {
51
408
  * (getBuddyReq / approvalFriendRequest / setBuddyRemark / delBuddy /
52
409
  * getDoubtBuddyReq / approvalDoubtBuddyReq)。
53
410
  */
54
- /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
55
- function unwrap$3(label, result, errMsg) {
56
- if (result === 0) return;
57
- throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
58
- }
59
411
  /** 好友 API:从 session 拿 buddy service,包装成语义化方法。 */
60
412
  var FriendApi = class {
61
413
  service;
@@ -126,7 +478,7 @@ var FriendApi = class {
126
478
  /** 好友申请列表(set_friend_add_request 应答前查找匹配项)。 */
127
479
  async getBuddyReqList() {
128
480
  const raw = await this.service.getBuddyReq();
129
- unwrap$3("getBuddyReq", raw.result, raw.errMsg);
481
+ unwrap("getBuddyReq", raw.result, raw.errMsg);
130
482
  return raw.buddyReqs ?? [];
131
483
  }
132
484
  /** 应答好友申请(accept=true 同意;remark 由调用方另行 setFriendRemark)。 */
@@ -146,18 +498,17 @@ var FriendApi = class {
146
498
  }
147
499
  /** 删除好友(tempBlock=拉黑,tempBothDel=双向删除)。 */
148
500
  async deleteFriend(uid, tempBlock = false, tempBothDel = false) {
149
- const res = await this.service.delBuddy({
501
+ checkLooseResult("delBuddy", await this.service.delBuddy({
150
502
  friendUid: uid,
151
503
  tempBlock,
152
504
  tempBothDel
153
- });
154
- if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`delBuddy 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
505
+ }));
155
506
  }
156
507
  /** 可疑好友申请列表(get_doubt_friends_add_request;uin 经 uidToUin 转换)。 */
157
508
  async getDoubtFriendRequest(count) {
158
509
  const reqId = String(Date.now());
159
510
  const raw = await this.service.getDoubtBuddyReq(reqId, count, "");
160
- unwrap$3("getDoubtBuddyReq", raw.result, raw.errMsg);
511
+ unwrap("getDoubtBuddyReq", raw.result, raw.errMsg);
161
512
  const list = raw.doubtList ?? [];
162
513
  let uinMap = /* @__PURE__ */ new Map();
163
514
  if (this.uidToUin !== void 0 && list.length > 0) uinMap = await this.uidToUin(list.map((item) => item.uid));
@@ -226,6 +577,254 @@ const TipGroupElementType = {
226
577
  SHUT_UP: 8
227
578
  };
228
579
  //#endregion
580
+ //#region src/types/listeners/group.ts
581
+ /** 群列表更新类型(说明书参考,值待探测校准)。 */
582
+ const GroupListUpdateType = {
583
+ /** 全量更新(groupList 为完整列表)。 */
584
+ ALL: 0,
585
+ /** 增量更新(合并到现有缓存)。 */
586
+ PARTIAL: 1
587
+ };
588
+ //#endregion
589
+ //#region src/types/services/msg-service.ts
590
+ /** 消息元素类型枚举(QQ wrapper 外部契约,自研描述)。 */
591
+ const ElementType = {
592
+ UNKNOWN: 0,
593
+ TEXT: 1,
594
+ PIC: 2,
595
+ FILE: 3,
596
+ PTT: 4,
597
+ VIDEO: 5,
598
+ FACE: 6,
599
+ REPLY: 7,
600
+ GRAY_TIP: 8,
601
+ WALLET: 9,
602
+ ARK: 10,
603
+ MFACE: 11,
604
+ LIVE_GIFT: 12,
605
+ STRUCT_LONG_MSG: 13,
606
+ MARKDOWN: 14,
607
+ GIPHY: 15,
608
+ MULTI_FORWARD: 16,
609
+ INLINE_KEYBOARD: 17,
610
+ INTEXT_GIFT: 18,
611
+ CALENDAR: 19
612
+ };
613
+ //#endregion
614
+ //#region src/types/message-element.ts
615
+ /**
616
+ * canonical 消息元素模型(协议无关,ADR-008 延伸)
617
+ *
618
+ * 描述 QQ 消息的事实结构,是各协议翻译层的公共中间表示:
619
+ * kernel 只做一次 NT ↔ canonical(toCanonicalElements / toSendElements),
620
+ * 各协议(onebot11/satori)只写薄映射 canonical ↔ 协议格式。
621
+ */
622
+ /** 文本 at 类型(QQ wrapper 契约,自研描述)。 */
623
+ const AtType = {
624
+ ALL: 1,
625
+ ONE: 2,
626
+ ME: 4
627
+ };
628
+ /** TEXT 元素 → canonical(at 或纯文本)。 */
629
+ function textToCanonical(textEl) {
630
+ if (textEl === void 0) return {
631
+ type: "text",
632
+ text: ""
633
+ };
634
+ const { atType, atUid, content } = textEl;
635
+ if (atType === AtType.ALL) return {
636
+ type: "at",
637
+ target: "all"
638
+ };
639
+ if (atType === AtType.ONE || atType === AtType.ME) {
640
+ const base = {
641
+ type: "at",
642
+ target: atUid ?? content
643
+ };
644
+ const display = content.startsWith("@") ? content.slice(1) : content;
645
+ if (display === "") return base;
646
+ return {
647
+ ...base,
648
+ display
649
+ };
650
+ }
651
+ return {
652
+ type: "text",
653
+ text: content
654
+ };
655
+ }
656
+ /** PIC 元素 → canonical image。 */
657
+ function picToCanonical(pic) {
658
+ if (pic === void 0) return {
659
+ type: "image",
660
+ path: ""
661
+ };
662
+ const path = pic.picPath ?? pic.picUrl ?? "";
663
+ const url = pic.picUrl;
664
+ if (url === void 0) return {
665
+ type: "image",
666
+ path
667
+ };
668
+ return {
669
+ type: "image",
670
+ path,
671
+ url
672
+ };
673
+ }
674
+ /** FACE 元素 → canonical face。 */
675
+ function faceToCanonical(face) {
676
+ if (face === void 0) return {
677
+ type: "face",
678
+ id: "0"
679
+ };
680
+ return {
681
+ type: "face",
682
+ id: String(face.faceIndex)
683
+ };
684
+ }
685
+ /** PTT 元素 → canonical voice。 */
686
+ function pttToCanonical(ptt) {
687
+ if (ptt === void 0) return {
688
+ type: "voice",
689
+ path: ""
690
+ };
691
+ return {
692
+ type: "voice",
693
+ path: ptt.filePath ?? ""
694
+ };
695
+ }
696
+ /** VIDEO 元素 → canonical video。 */
697
+ function videoToCanonical(video) {
698
+ if (video === void 0) return {
699
+ type: "video",
700
+ path: ""
701
+ };
702
+ const path = video.filePath ?? video.videoUrl ?? "";
703
+ const url = video.videoUrl;
704
+ if (url === void 0) return {
705
+ type: "video",
706
+ path
707
+ };
708
+ return {
709
+ type: "video",
710
+ path,
711
+ url
712
+ };
713
+ }
714
+ /** FILE 元素 → canonical file。 */
715
+ function fileToCanonical(file) {
716
+ if (file === void 0) return {
717
+ type: "file",
718
+ path: ""
719
+ };
720
+ const path = file.filePath ?? "";
721
+ const name = file.fileName;
722
+ if (name === void 0) return {
723
+ type: "file",
724
+ path
725
+ };
726
+ return {
727
+ type: "file",
728
+ path,
729
+ name
730
+ };
731
+ }
732
+ /** REPLY 元素 → canonical reply。 */
733
+ function replyToCanonical(reply) {
734
+ if (reply === void 0) return {
735
+ type: "reply",
736
+ messageId: ""
737
+ };
738
+ return {
739
+ type: "reply",
740
+ messageId: reply.replayMsgId
741
+ };
742
+ }
743
+ /** 单个 NT 元素 → canonical(接收方向,宽容:无法表达回 unknown,不抛错)。 */
744
+ function toCanonicalElement(el) {
745
+ switch (el.elementType) {
746
+ case ElementType.TEXT: return textToCanonical(el.textElement);
747
+ case ElementType.PIC: return picToCanonical(el.picElement);
748
+ case ElementType.FACE: return faceToCanonical(el.faceElement);
749
+ case ElementType.PTT: return pttToCanonical(el.pttElement);
750
+ case ElementType.VIDEO: return videoToCanonical(el.videoElement);
751
+ case ElementType.FILE: return fileToCanonical(el.fileElement);
752
+ case ElementType.REPLY: return replyToCanonical(el.replyElement);
753
+ default: return {
754
+ type: "unknown",
755
+ raw: el
756
+ };
757
+ }
758
+ }
759
+ /** NT RawMessage → canonical 元素(与 toSendElements 对称,接收方向)。 */
760
+ function toCanonicalElements(msg) {
761
+ return msg.elements.map(toCanonicalElement);
762
+ }
763
+ /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心五类)。 */
764
+ function toSendElements(elements) {
765
+ const out = [];
766
+ for (const el of elements) switch (el.type) {
767
+ case "text":
768
+ out.push({
769
+ elementType: ElementType.TEXT,
770
+ textElement: { content: el.text }
771
+ });
772
+ break;
773
+ case "at":
774
+ if (el.target === "all") out.push({
775
+ elementType: ElementType.TEXT,
776
+ textElement: {
777
+ content: el.display ?? "@",
778
+ atType: 1
779
+ }
780
+ });
781
+ else out.push({
782
+ elementType: ElementType.TEXT,
783
+ textElement: {
784
+ content: el.display ?? "@",
785
+ atType: 2,
786
+ atUid: el.target
787
+ }
788
+ });
789
+ break;
790
+ case "face":
791
+ out.push({
792
+ elementType: ElementType.FACE,
793
+ faceElement: { faceIndex: Number(el.id) }
794
+ });
795
+ break;
796
+ case "image":
797
+ out.push({
798
+ elementType: ElementType.PIC,
799
+ picElement: { picPath: el.path }
800
+ });
801
+ break;
802
+ case "voice":
803
+ out.push({
804
+ elementType: ElementType.PTT,
805
+ pttElement: { filePath: el.path }
806
+ });
807
+ break;
808
+ case "reply":
809
+ out.push({
810
+ elementType: ElementType.REPLY,
811
+ replyElement: {
812
+ replayMsgId: el.messageId,
813
+ replayMsgSeq: el.messageId,
814
+ replayMsgTime: "0"
815
+ }
816
+ });
817
+ break;
818
+ case "video":
819
+ case "file":
820
+ case "forward":
821
+ case "json":
822
+ case "xml":
823
+ case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
824
+ }
825
+ return out;
826
+ }
827
+ //#endregion
229
828
  //#region src/types/services/group-service.ts
230
829
  /** 群成员角色(QQ wrapper 契约)。 */
231
830
  const NTGroupMemberRole = {
@@ -271,6 +870,36 @@ const NTGroupRequestOperateTypes = {
271
870
  KDELETE: 4
272
871
  };
273
872
  //#endregion
873
+ //#region src/types/wrapper.ts
874
+ /**
875
+ * wrapper.node 的 NAPI 接口面(自研描述,非移植)
876
+ *
877
+ * 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
878
+ * 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
879
+ * 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
880
+ *
881
+ * 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
882
+ */
883
+ /** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
884
+ const PlatformType = {
885
+ KUNKNOWN: 0,
886
+ KANDROID: 1,
887
+ KIOS: 2,
888
+ KWINDOWS: 3,
889
+ KMAC: 4,
890
+ KLINUX: 5
891
+ };
892
+ /** 厂商类型(deviceInfo.vendorType)。 */
893
+ const VendorType = {
894
+ KNOSETONIOS: 0,
895
+ KUNSUPPORTANDROIDPUSH: 1,
896
+ KSUPPORTTPNS: 2,
897
+ KSUPPORTHMS: 3,
898
+ KSUPPORTOPPOPUSH: 4,
899
+ KSUPPORTVIVOPUSH: 5,
900
+ KSUPPORTGOOGLEPUSH: 99
901
+ };
902
+ //#endregion
274
903
  //#region src/apis/group.ts
275
904
  /**
276
905
  * GroupApi:群语义化 API(ADR-009 统一错误语义)
@@ -279,11 +908,6 @@ const NTGroupRequestOperateTypes = {
279
908
  * 方法面(P2-4 + P2-10):群列表 / 群详情 / 成员列表 / 成员详情 / uin↔uid +
280
909
  * 群管操作(踢人 / 禁言 / 管理员 / 名片 / 群名 / 退群 / 精华消息 / @all 剩余)。
281
910
  */
282
- /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
283
- function unwrap$2(label, result, errMsg) {
284
- if (result === 0) return;
285
- throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
286
- }
287
911
  /** 群 API:从 session 拿 group service,包装成语义化方法。 */
288
912
  var GroupApi = class {
289
913
  service;
@@ -300,7 +924,7 @@ var GroupApi = class {
300
924
  /** 群列表(force=true 强制刷新,默认缓存)。 */
301
925
  async getGroupList(force = false) {
302
926
  const raw = await this.service.getGroupList(force);
303
- if (raw.result !== 0) unwrap$2("getGroupList", raw.result, raw.errMsg);
927
+ if (raw.result !== 0) unwrap("getGroupList", raw.result, raw.errMsg);
304
928
  const list = raw.groupList;
305
929
  if (Array.isArray(list)) return list;
306
930
  return [];
@@ -313,7 +937,7 @@ var GroupApi = class {
313
937
  async getGroupInfo(groupCode) {
314
938
  const raw = await this.service.getGroupDetailInfo(groupCode, 0);
315
939
  const { result, errMsg } = raw;
316
- if (typeof result === "number" && result !== 0) unwrap$2("getGroupDetailInfo", result, errMsg);
940
+ if (typeof result === "number" && result !== 0) unwrap("getGroupDetailInfo", result, errMsg);
317
941
  if (result !== null && typeof result === "object") return result;
318
942
  const detail = raw;
319
943
  const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
@@ -330,7 +954,7 @@ var GroupApi = class {
330
954
  async getGroupMemberInfo(groupCode, uids) {
331
955
  if (uids.length === 0) return [];
332
956
  const raw = await this.service.getMemberInfo(groupCode, uids, true);
333
- unwrap$2("getMemberInfo", raw.result, raw.errMsg);
957
+ unwrap("getMemberInfo", raw.result, raw.errMsg);
334
958
  const { infos } = raw;
335
959
  if (infos instanceof Map) return [...infos.values()];
336
960
  return [];
@@ -376,7 +1000,7 @@ var GroupApi = class {
376
1000
  kickMsg: reason
377
1001
  };
378
1002
  const raw = await this.service.kickMemberV2(req);
379
- unwrap$2("kickMemberV2", raw.result, raw.errMsg);
1003
+ unwrap("kickMemberV2", raw.result, raw.errMsg);
380
1004
  }
381
1005
  /** 成员禁言(duration 秒,0 解除禁言)。 */
382
1006
  async setMemberShutUp(groupCode, members) {
@@ -385,12 +1009,12 @@ var GroupApi = class {
385
1009
  uid: m.uid,
386
1010
  timeStamp: m.duration
387
1011
  })));
388
- unwrap$2("setMemberShutUp", raw.result, raw.errMsg);
1012
+ unwrap("setMemberShutUp", raw.result, raw.errMsg);
389
1013
  }
390
1014
  /** 全员禁言(enable=true 开启全群禁言)。 */
391
1015
  async setGroupShutUp(groupCode, enable) {
392
1016
  const raw = await this.service.setGroupShutUp(groupCode, enable);
393
- unwrap$2("setGroupShutUp", raw.result, raw.errMsg);
1017
+ unwrap("setGroupShutUp", raw.result, raw.errMsg);
394
1018
  }
395
1019
  /** 设置管理员(role=ADMIN 设管理,MEMBER 取消;void 语义乐观处理)。 */
396
1020
  setMemberRole(groupCode, uid, role) {
@@ -403,7 +1027,7 @@ var GroupApi = class {
403
1027
  /** 修改群名。 */
404
1028
  async modifyGroupName(groupCode, groupName) {
405
1029
  const raw = await this.service.modifyGroupName(groupCode, groupName, false);
406
- unwrap$2("modifyGroupName", raw.result, raw.errMsg);
1030
+ unwrap("modifyGroupName", raw.result, raw.errMsg);
407
1031
  }
408
1032
  /** 退群(needDeleteLocalMsg=is_dismiss:是否解散群,仅群主)。 */
409
1033
  async quitGroup(groupCode, needDeleteLocalMsg = false) {
@@ -411,7 +1035,7 @@ var GroupApi = class {
411
1035
  groupCode,
412
1036
  needDeleteLocalMsg
413
1037
  });
414
- unwrap$2("quitGroupV2", raw.result, raw.errMsg);
1038
+ unwrap("quitGroupV2", raw.result, raw.errMsg);
415
1039
  }
416
1040
  /** 设置精华消息(msgId 经 msgService 取 msgSeq/msgRandom)。 */
417
1041
  async addGroupEssence(groupCode, msgId) {
@@ -428,7 +1052,7 @@ var GroupApi = class {
428
1052
  peerUid: groupCode
429
1053
  };
430
1054
  const raw = await this.msgService.getMsgsByMsgId(peer, [msgId]);
431
- unwrap$2("getMsgsByMsgId", raw.result, raw.errMsg);
1055
+ unwrap("getMsgsByMsgId", raw.result, raw.errMsg);
432
1056
  const msg = raw.msgList?.[0];
433
1057
  if (msg === void 0) throw kernelError(`消息 ${msgId} 不存在`, "NOT_FOUND");
434
1058
  const param = {
@@ -442,7 +1066,7 @@ var GroupApi = class {
442
1066
  label = "addGroupEssence";
443
1067
  res = await this.service.addGroupEssence(param);
444
1068
  } else res = await this.service.removeGroupEssence(param);
445
- if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
1069
+ checkLooseResult(label, res);
446
1070
  }
447
1071
  /** @all 剩余次数(get_group_at_all_remain)。 */
448
1072
  async getGroupRemainAtTimes(groupCode) {
@@ -464,11 +1088,6 @@ var GroupApi = class {
464
1088
  * getSingleScreenNotifies / getGroupShutUpMemberList 的列表可能走 listener 回调,
465
1089
  * 直接调用返回形状待探测校准:兼容 result 数组 / 字段 / 直接数组。
466
1090
  */
467
- /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
468
- function unwrap$1(label, result, errMsg) {
469
- if (result === 0) return;
470
- throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
471
- }
472
1091
  /** 群通知 API:从 session 拿 group service,包装成语义化方法。 */
473
1092
  var GroupNotifyApi = class {
474
1093
  service;
@@ -484,7 +1103,7 @@ var GroupNotifyApi = class {
484
1103
  /** 禁言成员列表(get_group_shut_list)。 */
485
1104
  async getGroupShutUpMemberList(groupCode) {
486
1105
  const raw = await this.service.getGroupShutUpMemberList(groupCode);
487
- if (raw.result !== 0) unwrap$1("getGroupShutUpMemberList", raw.result, raw.errMsg);
1106
+ if (raw.result !== 0) unwrap("getGroupShutUpMemberList", raw.result, raw.errMsg);
488
1107
  return extractMemberList(raw.result);
489
1108
  }
490
1109
  /** 处理群请求(set_group_add_request 应答;approve=true 同意 / false 拒绝)。 */
@@ -493,276 +1112,37 @@ var GroupNotifyApi = class {
493
1112
  let groupCode = "";
494
1113
  if (group !== void 0) ({groupCode} = group);
495
1114
  let postscript = reason;
496
- if (postscript === "") postscript = " ";
497
- await this.service.operateSysNotify(doubt, {
498
- operateType,
499
- targetMsg: {
500
- seq,
501
- type,
502
- groupCode,
503
- postscript
504
- }
505
- });
506
- }
507
- };
508
- /** 从 getSingleScreenNotifies 返回提取通知数组(兼容 result 数组 / notifies 字段)。 */
509
- function extractNotifyList(result) {
510
- if (Array.isArray(result)) return result;
511
- if (result !== null && typeof result === "object") {
512
- const obj = result;
513
- if (Array.isArray(obj.notifies)) return obj.notifies;
514
- if (Array.isArray(obj.groupNotifies)) return obj.groupNotifies;
515
- }
516
- return [];
517
- }
518
- /** 从 getGroupShutUpMemberList 返回提取成员数组(兼容 result 数组 / memberList 字段)。 */
519
- function extractMemberList(result) {
520
- if (Array.isArray(result)) return result;
521
- if (result !== null && typeof result === "object") {
522
- const obj = result;
523
- if (Array.isArray(obj.memberList)) return obj.memberList;
524
- if (Array.isArray(obj.list)) return obj.list;
525
- }
526
- return [];
527
- }
528
- //#endregion
529
- //#region src/types/services/msg-service.ts
530
- /** 消息元素类型枚举(QQ wrapper 外部契约,自研描述)。 */
531
- const ElementType = {
532
- UNKNOWN: 0,
533
- TEXT: 1,
534
- PIC: 2,
535
- FILE: 3,
536
- PTT: 4,
537
- VIDEO: 5,
538
- FACE: 6,
539
- REPLY: 7,
540
- GRAY_TIP: 8,
541
- WALLET: 9,
542
- ARK: 10,
543
- MFACE: 11,
544
- LIVE_GIFT: 12,
545
- STRUCT_LONG_MSG: 13,
546
- MARKDOWN: 14,
547
- GIPHY: 15,
548
- MULTI_FORWARD: 16,
549
- INLINE_KEYBOARD: 17,
550
- INTEXT_GIFT: 18,
551
- CALENDAR: 19
552
- };
553
- //#endregion
554
- //#region src/types/message-element.ts
555
- /**
556
- * canonical 消息元素模型(协议无关,ADR-008 延伸)
557
- *
558
- * 描述 QQ 消息的事实结构,是各协议翻译层的公共中间表示:
559
- * kernel 只做一次 NT ↔ canonical(toCanonicalElements / toSendElements),
560
- * 各协议(onebot11/satori)只写薄映射 canonical ↔ 协议格式。
561
- */
562
- /** 文本 at 类型(QQ wrapper 契约,自研描述)。 */
563
- const AtType = {
564
- ALL: 1,
565
- ONE: 2,
566
- ME: 4
567
- };
568
- /** TEXT 元素 → canonical(at 或纯文本)。 */
569
- function textToCanonical(textEl) {
570
- if (textEl === void 0) return {
571
- type: "text",
572
- text: ""
573
- };
574
- const { atType, atUid, content } = textEl;
575
- if (atType === AtType.ALL) return {
576
- type: "at",
577
- target: "all"
578
- };
579
- if (atType === AtType.ONE || atType === AtType.ME) {
580
- const base = {
581
- type: "at",
582
- target: atUid ?? content
583
- };
584
- const display = content.startsWith("@") ? content.slice(1) : content;
585
- if (display === "") return base;
586
- return {
587
- ...base,
588
- display
589
- };
590
- }
591
- return {
592
- type: "text",
593
- text: content
594
- };
595
- }
596
- /** PIC 元素 → canonical image。 */
597
- function picToCanonical(pic) {
598
- if (pic === void 0) return {
599
- type: "image",
600
- path: ""
601
- };
602
- const path = pic.picPath ?? pic.picUrl ?? "";
603
- const url = pic.picUrl;
604
- if (url === void 0) return {
605
- type: "image",
606
- path
607
- };
608
- return {
609
- type: "image",
610
- path,
611
- url
612
- };
613
- }
614
- /** FACE 元素 → canonical face。 */
615
- function faceToCanonical(face) {
616
- if (face === void 0) return {
617
- type: "face",
618
- id: "0"
619
- };
620
- return {
621
- type: "face",
622
- id: String(face.faceIndex)
623
- };
624
- }
625
- /** PTT 元素 → canonical voice。 */
626
- function pttToCanonical(ptt) {
627
- if (ptt === void 0) return {
628
- type: "voice",
629
- path: ""
630
- };
631
- return {
632
- type: "voice",
633
- path: ptt.filePath ?? ""
634
- };
635
- }
636
- /** VIDEO 元素 → canonical video。 */
637
- function videoToCanonical(video) {
638
- if (video === void 0) return {
639
- type: "video",
640
- path: ""
641
- };
642
- const path = video.filePath ?? video.videoUrl ?? "";
643
- const url = video.videoUrl;
644
- if (url === void 0) return {
645
- type: "video",
646
- path
647
- };
648
- return {
649
- type: "video",
650
- path,
651
- url
652
- };
653
- }
654
- /** FILE 元素 → canonical file。 */
655
- function fileToCanonical(file) {
656
- if (file === void 0) return {
657
- type: "file",
658
- path: ""
659
- };
660
- const path = file.filePath ?? "";
661
- const name = file.fileName;
662
- if (name === void 0) return {
663
- type: "file",
664
- path
665
- };
666
- return {
667
- type: "file",
668
- path,
669
- name
670
- };
671
- }
672
- /** REPLY 元素 → canonical reply。 */
673
- function replyToCanonical(reply) {
674
- if (reply === void 0) return {
675
- type: "reply",
676
- messageId: ""
677
- };
678
- return {
679
- type: "reply",
680
- messageId: reply.replayMsgId
681
- };
682
- }
683
- /** 单个 NT 元素 → canonical(接收方向,宽容:无法表达回 unknown,不抛错)。 */
684
- function toCanonicalElement(el) {
685
- switch (el.elementType) {
686
- case ElementType.TEXT: return textToCanonical(el.textElement);
687
- case ElementType.PIC: return picToCanonical(el.picElement);
688
- case ElementType.FACE: return faceToCanonical(el.faceElement);
689
- case ElementType.PTT: return pttToCanonical(el.pttElement);
690
- case ElementType.VIDEO: return videoToCanonical(el.videoElement);
691
- case ElementType.FILE: return fileToCanonical(el.fileElement);
692
- case ElementType.REPLY: return replyToCanonical(el.replyElement);
693
- default: return {
694
- type: "unknown",
695
- raw: el
696
- };
1115
+ if (postscript === "") postscript = " ";
1116
+ await this.service.operateSysNotify(doubt, {
1117
+ operateType,
1118
+ targetMsg: {
1119
+ seq,
1120
+ type,
1121
+ groupCode,
1122
+ postscript
1123
+ }
1124
+ });
697
1125
  }
1126
+ };
1127
+ /** 从 getSingleScreenNotifies 返回提取通知数组(兼容 result 数组 / notifies 字段)。 */
1128
+ function extractNotifyList(result) {
1129
+ if (Array.isArray(result)) return result;
1130
+ if (result !== null && typeof result === "object") {
1131
+ const obj = result;
1132
+ if (Array.isArray(obj.notifies)) return obj.notifies;
1133
+ if (Array.isArray(obj.groupNotifies)) return obj.groupNotifies;
1134
+ }
1135
+ return [];
698
1136
  }
699
- /** NT RawMessage → canonical 元素(与 toSendElements 对称,接收方向)。 */
700
- function toCanonicalElements(msg) {
701
- return msg.elements.map(toCanonicalElement);
702
- }
703
- /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心五类)。 */
704
- function toSendElements(elements) {
705
- const out = [];
706
- for (const el of elements) switch (el.type) {
707
- case "text":
708
- out.push({
709
- elementType: ElementType.TEXT,
710
- textElement: { content: el.text }
711
- });
712
- break;
713
- case "at":
714
- if (el.target === "all") out.push({
715
- elementType: ElementType.TEXT,
716
- textElement: {
717
- content: el.display ?? "@",
718
- atType: 1
719
- }
720
- });
721
- else out.push({
722
- elementType: ElementType.TEXT,
723
- textElement: {
724
- content: el.display ?? "@",
725
- atType: 2,
726
- atUid: el.target
727
- }
728
- });
729
- break;
730
- case "face":
731
- out.push({
732
- elementType: ElementType.FACE,
733
- faceElement: { faceIndex: Number(el.id) }
734
- });
735
- break;
736
- case "image":
737
- out.push({
738
- elementType: ElementType.PIC,
739
- picElement: { picPath: el.path }
740
- });
741
- break;
742
- case "voice":
743
- out.push({
744
- elementType: ElementType.PTT,
745
- pttElement: { filePath: el.path }
746
- });
747
- break;
748
- case "reply":
749
- out.push({
750
- elementType: ElementType.REPLY,
751
- replyElement: {
752
- replayMsgId: el.messageId,
753
- replayMsgSeq: el.messageId,
754
- replayMsgTime: "0"
755
- }
756
- });
757
- break;
758
- case "video":
759
- case "file":
760
- case "forward":
761
- case "json":
762
- case "xml":
763
- case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
1137
+ /** 从 getGroupShutUpMemberList 返回提取成员数组(兼容 result 数组 / memberList 字段)。 */
1138
+ function extractMemberList(result) {
1139
+ if (Array.isArray(result)) return result;
1140
+ if (result !== null && typeof result === "object") {
1141
+ const obj = result;
1142
+ if (Array.isArray(obj.memberList)) return obj.memberList;
1143
+ if (Array.isArray(obj.list)) return obj.list;
764
1144
  }
765
- return out;
1145
+ return [];
766
1146
  }
767
1147
  //#endregion
768
1148
  //#region src/apis/msg.ts
@@ -774,17 +1154,6 @@ function toSendElements(elements) {
774
1154
  *
775
1155
  * 方法面(P2-1):发送 / 撤回 / 拉历史 / 标记已读。group/friend 等后续 apis 同构。
776
1156
  */
777
- /** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
778
- function unwrapResult(label, raw) {
779
- if (raw.result === 0) return;
780
- const msg = raw.errMsg || "无错误详情";
781
- let code = "UNKNOWN";
782
- if (msg.includes("未登录") || msg.includes("login")) code = "NOT_LOGIN";
783
- else if (msg.includes("无权限") || msg.includes("permission")) code = "PERMISSION_DENIED";
784
- else if (msg.includes("不存在") || msg.includes("not found")) code = "NOT_FOUND";
785
- else if (label === "sendMsg") code = "SEND_FAILED";
786
- throw kernelError(`${label} 失败: ${msg}`, code);
787
- }
788
1157
  /** 消息 API:从 session 拿 msg service,包装成语义化方法。 */
789
1158
  var MsgApi = class {
790
1159
  service;
@@ -902,77 +1271,49 @@ var MsgApi = class {
902
1271
  }
903
1272
  };
904
1273
  //#endregion
905
- //#region src/apis/ticket.ts
906
- /**
907
- * TicketApi:票据语义化 API(ADR-009 统一错误语义,P2-13)
908
- *
909
- * - getClientKey:TicketService.forceFetchClientKey 解包(get_clientkey)
910
- * - getCookies:clientKey → ssl.ptlogin2.qq.com/jump 跳转 → 解析 set-cookie 头
911
- * (fetch 会吞掉 Set-Cookie 头,故用 node:https 手动请求)
912
- */
913
- /** 跳转请求超时(毫秒)。 */
914
- const JUMP_TIMEOUT_MS = 1e4;
915
- /** 票据 API:从 session 拿 ticket service,包装成语义化方法。 */
916
- var TicketApi = class {
917
- service;
918
- constructor(session) {
919
- const service = session.getTicketService();
920
- if (service === null || service === void 0) throw kernelError("getTicketService() 返回空(session 未 init)", "INVALID_STATE");
921
- this.service = service;
922
- }
923
- /** 获取 clientKey(get_clientkey)。 */
924
- async getClientKey() {
925
- const raw = await this.service.forceFetchClientKey("");
926
- if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
927
- return raw;
928
- }
929
- /** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
930
- async getCookies(domain, uin) {
931
- const { clientKey } = await this.getClientKey();
932
- const u1 = `https://${domain}/${uin}/infocenter`;
933
- return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
934
- }
935
- /** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
936
- static getBkn(skey) {
937
- let hash = 5381;
938
- for (let i = 0; i < skey.length; i += 1) {
939
- const code = skey.charCodeAt(i);
940
- hash = hash + (hash << 5) + code;
941
- }
942
- return (hash & 2147483647).toString();
943
- }
944
- };
945
- /** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
946
- function fetchJumpCookies(url) {
947
- return new Promise((resolve, reject) => {
948
- const req = get(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
949
- const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
950
- res.resume();
951
- res.on("end", () => resolve(cookies));
952
- });
953
- req.setTimeout(JUMP_TIMEOUT_MS, () => {
954
- req.destroy();
955
- reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
956
- });
957
- req.on("error", (err) => reject(err));
958
- });
1274
+ //#region src/apis/stranger-info.ts
1275
+ /** 宽松取值:undefined 时给默认值(null 也归默认)。 */
1276
+ function orDefault(value, fallback) {
1277
+ return value ?? fallback;
1278
+ }
1279
+ /** 合并两份详情:优先 info(uid 完整详情),字段缺失回退 byUin(uin 初查详情)。 */
1280
+ function mergeDetail(info, byUin) {
1281
+ const infoSimple = info.detail?.simpleInfo;
1282
+ const byUinSimple = byUin.detail?.simpleInfo;
1283
+ return {
1284
+ coreInfo: infoSimple?.coreInfo ?? byUinSimple?.coreInfo,
1285
+ baseInfo: infoSimple?.baseInfo ?? byUinSimple?.baseInfo,
1286
+ vasInfo: infoSimple?.vasInfo ?? byUinSimple?.vasInfo,
1287
+ commonExt: info.detail?.commonExt ?? byUin.detail?.commonExt,
1288
+ status: infoSimple?.status?.status ?? byUinSimple?.status?.status
1289
+ };
959
1290
  }
960
- /** 解析 set-cookie 头数组 → { name: value } dict。 */
961
- function parseSetCookieHeaders(raw) {
962
- const cookies = {};
963
- if (raw === void 0) return cookies;
964
- for (const line of raw) {
965
- const [pair] = line.split(";");
966
- if (pair !== void 0) {
967
- const eq = pair.indexOf("=");
968
- if (eq > 0) {
969
- const name = pair.slice(0, eq).trim();
970
- const value = pair.slice(eq + 1);
971
- if (name !== "") cookies[name] = value;
972
- }
973
- }
974
- }
975
- return cookies;
1291
+ /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1292
+ function flattenStrangerInfo(uin, uid, byUin, info) {
1293
+ const d = mergeDetail(info, byUin);
1294
+ return {
1295
+ user_id: Number(uin),
1296
+ uid,
1297
+ nickname: orDefault(d.coreInfo?.nick, ""),
1298
+ age: orDefault(d.baseInfo?.age, 0),
1299
+ qid: orDefault(d.baseInfo?.qid, ""),
1300
+ qq_level: orDefault(d.commonExt?.qqLevel, 0),
1301
+ sex: mapSex(d.baseInfo?.sex),
1302
+ long_nick: orDefault(d.baseInfo?.longNick, ""),
1303
+ reg_time: orDefault(d.commonExt?.regTime, 0),
1304
+ is_vip: orDefault(d.vasInfo?.svipFlag, false),
1305
+ is_years_vip: orDefault(d.vasInfo?.yearVipFlag, false),
1306
+ vip_level: orDefault(d.vasInfo?.vipLevel, 0),
1307
+ remark: orDefault(d.coreInfo?.remark, ""),
1308
+ status: orDefault(d.status, 0),
1309
+ login_days: 0
1310
+ };
1311
+ }
1312
+ /** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
1313
+ function mapSex(raw) {
1314
+ if (raw === 1) return "male";
1315
+ if (raw === 2) return "female";
1316
+ return "unknown";
976
1317
  }
977
1318
  //#endregion
978
1319
  //#region src/apis/profile.ts
@@ -994,13 +1335,11 @@ var ProfileApi = class {
994
1335
  }
995
1336
  /** 设置个性签名(set_self_longnick)。 */
996
1337
  async setLongNick(longNick) {
997
- const res = await this.service.setLongNick(longNick);
998
- if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setLongNick 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
1338
+ checkLooseResult("setLongNick", await this.service.setLongNick(longNick));
999
1339
  }
1000
1340
  /** 设置昵称(set_qq_profile)。 */
1001
1341
  async setNickName(nickName) {
1002
- const res = await this.service.setNickName(nickName);
1003
- if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setNickName 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
1342
+ checkLooseResult("setNickName", await this.service.setNickName(nickName));
1004
1343
  }
1005
1344
  /** 设置头像(set_qq_avatar;filePath 为本地路径)。 */
1006
1345
  async setHeader(filePath) {
@@ -1017,37 +1356,6 @@ var ProfileApi = class {
1017
1356
  return flattenStrangerInfo(uin, uid, byUin, info);
1018
1357
  }
1019
1358
  };
1020
- /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1021
- function flattenStrangerInfo(uin, uid, byUin, info) {
1022
- const coreInfo = info.detail?.simpleInfo?.coreInfo ?? byUin.detail?.simpleInfo?.coreInfo;
1023
- const baseInfo = info.detail?.simpleInfo?.baseInfo ?? byUin.detail?.simpleInfo?.baseInfo;
1024
- const vasInfo = info.detail?.simpleInfo?.vasInfo ?? byUin.detail?.simpleInfo?.vasInfo;
1025
- const commonExt = info.detail?.commonExt ?? byUin.detail?.commonExt;
1026
- const status = info.detail?.simpleInfo?.status?.status ?? 0;
1027
- return {
1028
- user_id: Number(uin),
1029
- uid,
1030
- nickname: coreInfo?.nick ?? "",
1031
- age: baseInfo?.age ?? 0,
1032
- qid: baseInfo?.qid ?? "",
1033
- qq_level: commonExt?.qqLevel ?? 0,
1034
- sex: mapSex(baseInfo?.sex),
1035
- long_nick: baseInfo?.longNick ?? "",
1036
- reg_time: commonExt?.regTime ?? 0,
1037
- is_vip: vasInfo?.svipFlag ?? false,
1038
- is_years_vip: vasInfo?.yearVipFlag ?? false,
1039
- vip_level: vasInfo?.vipLevel ?? 0,
1040
- remark: coreInfo?.remark ?? "",
1041
- status,
1042
- login_days: 0
1043
- };
1044
- }
1045
- /** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
1046
- function mapSex(raw) {
1047
- if (raw === 1) return "male";
1048
- if (raw === 2) return "female";
1049
- return "unknown";
1050
- }
1051
1359
  //#endregion
1052
1360
  //#region src/apis/profile-like.ts
1053
1361
  /**
@@ -1096,11 +1404,6 @@ const HIDE_ONLINEDOC_FOLDER = 0;
1096
1404
  const DEFAULT_FILE_COUNT = 50;
1097
1405
  /** 空间查询文件数量(单条即可)。 */
1098
1406
  const SPACE_QUERY_FILE_COUNT = 1;
1099
- /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
1100
- function unwrap(label, result, errMsg) {
1101
- if (result === 0) return;
1102
- throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
1103
- }
1104
1407
  /** 富媒体 API:从 session 拿 rich media service,包装成语义化方法。 */
1105
1408
  var RichMediaApi = class {
1106
1409
  service;
@@ -1183,21 +1486,94 @@ var RichMediaApi = class {
1183
1486
  if (raw.result !== 0) unwrap("transGroupFile", raw.result, raw.errMsg);
1184
1487
  }
1185
1488
  };
1186
- /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。 */
1187
- function extractFileList(result) {
1188
- if (Array.isArray(result)) return result;
1189
- if (result !== null && typeof result === "object") {
1190
- const obj = result;
1191
- if (Array.isArray(obj.items)) return obj.items;
1192
- if (Array.isArray(obj.fileList)) return obj.fileList;
1193
- if (Array.isArray(obj.list)) return obj.list;
1194
- }
1195
- return [];
1489
+ /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。 */
1490
+ function extractFileList(result) {
1491
+ if (Array.isArray(result)) return result;
1492
+ if (result !== null && typeof result === "object") {
1493
+ const obj = result;
1494
+ if (Array.isArray(obj.items)) return obj.items;
1495
+ if (Array.isArray(obj.fileList)) return obj.fileList;
1496
+ if (Array.isArray(obj.list)) return obj.list;
1497
+ }
1498
+ return [];
1499
+ }
1500
+ /** 统计文件数量(兼容数组 / { items })。 */
1501
+ function countGroupFiles(result) {
1502
+ if (Array.isArray(result)) return result.filter((item) => item.fileInfo !== void 0).length;
1503
+ return 0;
1504
+ }
1505
+ //#endregion
1506
+ //#region src/apis/ticket.ts
1507
+ /**
1508
+ * TicketApi:票据语义化 API(ADR-009 统一错误语义,P2-13)
1509
+ *
1510
+ * - getClientKey:TicketService.forceFetchClientKey 解包(get_clientkey)
1511
+ * - getCookies:clientKey → ssl.ptlogin2.qq.com/jump 跳转 → 解析 set-cookie 头
1512
+ * (fetch 会吞掉 Set-Cookie 头,故用 node:https 手动请求)
1513
+ */
1514
+ /** 跳转请求超时(毫秒)。 */
1515
+ const JUMP_TIMEOUT_MS = 1e4;
1516
+ /** 票据 API:从 session 拿 ticket service,包装成语义化方法。 */
1517
+ var TicketApi = class {
1518
+ service;
1519
+ constructor(session) {
1520
+ const service = session.getTicketService();
1521
+ if (service === null || service === void 0) throw kernelError("getTicketService() 返回空(session 未 init)", "INVALID_STATE");
1522
+ this.service = service;
1523
+ }
1524
+ /** 获取 clientKey(get_clientkey)。 */
1525
+ async getClientKey() {
1526
+ const raw = await this.service.forceFetchClientKey("");
1527
+ if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
1528
+ return raw;
1529
+ }
1530
+ /** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
1531
+ async getCookies(domain, uin) {
1532
+ const { clientKey } = await this.getClientKey();
1533
+ const u1 = `https://${domain}/${uin}/infocenter`;
1534
+ return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
1535
+ }
1536
+ /** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
1537
+ static getBkn(skey) {
1538
+ let hash = 5381;
1539
+ for (let i = 0; i < skey.length; i += 1) {
1540
+ const code = skey.charCodeAt(i);
1541
+ hash = hash + (hash << 5) + code;
1542
+ }
1543
+ return (hash & 2147483647).toString();
1544
+ }
1545
+ };
1546
+ /** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
1547
+ function fetchJumpCookies(url) {
1548
+ return new Promise((resolve, reject) => {
1549
+ const req = get(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
1550
+ const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
1551
+ res.resume();
1552
+ res.on("end", () => resolve(cookies));
1553
+ });
1554
+ req.setTimeout(JUMP_TIMEOUT_MS, () => {
1555
+ req.destroy();
1556
+ reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
1557
+ });
1558
+ req.on("error", (err) => reject(err));
1559
+ });
1196
1560
  }
1197
- /** 统计文件数量(兼容数组 / { items })。 */
1198
- function countGroupFiles(result) {
1199
- if (Array.isArray(result)) return result.filter((item) => item.fileInfo !== void 0).length;
1200
- return 0;
1561
+ /** 解析 set-cookie 头数组 → { name: value } dict。 */
1562
+ function parseSetCookieHeaders(raw) {
1563
+ const cookies = {};
1564
+ if (raw === void 0) return cookies;
1565
+ for (const line of raw) {
1566
+ const [pair] = line.split(";");
1567
+ if (pair !== void 0) {
1568
+ const eq = pair.indexOf("=");
1569
+ if (eq > 0) {
1570
+ const name = pair.slice(0, eq).trim();
1571
+ const value = pair.slice(eq + 1);
1572
+ if (name !== "") cookies[name] = value;
1573
+ }
1574
+ }
1575
+ }
1576
+ return cookies;
1201
1577
  }
1202
1578
  //#endregion
1203
1579
  //#region src/apis/webapi.ts
@@ -1280,29 +1656,33 @@ var WebApi = class {
1280
1656
  type: String(type)
1281
1657
  }).toString(), cookie);
1282
1658
  if (text === null) return [];
1283
- const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
1284
- if (match === null || match[1] === void 0) return [];
1285
- let state;
1286
- try {
1287
- state = JSON.parse(match[1]);
1288
- } catch {
1289
- return [];
1290
- }
1291
- const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
1292
- if (!Array.isArray(raw)) return [];
1293
- const out = [];
1294
- for (const item of raw) if (item !== null && typeof item === "object") {
1295
- const obj = item;
1296
- out.push({
1297
- uin: String(obj.uin ?? ""),
1298
- name: String(obj.name ?? ""),
1299
- avatar: String(obj.avatar ?? ""),
1300
- desc: String(obj.desc ?? "")
1301
- });
1302
- }
1303
- return out;
1659
+ return parseHonorList(text, type);
1304
1660
  }
1305
1661
  };
1662
+ /** 从 honorlist 页面提取 __INITIAL_STATE__ → 荣誉列表(解析失败返回空)。 */
1663
+ function parseHonorList(text, type) {
1664
+ const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
1665
+ if (match === null || match[1] === void 0) return [];
1666
+ let state;
1667
+ try {
1668
+ state = JSON.parse(match[1]);
1669
+ } catch {
1670
+ return [];
1671
+ }
1672
+ const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
1673
+ if (!Array.isArray(raw)) return [];
1674
+ const out = [];
1675
+ for (const item of raw) if (item !== null && typeof item === "object") {
1676
+ const obj = item;
1677
+ out.push({
1678
+ uin: String(obj.uin ?? ""),
1679
+ name: String(obj.name ?? ""),
1680
+ avatar: String(obj.avatar ?? ""),
1681
+ desc: String(obj.desc ?? "")
1682
+ });
1683
+ }
1684
+ return out;
1685
+ }
1306
1686
  /** 请求 qun.qq.com JSON 接口(带 Cookie;失败返回 null)。 */
1307
1687
  async function fetchQunJson(url, cookie) {
1308
1688
  try {
@@ -1405,15 +1785,6 @@ var MsgBridge = class {
1405
1785
  }
1406
1786
  };
1407
1787
  //#endregion
1408
- //#region src/types/listeners/group.ts
1409
- /** 群列表更新类型(说明书参考,值待探测校准)。 */
1410
- const GroupListUpdateType = {
1411
- /** 全量更新(groupList 为完整列表)。 */
1412
- ALL: 0,
1413
- /** 增量更新(合并到现有缓存)。 */
1414
- PARTIAL: 1
1415
- };
1416
- //#endregion
1417
1788
  //#region src/cache/group-cache.ts
1418
1789
  /**
1419
1790
  * 群/成员缓存:事件主动维护 + 查询惰性回填。
@@ -1566,181 +1937,6 @@ function createCoreContext(opts) {
1566
1937
  };
1567
1938
  }
1568
1939
  //#endregion
1569
- //#region src/infra/logger.ts
1570
- /**
1571
- * pino 日志封装(ADR-007)
1572
- *
1573
- * - console(pino-pretty 可读输出)+ 可选文件双目的地
1574
- * - 统一 level 控制
1575
- * - 内置 redact(token / 票据等敏感字段不打日志),可追加
1576
- * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
1577
- */
1578
- /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
1579
- const DEFAULT_REDACT_PATHS = [
1580
- "token",
1581
- "ticket",
1582
- "cookie",
1583
- "password",
1584
- "secret"
1585
- ];
1586
- function toSingle(streams) {
1587
- const [only] = streams;
1588
- if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
1589
- return only.stream;
1590
- }
1591
- /**
1592
- * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
1593
- *
1594
- * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
1595
- * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
1596
- */
1597
- function createLogger(opts = {}) {
1598
- const level = opts.level ?? "info";
1599
- const consoleEnabled = opts.console ?? true;
1600
- const { file, base } = opts;
1601
- const streams = [];
1602
- if (consoleEnabled) streams.push({ stream: pinoPretty({
1603
- colorize: true,
1604
- translateTime: "SYS:HH:MM:ss.l",
1605
- singleLine: false,
1606
- destination: process.stdout
1607
- }) });
1608
- if (file) {
1609
- mkdirSync(dirname(file), { recursive: true });
1610
- streams.push({ stream: pino.destination({
1611
- dest: file,
1612
- sync: true
1613
- }) });
1614
- }
1615
- if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
1616
- const pinoOptions = {
1617
- level,
1618
- redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
1619
- };
1620
- if (base) pinoOptions.base = {
1621
- pid: process.pid,
1622
- ...base
1623
- };
1624
- let stream;
1625
- if (streams.length === 1) stream = toSingle(streams);
1626
- else stream = pino.multistream(streams);
1627
- return pino(pinoOptions, stream);
1628
- }
1629
- //#endregion
1630
- //#region src/infra/paths.ts
1631
- /**
1632
- * 数据目录布局(ADR-016,2026-08-07 修订:配置文件独立于数据根)
1633
- *
1634
- * ```
1635
- * <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
1636
- * ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
1637
- * ├── logs/ # pino 文件日志
1638
- * └── cache/ # 临时文件、媒体缓存
1639
- *
1640
- * <项目根>/napuketto.toml # 全局配置文件(2026-08-07 用户拍板:放项目根目录)
1641
- * ```
1642
- *
1643
- * 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 > `~/.napuketto`(默认)。
1644
- * 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
1645
- */
1646
- /** 默认数据根目录名(用户目录下,程序目录可能只读,见 ADR-016)。 */
1647
- const DEFAULT_DATA_ROOT_NAME = ".napuketto";
1648
- /** 全局配置文件名。 */
1649
- const MAIN_CONFIG_FILE = "napuketto.toml";
1650
- /**
1651
- * 解析数据根:显式参数 > 环境变量 > 用户目录默认。
1652
- * 返回绝对路径。独立导出便于 cli / 探测脚本复用。
1653
- */
1654
- function resolveDataRoot(dataRoot) {
1655
- const explicit = dataRoot ?? process.env["NAPKETTO_DATA"];
1656
- if (explicit) return resolve(explicit);
1657
- return join(homedir(), DEFAULT_DATA_ROOT_NAME);
1658
- }
1659
- /** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
1660
- function findProjectRoot(startDir, marker) {
1661
- let dir = startDir;
1662
- for (;;) {
1663
- if (existsSync(join(dir, marker))) return dir;
1664
- const parent = dirname(dir);
1665
- if (parent === dir) return;
1666
- dir = parent;
1667
- }
1668
- }
1669
- /**
1670
- * 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
1671
- * 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
1672
- *
1673
- * 优先级:
1674
- * 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
1675
- * 2. 入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即项目根)
1676
- * 3. 运行目录(cwd)向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)
1677
- * 4. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
1678
- *
1679
- * 返回绝对路径。独立导出便于 cli / loader 装配链复用。
1680
- */
1681
- function resolveConfigPath(opts = {}) {
1682
- const explicit = process.env["NAPKETTO_CONFIG"];
1683
- if (explicit !== void 0 && explicit !== "") return resolve(explicit);
1684
- const moduleRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url)), "pnpm-workspace.yaml");
1685
- if (moduleRoot !== void 0) return join(moduleRoot, MAIN_CONFIG_FILE);
1686
- const cwdRoot = findProjectRoot(process.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(process.cwd(), "package.json");
1687
- if (cwdRoot !== void 0) return join(cwdRoot, MAIN_CONFIG_FILE);
1688
- return join(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
1689
- }
1690
- /**
1691
- * 路径装配器:持有账号数据目录下的全部子目录。
1692
- * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
1693
- */
1694
- var PathWrapper = class {
1695
- /** 数据根(绝对路径)。 */
1696
- root;
1697
- /** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
1698
- accountDir;
1699
- /** config 目录(napuketto.json + 各协议配置)。 */
1700
- configDir;
1701
- /** logs 目录(pino 文件日志)。 */
1702
- logsDir;
1703
- /** cache 目录(临时文件、媒体缓存)。 */
1704
- cacheDir;
1705
- constructor(opts = {}) {
1706
- const root = resolveDataRoot(opts.dataRoot);
1707
- this.root = root;
1708
- if (opts.account) this.accountDir = join(root, opts.account);
1709
- else this.accountDir = root;
1710
- this.configDir = join(this.accountDir, "config");
1711
- this.logsDir = join(this.accountDir, "logs");
1712
- this.cacheDir = join(this.accountDir, "cache");
1713
- }
1714
- /** 递归创建全部目录,启动装配时调用一次。 */
1715
- ensure() {
1716
- for (const dir of [
1717
- this.configDir,
1718
- this.logsDir,
1719
- this.cacheDir
1720
- ]) mkdirSync(dir, { recursive: true });
1721
- }
1722
- /** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
1723
- file(...segments) {
1724
- return join(this.accountDir, ...segments);
1725
- }
1726
- /**
1727
- * 清空缓存目录(clean_cache 动作经注入回调消费)。
1728
- * 只删除 cacheDir 下的条目,保留目录本身。
1729
- */
1730
- clearCache() {
1731
- let entries;
1732
- try {
1733
- entries = readdirSync(this.cacheDir);
1734
- } catch {
1735
- return;
1736
- }
1737
- for (const entry of entries) rmSync(join(this.cacheDir, entry), {
1738
- recursive: true,
1739
- force: true
1740
- });
1741
- }
1742
- };
1743
- //#endregion
1744
1940
  //#region src/wrapper/wrapper-adapters.ts
1745
1941
  /** NAPI 回调占位(方法存在即可被反射调用,无副作用)。 */
1746
1942
  const noop = () => void 0;
@@ -1791,26 +1987,9 @@ function createLoginListener() {
1791
1987
  return {};
1792
1988
  }
1793
1989
  //#endregion
1794
- //#region src/login/lifecycle.ts
1990
+ //#region src/login/wait.ts
1795
1991
  /**
1796
- * 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
1797
- *
1798
- * 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
1799
- * 运行时实测确认,自研实现:
1800
- * 1. engine.initWithDeskTopConfig(appid/qua/版本)
1801
- * 2. loginService.initConfig + addKernelLoginListener
1802
- * 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
1803
- * 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
1804
- * 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
1805
- *
1806
- * 关键(2026-08-05 实测修正):
1807
- * - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
1808
- * 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
1809
- *
1810
- * 模块边界(2026-08-05 解耦):
1811
- * - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
1812
- * - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
1813
- * - 本文件只保留流程编排:快速登录 + session 初始化 + init 完成信号等待。
1992
+ * 等待/延迟工具(login 模块共享,2026-08-08 克隆合并)
1814
1993
  */
1815
1994
  /** 等待条件满足(轮询,带超时)。 */
1816
1995
  function waitFor(predicate, timeoutMs, intervalMs = 500) {
@@ -1830,32 +2009,21 @@ function waitFor(predicate, timeoutMs, intervalMs = 500) {
1830
2009
  tick();
1831
2010
  });
1832
2011
  }
1833
- /** session init 默认超时(毫秒)。 */
1834
- const DEFAULT_INIT_TIMEOUT_MS = 15e3;
1835
- /** session 就绪等待默认超时(毫秒)。 */
1836
- const SESSION_READY_TIMEOUT_MS = 3e4;
1837
- /** session 就绪轮询间隔(毫秒)。 */
1838
- const SESSION_READY_POLL_MS = 1e3;
2012
+ /** 短延迟。 */
2013
+ function sleep(ms) {
2014
+ return new Promise((resolve) => {
2015
+ setTimeout(resolve, ms);
2016
+ });
2017
+ }
2018
+ //#endregion
2019
+ //#region src/login/login-connect.ts
1839
2020
  /**
1840
- * 等待 QQ 自己完成 session 初始化(framework 复用模式,2026-08-05 修正)。
2021
+ * 登录连接与快速登录(从 lifecycle.ts 拆分,2026-08-08 FTA 优化)
1841
2022
  *
1842
- * 背景:QQ 9.9.31 实测——session 由 QQ 主进程自己创建并 init(登录后)。
1843
- * 我们不自己 init(重复 init 断言 "implementation of IQQNTWrapperSession is not valid"),
1844
- * 也不自己 create(登录前 create() 干扰 QQ,实测 QQ 退出 code=0)。
1845
- * 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
2023
+ * - waitForNetworkConnection:等 MSF 网络连接就绪(getMsfStatus() === 3)
2024
+ * - ensureLoginConnected:connect() → 等 onLoginConnected(自建宿主必需)
2025
+ * - quickLogin:历史账号快速登录(网络异常 1006511 等就绪后重试)
1846
2026
  */
1847
- async function waitSessionReady(ctx, opts) {
1848
- const { session } = ctx;
1849
- if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
1850
- if (!await waitFor(() => {
1851
- try {
1852
- const svc = session.getMsgService();
1853
- return svc !== null && svc !== void 0;
1854
- } catch {
1855
- return false;
1856
- }
1857
- }, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
1858
- }
1859
2027
  /**
1860
2028
  * 网络状态(MSF)常量。
1861
2029
  * 3 = 已连接(NapCat waitForNetworkConnection 语义,自研描述)。
@@ -1871,6 +2039,8 @@ const NETWORK_RETRY_MAX = 3;
1871
2039
  const NETWORK_READY_TIMEOUT_MS = 15e3;
1872
2040
  /** 网络就绪轮询间隔(毫秒)。 */
1873
2041
  const NETWORK_READY_POLL_MS = 1e3;
2042
+ /** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
2043
+ const CONNECTION_SETTLE_MS = 3e3;
1874
2044
  /**
1875
2045
  * 等待网络连接就绪(loginService.getMsfStatus() === 3)。
1876
2046
  * 参考 NapCat waitForNetworkConnection 思路(自研实现):快速登录在 QQ 刚拉起、
@@ -1931,41 +2101,21 @@ async function ensureLoginConnected(ctx, opts) {
1931
2101
  });
1932
2102
  await sleep(CONNECTION_SETTLE_MS);
1933
2103
  }
1934
- /** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
1935
- const CONNECTION_SETTLE_MS = 3e3;
1936
- /** 短延迟。 */
1937
- function sleep(ms) {
1938
- return new Promise((resolve) => {
1939
- setTimeout(resolve, ms);
1940
- });
1941
- }
1942
2104
  /**
1943
- * 快速登录:遍历历史登录列表尝试。
1944
- * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
2105
+ * 目标账号选择(2026-08-07 修复:显式 uin 不在列表时**报错**而非静默 fallback——
2106
+ * 之前会退到第一个可快速登录账号,导致 `-q <uin>` 实际登成别的号)。
1945
2107
  */
1946
- async function quickLogin(ctx, opts) {
1947
- const raw = ctx.loginService;
1948
- if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
1949
- const loginService = raw;
1950
- await ensureLoginConnected(ctx, { ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} });
1951
- const items = (await loginService.getLoginList()).LocalLoginInfoList;
1952
- if (items.length === 0) throw kernelError("无历史登录账号", "NOT_LOGIN");
1953
- let target;
1954
- if (opts.uin !== void 0) {
1955
- target = items.find((i) => i.uin === opts.uin);
1956
- if (target === void 0) {
1957
- const available = items.map((i) => i.uin).join("、");
1958
- throw kernelError(`账号 ${opts.uin} 不在登录列表(可用:${available})`, "NOT_FOUND");
1959
- }
1960
- } else {
1961
- target = items.find((i) => i.isQuickLogin);
1962
- if (target === void 0) {
1963
- const [first] = items;
1964
- target = first;
1965
- }
1966
- }
1967
- if (target === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
1968
- return loginWithNetworkRetry(ctx, loginService, target, opts);
2108
+ function pickLoginTarget(items, uin) {
2109
+ if (uin !== void 0) {
2110
+ const target = items.find((i) => i.uin === uin);
2111
+ if (target === void 0) throw kernelError(`账号 ${uin} 不在登录列表(可用:${items.map((i) => i.uin).join("、")})`, "NOT_FOUND");
2112
+ return target;
2113
+ }
2114
+ const quick = items.find((i) => i.isQuickLogin);
2115
+ if (quick !== void 0) return quick;
2116
+ const [first] = items;
2117
+ if (first === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
2118
+ return first;
1969
2119
  }
1970
2120
  /**
1971
2121
  * 带网络重试的快速登录(P2-1)。
@@ -1986,6 +2136,69 @@ async function loginWithNetworkRetry(ctx, loginService, target, opts) {
1986
2136
  }
1987
2137
  throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
1988
2138
  }
2139
+ /**
2140
+ * 快速登录:遍历历史登录列表尝试。
2141
+ * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
2142
+ */
2143
+ async function quickLogin(ctx, opts) {
2144
+ const raw = ctx.loginService;
2145
+ if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
2146
+ const loginService = raw;
2147
+ await ensureLoginConnected(ctx, { ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} });
2148
+ const items = (await loginService.getLoginList()).LocalLoginInfoList;
2149
+ if (items.length === 0) throw kernelError("无历史登录账号", "NOT_LOGIN");
2150
+ return loginWithNetworkRetry(ctx, loginService, pickLoginTarget(items, opts.uin), opts);
2151
+ }
2152
+ //#endregion
2153
+ //#region src/login/lifecycle.ts
2154
+ /**
2155
+ * 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
2156
+ *
2157
+ * 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
2158
+ * 运行时实测确认,自研实现:
2159
+ * 1. engine.initWithDeskTopConfig(appid/qua/版本)
2160
+ * 2. loginService.initConfig + addKernelLoginListener
2161
+ * 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
2162
+ * 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
2163
+ * 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
2164
+ *
2165
+ * 关键(2026-08-05 实测修正):
2166
+ * - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
2167
+ * 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
2168
+ *
2169
+ * 模块边界(2026-08-05 解耦;2026-08-08 FTA 优化再拆):
2170
+ * - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
2171
+ * - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
2172
+ * - 登录连接 + 快速登录(waitForNetworkConnection / quickLogin 等)→ login-connect.ts
2173
+ * - 本文件只保留 session 流程编排:就绪等待(waitSessionReady)+ session 初始化(initAndStartSession)。
2174
+ */
2175
+ /** session init 默认超时(毫秒)。 */
2176
+ const DEFAULT_INIT_TIMEOUT_MS = 15e3;
2177
+ /** session 就绪等待默认超时(毫秒)。 */
2178
+ const SESSION_READY_TIMEOUT_MS = 3e4;
2179
+ /** session 就绪轮询间隔(毫秒)。 */
2180
+ const SESSION_READY_POLL_MS = 1e3;
2181
+ /**
2182
+ * 等待 QQ 自己完成 session 初始化(framework 复用模式,2026-08-05 修正)。
2183
+ *
2184
+ * 背景:QQ 9.9.31 实测——session 由 QQ 主进程自己创建并 init(登录后)。
2185
+ * 我们不自己 init(重复 init 断言 "implementation of IQQNTWrapperSession is not valid"),
2186
+ * 也不自己 create(登录前 create() 干扰 QQ,实测 QQ 退出 code=0)。
2187
+ * 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
2188
+ */
2189
+ async function waitSessionReady(ctx, opts) {
2190
+ const { session } = ctx;
2191
+ if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
2192
+ if (!await waitFor(() => {
2193
+ try {
2194
+ const svc = session.getMsgService();
2195
+ return svc !== null && svc !== void 0;
2196
+ } catch {
2197
+ return false;
2198
+ }
2199
+ }, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
2200
+ }
2201
+ /** 登录连接 + 快速登录(quickLogin 等)已拆到 login-connect.ts(见文件头 re-export)。 */
1989
2202
  /** session 初始化(4 参全为普通 JS 对象,等 init 完成信号)。 */
1990
2203
  async function initAndStartSession(ctx, config, listener, opts) {
1991
2204
  const { session } = ctx;
@@ -2200,36 +2413,6 @@ function findInObject(value) {
2200
2413
  return null;
2201
2414
  }
2202
2415
  //#endregion
2203
- //#region src/types/wrapper.ts
2204
- /**
2205
- * wrapper.node 的 NAPI 接口面(自研描述,非移植)
2206
- *
2207
- * 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
2208
- * 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
2209
- * 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
2210
- *
2211
- * 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
2212
- */
2213
- /** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
2214
- const PlatformType = {
2215
- KUNKNOWN: 0,
2216
- KANDROID: 1,
2217
- KIOS: 2,
2218
- KWINDOWS: 3,
2219
- KMAC: 4,
2220
- KLINUX: 5
2221
- };
2222
- /** 厂商类型(deviceInfo.vendorType)。 */
2223
- const VendorType = {
2224
- KNOSETONIOS: 0,
2225
- KUNSUPPORTANDROIDPUSH: 1,
2226
- KSUPPORTTPNS: 2,
2227
- KSUPPORTHMS: 3,
2228
- KSUPPORTOPPOPUSH: 4,
2229
- KSUPPORTVIVOPUSH: 5,
2230
- KSUPPORTGOOGLEPUSH: 99
2231
- };
2232
- //#endregion
2233
2416
  //#region src/wrapper/wrapper-config.ts
2234
2417
  /**
2235
2418
  * wrapper 交互配置装配(纯函数,无状态)
@@ -2542,20 +2725,8 @@ function startNapuketto(options) {
2542
2725
  const isSelfHost = electronProcessType() === void 0;
2543
2726
  if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2544
2727
  else if (isSelfHost) createSession(ctx);
2545
- let qqGlobalPath = null;
2546
- if (electronProcessType() !== "browser") {
2547
- const root = resolveQqUserDataRoot(wrapperExports);
2548
- if (root !== null) qqGlobalPath = resolveQqGlobalPath(root);
2549
- }
2550
- initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, qqGlobalPath ?? void 0));
2551
- if (qqLoginService !== void 0 && qqLoginService !== null) ctx.loginService = qqLoginService;
2552
- else try {
2553
- const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
2554
- if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
2555
- else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
2556
- } catch {
2557
- ctx.loginService = null;
2558
- }
2728
+ initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx)));
2729
+ resolveLoginService(ctx, qqLoginService);
2559
2730
  if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2560
2731
  else if (!isSelfHost) createSession(ctx);
2561
2732
  if (sessionConfig !== void 0) {
@@ -2564,6 +2735,37 @@ function startNapuketto(options) {
2564
2735
  }
2565
2736
  return ctx;
2566
2737
  }
2738
+ /**
2739
+ * 解析 QQ 真实数据目录的 global 路径(desktopGlobalPath 三要素之三)。
2740
+ * worker(utilityProcess)模式 + 自建宿主(标准 node)下必须指向 QQ 真实
2741
+ * 数据目录(数据根/nt_qq/global,HANDOVER-V6),否则登录数据读写错位
2742
+ * (P2-1 实测 2026-08-06;getLoginList 空)。QQ 主进程(V1)QQ 自己已配置
2743
+ * engine,无需解析。
2744
+ */
2745
+ function resolveBootQqGlobalPath(ctx) {
2746
+ if (electronProcessType() === "browser") return;
2747
+ const root = resolveQqUserDataRoot(ctx.exports);
2748
+ if (root === null) return;
2749
+ return resolveQqGlobalPath(root);
2750
+ }
2751
+ /**
2752
+ * 解析 loginService:优先捕获实例,其次单例 get()(自建宿主/worker 下 QQ 环境
2753
+ * 创建的实例才完整——`new` 自建实例读不到登录列表(getLoginList 空),
2754
+ * HANDOVER-V6 p0-login3 实证),最后 new 回退。
2755
+ */
2756
+ function resolveLoginService(ctx, qqLoginService) {
2757
+ if (qqLoginService !== void 0 && qqLoginService !== null) {
2758
+ ctx.loginService = qqLoginService;
2759
+ return;
2760
+ }
2761
+ try {
2762
+ const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
2763
+ if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
2764
+ else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
2765
+ } catch {
2766
+ ctx.loginService = null;
2767
+ }
2768
+ }
2567
2769
  //#endregion
2568
2770
  //#region src/core.ts
2569
2771
  /**
@@ -2686,9 +2888,14 @@ var NapukettoCore = class NapukettoCore {
2686
2888
  session.onQrCode((qr) => {
2687
2889
  this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
2688
2890
  if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
2891
+ opts.onLoginProgress?.({
2892
+ state: "waiting_scan",
2893
+ qr
2894
+ });
2689
2895
  });
2690
2896
  session.onStateChange((state) => {
2691
2897
  this.ctx.logger.info({ state }, "QR 登录状态");
2898
+ opts.onLoginProgress?.({ state });
2692
2899
  });
2693
2900
  const startOpts = {};
2694
2901
  if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
@@ -2754,6 +2961,7 @@ var NTEventChannel = class {
2754
2961
  serviceName;
2755
2962
  emitter = new EventEmitter();
2756
2963
  errorHandlers = /* @__PURE__ */ new Set();
2964
+ anyHandlers = /* @__PURE__ */ new Set();
2757
2965
  constructor(serviceName) {
2758
2966
  this.serviceName = serviceName;
2759
2967
  this.emitter.setMaxListeners(0);
@@ -2765,170 +2973,69 @@ var NTEventChannel = class {
2765
2973
  on(event, handler) {
2766
2974
  this.emitter.addListener(event, handler);
2767
2975
  return () => {
2768
- this.emitter.removeListener(event, handler);
2769
- };
2770
- }
2771
- /**
2772
- * 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
2773
- * 替代魔法字符串事件名方案。
2774
- */
2775
- waitFor(event, opts = {}) {
2776
- const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
2777
- return new Promise((resolve, reject) => {
2778
- const handler = (...args) => {
2779
- if (opts.filter && !opts.filter(...args)) return;
2780
- cleanup();
2781
- resolve(args);
2782
- };
2783
- const timer = setTimeout(() => {
2784
- cleanup();
2785
- reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
2786
- }, timeoutMs);
2787
- const cleanup = () => {
2788
- clearTimeout(timer);
2789
- this.emitter.removeListener(event, handler);
2790
- };
2791
- this.emitter.addListener(event, handler);
2792
- });
2793
- }
2794
- /**
2795
- * 类型化派发:wrapper-loader 的原生回调经此推入通道。
2796
- * 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
2797
- * listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
2798
- * 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
2799
- */
2800
- emit(event, ...args) {
2801
- const listeners = this.emitter.listeners(event);
2802
- for (const listener of listeners) try {
2803
- listener(...args);
2804
- } catch (err) {
2805
- for (const handler of this.errorHandlers) handler(err);
2806
- }
2807
- }
2808
- /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
2809
- onError(handler) {
2810
- this.errorHandlers.add(handler);
2811
- return () => {
2812
- this.errorHandlers.delete(handler);
2813
- };
2814
- }
2815
- };
2816
- //#endregion
2817
- //#region src/infra/config.ts
2818
- /**
2819
- * 配置基类(ADR-012)
2820
- *
2821
- * 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
2822
- * (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
2823
- *
2824
- * kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
2825
- * 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
2826
- * 协议配置 schema 在各自协议包,见各包 docs/design.md。
2827
- */
2828
- /** 按扩展名推断格式(.toml → toml,其余 → json)。 */
2829
- function inferFormat(path) {
2830
- if (extname(path).toLowerCase() === ".toml") return "toml";
2831
- return "json";
2832
- }
2833
- function isMissingFileError(err) {
2834
- if (err instanceof Error) return err.code === "ENOENT";
2835
- return false;
2836
- }
2837
- /**
2838
- * 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
2839
- * 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
2840
- * - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
2841
- * - `save()`:按格式序列化(TOML/JSON)原子写。
2842
- */
2843
- var ConfigBase = class {
2844
- path;
2845
- defaults;
2846
- schema;
2847
- format;
2848
- seed;
2849
- value;
2850
- constructor(opts) {
2851
- this.path = opts.path;
2852
- this.defaults = opts.defaults;
2853
- this.schema = opts.schema;
2854
- this.format = opts.format ?? inferFormat(opts.path);
2855
- this.seed = opts.seed;
2856
- this.value = opts.seed ?? opts.defaults;
2857
- }
2858
- /** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
2859
- get() {
2860
- return this.value;
2976
+ this.emitter.removeListener(event, handler);
2977
+ };
2861
2978
  }
2862
2979
  /**
2863
- * 加载配置:
2864
- * - seed 存在 → 直接用 seed(已校验,不读文件;全局 TOML 分段装配方式);
2865
- * - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
2866
- * - 文件存在 → 读取 + 校验,更新内存,返回。
2980
+ * 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
2981
+ * 替代魔法字符串事件名方案。
2867
2982
  */
2868
- async load() {
2869
- if (this.seed !== void 0) {
2870
- this.value = this.seed;
2871
- return this.value;
2872
- }
2873
- let raw;
2874
- try {
2875
- raw = await readFile(this.path, "utf8");
2876
- } catch (err) {
2877
- if (isMissingFileError(err)) {
2878
- await this.save(this.defaults);
2879
- return this.value;
2880
- }
2881
- throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
2882
- }
2883
- this.value = this.parse(raw);
2884
- return this.value;
2885
- }
2886
- /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
2887
- reload() {
2888
- return this.load();
2889
- }
2890
- /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
2891
- async save(next) {
2892
- const parsed = this.parse(this.serialize(next));
2893
- mkdirSync(dirname(this.path), { recursive: true });
2894
- const tmpPath = `${this.path}.tmp`;
2895
- await writeFile(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
2896
- await rename(tmpPath, this.path);
2897
- this.value = parsed;
2898
- }
2899
- /** 对象 → 文本(TOML/JSON)。 */
2900
- serialize(value) {
2901
- if (this.format === "toml") return stringify(value);
2902
- return JSON.stringify(value, null, 4);
2983
+ waitFor(event, opts = {}) {
2984
+ const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
2985
+ return new Promise((resolve, reject) => {
2986
+ const handler = (...args) => {
2987
+ if (opts.filter && !opts.filter(...args)) return;
2988
+ cleanup();
2989
+ resolve(args);
2990
+ };
2991
+ const timer = setTimeout(() => {
2992
+ cleanup();
2993
+ reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
2994
+ }, timeoutMs);
2995
+ const cleanup = () => {
2996
+ clearTimeout(timer);
2997
+ this.emitter.removeListener(event, handler);
2998
+ };
2999
+ this.emitter.addListener(event, handler);
3000
+ });
2903
3001
  }
2904
- /** 文本 → 对象(TOML/JSON)。 */
2905
- deserialize(raw) {
2906
- if (this.format === "toml") return parse(raw);
2907
- return JSON.parse(raw);
3002
+ /**
3003
+ * 类型化派发:wrapper-loader 的原生回调经此推入通道。
3004
+ * 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
3005
+ * listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
3006
+ * 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
3007
+ */
3008
+ /**
3009
+ * 订阅所有事件(IPC 子进程模式整通道转发用:事件名 `"${service}/${method}"`
3010
+ * 经 handler 首参透出,args 原样透传)。返回退订函数。
3011
+ */
3012
+ onAny(handler) {
3013
+ this.anyHandlers.add(handler);
3014
+ return () => {
3015
+ this.anyHandlers.delete(handler);
3016
+ };
2908
3017
  }
2909
- /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
2910
- parse(raw) {
2911
- let input;
2912
- try {
2913
- input = this.deserialize(raw);
3018
+ emit(event, ...args) {
3019
+ for (const anyHandler of this.anyHandlers) try {
3020
+ anyHandler(event, ...args);
2914
3021
  } catch (err) {
2915
- throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
3022
+ for (const errorHandler of this.errorHandlers) errorHandler(err);
2916
3023
  }
2917
- try {
2918
- return this.schema.parse(input);
3024
+ const listeners = this.emitter.listeners(event);
3025
+ for (const listener of listeners) try {
3026
+ listener(...args);
2919
3027
  } catch (err) {
2920
- throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
3028
+ for (const handler of this.errorHandlers) handler(err);
2921
3029
  }
2922
3030
  }
3031
+ /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
3032
+ onError(handler) {
3033
+ this.errorHandlers.add(handler);
3034
+ return () => {
3035
+ this.errorHandlers.delete(handler);
3036
+ };
3037
+ }
2923
3038
  };
2924
- /** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
2925
- function parseToml(text) {
2926
- return parse(text);
2927
- }
2928
- /** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
2929
- function stringifyToml(value) {
2930
- return stringify(value);
2931
- }
2932
3039
  //#endregion
2933
3040
  //#region src/wrapper/session-resolver.ts
2934
3041
  /** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
@@ -2990,50 +3097,14 @@ function getMainSession(ctx) {
2990
3097
  return null;
2991
3098
  }
2992
3099
  //#endregion
2993
- //#region src/wrapper/probe.ts
3100
+ //#region src/wrapper/probe/probe-serialize.ts
2994
3101
  /**
2995
- * 运行时反射探测(ADR-006 落地,P1-4)
2996
- *
2997
- * 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
2998
- * 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
2999
- * 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
3102
+ * 运行时反射探测的序列化工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
3000
3103
  *
3001
- * 用法:boot.cjs 在 NAPUTO_PROBE=1 时于 startNapuketto 后调用 probeRuntime(ctx)。
3002
- * 产物是研究工具数据,类型层据此补全(自研描述,非移植)。
3104
+ * - serialize:深度受限序列化(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)
3105
+ * - tryShape:解析 service getter 返回形状
3106
+ * - shapeKeyGetters:探测 service 关键数据 getter 返回形状
3003
3107
  */
3004
- /** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
3005
- function listMethods(obj) {
3006
- if (obj === null || obj === void 0) return [];
3007
- const names = /* @__PURE__ */ new Set();
3008
- let proto = Object.getPrototypeOf(obj);
3009
- while (proto && proto !== Object.prototype && proto !== Function.prototype) {
3010
- for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3011
- proto = Object.getPrototypeOf(proto);
3012
- }
3013
- for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3014
- return [...names].sort();
3015
- }
3016
- /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3017
- function tryCall(obj, name) {
3018
- try {
3019
- const fn = obj[name];
3020
- if (typeof fn !== "function") return {
3021
- ok: false,
3022
- error: `not a function (typeof=${typeof fn})`
3023
- };
3024
- return {
3025
- ok: true,
3026
- value: fn.call(obj)
3027
- };
3028
- } catch (e) {
3029
- let message = String(e);
3030
- if (e instanceof Error) message = e.message;
3031
- return {
3032
- ok: false,
3033
- error: message
3034
- };
3035
- }
3036
- }
3037
3108
  /** 序列化深度上限(循环引用/深层对象防护)。 */
3038
3109
  const MAX_SERIALIZE_DEPTH = 4;
3039
3110
  /** 数组序列化条数上限。 */
@@ -3048,34 +3119,52 @@ function serialize(value, depth = 0) {
3048
3119
  if (typeof value !== "object") return value;
3049
3120
  if (depth > MAX_SERIALIZE_DEPTH) return "[depth-limit]";
3050
3121
  try {
3051
- if (Array.isArray(value)) return value.slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1));
3052
- if (value instanceof Map) {
3053
- const out = {};
3054
- let i = 0;
3055
- for (const [k, v] of value) {
3056
- if (i >= MAX_OBJECT_KEYS) break;
3057
- out[String(k)] = serialize(v, depth + 1);
3058
- i += 1;
3059
- }
3060
- return {
3061
- kind: "Map",
3062
- size: value.size,
3063
- entries: out
3064
- };
3065
- }
3066
- if (value instanceof Set) return {
3067
- kind: "Set",
3068
- size: value.size,
3069
- values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
3070
- };
3071
- if (typeof value.then === "function") return "[Promise]";
3072
- const out = {};
3073
- for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
3074
- return out;
3122
+ return serializeContainer(value, depth);
3075
3123
  } catch {
3076
3124
  return "[unserializable]";
3077
3125
  }
3078
3126
  }
3127
+ /** 序列化容器对象(数组 / Map / Set / Promise / 普通对象)。 */
3128
+ function serializeContainer(value, depth) {
3129
+ if (Array.isArray(value)) return serializeArray(value, depth);
3130
+ if (value instanceof Map) return serializeMap(value, depth);
3131
+ if (value instanceof Set) return serializeSet(value, depth);
3132
+ if (typeof value.then === "function") return "[Promise]";
3133
+ return serializeObject(value, depth);
3134
+ }
3135
+ /** 数组序列化(条数截断)。 */
3136
+ function serializeArray(value, depth) {
3137
+ return value.slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1));
3138
+ }
3139
+ /** Map 序列化(键转字符串,条目截断)。 */
3140
+ function serializeMap(value, depth) {
3141
+ const out = {};
3142
+ let i = 0;
3143
+ for (const [k, v] of value) {
3144
+ if (i >= MAX_OBJECT_KEYS) break;
3145
+ out[String(k)] = serialize(v, depth + 1);
3146
+ i += 1;
3147
+ }
3148
+ return {
3149
+ kind: "Map",
3150
+ size: value.size,
3151
+ entries: out
3152
+ };
3153
+ }
3154
+ /** Set 序列化(值截断)。 */
3155
+ function serializeSet(value, depth) {
3156
+ return {
3157
+ kind: "Set",
3158
+ size: value.size,
3159
+ values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
3160
+ };
3161
+ }
3162
+ /** 普通对象序列化(键截断)。 */
3163
+ function serializeObject(value, depth) {
3164
+ const out = {};
3165
+ for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
3166
+ return out;
3167
+ }
3079
3168
  /** 尝试解析一个 service getter 的返回值形状(深度受限)。 */
3080
3169
  function tryShape(session, getter) {
3081
3170
  try {
@@ -3097,6 +3186,49 @@ function shapeKeyGetters(session, serviceMethods) {
3097
3186
  for (const m of candidates.slice(0, 8)) out[m] = tryShape(session, m);
3098
3187
  return out;
3099
3188
  }
3189
+ //#endregion
3190
+ //#region src/wrapper/probe/probe-utils.ts
3191
+ /**
3192
+ * 运行时反射探测基础工具(从 probe.ts 拆分,2026-08-08 FTA 优化)
3193
+ *
3194
+ * - listMethods:反射枚举对象原型链上的方法名(去重、去构造器)
3195
+ * - tryCall:安全调用方法拿返回值(失败记录错误,不中断探测)
3196
+ */
3197
+ /** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
3198
+ function listMethods(obj) {
3199
+ if (obj === null || obj === void 0) return [];
3200
+ const names = /* @__PURE__ */ new Set();
3201
+ let proto = Object.getPrototypeOf(obj);
3202
+ while (proto && proto !== Object.prototype && proto !== Function.prototype) {
3203
+ for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3204
+ proto = Object.getPrototypeOf(proto);
3205
+ }
3206
+ for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3207
+ return [...names].sort();
3208
+ }
3209
+ /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3210
+ function tryCall(obj, name) {
3211
+ try {
3212
+ const fn = obj[name];
3213
+ if (typeof fn !== "function") return {
3214
+ ok: false,
3215
+ error: `not a function (typeof=${typeof fn})`
3216
+ };
3217
+ return {
3218
+ ok: true,
3219
+ value: fn.call(obj)
3220
+ };
3221
+ } catch (e) {
3222
+ let message = String(e);
3223
+ if (e instanceof Error) message = e.message;
3224
+ return {
3225
+ ok: false,
3226
+ error: message
3227
+ };
3228
+ }
3229
+ }
3230
+ //#endregion
3231
+ //#region src/wrapper/probe/probe-services.ts
3100
3232
  /** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
3101
3233
  function probeExportCtors(ctx) {
3102
3234
  const keys = [
@@ -3154,6 +3286,22 @@ function probeLoginService(ctx) {
3154
3286
  ]) if (typeof inst.value[m] === "function") out[m] = tryShape(inst.value, m);
3155
3287
  return out;
3156
3288
  }
3289
+ //#endregion
3290
+ //#region src/wrapper/probe/probe.ts
3291
+ /**
3292
+ * 运行时反射探测(ADR-006 落地,P1-4)
3293
+ *
3294
+ * 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
3295
+ * 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
3296
+ * 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
3297
+ *
3298
+ * 用法:boot.cjs 在 NAPUTO_PROBE=1 时于 startNapuketto 后调用 probeRuntime(ctx)。
3299
+ * 产物是研究工具数据,类型层据此补全(自研描述,非移植)。
3300
+ *
3301
+ * 工具拆分(2026-08-08 FTA 优化):方法枚举/安全调用 → probe-utils.ts;
3302
+ * 序列化/形状探测 → probe-serialize.ts;exports/service 探测 → probe-services.ts;
3303
+ * 本文件只留 session 探测编排(probeStartup/probeSession/probeRuntime)。
3304
+ */
3157
3305
  /** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
3158
3306
  function enumerateSessionIds(ctx) {
3159
3307
  const S = ctx.exports.NodeIQQNTWrapperSession;
@@ -3184,47 +3332,38 @@ function enumerateSessionIds(ctx) {
3184
3332
  } catch {}
3185
3333
  return out;
3186
3334
  }
3187
- /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3188
- function probeStartup(ctx) {
3189
- const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
3190
- if (startup === null || typeof startup.create !== "function") return null;
3191
- const out = { staticMethods: listMethods(startup) };
3192
- const created = tryCall(startup, "create");
3193
- out["create"] = created.ok ? {
3194
- methods: created.value ? listMethods(created.value) : [],
3195
- value: serialize(created.value)
3196
- } : { error: created.error };
3197
- if (created.ok && created.value) {
3198
- const started = tryCall(created.value, "start");
3199
- out["start"] = started.ok ? { ok: true } : { error: started.error };
3200
- const ids = tryCall(created.value, "getSessionIdList");
3201
- const idsValue = ids.ok ? serialize(ids.value) : null;
3202
- out["createdGetSessionIdList"] = ids.ok ? { value: idsValue } : { error: ids.error };
3203
- const foundIds = [];
3204
- if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
3205
- const id = typeof v === "string" ? v : String(k);
3206
- foundIds.push(id);
3207
- }
3208
- out["sessionIds"] = foundIds;
3209
- if (foundIds.length > 0) {
3210
- const mainId = foundIds.find((id) => id.startsWith("nt_")) ?? foundIds[0];
3211
- out["mainSessionId"] = mainId;
3212
- const S = ctx.exports.NodeIQQNTWrapperSession;
3213
- const fn = S["getNTWrapperSession"];
3214
- if (typeof fn === "function" && mainId !== void 0) try {
3215
- const value = fn.call(S, mainId);
3216
- out["mainSession"] = value ? {
3217
- methods: listMethods(value),
3218
- ownKeys: Object.getOwnPropertyNames(value)
3219
- } : null;
3220
- } catch (e) {
3221
- out["mainSession"] = { error: e instanceof Error ? e.message : String(e) };
3222
- }
3223
- out["enumSessionIds"] = enumerateSessionIds(ctx);
3224
- return out;
3225
- }
3335
+ /** 探测 startup 实例:start() + getSessionIdList(),提取 sessionIds(Map 值或键)。 */
3336
+ function probeStartupChain(created) {
3337
+ const started = tryCall(created, "start");
3338
+ const ids = tryCall(created, "getSessionIdList");
3339
+ const sessionIds = [];
3340
+ if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
3341
+ const id = typeof v === "string" ? v : String(k);
3342
+ sessionIds.push(id);
3226
3343
  }
3227
- out["enumSessionIds"] = enumerateSessionIds(ctx);
3344
+ return {
3345
+ start: started.ok ? { ok: true } : { error: started.error },
3346
+ createdGetSessionIdList: ids.ok ? { value: serialize(ids.value) } : { error: ids.error },
3347
+ sessionIds
3348
+ };
3349
+ }
3350
+ /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
3351
+ function probeSessionById(ctx, id) {
3352
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3353
+ const fn = S["getNTWrapperSession"];
3354
+ if (typeof fn !== "function") return null;
3355
+ try {
3356
+ const value = fn.call(S, id);
3357
+ return value ? {
3358
+ methods: listMethods(value),
3359
+ ownKeys: Object.getOwnPropertyNames(value)
3360
+ } : null;
3361
+ } catch (e) {
3362
+ return { error: e instanceof Error ? e.message : String(e) };
3363
+ }
3364
+ }
3365
+ /** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
3366
+ function probeByNameFallback(ctx) {
3228
3367
  const S = ctx.exports.NodeIQQNTWrapperSession;
3229
3368
  const candidates = [
3230
3369
  "main",
@@ -3244,7 +3383,31 @@ function probeStartup(ctx) {
3244
3383
  } catch (e) {
3245
3384
  getResults[name === "" ? "<empty>" : name] = { error: e instanceof Error ? e.message : String(e) };
3246
3385
  }
3247
- out["getNTWrapperSession"] = getResults;
3386
+ return getResults;
3387
+ }
3388
+ /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3389
+ function probeStartup(ctx) {
3390
+ const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
3391
+ if (startup === null || typeof startup.create !== "function") return null;
3392
+ const out = { staticMethods: listMethods(startup) };
3393
+ const created = tryCall(startup, "create");
3394
+ out["create"] = created.ok ? {
3395
+ methods: created.value ? listMethods(created.value) : [],
3396
+ value: serialize(created.value)
3397
+ } : { error: created.error };
3398
+ if (created.ok && created.value) {
3399
+ const chain = probeStartupChain(created.value);
3400
+ out["start"] = chain.start;
3401
+ out["createdGetSessionIdList"] = chain.createdGetSessionIdList;
3402
+ out["sessionIds"] = chain.sessionIds;
3403
+ const mainId = chain.sessionIds.find((id) => id.startsWith("nt_")) ?? chain.sessionIds[0];
3404
+ if (mainId !== void 0) {
3405
+ out["mainSessionId"] = mainId;
3406
+ out["mainSession"] = probeSessionById(ctx, mainId);
3407
+ }
3408
+ }
3409
+ out["enumSessionIds"] = enumerateSessionIds(ctx);
3410
+ if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
3248
3411
  return out;
3249
3412
  }
3250
3413
  /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */