@napuketto/kernel 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -159,14 +159,311 @@ function stringifyToml(value) {
159
159
  return stringify(value);
160
160
  }
161
161
  //#endregion
162
+ //#region src/infra/log-config.ts
163
+ /**
164
+ * [log] 配置段解析(napuketto.toml 全局段,2026-10-25 日志可配置化)。
165
+ *
166
+ * 统一解析入口:cli(apps/cli)与 loader 引导链共用本函数(经 kernel 导出),
167
+ * 避免两处手写解析漂移。输入为 TOML 解析后的 [log] 段原对象(缺失传 undefined)
168
+ * + 环境变量快照;输出为已解析的 LogConfig(env 覆盖优先于 toml)。
169
+ *
170
+ * 环境变量(优先级高于 toml;非法值忽略——与既有 NAPKETTO_LOG_LEVEL 行为一致):
171
+ * - NAPUKETTO_LOG_LEVEL / NAPKETTO_LOG_LEVEL(兼容旧拼写,NAPUKETTO_ 优先)
172
+ * - NAPUKETTO_LOG_FORMAT(pretty | single-line | json)
173
+ * - NAPUKETTO_NO_BANNER(1/true = 关 banner)
174
+ * - NAPUKETTO_LOG_COLORIZE(1/true/0/false,CLI 顶层进程向子进程传播控制台
175
+ * 渲染决策用;toml 不暴露此键)
176
+ */
177
+ /** pino 标准级别 + silent。 */
178
+ const LOG_LEVEL_VALUES = [
179
+ "trace",
180
+ "debug",
181
+ "info",
182
+ "warn",
183
+ "error",
184
+ "fatal",
185
+ "silent"
186
+ ];
187
+ const LOG_FORMAT_VALUES = [
188
+ "pretty",
189
+ "single-line",
190
+ "json"
191
+ ];
192
+ const TIMESTAMP_MODE_VALUES = [
193
+ "local",
194
+ "iso8601",
195
+ "elapsed"
196
+ ];
197
+ const MODULE_NAME_VALUES = [
198
+ "cli",
199
+ "kernel",
200
+ "loader",
201
+ "ob11"
202
+ ];
203
+ /** 校验级别字面量;非法值抛 INVALID_PARAM。 */
204
+ function requireLevel(value, key) {
205
+ if (typeof value !== "string" || !LOG_LEVEL_VALUES.includes(value)) throw kernelError(`主配置 ${key} 必须是日志级别(${LOG_LEVEL_VALUES.join(" / ")})`, "INVALID_PARAM");
206
+ return value;
207
+ }
208
+ /** 宽松级别解析(env 覆盖用;非法/缺失返回 undefined 不生效)。 */
209
+ function optionalLevel(value) {
210
+ if (value === void 0) return;
211
+ return LOG_LEVEL_VALUES.includes(value) ? value : void 0;
212
+ }
213
+ /** 宽松格式解析(env 覆盖用)。 */
214
+ function optionalFormat(value) {
215
+ if (value === void 0) return;
216
+ return LOG_FORMAT_VALUES.includes(value) ? value : void 0;
217
+ }
218
+ /** 宽松布尔解析(env 用:仅识别 1/true/0/false,其余忽略)。 */
219
+ function optionalBool(value) {
220
+ if (value === "1" || value === "true") return true;
221
+ if (value === "0" || value === "false") return false;
222
+ }
223
+ /** 解析 [log.levels] 按模块覆盖表(未知模块名报错,键集封闭防拼写错误)。 */
224
+ function parseModuleLevels(raw) {
225
+ if (raw === void 0) return {};
226
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.levels 必须是对象", "INVALID_PARAM");
227
+ const out = {};
228
+ for (const [key, value] of Object.entries(raw)) {
229
+ if (!MODULE_NAME_VALUES.includes(key)) throw kernelError(`主配置 log.levels 未知模块 "${key}"(合法模块:${MODULE_NAME_VALUES.join(" / ")})`, "INVALID_PARAM");
230
+ out[key] = requireLevel(value, `log.levels.${key}`);
231
+ }
232
+ return out;
233
+ }
234
+ /** 解析 [log.file] 独立文件 sink。 */
235
+ function parseLogFile(raw, fallbackLevel) {
236
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.file 必须是对象", "INVALID_PARAM");
237
+ const record = raw;
238
+ const path = record["path"];
239
+ if (typeof path !== "string" || path === "") throw kernelError("主配置 log.file.path 必须是非空字符串", "INVALID_PARAM");
240
+ return {
241
+ path,
242
+ level: record["level"] === void 0 ? fallbackLevel : requireLevel(record["level"], "log.file.level")
243
+ };
244
+ }
245
+ /** 校验字面量枚举值(非法抛 INVALID_PARAM,报错文案带键名与合法值集)。 */
246
+ function requireEnum(value, key, values) {
247
+ if (typeof value !== "string" || !values.includes(value)) throw kernelError(`主配置 ${key} 必须是(${values.join(" / ")})`, "INVALID_PARAM");
248
+ return value;
249
+ }
250
+ /** 校验布尔字段(undefined 保留原值)。 */
251
+ function applyBoolean(draft, raw, key) {
252
+ if (raw[key] === void 0) return;
253
+ if (typeof raw[key] !== "boolean") throw kernelError(`主配置 log.${key} 必须是布尔值`, "INVALID_PARAM");
254
+ draft[key] = raw[key];
255
+ }
256
+ /** toml [log] 段字段填充(level/format/timestamp/banner/unboundReceive)。 */
257
+ function applySectionFields(draft, raw) {
258
+ if (raw["level"] !== void 0) draft.level = requireLevel(raw["level"], "log.level");
259
+ if (raw["format"] !== void 0) draft.format = requireEnum(raw["format"], "log.format", LOG_FORMAT_VALUES);
260
+ if (raw["timestamp"] !== void 0) draft.timestamp = requireEnum(raw["timestamp"], "log.timestamp", TIMESTAMP_MODE_VALUES);
261
+ applyBoolean(draft, raw, "banner");
262
+ applyBoolean(draft, raw, "unboundReceive");
263
+ }
264
+ /** env 覆盖(优先级高于 toml;非法值忽略):兼容旧拼写 NAPKETTO_LOG_LEVEL。
265
+ * file.level 显式配置时不被 env 全局级别顶掉(缺省跟随全局)。 */
266
+ function applyEnvOverrides(draft, env) {
267
+ const envLevel = optionalLevel(env["NAPUKETTO_LOG_LEVEL"]) ?? optionalLevel(env["NAPKETTO_LOG_LEVEL"]);
268
+ if (envLevel !== void 0) {
269
+ draft.level = envLevel;
270
+ if (draft.file !== void 0 && draft.fileLevelExplicit !== true) draft.file = {
271
+ ...draft.file,
272
+ level: envLevel
273
+ };
274
+ }
275
+ const envFormat = optionalFormat(env["NAPUKETTO_LOG_FORMAT"]);
276
+ if (envFormat !== void 0) draft.format = envFormat;
277
+ if (optionalBool(env["NAPUKETTO_NO_BANNER"]) === true) draft.banner = false;
278
+ const envColorize = optionalBool(env["NAPUKETTO_LOG_COLORIZE"]);
279
+ if (envColorize !== void 0) draft.colorize = envColorize;
280
+ }
281
+ /**
282
+ * 解析 [log] 段 + 环境变量覆盖。
283
+ *
284
+ * @param section TOML 解析后的 [log] 段(缺失/undefined = 全缺省)
285
+ * @param env 环境变量快照(默认空对象 = 无 env 覆盖)
286
+ */
287
+ function parseLogConfig(section, env = {}) {
288
+ const draft = {
289
+ level: "info",
290
+ banner: true,
291
+ timestamp: "local",
292
+ unboundReceive: true,
293
+ levels: {}
294
+ };
295
+ if (section !== void 0) {
296
+ if (typeof section !== "object" || section === null) throw kernelError("主配置 log 段必须是对象", "INVALID_PARAM");
297
+ const raw = section;
298
+ applySectionFields(draft, raw);
299
+ draft.levels = parseModuleLevels(raw["levels"]);
300
+ if (raw["file"] !== void 0) {
301
+ draft.file = parseLogFile(raw["file"], draft.level);
302
+ draft.fileLevelExplicit = typeof raw["file"] === "object" && raw["file"] !== null && raw["file"]["level"] !== void 0;
303
+ }
304
+ }
305
+ applyEnvOverrides(draft, env);
306
+ delete draft.fileLevelExplicit;
307
+ return draft;
308
+ }
309
+ //#endregion
310
+ //#region src/infra/log-format.ts
311
+ /**
312
+ * 结构化控制台流(single-line / json 格式,2026-10-25 日志可配置化)。
313
+ *
314
+ * pino-pretty 面向交互终端(多行展开 / JSON 内联渲染),其 singleLine 输出为
315
+ * `msg {"k":"v"}` 形态且 messageFormat 会与字段追加双重渲染——无法干净满足
316
+ * 嵌入式宿主的「一条记录一行、字段 k=v / NDJSON」契约,故自写轻量渲染流:
317
+ *
318
+ * single-line:[时间] LEVEL (name/pid): 消息 key=value key=value
319
+ * json:NDJSON,每行 {time, level, module, account?, pid, msg, ...fields}
320
+ *
321
+ * 时间渲染(LogConfig.timestamp):local = [HH:MM:ss.l](与 pretty 缺省一致)、
322
+ * iso8601 = 2026-10-25T16:01:33.294+08:00、elapsed = +123.456s(进程内单调秒)。
323
+ * json 的 time 恒为 ISO8601(机器可解析契约,不受 timestamp 键影响)。
324
+ */
325
+ /** 级别数值 → 名称(pino 标准映射)。 */
326
+ const LEVEL_LABELS = {
327
+ 10: "trace",
328
+ 20: "debug",
329
+ 30: "info",
330
+ 40: "warn",
331
+ 50: "error",
332
+ 60: "fatal"
333
+ };
334
+ /** single-line 颜色(colorize 时仅着色级别与元数据头,正文默认色)。 */
335
+ const LEVEL_COLORS = {
336
+ trace: "\x1B[34m",
337
+ debug: "\x1B[36m",
338
+ info: "\x1B[32m",
339
+ warn: "\x1B[33m",
340
+ error: "\x1B[31m",
341
+ fatal: "\x1B[91m"
342
+ };
343
+ const DIM = "\x1B[2m";
344
+ const RESET = "\x1B[0m";
345
+ /** 2 位补零。 */
346
+ function pad2(n) {
347
+ return String(n).padStart(2, "0");
348
+ }
349
+ /** 单调墙钟毫秒 → 本地时区 ISO8601(+08:00 形态;new Date(epoch) 构造不受 Date.now 篡改影响)。 */
350
+ function isoLocalTime(epochMs) {
351
+ const d = new Date(epochMs);
352
+ const offsetMinutes = -d.getTimezoneOffset();
353
+ const sign = offsetMinutes >= 0 ? "+" : "-";
354
+ const abs = Math.abs(offsetMinutes);
355
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
356
+ }
357
+ /** 单调墙钟毫秒 → 本地时分秒(与 pino-pretty SYS:HH:MM:ss.l 一致的短格式)。 */
358
+ function localShortTime(epochMs) {
359
+ const d = new Date(epochMs);
360
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`;
361
+ }
362
+ /** 单调墙钟毫秒 → 进程内相对秒(+123.456s)。 */
363
+ function elapsedLabel(epochMs, timeOriginMs) {
364
+ return `+${((epochMs - timeOriginMs) / 1e3).toFixed(3)}s`;
365
+ }
366
+ /** 渲染记录时间(timestamp 模式分发;json 恒 ISO8601)。 */
367
+ function renderTime(rawTime, mode, forceIso, timeOriginMs) {
368
+ if (typeof rawTime === "string") return rawTime;
369
+ const epoch = typeof rawTime === "number" ? rawTime : timeOriginMs;
370
+ if (forceIso || mode === "iso8601") return isoLocalTime(epoch);
371
+ if (mode === "elapsed") return elapsedLabel(epoch, timeOriginMs);
372
+ return localShortTime(epoch);
373
+ }
374
+ /** 级别数值 → 标签名。 */
375
+ function levelLabel(raw) {
376
+ if (typeof raw === "number") return LEVEL_LABELS[raw] ?? String(raw);
377
+ return typeof raw === "string" ? raw.toUpperCase() : "LOG";
378
+ }
379
+ /** k=v 内联值:字符串裸输出(含空格时不加引号——宽松人类格式),其余 JSON 序列化。 */
380
+ function inlineValue(value) {
381
+ if (typeof value === "string") return value;
382
+ return JSON.stringify(value) ?? "null";
383
+ }
384
+ /** 结构化输出忽略的标准字段(渲染进头/独立字段,不进 k=v 尾部)。 */
385
+ const RESERVED_KEYS = /* @__PURE__ */ new Set([
386
+ "pid",
387
+ "hostname",
388
+ "level",
389
+ "time",
390
+ "msg",
391
+ "name"
392
+ ]);
393
+ /** json 渲染:NDJSON 契约 {time, level, module, account?, pid, msg, ...fields}。 */
394
+ function renderJsonLine(record, ctx) {
395
+ const { time, level, name, module, account, pid, msg: _msg, ...fields } = record;
396
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
397
+ const jsonLine = {
398
+ time: renderTime(time, ctx.timestamp, true, ctx.timeOriginMs),
399
+ level: levelLabel(level).toLowerCase(),
400
+ ...module !== void 0 ? { module } : typeof name === "string" ? { module: name } : {},
401
+ ...account !== void 0 ? { account } : {},
402
+ ...typeof pid === "number" ? { pid } : {},
403
+ msg,
404
+ ...fields
405
+ };
406
+ return `${JSON.stringify(jsonLine)}\n`;
407
+ }
408
+ /** single-line 元数据头:(name/pid),无 name 时 (pid)。 */
409
+ function renderMeta(record) {
410
+ const pid = record["pid"] ?? "-";
411
+ const name = record["name"];
412
+ return typeof name === "string" ? `(${name}/${pid})` : `(${pid})`;
413
+ }
414
+ /** single-line 附加字段:非保留键按 k=v 内联(无字段时空串)。 */
415
+ function renderKvFields(record) {
416
+ const fields = [];
417
+ for (const [key, value] of Object.entries(record)) if (!RESERVED_KEYS.has(key)) fields.push(`${key}=${inlineValue(value)}`);
418
+ return fields.length > 0 ? ` ${fields.join(" ")}` : "";
419
+ }
420
+ /** single-line 渲染:[时间] LEVEL (name/pid): 消息 k=v k=v(级别大写与 pretty 一致)。 */
421
+ function renderSingleLine(record, ctx) {
422
+ const label = levelLabel(record["level"]);
423
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
424
+ const time = renderTime(record["time"], ctx.timestamp, false, ctx.timeOriginMs);
425
+ const labelUpper = label.toUpperCase();
426
+ const meta = renderMeta(record);
427
+ const fieldsSuffix = renderKvFields(record);
428
+ if (ctx.useColor) {
429
+ const color = LEVEL_COLORS[label.toLowerCase()] ?? "";
430
+ return `${DIM}[${time}]${RESET} ${color}${labelUpper}${RESET} ${DIM}${meta}:${RESET} ${msg}${fieldsSuffix}\n`;
431
+ }
432
+ return `[${time}] ${labelUpper} ${meta}: ${msg}${fieldsSuffix}\n`;
433
+ }
434
+ /** 创建结构化控制台流(single-line / json)。 */
435
+ function createConsoleFormatStream(opts) {
436
+ const out = opts.destination ?? process.stdout;
437
+ const ctx = {
438
+ timestamp: opts.timestamp,
439
+ timeOriginMs: performance.timeOrigin,
440
+ useColor: opts.format === "single-line" && opts.colorize
441
+ };
442
+ const render = opts.format === "json" ? renderJsonLine : renderSingleLine;
443
+ return { write(line) {
444
+ let record;
445
+ try {
446
+ record = JSON.parse(line);
447
+ } catch {
448
+ out.write(`${line}\n`);
449
+ return;
450
+ }
451
+ out.write(render(record, ctx));
452
+ } };
453
+ }
454
+ //#endregion
162
455
  //#region src/infra/logger.ts
163
456
  /**
164
457
  * pino 日志封装(ADR-007)
165
458
  *
166
- * - console(pino-pretty 可读输出)+ 可选文件双目的地
167
- * - 统一 level 控制
459
+ * - console(pretty 可读输出 / single-line / json 结构化)+ 可选文件双目的地
460
+ * - 统一 level 控制(控制台与文件可分别设级,2026-10-25 日志可配置化)
168
461
  * - 内置 redact(token / 票据等敏感字段不打日志),可追加
169
462
  * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
463
+ *
464
+ * 时间源(2026-10-25 修复):QQNT 原生层会在 onRecvSysMsg 回调窗口内篡改进程
465
+ * Date(实测记录时间冻结为假值、回调外恢复),故时间戳统一取
466
+ * performance.timeOrigin + performance.now() 单调派生墙钟,不受 Date 篡改影响。
170
467
  */
171
468
  /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
172
469
  const DEFAULT_REDACT_PATHS = [
@@ -176,39 +473,91 @@ const DEFAULT_REDACT_PATHS = [
176
473
  "password",
177
474
  "secret"
178
475
  ];
476
+ /** 级别数值序(求多目的地最低级别用)。 */
477
+ const LEVEL_ORDER = [
478
+ "trace",
479
+ "debug",
480
+ "info",
481
+ "warn",
482
+ "error",
483
+ "fatal",
484
+ "silent"
485
+ ];
486
+ /** 取两级别中更低(更宽容)的一个。 */
487
+ function minLevel(a, b) {
488
+ return LEVEL_ORDER.indexOf(a) <= LEVEL_ORDER.indexOf(b) ? a : b;
489
+ }
490
+ /** 单调墙钟毫秒(timeOrigin 为进程启动真实 Unix 时间,不受进程内 Date 篡改影响)。 */
491
+ function wallMs() {
492
+ return performance.timeOrigin + performance.now();
493
+ }
494
+ /** 进程内相对秒标签(elapsed 模式,pretty 字符串时间路径用)。 */
495
+ function elapsedLabelNow() {
496
+ return `+${(performance.now() / 1e3).toFixed(3)}s`;
497
+ }
179
498
  function toSingle(streams) {
180
499
  const [only] = streams;
181
500
  if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
182
501
  return only.stream;
183
502
  }
184
- /**
185
- * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
186
- *
187
- * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
188
- * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
189
- */
503
+ /** 构造控制台流:pretty 走 pino-pretty,single-line/json 走结构化渲染流。 */
504
+ function buildConsoleStream(params) {
505
+ const { format, timestampMode, colorize, destination } = params;
506
+ if (format !== "pretty") return createConsoleFormatStream({
507
+ format,
508
+ timestamp: timestampMode,
509
+ colorize,
510
+ destination
511
+ });
512
+ return pinoPretty({
513
+ colorize,
514
+ ...timestampMode === "local" ? { translateTime: "SYS:HH:MM:ss.l" } : {},
515
+ ...timestampMode === "iso8601" ? { translateTime: "SYS:yyyy-mm-dd'T'HH:MM:ss.l Z" } : {},
516
+ singleLine: false,
517
+ destination
518
+ });
519
+ }
520
+ /** 折叠 LoggerOptions 的渲染缺省:格式按 TTY 自动(TTY pretty / pipe single-line)、
521
+ * 颜色仅在 pretty 且 TTY 时开。 */
522
+ function resolveRenderParams(opts) {
523
+ const destination = opts.destination ?? process.stdout;
524
+ const isTty = destination.isTTY === true;
525
+ const format = opts.format ?? (isTty ? "pretty" : "single-line");
526
+ return {
527
+ format,
528
+ timestampMode: opts.timestamp ?? "local",
529
+ colorize: opts.colorize ?? (format === "pretty" && isTty),
530
+ destination
531
+ };
532
+ }
190
533
  function createLogger(opts = {}) {
191
- const level = opts.level ?? "info";
192
- const consoleEnabled = opts.console ?? true;
534
+ const consoleLevel = opts.level ?? "info";
535
+ const fileLevel = opts.fileLevel ?? consoleLevel;
193
536
  const { file, base } = opts;
537
+ const render = resolveRenderParams(opts);
194
538
  const streams = [];
195
- if (consoleEnabled) streams.push({ stream: pinoPretty({
196
- colorize: true,
197
- translateTime: "SYS:HH:MM:ss.l",
198
- singleLine: false,
199
- destination: process.stdout
200
- }) });
539
+ if ((opts.console ?? true) === true) streams.push({
540
+ stream: buildConsoleStream(render),
541
+ level: consoleLevel
542
+ });
201
543
  if (file) {
202
544
  mkdirSync(dirname(file), { recursive: true });
203
- streams.push({ stream: pino.destination({
204
- dest: file,
205
- sync: true
206
- }) });
545
+ streams.push({
546
+ stream: pino.destination({
547
+ dest: file,
548
+ sync: true
549
+ }),
550
+ level: fileLevel
551
+ });
207
552
  }
208
553
  if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
554
+ let gate = "fatal";
555
+ for (const entry of streams) gate = minLevel(gate, entry.level);
556
+ const pinoTimestamp = render.format === "pretty" && render.timestampMode === "elapsed" ? () => `,"time":"${elapsedLabelNow()}"` : () => `,"time":${wallMs()}`;
209
557
  const pinoOptions = {
210
- level,
211
- redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
558
+ level: gate,
559
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []],
560
+ timestamp: pinoTimestamp
212
561
  };
213
562
  if (base) pinoOptions.base = {
214
563
  pid: process.pid,
@@ -3745,7 +4094,14 @@ var NapukettoCore = class NapukettoCore {
3745
4094
  level: opts.logLevel ?? "info",
3746
4095
  console: opts.consoleLog ?? true,
3747
4096
  file: opts.logFile ?? paths.file("logs", "napuketto.log"),
3748
- base: { name: "kernel" }
4097
+ fileLevel: opts.logFileLevel,
4098
+ format: opts.logFormat,
4099
+ timestamp: opts.logTimestamp,
4100
+ colorize: opts.logColorize,
4101
+ base: {
4102
+ name: "kernel",
4103
+ account: opts.account
4104
+ }
3749
4105
  });
3750
4106
  return new NapukettoCore(createCoreContext({
3751
4107
  logger,
@@ -3790,22 +4146,23 @@ var NapukettoCore = class NapukettoCore {
3790
4146
  this.ctx.login = loginResult;
3791
4147
  return loginResult;
3792
4148
  }
3793
- let loginResult;
4149
+ const loginResult = await this.loginQuickFirst(wrapper, opts);
4150
+ this.ctx.login = loginResult;
4151
+ return loginResult;
4152
+ }
4153
+ /** 快速登录优先,失败按 qrFallback 决定是否回退 QR(QR 应直接出码,见 loginByQr 注)。 */
4154
+ async loginQuickFirst(wrapper, opts) {
3794
4155
  try {
3795
- const quickOpts = {};
3796
- if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
3797
- if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
3798
- loginResult = await quickLogin(wrapper, quickOpts);
4156
+ const result = await quickLogin(wrapper, buildQuickLoginOptions(opts));
3799
4157
  this.ctx.logger.info({
3800
- uin: loginResult.uin,
3801
- uid: loginResult.uid
4158
+ uin: result.uin,
4159
+ uid: result.uid
3802
4160
  }, "快速登录成功");
4161
+ return result;
3803
4162
  } catch (err) {
3804
4163
  if (opts.qrFallback !== true) throw err;
3805
- loginResult = await this.loginByQr(opts);
4164
+ return await this.loginByQr(opts);
3806
4165
  }
3807
- this.ctx.login = loginResult;
3808
- return loginResult;
3809
4166
  }
3810
4167
  /**
3811
4168
  * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
@@ -3854,8 +4211,7 @@ var NapukettoCore = class NapukettoCore {
3854
4211
  try {
3855
4212
  session.start();
3856
4213
  await waitQrLoggedIn(session);
3857
- const self = session.selfInfo;
3858
- if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4214
+ const self = requireQrSelfInfo(session);
3859
4215
  return {
3860
4216
  uin: self.uin,
3861
4217
  uid: self.uid,
@@ -3877,14 +4233,14 @@ var NapukettoCore = class NapukettoCore {
3877
4233
  });
3878
4234
  });
3879
4235
  session.onStateChange((state) => {
3880
- this.ctx.logger.info({ state }, "QR 登录状态");
3881
- const message = state === "failed" ? session.failureReason ?? void 0 : void 0;
3882
- opts.onLoginProgress?.({
3883
- state,
3884
- ...message !== void 0 ? { message } : {}
3885
- });
4236
+ this.forwardQrState(session, opts, state);
3886
4237
  });
3887
4238
  }
4239
+ /** QR 状态转发:日志 + onLoginProgress(failed 附 failureReason)。 */
4240
+ forwardQrState(session, opts, state) {
4241
+ this.ctx.logger.info({ state }, "QR 登录状态");
4242
+ opts.onLoginProgress?.(qrProgressEvent(state, session.failureReason ?? void 0));
4243
+ }
3888
4244
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
3889
4245
  stop() {
3890
4246
  this.ctx.logger.info("NapukettoCore 停止");
@@ -3897,6 +4253,27 @@ function writeQrCodePng(filePath, pngBase64) {
3897
4253
  writeFileSync(filePath, Buffer.from(pngBase64, "base64"));
3898
4254
  } catch {}
3899
4255
  }
4256
+ /** CoreLoginOptions → quickLogin 参数(仅取快速登录相关两键,QR 键不透传)。 */
4257
+ function buildQuickLoginOptions(opts) {
4258
+ const quickOpts = {};
4259
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
4260
+ if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
4261
+ return quickOpts;
4262
+ }
4263
+ /** QR 登录终态后的 selfInfo 提取(空值按登录失败抛出)。 */
4264
+ function requireQrSelfInfo(session) {
4265
+ const self = session.selfInfo;
4266
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4267
+ return self;
4268
+ }
4269
+ /** QR 进度事件折叠(failed 附 failureReason 为 message,其余不带 message 键)。 */
4270
+ function qrProgressEvent(state, reason) {
4271
+ const message = state === "failed" ? reason : void 0;
4272
+ return {
4273
+ state,
4274
+ ...message !== void 0 ? { message } : {}
4275
+ };
4276
+ }
3900
4277
  /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3901
4278
  function waitQrLoggedIn(session) {
3902
4279
  return new Promise((resolve, reject) => {
@@ -4498,6 +4875,6 @@ function listQQVersions(installDir) {
4498
4875
  }).sort().reverse();
4499
4876
  }
4500
4877
  //#endregion
4501
- export { BuddyCache, ChatType, ConfigBase, DEFAULT_DATA_ROOT_NAME, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
4878
+ export { BuddyCache, ChatType, ConfigBase, DEFAULT_DATA_ROOT_NAME, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseLogConfig, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
4502
4879
 
4503
4880
  //# sourceMappingURL=index.mjs.map