@napuketto/kernel 0.2.5 → 0.4.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
@@ -68,6 +68,25 @@ function kernelError(message, code = "UNKNOWN", options) {
68
68
  function isKernelError(err) {
69
69
  return err instanceof KernelError;
70
70
  }
71
+ /**
72
+ * 配置校验失败(INVALID_PARAM 子型,2026-10-25 嵌入式宿主契约):仅用于
73
+ * 「配置未就绪」一族——主配置校验/反序列化、内联配置、协议段 seed 校验。
74
+ * code 恒为 INVALID_PARAM,协议层错误码映射零涟漪;退出码分类走 instanceof
75
+ * (CLI/self-host 进程以 EXIT_CONFIG_INVALID 退出),与错误文案完全解耦——
76
+ * 文案是人读的,退出码是宿主消费的机器契约。
77
+ */
78
+ var ConfigError = class extends KernelError {
79
+ constructor(message, options) {
80
+ super(message, "INVALID_PARAM", options);
81
+ this.name = "ConfigError";
82
+ }
83
+ };
84
+ /** 便捷构造:`configError('主配置缺少 accounts', { cause })`。 */
85
+ function configError(message, options) {
86
+ return new ConfigError(message, options);
87
+ }
88
+ /** 配置校验失败的进程退出码(UNIX sysexits EX_CONFIG 惯例)。 */
89
+ const EXIT_CONFIG_INVALID = 78;
71
90
  //#endregion
72
91
  //#region src/infra/config.ts
73
92
  /**
@@ -133,14 +152,18 @@ var ConfigBase = class {
133
152
  await this.save(this.defaults);
134
153
  return this.value;
135
154
  }
136
- throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
155
+ throw new ConfigError(`读取配置文件失败: ${this.path}`, { cause: err });
137
156
  }
138
157
  this.value = this.parse(raw);
139
158
  return this.value;
140
159
  }
141
- /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
142
- reload() {
143
- return this.load();
160
+ /**
161
+ * 校验纯文本配置(反序列化 + schema 校验,不读写文件;`config validate`
162
+ * 等只读场景用)。错误包装与 load()/save() 完全一致(ConfigError,
163
+ * 文案含 this.path),保证 validate 与运行时校验逐字一致。
164
+ */
165
+ parseText(raw) {
166
+ return this.parse(raw);
144
167
  }
145
168
  /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
146
169
  async save(next) {
@@ -161,18 +184,18 @@ var ConfigBase = class {
161
184
  if (this.format === "toml") return (0, smol_toml.parse)(raw);
162
185
  return JSON.parse(raw);
163
186
  }
164
- /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
187
+ /** 反序列化 + schema 校验,错误统一包装为 ConfigError(配置未就绪一族)。 */
165
188
  parse(raw) {
166
189
  let input;
167
190
  try {
168
191
  input = this.deserialize(raw);
169
192
  } catch (err) {
170
- throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
193
+ throw new ConfigError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, { cause: err });
171
194
  }
172
195
  try {
173
196
  return this.schema.parse(input);
174
197
  } catch (err) {
175
- throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
198
+ throw new ConfigError(`配置文件校验失败: ${this.path}`, { cause: err });
176
199
  }
177
200
  }
178
201
  };
@@ -185,14 +208,311 @@ function stringifyToml(value) {
185
208
  return (0, smol_toml.stringify)(value);
186
209
  }
187
210
  //#endregion
211
+ //#region src/infra/log-config.ts
212
+ /**
213
+ * [log] 配置段解析(napuketto.toml 全局段,2026-10-25 日志可配置化)。
214
+ *
215
+ * 统一解析入口:cli(apps/cli)与 loader 引导链共用本函数(经 kernel 导出),
216
+ * 避免两处手写解析漂移。输入为 TOML 解析后的 [log] 段原对象(缺失传 undefined)
217
+ * + 环境变量快照;输出为已解析的 LogConfig(env 覆盖优先于 toml)。
218
+ *
219
+ * 环境变量(优先级高于 toml;非法值忽略——与既有 NAPKETTO_LOG_LEVEL 行为一致):
220
+ * - NAPUKETTO_LOG_LEVEL / NAPKETTO_LOG_LEVEL(兼容旧拼写,NAPUKETTO_ 优先)
221
+ * - NAPUKETTO_LOG_FORMAT(pretty | single-line | json)
222
+ * - NAPUKETTO_NO_BANNER(1/true = 关 banner)
223
+ * - NAPUKETTO_LOG_COLORIZE(1/true/0/false,CLI 顶层进程向子进程传播控制台
224
+ * 渲染决策用;toml 不暴露此键)
225
+ */
226
+ /** pino 标准级别 + silent。 */
227
+ const LOG_LEVEL_VALUES = [
228
+ "trace",
229
+ "debug",
230
+ "info",
231
+ "warn",
232
+ "error",
233
+ "fatal",
234
+ "silent"
235
+ ];
236
+ const LOG_FORMAT_VALUES = [
237
+ "pretty",
238
+ "single-line",
239
+ "json"
240
+ ];
241
+ const TIMESTAMP_MODE_VALUES = [
242
+ "local",
243
+ "iso8601",
244
+ "elapsed"
245
+ ];
246
+ const MODULE_NAME_VALUES = [
247
+ "cli",
248
+ "kernel",
249
+ "loader",
250
+ "ob11"
251
+ ];
252
+ /** 校验级别字面量;非法值抛 INVALID_PARAM。 */
253
+ function requireLevel(value, key) {
254
+ if (typeof value !== "string" || !LOG_LEVEL_VALUES.includes(value)) throw kernelError(`主配置 ${key} 必须是日志级别(${LOG_LEVEL_VALUES.join(" / ")})`, "INVALID_PARAM");
255
+ return value;
256
+ }
257
+ /** 宽松级别解析(env 覆盖用;非法/缺失返回 undefined 不生效)。 */
258
+ function optionalLevel(value) {
259
+ if (value === void 0) return;
260
+ return LOG_LEVEL_VALUES.includes(value) ? value : void 0;
261
+ }
262
+ /** 宽松格式解析(env 覆盖用)。 */
263
+ function optionalFormat(value) {
264
+ if (value === void 0) return;
265
+ return LOG_FORMAT_VALUES.includes(value) ? value : void 0;
266
+ }
267
+ /** 宽松布尔解析(env 用:仅识别 1/true/0/false,其余忽略)。 */
268
+ function optionalBool(value) {
269
+ if (value === "1" || value === "true") return true;
270
+ if (value === "0" || value === "false") return false;
271
+ }
272
+ /** 解析 [log.levels] 按模块覆盖表(未知模块名报错,键集封闭防拼写错误)。 */
273
+ function parseModuleLevels(raw) {
274
+ if (raw === void 0) return {};
275
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.levels 必须是对象", "INVALID_PARAM");
276
+ const out = {};
277
+ for (const [key, value] of Object.entries(raw)) {
278
+ if (!MODULE_NAME_VALUES.includes(key)) throw kernelError(`主配置 log.levels 未知模块 "${key}"(合法模块:${MODULE_NAME_VALUES.join(" / ")})`, "INVALID_PARAM");
279
+ out[key] = requireLevel(value, `log.levels.${key}`);
280
+ }
281
+ return out;
282
+ }
283
+ /** 解析 [log.file] 独立文件 sink。 */
284
+ function parseLogFile(raw, fallbackLevel) {
285
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.file 必须是对象", "INVALID_PARAM");
286
+ const record = raw;
287
+ const path = record["path"];
288
+ if (typeof path !== "string" || path === "") throw kernelError("主配置 log.file.path 必须是非空字符串", "INVALID_PARAM");
289
+ return {
290
+ path,
291
+ level: record["level"] === void 0 ? fallbackLevel : requireLevel(record["level"], "log.file.level")
292
+ };
293
+ }
294
+ /** 校验字面量枚举值(非法抛 INVALID_PARAM,报错文案带键名与合法值集)。 */
295
+ function requireEnum(value, key, values) {
296
+ if (typeof value !== "string" || !values.includes(value)) throw kernelError(`主配置 ${key} 必须是(${values.join(" / ")})`, "INVALID_PARAM");
297
+ return value;
298
+ }
299
+ /** 校验布尔字段(undefined 保留原值)。 */
300
+ function applyBoolean(draft, raw, key) {
301
+ if (raw[key] === void 0) return;
302
+ if (typeof raw[key] !== "boolean") throw kernelError(`主配置 log.${key} 必须是布尔值`, "INVALID_PARAM");
303
+ draft[key] = raw[key];
304
+ }
305
+ /** toml [log] 段字段填充(level/format/timestamp/banner/unboundReceive)。 */
306
+ function applySectionFields(draft, raw) {
307
+ if (raw["level"] !== void 0) draft.level = requireLevel(raw["level"], "log.level");
308
+ if (raw["format"] !== void 0) draft.format = requireEnum(raw["format"], "log.format", LOG_FORMAT_VALUES);
309
+ if (raw["timestamp"] !== void 0) draft.timestamp = requireEnum(raw["timestamp"], "log.timestamp", TIMESTAMP_MODE_VALUES);
310
+ applyBoolean(draft, raw, "banner");
311
+ applyBoolean(draft, raw, "unboundReceive");
312
+ }
313
+ /** env 覆盖(优先级高于 toml;非法值忽略):兼容旧拼写 NAPKETTO_LOG_LEVEL。
314
+ * file.level 显式配置时不被 env 全局级别顶掉(缺省跟随全局)。 */
315
+ function applyEnvOverrides(draft, env) {
316
+ const envLevel = optionalLevel(env["NAPUKETTO_LOG_LEVEL"]) ?? optionalLevel(env["NAPKETTO_LOG_LEVEL"]);
317
+ if (envLevel !== void 0) {
318
+ draft.level = envLevel;
319
+ if (draft.file !== void 0 && draft.fileLevelExplicit !== true) draft.file = {
320
+ ...draft.file,
321
+ level: envLevel
322
+ };
323
+ }
324
+ const envFormat = optionalFormat(env["NAPUKETTO_LOG_FORMAT"]);
325
+ if (envFormat !== void 0) draft.format = envFormat;
326
+ if (optionalBool(env["NAPUKETTO_NO_BANNER"]) === true) draft.banner = false;
327
+ const envColorize = optionalBool(env["NAPUKETTO_LOG_COLORIZE"]);
328
+ if (envColorize !== void 0) draft.colorize = envColorize;
329
+ }
330
+ /**
331
+ * 解析 [log] 段 + 环境变量覆盖。
332
+ *
333
+ * @param section TOML 解析后的 [log] 段(缺失/undefined = 全缺省)
334
+ * @param env 环境变量快照(默认空对象 = 无 env 覆盖)
335
+ */
336
+ function parseLogConfig(section, env = {}) {
337
+ const draft = {
338
+ level: "info",
339
+ banner: true,
340
+ timestamp: "local",
341
+ unboundReceive: true,
342
+ levels: {}
343
+ };
344
+ if (section !== void 0) {
345
+ if (typeof section !== "object" || section === null) throw kernelError("主配置 log 段必须是对象", "INVALID_PARAM");
346
+ const raw = section;
347
+ applySectionFields(draft, raw);
348
+ draft.levels = parseModuleLevels(raw["levels"]);
349
+ if (raw["file"] !== void 0) {
350
+ draft.file = parseLogFile(raw["file"], draft.level);
351
+ draft.fileLevelExplicit = typeof raw["file"] === "object" && raw["file"] !== null && raw["file"]["level"] !== void 0;
352
+ }
353
+ }
354
+ applyEnvOverrides(draft, env);
355
+ delete draft.fileLevelExplicit;
356
+ return draft;
357
+ }
358
+ //#endregion
359
+ //#region src/infra/log-format.ts
360
+ /**
361
+ * 结构化控制台流(single-line / json 格式,2026-10-25 日志可配置化)。
362
+ *
363
+ * pino-pretty 面向交互终端(多行展开 / JSON 内联渲染),其 singleLine 输出为
364
+ * `msg {"k":"v"}` 形态且 messageFormat 会与字段追加双重渲染——无法干净满足
365
+ * 嵌入式宿主的「一条记录一行、字段 k=v / NDJSON」契约,故自写轻量渲染流:
366
+ *
367
+ * single-line:[时间] LEVEL (name/pid): 消息 key=value key=value
368
+ * json:NDJSON,每行 {time, level, module, account?, pid, msg, ...fields}
369
+ *
370
+ * 时间渲染(LogConfig.timestamp):local = [HH:MM:ss.l](与 pretty 缺省一致)、
371
+ * iso8601 = 2026-10-25T16:01:33.294+08:00、elapsed = +123.456s(进程内单调秒)。
372
+ * json 的 time 恒为 ISO8601(机器可解析契约,不受 timestamp 键影响)。
373
+ */
374
+ /** 级别数值 → 名称(pino 标准映射)。 */
375
+ const LEVEL_LABELS = {
376
+ 10: "trace",
377
+ 20: "debug",
378
+ 30: "info",
379
+ 40: "warn",
380
+ 50: "error",
381
+ 60: "fatal"
382
+ };
383
+ /** single-line 颜色(colorize 时仅着色级别与元数据头,正文默认色)。 */
384
+ const LEVEL_COLORS = {
385
+ trace: "\x1B[34m",
386
+ debug: "\x1B[36m",
387
+ info: "\x1B[32m",
388
+ warn: "\x1B[33m",
389
+ error: "\x1B[31m",
390
+ fatal: "\x1B[91m"
391
+ };
392
+ const DIM = "\x1B[2m";
393
+ const RESET = "\x1B[0m";
394
+ /** 2 位补零。 */
395
+ function pad2(n) {
396
+ return String(n).padStart(2, "0");
397
+ }
398
+ /** 单调墙钟毫秒 → 本地时区 ISO8601(+08:00 形态;new Date(epoch) 构造不受 Date.now 篡改影响)。 */
399
+ function isoLocalTime(epochMs) {
400
+ const d = new Date(epochMs);
401
+ const offsetMinutes = -d.getTimezoneOffset();
402
+ const sign = offsetMinutes >= 0 ? "+" : "-";
403
+ const abs = Math.abs(offsetMinutes);
404
+ 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)}`;
405
+ }
406
+ /** 单调墙钟毫秒 → 本地时分秒(与 pino-pretty SYS:HH:MM:ss.l 一致的短格式)。 */
407
+ function localShortTime(epochMs) {
408
+ const d = new Date(epochMs);
409
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`;
410
+ }
411
+ /** 单调墙钟毫秒 → 进程内相对秒(+123.456s)。 */
412
+ function elapsedLabel(epochMs, timeOriginMs) {
413
+ return `+${((epochMs - timeOriginMs) / 1e3).toFixed(3)}s`;
414
+ }
415
+ /** 渲染记录时间(timestamp 模式分发;json 恒 ISO8601)。 */
416
+ function renderTime(rawTime, mode, forceIso, timeOriginMs) {
417
+ if (typeof rawTime === "string") return rawTime;
418
+ const epoch = typeof rawTime === "number" ? rawTime : timeOriginMs;
419
+ if (forceIso || mode === "iso8601") return isoLocalTime(epoch);
420
+ if (mode === "elapsed") return elapsedLabel(epoch, timeOriginMs);
421
+ return localShortTime(epoch);
422
+ }
423
+ /** 级别数值 → 标签名。 */
424
+ function levelLabel(raw) {
425
+ if (typeof raw === "number") return LEVEL_LABELS[raw] ?? String(raw);
426
+ return typeof raw === "string" ? raw.toUpperCase() : "LOG";
427
+ }
428
+ /** k=v 内联值:字符串裸输出(含空格时不加引号——宽松人类格式),其余 JSON 序列化。 */
429
+ function inlineValue(value) {
430
+ if (typeof value === "string") return value;
431
+ return JSON.stringify(value) ?? "null";
432
+ }
433
+ /** 结构化输出忽略的标准字段(渲染进头/独立字段,不进 k=v 尾部)。 */
434
+ const RESERVED_KEYS = /* @__PURE__ */ new Set([
435
+ "pid",
436
+ "hostname",
437
+ "level",
438
+ "time",
439
+ "msg",
440
+ "name"
441
+ ]);
442
+ /** json 渲染:NDJSON 契约 {time, level, module, account?, pid, msg, ...fields}。 */
443
+ function renderJsonLine(record, ctx) {
444
+ const { time, level, name, module, account, pid, msg: _msg, ...fields } = record;
445
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
446
+ const jsonLine = {
447
+ time: renderTime(time, ctx.timestamp, true, ctx.timeOriginMs),
448
+ level: levelLabel(level).toLowerCase(),
449
+ ...module !== void 0 ? { module } : typeof name === "string" ? { module: name } : {},
450
+ ...account !== void 0 ? { account } : {},
451
+ ...typeof pid === "number" ? { pid } : {},
452
+ msg,
453
+ ...fields
454
+ };
455
+ return `${JSON.stringify(jsonLine)}\n`;
456
+ }
457
+ /** single-line 元数据头:(name/pid),无 name 时 (pid)。 */
458
+ function renderMeta(record) {
459
+ const pid = record["pid"] ?? "-";
460
+ const name = record["name"];
461
+ return typeof name === "string" ? `(${name}/${pid})` : `(${pid})`;
462
+ }
463
+ /** single-line 附加字段:非保留键按 k=v 内联(无字段时空串)。 */
464
+ function renderKvFields(record) {
465
+ const fields = [];
466
+ for (const [key, value] of Object.entries(record)) if (!RESERVED_KEYS.has(key)) fields.push(`${key}=${inlineValue(value)}`);
467
+ return fields.length > 0 ? ` ${fields.join(" ")}` : "";
468
+ }
469
+ /** single-line 渲染:[时间] LEVEL (name/pid): 消息 k=v k=v(级别大写与 pretty 一致)。 */
470
+ function renderSingleLine(record, ctx) {
471
+ const label = levelLabel(record["level"]);
472
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
473
+ const time = renderTime(record["time"], ctx.timestamp, false, ctx.timeOriginMs);
474
+ const labelUpper = label.toUpperCase();
475
+ const meta = renderMeta(record);
476
+ const fieldsSuffix = renderKvFields(record);
477
+ if (ctx.useColor) {
478
+ const color = LEVEL_COLORS[label.toLowerCase()] ?? "";
479
+ return `${DIM}[${time}]${RESET} ${color}${labelUpper}${RESET} ${DIM}${meta}:${RESET} ${msg}${fieldsSuffix}\n`;
480
+ }
481
+ return `[${time}] ${labelUpper} ${meta}: ${msg}${fieldsSuffix}\n`;
482
+ }
483
+ /** 创建结构化控制台流(single-line / json)。 */
484
+ function createConsoleFormatStream(opts) {
485
+ const out = opts.destination ?? node_process.default.stdout;
486
+ const ctx = {
487
+ timestamp: opts.timestamp,
488
+ timeOriginMs: performance.timeOrigin,
489
+ useColor: opts.format === "single-line" && opts.colorize
490
+ };
491
+ const render = opts.format === "json" ? renderJsonLine : renderSingleLine;
492
+ return { write(line) {
493
+ let record;
494
+ try {
495
+ record = JSON.parse(line);
496
+ } catch {
497
+ out.write(`${line}\n`);
498
+ return;
499
+ }
500
+ out.write(render(record, ctx));
501
+ } };
502
+ }
503
+ //#endregion
188
504
  //#region src/infra/logger.ts
189
505
  /**
190
506
  * pino 日志封装(ADR-007)
191
507
  *
192
- * - console(pino-pretty 可读输出)+ 可选文件双目的地
193
- * - 统一 level 控制
508
+ * - console(pretty 可读输出 / single-line / json 结构化)+ 可选文件双目的地
509
+ * - 统一 level 控制(控制台与文件可分别设级,2026-10-25 日志可配置化)
194
510
  * - 内置 redact(token / 票据等敏感字段不打日志),可追加
195
511
  * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
512
+ *
513
+ * 时间源(2026-10-25 修复):QQNT 原生层会在 onRecvSysMsg 回调窗口内篡改进程
514
+ * Date(实测记录时间冻结为假值、回调外恢复),故时间戳统一取
515
+ * performance.timeOrigin + performance.now() 单调派生墙钟,不受 Date 篡改影响。
196
516
  */
197
517
  /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
198
518
  const DEFAULT_REDACT_PATHS = [
@@ -202,39 +522,91 @@ const DEFAULT_REDACT_PATHS = [
202
522
  "password",
203
523
  "secret"
204
524
  ];
525
+ /** 级别数值序(求多目的地最低级别用)。 */
526
+ const LEVEL_ORDER = [
527
+ "trace",
528
+ "debug",
529
+ "info",
530
+ "warn",
531
+ "error",
532
+ "fatal",
533
+ "silent"
534
+ ];
535
+ /** 取两级别中更低(更宽容)的一个。 */
536
+ function minLevel(a, b) {
537
+ return LEVEL_ORDER.indexOf(a) <= LEVEL_ORDER.indexOf(b) ? a : b;
538
+ }
539
+ /** 单调墙钟毫秒(timeOrigin 为进程启动真实 Unix 时间,不受进程内 Date 篡改影响)。 */
540
+ function wallMs() {
541
+ return performance.timeOrigin + performance.now();
542
+ }
543
+ /** 进程内相对秒标签(elapsed 模式,pretty 字符串时间路径用)。 */
544
+ function elapsedLabelNow() {
545
+ return `+${(performance.now() / 1e3).toFixed(3)}s`;
546
+ }
205
547
  function toSingle(streams) {
206
548
  const [only] = streams;
207
549
  if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
208
550
  return only.stream;
209
551
  }
210
- /**
211
- * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
212
- *
213
- * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
214
- * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
215
- */
552
+ /** 构造控制台流:pretty 走 pino-pretty,single-line/json 走结构化渲染流。 */
553
+ function buildConsoleStream(params) {
554
+ const { format, timestampMode, colorize, destination } = params;
555
+ if (format !== "pretty") return createConsoleFormatStream({
556
+ format,
557
+ timestamp: timestampMode,
558
+ colorize,
559
+ destination
560
+ });
561
+ return (0, pino_pretty.default)({
562
+ colorize,
563
+ ...timestampMode === "local" ? { translateTime: "SYS:HH:MM:ss.l" } : {},
564
+ ...timestampMode === "iso8601" ? { translateTime: "SYS:yyyy-mm-dd'T'HH:MM:ss.l Z" } : {},
565
+ singleLine: false,
566
+ destination
567
+ });
568
+ }
569
+ /** 折叠 LoggerOptions 的渲染缺省:格式按 TTY 自动(TTY pretty / pipe single-line)、
570
+ * 颜色仅在 pretty 且 TTY 时开。 */
571
+ function resolveRenderParams(opts) {
572
+ const destination = opts.destination ?? node_process.default.stdout;
573
+ const isTty = destination.isTTY === true;
574
+ const format = opts.format ?? (isTty ? "pretty" : "single-line");
575
+ return {
576
+ format,
577
+ timestampMode: opts.timestamp ?? "local",
578
+ colorize: opts.colorize ?? (format === "pretty" && isTty),
579
+ destination
580
+ };
581
+ }
216
582
  function createLogger(opts = {}) {
217
- const level = opts.level ?? "info";
218
- const consoleEnabled = opts.console ?? true;
583
+ const consoleLevel = opts.level ?? "info";
584
+ const fileLevel = opts.fileLevel ?? consoleLevel;
219
585
  const { file, base } = opts;
586
+ const render = resolveRenderParams(opts);
220
587
  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
- }) });
588
+ if ((opts.console ?? true) === true) streams.push({
589
+ stream: buildConsoleStream(render),
590
+ level: consoleLevel
591
+ });
227
592
  if (file) {
228
593
  (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
- }) });
594
+ streams.push({
595
+ stream: pino.default.destination({
596
+ dest: file,
597
+ sync: true
598
+ }),
599
+ level: fileLevel
600
+ });
233
601
  }
234
602
  if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
603
+ let gate = "fatal";
604
+ for (const entry of streams) gate = minLevel(gate, entry.level);
605
+ const pinoTimestamp = render.format === "pretty" && render.timestampMode === "elapsed" ? () => `,"time":"${elapsedLabelNow()}"` : () => `,"time":${wallMs()}`;
235
606
  const pinoOptions = {
236
- level,
237
- redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
607
+ level: gate,
608
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []],
609
+ timestamp: pinoTimestamp
238
610
  };
239
611
  if (base) pinoOptions.base = {
240
612
  pid: node_process.default.pid,
@@ -3771,7 +4143,14 @@ var NapukettoCore = class NapukettoCore {
3771
4143
  level: opts.logLevel ?? "info",
3772
4144
  console: opts.consoleLog ?? true,
3773
4145
  file: opts.logFile ?? paths.file("logs", "napuketto.log"),
3774
- base: { name: "kernel" }
4146
+ fileLevel: opts.logFileLevel,
4147
+ format: opts.logFormat,
4148
+ timestamp: opts.logTimestamp,
4149
+ colorize: opts.logColorize,
4150
+ base: {
4151
+ name: "kernel",
4152
+ account: opts.account
4153
+ }
3775
4154
  });
3776
4155
  return new NapukettoCore(createCoreContext({
3777
4156
  logger,
@@ -3816,22 +4195,23 @@ var NapukettoCore = class NapukettoCore {
3816
4195
  this.ctx.login = loginResult;
3817
4196
  return loginResult;
3818
4197
  }
3819
- let loginResult;
4198
+ const loginResult = await this.loginQuickFirst(wrapper, opts);
4199
+ this.ctx.login = loginResult;
4200
+ return loginResult;
4201
+ }
4202
+ /** 快速登录优先,失败按 qrFallback 决定是否回退 QR(QR 应直接出码,见 loginByQr 注)。 */
4203
+ async loginQuickFirst(wrapper, opts) {
3820
4204
  try {
3821
- const quickOpts = {};
3822
- if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
3823
- if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
3824
- loginResult = await quickLogin(wrapper, quickOpts);
4205
+ const result = await quickLogin(wrapper, buildQuickLoginOptions(opts));
3825
4206
  this.ctx.logger.info({
3826
- uin: loginResult.uin,
3827
- uid: loginResult.uid
4207
+ uin: result.uin,
4208
+ uid: result.uid
3828
4209
  }, "快速登录成功");
4210
+ return result;
3829
4211
  } catch (err) {
3830
4212
  if (opts.qrFallback !== true) throw err;
3831
- loginResult = await this.loginByQr(opts);
4213
+ return await this.loginByQr(opts);
3832
4214
  }
3833
- this.ctx.login = loginResult;
3834
- return loginResult;
3835
4215
  }
3836
4216
  /**
3837
4217
  * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
@@ -3880,8 +4260,7 @@ var NapukettoCore = class NapukettoCore {
3880
4260
  try {
3881
4261
  session.start();
3882
4262
  await waitQrLoggedIn(session);
3883
- const self = session.selfInfo;
3884
- if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4263
+ const self = requireQrSelfInfo(session);
3885
4264
  return {
3886
4265
  uin: self.uin,
3887
4266
  uid: self.uid,
@@ -3903,14 +4282,14 @@ var NapukettoCore = class NapukettoCore {
3903
4282
  });
3904
4283
  });
3905
4284
  session.onStateChange((state) => {
3906
- this.ctx.logger.info({ state }, "QR 登录状态");
3907
- const message = state === "failed" ? session.failureReason ?? void 0 : void 0;
3908
- opts.onLoginProgress?.({
3909
- state,
3910
- ...message !== void 0 ? { message } : {}
3911
- });
4285
+ this.forwardQrState(session, opts, state);
3912
4286
  });
3913
4287
  }
4288
+ /** QR 状态转发:日志 + onLoginProgress(failed 附 failureReason)。 */
4289
+ forwardQrState(session, opts, state) {
4290
+ this.ctx.logger.info({ state }, "QR 登录状态");
4291
+ opts.onLoginProgress?.(qrProgressEvent(state, session.failureReason ?? void 0));
4292
+ }
3914
4293
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
3915
4294
  stop() {
3916
4295
  this.ctx.logger.info("NapukettoCore 停止");
@@ -3923,6 +4302,27 @@ function writeQrCodePng(filePath, pngBase64) {
3923
4302
  (0, node_fs.writeFileSync)(filePath, Buffer.from(pngBase64, "base64"));
3924
4303
  } catch {}
3925
4304
  }
4305
+ /** CoreLoginOptions → quickLogin 参数(仅取快速登录相关两键,QR 键不透传)。 */
4306
+ function buildQuickLoginOptions(opts) {
4307
+ const quickOpts = {};
4308
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
4309
+ if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
4310
+ return quickOpts;
4311
+ }
4312
+ /** QR 登录终态后的 selfInfo 提取(空值按登录失败抛出)。 */
4313
+ function requireQrSelfInfo(session) {
4314
+ const self = session.selfInfo;
4315
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4316
+ return self;
4317
+ }
4318
+ /** QR 进度事件折叠(failed 附 failureReason 为 message,其余不带 message 键)。 */
4319
+ function qrProgressEvent(state, reason) {
4320
+ const message = state === "failed" ? reason : void 0;
4321
+ return {
4322
+ state,
4323
+ ...message !== void 0 ? { message } : {}
4324
+ };
4325
+ }
3926
4326
  /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3927
4327
  function waitQrLoggedIn(session) {
3928
4328
  return new Promise((resolve, reject) => {
@@ -4527,7 +4927,9 @@ function listQQVersions(installDir) {
4527
4927
  exports.BuddyCache = BuddyCache;
4528
4928
  exports.ChatType = ChatType;
4529
4929
  exports.ConfigBase = ConfigBase;
4930
+ exports.ConfigError = ConfigError;
4530
4931
  exports.DEFAULT_DATA_ROOT_NAME = DEFAULT_DATA_ROOT_NAME;
4932
+ exports.EXIT_CONFIG_INVALID = EXIT_CONFIG_INVALID;
4531
4933
  exports.ElementType = ElementType;
4532
4934
  exports.FriendApi = FriendApi;
4533
4935
  exports.FriendBridge = FriendBridge;
@@ -4560,6 +4962,7 @@ exports.WebHonorType = WebHonorType;
4560
4962
  exports.buildEngineConfig = buildEngineConfig;
4561
4963
  exports.buildLoginConfig = buildLoginConfig;
4562
4964
  exports.buildSessionConfig = buildSessionConfig;
4965
+ exports.configError = configError;
4563
4966
  exports.createCoreContext = createCoreContext;
4564
4967
  exports.createLifecycleSessionListener = createSessionListener;
4565
4968
  exports.createLogger = createLogger;
@@ -4579,6 +4982,7 @@ exports.listLoginAccounts = listLoginAccounts;
4579
4982
  exports.listQQVersions = listQQVersions;
4580
4983
  exports.narrowBuddyCategories = narrowBuddyCategories;
4581
4984
  exports.parseAppidFromMajor = parseAppidFromMajor;
4985
+ exports.parseLogConfig = parseLogConfig;
4582
4986
  exports.parseToml = parseToml;
4583
4987
  exports.probeRuntime = probeRuntime;
4584
4988
  exports.quickLogin = quickLogin;