@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.mjs CHANGED
@@ -42,6 +42,25 @@ function kernelError(message, code = "UNKNOWN", options) {
42
42
  function isKernelError(err) {
43
43
  return err instanceof KernelError;
44
44
  }
45
+ /**
46
+ * 配置校验失败(INVALID_PARAM 子型,2026-10-25 嵌入式宿主契约):仅用于
47
+ * 「配置未就绪」一族——主配置校验/反序列化、内联配置、协议段 seed 校验。
48
+ * code 恒为 INVALID_PARAM,协议层错误码映射零涟漪;退出码分类走 instanceof
49
+ * (CLI/self-host 进程以 EXIT_CONFIG_INVALID 退出),与错误文案完全解耦——
50
+ * 文案是人读的,退出码是宿主消费的机器契约。
51
+ */
52
+ var ConfigError = class extends KernelError {
53
+ constructor(message, options) {
54
+ super(message, "INVALID_PARAM", options);
55
+ this.name = "ConfigError";
56
+ }
57
+ };
58
+ /** 便捷构造:`configError('主配置缺少 accounts', { cause })`。 */
59
+ function configError(message, options) {
60
+ return new ConfigError(message, options);
61
+ }
62
+ /** 配置校验失败的进程退出码(UNIX sysexits EX_CONFIG 惯例)。 */
63
+ const EXIT_CONFIG_INVALID = 78;
45
64
  //#endregion
46
65
  //#region src/infra/config.ts
47
66
  /**
@@ -107,14 +126,18 @@ var ConfigBase = class {
107
126
  await this.save(this.defaults);
108
127
  return this.value;
109
128
  }
110
- throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
129
+ throw new ConfigError(`读取配置文件失败: ${this.path}`, { cause: err });
111
130
  }
112
131
  this.value = this.parse(raw);
113
132
  return this.value;
114
133
  }
115
- /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
116
- reload() {
117
- return this.load();
134
+ /**
135
+ * 校验纯文本配置(反序列化 + schema 校验,不读写文件;`config validate`
136
+ * 等只读场景用)。错误包装与 load()/save() 完全一致(ConfigError,
137
+ * 文案含 this.path),保证 validate 与运行时校验逐字一致。
138
+ */
139
+ parseText(raw) {
140
+ return this.parse(raw);
118
141
  }
119
142
  /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
120
143
  async save(next) {
@@ -135,18 +158,18 @@ var ConfigBase = class {
135
158
  if (this.format === "toml") return parse(raw);
136
159
  return JSON.parse(raw);
137
160
  }
138
- /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
161
+ /** 反序列化 + schema 校验,错误统一包装为 ConfigError(配置未就绪一族)。 */
139
162
  parse(raw) {
140
163
  let input;
141
164
  try {
142
165
  input = this.deserialize(raw);
143
166
  } catch (err) {
144
- throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
167
+ throw new ConfigError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, { cause: err });
145
168
  }
146
169
  try {
147
170
  return this.schema.parse(input);
148
171
  } catch (err) {
149
- throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
172
+ throw new ConfigError(`配置文件校验失败: ${this.path}`, { cause: err });
150
173
  }
151
174
  }
152
175
  };
@@ -159,14 +182,311 @@ function stringifyToml(value) {
159
182
  return stringify(value);
160
183
  }
161
184
  //#endregion
185
+ //#region src/infra/log-config.ts
186
+ /**
187
+ * [log] 配置段解析(napuketto.toml 全局段,2026-10-25 日志可配置化)。
188
+ *
189
+ * 统一解析入口:cli(apps/cli)与 loader 引导链共用本函数(经 kernel 导出),
190
+ * 避免两处手写解析漂移。输入为 TOML 解析后的 [log] 段原对象(缺失传 undefined)
191
+ * + 环境变量快照;输出为已解析的 LogConfig(env 覆盖优先于 toml)。
192
+ *
193
+ * 环境变量(优先级高于 toml;非法值忽略——与既有 NAPKETTO_LOG_LEVEL 行为一致):
194
+ * - NAPUKETTO_LOG_LEVEL / NAPKETTO_LOG_LEVEL(兼容旧拼写,NAPUKETTO_ 优先)
195
+ * - NAPUKETTO_LOG_FORMAT(pretty | single-line | json)
196
+ * - NAPUKETTO_NO_BANNER(1/true = 关 banner)
197
+ * - NAPUKETTO_LOG_COLORIZE(1/true/0/false,CLI 顶层进程向子进程传播控制台
198
+ * 渲染决策用;toml 不暴露此键)
199
+ */
200
+ /** pino 标准级别 + silent。 */
201
+ const LOG_LEVEL_VALUES = [
202
+ "trace",
203
+ "debug",
204
+ "info",
205
+ "warn",
206
+ "error",
207
+ "fatal",
208
+ "silent"
209
+ ];
210
+ const LOG_FORMAT_VALUES = [
211
+ "pretty",
212
+ "single-line",
213
+ "json"
214
+ ];
215
+ const TIMESTAMP_MODE_VALUES = [
216
+ "local",
217
+ "iso8601",
218
+ "elapsed"
219
+ ];
220
+ const MODULE_NAME_VALUES = [
221
+ "cli",
222
+ "kernel",
223
+ "loader",
224
+ "ob11"
225
+ ];
226
+ /** 校验级别字面量;非法值抛 INVALID_PARAM。 */
227
+ function requireLevel(value, key) {
228
+ if (typeof value !== "string" || !LOG_LEVEL_VALUES.includes(value)) throw kernelError(`主配置 ${key} 必须是日志级别(${LOG_LEVEL_VALUES.join(" / ")})`, "INVALID_PARAM");
229
+ return value;
230
+ }
231
+ /** 宽松级别解析(env 覆盖用;非法/缺失返回 undefined 不生效)。 */
232
+ function optionalLevel(value) {
233
+ if (value === void 0) return;
234
+ return LOG_LEVEL_VALUES.includes(value) ? value : void 0;
235
+ }
236
+ /** 宽松格式解析(env 覆盖用)。 */
237
+ function optionalFormat(value) {
238
+ if (value === void 0) return;
239
+ return LOG_FORMAT_VALUES.includes(value) ? value : void 0;
240
+ }
241
+ /** 宽松布尔解析(env 用:仅识别 1/true/0/false,其余忽略)。 */
242
+ function optionalBool(value) {
243
+ if (value === "1" || value === "true") return true;
244
+ if (value === "0" || value === "false") return false;
245
+ }
246
+ /** 解析 [log.levels] 按模块覆盖表(未知模块名报错,键集封闭防拼写错误)。 */
247
+ function parseModuleLevels(raw) {
248
+ if (raw === void 0) return {};
249
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.levels 必须是对象", "INVALID_PARAM");
250
+ const out = {};
251
+ for (const [key, value] of Object.entries(raw)) {
252
+ if (!MODULE_NAME_VALUES.includes(key)) throw kernelError(`主配置 log.levels 未知模块 "${key}"(合法模块:${MODULE_NAME_VALUES.join(" / ")})`, "INVALID_PARAM");
253
+ out[key] = requireLevel(value, `log.levels.${key}`);
254
+ }
255
+ return out;
256
+ }
257
+ /** 解析 [log.file] 独立文件 sink。 */
258
+ function parseLogFile(raw, fallbackLevel) {
259
+ if (typeof raw !== "object" || raw === null) throw kernelError("主配置 log.file 必须是对象", "INVALID_PARAM");
260
+ const record = raw;
261
+ const path = record["path"];
262
+ if (typeof path !== "string" || path === "") throw kernelError("主配置 log.file.path 必须是非空字符串", "INVALID_PARAM");
263
+ return {
264
+ path,
265
+ level: record["level"] === void 0 ? fallbackLevel : requireLevel(record["level"], "log.file.level")
266
+ };
267
+ }
268
+ /** 校验字面量枚举值(非法抛 INVALID_PARAM,报错文案带键名与合法值集)。 */
269
+ function requireEnum(value, key, values) {
270
+ if (typeof value !== "string" || !values.includes(value)) throw kernelError(`主配置 ${key} 必须是(${values.join(" / ")})`, "INVALID_PARAM");
271
+ return value;
272
+ }
273
+ /** 校验布尔字段(undefined 保留原值)。 */
274
+ function applyBoolean(draft, raw, key) {
275
+ if (raw[key] === void 0) return;
276
+ if (typeof raw[key] !== "boolean") throw kernelError(`主配置 log.${key} 必须是布尔值`, "INVALID_PARAM");
277
+ draft[key] = raw[key];
278
+ }
279
+ /** toml [log] 段字段填充(level/format/timestamp/banner/unboundReceive)。 */
280
+ function applySectionFields(draft, raw) {
281
+ if (raw["level"] !== void 0) draft.level = requireLevel(raw["level"], "log.level");
282
+ if (raw["format"] !== void 0) draft.format = requireEnum(raw["format"], "log.format", LOG_FORMAT_VALUES);
283
+ if (raw["timestamp"] !== void 0) draft.timestamp = requireEnum(raw["timestamp"], "log.timestamp", TIMESTAMP_MODE_VALUES);
284
+ applyBoolean(draft, raw, "banner");
285
+ applyBoolean(draft, raw, "unboundReceive");
286
+ }
287
+ /** env 覆盖(优先级高于 toml;非法值忽略):兼容旧拼写 NAPKETTO_LOG_LEVEL。
288
+ * file.level 显式配置时不被 env 全局级别顶掉(缺省跟随全局)。 */
289
+ function applyEnvOverrides(draft, env) {
290
+ const envLevel = optionalLevel(env["NAPUKETTO_LOG_LEVEL"]) ?? optionalLevel(env["NAPKETTO_LOG_LEVEL"]);
291
+ if (envLevel !== void 0) {
292
+ draft.level = envLevel;
293
+ if (draft.file !== void 0 && draft.fileLevelExplicit !== true) draft.file = {
294
+ ...draft.file,
295
+ level: envLevel
296
+ };
297
+ }
298
+ const envFormat = optionalFormat(env["NAPUKETTO_LOG_FORMAT"]);
299
+ if (envFormat !== void 0) draft.format = envFormat;
300
+ if (optionalBool(env["NAPUKETTO_NO_BANNER"]) === true) draft.banner = false;
301
+ const envColorize = optionalBool(env["NAPUKETTO_LOG_COLORIZE"]);
302
+ if (envColorize !== void 0) draft.colorize = envColorize;
303
+ }
304
+ /**
305
+ * 解析 [log] 段 + 环境变量覆盖。
306
+ *
307
+ * @param section TOML 解析后的 [log] 段(缺失/undefined = 全缺省)
308
+ * @param env 环境变量快照(默认空对象 = 无 env 覆盖)
309
+ */
310
+ function parseLogConfig(section, env = {}) {
311
+ const draft = {
312
+ level: "info",
313
+ banner: true,
314
+ timestamp: "local",
315
+ unboundReceive: true,
316
+ levels: {}
317
+ };
318
+ if (section !== void 0) {
319
+ if (typeof section !== "object" || section === null) throw kernelError("主配置 log 段必须是对象", "INVALID_PARAM");
320
+ const raw = section;
321
+ applySectionFields(draft, raw);
322
+ draft.levels = parseModuleLevels(raw["levels"]);
323
+ if (raw["file"] !== void 0) {
324
+ draft.file = parseLogFile(raw["file"], draft.level);
325
+ draft.fileLevelExplicit = typeof raw["file"] === "object" && raw["file"] !== null && raw["file"]["level"] !== void 0;
326
+ }
327
+ }
328
+ applyEnvOverrides(draft, env);
329
+ delete draft.fileLevelExplicit;
330
+ return draft;
331
+ }
332
+ //#endregion
333
+ //#region src/infra/log-format.ts
334
+ /**
335
+ * 结构化控制台流(single-line / json 格式,2026-10-25 日志可配置化)。
336
+ *
337
+ * pino-pretty 面向交互终端(多行展开 / JSON 内联渲染),其 singleLine 输出为
338
+ * `msg {"k":"v"}` 形态且 messageFormat 会与字段追加双重渲染——无法干净满足
339
+ * 嵌入式宿主的「一条记录一行、字段 k=v / NDJSON」契约,故自写轻量渲染流:
340
+ *
341
+ * single-line:[时间] LEVEL (name/pid): 消息 key=value key=value
342
+ * json:NDJSON,每行 {time, level, module, account?, pid, msg, ...fields}
343
+ *
344
+ * 时间渲染(LogConfig.timestamp):local = [HH:MM:ss.l](与 pretty 缺省一致)、
345
+ * iso8601 = 2026-10-25T16:01:33.294+08:00、elapsed = +123.456s(进程内单调秒)。
346
+ * json 的 time 恒为 ISO8601(机器可解析契约,不受 timestamp 键影响)。
347
+ */
348
+ /** 级别数值 → 名称(pino 标准映射)。 */
349
+ const LEVEL_LABELS = {
350
+ 10: "trace",
351
+ 20: "debug",
352
+ 30: "info",
353
+ 40: "warn",
354
+ 50: "error",
355
+ 60: "fatal"
356
+ };
357
+ /** single-line 颜色(colorize 时仅着色级别与元数据头,正文默认色)。 */
358
+ const LEVEL_COLORS = {
359
+ trace: "\x1B[34m",
360
+ debug: "\x1B[36m",
361
+ info: "\x1B[32m",
362
+ warn: "\x1B[33m",
363
+ error: "\x1B[31m",
364
+ fatal: "\x1B[91m"
365
+ };
366
+ const DIM = "\x1B[2m";
367
+ const RESET = "\x1B[0m";
368
+ /** 2 位补零。 */
369
+ function pad2(n) {
370
+ return String(n).padStart(2, "0");
371
+ }
372
+ /** 单调墙钟毫秒 → 本地时区 ISO8601(+08:00 形态;new Date(epoch) 构造不受 Date.now 篡改影响)。 */
373
+ function isoLocalTime(epochMs) {
374
+ const d = new Date(epochMs);
375
+ const offsetMinutes = -d.getTimezoneOffset();
376
+ const sign = offsetMinutes >= 0 ? "+" : "-";
377
+ const abs = Math.abs(offsetMinutes);
378
+ 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)}`;
379
+ }
380
+ /** 单调墙钟毫秒 → 本地时分秒(与 pino-pretty SYS:HH:MM:ss.l 一致的短格式)。 */
381
+ function localShortTime(epochMs) {
382
+ const d = new Date(epochMs);
383
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`;
384
+ }
385
+ /** 单调墙钟毫秒 → 进程内相对秒(+123.456s)。 */
386
+ function elapsedLabel(epochMs, timeOriginMs) {
387
+ return `+${((epochMs - timeOriginMs) / 1e3).toFixed(3)}s`;
388
+ }
389
+ /** 渲染记录时间(timestamp 模式分发;json 恒 ISO8601)。 */
390
+ function renderTime(rawTime, mode, forceIso, timeOriginMs) {
391
+ if (typeof rawTime === "string") return rawTime;
392
+ const epoch = typeof rawTime === "number" ? rawTime : timeOriginMs;
393
+ if (forceIso || mode === "iso8601") return isoLocalTime(epoch);
394
+ if (mode === "elapsed") return elapsedLabel(epoch, timeOriginMs);
395
+ return localShortTime(epoch);
396
+ }
397
+ /** 级别数值 → 标签名。 */
398
+ function levelLabel(raw) {
399
+ if (typeof raw === "number") return LEVEL_LABELS[raw] ?? String(raw);
400
+ return typeof raw === "string" ? raw.toUpperCase() : "LOG";
401
+ }
402
+ /** k=v 内联值:字符串裸输出(含空格时不加引号——宽松人类格式),其余 JSON 序列化。 */
403
+ function inlineValue(value) {
404
+ if (typeof value === "string") return value;
405
+ return JSON.stringify(value) ?? "null";
406
+ }
407
+ /** 结构化输出忽略的标准字段(渲染进头/独立字段,不进 k=v 尾部)。 */
408
+ const RESERVED_KEYS = /* @__PURE__ */ new Set([
409
+ "pid",
410
+ "hostname",
411
+ "level",
412
+ "time",
413
+ "msg",
414
+ "name"
415
+ ]);
416
+ /** json 渲染:NDJSON 契约 {time, level, module, account?, pid, msg, ...fields}。 */
417
+ function renderJsonLine(record, ctx) {
418
+ const { time, level, name, module, account, pid, msg: _msg, ...fields } = record;
419
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
420
+ const jsonLine = {
421
+ time: renderTime(time, ctx.timestamp, true, ctx.timeOriginMs),
422
+ level: levelLabel(level).toLowerCase(),
423
+ ...module !== void 0 ? { module } : typeof name === "string" ? { module: name } : {},
424
+ ...account !== void 0 ? { account } : {},
425
+ ...typeof pid === "number" ? { pid } : {},
426
+ msg,
427
+ ...fields
428
+ };
429
+ return `${JSON.stringify(jsonLine)}\n`;
430
+ }
431
+ /** single-line 元数据头:(name/pid),无 name 时 (pid)。 */
432
+ function renderMeta(record) {
433
+ const pid = record["pid"] ?? "-";
434
+ const name = record["name"];
435
+ return typeof name === "string" ? `(${name}/${pid})` : `(${pid})`;
436
+ }
437
+ /** single-line 附加字段:非保留键按 k=v 内联(无字段时空串)。 */
438
+ function renderKvFields(record) {
439
+ const fields = [];
440
+ for (const [key, value] of Object.entries(record)) if (!RESERVED_KEYS.has(key)) fields.push(`${key}=${inlineValue(value)}`);
441
+ return fields.length > 0 ? ` ${fields.join(" ")}` : "";
442
+ }
443
+ /** single-line 渲染:[时间] LEVEL (name/pid): 消息 k=v k=v(级别大写与 pretty 一致)。 */
444
+ function renderSingleLine(record, ctx) {
445
+ const label = levelLabel(record["level"]);
446
+ const msg = typeof record["msg"] === "string" ? record["msg"] : "";
447
+ const time = renderTime(record["time"], ctx.timestamp, false, ctx.timeOriginMs);
448
+ const labelUpper = label.toUpperCase();
449
+ const meta = renderMeta(record);
450
+ const fieldsSuffix = renderKvFields(record);
451
+ if (ctx.useColor) {
452
+ const color = LEVEL_COLORS[label.toLowerCase()] ?? "";
453
+ return `${DIM}[${time}]${RESET} ${color}${labelUpper}${RESET} ${DIM}${meta}:${RESET} ${msg}${fieldsSuffix}\n`;
454
+ }
455
+ return `[${time}] ${labelUpper} ${meta}: ${msg}${fieldsSuffix}\n`;
456
+ }
457
+ /** 创建结构化控制台流(single-line / json)。 */
458
+ function createConsoleFormatStream(opts) {
459
+ const out = opts.destination ?? process.stdout;
460
+ const ctx = {
461
+ timestamp: opts.timestamp,
462
+ timeOriginMs: performance.timeOrigin,
463
+ useColor: opts.format === "single-line" && opts.colorize
464
+ };
465
+ const render = opts.format === "json" ? renderJsonLine : renderSingleLine;
466
+ return { write(line) {
467
+ let record;
468
+ try {
469
+ record = JSON.parse(line);
470
+ } catch {
471
+ out.write(`${line}\n`);
472
+ return;
473
+ }
474
+ out.write(render(record, ctx));
475
+ } };
476
+ }
477
+ //#endregion
162
478
  //#region src/infra/logger.ts
163
479
  /**
164
480
  * pino 日志封装(ADR-007)
165
481
  *
166
- * - console(pino-pretty 可读输出)+ 可选文件双目的地
167
- * - 统一 level 控制
482
+ * - console(pretty 可读输出 / single-line / json 结构化)+ 可选文件双目的地
483
+ * - 统一 level 控制(控制台与文件可分别设级,2026-10-25 日志可配置化)
168
484
  * - 内置 redact(token / 票据等敏感字段不打日志),可追加
169
485
  * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
486
+ *
487
+ * 时间源(2026-10-25 修复):QQNT 原生层会在 onRecvSysMsg 回调窗口内篡改进程
488
+ * Date(实测记录时间冻结为假值、回调外恢复),故时间戳统一取
489
+ * performance.timeOrigin + performance.now() 单调派生墙钟,不受 Date 篡改影响。
170
490
  */
171
491
  /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
172
492
  const DEFAULT_REDACT_PATHS = [
@@ -176,39 +496,91 @@ const DEFAULT_REDACT_PATHS = [
176
496
  "password",
177
497
  "secret"
178
498
  ];
499
+ /** 级别数值序(求多目的地最低级别用)。 */
500
+ const LEVEL_ORDER = [
501
+ "trace",
502
+ "debug",
503
+ "info",
504
+ "warn",
505
+ "error",
506
+ "fatal",
507
+ "silent"
508
+ ];
509
+ /** 取两级别中更低(更宽容)的一个。 */
510
+ function minLevel(a, b) {
511
+ return LEVEL_ORDER.indexOf(a) <= LEVEL_ORDER.indexOf(b) ? a : b;
512
+ }
513
+ /** 单调墙钟毫秒(timeOrigin 为进程启动真实 Unix 时间,不受进程内 Date 篡改影响)。 */
514
+ function wallMs() {
515
+ return performance.timeOrigin + performance.now();
516
+ }
517
+ /** 进程内相对秒标签(elapsed 模式,pretty 字符串时间路径用)。 */
518
+ function elapsedLabelNow() {
519
+ return `+${(performance.now() / 1e3).toFixed(3)}s`;
520
+ }
179
521
  function toSingle(streams) {
180
522
  const [only] = streams;
181
523
  if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
182
524
  return only.stream;
183
525
  }
184
- /**
185
- * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
186
- *
187
- * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
188
- * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
189
- */
526
+ /** 构造控制台流:pretty 走 pino-pretty,single-line/json 走结构化渲染流。 */
527
+ function buildConsoleStream(params) {
528
+ const { format, timestampMode, colorize, destination } = params;
529
+ if (format !== "pretty") return createConsoleFormatStream({
530
+ format,
531
+ timestamp: timestampMode,
532
+ colorize,
533
+ destination
534
+ });
535
+ return pinoPretty({
536
+ colorize,
537
+ ...timestampMode === "local" ? { translateTime: "SYS:HH:MM:ss.l" } : {},
538
+ ...timestampMode === "iso8601" ? { translateTime: "SYS:yyyy-mm-dd'T'HH:MM:ss.l Z" } : {},
539
+ singleLine: false,
540
+ destination
541
+ });
542
+ }
543
+ /** 折叠 LoggerOptions 的渲染缺省:格式按 TTY 自动(TTY pretty / pipe single-line)、
544
+ * 颜色仅在 pretty 且 TTY 时开。 */
545
+ function resolveRenderParams(opts) {
546
+ const destination = opts.destination ?? process.stdout;
547
+ const isTty = destination.isTTY === true;
548
+ const format = opts.format ?? (isTty ? "pretty" : "single-line");
549
+ return {
550
+ format,
551
+ timestampMode: opts.timestamp ?? "local",
552
+ colorize: opts.colorize ?? (format === "pretty" && isTty),
553
+ destination
554
+ };
555
+ }
190
556
  function createLogger(opts = {}) {
191
- const level = opts.level ?? "info";
192
- const consoleEnabled = opts.console ?? true;
557
+ const consoleLevel = opts.level ?? "info";
558
+ const fileLevel = opts.fileLevel ?? consoleLevel;
193
559
  const { file, base } = opts;
560
+ const render = resolveRenderParams(opts);
194
561
  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
- }) });
562
+ if ((opts.console ?? true) === true) streams.push({
563
+ stream: buildConsoleStream(render),
564
+ level: consoleLevel
565
+ });
201
566
  if (file) {
202
567
  mkdirSync(dirname(file), { recursive: true });
203
- streams.push({ stream: pino.destination({
204
- dest: file,
205
- sync: true
206
- }) });
568
+ streams.push({
569
+ stream: pino.destination({
570
+ dest: file,
571
+ sync: true
572
+ }),
573
+ level: fileLevel
574
+ });
207
575
  }
208
576
  if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
577
+ let gate = "fatal";
578
+ for (const entry of streams) gate = minLevel(gate, entry.level);
579
+ const pinoTimestamp = render.format === "pretty" && render.timestampMode === "elapsed" ? () => `,"time":"${elapsedLabelNow()}"` : () => `,"time":${wallMs()}`;
209
580
  const pinoOptions = {
210
- level,
211
- redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
581
+ level: gate,
582
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []],
583
+ timestamp: pinoTimestamp
212
584
  };
213
585
  if (base) pinoOptions.base = {
214
586
  pid: process.pid,
@@ -3745,7 +4117,14 @@ var NapukettoCore = class NapukettoCore {
3745
4117
  level: opts.logLevel ?? "info",
3746
4118
  console: opts.consoleLog ?? true,
3747
4119
  file: opts.logFile ?? paths.file("logs", "napuketto.log"),
3748
- base: { name: "kernel" }
4120
+ fileLevel: opts.logFileLevel,
4121
+ format: opts.logFormat,
4122
+ timestamp: opts.logTimestamp,
4123
+ colorize: opts.logColorize,
4124
+ base: {
4125
+ name: "kernel",
4126
+ account: opts.account
4127
+ }
3749
4128
  });
3750
4129
  return new NapukettoCore(createCoreContext({
3751
4130
  logger,
@@ -3790,22 +4169,23 @@ var NapukettoCore = class NapukettoCore {
3790
4169
  this.ctx.login = loginResult;
3791
4170
  return loginResult;
3792
4171
  }
3793
- let loginResult;
4172
+ const loginResult = await this.loginQuickFirst(wrapper, opts);
4173
+ this.ctx.login = loginResult;
4174
+ return loginResult;
4175
+ }
4176
+ /** 快速登录优先,失败按 qrFallback 决定是否回退 QR(QR 应直接出码,见 loginByQr 注)。 */
4177
+ async loginQuickFirst(wrapper, opts) {
3794
4178
  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);
4179
+ const result = await quickLogin(wrapper, buildQuickLoginOptions(opts));
3799
4180
  this.ctx.logger.info({
3800
- uin: loginResult.uin,
3801
- uid: loginResult.uid
4181
+ uin: result.uin,
4182
+ uid: result.uid
3802
4183
  }, "快速登录成功");
4184
+ return result;
3803
4185
  } catch (err) {
3804
4186
  if (opts.qrFallback !== true) throw err;
3805
- loginResult = await this.loginByQr(opts);
4187
+ return await this.loginByQr(opts);
3806
4188
  }
3807
- this.ctx.login = loginResult;
3808
- return loginResult;
3809
4189
  }
3810
4190
  /**
3811
4191
  * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
@@ -3854,8 +4234,7 @@ var NapukettoCore = class NapukettoCore {
3854
4234
  try {
3855
4235
  session.start();
3856
4236
  await waitQrLoggedIn(session);
3857
- const self = session.selfInfo;
3858
- if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4237
+ const self = requireQrSelfInfo(session);
3859
4238
  return {
3860
4239
  uin: self.uin,
3861
4240
  uid: self.uid,
@@ -3877,14 +4256,14 @@ var NapukettoCore = class NapukettoCore {
3877
4256
  });
3878
4257
  });
3879
4258
  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
- });
4259
+ this.forwardQrState(session, opts, state);
3886
4260
  });
3887
4261
  }
4262
+ /** QR 状态转发:日志 + onLoginProgress(failed 附 failureReason)。 */
4263
+ forwardQrState(session, opts, state) {
4264
+ this.ctx.logger.info({ state }, "QR 登录状态");
4265
+ opts.onLoginProgress?.(qrProgressEvent(state, session.failureReason ?? void 0));
4266
+ }
3888
4267
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
3889
4268
  stop() {
3890
4269
  this.ctx.logger.info("NapukettoCore 停止");
@@ -3897,6 +4276,27 @@ function writeQrCodePng(filePath, pngBase64) {
3897
4276
  writeFileSync(filePath, Buffer.from(pngBase64, "base64"));
3898
4277
  } catch {}
3899
4278
  }
4279
+ /** CoreLoginOptions → quickLogin 参数(仅取快速登录相关两键,QR 键不透传)。 */
4280
+ function buildQuickLoginOptions(opts) {
4281
+ const quickOpts = {};
4282
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
4283
+ if (opts.quickLoginTimeoutMs !== void 0) quickOpts.quickLoginTimeoutMs = opts.quickLoginTimeoutMs;
4284
+ return quickOpts;
4285
+ }
4286
+ /** QR 登录终态后的 selfInfo 提取(空值按登录失败抛出)。 */
4287
+ function requireQrSelfInfo(session) {
4288
+ const self = session.selfInfo;
4289
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
4290
+ return self;
4291
+ }
4292
+ /** QR 进度事件折叠(failed 附 failureReason 为 message,其余不带 message 键)。 */
4293
+ function qrProgressEvent(state, reason) {
4294
+ const message = state === "failed" ? reason : void 0;
4295
+ return {
4296
+ state,
4297
+ ...message !== void 0 ? { message } : {}
4298
+ };
4299
+ }
3900
4300
  /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3901
4301
  function waitQrLoggedIn(session) {
3902
4302
  return new Promise((resolve, reject) => {
@@ -4498,6 +4898,6 @@ function listQQVersions(installDir) {
4498
4898
  }).sort().reverse();
4499
4899
  }
4500
4900
  //#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 };
4901
+ export { BuddyCache, ChatType, ConfigBase, ConfigError, DEFAULT_DATA_ROOT_NAME, EXIT_CONFIG_INVALID, 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, configError, 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
4902
 
4503
4903
  //# sourceMappingURL=index.mjs.map