mslxdff 0.1.160 → 0.1.161
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/bin/mslxdff.js +4 -4
- package/docs/cli_help_mini.md +135 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/autostart.js +3 -0
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/empty-turn.js +119 -0
- package/src/chat-pipeline/index.js +126 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +53 -19
- package/src/cli/commands/daemon.js +4 -4
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -0
- package/src/cli/commands/provider/index.js +126 -124
- package/src/cli/commands/provider/models.js +142 -139
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/commands/system.js +2 -2
- package/src/cli/format.js +6 -0
- package/src/cli/policy.js +2 -2
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +101 -96
- package/src/logs.js +14 -1
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +5 -2
- package/src/models.js +225 -225
- package/src/providers/AGENTS.md +46 -0
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/globalqwenwork/account-store.js +141 -0
- package/src/providers/globalqwenwork/constants.js +73 -0
- package/src/providers/globalqwenwork/cosy.js +123 -0
- package/src/providers/globalqwenwork/crypto.js +220 -0
- package/src/providers/globalqwenwork/http.js +22 -0
- package/src/providers/globalqwenwork/index.js +331 -0
- package/src/providers/globalqwenwork/payload.js +145 -0
- package/src/providers/globalqwenwork/rsa.js +56 -0
- package/src/providers/globalqwenwork/sse.js +270 -0
- package/src/providers/globalqwenwork/stream.js +132 -0
- package/src/providers/globalqwenwork/upstream.js +122 -0
- package/src/providers/globalqwenwork.js +1 -0
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +183 -183
- package/src/providers/qoder/index.js +230 -230
- package/src/providers/qoder/sse.js +103 -103
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +142 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/registry.js +74 -66
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/sse.js +19 -2
- package/src/reasoning.js +32 -32
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/exhausted-handler.js +23 -8
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/local-handler.js +3 -1
- package/src/routes/chat/relay-pipeline.js +276 -264
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +255 -255
- package/src/routes/helpers.js +20 -4
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-hold.js +138 -0
- package/src/routes/stream-scan.js +192 -0
- package/src/routes/stream.js +386 -393
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/lifecycle-forensics.js +116 -0
- package/src/runtime/lifecycle-log.js +23 -0
- package/src/runtime/provider-gate.js +34 -33
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/talk-log.js +226 -0
- package/src/timeline.js +5 -2
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
package/src/runtime/bootstrap.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { appendCall, appendError, appendEvent, appendTimeline } from "../logs.js";
|
|
2
|
-
import { setupProviders } from "./providers-setup.js";
|
|
3
|
-
import { startServerLifecycle } from "./server-lifecycle.js";
|
|
4
|
-
import { appendModelTrace } from "../model-trace.js";
|
|
5
|
-
import { startGroupSync } from "./group-sync.js";
|
|
6
|
-
import { startBroadband } from "./broadband.js";
|
|
7
|
-
|
|
8
|
-
const logs = { appendCall, appendError, appendEvent, appendTimeline, appendModelTrace };
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* daemon 启动门面 — 仅编排:组装世界 → 服务生命周期 → 群组同步 → 宽带中继 → 自更新。
|
|
12
|
-
* 重活下沉 providers-setup / server-lifecycle / group-sync / broadband(-stream)。
|
|
13
|
-
*/
|
|
14
|
-
export async function startDaemonMain(VERSION) {
|
|
15
|
-
const { installLifecycleLog } = await import("./lifecycle-log.js");
|
|
16
|
-
installLifecycleLog({ version: VERSION, logs });
|
|
17
|
-
const ctx = await setupProviders();
|
|
18
|
-
const { srv, bus } = await startServerLifecycle({ VERSION, ...ctx, logs });
|
|
19
|
-
void srv;
|
|
20
|
-
startGroupSync({ peers: ctx.peers, groups: ctx.groups });
|
|
21
|
-
startBroadband({ token: ctx.token, upstream: ctx.upstream });
|
|
22
|
-
|
|
23
|
-
const { setupAutoUpdate } = await import("./auto-update.js");
|
|
24
|
-
setupAutoUpdate({ VERSION, bus, logs });
|
|
25
|
-
|
|
26
|
-
const { setupWorkbuddyCheckin } = await import("./workbuddy-checkin.js");
|
|
27
|
-
void setupWorkbuddyCheckin({ bus, logs }).catch((e) => {
|
|
28
|
-
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const { setupTraeworkCheckin } = await import("./traework-checkin.js");
|
|
32
|
-
void setupTraeworkCheckin({ bus, logs }).catch((e) => {
|
|
33
|
-
try { logs.appendEvent({ ts: Date.now(), type: "traework-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const { setupQoderCheckin } = await import("./qoder-checkin.js");
|
|
37
|
-
void setupQoderCheckin({ bus, logs }).catch((e) => {
|
|
38
|
-
try { logs.appendEvent({ ts: Date.now(), type: "qoder-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
|
|
42
|
-
void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
|
|
43
|
-
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
1
|
+
import { appendCall, appendError, appendEvent, appendTimeline } from "../logs.js";
|
|
2
|
+
import { setupProviders } from "./providers-setup.js";
|
|
3
|
+
import { startServerLifecycle } from "./server-lifecycle.js";
|
|
4
|
+
import { appendModelTrace } from "../model-trace.js";
|
|
5
|
+
import { startGroupSync } from "./group-sync.js";
|
|
6
|
+
import { startBroadband } from "./broadband.js";
|
|
7
|
+
|
|
8
|
+
const logs = { appendCall, appendError, appendEvent, appendTimeline, appendModelTrace };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* daemon 启动门面 — 仅编排:组装世界 → 服务生命周期 → 群组同步 → 宽带中继 → 自更新。
|
|
12
|
+
* 重活下沉 providers-setup / server-lifecycle / group-sync / broadband(-stream)。
|
|
13
|
+
*/
|
|
14
|
+
export async function startDaemonMain(VERSION) {
|
|
15
|
+
const { installLifecycleLog } = await import("./lifecycle-log.js");
|
|
16
|
+
installLifecycleLog({ version: VERSION, logs });
|
|
17
|
+
const ctx = await setupProviders();
|
|
18
|
+
const { srv, bus } = await startServerLifecycle({ VERSION, ...ctx, logs });
|
|
19
|
+
void srv;
|
|
20
|
+
startGroupSync({ peers: ctx.peers, groups: ctx.groups });
|
|
21
|
+
startBroadband({ token: ctx.token, upstream: ctx.upstream });
|
|
22
|
+
|
|
23
|
+
const { setupAutoUpdate } = await import("./auto-update.js");
|
|
24
|
+
setupAutoUpdate({ VERSION, bus, logs });
|
|
25
|
+
|
|
26
|
+
const { setupWorkbuddyCheckin } = await import("./workbuddy-checkin.js");
|
|
27
|
+
void setupWorkbuddyCheckin({ bus, logs }).catch((e) => {
|
|
28
|
+
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { setupTraeworkCheckin } = await import("./traework-checkin.js");
|
|
32
|
+
void setupTraeworkCheckin({ bus, logs }).catch((e) => {
|
|
33
|
+
try { logs.appendEvent({ ts: Date.now(), type: "traework-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const { setupQoderCheckin } = await import("./qoder-checkin.js");
|
|
37
|
+
void setupQoderCheckin({ bus, logs }).catch((e) => {
|
|
38
|
+
try { logs.appendEvent({ ts: Date.now(), type: "qoder-checkin-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const { setupWorkbuddyGrowth } = await import("./workbuddy-growth.js");
|
|
42
|
+
void setupWorkbuddyGrowth({ bus, logs }).catch((e) => {
|
|
43
|
+
try { logs.appendEvent({ ts: Date.now(), type: "workbuddy-growth-setup-failed", error: String(e?.message || e).slice(0, 200) }); } catch {}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// daemon 上一次死亡的验尸官 —— 死亡证明从来不能由死者开:Windows 的关机/TerminateProcess
|
|
2
|
+
// 不给被杀进程跑任何 JS 的机会,硬死在遗言层面是零痕迹的(见 crosscut 叶"四种死法")。
|
|
3
|
+
// 所以抓捕靠三方各出一块拼图:
|
|
4
|
+
// ① 杀者留牌:stopDaemon / autostart 清场等"我方所杀"在动手前写 daemon.exit-marker
|
|
5
|
+
// (带死者 pid/版本/意图)——反正 Windows 下被杀者说不出话,牌只能杀者挂;
|
|
6
|
+
// ② 活体信标:daemon 心跳同步写 daemon.alive,钉死"最后一次确证活着"的时刻(死亡窗口下界);
|
|
7
|
+
// ③ 开机验尸:新进程在 installLifecycleLog(早于 writePid 覆盖 pid 文件)读
|
|
8
|
+
// pid/alive/marker + os.uptime() 回推本机开机时刻,纯函数分桶,结论落 errors.log。
|
|
9
|
+
// 分类恒给 diedBetween(死亡窗口)与 detail(中文归因),unknown/reboot/kill 各归各桶,
|
|
10
|
+
// 宁可报"不可解释的强杀"也不假装干净。全部 IO 吞异常——验尸失败绝不挡启动。
|
|
11
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
|
|
15
|
+
const SLACK_MS = 15_000; // 时钟/顺序抖动容差:牌的 at 不早于最后心跳−slack 才算"死于本牌"
|
|
16
|
+
|
|
17
|
+
export function aliveFile(dir) { return join(dir, "daemon.alive"); }
|
|
18
|
+
export function markerFile(dir) { return join(dir, "daemon.exit-marker"); }
|
|
19
|
+
export function pidPath(dir) { return join(dir, "daemon.pid"); }
|
|
20
|
+
|
|
21
|
+
function readJsonSafe(file) {
|
|
22
|
+
try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
function writeJsonSafe(file, obj) {
|
|
25
|
+
try {
|
|
26
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
27
|
+
writeFileSync(file, JSON.stringify(obj), { mode: 0o600 });
|
|
28
|
+
return true;
|
|
29
|
+
} catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 活体信标:{pid, version, at, uptimeMs, rssMb}。daemon 模式专属写(debug 前台不碰,防污染死亡窗口)。 */
|
|
33
|
+
export function writeAlive(dir, info) { return writeJsonSafe(aliveFile(dir), { at: Date.now(), ...info }); }
|
|
34
|
+
export function readAlive(dir) { return readJsonSafe(aliveFile(dir)); }
|
|
35
|
+
|
|
36
|
+
/** 杀者牌:动手前写,必须自带死者身份(stopDaemon 随后会 unlink pid 文件)。 */
|
|
37
|
+
export function writeExitMarker(dir, { reason, prevPid, prevVersion = null, byPid = process.pid, code = null, at } = {}) {
|
|
38
|
+
return writeJsonSafe(markerFile(dir), { reason, prevPid, prevVersion, byPid, code, at: at ?? Date.now() });
|
|
39
|
+
}
|
|
40
|
+
/** 读后即删:一张牌只许生效一代,防止下代异常死亡被旧牌误导归因。 */
|
|
41
|
+
export function consumeExitMarker(dir) {
|
|
42
|
+
const m = readJsonSafe(markerFile(dir));
|
|
43
|
+
try { if (m) unlinkSync(markerFile(dir)); } catch {}
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readPidFrom(dir) {
|
|
48
|
+
try {
|
|
49
|
+
const n = Number(String(readFileSync(pidPath(dir), "utf8")).trim().split("\n")[0]);
|
|
50
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
export function readPidVersionFrom(dir) {
|
|
54
|
+
try {
|
|
55
|
+
const l = String(readFileSync(pidPath(dir), "utf8")).split("\n");
|
|
56
|
+
return l.length > 1 && l[1].trim() ? l[1].trim() : null;
|
|
57
|
+
} catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 开机时刻 = now − 整机 uptime。重启后它是硬事实,任何进程都伪造不了。 */
|
|
61
|
+
export function bootTimeMs(now = Date.now()) {
|
|
62
|
+
try { return now - Math.round(os.uptime() * 1000); } catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 纯函数分桶(可测,不碰 fs):
|
|
67
|
+
* none 无前任痕迹(首装 / uninstall 清干净后)
|
|
68
|
+
* clean-exit 死者自己跑完了 exit 钩子(Linux 信号优雅退出 / process.exit)
|
|
69
|
+
* killed-by-cli 新鲜杀者牌:via=stop|restart|upgrade|debug-takeover|uninstall|autostart-cleanup…
|
|
70
|
+
* reboot 无牌,但开机时刻晚于最后信标 → 整机重启带走
|
|
71
|
+
* holder-alive 旧 pid 仍活(抢端口/接管现场)
|
|
72
|
+
* unknown 有旧 pid 无信标(早于验尸功能的旧版本残留),死亡窗口给不出
|
|
73
|
+
* unexplained-kill 机器没重启、无牌、信标戛然而止 → taskkill/任务管理器/硬杀
|
|
74
|
+
*/
|
|
75
|
+
export function classifyPrevExit({ prevPid = null, prevVersion = null, alive = null, marker = null, bootAtMs = null, now = Date.now(), selfPid = 0, prevStillRunning = false } = {}) {
|
|
76
|
+
const lastAlive = Number.isFinite(alive?.at) ? alive.at : null;
|
|
77
|
+
if (!prevPid && !alive && !marker) return { verdict: "none", detail: "无前任 daemon 痕迹" };
|
|
78
|
+
const freshMarker = marker && Number.isFinite(marker.at) && (lastAlive == null || marker.at >= lastAlive - SLACK_MS);
|
|
79
|
+
if (freshMarker && marker.reason === "process-exit") {
|
|
80
|
+
return { verdict: "clean-exit", prevPid: marker.prevPid ?? prevPid, prevVersion: marker.prevVersion ?? prevVersion, at: marker.at, diedBetween: [lastAlive, marker.at], detail: `前任进程自写遗书正常退出 (pid=${marker.prevPid ?? prevPid} exit code=${marker.code ?? "?"})` };
|
|
81
|
+
}
|
|
82
|
+
if (freshMarker) {
|
|
83
|
+
return { verdict: "killed-by-cli", via: marker.reason, prevPid: marker.prevPid ?? prevPid, prevVersion: marker.prevVersion ?? prevVersion, byPid: marker.byPid ?? null, at: marker.at, diedBetween: [lastAlive, marker.at], detail: `被 CLI/清理方 (pid=${marker.byPid ?? "?"}) 以 ${marker.reason} 名义主动杀死` };
|
|
84
|
+
}
|
|
85
|
+
if (lastAlive != null && bootAtMs != null && bootAtMs > lastAlive + SLACK_MS) {
|
|
86
|
+
return { verdict: "reboot", prevPid, prevVersion, lastAliveAt: lastAlive, bootAt: bootAtMs, diedBetween: [lastAlive, bootAtMs], detail: "死者零遗言,且本机开机时刻晚于最后心跳 → 整机重启带走(Windows 关机不跑 exit 钩子,属预期盲区,靠本桶收口)" };
|
|
87
|
+
}
|
|
88
|
+
if (prevPid && prevStillRunning && prevPid !== selfPid) {
|
|
89
|
+
return { verdict: "holder-alive", prevPid, prevVersion, detail: `旧 pid=${prevPid} 仍在世(抢端口/接管现场,本进程=${selfPid})` };
|
|
90
|
+
}
|
|
91
|
+
if (lastAlive == null) {
|
|
92
|
+
return { verdict: "unknown", prevPid, prevVersion, detail: "有旧 pid 无活体信标(前任早于验尸功能上线),只能确认已死" };
|
|
93
|
+
}
|
|
94
|
+
return { verdict: "unexplained-kill", prevPid, prevVersion, lastAliveAt: lastAlive, diedBetween: [lastAlive, now], detail: "机器未重启、无杀者牌、信标戛然而止:taskkill/任务管理器/硬杀或断电级崩溃(exit 钩子未跑)" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function defaultIsPidAlive(pid) {
|
|
98
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err.code !== "ESRCH"; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 开机验尸入口:读齐三方证据 → 收牌(读后即删)→ 分桶。collect 与 report 一起回,供调用方落日志。 */
|
|
102
|
+
export function collectAutopsy({ dir, selfPid = process.pid, now = Date.now(), isPidAliveFn } = {}) {
|
|
103
|
+
const prevPid = readPidFrom(dir);
|
|
104
|
+
const alive = readAlive(dir);
|
|
105
|
+
const input = {
|
|
106
|
+
prevPid,
|
|
107
|
+
prevVersion: readPidVersionFrom(dir),
|
|
108
|
+
alive,
|
|
109
|
+
marker: consumeExitMarker(dir),
|
|
110
|
+
bootAtMs: bootTimeMs(now),
|
|
111
|
+
now,
|
|
112
|
+
selfPid,
|
|
113
|
+
prevStillRunning: !!prevPid && prevPid !== selfPid ? (isPidAliveFn ?? defaultIsPidAlive)(prevPid) : false,
|
|
114
|
+
};
|
|
115
|
+
return { input, report: classifyPrevExit(input) };
|
|
116
|
+
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// 被信号杀(有 `SIGTERM received` 行)、崩溃(有 uncaughtException 行)、
|
|
4
4
|
// 被 SIGKILL/任务管理器杀(只有未送达的 exit 行 → 看最后一条 heartbeat 定位死亡时刻)、
|
|
5
5
|
// OOM 积累(heartbeat 的 rss 曲线)。
|
|
6
|
+
// 验尸三件套(详见 src/runtime/lifecycle-forensics.js):daemon.alive 活体信标 + daemon.exit-marker 杀者牌 + 开机分桶 → errors.log(type=daemon-exit)。
|
|
6
7
|
// 顶层异常只记录不退出(代理服务无持久状态,活着比干净退出更有价值;异常多为请求级漏网)。
|
|
7
8
|
import { appendEvent } from "../logs.js";
|
|
9
|
+
import { daemonDir } from "../daemon.js";
|
|
10
|
+
import { collectAutopsy, writeAlive, writeExitMarker } from "./lifecycle-forensics.js";
|
|
8
11
|
|
|
9
12
|
const HEARTBEAT_MS = Math.max(60_000, Number(process.env.MSLXDFF_HEARTBEAT_MS) || 30 * 60 * 1000);
|
|
10
13
|
|
|
@@ -39,25 +42,45 @@ export function installLifecycleLog({ version, bus, logs } = {}) {
|
|
|
39
42
|
}));
|
|
40
43
|
emit("lifecycle-start", { pid: process.pid, ppid: process.ppid, version, node: process.version });
|
|
41
44
|
|
|
45
|
+
// 开机验尸:上一次死亡归因(七桶),结论落 errors.log 供事后排查——必须赶在 server-lifecycle writePid 覆盖旧 pid 文件之前
|
|
46
|
+
const dir = daemonDir();
|
|
47
|
+
const isDaemon = process.env.MSLXDFF_DAEMON === "1";
|
|
48
|
+
try {
|
|
49
|
+
const { report } = collectAutopsy({ dir, selfPid: process.pid });
|
|
50
|
+
if (report.verdict !== "none") {
|
|
51
|
+
const w = report.diedBetween && report.diedBetween[0] ? ` window=${new Date(report.diedBetween[0]).toISOString()}..${report.diedBetween[1] ? new Date(report.diedBetween[1]).toISOString() : "?"}` : "";
|
|
52
|
+
log(`[lifecycle] autopsy prev-run verdict=${report.verdict}${w} ${report.detail}`);
|
|
53
|
+
try { logs?.appendError?.({ type: "daemon-exit", model: "daemon", status: 0, verdict: report.verdict, via: report.via ?? null, prevPid: report.prevPid ?? null, prevVersion: report.prevVersion ?? null, byPid: report.byPid ?? null, lastAliveAt: report.lastAliveAt ?? null, diedBetween: report.diedBetween ?? null, bootAt: report.bootAt ?? null, detail: report.detail }); } catch {}
|
|
54
|
+
emit("lifecycle-autopsy", { verdict: report.verdict, via: report.via ?? null, prevPid: report.prevPid ?? null });
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
// 活体信标:启动即写一条,死亡窗口下界不必等 30min 心跳
|
|
58
|
+
if (isDaemon) { try { writeAlive(dir, { pid: process.pid, version, uptimeMs: 0, rssMb: rssMb() }); } catch {} }
|
|
59
|
+
|
|
42
60
|
process.on("uncaughtException", (e) => {
|
|
43
61
|
const detail = String(e?.stack || e?.message || e).slice(0, 2000);
|
|
44
62
|
log(`[lifecycle] uncaughtException (kept running): ${detail}`);
|
|
45
63
|
emit("lifecycle-uncaught", { detail: detail.slice(0, 500) });
|
|
64
|
+
try { logs?.appendError?.({ type: "daemon-uncaught", model: "daemon", status: 0, message: `uncaughtException (kept running): ${detail.slice(0, 400)}` }); } catch {}
|
|
46
65
|
});
|
|
47
66
|
process.on("unhandledRejection", (r) => {
|
|
48
67
|
const detail = String(r?.stack || r?.message || r).slice(0, 2000);
|
|
49
68
|
log(`[lifecycle] unhandledRejection (kept running): ${detail}`);
|
|
50
69
|
emit("lifecycle-unhandled-rejection", { detail: detail.slice(0, 500) });
|
|
70
|
+
try { logs?.appendError?.({ type: "daemon-unhandled", model: "daemon", status: 0, message: `unhandledRejection (kept running): ${detail.slice(0, 400)}` }); } catch {}
|
|
51
71
|
});
|
|
52
72
|
process.on("exit", (code) => {
|
|
53
73
|
const uptimeSec = Math.round((Date.now() - t0) / 1000);
|
|
54
74
|
log(`[lifecycle] exit code=${code} uptime=${uptimeSec}s rss=${rssMb()}mb`);
|
|
75
|
+
// 遗书:exit 钩子跑得动 = 非硬杀(Linux 信号善终 / process.exit);Windows 强杀场景永远看不到这行,缺它本身就是证据
|
|
76
|
+
try { writeExitMarker(dir, { reason: "process-exit", prevPid: process.pid, prevVersion: version, byPid: process.pid, code }); } catch {}
|
|
55
77
|
});
|
|
56
78
|
process.on("SIGHUP", () => log(`[lifecycle] SIGHUP received (ignored, pid ${process.pid})`));
|
|
57
79
|
|
|
58
80
|
const hb = setInterval(() => {
|
|
59
81
|
// 心跳只落 daemon.log(不写 events.log:48 条/天,避免污染结构化事件流)
|
|
60
82
|
log(lifecycleHeartbeatLine({ pid: process.pid, uptimeMin: Math.round((Date.now() - t0) / 60000), rssMb: rssMb() }));
|
|
83
|
+
if (isDaemon) { try { writeAlive(dir, { pid: process.pid, version, uptimeMs: Date.now() - t0, rssMb: rssMb() }); } catch {} }
|
|
61
84
|
}, HEARTBEAT_MS);
|
|
62
85
|
hb.unref?.();
|
|
63
86
|
return { heartbeatMs: HEARTBEAT_MS };
|
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
// 定制 provider 启用门禁(纯函数 + auth 号型名单),从 providers-setup.js 拆出以守住 ≤10KB 体积门。
|
|
2
|
-
// 背景:门禁曾要求 `baseUrl && keys` 两者皆有,导致 qoder(keys 有、baseUrl 空)被静默跳过 ——
|
|
3
|
-
// 表现为 /v1/models 无该供应商模型、`-models` 挑不到、网关报 `Model qoder/x is not supported`。
|
|
4
|
-
// Note: auth 号型不要求 baseUrl 是硬约束(旧门禁 baseUrl&&keys 曾静默跳过整家 qoder)— 见 .agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md
|
|
5
|
-
|
|
6
|
-
/** 凭证在 auth 目录、无需 baseUrl 的「auth 号型」定制供应商(端点由各自 constants 决定)。 */
|
|
7
|
-
export const AUTH_DOC_PROVIDER_IDS = ["workbuddy", "traework", "qoder", "qwenwork"];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* - auth 号型:keys 或 auths 或 auth 目录有号 → 启用(baseUrl 可空)
|
|
11
|
-
* - 其他定制(cline/codearts/插件注册):有 keys → 启用(baseUrl 可空,端点由工厂决定)
|
|
12
|
-
*/
|
|
13
|
-
export function shouldEnableCustomProvider(gid, { keys = [], auths = [], hasAuthDocs = false } = {}) {
|
|
14
|
-
if (AUTH_DOC_PROVIDER_IDS.includes(gid)) return keys.length > 0 || auths.length > 0 || hasAuthDocs;
|
|
15
|
-
return keys.length > 0;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 按 gid 载入各自 account-store 并列出落盘账号(失败/无目录 → 空数组,不抛)。 */
|
|
19
|
-
export async function loadAuthDocs(gid) {
|
|
20
|
-
try {
|
|
21
|
-
const store = gid === "workbuddy"
|
|
22
|
-
? await import("../providers/workbuddy/account-store.js")
|
|
23
|
-
: gid === "traework"
|
|
24
|
-
? await import("../providers/traework/account-store.js")
|
|
25
|
-
: gid === "qwenwork"
|
|
26
|
-
? await import("../providers/qwenwork/account-store.js")
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
1
|
+
// 定制 provider 启用门禁(纯函数 + auth 号型名单),从 providers-setup.js 拆出以守住 ≤10KB 体积门。
|
|
2
|
+
// 背景:门禁曾要求 `baseUrl && keys` 两者皆有,导致 qoder(keys 有、baseUrl 空)被静默跳过 ——
|
|
3
|
+
// 表现为 /v1/models 无该供应商模型、`-models` 挑不到、网关报 `Model qoder/x is not supported`。
|
|
4
|
+
// Note: auth 号型不要求 baseUrl 是硬约束(旧门禁 baseUrl&&keys 曾静默跳过整家 qoder)— 见 .agents/notes/implemented/bug-fix/2026-09-21-auth-doc-provider-gate.md
|
|
5
|
+
|
|
6
|
+
/** 凭证在 auth 目录、无需 baseUrl 的「auth 号型」定制供应商(端点由各自 constants 决定)。 */
|
|
7
|
+
export const AUTH_DOC_PROVIDER_IDS = ["workbuddy", "traework", "qoder", "qwenwork", "globalqwenwork"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* - auth 号型:keys 或 auths 或 auth 目录有号 → 启用(baseUrl 可空)
|
|
11
|
+
* - 其他定制(cline/codearts/插件注册):有 keys → 启用(baseUrl 可空,端点由工厂决定)
|
|
12
|
+
*/
|
|
13
|
+
export function shouldEnableCustomProvider(gid, { keys = [], auths = [], hasAuthDocs = false } = {}) {
|
|
14
|
+
if (AUTH_DOC_PROVIDER_IDS.includes(gid)) return keys.length > 0 || auths.length > 0 || hasAuthDocs;
|
|
15
|
+
return keys.length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 按 gid 载入各自 account-store 并列出落盘账号(失败/无目录 → 空数组,不抛)。 */
|
|
19
|
+
export async function loadAuthDocs(gid) {
|
|
20
|
+
try {
|
|
21
|
+
const store = gid === "workbuddy"
|
|
22
|
+
? await import("../providers/workbuddy/account-store.js")
|
|
23
|
+
: gid === "traework"
|
|
24
|
+
? await import("../providers/traework/account-store.js")
|
|
25
|
+
: gid === "qwenwork"
|
|
26
|
+
? await import("../providers/qwenwork/account-store.js")
|
|
27
|
+
: gid === "globalqwenwork"
|
|
28
|
+
? await import("../providers/globalqwenwork/account-store.js")
|
|
29
|
+
: await import("../providers/qoder/account-store.js");
|
|
30
|
+
return store.listAccountDocs();
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|