@napuketto/kernel 0.2.4 → 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.cjs CHANGED
@@ -185,14 +185,311 @@ function stringifyToml(value) {
185
185
  return (0, smol_toml.stringify)(value);
186
186
  }
187
187
  //#endregion
188
+ //#region src/infra/log-config.ts
189
+ /**
190
+ * [log] 配置段解析(napuketto.toml 全局段,2026-10-25 日志可配置化)。
191
+ *
192
+ * 统一解析入口:cli(apps/cli)与 loader 引导链共用本函数(经 kernel 导出),
193
+ * 避免两处手写解析漂移。输入为 TOML 解析后的 [log] 段原对象(缺失传 undefined)
194
+ * + 环境变量快照;输出为已解析的 LogConfig(env 覆盖优先于 toml)。
195
+ *
196
+ * 环境变量(优先级高于 toml;非法值忽略——与既有 NAPKETTO_LOG_LEVEL 行为一致):
197
+ * - NAPUKETTO_LOG_LEVEL / NAPKETTO_LOG_LEVEL(兼容旧拼写,NAPUKETTO_ 优先)
198
+ * - NAPUKETTO_LOG_FORMAT(pretty | single-line | json)
199
+ * - NAPUKETTO_NO_BANNER(1/true = 关 banner)
200
+ * - NAPUKETTO_LOG_COLORIZE(1/true/0/false,CLI 顶层进程向子进程传播控制台
201
+ * 渲染决策用;toml 不暴露此键)
202
+ */
203
+ /** pino 标准级别 + silent。 */
204
+ const LOG_LEVEL_VALUES = [
205
+ "trace",
206
+ "debug",
207
+ "info",
208
+ "warn",
209
+ "error",
210
+ "fatal",
211
+ "silent"
212
+ ];
213
+ const LOG_FORMAT_VALUES = [
214
+ "pretty",
215
+ "single-line",
216
+ "json"
217
+ ];
218
+ const TIMESTAMP_MODE_VALUES = [
219
+ "local",
220
+ "iso8601",
221
+ "elapsed"
222
+ ];
223
+ const MODULE_NAME_VALUES = [
224
+ "cli",
225
+ "kernel",
226
+ "loader",
227
+ "ob11"
228
+ ];
229
+ /** 校验级别字面量;非法值抛 INVALID_PARAM。 */
230
+ function requireLevel(value, key) {
231
+ if (typeof value !== "string" || !LOG_LEVEL_VALUES.includes(value)) throw kernelError(`主配置 ${key} 必须是日志级别(${LOG_LEVEL_VALUES.join(" / ")})`, "INVALID_PARAM");
232
+ return value;
233
+ }
234
+ /** 宽松级别解析(env 覆盖用;非法/缺失返回 undefined 不生效)。 */
235
+ function optionalLevel(value) {
236
+ if (value === void 0) return;
237
+ return LOG_LEVEL_VALUES.includes(value) ? value : void 0;
238
+ }
239
+ /** 宽松格式解析(env 覆盖用)。 */
240
+ function optionalFormat(value) {
241
+ if (value === void 0) return;
242
+ return LOG_FORMAT_VALUES.includes(value) ? value : void 0;
243
+ }
244
+ /** 宽松布尔解析(env 用:仅识别 1/true/0/false,其余忽略)。 */
245
+ function optionalBool(value) {
246
+ if (value === "1" || value === "true") return true;
247
+ if (value === "0" || value === "false") return false;
248
+ }
249
+ /** 解析 [log.levels] 按模块覆盖表(未知模块名报错,键集封闭防拼写错误)。 */
250
+ function parseModuleLevels(raw) {
251
+ if (raw === void 0) return {};
252
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.levels 必须是对象", "INVALID_PARAM");
253
+ const out = {};
254
+ for (const [key, value] of Object.entries(raw)) {
255
+ if (!MODULE_NAME_VALUES.includes(key)) throw kernelError(`主配置 log.levels 未知模块 "${key}"(合法模块:${MODULE_NAME_VALUES.join(" / ")})`, "INVALID_PARAM");
256
+ out[key] = requireLevel(value, `log.levels.${key}`);
257
+ }
258
+ return out;
259
+ }
260
+ /** 解析 [log.file] 独立文件 sink。 */
261
+ function parseLogFile(raw, fallbackLevel) {
262
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.file 必须是对象", "INVALID_PARAM");
263
+ const record = raw;
264
+ const path = record["path"];
265
+ if (typeof path !== "string" || path === "") throw kernelError("主配置 log.file.path 必须是非空字符串", "INVALID_PARAM");
266
+ return {
267
+ path,
268
+ level: record["level"] === void 0 ? fallbackLevel : requireLevel(record["level"], "log.file.level")
269
+ };
270
+ }
271
+ /** 校验字面量枚举值(非法抛 INVALID_PARAM,报错文案带键名与合法值集)。 */
272
+ function requireEnum(value, key, values) {
273
+ if (typeof value !== "string" || !values.includes(value)) throw kernelError(`主配置 ${key} 必须是(${values.join(" / ")})`, "INVALID_PARAM");
274
+ return value;
275
+ }
276
+ /** 校验布尔字段(undefined 保留原值)。 */
277
+ function applyBoolean(draft, raw, key) {
278
+ if (raw[key] === void 0) return;
279
+ if (typeof raw[key] !== "boolean") throw kernelError(`主配置 log.${key} 必须是布尔值`, "INVALID_PARAM");
280
+ draft[key] = raw[key];
281
+ }
282
+ /** toml [log] 段字段填充(level/format/timestamp/banner/unboundReceive)。 */
283
+ function applySectionFields(draft, raw) {
284
+ if (raw["level"] !== void 0) draft.level = requireLevel(raw["level"], "log.level");
285
+ if (raw["format"] !== void 0) draft.format = requireEnum(raw["format"], "log.format", LOG_FORMAT_VALUES);
286
+ if (raw["timestamp"] !== void 0) draft.timestamp = requireEnum(raw["timestamp"], "log.timestamp", TIMESTAMP_MODE_VALUES);
287
+ applyBoolean(draft, raw, "banner");
288
+ applyBoolean(draft, raw, "unboundReceive");
289
+ }
290
+ /** env 覆盖(优先级高于 toml;非法值忽略):兼容旧拼写 NAPKETTO_LOG_LEVEL。
291
+ * file.level 显式配置时不被 env 全局级别顶掉(缺省跟随全局)。 */
292
+ function applyEnvOverrides(draft, env) {
293
+ const envLevel = optionalLevel(env["NAPUKETTO_LOG_LEVEL"]) ?? optionalLevel(env["NAPKETTO_LOG_LEVEL"]);
294
+ if (envLevel !== void 0) {
295
+ draft.level = envLevel;
296
+ if (draft.file !== void 0 && draft.fileLevelExplicit !== true) draft.file = {
297
+ ...draft.file,
298
+ level: envLevel
299
+ };
300
+ }
301
+ const envFormat = optionalFormat(env["NAPUKETTO_LOG_FORMAT"]);
302
+ if (envFormat !== void 0) draft.format = envFormat;
303
+ if (optionalBool(env["NAPUKETTO_NO_BANNER"]) === true) draft.banner = false;
304
+ const envColorize = optionalBool(env["NAPUKETTO_LOG_COLORIZE"]);
305
+ if (envColorize !== void 0) draft.colorize = envColorize;
306
+ }
307
+ /**
308
+ * 解析 [log] 段 + 环境变量覆盖。
309
+ *
310
+ * @param section TOML 解析后的 [log] 段(缺失/undefined = 全缺省)
311
+ * @param env 环境变量快照(默认空对象 = 无 env 覆盖)
312
+ */
313
+ function parseLogConfig(section, env = {}) {
314
+ const draft = {
315
+ level: "info",
316
+ banner: true,
317
+ timestamp: "local",
318
+ unboundReceive: true,
319
+ levels: {}
320
+ };
321
+ if (section !== void 0) {
322
+ if (typeof section !== "object" || section === null) throw kernelError("主配置 log 段必须是对象", "INVALID_PARAM");
323
+ const raw = section;
324
+ applySectionFields(draft, raw);
325
+ draft.levels = parseModuleLevels(raw["levels"]);
326
+ if (raw["file"] !== void 0) {
327
+ draft.file = parseLogFile(raw["file"], draft.level);
328
+ draft.fileLevelExplicit = typeof raw["file"] === "object" && raw["file"] !== null && raw["file"]["level"] !== void 0;
329
+ }
330
+ }
331
+ applyEnvOverrides(draft, env);
332
+ delete draft.fileLevelExplicit;
333
+ return draft;
334
+ }
335
+ //#endregion
336
+ //#region src/infra/log-format.ts
337
+ /**
338
+ * 结构化控制台流(single-line / json 格式,2026-10-25 日志可配置化)。
339
+ *
340
+ * pino-pretty 面向交互终端(多行展开 / JSON 内联渲染),其 singleLine 输出为
341
+ * `msg {"k":"v"}` 形态且 messageFormat 会与字段追加双重渲染——无法干净满足
342
+ * 嵌入式宿主的「一条记录一行、字段 k=v / NDJSON」契约,故自写轻量渲染流:
343
+ *
344
+ * single-line:[时间] LEVEL (name/pid): 消息 key=value key=value
345
+ * json:NDJSON,每行 {time, level, module, account?, pid, msg, ...fields}
346
+ *
347
+ * 时间渲染(LogConfig.timestamp):local = [HH:MM:ss.l](与 pretty 缺省一致)、
348
+ * iso8601 = 2026-10-25T16:01:33.294+08:00、elapsed = +123.456s(进程内单调秒)。
349
+ * json 的 time 恒为 ISO8601(机器可解析契约,不受 timestamp 键影响)。
350
+ */
351
+ /** 级别数值 → 名称(pino 标准映射)。 */
352
+ const LEVEL_LABELS = {
353
+ 10: "trace",
354
+ 20: "debug",
355
+ 30: "info",
356
+ 40: "warn",
357
+ 50: "error",
358
+ 60: "fatal"
359
+ };
360
+ /** single-line 颜色(colorize 时仅着色级别与元数据头,正文默认色)。 */
361
+ const LEVEL_COLORS = {
362
+ trace: "\x1B[34m",
363
+ debug: "\x1B[36m",
364
+ info: "\x1B[32m",
365
+ warn: "\x1B[33m",
366
+ error: "\x1B[31m",
367
+ fatal: "\x1B[91m"
368
+ };
369
+ const DIM = "\x1B[2m";
370
+ const RESET = "\x1B[0m";
371
+ /** 2 位补零。 */
372
+ function pad2(n) {
373
+ return String(n).padStart(2, "0");
374
+ }
375
+ /** 单调墙钟毫秒 → 本地时区 ISO8601(+08:00 形态;new Date(epoch) 构造不受 Date.now 篡改影响)。 */
376
+ function isoLocalTime(epochMs) {
377
+ const d = new Date(epochMs);
378
+ const offsetMinutes = -d.getTimezoneOffset();
379
+ const sign = offsetMinutes >= 0 ? "+" : "-";
380
+ const abs = Math.abs(offsetMinutes);
381
+ 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)}`;
382
+ }
383
+ /** 单调墙钟毫秒 → 本地时分秒(与 pino-pretty SYS:HH:MM:ss.l 一致的短格式)。 */
384
+ function localShortTime(epochMs) {
385
+ const d = new Date(epochMs);
386
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`;
387
+ }
388
+ /** 单调墙钟毫秒 → 进程内相对秒(+123.456s)。 */
389
+ function elapsedLabel(epochMs, timeOriginMs) {
390
+ return `+${((epochMs - timeOriginMs) / 1e3).toFixed(3)}s`;
391
+ }
392
+ /** 渲染记录时间(timestamp 模式分发;json 恒 ISO8601)。 */
393
+ function renderTime(rawTime, mode, forceIso, timeOriginMs) {
394
+ if (typeof rawTime === "string") return rawTime;
395
+ const epoch = typeof rawTime === "number" ? rawTime : timeOriginMs;
396
+ if (forceIso || mode === "iso8601") return isoLocalTime(epoch);
397
+ if (mode === "elapsed") return elapsedLabel(epoch, timeOriginMs);
398
+ return localShortTime(epoch);
399
+ }
400
+ /** 级别数值 → 标签名。 */
401
+ function levelLabel(raw) {
402
+ if (typeof raw === "number") return LEVEL_LABELS[raw] ?? String(raw);
403
+ return typeof raw === "string" ? raw.toUpperCase() : "LOG";
404
+ }
405
+ /** k=v 内联值:字符串裸输出(含空格时不加引号——宽松人类格式),其余 JSON 序列化。 */
406
+ function inlineValue(value) {
407
+ if (typeof value === "string") return value;
408
+ return JSON.stringify(value) ?? "null";
409
+ }
410
+ /** 结构化输出忽略的标准字段(渲染进头/独立字段,不进 k=v 尾部)。 */
411
+ const RESERVED_KEYS = /* @__PURE__ */ new Set([
412
+ "pid",
413
+ "hostname",
414
+ "level",
415
+ "time",
416
+ "msg",
417
+ "name"
418
+ ]);
419
+ /** json 渲染:NDJSON 契约 {time, level, module, account?, pid, msg, ...fields}。 */
420
+ function renderJsonLine(record, ctx) {
421
+ const { time, level, name, module, account, pid, msg: _msg, ...fields } = record;
422
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
423
+ const jsonLine = {
424
+ time: renderTime(time, ctx.timestamp, true, ctx.timeOriginMs),
425
+ level: levelLabel(level).toLowerCase(),
426
+ ...module !== void 0 ? { module } : typeof name === "string" ? { module: name } : {},
427
+ ...account !== void 0 ? { account } : {},
428
+ ...typeof pid === "number" ? { pid } : {},
429
+ msg,
430
+ ...fields
431
+ };
432
+ return `${JSON.stringify(jsonLine)}\n`;
433
+ }
434
+ /** single-line 元数据头:(name/pid),无 name 时 (pid)。 */
435
+ function renderMeta(record) {
436
+ const pid = record["pid"] ?? "-";
437
+ const name = record["name"];
438
+ return typeof name === "string" ? `(${name}/${pid})` : `(${pid})`;
439
+ }
440
+ /** single-line 附加字段:非保留键按 k=v 内联(无字段时空串)。 */
441
+ function renderKvFields(record) {
442
+ const fields = [];
443
+ for (const [key, value] of Object.entries(record)) if (!RESERVED_KEYS.has(key)) fields.push(`${key}=${inlineValue(value)}`);
444
+ return fields.length > 0 ? ` ${fields.join(" ")}` : "";
445
+ }
446
+ /** single-line 渲染:[时间] LEVEL (name/pid): 消息 k=v k=v(级别大写与 pretty 一致)。 */
447
+ function renderSingleLine(record, ctx) {
448
+ const label = levelLabel(record["level"]);
449
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
450
+ const time = renderTime(record["time"], ctx.timestamp, false, ctx.timeOriginMs);
451
+ const labelUpper = label.toUpperCase();
452
+ const meta = renderMeta(record);
453
+ const fieldsSuffix = renderKvFields(record);
454
+ if (ctx.useColor) {
455
+ const color = LEVEL_COLORS[label.toLowerCase()] ?? "";
456
+ return `${DIM}[${time}]${RESET} ${color}${labelUpper}${RESET} ${DIM}${meta}:${RESET} ${msg}${fieldsSuffix}\n`;
457
+ }
458
+ return `[${time}] ${labelUpper} ${meta}: ${msg}${fieldsSuffix}\n`;
459
+ }
460
+ /** 创建结构化控制台流(single-line / json)。 */
461
+ function createConsoleFormatStream(opts) {
462
+ const out = opts.destination ?? node_process.default.stdout;
463
+ const ctx = {
464
+ timestamp: opts.timestamp,
465
+ timeOriginMs: performance.timeOrigin,
466
+ useColor: opts.format === "single-line" && opts.colorize
467
+ };
468
+ const render = opts.format === "json" ? renderJsonLine : renderSingleLine;
469
+ return { write(line) {
470
+ let record;
471
+ try {
472
+ record = JSON.parse(line);
473
+ } catch {
474
+ out.write(`${line}\n`);
475
+ return;
476
+ }
477
+ out.write(render(record, ctx));
478
+ } };
479
+ }
480
+ //#endregion
188
481
  //#region src/infra/logger.ts
189
482
  /**
190
483
  * pino 日志封装(ADR-007)
191
484
  *
192
- * - console(pino-pretty 可读输出)+ 可选文件双目的地
193
- * - 统一 level 控制
485
+ * - console(pretty 可读输出 / single-line / json 结构化)+ 可选文件双目的地
486
+ * - 统一 level 控制(控制台与文件可分别设级,2026-10-25 日志可配置化)
194
487
  * - 内置 redact(token / 票据等敏感字段不打日志),可追加
195
488
  * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
489
+ *
490
+ * 时间源(2026-10-25 修复):QQNT 原生层会在 onRecvSysMsg 回调窗口内篡改进程
491
+ * Date(实测记录时间冻结为假值、回调外恢复),故时间戳统一取
492
+ * performance.timeOrigin + performance.now() 单调派生墙钟,不受 Date 篡改影响。
196
493
  */
197
494
  /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
198
495
  const DEFAULT_REDACT_PATHS = [
@@ -202,39 +499,91 @@ const DEFAULT_REDACT_PATHS = [
202
499
  "password",
203
500
  "secret"
204
501
  ];
502
+ /** 级别数值序(求多目的地最低级别用)。 */
503
+ const LEVEL_ORDER = [
504
+ "trace",
505
+ "debug",
506
+ "info",
507
+ "warn",
508
+ "error",
509
+ "fatal",
510
+ "silent"
511
+ ];
512
+ /** 取两级别中更低(更宽容)的一个。 */
513
+ function minLevel(a, b) {
514
+ return LEVEL_ORDER.indexOf(a) <= LEVEL_ORDER.indexOf(b) ? a : b;
515
+ }
516
+ /** 单调墙钟毫秒(timeOrigin 为进程启动真实 Unix 时间,不受进程内 Date 篡改影响)。 */
517
+ function wallMs() {
518
+ return performance.timeOrigin + performance.now();
519
+ }
520
+ /** 进程内相对秒标签(elapsed 模式,pretty 字符串时间路径用)。 */
521
+ function elapsedLabelNow() {
522
+ return `+${(performance.now() / 1e3).toFixed(3)}s`;
523
+ }
205
524
  function toSingle(streams) {
206
525
  const [only] = streams;
207
526
  if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
208
527
  return only.stream;
209
528
  }
210
- /**
211
- * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
212
- *
213
- * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
214
- * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
215
- */
529
+ /** 构造控制台流:pretty 走 pino-pretty,single-line/json 走结构化渲染流。 */
530
+ function buildConsoleStream(params) {
531
+ const { format, timestampMode, colorize, destination } = params;
532
+ if (format !== "pretty") return createConsoleFormatStream({
533
+ format,
534
+ timestamp: timestampMode,
535
+ colorize,
536
+ destination
537
+ });
538
+ return (0, pino_pretty.default)({
539
+ colorize,
540
+ ...timestampMode === "local" ? { translateTime: "SYS:HH:MM:ss.l" } : {},
541
+ ...timestampMode === "iso8601" ? { translateTime: "SYS:yyyy-mm-dd'T'HH:MM:ss.l Z" } : {},
542
+ singleLine: false,
543
+ destination
544
+ });
545
+ }
546
+ /** 折叠 LoggerOptions 的渲染缺省:格式按 TTY 自动(TTY pretty / pipe single-line)、
547
+ * 颜色仅在 pretty 且 TTY 时开。 */
548
+ function resolveRenderParams(opts) {
549
+ const destination = opts.destination ?? node_process.default.stdout;
550
+ const isTty = destination.isTTY === true;
551
+ const format = opts.format ?? (isTty ? "pretty" : "single-line");
552
+ return {
553
+ format,
554
+ timestampMode: opts.timestamp ?? "local",
555
+ colorize: opts.colorize ?? (format === "pretty" && isTty),
556
+ destination
557
+ };
558
+ }
216
559
  function createLogger(opts = {}) {
217
- const level = opts.level ?? "info";
218
- const consoleEnabled = opts.console ?? true;
560
+ const consoleLevel = opts.level ?? "info";
561
+ const fileLevel = opts.fileLevel ?? consoleLevel;
219
562
  const { file, base } = opts;
563
+ const render = resolveRenderParams(opts);
220
564
  const streams = [];
221
- if (consoleEnabled) streams.push({ stream: (0, pino_pretty.default)({
222
- colorize: true,
223
- translateTime: "SYS:HH:MM:ss.l",
224
- singleLine: false,
225
- destination: node_process.default.stdout
226
- }) });
565
+ if ((opts.console ?? true) === true) streams.push({
566
+ stream: buildConsoleStream(render),
567
+ level: consoleLevel
568
+ });
227
569
  if (file) {
228
570
  (0, node_fs.mkdirSync)((0, node_path.dirname)(file), { recursive: true });
229
- streams.push({ stream: pino.default.destination({
230
- dest: file,
231
- sync: true
232
- }) });
571
+ streams.push({
572
+ stream: pino.default.destination({
573
+ dest: file,
574
+ sync: true
575
+ }),
576
+ level: fileLevel
577
+ });
233
578
  }
234
579
  if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
580
+ let gate = "fatal";
581
+ for (const entry of streams) gate = minLevel(gate, entry.level);
582
+ const pinoTimestamp = render.format === "pretty" && render.timestampMode === "elapsed" ? () => `,"time":"${elapsedLabelNow()}"` : () => `,"time":${wallMs()}`;
235
583
  const pinoOptions = {
236
- level,
237
- redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
584
+ level: gate,
585
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []],
586
+ timestamp: pinoTimestamp
238
587
  };
239
588
  if (base) pinoOptions.base = {
240
589
  pid: node_process.default.pid,
@@ -801,6 +1150,24 @@ function replyToCanonical(reply) {
801
1150
  messageId: reply.replayMsgId
802
1151
  };
803
1152
  }
1153
+ /**
1154
+ * ARK 元素 → canonical json(小程序/分享卡片,2026-10-25)。
1155
+ *
1156
+ * bytesData 是卡片 JSON 字符串,原样透传不解析重组——OB11 json 段 data.data
1157
+ * 保持原始载荷(go-cqhttp/NapCat 生态一致),title/url 由下游自行 parse。
1158
+ * bytesData 缺失/为空时维持 unknown 兜底:空 json 段无信息量,保留 raw 供探测。
1159
+ */
1160
+ function arkToCanonical(ark, el) {
1161
+ const bytes = ark?.bytesData;
1162
+ if (bytes === void 0 || bytes === "") return {
1163
+ type: "unknown",
1164
+ raw: el
1165
+ };
1166
+ return {
1167
+ type: "json",
1168
+ raw: bytes
1169
+ };
1170
+ }
804
1171
  /** 单个 NT 元素 → canonical(接收方向,宽容:无法表达回 unknown,不抛错)。 */
805
1172
  function toCanonicalElement(el) {
806
1173
  switch (el.elementType) {
@@ -811,6 +1178,7 @@ function toCanonicalElement(el) {
811
1178
  case ElementType.VIDEO: return videoToCanonical(el.videoElement);
812
1179
  case ElementType.FILE: return fileToCanonical(el.fileElement);
813
1180
  case ElementType.REPLY: return replyToCanonical(el.replyElement);
1181
+ case ElementType.ARK: return arkToCanonical(el.arkElement, el);
814
1182
  default: return {
815
1183
  type: "unknown",
816
1184
  raw: el
@@ -3752,7 +4120,14 @@ var NapukettoCore = class NapukettoCore {
3752
4120
  level: opts.logLevel ?? "info",
3753
4121
  console: opts.consoleLog ?? true,
3754
4122
  file: opts.logFile ?? paths.file("logs", "napuketto.log"),
3755
- base: { name: "kernel" }
4123
+ fileLevel: opts.logFileLevel,
4124
+ format: opts.logFormat,
4125
+ timestamp: opts.logTimestamp,
4126
+ colorize: opts.logColorize,
4127
+ base: {
4128
+ name: "kernel",
4129
+ account: opts.account
4130
+ }
3756
4131
  });
3757
4132
  return new NapukettoCore(createCoreContext({
3758
4133
  logger,
@@ -3797,22 +4172,23 @@ var NapukettoCore = class NapukettoCore {
3797
4172
  this.ctx.login = loginResult;
3798
4173
  return loginResult;
3799
4174
  }
3800
- let loginResult;
4175
+ const loginResult = await this.loginQuickFirst(wrapper, opts);
4176
+ this.ctx.login = loginResult;
4177
+ return loginResult;
4178
+ }
4179
+ /** 快速登录优先,失败按 qrFallback 决定是否回退 QR(QR 应直接出码,见 loginByQr 注)。 */
4180
+ async loginQuickFirst(wrapper, opts) {
3801
4181
  try {
3802
- const quickOpts = {};
3803
- if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
3804
- if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
3805
- loginResult = await quickLogin(wrapper, quickOpts);
4182
+ const result = await quickLogin(wrapper, buildQuickLoginOptions(opts));
3806
4183
  this.ctx.logger.info({
3807
- uin: loginResult.uin,
3808
- uid: loginResult.uid
4184
+ uin: result.uin,
4185
+ uid: result.uid
3809
4186
  }, "快速登录成功");
4187
+ return result;
3810
4188
  } catch (err) {
3811
4189
  if (opts.qrFallback !== true) throw err;
3812
- loginResult = await this.loginByQr(opts);
4190
+ return await this.loginByQr(opts);
3813
4191
  }
3814
- this.ctx.login = loginResult;
3815
- return loginResult;
3816
4192
  }
3817
4193
  /**
3818
4194
  * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
@@ -3861,8 +4237,7 @@ var NapukettoCore = class NapukettoCore {
3861
4237
  try {
3862
4238
  session.start();
3863
4239
  await waitQrLoggedIn(session);
3864
- const self = session.selfInfo;
3865
- if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4240
+ const self = requireQrSelfInfo(session);
3866
4241
  return {
3867
4242
  uin: self.uin,
3868
4243
  uid: self.uid,
@@ -3884,14 +4259,14 @@ var NapukettoCore = class NapukettoCore {
3884
4259
  });
3885
4260
  });
3886
4261
  session.onStateChange((state) => {
3887
- this.ctx.logger.info({ state }, "QR 登录状态");
3888
- const message = state === "failed" ? session.failureReason ?? void 0 : void 0;
3889
- opts.onLoginProgress?.({
3890
- state,
3891
- ...message !== void 0 ? { message } : {}
3892
- });
4262
+ this.forwardQrState(session, opts, state);
3893
4263
  });
3894
4264
  }
4265
+ /** QR 状态转发:日志 + onLoginProgress(failed 附 failureReason)。 */
4266
+ forwardQrState(session, opts, state) {
4267
+ this.ctx.logger.info({ state }, "QR 登录状态");
4268
+ opts.onLoginProgress?.(qrProgressEvent(state, session.failureReason ?? void 0));
4269
+ }
3895
4270
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
3896
4271
  stop() {
3897
4272
  this.ctx.logger.info("NapukettoCore 停止");
@@ -3904,6 +4279,27 @@ function writeQrCodePng(filePath, pngBase64) {
3904
4279
  (0, node_fs.writeFileSync)(filePath, Buffer.from(pngBase64, "base64"));
3905
4280
  } catch {}
3906
4281
  }
4282
+ /** CoreLoginOptions → quickLogin 参数(仅取快速登录相关两键,QR 键不透传)。 */
4283
+ function buildQuickLoginOptions(opts) {
4284
+ const quickOpts = {};
4285
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
4286
+ if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
4287
+ return quickOpts;
4288
+ }
4289
+ /** QR 登录终态后的 selfInfo 提取(空值按登录失败抛出)。 */
4290
+ function requireQrSelfInfo(session) {
4291
+ const self = session.selfInfo;
4292
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4293
+ return self;
4294
+ }
4295
+ /** QR 进度事件折叠(failed 附 failureReason 为 message,其余不带 message 键)。 */
4296
+ function qrProgressEvent(state, reason) {
4297
+ const message = state === "failed" ? reason : void 0;
4298
+ return {
4299
+ state,
4300
+ ...message !== void 0 ? { message } : {}
4301
+ };
4302
+ }
3907
4303
  /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3908
4304
  function waitQrLoggedIn(session) {
3909
4305
  return new Promise((resolve, reject) => {
@@ -4560,6 +4956,7 @@ exports.listLoginAccounts = listLoginAccounts;
4560
4956
  exports.listQQVersions = listQQVersions;
4561
4957
  exports.narrowBuddyCategories = narrowBuddyCategories;
4562
4958
  exports.parseAppidFromMajor = parseAppidFromMajor;
4959
+ exports.parseLogConfig = parseLogConfig;
4563
4960
  exports.parseToml = parseToml;
4564
4961
  exports.probeRuntime = probeRuntime;
4565
4962
  exports.quickLogin = quickLogin;