@napuketto/loader 0.0.1 → 0.0.3
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/README.md +11 -0
- package/dist/host/self-host.cjs +917 -258
- package/dist/index.d.mts +66 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +111 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/host/self-host.cjs
CHANGED
|
@@ -1,6 +1,135 @@
|
|
|
1
|
-
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
2
23
|
let node_fs = require("node:fs");
|
|
24
|
+
let node_path = require("node:path");
|
|
25
|
+
let node_process = require("node:process");
|
|
26
|
+
node_process = __toESM(node_process, 1);
|
|
27
|
+
let node_readline = require("node:readline");
|
|
3
28
|
let node_os = require("node:os");
|
|
29
|
+
//#region src/instance-lock.ts
|
|
30
|
+
/**
|
|
31
|
+
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
32
|
+
*
|
|
33
|
+
* 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
|
|
34
|
+
* QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
|
|
35
|
+
* loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
|
|
36
|
+
* 副本实例常驻占锁,导致用户后续启动全部卡死。
|
|
37
|
+
*
|
|
38
|
+
* 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
|
|
39
|
+
* pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
|
|
40
|
+
* - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
|
|
41
|
+
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
42
|
+
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
43
|
+
*
|
|
44
|
+
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
45
|
+
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
46
|
+
*
|
|
47
|
+
* 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
|
|
48
|
+
* (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
|
|
49
|
+
* 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
|
|
50
|
+
*/
|
|
51
|
+
/** 锁文件名(置于数据目录内)。 */
|
|
52
|
+
const INSTANCE_LOCK_FILE = "instance.lock";
|
|
53
|
+
/**
|
|
54
|
+
* 检测指定数据目录是否已被其他实例占用。
|
|
55
|
+
*
|
|
56
|
+
* @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
|
|
57
|
+
*/
|
|
58
|
+
function checkInstanceLock(dataDir) {
|
|
59
|
+
const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
|
|
60
|
+
let info = null;
|
|
61
|
+
try {
|
|
62
|
+
info = JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
occupied: false,
|
|
66
|
+
pid: null,
|
|
67
|
+
lockPath
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (typeof info.pid !== "number" || Number.isNaN(info.pid) || info.pid <= 0) return {
|
|
71
|
+
occupied: false,
|
|
72
|
+
pid: null,
|
|
73
|
+
lockPath
|
|
74
|
+
};
|
|
75
|
+
let alive = false;
|
|
76
|
+
try {
|
|
77
|
+
node_process.default.kill(info.pid, 0);
|
|
78
|
+
alive = true;
|
|
79
|
+
} catch {
|
|
80
|
+
alive = false;
|
|
81
|
+
}
|
|
82
|
+
if (!alive) return {
|
|
83
|
+
occupied: false,
|
|
84
|
+
pid: null,
|
|
85
|
+
lockPath
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
occupied: true,
|
|
89
|
+
pid: info.pid,
|
|
90
|
+
lockPath
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
|
|
95
|
+
* 幂等:本进程已持有则直接返回 true(不重复写)。
|
|
96
|
+
*/
|
|
97
|
+
function acquireInstanceLock(dataDir, cmdline) {
|
|
98
|
+
const { occupied, pid, lockPath } = checkInstanceLock(dataDir);
|
|
99
|
+
if (occupied) {
|
|
100
|
+
if (pid === node_process.default.pid) return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const info = {
|
|
104
|
+
pid: node_process.default.pid,
|
|
105
|
+
startedAt: Date.now(),
|
|
106
|
+
...cmdline !== void 0 && cmdline !== "" ? { cmdline } : {}
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
(0, node_fs.rmSync)(lockPath, { force: true });
|
|
110
|
+
(0, node_fs.writeFileSync)(lockPath, JSON.stringify(info), "utf-8");
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
|
|
118
|
+
* 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
|
|
119
|
+
*/
|
|
120
|
+
function releaseInstanceLock(dataDir) {
|
|
121
|
+
const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
|
|
122
|
+
try {
|
|
123
|
+
if (JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8")).pid === node_process.default.pid) (0, node_fs.rmSync)(lockPath, { force: true });
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
|
|
127
|
+
function registerLockCleanup(dataDir) {
|
|
128
|
+
node_process.default.on("exit", () => {
|
|
129
|
+
releaseInstanceLock(dataDir);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
4
133
|
//#region src/host/env.ts
|
|
5
134
|
/**
|
|
6
135
|
* env.ts:引导进程环境变量访问层(2026-08-07 阶段 2)。
|
|
@@ -32,6 +161,8 @@ const env = {
|
|
|
32
161
|
NAPUTO_QUICK_UIN: process.env["NAPUTO_QUICK_UIN"],
|
|
33
162
|
/** 收发消息冒烟自检开关(=1 触发)。 */
|
|
34
163
|
NAPUTO_SMOKE: process.env["NAPUTO_SMOKE"],
|
|
164
|
+
/** IPC 子进程模式(=1:koishi 插件驱动——stdout 走 JSON 行协议,stdin 收 action/control)。 */
|
|
165
|
+
NAPUTO_IPC: process.env["NAPUTO_IPC"],
|
|
35
166
|
/** 冒烟目标 peer(c2c:<uin> / group:<uin>,缺省发给自己)。 */
|
|
36
167
|
NAPUTO_SMOKE_PEER: process.env["NAPUTO_SMOKE_PEER"],
|
|
37
168
|
/** 运行时探测开关(=1 触发 probeRuntime)。 */
|
|
@@ -42,6 +173,293 @@ const env = {
|
|
|
42
173
|
NAPKETTO_DATA: process.env["NAPKETTO_DATA"]
|
|
43
174
|
};
|
|
44
175
|
//#endregion
|
|
176
|
+
//#region src/host/ipc/ipc-actions.ts
|
|
177
|
+
/**
|
|
178
|
+
* 从宽松 params 构造 Peer(缺省 chatType=1 群聊)。
|
|
179
|
+
* 优先 peerUid(uid 直通);缺省时 peerUin(QQ 号)经 uinToUid 转换。
|
|
180
|
+
*/
|
|
181
|
+
async function toPeer(params, uinToUid) {
|
|
182
|
+
const chatType = typeof params["chatType"] === "number" ? params["chatType"] : 1;
|
|
183
|
+
const uidParam = params["peerUid"];
|
|
184
|
+
if (typeof uidParam === "string" && uidParam !== "") return {
|
|
185
|
+
chatType,
|
|
186
|
+
peerUid: uidParam
|
|
187
|
+
};
|
|
188
|
+
const uinParam = params["peerUin"];
|
|
189
|
+
if (typeof uinParam === "string" && uinParam !== "" && uinToUid !== void 0) {
|
|
190
|
+
const uid = (await uinToUid([uinParam])).get(uinParam);
|
|
191
|
+
if (uid !== void 0 && uid !== "") return {
|
|
192
|
+
chatType,
|
|
193
|
+
peerUid: uid
|
|
194
|
+
};
|
|
195
|
+
throw new Error(`uin 转 uid 失败: ${uinParam}`);
|
|
196
|
+
}
|
|
197
|
+
throw new Error("缺 peerUid(或 peerUin 且未注入 uinToUid)");
|
|
198
|
+
}
|
|
199
|
+
/** 构造动作表(map 顺序即注册顺序,重复动作名后注册覆盖)。 */
|
|
200
|
+
function createIpcActions(ctx) {
|
|
201
|
+
const actions = /* @__PURE__ */ new Map();
|
|
202
|
+
actions.set("login.getSelf", async () => ctx.self);
|
|
203
|
+
actions.set("msg.sendMessage", async (params) => {
|
|
204
|
+
const peer = await toPeer(params, ctx.uinToUid);
|
|
205
|
+
const elements = Array.isArray(params["elements"]) ? params["elements"] : [];
|
|
206
|
+
return ctx.msgApi.sendMessage(peer, elements);
|
|
207
|
+
});
|
|
208
|
+
actions.set("msg.recallMessage", async (params) => {
|
|
209
|
+
const peer = await toPeer(params, ctx.uinToUid);
|
|
210
|
+
const msgIds = Array.isArray(params["msgIds"]) ? params["msgIds"].map(String) : [];
|
|
211
|
+
await ctx.msgApi.recallMessage(peer, msgIds);
|
|
212
|
+
});
|
|
213
|
+
actions.set("msg.fetchMessages", async (params) => {
|
|
214
|
+
const peer = await toPeer(params, ctx.uinToUid);
|
|
215
|
+
const count = typeof params["count"] === "number" ? params["count"] : 20;
|
|
216
|
+
const msgId = typeof params["msgId"] === "string" ? params["msgId"] : void 0;
|
|
217
|
+
return ctx.msgApi.fetchMessages(peer, {
|
|
218
|
+
count,
|
|
219
|
+
...msgId !== void 0 ? { msgId } : {}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
actions.set("msg.markRead", async (params) => {
|
|
223
|
+
const peer = await toPeer(params, ctx.uinToUid);
|
|
224
|
+
await ctx.msgApi.markRead(peer);
|
|
225
|
+
});
|
|
226
|
+
actions.set("group.getGroupList", async (params) => {
|
|
227
|
+
const force = params["force"] === true;
|
|
228
|
+
return ctx.groupApi.getGroupList(force);
|
|
229
|
+
});
|
|
230
|
+
actions.set("friend.getFriendList", async () => ctx.friendApi.getFriendList());
|
|
231
|
+
return actions;
|
|
232
|
+
}
|
|
233
|
+
/** 统一动作调用:查找动作表 → 执行 → 返回结果或错误(不抛,调用方转 result)。 */
|
|
234
|
+
async function callIpcAction(actions, action, params) {
|
|
235
|
+
const handler = actions.get(action);
|
|
236
|
+
if (handler === void 0) return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: {
|
|
239
|
+
code: "NOT_FOUND",
|
|
240
|
+
message: `未知动作: ${action}`
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
value: await handler(params ?? {})
|
|
247
|
+
};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: errorCodeOf(err),
|
|
254
|
+
message
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** 宽松提取错误码(KernelError 有 string code 字段;其他错误 UNKNOWN)。 */
|
|
260
|
+
function errorCodeOf(err) {
|
|
261
|
+
if (typeof err === "object" && err !== null) {
|
|
262
|
+
const code = err.code;
|
|
263
|
+
if (typeof code === "string" && code !== "") return code;
|
|
264
|
+
}
|
|
265
|
+
return "UNKNOWN";
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/host/ipc/ipc-types.ts
|
|
269
|
+
/** 消息类型集合(解码校验用)。 */
|
|
270
|
+
const IPC_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
271
|
+
"status",
|
|
272
|
+
"login",
|
|
273
|
+
"qr",
|
|
274
|
+
"event",
|
|
275
|
+
"result",
|
|
276
|
+
"log",
|
|
277
|
+
"ping",
|
|
278
|
+
"pong",
|
|
279
|
+
"action",
|
|
280
|
+
"control"
|
|
281
|
+
]);
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/host/ipc/ipc-sender.ts
|
|
284
|
+
/**
|
|
285
|
+
* ipc-sender.ts:IPC 消息发送封装(self-host 子进程 → koishi 插件)。
|
|
286
|
+
*
|
|
287
|
+
* 仅 NAPUTO_IPC=1 启用:stdout 专用于 JSON 行协议(stdin 收 action/control)。
|
|
288
|
+
* 非 IPC 模式(cli pnpm start)stdout 不输出协议行,零干扰。
|
|
289
|
+
*/
|
|
290
|
+
/** IPC 模式开关(self-host.ts 在 env.NAPUTO_IPC=1 时 enable)。 */
|
|
291
|
+
let enabled = false;
|
|
292
|
+
/** 启用 IPC 发送(幂等)。 */
|
|
293
|
+
function enableIpc() {
|
|
294
|
+
enabled = true;
|
|
295
|
+
}
|
|
296
|
+
/** 发送一条 IPC 消息(JSON 行)。失败静默(不阻塞引导)。 */
|
|
297
|
+
function sendIpc(message) {
|
|
298
|
+
if (!enabled) return;
|
|
299
|
+
try {
|
|
300
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
301
|
+
} catch {}
|
|
302
|
+
}
|
|
303
|
+
/** 引导状态(booting/dlopening/logging/sessioning/ready/failed)。 */
|
|
304
|
+
function sendStatus(phase, message, error) {
|
|
305
|
+
sendIpc({
|
|
306
|
+
v: 1,
|
|
307
|
+
type: "status",
|
|
308
|
+
payload: {
|
|
309
|
+
phase,
|
|
310
|
+
...message !== void 0 ? { message } : {},
|
|
311
|
+
...error !== void 0 ? { error } : {}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** 登录状态(QR 状态机 idle/waiting_scan/scanned/logged_in/failed)。 */
|
|
316
|
+
function sendLogin(state, selfInfo) {
|
|
317
|
+
sendIpc({
|
|
318
|
+
v: 1,
|
|
319
|
+
type: "login",
|
|
320
|
+
payload: {
|
|
321
|
+
state,
|
|
322
|
+
...selfInfo !== void 0 ? { selfInfo } : {}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/** 二维码数据(扫码登录展示用)。 */
|
|
327
|
+
function sendQr(pngBase64, qrcodeUrl) {
|
|
328
|
+
sendIpc({
|
|
329
|
+
v: 1,
|
|
330
|
+
type: "qr",
|
|
331
|
+
payload: {
|
|
332
|
+
pngBase64,
|
|
333
|
+
qrcodeUrl
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** kernel 事件转发(service/name/args 原样透传,koishi 插件翻译层消费)。 */
|
|
338
|
+
function sendEvent(service, name, args) {
|
|
339
|
+
sendIpc({
|
|
340
|
+
v: 1,
|
|
341
|
+
type: "event",
|
|
342
|
+
payload: {
|
|
343
|
+
service,
|
|
344
|
+
name,
|
|
345
|
+
args
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/** 动作响应。 */
|
|
350
|
+
function sendResult(id, payload) {
|
|
351
|
+
sendIpc({
|
|
352
|
+
v: 1,
|
|
353
|
+
type: "result",
|
|
354
|
+
id,
|
|
355
|
+
payload
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** 心跳应答(收到 ping 时回)。 */
|
|
359
|
+
function sendPong() {
|
|
360
|
+
sendIpc({
|
|
361
|
+
v: 1,
|
|
362
|
+
type: "pong"
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/** 心跳(定期发送,koishi 插件探活)。 */
|
|
366
|
+
function sendPing() {
|
|
367
|
+
sendIpc({
|
|
368
|
+
v: 1,
|
|
369
|
+
type: "ping"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/host/ipc/ipc-codec.ts
|
|
374
|
+
/**
|
|
375
|
+
* ipc-codec.ts:IPC 消息编解码(JSON 行协议,与 koishi 插件侧 src/ipc/codec.ts 对齐)。
|
|
376
|
+
*
|
|
377
|
+
* 每行一条 JSON 消息,`\n` 结尾。解码宽松:非法行 / 空行 / v 不匹配 /
|
|
378
|
+
* 未知 type → null,调用方忽略,不崩通道。
|
|
379
|
+
*/
|
|
380
|
+
/** 类型守卫:v 匹配 + type 合法。 */
|
|
381
|
+
function isIpcMessage(value) {
|
|
382
|
+
if (typeof value !== "object" || value === null) return false;
|
|
383
|
+
const record = value;
|
|
384
|
+
return record["v"] === 1 && typeof record["type"] === "string" && IPC_MESSAGE_TYPES.has(record["type"]);
|
|
385
|
+
}
|
|
386
|
+
/** 解码一行 JSON;非法行/空行返回 null。 */
|
|
387
|
+
function decodeIpcMessage(line) {
|
|
388
|
+
const trimmed = line.trim();
|
|
389
|
+
if (trimmed === "") return null;
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(trimmed);
|
|
392
|
+
return isIpcMessage(parsed) ? parsed : null;
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** 启动 IPC 服务端(stdin 接收 + 心跳)。返回停止函数。 */
|
|
398
|
+
function startIpcServer(options) {
|
|
399
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
400
|
+
const onExit = options.onExit ?? (() => process.exit(0));
|
|
401
|
+
const rl = (0, node_readline.createInterface)({
|
|
402
|
+
input: process.stdin,
|
|
403
|
+
crlfDelay: Infinity
|
|
404
|
+
});
|
|
405
|
+
rl.on("line", (line) => {
|
|
406
|
+
const message = decodeIpcMessage(line);
|
|
407
|
+
if (message === null) return;
|
|
408
|
+
switch (message.type) {
|
|
409
|
+
case "action":
|
|
410
|
+
handleAction(message.id, message.payload.action, message.payload.params, options.actions);
|
|
411
|
+
break;
|
|
412
|
+
case "control":
|
|
413
|
+
handleControl(message.payload, onExit);
|
|
414
|
+
break;
|
|
415
|
+
case "ping": sendPong();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
const timer = setInterval(() => sendPing(), heartbeatMs);
|
|
419
|
+
return () => {
|
|
420
|
+
clearInterval(timer);
|
|
421
|
+
rl.close();
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/** 动作执行 → result 响应(不抛:callIpcAction 内部兜底转错误)。 */
|
|
425
|
+
async function handleAction(id, action, params, actions) {
|
|
426
|
+
sendResult(id, await callIpcAction(actions, action, params));
|
|
427
|
+
}
|
|
428
|
+
/** 控制指令处理(stop/restart 退出;login 预留——引导期登录由 bootstrap 内部驱动)。 */
|
|
429
|
+
function handleControl(payload, onExit) {
|
|
430
|
+
if (payload.command === "stop" || payload.command === "restart") onExit();
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/host/ipc/ipc-bootstrap.ts
|
|
434
|
+
/** 订阅事件通道 → event 消息转发(事件名 "Service/method" 拆分)。 */
|
|
435
|
+
function forwardChannel(channel, stops) {
|
|
436
|
+
if (typeof channel.onAny !== "function") return;
|
|
437
|
+
const off = channel.onAny((event, ...args) => {
|
|
438
|
+
const slash = event.indexOf("/");
|
|
439
|
+
sendEvent(slash >= 0 ? event.slice(0, slash) : event, slash >= 0 ? event.slice(slash + 1) : "", args);
|
|
440
|
+
});
|
|
441
|
+
if (typeof off === "function") stops.push(off);
|
|
442
|
+
}
|
|
443
|
+
/** IPC 模式装配(enableIpc + 动作表 + 服务端 + 事件转发)。返回停止函数。 */
|
|
444
|
+
function startIpcMode(services) {
|
|
445
|
+
enableIpc();
|
|
446
|
+
const groupApi = services.groupApi;
|
|
447
|
+
const stopServer = startIpcServer({ actions: createIpcActions({
|
|
448
|
+
msgApi: services.msgApi,
|
|
449
|
+
groupApi: services.groupApi,
|
|
450
|
+
friendApi: services.friendApi,
|
|
451
|
+
self: services.self,
|
|
452
|
+
...groupApi.uinToUid !== void 0 ? { uinToUid: groupApi.uinToUid } : {}
|
|
453
|
+
}) });
|
|
454
|
+
const stops = [];
|
|
455
|
+
forwardChannel(services.channel, stops);
|
|
456
|
+
forwardChannel(services.groupChannel, stops);
|
|
457
|
+
return () => {
|
|
458
|
+
stopServer();
|
|
459
|
+
for (const stop of stops) stop();
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
45
463
|
//#region src/host/util.ts
|
|
46
464
|
/**
|
|
47
465
|
* util.ts:引导公共工具(日志 + 共享状态)。
|
|
@@ -60,6 +478,17 @@ function log(msg) {
|
|
|
60
478
|
function errMsg(e) {
|
|
61
479
|
return `${e?.message ?? e}`;
|
|
62
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* 逐条处理消息回调参数(onRecvMsg 为消息数组——2026-08-07 运行时实证;
|
|
483
|
+
* 兼容单条对象,过滤无效项)。
|
|
484
|
+
*/
|
|
485
|
+
function forEachRawMessage(msgs, handler) {
|
|
486
|
+
const list = Array.isArray(msgs) ? msgs : [msgs];
|
|
487
|
+
for (const msg of list) {
|
|
488
|
+
if (!msg || typeof msg !== "object") continue;
|
|
489
|
+
handler(msg);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
63
492
|
function createState() {
|
|
64
493
|
return {
|
|
65
494
|
wrapperExports: null,
|
|
@@ -69,7 +498,7 @@ function createState() {
|
|
|
69
498
|
};
|
|
70
499
|
}
|
|
71
500
|
//#endregion
|
|
72
|
-
//#region src/host/login.ts
|
|
501
|
+
//#region src/host/core/login.ts
|
|
73
502
|
/** 打印可用快速登录账号 + 返回目标账号(启动横幅)。forcedUin(cli `-q` / NAPUTO_QUICK_UIN)优先。 */
|
|
74
503
|
async function pickLoginAccount(kernel, ctx, loginResultRef, forcedUin) {
|
|
75
504
|
try {
|
|
@@ -112,32 +541,56 @@ async function doLogin(core, opts) {
|
|
|
112
541
|
}
|
|
113
542
|
}
|
|
114
543
|
//#endregion
|
|
115
|
-
//#region src/host/
|
|
544
|
+
//#region src/host/load-config.ts
|
|
116
545
|
/**
|
|
117
|
-
* protocols.ts
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
546
|
+
* 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化;2026-08-08 结构拍板:按账号取段)
|
|
547
|
+
*
|
|
548
|
+
* 全局 TOML 配置(<项目根>/napuketto.toml):**协议配置嵌在账号内**——
|
|
549
|
+
* accounts 数组每项含 [onebot11] / [satori] 段,登录成功后按账号 uin 找到对应项,
|
|
550
|
+
* 取其段作 seed(zod 校验)。路径优先级:NAPKETTO_CONFIG(launcher 注入)>
|
|
551
|
+
* 装配链自身探测(kernel.resolveConfigPath)> NAPUTO_CFG_DIR 兜底(旧行为兼容)。
|
|
552
|
+
* (ConfigBase seed 模式:load() 直接用内存值,不再读写独立协议文件)
|
|
121
553
|
*/
|
|
554
|
+
/** 账号内取协议段(宽松对象,装配时 zod 校验)。 */
|
|
555
|
+
function sectionOf(account, key) {
|
|
556
|
+
const section = account[key];
|
|
557
|
+
if (section !== void 0 && section !== null && typeof section === "object") return section;
|
|
558
|
+
}
|
|
122
559
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* 仅 console 版嵌入;boot 文件日志用纯文本版(log),不污染文件。
|
|
560
|
+
* 按登录账号 uin 读取全局 TOML 的协议段(读取失败 / 未找到账号 / 无协议段 → 空段,
|
|
561
|
+
* 不阻塞装配)。uin 为登录成功后实际账号(-q 指定账号与配置不一致时以 uin 为准)。
|
|
126
562
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
563
|
+
function loadProtocolSections(kernel, uin) {
|
|
564
|
+
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
565
|
+
let ob11Section = {};
|
|
566
|
+
let satoriSection = {};
|
|
567
|
+
try {
|
|
568
|
+
const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
|
|
569
|
+
const accounts = kernel.parseToml(raw)["accounts"];
|
|
570
|
+
if (Array.isArray(accounts)) {
|
|
571
|
+
const mine = accounts.find((item) => typeof item === "object" && item !== null && String(item["qq"]) === uin);
|
|
572
|
+
if (mine !== void 0) {
|
|
573
|
+
ob11Section = sectionOf(mine, "onebot11") ?? {};
|
|
574
|
+
satoriSection = sectionOf(mine, "satori") ?? {};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} catch (e) {
|
|
578
|
+
log(`bootstrap: 全局配置读取失败(用默认 ob11/satori 配置): ${errMsg(e)}`);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
cfgFile,
|
|
582
|
+
ob11Section,
|
|
583
|
+
satoriSection
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/host/core/assemble-protocols.ts
|
|
133
588
|
/**
|
|
134
|
-
*
|
|
589
|
+
* assemble-protocols.ts:OB11 / Satori 协议装配(非 IPC 模式,cli pnpm start)。
|
|
590
|
+
* 2026-08-08:从 protocols.ts 拆分——protocols.ts 只留入口 + IPC 分支。
|
|
135
591
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console: true,
|
|
139
|
-
base: { name: "loader" }
|
|
140
|
-
});
|
|
592
|
+
/** 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。 */
|
|
593
|
+
async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
141
594
|
const adapterEntry = env.NAPUTO_ADAPTER_ENTRY;
|
|
142
595
|
const networkEntry = env.NAPUTO_NETWORK_ENTRY;
|
|
143
596
|
if (!adapterEntry || !networkEntry) {
|
|
@@ -147,80 +600,14 @@ async function startProtocols(kernel, ctx, loginResult) {
|
|
|
147
600
|
try {
|
|
148
601
|
const network = await import(`file://${networkEntry.replace(/\\/g, "/")}`);
|
|
149
602
|
const onebot11Entry = adapterEntry.replace(/index\.mjs$/, "onebot11/index.mjs");
|
|
603
|
+
const satoriEntry = adapterEntry.replace(/index\.mjs$/, "satori/index.mjs");
|
|
150
604
|
const coreEntry = adapterEntry.replace(/index\.mjs$/, "core/index.mjs");
|
|
151
605
|
const adapter = await import(`file://${onebot11Entry.replace(/\\/g, "/")}`);
|
|
606
|
+
const satoriAdapter = await import(`file://${satoriEntry.replace(/\\/g, "/")}`);
|
|
152
607
|
const adapterCore = await import(`file://${coreEntry.replace(/\\/g, "/")}`);
|
|
153
|
-
const
|
|
154
|
-
if (!session) {
|
|
155
|
-
log("bootstrap: session 为空,无法装配协议");
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const channel = new kernel.NTEventChannel("Msg");
|
|
159
|
-
new kernel.MsgBridge(session, channel).register();
|
|
160
|
-
channel.on("Msg/onRecvMsg", (msgs) => {
|
|
161
|
-
const list = Array.isArray(msgs) ? msgs : [msgs];
|
|
162
|
-
for (const msg of list) {
|
|
163
|
-
if (!msg || typeof msg !== "object") continue;
|
|
164
|
-
try {
|
|
165
|
-
let rendered = "";
|
|
166
|
-
for (const el of kernel.toCanonicalElements(msg)) if (el.type === "text") rendered += el.text;
|
|
167
|
-
else if (el.type === "at") {
|
|
168
|
-
const name = el.display ?? (el.target === "all" ? "全体" : el.target);
|
|
169
|
-
rendered += `@${name}`;
|
|
170
|
-
} else if (el.type === "face") rendered += "[表情]";
|
|
171
|
-
else if (el.type === "image") rendered += "[图片]";
|
|
172
|
-
else if (el.type === "voice") rendered += "[语音]";
|
|
173
|
-
else if (el.type === "video") rendered += "[视频]";
|
|
174
|
-
else if (el.type === "reply") rendered += "[回复]";
|
|
175
|
-
else if (el.type === "file") rendered += `[文件${el.name ?? ""}]`;
|
|
176
|
-
else if (el.type === "forward") rendered += "[合并转发]";
|
|
177
|
-
else if (el.type === "json") rendered += "[json]";
|
|
178
|
-
else if (el.type === "xml") rendered += "[xml]";
|
|
179
|
-
if (rendered === "") rendered = "[空消息/媒体]";
|
|
180
|
-
const isGroup = msg.chatType === kernel.ChatType.GROUP;
|
|
181
|
-
const kind = isGroup ? "群聊" : "私聊";
|
|
182
|
-
const peerUin = String(msg.peerUin ?? "");
|
|
183
|
-
const senderUin = String(msg.senderUin ?? (peerUin || "未知"));
|
|
184
|
-
const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
|
|
185
|
-
const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
|
|
186
|
-
const content = rendered.replace(/\n/g, "\n ");
|
|
187
|
-
const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
|
|
188
|
-
const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
|
|
189
|
-
log(plain);
|
|
190
|
-
logger?.info(colored);
|
|
191
|
-
} catch (e) {
|
|
192
|
-
const line2 = `接收消息(解析失败: ${errMsg(e)})`;
|
|
193
|
-
log(line2);
|
|
194
|
-
logger?.warn(line2);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
const groupApi = new kernel.GroupApi(session);
|
|
199
|
-
const msgApi = new kernel.MsgApi(session);
|
|
200
|
-
const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
|
|
201
|
-
const groupChannel = new kernel.NTEventChannel("Group");
|
|
202
|
-
new kernel.GroupBridge(session, groupChannel).register();
|
|
203
|
-
const groupCache = new kernel.GroupCache({
|
|
204
|
-
channel: groupChannel,
|
|
205
|
-
groupApi
|
|
206
|
-
});
|
|
207
|
-
groupCache.register();
|
|
208
|
-
const groupNotifyApi = new kernel.GroupNotifyApi(session);
|
|
209
|
-
const ticketApi = new kernel.TicketApi(session);
|
|
210
|
-
const richMediaApi = new kernel.RichMediaApi(session);
|
|
211
|
-
const profileApi = new kernel.ProfileApi(session);
|
|
212
|
-
const profileLikeApi = new kernel.ProfileLikeApi(session);
|
|
213
|
-
const webApi = new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) });
|
|
608
|
+
const { channel, groupCache } = services;
|
|
214
609
|
const broadcaster = new network.EventBroadcaster();
|
|
215
|
-
|
|
216
|
-
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
217
|
-
try {
|
|
218
|
-
const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
|
|
219
|
-
const parsed = kernel.parseToml(raw);
|
|
220
|
-
if (parsed && typeof parsed["onebot11"] === "object" && parsed["onebot11"] !== null) ob11Section = parsed["onebot11"];
|
|
221
|
-
} catch (e) {
|
|
222
|
-
log(`bootstrap: 全局配置读取失败(用默认 ob11 配置): ${errMsg(e)}`);
|
|
223
|
-
}
|
|
610
|
+
const { cfgFile, ob11Section, satoriSection } = loadProtocolSections(kernel, loginResult.uin);
|
|
224
611
|
const ob11Config = new adapterCore.ProtocolConfig({
|
|
225
612
|
path: cfgFile,
|
|
226
613
|
schema: adapter.ob11ConfigSchema,
|
|
@@ -231,19 +618,16 @@ async function startProtocols(kernel, ctx, loginResult) {
|
|
|
231
618
|
config: ob11Config,
|
|
232
619
|
broadcaster,
|
|
233
620
|
msgChannel: channel,
|
|
234
|
-
msgApi,
|
|
235
|
-
groupApi,
|
|
236
|
-
groupNotifyApi,
|
|
237
|
-
friendApi,
|
|
238
|
-
ticketApi,
|
|
239
|
-
richMediaApi,
|
|
240
|
-
profileApi,
|
|
241
|
-
profileLikeApi,
|
|
242
|
-
webApi,
|
|
243
|
-
self:
|
|
244
|
-
uin: loginResult.uin,
|
|
245
|
-
nickname: loginResult.nick
|
|
246
|
-
},
|
|
621
|
+
msgApi: services.msgApi,
|
|
622
|
+
groupApi: services.groupApi,
|
|
623
|
+
groupNotifyApi: services.groupNotifyApi,
|
|
624
|
+
friendApi: services.friendApi,
|
|
625
|
+
ticketApi: services.ticketApi,
|
|
626
|
+
richMediaApi: services.richMediaApi,
|
|
627
|
+
profileApi: services.profileApi,
|
|
628
|
+
profileLikeApi: services.profileLikeApi,
|
|
629
|
+
webApi: services.webApi,
|
|
630
|
+
self: services.self,
|
|
247
631
|
system: {
|
|
248
632
|
appVersion: env.NAPUTO_QQ_VERSION || "unknown",
|
|
249
633
|
cleanCache: async () => {
|
|
@@ -265,12 +649,168 @@ async function startProtocols(kernel, ctx, loginResult) {
|
|
|
265
649
|
groupCache
|
|
266
650
|
}).start();
|
|
267
651
|
log("bootstrap: onebot11 adapter started");
|
|
652
|
+
const satoriCacheDir = typeof satoriSection["cacheDir"] === "string" && satoriSection["cacheDir"] !== "" ? satoriSection["cacheDir"] : (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "cache");
|
|
653
|
+
const satoriConfig = new adapterCore.ProtocolConfig({
|
|
654
|
+
path: cfgFile,
|
|
655
|
+
schema: satoriAdapter.satoriConfigSchema,
|
|
656
|
+
defaults: satoriAdapter.satoriConfigSchema.parse({}),
|
|
657
|
+
seed: satoriAdapter.satoriConfigSchema.parse(satoriSection)
|
|
658
|
+
});
|
|
659
|
+
await new satoriAdapter.NapukettoSatoriAdapter({
|
|
660
|
+
config: satoriConfig,
|
|
661
|
+
broadcaster,
|
|
662
|
+
msgChannel: channel,
|
|
663
|
+
msgApi: services.msgApi,
|
|
664
|
+
groupApi: services.groupApi,
|
|
665
|
+
groupNotifyApi: services.groupNotifyApi,
|
|
666
|
+
friendApi: services.friendApi,
|
|
667
|
+
profileApi: services.profileApi,
|
|
668
|
+
self: services.self,
|
|
669
|
+
cacheDir: satoriCacheDir,
|
|
670
|
+
groupCache
|
|
671
|
+
}).start();
|
|
672
|
+
log("bootstrap: satori adapter started");
|
|
673
|
+
} catch (e) {
|
|
674
|
+
log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/host/msg-log.ts
|
|
679
|
+
/** 终端 ANSI 颜色(消息日志颜色分层,2026-08-07 用户定稿)。 */
|
|
680
|
+
const ANSI = {
|
|
681
|
+
gray: "\x1B[90m",
|
|
682
|
+
cyan: "\x1B[36m",
|
|
683
|
+
green: "\x1B[32m",
|
|
684
|
+
reset: "\x1B[0m"
|
|
685
|
+
};
|
|
686
|
+
/** canonical 元素 → 日志占位文本。 */
|
|
687
|
+
function elementToText(el) {
|
|
688
|
+
if (el.type === "text") return el.text ?? "";
|
|
689
|
+
if (el.type === "at") return `@${el.display ?? (el.target === "all" ? "全体" : el.target)}`;
|
|
690
|
+
if (el.type === "file") return `[文件${el.name ?? ""}]`;
|
|
691
|
+
if (el.type === "forward") return "[合并转发]";
|
|
692
|
+
return `[${el.type}]`;
|
|
693
|
+
}
|
|
694
|
+
/** 渲染消息为单行文本(text 拼接,at/媒体元素转占位)。 */
|
|
695
|
+
function renderMessage(kernel, msg) {
|
|
696
|
+
let rendered = "";
|
|
697
|
+
for (const el of kernel.toCanonicalElements(msg)) rendered += elementToText(el);
|
|
698
|
+
return rendered === "" ? "[空消息/媒体]" : rendered;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* 消息日志订阅(onRecvMsg 回调参数为消息数组——2026-08-07 运行时实证,遍历逐条打印)。
|
|
702
|
+
*/
|
|
703
|
+
function setupMsgLogging(kernel, channel, logger) {
|
|
704
|
+
channel.on("Msg/onRecvMsg", (msgs) => {
|
|
705
|
+
forEachRawMessage(msgs, (m) => {
|
|
706
|
+
try {
|
|
707
|
+
const raw = m;
|
|
708
|
+
const rendered = renderMessage(kernel, m);
|
|
709
|
+
const isGroup = raw.chatType === kernel.ChatType.GROUP;
|
|
710
|
+
const kind = isGroup ? "群聊" : "私聊";
|
|
711
|
+
const peerUin = String(raw.peerUin ?? "");
|
|
712
|
+
const senderUin = String(raw.senderUin ?? (peerUin || "未知"));
|
|
713
|
+
const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
|
|
714
|
+
const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
|
|
715
|
+
const content = rendered.replace(/\n/g, "\n ");
|
|
716
|
+
const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
|
|
717
|
+
const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
|
|
718
|
+
log(plain);
|
|
719
|
+
logger?.info(colored);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
const line2 = `接收消息(解析失败: ${errMsg(e)})`;
|
|
722
|
+
log(line2);
|
|
723
|
+
logger?.warn(line2);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/host/core/kernel-services.ts
|
|
730
|
+
/**
|
|
731
|
+
* kernel-services.ts:kernel 业务服务装配(IPC 模式与协议模式共用)。
|
|
732
|
+
*
|
|
733
|
+
* 登录成功后创建:消息/群事件通道 + 桥 + 缓存 + kernel 各 Api + 自身信息。
|
|
734
|
+
* IPC 模式(koishi 插件)经此拿服务装配 ipc-server;非 IPC(cli)经此拿服务
|
|
735
|
+
* 装配 OB11/Satori 适配器。
|
|
736
|
+
*/
|
|
737
|
+
/** 登录成功后创建 kernel 业务服务(channel/bridge/cache/apis)。失败返回 null。 */
|
|
738
|
+
async function createKernelServices(kernel, ctx, loginResult) {
|
|
739
|
+
const logger = kernel.createLogger?.({
|
|
740
|
+
console: true,
|
|
741
|
+
base: { name: "loader" }
|
|
742
|
+
});
|
|
743
|
+
const session = ctx.session;
|
|
744
|
+
if (!session) {
|
|
745
|
+
log("bootstrap: session 为空,无法创建 kernel 服务");
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
const channel = new kernel.NTEventChannel("Msg");
|
|
749
|
+
new kernel.MsgBridge(session, channel).register();
|
|
750
|
+
setupMsgLogging(kernel, channel, logger);
|
|
751
|
+
const groupApi = new kernel.GroupApi(session);
|
|
752
|
+
const msgApi = new kernel.MsgApi(session);
|
|
753
|
+
const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
|
|
754
|
+
const groupChannel = new kernel.NTEventChannel("Group");
|
|
755
|
+
new kernel.GroupBridge(session, groupChannel).register();
|
|
756
|
+
const groupCache = new kernel.GroupCache({
|
|
757
|
+
channel: groupChannel,
|
|
758
|
+
groupApi
|
|
759
|
+
});
|
|
760
|
+
groupCache.register();
|
|
761
|
+
const groupNotifyApi = new kernel.GroupNotifyApi(session);
|
|
762
|
+
const ticketApi = new kernel.TicketApi(session);
|
|
763
|
+
return {
|
|
764
|
+
kernel,
|
|
765
|
+
ctx,
|
|
766
|
+
logger,
|
|
767
|
+
channel,
|
|
768
|
+
groupChannel,
|
|
769
|
+
msgApi,
|
|
770
|
+
groupApi,
|
|
771
|
+
friendApi,
|
|
772
|
+
groupCache,
|
|
773
|
+
groupNotifyApi,
|
|
774
|
+
ticketApi,
|
|
775
|
+
richMediaApi: new kernel.RichMediaApi(session),
|
|
776
|
+
profileApi: new kernel.ProfileApi(session),
|
|
777
|
+
profileLikeApi: new kernel.ProfileLikeApi(session),
|
|
778
|
+
webApi: new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) }),
|
|
779
|
+
self: {
|
|
780
|
+
uin: loginResult.uin,
|
|
781
|
+
nickname: loginResult.nick ?? ""
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/host/core/protocols.ts
|
|
787
|
+
/**
|
|
788
|
+
* protocols.ts:协议装配入口(登录成功后)。
|
|
789
|
+
* 2026-08-08 重构:kernel 服务创建 → kernel-services.ts;OB11/Satori 装配 →
|
|
790
|
+
* assemble-protocols.ts。本文件只留入口 + IPC 模式分支。
|
|
791
|
+
*
|
|
792
|
+
* 分支:
|
|
793
|
+
* - IPC 模式(NAPUTO_IPC=1,koishi 插件驱动):跳过 OB11/Satori(插件直接消费
|
|
794
|
+
* kernel 服务),返回 KernelServices 由 bootstrap 装配 ipc-server。
|
|
795
|
+
* - 非 IPC(cli pnpm start):照旧装配 OB11/Satori 网络传输。
|
|
796
|
+
*/
|
|
797
|
+
/** 登录成功后装配协议:IPC 模式返回 kernel 服务(供 ipc-server),否则装配 OB11/Satori。 */
|
|
798
|
+
async function startProtocols(kernel, ctx, loginResult) {
|
|
799
|
+
const services = await createKernelServices(kernel, ctx, loginResult);
|
|
800
|
+
if (services === null) return null;
|
|
801
|
+
if (env.NAPUTO_IPC === "1") {
|
|
802
|
+
log("bootstrap: IPC 模式,跳过 OB11/Satori 装配(koishi 插件直接消费 kernel 服务)");
|
|
803
|
+
return services;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
await assembleOb11AndSatori(kernel, services, loginResult);
|
|
268
807
|
} catch (e) {
|
|
269
808
|
log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
|
|
270
809
|
}
|
|
810
|
+
return services;
|
|
271
811
|
}
|
|
272
812
|
//#endregion
|
|
273
|
-
//#region src/host/session.ts
|
|
813
|
+
//#region src/host/core/session.ts
|
|
274
814
|
/** 对象有效性判断:getMsgService() 调用不抛断言(cpp_impl 已激活)。
|
|
275
815
|
* 与 isSessionUsable 的区别:这里允许返回 null(未 init 但对象有效)。 */
|
|
276
816
|
function isSessionObjectValid(s) {
|
|
@@ -395,7 +935,7 @@ function pickBestSession(candidates) {
|
|
|
395
935
|
return bestValid;
|
|
396
936
|
}
|
|
397
937
|
//#endregion
|
|
398
|
-
//#region src/host/smoke.ts
|
|
938
|
+
//#region src/host/core/smoke.ts
|
|
399
939
|
/**
|
|
400
940
|
* smoke.ts:P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1 触发,2026-08-06)。
|
|
401
941
|
* 2026-08-07 阶段 2:由 runtime/boot-smoke.js TS 化(零语义改动)。
|
|
@@ -434,6 +974,10 @@ function parseSmokeTarget(envPeer, selfUin) {
|
|
|
434
974
|
uin: selfUin
|
|
435
975
|
};
|
|
436
976
|
}
|
|
977
|
+
/** 提取消息纯文本(text 元素拼接,onRecvMsg 与落库核对共用)。 */
|
|
978
|
+
function extractTexts(kernel, msg) {
|
|
979
|
+
return kernel.toCanonicalElements(msg).filter((el) => el.type === "text").map((el) => el.text).join("");
|
|
980
|
+
}
|
|
437
981
|
/** uin → uid(c2c 目标用;group 直接拿群号作 peerUid)。 */
|
|
438
982
|
async function resolvePeerUid(kernel, ctx, target, selfUid) {
|
|
439
983
|
const groupApi = new kernel.GroupApi(ctx.session);
|
|
@@ -471,18 +1015,16 @@ async function runSmokeTest(kernel, ctx, loginResult) {
|
|
|
471
1015
|
let received = 0;
|
|
472
1016
|
let receivedText = "";
|
|
473
1017
|
channel.on("Msg/onRecvMsg", (msgs) => {
|
|
474
|
-
|
|
475
|
-
for (const msg of list) {
|
|
476
|
-
if (!msg || typeof msg !== "object") continue;
|
|
1018
|
+
forEachRawMessage(msgs, (m) => {
|
|
477
1019
|
received += 1;
|
|
478
1020
|
try {
|
|
479
|
-
const texts = kernel
|
|
1021
|
+
const texts = extractTexts(kernel, m);
|
|
480
1022
|
if (texts) receivedText = texts;
|
|
481
1023
|
} catch (e) {
|
|
482
1024
|
log(`smoke: onRecvMsg 解析失败: ${errMsg(e)}`);
|
|
483
1025
|
}
|
|
484
|
-
log(`smoke: 📥 onRecvMsg 收到 #${received}(msgId=${
|
|
485
|
-
}
|
|
1026
|
+
log(`smoke: 📥 onRecvMsg 收到 #${received}(msgId=${m.msgId ?? "?"} seq=${m.msgSeq ?? "?"} text="${receivedText}")`);
|
|
1027
|
+
});
|
|
486
1028
|
});
|
|
487
1029
|
const msgApi = new kernel.MsgApi(session);
|
|
488
1030
|
const peer = await resolvePeerUid(kernel, ctx, target, loginResult.uid);
|
|
@@ -504,10 +1046,7 @@ async function runSmokeTest(kernel, ctx, loginResult) {
|
|
|
504
1046
|
const recent = await msgApi.fetchMessages(peer, { count: 10 });
|
|
505
1047
|
landed = recent.some((m) => m.msgId === sentMsgId || m.msgSeq === sentMsgId);
|
|
506
1048
|
log(`smoke: 落库验证: 最近 ${recent.length} 条${landed ? " ✅ 含刚发消息" : " ❌ 未找到(可能同步延迟)"}`);
|
|
507
|
-
if (!landed) for (const m of recent.slice(0, 3)) {
|
|
508
|
-
const texts = kernel.toCanonicalElements(m).filter((el) => el.type === "text").map((el) => el.text).join("");
|
|
509
|
-
log(`smoke: 最近消息 msgId=${m.msgId} seq=${m.msgSeq} text="${texts}"`);
|
|
510
|
-
}
|
|
1049
|
+
if (!landed) for (const m of recent.slice(0, 3)) log(`smoke: 最近消息 msgId=${m.msgId} seq=${m.msgSeq} text="${extractTexts(kernel, m)}"`);
|
|
511
1050
|
} catch (e) {
|
|
512
1051
|
log(`smoke: 落库验证失败: ${errMsg(e)}`);
|
|
513
1052
|
}
|
|
@@ -522,7 +1061,176 @@ async function runSmokeTest(kernel, ctx, loginResult) {
|
|
|
522
1061
|
return ok;
|
|
523
1062
|
}
|
|
524
1063
|
//#endregion
|
|
525
|
-
//#region src/host/bootstrap.ts
|
|
1064
|
+
//#region src/host/core/bootstrap-core.ts
|
|
1065
|
+
/**
|
|
1066
|
+
* bootstrap-core.ts:NapukettoCore 装配路径(从 bootstrap.ts 拆分,2026-08-08 FTA 优化)。
|
|
1067
|
+
*
|
|
1068
|
+
* NapukettoCore.create → attachWrapper → 登录(pickLoginAccount + doLogin)→
|
|
1069
|
+
* session 替换/激活 → 就绪等待 → 冒烟自检 → 协议装配 → 探测模式。
|
|
1070
|
+
*/
|
|
1071
|
+
/** 探测:create() 与捕获的 QQ session 关系(确认 create() 是否干扰 QQ)。 */
|
|
1072
|
+
function probeCreatedSession(state, ctx) {
|
|
1073
|
+
try {
|
|
1074
|
+
const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
|
|
1075
|
+
const created = S2 && typeof S2.create === "function" ? S2.create() : null;
|
|
1076
|
+
log(`BOOT: create()===qqSession? ${created === state.qqSession} | create()===ctx.session? ${created === ctx.session} | qqSession===ctx.session? ${state.qqSession === ctx.session}`);
|
|
1077
|
+
const sess = created;
|
|
1078
|
+
const svc = sess && typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
|
|
1079
|
+
const qs = state.qqSession;
|
|
1080
|
+
log(`BOOT: create().getMsgService=${svc !== null && svc !== void 0 ? "ready" : "null"} qqSession.getMsgService=${qs && typeof qs.getMsgService === "function" && qs.getMsgService() !== null && qs.getMsgService() !== void 0 ? "ready" : "null"}`);
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
log(`BOOT: create() 探测失败: ${errMsg(e)}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/** 登录状态字面量(IPC login 消息,与 kernel LoginState 对齐)。 */
|
|
1086
|
+
const LOGIN_STATES = [
|
|
1087
|
+
"idle",
|
|
1088
|
+
"waiting_scan",
|
|
1089
|
+
"scanned",
|
|
1090
|
+
"logged_in",
|
|
1091
|
+
"failed"
|
|
1092
|
+
];
|
|
1093
|
+
/** 宽松收窄(kernel onLoginProgress.state 是 string)。 */
|
|
1094
|
+
function isLoginState(value) {
|
|
1095
|
+
return LOGIN_STATES.includes(value);
|
|
1096
|
+
}
|
|
1097
|
+
/** 登录参数(NAPUTO_QUICK_UIN 强制指定 / ref 目标 / 默认)。 */
|
|
1098
|
+
function buildLoginOpts(Appid, forcedUin, ref) {
|
|
1099
|
+
const quickUin = forcedUin ?? ref.targetUin;
|
|
1100
|
+
const opts = {
|
|
1101
|
+
appid: Appid,
|
|
1102
|
+
initTimeoutMs: 2e4,
|
|
1103
|
+
...quickUin !== void 0 ? { quickUin } : {}
|
|
1104
|
+
};
|
|
1105
|
+
if (env.NAPUTO_IPC === "1") opts["onLoginProgress"] = (progress) => {
|
|
1106
|
+
if (progress.qr !== void 0) sendQr(progress.qr.pngBase64, progress.qr.qrcodeUrl);
|
|
1107
|
+
if (isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo);
|
|
1108
|
+
};
|
|
1109
|
+
return opts;
|
|
1110
|
+
}
|
|
1111
|
+
/** V2 登录后替换 session:优先 QQ 主 session,其次 vehicle 单例表;自建宿主例外。 */
|
|
1112
|
+
function replaceSession(kernel, core, state, ctx) {
|
|
1113
|
+
if (env.NAPUTO_SELF_HOST === "1") return null;
|
|
1114
|
+
const chosen = pickBestSession(collectCandidateSessions(state, kernel, ctx));
|
|
1115
|
+
if (chosen && chosen.s !== ctx.session) {
|
|
1116
|
+
core.setSession(chosen.s);
|
|
1117
|
+
log(`bootstrap: 已替换 session(来源=${chosen.tag},msgSvc=${isSessionUsable(chosen.s) ? "READY" : "null"})`);
|
|
1118
|
+
} else if (chosen) log(`bootstrap: 候选 session 与 ctx.session 相同(${chosen.tag})`);
|
|
1119
|
+
else log("bootstrap: 无有效候选 session(保留 kernel 自建 session)");
|
|
1120
|
+
return chosen;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* 激活目标 session(先 init 后 startupSession.start,HANDOVER-V6 三要素)。
|
|
1124
|
+
* 自建宿主无候选时对 kernel 自建 session 走 NapCat 激活。
|
|
1125
|
+
*/
|
|
1126
|
+
async function activateSession(kernel, state, ctx, chosen, loginResult, Appid, bootEnv) {
|
|
1127
|
+
const activateTarget = chosen ?? {
|
|
1128
|
+
s: ctx.session,
|
|
1129
|
+
tag: "kernel 自建 session"
|
|
1130
|
+
};
|
|
1131
|
+
if (!activateTarget.s || isSessionUsable(activateTarget.s)) return;
|
|
1132
|
+
log(`bootstrap: 激活 session(${activateTarget.tag},先 init 后 startupSession.start)...`);
|
|
1133
|
+
try {
|
|
1134
|
+
const qqDataRoot = typeof kernel.resolveQqUserDataRoot === "function" ? kernel.resolveQqUserDataRoot(state.wrapperExports) : null;
|
|
1135
|
+
const accountPath = qqDataRoot && typeof kernel.resolveQqGlobalPath === "function" ? kernel.resolveQqGlobalPath(qqDataRoot) : bootEnv.dataDir || ".";
|
|
1136
|
+
const sessionConfig = kernel.buildSessionConfig({
|
|
1137
|
+
appid: Appid,
|
|
1138
|
+
fullVersion: bootEnv.qqVersion || "",
|
|
1139
|
+
selfUin: loginResult.uin,
|
|
1140
|
+
selfUid: loginResult.uid,
|
|
1141
|
+
accountPath,
|
|
1142
|
+
downloadPath: (0, node_path.join)(accountPath, "NapCat", "temp")
|
|
1143
|
+
});
|
|
1144
|
+
const listener = kernel.createLifecycleSessionListener();
|
|
1145
|
+
await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
|
|
1146
|
+
log("bootstrap: 激活 session init + start 完成");
|
|
1147
|
+
} catch (initErr) {
|
|
1148
|
+
log(`bootstrap: 激活 session init 失败: ${errMsg(initErr)}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/** 等 session 就绪(getMsgService 非 null)——init 完成后才有。 */
|
|
1152
|
+
async function waitSessionReady(kernel, ctx) {
|
|
1153
|
+
try {
|
|
1154
|
+
await kernel.waitSessionReady(ctx, { timeoutMs: 3e4 });
|
|
1155
|
+
log("bootstrap: QQ session 就绪(getMsgService 可用)");
|
|
1156
|
+
} catch (readyErr) {
|
|
1157
|
+
log(`bootstrap: 等待 session 就绪失败: ${errMsg(readyErr)}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/** P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1):MsgBridge + MsgApi 真发/收一条。 */
|
|
1161
|
+
async function runSmokeIfEnabled(kernel, ctx, loginResult) {
|
|
1162
|
+
if (env.NAPUTO_SMOKE !== "1") return;
|
|
1163
|
+
try {
|
|
1164
|
+
log(`bootstrap: 冒烟自检${await runSmokeTest(kernel, ctx, loginResult) ? "通过" : "未完全通过"}(见上文 smoke 日志)`);
|
|
1165
|
+
} catch (smokeErr) {
|
|
1166
|
+
log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/** 探测模式(NAPUTO_PROBE=1):立即探测 + 35s 后晚探测(napuketto-probe-late.json)。 */
|
|
1170
|
+
function runProbePhase(kernel, ctx) {
|
|
1171
|
+
if (env.NAPUTO_PROBE !== "1" || typeof kernel.probeRuntime !== "function") return;
|
|
1172
|
+
try {
|
|
1173
|
+
const probe = kernel.probeRuntime(ctx);
|
|
1174
|
+
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
1175
|
+
setTimeout(() => {
|
|
1176
|
+
try {
|
|
1177
|
+
if (typeof kernel.probeRuntime !== "function") return;
|
|
1178
|
+
const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
|
|
1179
|
+
log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
|
|
1180
|
+
} catch (e2) {
|
|
1181
|
+
log(`bootstrap: probe-late error: ${errMsg(e2)}`);
|
|
1182
|
+
}
|
|
1183
|
+
}, 35e3);
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/** NapukettoCore 装配路径:create → attachWrapper → 登录 → 激活 → 协议 → 探测。 */
|
|
1189
|
+
async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
|
|
1190
|
+
const coreCtor = kernel.NapukettoCore;
|
|
1191
|
+
if (coreCtor === void 0) return;
|
|
1192
|
+
const core = coreCtor.create({
|
|
1193
|
+
paths: { dataRoot: bootEnv.dataDir },
|
|
1194
|
+
logLevel: "info"
|
|
1195
|
+
});
|
|
1196
|
+
const ctx = core.attachWrapper(state.wrapperExports, { ...bootEnv });
|
|
1197
|
+
log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
|
|
1198
|
+
probeCreatedSession(state, ctx);
|
|
1199
|
+
startSessionProbe(state, ctx);
|
|
1200
|
+
probeSessionMethods(ctx);
|
|
1201
|
+
if (typeof core.login !== "function") {
|
|
1202
|
+
log("bootstrap: kernel core missing login fn");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const forcedUin = env.NAPUTO_QUICK_UIN;
|
|
1206
|
+
const ref = { targetUin: void 0 };
|
|
1207
|
+
await pickLoginAccount(kernel, ctx, ref, forcedUin);
|
|
1208
|
+
if (env.NAPUTO_IPC === "1") sendStatus("logging");
|
|
1209
|
+
const loginResult = await doLogin(core, buildLoginOpts(Appid, forcedUin, ref));
|
|
1210
|
+
if (loginResult === null) {
|
|
1211
|
+
log("bootstrap: 登录失败,引导中止");
|
|
1212
|
+
if (env.NAPUTO_IPC === "1") sendStatus("failed", "登录失败", {
|
|
1213
|
+
code: "NOT_LOGIN",
|
|
1214
|
+
message: "登录失败"
|
|
1215
|
+
});
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
log(`bootstrap: 登录成功 uin=${loginResult.uin} uid=${loginResult.uid} nick=${loginResult.nick}`);
|
|
1219
|
+
if (env.NAPUTO_IPC === "1") sendLogin("logged_in", {
|
|
1220
|
+
uin: loginResult.uin,
|
|
1221
|
+
uid: loginResult.uid,
|
|
1222
|
+
nick: loginResult.nick ?? ""
|
|
1223
|
+
});
|
|
1224
|
+
await activateSession(kernel, state, ctx, replaceSession(kernel, core, state, ctx), loginResult, Appid, bootEnv);
|
|
1225
|
+
await waitSessionReady(kernel, ctx);
|
|
1226
|
+
if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
|
|
1227
|
+
await runSmokeIfEnabled(kernel, ctx, loginResult);
|
|
1228
|
+
const services = await startProtocols(kernel, ctx, loginResult);
|
|
1229
|
+
if (env.NAPUTO_IPC === "1" && services !== null) startIpcMode(services);
|
|
1230
|
+
runProbePhase(kernel, ctx);
|
|
1231
|
+
}
|
|
1232
|
+
//#endregion
|
|
1233
|
+
//#region src/host/core/bootstrap.ts
|
|
526
1234
|
/**
|
|
527
1235
|
* bootstrap.ts:kernel 引导核心(import kernel → 装配 → 登录 → session 替换 → 协议装配)。
|
|
528
1236
|
* 2026-08-07 阶段 2:由 runtime/boot-bootstrap.js TS 化(零语义改动)。
|
|
@@ -532,6 +1240,7 @@ async function runSmokeTest(kernel, ctx, loginResult) {
|
|
|
532
1240
|
* - 登录流程(选账号 / 快速登录 / QR 回退)→ login.ts
|
|
533
1241
|
* - session 选择 / 探测 / 就绪 → session.ts
|
|
534
1242
|
* - 协议装配 → protocols.ts;冒烟自检 → smoke.ts
|
|
1243
|
+
* 2026-08-08 FTA 优化:NapukettoCore 装配路径 → bootstrap-core.ts(本文件留入口 + fallback)。
|
|
535
1244
|
*
|
|
536
1245
|
* session 来源(2026-08-07 实测结论;V1 Proxy 捕获与 vehicle 载具已归档 archive/):
|
|
537
1246
|
* 登录成功后按优先级取 session:
|
|
@@ -540,6 +1249,52 @@ async function runSmokeTest(kernel, ctx, loginResult) {
|
|
|
540
1249
|
* ② get() / qqSession(V1 兼容)
|
|
541
1250
|
* 取到后 core.setSession 替换 → waitSessionReady 确认 getMsgService READY。
|
|
542
1251
|
*/
|
|
1252
|
+
/** ⭐ appid 动态解析(2026-08-06 P2-0 实测:硬编码 537237765 在 9.9.33 扫码失败
|
|
1253
|
+
* 「请下载最新版」;major.node 解析的 537376818 成功)。自研,参考 NapCat 思路。 */
|
|
1254
|
+
function resolveAppid(kernel, bootEnv) {
|
|
1255
|
+
return typeof kernel.parseAppidFromMajor === "function" && bootEnv.wrapperPath && kernel.parseAppidFromMajor((0, node_path.join)((0, node_path.dirname)(bootEnv.wrapperPath), "major.node")) || typeof kernel.resolveAppidQua === "function" && kernel.resolveAppidQua(bootEnv.qqVersion || "").appid || "537237765";
|
|
1256
|
+
}
|
|
1257
|
+
/** 回退:旧装配路径(startNapuketto + 手工 lifecycle)。 */
|
|
1258
|
+
async function bootstrapFallback(kernel, state, bootEnv, Appid) {
|
|
1259
|
+
log("bootstrap: kernel has no NapukettoCore, falling back");
|
|
1260
|
+
const ctx = kernel.startNapuketto?.({
|
|
1261
|
+
wrapperExports: state.wrapperExports,
|
|
1262
|
+
env: { ...bootEnv }
|
|
1263
|
+
});
|
|
1264
|
+
if (ctx === void 0) {
|
|
1265
|
+
log("bootstrap: startNapuketto 不可用");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
|
|
1269
|
+
if (typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function") {
|
|
1270
|
+
if (typeof kernel.buildLoginConfig === "function" && ctx.loginService) {
|
|
1271
|
+
const loginCfg = kernel.buildLoginConfig(Appid, bootEnv.qqVersion || "", bootEnv.dataDir || ".");
|
|
1272
|
+
if (typeof ctx.loginService.initConfig === "function") {
|
|
1273
|
+
ctx.loginService.initConfig(loginCfg);
|
|
1274
|
+
log("bootstrap: loginService.initConfig OK");
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const loginResult = await kernel.quickLogin(ctx, {});
|
|
1278
|
+
log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
|
|
1279
|
+
const sessionConfig = kernel.buildSessionConfig({
|
|
1280
|
+
appid: Appid,
|
|
1281
|
+
fullVersion: bootEnv.qqVersion || "",
|
|
1282
|
+
selfUin: loginResult.uin,
|
|
1283
|
+
selfUid: loginResult.uid,
|
|
1284
|
+
accountPath: bootEnv.dataDir || ".",
|
|
1285
|
+
downloadPath: (0, node_path.join)(bootEnv.dataDir || ".", "temp")
|
|
1286
|
+
});
|
|
1287
|
+
const listener = kernel.createLifecycleSessionListener();
|
|
1288
|
+
await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
|
|
1289
|
+
log("bootstrap: session init + startNT OK!");
|
|
1290
|
+
} else log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
|
|
1291
|
+
if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
|
|
1292
|
+
const probe = kernel.probeRuntime(ctx);
|
|
1293
|
+
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
543
1298
|
/** 核心引导:import kernel → 装配 → 登录 → 替换 session → 等待就绪 → 协议装配。 */
|
|
544
1299
|
async function bootstrap(state) {
|
|
545
1300
|
const kernelEntry = env.NAPUTO_KERNEL_ENTRY;
|
|
@@ -555,148 +1310,15 @@ async function bootstrap(state) {
|
|
|
555
1310
|
return;
|
|
556
1311
|
}
|
|
557
1312
|
const bootEnv = {
|
|
558
|
-
qqVersion: env.NAPUTO_QQ_VERSION,
|
|
559
|
-
dataDir: env.NAPUTO_CFG_DIR,
|
|
560
|
-
wrapperPath: env.NAPUTO_WRAPPER_PATH
|
|
1313
|
+
qqVersion: env.NAPUTO_QQ_VERSION || "",
|
|
1314
|
+
dataDir: env.NAPUTO_CFG_DIR || "",
|
|
1315
|
+
wrapperPath: env.NAPUTO_WRAPPER_PATH || ""
|
|
561
1316
|
};
|
|
562
|
-
const Appid =
|
|
1317
|
+
const Appid = resolveAppid(kernel, bootEnv);
|
|
563
1318
|
log(`bootstrap: appid=${Appid}(wrapper=${bootEnv.wrapperPath})`);
|
|
564
1319
|
try {
|
|
565
|
-
if (kernel.NapukettoCore !== void 0)
|
|
566
|
-
|
|
567
|
-
paths: { dataRoot: bootEnv.dataDir },
|
|
568
|
-
logLevel: "info"
|
|
569
|
-
});
|
|
570
|
-
const ctx = core.attachWrapper(state.wrapperExports, bootEnv);
|
|
571
|
-
log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
|
|
572
|
-
try {
|
|
573
|
-
const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
|
|
574
|
-
const created = S2 && typeof S2.create === "function" ? S2.create() : null;
|
|
575
|
-
log(`BOOT: create()===qqSession? ${created === state.qqSession} | create()===ctx.session? ${created === ctx.session} | qqSession===ctx.session? ${state.qqSession === ctx.session}`);
|
|
576
|
-
const sess = created;
|
|
577
|
-
const svc = sess && typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
|
|
578
|
-
const qs = state.qqSession;
|
|
579
|
-
log(`BOOT: create().getMsgService=${svc !== null && svc !== void 0 ? "ready" : "null"} qqSession.getMsgService=${qs && typeof qs.getMsgService === "function" && qs.getMsgService() !== null && qs.getMsgService() !== void 0 ? "ready" : "null"}`);
|
|
580
|
-
} catch (e) {
|
|
581
|
-
log(`BOOT: create() 探测失败: ${errMsg(e)}`);
|
|
582
|
-
}
|
|
583
|
-
startSessionProbe(state, ctx);
|
|
584
|
-
probeSessionMethods(ctx);
|
|
585
|
-
let loginResult = null;
|
|
586
|
-
if (typeof core.login === "function") {
|
|
587
|
-
const forcedUin = env.NAPUTO_QUICK_UIN;
|
|
588
|
-
const ref = { targetUin: void 0 };
|
|
589
|
-
await pickLoginAccount(kernel, ctx, ref, forcedUin);
|
|
590
|
-
loginResult = await doLogin(core, {
|
|
591
|
-
appid: Appid,
|
|
592
|
-
initTimeoutMs: 2e4,
|
|
593
|
-
...forcedUin ? { quickUin: forcedUin } : ref.targetUin !== void 0 ? { quickUin: ref.targetUin } : {}
|
|
594
|
-
});
|
|
595
|
-
if (loginResult === null) {
|
|
596
|
-
log("bootstrap: 登录失败,引导中止");
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
log(`bootstrap: 登录成功 uin=${loginResult.uin} uid=${loginResult.uid} nick=${loginResult.nick}`);
|
|
600
|
-
const isSelfHost = env.NAPUTO_SELF_HOST === "1";
|
|
601
|
-
let chosen = null;
|
|
602
|
-
if (!isSelfHost) chosen = pickBestSession(collectCandidateSessions(state, kernel, ctx));
|
|
603
|
-
if (chosen && chosen.s !== ctx.session) {
|
|
604
|
-
core.setSession(chosen.s);
|
|
605
|
-
log(`bootstrap: 已替换 session(来源=${chosen.tag},msgSvc=${isSessionUsable(chosen.s) ? "READY" : "null"})`);
|
|
606
|
-
} else if (chosen) log(`bootstrap: 候选 session 与 ctx.session 相同(${chosen.tag})`);
|
|
607
|
-
else log("bootstrap: 无有效候选 session(保留 kernel 自建 session)");
|
|
608
|
-
const activateTarget = chosen ?? {
|
|
609
|
-
s: ctx.session,
|
|
610
|
-
tag: "kernel 自建 session"
|
|
611
|
-
};
|
|
612
|
-
if (activateTarget.s && !isSessionUsable(activateTarget.s)) {
|
|
613
|
-
log(`bootstrap: 激活 session(${activateTarget.tag},先 init 后 startupSession.start)...`);
|
|
614
|
-
try {
|
|
615
|
-
const qqDataRoot = typeof kernel.resolveQqUserDataRoot === "function" ? kernel.resolveQqUserDataRoot(state.wrapperExports) : null;
|
|
616
|
-
const accountPath = qqDataRoot && typeof kernel.resolveQqGlobalPath === "function" ? kernel.resolveQqGlobalPath(qqDataRoot) : bootEnv.dataDir || ".";
|
|
617
|
-
const sessionConfig = kernel.buildSessionConfig({
|
|
618
|
-
appid: Appid,
|
|
619
|
-
fullVersion: bootEnv.qqVersion || "",
|
|
620
|
-
selfUin: loginResult.uin,
|
|
621
|
-
selfUid: loginResult.uid,
|
|
622
|
-
accountPath,
|
|
623
|
-
downloadPath: (0, node_path.join)(accountPath, "NapCat", "temp")
|
|
624
|
-
});
|
|
625
|
-
const listener = kernel.createLifecycleSessionListener();
|
|
626
|
-
await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
|
|
627
|
-
log("bootstrap: 激活 session init + start 完成");
|
|
628
|
-
} catch (initErr) {
|
|
629
|
-
log(`bootstrap: 激活 session init 失败: ${errMsg(initErr)}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
try {
|
|
633
|
-
await kernel.waitSessionReady(ctx, { timeoutMs: 3e4 });
|
|
634
|
-
log("bootstrap: QQ session 就绪(getMsgService 可用)");
|
|
635
|
-
} catch (readyErr) {
|
|
636
|
-
log(`bootstrap: 等待 session 就绪失败: ${errMsg(readyErr)}`);
|
|
637
|
-
}
|
|
638
|
-
if (env.NAPUTO_SMOKE === "1" && loginResult !== null) try {
|
|
639
|
-
log(`bootstrap: 冒烟自检${await runSmokeTest(kernel, ctx, loginResult) ? "通过" : "未完全通过"}(见上文 smoke 日志)`);
|
|
640
|
-
} catch (smokeErr) {
|
|
641
|
-
log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
|
|
642
|
-
}
|
|
643
|
-
} else log("bootstrap: kernel core missing login fn");
|
|
644
|
-
if (loginResult !== null) await startProtocols(kernel, ctx, loginResult);
|
|
645
|
-
if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
|
|
646
|
-
const probe = kernel.probeRuntime(ctx);
|
|
647
|
-
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
648
|
-
setTimeout(() => {
|
|
649
|
-
try {
|
|
650
|
-
if (typeof kernel.probeRuntime !== "function") return;
|
|
651
|
-
const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
|
|
652
|
-
log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
|
|
653
|
-
} catch (e2) {
|
|
654
|
-
log(`bootstrap: probe-late error: ${errMsg(e2)}`);
|
|
655
|
-
}
|
|
656
|
-
}, 35e3);
|
|
657
|
-
} catch (e) {
|
|
658
|
-
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
659
|
-
}
|
|
660
|
-
} else {
|
|
661
|
-
log("bootstrap: kernel has no NapukettoCore, falling back");
|
|
662
|
-
const ctx = kernel.startNapuketto?.({
|
|
663
|
-
wrapperExports: state.wrapperExports,
|
|
664
|
-
env: bootEnv
|
|
665
|
-
});
|
|
666
|
-
if (ctx === void 0) {
|
|
667
|
-
log("bootstrap: startNapuketto 不可用");
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
|
|
671
|
-
if (typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function") {
|
|
672
|
-
if (typeof kernel.buildLoginConfig === "function" && ctx.loginService) {
|
|
673
|
-
const loginCfg = kernel.buildLoginConfig(Appid, bootEnv.qqVersion || "", bootEnv.dataDir || ".");
|
|
674
|
-
if (typeof ctx.loginService.initConfig === "function") {
|
|
675
|
-
ctx.loginService.initConfig(loginCfg);
|
|
676
|
-
log("bootstrap: loginService.initConfig OK");
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
const loginResult = await kernel.quickLogin(ctx, {});
|
|
680
|
-
log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
|
|
681
|
-
const sessionConfig = kernel.buildSessionConfig({
|
|
682
|
-
appid: Appid,
|
|
683
|
-
fullVersion: bootEnv.qqVersion || "",
|
|
684
|
-
selfUin: loginResult.uin,
|
|
685
|
-
selfUid: loginResult.uid,
|
|
686
|
-
accountPath: bootEnv.dataDir || ".",
|
|
687
|
-
downloadPath: (0, node_path.join)(bootEnv.dataDir || ".", "temp")
|
|
688
|
-
});
|
|
689
|
-
const listener = kernel.createLifecycleSessionListener();
|
|
690
|
-
await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
|
|
691
|
-
log("bootstrap: session init + startNT OK!");
|
|
692
|
-
} else log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
|
|
693
|
-
if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
|
|
694
|
-
const probe = kernel.probeRuntime(ctx);
|
|
695
|
-
log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
|
|
696
|
-
} catch (e) {
|
|
697
|
-
log(`bootstrap: probe error: ${errMsg(e)}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
1320
|
+
if (kernel.NapukettoCore !== void 0) await bootstrapWithCore(kernel, state, bootEnv, Appid);
|
|
1321
|
+
else await bootstrapFallback(kernel, state, bootEnv, Appid);
|
|
700
1322
|
} catch (e) {
|
|
701
1323
|
log(`bootstrap: lifecycle error: ${errMsg(e)}`);
|
|
702
1324
|
}
|
|
@@ -705,7 +1327,7 @@ async function bootstrap(state) {
|
|
|
705
1327
|
}
|
|
706
1328
|
}
|
|
707
1329
|
//#endregion
|
|
708
|
-
//#region src/host/self-host.ts
|
|
1330
|
+
//#region src/host/core/self-host.ts
|
|
709
1331
|
/**
|
|
710
1332
|
* self-host.ts:自建宿主(路线 A,NAPUTO_SELF_HOST)入口——标准 Node 进程直接运行。
|
|
711
1333
|
* 2026-08-07 阶段 2:由 runtime/self-host.cjs TS 化(零语义改动)。
|
|
@@ -730,14 +1352,38 @@ async function bootstrap(state) {
|
|
|
730
1352
|
* → kernel 装配 → 登录(快速/QR)→ session(先 init 后 start)→ 冒烟 → 协议装配
|
|
731
1353
|
*/
|
|
732
1354
|
const state = createState();
|
|
1355
|
+
const ipcMode = env.NAPUTO_IPC === "1";
|
|
1356
|
+
if (ipcMode) {
|
|
1357
|
+
enableIpc();
|
|
1358
|
+
sendStatus("booting");
|
|
1359
|
+
}
|
|
733
1360
|
log(`[self-host] 启动 @ ${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} node=${process.version} type=${process.type ?? "(标准 node)"}`);
|
|
734
1361
|
log(`[self-host] stub 目录: ${env.NAPUTO_STUB_DIR ?? "(PATH 前置,未记录)"}`);
|
|
1362
|
+
const lockDataDir = env.NAPUTO_CFG_DIR;
|
|
1363
|
+
if (lockDataDir) {
|
|
1364
|
+
if (!acquireInstanceLock(lockDataDir)) {
|
|
1365
|
+
const { pid: holderPid } = checkInstanceLock(lockDataDir);
|
|
1366
|
+
log(`[self-host] ❌ 数据目录已被其他实例占用(pid=${holderPid ?? "?"}),退出;同一账号数据目录仅允许一个实例,请先停止已有实例`);
|
|
1367
|
+
if (ipcMode) sendStatus("failed", "数据目录被占用", {
|
|
1368
|
+
code: "INVALID_STATE",
|
|
1369
|
+
message: `数据目录已被其他实例占用(pid=${holderPid ?? "?"})`
|
|
1370
|
+
});
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
registerLockCleanup(lockDataDir);
|
|
1374
|
+
log(`[self-host] ✅ 已获取数据目录锁(${lockDataDir})`);
|
|
1375
|
+
} else log("[self-host] ⚠️ NAPUTO_CFG_DIR 未设置,跳过单实例锁");
|
|
735
1376
|
const wrapperPath = env.NAPUTO_WRAPPER_PATH;
|
|
736
1377
|
if (!wrapperPath) {
|
|
737
1378
|
log("[self-host] NAPUTO_WRAPPER_PATH 未设置,退出");
|
|
1379
|
+
if (ipcMode) sendStatus("failed", "NAPUTO_WRAPPER_PATH 未设置", {
|
|
1380
|
+
code: "INVALID_STATE",
|
|
1381
|
+
message: "NAPUTO_WRAPPER_PATH 未设置,退出"
|
|
1382
|
+
});
|
|
738
1383
|
process.exit(1);
|
|
739
1384
|
}
|
|
740
1385
|
log(`[self-host] dlopen wrapper.node: ${wrapperPath}`);
|
|
1386
|
+
if (ipcMode) sendStatus("dlopening");
|
|
741
1387
|
try {
|
|
742
1388
|
const m = { exports: {} };
|
|
743
1389
|
process.dlopen(m, wrapperPath);
|
|
@@ -745,11 +1391,19 @@ try {
|
|
|
745
1391
|
log(`[self-host] ✅ dlopen 成功,exports ${keys.length} 个`);
|
|
746
1392
|
if (!keys.includes("NodeIKernelLoginService")) {
|
|
747
1393
|
log("[self-host] ❌ exports 无 NodeIKernelLoginService(stub 环境异常?)");
|
|
1394
|
+
if (ipcMode) sendStatus("failed", "exports 无 NodeIKernelLoginService", {
|
|
1395
|
+
code: "INVALID_STATE",
|
|
1396
|
+
message: "exports 无 NodeIKernelLoginService(stub 环境异常?)"
|
|
1397
|
+
});
|
|
748
1398
|
process.exit(1);
|
|
749
1399
|
}
|
|
750
1400
|
state.wrapperExports = m.exports;
|
|
751
1401
|
} catch (e) {
|
|
752
1402
|
log(`[self-host] ❌ dlopen 失败: ${errMsg(e)}`);
|
|
1403
|
+
if (ipcMode) sendStatus("failed", "dlopen 失败", {
|
|
1404
|
+
code: "UNKNOWN",
|
|
1405
|
+
message: errMsg(e)
|
|
1406
|
+
});
|
|
753
1407
|
process.exit(1);
|
|
754
1408
|
}
|
|
755
1409
|
try {
|
|
@@ -766,8 +1420,13 @@ log("[self-host] 调 bootstrap(state) ...");
|
|
|
766
1420
|
try {
|
|
767
1421
|
await bootstrap(state);
|
|
768
1422
|
log("[self-host] bootstrap 完成");
|
|
1423
|
+
if (ipcMode) sendStatus("ready");
|
|
769
1424
|
} catch (e) {
|
|
770
1425
|
log(`[self-host] bootstrap 失败: ${errMsg(e)}`);
|
|
1426
|
+
if (ipcMode) sendStatus("failed", "bootstrap 失败", {
|
|
1427
|
+
code: "UNKNOWN",
|
|
1428
|
+
message: errMsg(e)
|
|
1429
|
+
});
|
|
771
1430
|
process.exit(1);
|
|
772
1431
|
}
|
|
773
1432
|
})();
|