@napuketto/loader 0.1.0 → 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/host/self-host.cjs +303 -139
- package/package.json +1 -1
package/dist/host/self-host.cjs
CHANGED
|
@@ -373,6 +373,8 @@ const env = {
|
|
|
373
373
|
NAPUTO_PROBE: process.env["NAPUTO_PROBE"],
|
|
374
374
|
/** 全局 TOML 配置显式路径(优先级最高)。 */
|
|
375
375
|
NAPKETTO_CONFIG: process.env["NAPKETTO_CONFIG"],
|
|
376
|
+
/** 内联主配置 JSON 文本(嵌入式宿主注入;优先级高于 NAPKETTO_CONFIG 文件,2026-10-25)。 */
|
|
377
|
+
NAPKETTO_CONFIG_JSON: process.env["NAPKETTO_CONFIG_JSON"],
|
|
376
378
|
/** 数据根(kernel 数据目录,cleanCache 用)。 */
|
|
377
379
|
NAPKETTO_DATA: process.env["NAPKETTO_DATA"],
|
|
378
380
|
/** 数据根(launcher 注入,停止哨兵目录解析用)。 */
|
|
@@ -398,6 +400,39 @@ function errMsg(e) {
|
|
|
398
400
|
return `${e?.message ?? e}`;
|
|
399
401
|
}
|
|
400
402
|
/**
|
|
403
|
+
* 配置校验失败标记错误(2026-10-25 嵌入式退出码契约)。
|
|
404
|
+
* 与 kernel ConfigError 同契约定名(name="ConfigError")——loader 不静态依赖
|
|
405
|
+
* kernel(运行期动态 import),按错误名复刻判定面:协议段 seed 校验失败与
|
|
406
|
+
* kernel 主配置校验失败同族,bootstrap 链重抛后由 self-host 以 78 退出
|
|
407
|
+
* (宿主据此区分「配置未就绪」与运行时崩溃,免重启)。
|
|
408
|
+
*/
|
|
409
|
+
var ConfigError = class extends Error {
|
|
410
|
+
constructor(message, options) {
|
|
411
|
+
super(message, options);
|
|
412
|
+
this.name = "ConfigError";
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
/** 配置校验失败判定(name 契约:kernel ConfigError 与 loader 复刻类都命中)。 */
|
|
416
|
+
function isConfigError(err) {
|
|
417
|
+
return err instanceof Error && err.name === "ConfigError";
|
|
418
|
+
}
|
|
419
|
+
/** bootstrap 失败 → 进程退出码(78 = 配置校验失败一族;1 = 其余运行时失败)。 */
|
|
420
|
+
function bootstrapExitCode(err) {
|
|
421
|
+
return isConfigError(err) ? 78 : 1;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* bootstrap 失败收敛(2026-10-25 退出码契约):配置校验失败(ConfigError 一族)
|
|
425
|
+
* 原样重抛——self-host 据此以 78 退出(宿主免重启),链条上任何 catch 都不得
|
|
426
|
+
* 吞掉;其余失败记日志并收敛 false(引导失败原因见 napuketto-boot.log)。
|
|
427
|
+
* @param phase 失败阶段标注(日志前缀,如 "lifecycle error" / "import kernel failed")
|
|
428
|
+
* @returns 恒 false(调用方直接 `return convergeBootstrapFailure(e, ...)`)
|
|
429
|
+
*/
|
|
430
|
+
function convergeBootstrapFailure(e, phase) {
|
|
431
|
+
if (isConfigError(e)) throw e;
|
|
432
|
+
log(`bootstrap: ${phase}: ${errMsg(e)}`);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
401
436
|
* 逐条处理消息回调参数(onRecvMsg 为消息数组——2026-08-07 运行时实证;
|
|
402
437
|
* 兼容单条对象,过滤无效项)。
|
|
403
438
|
*/
|
|
@@ -1313,15 +1348,109 @@ function handleControl(payload, onExit, onLogin) {
|
|
|
1313
1348
|
});
|
|
1314
1349
|
}
|
|
1315
1350
|
//#endregion
|
|
1351
|
+
//#region src/host/load-config.ts
|
|
1352
|
+
/**
|
|
1353
|
+
* 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化;2026-08-08 结构拍板:按账号取段)
|
|
1354
|
+
*
|
|
1355
|
+
* 主配置来源(2026-10-25 嵌入式内联契约):**NAPKETTO_CONFIG_JSON(内联 JSON,
|
|
1356
|
+
* 与 napuketto.toml 同构)> NAPKETTO_CONFIG(文件)> NAPUTO_CFG_DIR 兜底(旧行为
|
|
1357
|
+
* 兼容)**。内联生效时不读任何 toml 文件,cfgFile 返回哨兵 "<inline>"(仅作
|
|
1358
|
+
* ProtocolConfig 来源标注;seed 模式本就不读盘,协议段热更新在内联形态不可用)。
|
|
1359
|
+
*
|
|
1360
|
+
* 全局配置:**协议配置嵌在账号内**——accounts 数组每项含 [onebot11] / [satori] /
|
|
1361
|
+
* [kurobot] / [milky] 段,登录成功后按账号 uin 找到对应项,取其段作 seed(zod 校验)。
|
|
1362
|
+
* (ConfigBase seed 模式:load() 直接用内存值,不再读写独立协议文件)
|
|
1363
|
+
*
|
|
1364
|
+
* 内联分支宽松(解析失败回退文件形态并记日志,不阻塞引导):cli 主路径已在
|
|
1365
|
+
* 拉起前完整校验(exit 78),走到这里的坏内联多见于绕过 cli 的直跑场景。
|
|
1366
|
+
*/
|
|
1367
|
+
/** 内联来源时 cfgFile 的哨兵值(非文件路径;日志/错误标注里不出现真实路径)。 */
|
|
1368
|
+
const INLINE_CONFIG_SENTINEL = "<inline>";
|
|
1369
|
+
/** 账号内取协议段(宽松对象,装配时 zod 校验)。 */
|
|
1370
|
+
function sectionOf(account, key) {
|
|
1371
|
+
const section = account[key];
|
|
1372
|
+
if (section !== void 0 && section !== null && typeof section === "object") return section;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* 读内联配置文档(NAPKETTO_CONFIG_JSON 非空才生效;JSON 解析失败 / 非对象 →
|
|
1376
|
+
* undefined 回退文件形态并记日志)。
|
|
1377
|
+
*/
|
|
1378
|
+
function readInlineConfigDoc() {
|
|
1379
|
+
const raw = env.NAPKETTO_CONFIG_JSON;
|
|
1380
|
+
if (raw === void 0 || raw.trim() === "") return;
|
|
1381
|
+
try {
|
|
1382
|
+
const doc = JSON.parse(raw);
|
|
1383
|
+
if (typeof doc === "object" && doc !== null && !Array.isArray(doc)) return doc;
|
|
1384
|
+
log("bootstrap: 内联配置不是 JSON 对象(回退文件形态)");
|
|
1385
|
+
return;
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
log(`bootstrap: 内联配置 JSON 解析失败(回退文件形态): ${errMsg(e)}`);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/** accounts 数组按登录 uin 取协议段(文件/内联共用;未找到账号 / 无协议段 → 空段)。 */
|
|
1392
|
+
function extractSections(accounts, uin) {
|
|
1393
|
+
let ob11Section = {};
|
|
1394
|
+
let satoriSection = {};
|
|
1395
|
+
let kurobotSection = {};
|
|
1396
|
+
let milkySection = {};
|
|
1397
|
+
if (Array.isArray(accounts)) {
|
|
1398
|
+
const mine = accounts.find((item) => typeof item === "object" && item !== null && String(item["qq"]) === uin);
|
|
1399
|
+
if (mine !== void 0) {
|
|
1400
|
+
ob11Section = sectionOf(mine, "onebot11") ?? {};
|
|
1401
|
+
satoriSection = sectionOf(mine, "satori") ?? {};
|
|
1402
|
+
kurobotSection = sectionOf(mine, "kurobot") ?? {};
|
|
1403
|
+
milkySection = sectionOf(mine, "milky") ?? {};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
ob11Section,
|
|
1408
|
+
satoriSection,
|
|
1409
|
+
kurobotSection,
|
|
1410
|
+
milkySection
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* 按登录账号 uin 读取主配置的协议段(读取失败 / 未找到账号 / 无协议段 → 空段,
|
|
1415
|
+
* 不阻塞装配)。uin 为登录成功后实际账号(-q 指定账号与配置不一致时以 uin 为准)。
|
|
1416
|
+
*/
|
|
1417
|
+
function loadProtocolSections(kernel, uin) {
|
|
1418
|
+
const inlineDoc = readInlineConfigDoc();
|
|
1419
|
+
if (inlineDoc !== void 0) {
|
|
1420
|
+
log("bootstrap: 配置来源: inline(NAPKETTO_CONFIG_JSON)");
|
|
1421
|
+
return {
|
|
1422
|
+
cfgFile: INLINE_CONFIG_SENTINEL,
|
|
1423
|
+
...extractSections(inlineDoc["accounts"], uin)
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
1427
|
+
let sections = {
|
|
1428
|
+
ob11Section: {},
|
|
1429
|
+
satoriSection: {},
|
|
1430
|
+
kurobotSection: {},
|
|
1431
|
+
milkySection: {}
|
|
1432
|
+
};
|
|
1433
|
+
try {
|
|
1434
|
+
const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
|
|
1435
|
+
sections = extractSections(kernel.parseToml(raw)["accounts"], uin);
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
log(`bootstrap: 全局配置读取失败(用默认 ob11/satori 配置): ${errMsg(e)}`);
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
cfgFile,
|
|
1441
|
+
...sections
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
//#endregion
|
|
1316
1445
|
//#region src/host/log-cfg.ts
|
|
1317
1446
|
/**
|
|
1318
|
-
* loader 引导链日志配置解析(2026-10-25
|
|
1447
|
+
* loader 引导链日志配置解析(2026-10-25 日志可配置化;2026-10-25 内联配置接入)。
|
|
1319
1448
|
*
|
|
1320
1449
|
* 自建宿主进程(self-host → bootstrap → NapukettoCore / kernel-services)不经过
|
|
1321
|
-
* cli 的 loadCliConfig
|
|
1322
|
-
* loadProtocolSections 一致:
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1450
|
+
* cli 的 loadCliConfig,这里按同一契约解析:主配置 [log] 段经 kernel.parseLogConfig
|
|
1451
|
+
* (env 覆盖在内)解析。主配置来源优先级与 loadProtocolSections 一致:
|
|
1452
|
+
* NAPKETTO_CONFIG_JSON(内联,不读盘)> NAPKETTO_CONFIG > NAPUTO_CFG_DIR 兜底。
|
|
1453
|
+
* kernel 无该导出(老版本)或配置读取失败时回退缺省——日志配置损坏不阻塞引导。
|
|
1325
1454
|
*/
|
|
1326
1455
|
/** 缺省(kernel parseLogConfig 缺省值的最小面镜像;未配置/解析失败兜底)。 */
|
|
1327
1456
|
function defaultLogConfig() {
|
|
@@ -1333,13 +1462,18 @@ function defaultLogConfig() {
|
|
|
1333
1462
|
levels: {}
|
|
1334
1463
|
};
|
|
1335
1464
|
}
|
|
1336
|
-
/**
|
|
1465
|
+
/** 解析引导链日志配置(内联 > toml 文件的 [log] 段 + env 覆盖;失败回退缺省)。 */
|
|
1337
1466
|
function resolveLoaderLogConfig(kernel) {
|
|
1338
1467
|
if (typeof kernel.parseLogConfig !== "function") return defaultLogConfig();
|
|
1339
|
-
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
1340
1468
|
try {
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1469
|
+
const inlineDoc = readInlineConfigDoc();
|
|
1470
|
+
let section;
|
|
1471
|
+
if (inlineDoc !== void 0) section = inlineDoc["log"];
|
|
1472
|
+
else {
|
|
1473
|
+
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
1474
|
+
section = kernel.parseToml((0, node_fs.readFileSync)(cfgFile, "utf8"))["log"];
|
|
1475
|
+
}
|
|
1476
|
+
return kernel.parseLogConfig(section, process.env);
|
|
1343
1477
|
} catch {
|
|
1344
1478
|
return defaultLogConfig();
|
|
1345
1479
|
}
|
|
@@ -1398,52 +1532,6 @@ async function doLogin(core, opts) {
|
|
|
1398
1532
|
}
|
|
1399
1533
|
}
|
|
1400
1534
|
//#endregion
|
|
1401
|
-
//#region src/host/load-config.ts
|
|
1402
|
-
/**
|
|
1403
|
-
* 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化;2026-08-08 结构拍板:按账号取段)
|
|
1404
|
-
*
|
|
1405
|
-
* 全局 TOML 配置(<项目根>/napuketto.toml):**协议配置嵌在账号内**——
|
|
1406
|
-
* accounts 数组每项含 [onebot11] / [satori] 段,登录成功后按账号 uin 找到对应项,
|
|
1407
|
-
* 取其段作 seed(zod 校验)。路径优先级:NAPKETTO_CONFIG(launcher 注入)>
|
|
1408
|
-
* 装配链自身探测(kernel.resolveConfigPath)> NAPUTO_CFG_DIR 兜底(旧行为兼容)。
|
|
1409
|
-
* (ConfigBase seed 模式:load() 直接用内存值,不再读写独立协议文件)
|
|
1410
|
-
*/
|
|
1411
|
-
/** 账号内取协议段(宽松对象,装配时 zod 校验)。 */
|
|
1412
|
-
function sectionOf(account, key) {
|
|
1413
|
-
const section = account[key];
|
|
1414
|
-
if (section !== void 0 && section !== null && typeof section === "object") return section;
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* 按登录账号 uin 读取全局 TOML 的协议段(读取失败 / 未找到账号 / 无协议段 → 空段,
|
|
1418
|
-
* 不阻塞装配)。uin 为登录成功后实际账号(-q 指定账号与配置不一致时以 uin 为准)。
|
|
1419
|
-
*/
|
|
1420
|
-
function loadProtocolSections(kernel, uin) {
|
|
1421
|
-
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
1422
|
-
let ob11Section = {};
|
|
1423
|
-
let satoriSection = {};
|
|
1424
|
-
let kurobotSection = {};
|
|
1425
|
-
try {
|
|
1426
|
-
const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
|
|
1427
|
-
const accounts = kernel.parseToml(raw)["accounts"];
|
|
1428
|
-
if (Array.isArray(accounts)) {
|
|
1429
|
-
const mine = accounts.find((item) => typeof item === "object" && item !== null && String(item["qq"]) === uin);
|
|
1430
|
-
if (mine !== void 0) {
|
|
1431
|
-
ob11Section = sectionOf(mine, "onebot11") ?? {};
|
|
1432
|
-
satoriSection = sectionOf(mine, "satori") ?? {};
|
|
1433
|
-
kurobotSection = sectionOf(mine, "kurobot") ?? {};
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
} catch (e) {
|
|
1437
|
-
log(`bootstrap: 全局配置读取失败(用默认 ob11/satori 配置): ${errMsg(e)}`);
|
|
1438
|
-
}
|
|
1439
|
-
return {
|
|
1440
|
-
cfgFile,
|
|
1441
|
-
ob11Section,
|
|
1442
|
-
satoriSection,
|
|
1443
|
-
kurobotSection
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
//#endregion
|
|
1447
1535
|
//#region src/host/core/assemble-protocols.ts
|
|
1448
1536
|
/**
|
|
1449
1537
|
* assemble-protocols.ts:OB11 / Satori 协议装配(非 IPC 模式,cli pnpm start)。
|
|
@@ -1454,6 +1542,20 @@ const BACKSLASH_RE = /\\/g;
|
|
|
1454
1542
|
/** adapter 入口 index.mjs 后缀(子路径导出替换用)。 */
|
|
1455
1543
|
const INDEX_MJS_RE = /index\.mjs$/;
|
|
1456
1544
|
/**
|
|
1545
|
+
* 协议段 seed 校验(zod 错误 → ConfigError,2026-10-25 嵌入式退出码契约):
|
|
1546
|
+
* 「必填缺失/形状非法」的本体就在这里(如 kurobot url 必填)——包装后沿
|
|
1547
|
+
* startProtocols → bootstrap 重抛,self-host 以 78 退出,supervisor 识别 78
|
|
1548
|
+
* 不再守护重启,堵死「少配协议必填项 → 子进程死循环重启」。zod issue 列表
|
|
1549
|
+
* 是多行 JSON,压成单行(日志单行契约)。
|
|
1550
|
+
*/
|
|
1551
|
+
function parseSeed(label, schema, section) {
|
|
1552
|
+
try {
|
|
1553
|
+
return schema.parse(section);
|
|
1554
|
+
} catch (e) {
|
|
1555
|
+
throw new ConfigError(`协议配置 ${label} 段校验失败: ${errMsg(e).replace(/\s+/g, " ").trim()}`, { cause: e });
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1457
1559
|
* 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。
|
|
1458
1560
|
*
|
|
1459
1561
|
* 返回停止函数(ob11/satori 适配器 stop:传输关闭 + 订阅清理;装配跳过时
|
|
@@ -1472,23 +1574,26 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1472
1574
|
return async () => {};
|
|
1473
1575
|
}
|
|
1474
1576
|
try {
|
|
1577
|
+
const milkyAdapters = [];
|
|
1475
1578
|
const network = await import(`file://${networkEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1476
1579
|
const onebot11Entry = adapterEntry.replace(INDEX_MJS_RE, "onebot11/index.mjs");
|
|
1477
1580
|
const satoriEntry = adapterEntry.replace(INDEX_MJS_RE, "satori/index.mjs");
|
|
1478
1581
|
const kurobotEntry = adapterEntry.replace(INDEX_MJS_RE, "kurobot/index.mjs");
|
|
1582
|
+
const milkyEntry = adapterEntry.replace(INDEX_MJS_RE, "milky/index.mjs");
|
|
1479
1583
|
const coreEntry = adapterEntry.replace(INDEX_MJS_RE, "core/index.mjs");
|
|
1480
1584
|
const adapter = await import(`file://${onebot11Entry.replace(BACKSLASH_RE, "/")}`);
|
|
1481
1585
|
const satoriAdapter = await import(`file://${satoriEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1482
1586
|
const kurobotAdapter = await import(`file://${kurobotEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1587
|
+
const milkyAdapter = await import(`file://${milkyEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1483
1588
|
const adapterCore = await import(`file://${coreEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1484
1589
|
const { channel, groupCache } = services;
|
|
1485
1590
|
const broadcaster = new network.EventBroadcaster();
|
|
1486
|
-
const { cfgFile, ob11Section, satoriSection, kurobotSection } = loadProtocolSections(kernel, loginResult.uin);
|
|
1591
|
+
const { cfgFile, ob11Section, satoriSection, kurobotSection, milkySection } = loadProtocolSections(kernel, loginResult.uin);
|
|
1487
1592
|
const ob11Config = new adapterCore.ProtocolConfig({
|
|
1488
1593
|
path: cfgFile,
|
|
1489
1594
|
schema: adapter.ob11ConfigSchema,
|
|
1490
1595
|
defaults: adapter.ob11ConfigSchema.parse({}),
|
|
1491
|
-
seed: adapter.ob11ConfigSchema
|
|
1596
|
+
seed: parseSeed("onebot11", adapter.ob11ConfigSchema, ob11Section)
|
|
1492
1597
|
});
|
|
1493
1598
|
const wrapperExports = services.ctx.wrapper?.exports;
|
|
1494
1599
|
const qqRoot = kernel.resolveQqUserDataRoot?.(wrapperExports) ?? null;
|
|
@@ -1539,7 +1644,7 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1539
1644
|
path: cfgFile,
|
|
1540
1645
|
schema: satoriAdapter.satoriConfigSchema,
|
|
1541
1646
|
defaults: satoriAdapter.satoriConfigSchema.parse({}),
|
|
1542
|
-
seed: satoriAdapter.satoriConfigSchema
|
|
1647
|
+
seed: parseSeed("satori", satoriAdapter.satoriConfigSchema, satoriSection)
|
|
1543
1648
|
});
|
|
1544
1649
|
const satori = new satoriAdapter.NapukettoSatoriAdapter({
|
|
1545
1650
|
config: satoriConfig,
|
|
@@ -1562,7 +1667,7 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1562
1667
|
path: cfgFile,
|
|
1563
1668
|
schema: kurobotAdapter.kurobotConfigSchema,
|
|
1564
1669
|
defaults: kurobotAdapter.kurobotConfigDefaults(),
|
|
1565
|
-
seed: kurobotAdapter.kurobotConfigSchema
|
|
1670
|
+
seed: parseSeed("kurobot", kurobotAdapter.kurobotConfigSchema, kurobotSection)
|
|
1566
1671
|
});
|
|
1567
1672
|
kurobot = new kurobotAdapter.NapukettoKurobotAdapter({
|
|
1568
1673
|
config: kurobotConfig,
|
|
@@ -1575,10 +1680,36 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1575
1680
|
await kurobot.start();
|
|
1576
1681
|
log("bootstrap: kurobot adapter started");
|
|
1577
1682
|
}
|
|
1683
|
+
if (Object.keys(milkySection).length > 0) {
|
|
1684
|
+
const milkyConfig = new adapterCore.ProtocolConfig({
|
|
1685
|
+
path: cfgFile,
|
|
1686
|
+
schema: milkyAdapter.milkyConfigSchema,
|
|
1687
|
+
defaults: milkyAdapter.milkyConfigSchema.parse({}),
|
|
1688
|
+
seed: parseSeed("milky", milkyAdapter.milkyConfigSchema, milkySection)
|
|
1689
|
+
});
|
|
1690
|
+
const milky = new milkyAdapter.NapukettoMilkyAdapter({
|
|
1691
|
+
config: milkyConfig,
|
|
1692
|
+
broadcaster,
|
|
1693
|
+
msgChannel: channel,
|
|
1694
|
+
msgApi: services.msgApi,
|
|
1695
|
+
groupApi: services.groupApi,
|
|
1696
|
+
friendApi: services.friendApi,
|
|
1697
|
+
profileApi: services.profileApi,
|
|
1698
|
+
self: services.self,
|
|
1699
|
+
groupCache,
|
|
1700
|
+
qqVersion: env.NAPUTO_QQ_VERSION || "unknown",
|
|
1701
|
+
mediaDir: (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "cache"),
|
|
1702
|
+
logger: services.calibLogger
|
|
1703
|
+
});
|
|
1704
|
+
await milky.start();
|
|
1705
|
+
log("bootstrap: milky adapter started");
|
|
1706
|
+
milkyAdapters.push(milky);
|
|
1707
|
+
}
|
|
1578
1708
|
return async () => {
|
|
1579
1709
|
await ob11.stop();
|
|
1580
1710
|
await satori.stop();
|
|
1581
1711
|
if (kurobot !== null) await kurobot.stop();
|
|
1712
|
+
for (const milky of milkyAdapters) await milky.stop();
|
|
1582
1713
|
};
|
|
1583
1714
|
} catch (e) {
|
|
1584
1715
|
log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
|
|
@@ -1808,6 +1939,7 @@ async function startProtocols(kernel, ctx, loginResult, logCfg) {
|
|
|
1808
1939
|
services.stopAdapters = await assembleOb11AndSatori(kernel, services, loginResult);
|
|
1809
1940
|
} catch (e) {
|
|
1810
1941
|
log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
|
|
1942
|
+
if (isConfigError(e)) throw e;
|
|
1811
1943
|
return null;
|
|
1812
1944
|
}
|
|
1813
1945
|
return services;
|
|
@@ -2320,25 +2452,30 @@ async function runSmokeIfEnabled(kernel, ctx, loginResult) {
|
|
|
2320
2452
|
log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
|
|
2321
2453
|
}
|
|
2322
2454
|
}
|
|
2323
|
-
/**
|
|
2324
|
-
function
|
|
2455
|
+
/** 立即探测一次(NAPUTO_PROBE=1):fallback / core 两处引导路径共用的探测原语。 */
|
|
2456
|
+
function probeOnce(kernel, ctx) {
|
|
2325
2457
|
if (env.NAPUTO_PROBE !== "1" || typeof kernel.probeRuntime !== "function") return;
|
|
2326
2458
|
try {
|
|
2327
2459
|
const probe = kernel.probeRuntime(ctx);
|
|
2328
2460
|
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
2329
|
-
setTimeout(() => {
|
|
2330
|
-
try {
|
|
2331
|
-
if (typeof kernel.probeRuntime !== "function") return;
|
|
2332
|
-
const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
|
|
2333
|
-
log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
|
|
2334
|
-
} catch (e2) {
|
|
2335
|
-
log(`bootstrap: probe-late error: ${errMsg(e2)}`);
|
|
2336
|
-
}
|
|
2337
|
-
}, 35e3);
|
|
2338
2461
|
} catch (e) {
|
|
2339
2462
|
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
2340
2463
|
}
|
|
2341
2464
|
}
|
|
2465
|
+
/** 探测模式(NAPUTO_PROBE=1):立即探测 + 35s 后晚探测(napuketto-probe-late.json)。 */
|
|
2466
|
+
function runProbePhase(kernel, ctx) {
|
|
2467
|
+
if (env.NAPUTO_PROBE !== "1") return;
|
|
2468
|
+
probeOnce(kernel, ctx);
|
|
2469
|
+
setTimeout(() => {
|
|
2470
|
+
try {
|
|
2471
|
+
if (typeof kernel.probeRuntime !== "function") return;
|
|
2472
|
+
const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
|
|
2473
|
+
log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
|
|
2474
|
+
} catch (e2) {
|
|
2475
|
+
log(`bootstrap: probe-late error: ${errMsg(e2)}`);
|
|
2476
|
+
}
|
|
2477
|
+
}, 35e3);
|
|
2478
|
+
}
|
|
2342
2479
|
/** NapukettoCore.create 选项([log] 段 + env 折叠;IPC 模式关 console)。 */
|
|
2343
2480
|
function buildCoreCreateOptions(logCfg, bootEnv) {
|
|
2344
2481
|
return {
|
|
@@ -2513,45 +2650,47 @@ function resolveAppid(kernel, bootEnv) {
|
|
|
2513
2650
|
}
|
|
2514
2651
|
throw new Error("无法从 major.node 解析 appid,请确认 wrapper.node/major.node 完整,或更新 qq-releases.json");
|
|
2515
2652
|
}
|
|
2516
|
-
/**
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
if (typeof kernel.buildLoginConfig
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
}
|
|
2653
|
+
/** kernel 生命周期能力探测(quickLogin / initAndStartSession 是否可用)。 */
|
|
2654
|
+
function hasLifecycleFns(kernel) {
|
|
2655
|
+
return typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function";
|
|
2656
|
+
}
|
|
2657
|
+
/** 登录前置:loginConfig 构建并 initConfig(kernel/ctx 能力缺失时跳过,不阻塞登录)。 */
|
|
2658
|
+
function initLoginConfig(kernel, ctx, Appid, qqVersion, dataDir) {
|
|
2659
|
+
if (typeof kernel.buildLoginConfig !== "function" || !ctx.loginService) return;
|
|
2660
|
+
const loginCfg = kernel.buildLoginConfig(Appid, qqVersion, dataDir);
|
|
2661
|
+
if (typeof ctx.loginService.initConfig === "function") {
|
|
2662
|
+
ctx.loginService.initConfig(loginCfg);
|
|
2663
|
+
log("bootstrap: loginService.initConfig OK");
|
|
2528
2664
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2665
|
+
}
|
|
2666
|
+
/** NT session 配置构建 + initAndStartSession(登录成功后拉起 session)。 */
|
|
2667
|
+
async function buildAndStartSession(kernel, ctx, loginResult, Appid, qqVersion, dataDir) {
|
|
2531
2668
|
const machineGuid = typeof kernel.readMachineGuid === "function" ? kernel.readMachineGuid(ctx.loginService) : "";
|
|
2532
2669
|
const sessionConfig = kernel.buildSessionConfig({
|
|
2533
2670
|
appid: Appid,
|
|
2534
|
-
fullVersion:
|
|
2671
|
+
fullVersion: qqVersion,
|
|
2535
2672
|
selfUin: loginResult.uin,
|
|
2536
2673
|
selfUid: loginResult.uid,
|
|
2537
|
-
accountPath:
|
|
2538
|
-
downloadPath: (0, node_path.join)(
|
|
2674
|
+
accountPath: dataDir,
|
|
2675
|
+
downloadPath: (0, node_path.join)(dataDir, "temp"),
|
|
2539
2676
|
machineGuid
|
|
2540
2677
|
});
|
|
2541
2678
|
const listener = kernel.createLifecycleSessionListener();
|
|
2542
2679
|
await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
|
|
2543
|
-
log("bootstrap: session init + startNT OK!");
|
|
2544
|
-
return true;
|
|
2545
2680
|
}
|
|
2546
|
-
/**
|
|
2547
|
-
async function
|
|
2548
|
-
if (
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
2552
|
-
} catch (e) {
|
|
2553
|
-
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
2681
|
+
/** 快速登录 + session 初始化(kernel lifecycle 路径)。生命周期方法缺失 → false(无登录态,引导不完整)。 */
|
|
2682
|
+
async function quickLoginAndStartSession(kernel, ctx, Appid, bootEnv) {
|
|
2683
|
+
if (!hasLifecycleFns(kernel)) {
|
|
2684
|
+
log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
|
|
2685
|
+
return false;
|
|
2554
2686
|
}
|
|
2687
|
+
const dataDir = bootEnv.dataDir || ".";
|
|
2688
|
+
initLoginConfig(kernel, ctx, Appid, bootEnv.qqVersion, dataDir);
|
|
2689
|
+
const loginResult = await kernel.quickLogin(ctx, {});
|
|
2690
|
+
log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
|
|
2691
|
+
await buildAndStartSession(kernel, ctx, loginResult, Appid, bootEnv.qqVersion, dataDir);
|
|
2692
|
+
log("bootstrap: session init + startNT OK!");
|
|
2693
|
+
return true;
|
|
2555
2694
|
}
|
|
2556
2695
|
/** 回退:旧装配路径(startNapuketto + 手工 lifecycle)。返回引导是否完整完成。 */
|
|
2557
2696
|
async function bootstrapFallback(kernel, state, bootEnv, Appid) {
|
|
@@ -2566,45 +2705,61 @@ async function bootstrapFallback(kernel, state, bootEnv, Appid) {
|
|
|
2566
2705
|
}
|
|
2567
2706
|
log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
|
|
2568
2707
|
const loggedIn = await quickLoginAndStartSession(kernel, ctx, Appid, bootEnv);
|
|
2569
|
-
|
|
2708
|
+
probeOnce(kernel, ctx);
|
|
2570
2709
|
return loggedIn;
|
|
2571
2710
|
}
|
|
2711
|
+
/** kernel 入口能力探测(startNapuketto / NapukettoCore 任一装配入口即可)。 */
|
|
2712
|
+
function hasAssemblyEntry(kernel) {
|
|
2713
|
+
return typeof kernel.startNapuketto === "function" || typeof kernel.NapukettoCore === "function";
|
|
2714
|
+
}
|
|
2715
|
+
/** 动态 import kernel 入口(未注入 / 导入失败 / 无装配入口 → undefined,调用方收敛 false)。 */
|
|
2716
|
+
async function importKernel() {
|
|
2717
|
+
const kernelEntry = env.NAPUTO_KERNEL_ENTRY;
|
|
2718
|
+
if (!kernelEntry) {
|
|
2719
|
+
log("bootstrap: NAPUTO_KERNEL_ENTRY not set");
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const kernel = await import(`file://${kernelEntry.replace(/\\/g, "/")}`);
|
|
2723
|
+
log(`bootstrap: kernel imported, keys: ${Object.keys(kernel).join(", ")}`);
|
|
2724
|
+
if (!hasAssemblyEntry(kernel)) {
|
|
2725
|
+
log("bootstrap: kernel 无 startNapuketto/NapukettoCore 导出");
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
return kernel;
|
|
2729
|
+
}
|
|
2730
|
+
/** bootEnv 三元组归一化(env 快照 string | undefined → string)。 */
|
|
2731
|
+
function buildBootEnv() {
|
|
2732
|
+
return {
|
|
2733
|
+
qqVersion: env.NAPUTO_QQ_VERSION || "",
|
|
2734
|
+
dataDir: env.NAPUTO_CFG_DIR || "",
|
|
2735
|
+
wrapperPath: env.NAPUTO_WRAPPER_PATH || ""
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
/** appid 解析 + 装配路径分发(NapukettoCore 走 core 装配,否则旧 fallback 路径)。 */
|
|
2739
|
+
async function dispatchAssembly(kernel, state, bootEnv) {
|
|
2740
|
+
try {
|
|
2741
|
+
const Appid = resolveAppid(kernel, bootEnv);
|
|
2742
|
+
log(`bootstrap: appid=${Appid}(wrapper=${bootEnv.wrapperPath})`);
|
|
2743
|
+
if (kernel.NapukettoCore !== void 0) return await bootstrapWithCore(kernel, state, bootEnv, Appid);
|
|
2744
|
+
return await bootstrapFallback(kernel, state, bootEnv, Appid);
|
|
2745
|
+
} catch (e) {
|
|
2746
|
+
return convergeBootstrapFailure(e, "lifecycle error");
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2572
2749
|
/**
|
|
2573
2750
|
* 核心引导:import kernel → 装配 → 登录 → 替换 session → 等待就绪 → 协议装配。
|
|
2574
2751
|
* @returns 引导是否完整完成(IPC 模式 = 协议服务装配成功;各失败路径静默收敛为
|
|
2575
2752
|
* false 而非抛错,具体原因记录在 NAPUTO_CFG_DIR 下 napuketto-boot.log——
|
|
2576
|
-
* 调用方 self-host 依此决定 ready/failed
|
|
2753
|
+
* 调用方 self-host 依此决定 ready/failed 与进程去留)。唯一例外:配置校验
|
|
2754
|
+
* 失败(ConfigError 契约)重抛,self-host 以 78 区分配置未就绪与运行时崩溃。
|
|
2577
2755
|
*/
|
|
2578
2756
|
async function bootstrap(state) {
|
|
2579
|
-
const kernelEntry = env.NAPUTO_KERNEL_ENTRY;
|
|
2580
|
-
if (!kernelEntry) {
|
|
2581
|
-
log("bootstrap: NAPUTO_KERNEL_ENTRY not set");
|
|
2582
|
-
return false;
|
|
2583
|
-
}
|
|
2584
2757
|
try {
|
|
2585
|
-
const kernel = await
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
log("bootstrap: kernel 无 startNapuketto/NapukettoCore 导出");
|
|
2589
|
-
return false;
|
|
2590
|
-
}
|
|
2591
|
-
const bootEnv = {
|
|
2592
|
-
qqVersion: env.NAPUTO_QQ_VERSION || "",
|
|
2593
|
-
dataDir: env.NAPUTO_CFG_DIR || "",
|
|
2594
|
-
wrapperPath: env.NAPUTO_WRAPPER_PATH || ""
|
|
2595
|
-
};
|
|
2596
|
-
try {
|
|
2597
|
-
const Appid = resolveAppid(kernel, bootEnv);
|
|
2598
|
-
log(`bootstrap: appid=${Appid}(wrapper=${bootEnv.wrapperPath})`);
|
|
2599
|
-
if (kernel.NapukettoCore !== void 0) return await bootstrapWithCore(kernel, state, bootEnv, Appid);
|
|
2600
|
-
return await bootstrapFallback(kernel, state, bootEnv, Appid);
|
|
2601
|
-
} catch (e) {
|
|
2602
|
-
log(`bootstrap: lifecycle error: ${errMsg(e)}`);
|
|
2603
|
-
return false;
|
|
2604
|
-
}
|
|
2758
|
+
const kernel = await importKernel();
|
|
2759
|
+
if (kernel === void 0) return false;
|
|
2760
|
+
return await dispatchAssembly(kernel, state, buildBootEnv());
|
|
2605
2761
|
} catch (e) {
|
|
2606
|
-
|
|
2607
|
-
return false;
|
|
2762
|
+
return convergeBootstrapFailure(e, "import kernel failed");
|
|
2608
2763
|
}
|
|
2609
2764
|
}
|
|
2610
2765
|
//#endregion
|
|
@@ -2711,24 +2866,33 @@ try {
|
|
|
2711
2866
|
log(`[self-host] ⚠️ O3MiscService 激活失败: ${errMsg(e)}`);
|
|
2712
2867
|
}
|
|
2713
2868
|
log("[self-host] 调 bootstrap(state) ...");
|
|
2869
|
+
/** 引导未完成出口:补发(或不覆盖)failed 状态后退出——运行时失败,exit 1。 */
|
|
2870
|
+
function exitBootIncomplete() {
|
|
2871
|
+
if (shouldSendGenericBootFailed(ipcMode, lastIpcStatusPhase())) sendStatus("failed", "bootstrap 未完成", {
|
|
2872
|
+
code: "UNKNOWN",
|
|
2873
|
+
message: "bootstrap 未完成,详见 napuketto-boot.log"
|
|
2874
|
+
});
|
|
2875
|
+
process.exit(1);
|
|
2876
|
+
}
|
|
2877
|
+
/** bootstrap 抛错出口(2026-10-25 退出码契约):配置校验失败(ConfigError 一族,
|
|
2878
|
+
* 含协议段 seed zod 校验)= 78(EX_CONFIG 惯例,宿主据此免重启);其余 = 1。
|
|
2879
|
+
* 日志文案不做机器耦合,判定只认错误名。 */
|
|
2880
|
+
function exitOnBootstrapError(e) {
|
|
2881
|
+
const code = bootstrapExitCode(e);
|
|
2882
|
+
log(`[self-host] bootstrap 失败${code === 78 ? "(配置校验失败)" : ""}: ${errMsg(e)}`);
|
|
2883
|
+
if (ipcMode) sendStatus("failed", "bootstrap 失败", {
|
|
2884
|
+
code: code === 78 ? "INVALID_PARAM" : "UNKNOWN",
|
|
2885
|
+
message: errMsg(e)
|
|
2886
|
+
});
|
|
2887
|
+
process.exit(code);
|
|
2888
|
+
}
|
|
2714
2889
|
(async () => {
|
|
2715
2890
|
try {
|
|
2716
|
-
if (!await bootstrap(state))
|
|
2717
|
-
if (shouldSendGenericBootFailed(ipcMode, lastIpcStatusPhase())) sendStatus("failed", "bootstrap 未完成", {
|
|
2718
|
-
code: "UNKNOWN",
|
|
2719
|
-
message: "bootstrap 未完成,详见 napuketto-boot.log"
|
|
2720
|
-
});
|
|
2721
|
-
process.exit(1);
|
|
2722
|
-
}
|
|
2891
|
+
if (!await bootstrap(state)) exitBootIncomplete();
|
|
2723
2892
|
log("[self-host] bootstrap 完成");
|
|
2724
2893
|
if (ipcMode) sendStatus("ready");
|
|
2725
2894
|
} catch (e) {
|
|
2726
|
-
|
|
2727
|
-
if (ipcMode) sendStatus("failed", "bootstrap 失败", {
|
|
2728
|
-
code: "UNKNOWN",
|
|
2729
|
-
message: errMsg(e)
|
|
2730
|
-
});
|
|
2731
|
-
process.exit(1);
|
|
2895
|
+
exitOnBootstrapError(e);
|
|
2732
2896
|
}
|
|
2733
2897
|
})();
|
|
2734
2898
|
//#endregion
|