@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.cjs +419 -41
- package/dist/index.d.cts +91 -14
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +91 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +419 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
218
|
-
const
|
|
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 (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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({
|
|
230
|
-
|
|
231
|
-
|
|
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,
|
|
@@ -3771,7 +4120,14 @@ var NapukettoCore = class NapukettoCore {
|
|
|
3771
4120
|
level: opts.logLevel ?? "info",
|
|
3772
4121
|
console: opts.consoleLog ?? true,
|
|
3773
4122
|
file: opts.logFile ?? paths.file("logs", "napuketto.log"),
|
|
3774
|
-
|
|
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
|
+
}
|
|
3775
4131
|
});
|
|
3776
4132
|
return new NapukettoCore(createCoreContext({
|
|
3777
4133
|
logger,
|
|
@@ -3816,22 +4172,23 @@ var NapukettoCore = class NapukettoCore {
|
|
|
3816
4172
|
this.ctx.login = loginResult;
|
|
3817
4173
|
return loginResult;
|
|
3818
4174
|
}
|
|
3819
|
-
|
|
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) {
|
|
3820
4181
|
try {
|
|
3821
|
-
const
|
|
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);
|
|
4182
|
+
const result = await quickLogin(wrapper, buildQuickLoginOptions(opts));
|
|
3825
4183
|
this.ctx.logger.info({
|
|
3826
|
-
uin:
|
|
3827
|
-
uid:
|
|
4184
|
+
uin: result.uin,
|
|
4185
|
+
uid: result.uid
|
|
3828
4186
|
}, "快速登录成功");
|
|
4187
|
+
return result;
|
|
3829
4188
|
} catch (err) {
|
|
3830
4189
|
if (opts.qrFallback !== true) throw err;
|
|
3831
|
-
|
|
4190
|
+
return await this.loginByQr(opts);
|
|
3832
4191
|
}
|
|
3833
|
-
this.ctx.login = loginResult;
|
|
3834
|
-
return loginResult;
|
|
3835
4192
|
}
|
|
3836
4193
|
/**
|
|
3837
4194
|
* loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
|
|
@@ -3880,8 +4237,7 @@ var NapukettoCore = class NapukettoCore {
|
|
|
3880
4237
|
try {
|
|
3881
4238
|
session.start();
|
|
3882
4239
|
await waitQrLoggedIn(session);
|
|
3883
|
-
const self = session
|
|
3884
|
-
if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
|
|
4240
|
+
const self = requireQrSelfInfo(session);
|
|
3885
4241
|
return {
|
|
3886
4242
|
uin: self.uin,
|
|
3887
4243
|
uid: self.uid,
|
|
@@ -3903,14 +4259,14 @@ var NapukettoCore = class NapukettoCore {
|
|
|
3903
4259
|
});
|
|
3904
4260
|
});
|
|
3905
4261
|
session.onStateChange((state) => {
|
|
3906
|
-
this.
|
|
3907
|
-
const message = state === "failed" ? session.failureReason ?? void 0 : void 0;
|
|
3908
|
-
opts.onLoginProgress?.({
|
|
3909
|
-
state,
|
|
3910
|
-
...message !== void 0 ? { message } : {}
|
|
3911
|
-
});
|
|
4262
|
+
this.forwardQrState(session, opts, state);
|
|
3912
4263
|
});
|
|
3913
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
|
+
}
|
|
3914
4270
|
/** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
|
|
3915
4271
|
stop() {
|
|
3916
4272
|
this.ctx.logger.info("NapukettoCore 停止");
|
|
@@ -3923,6 +4279,27 @@ function writeQrCodePng(filePath, pngBase64) {
|
|
|
3923
4279
|
(0, node_fs.writeFileSync)(filePath, Buffer.from(pngBase64, "base64"));
|
|
3924
4280
|
} catch {}
|
|
3925
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
|
+
}
|
|
3926
4303
|
/** 等待 QR 登录终态:logged_in resolve,failed reject。 */
|
|
3927
4304
|
function waitQrLoggedIn(session) {
|
|
3928
4305
|
return new Promise((resolve, reject) => {
|
|
@@ -4579,6 +4956,7 @@ exports.listLoginAccounts = listLoginAccounts;
|
|
|
4579
4956
|
exports.listQQVersions = listQQVersions;
|
|
4580
4957
|
exports.narrowBuddyCategories = narrowBuddyCategories;
|
|
4581
4958
|
exports.parseAppidFromMajor = parseAppidFromMajor;
|
|
4959
|
+
exports.parseLogConfig = parseLogConfig;
|
|
4582
4960
|
exports.parseToml = parseToml;
|
|
4583
4961
|
exports.probeRuntime = probeRuntime;
|
|
4584
4962
|
exports.quickLogin = quickLogin;
|