larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
package/dist/app/run.mjs
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import "../platform/check-bun-version.cjs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import * as larkinConfig from "../platform/config.mjs";
|
|
9
|
+
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
10
|
+
import { acquireProcessLock, readProcessState, terminateOwnedProcess, waitForProcessExit, } from "../platform/process-state.mjs";
|
|
11
|
+
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
12
|
+
import { cleanupStaleAgentControlSocket, createSupervisorControlServer, initializeControlAuthority, removeControlAuthority, requestDashboardRecovery, } from "./local-control.mjs";
|
|
13
|
+
import { internalCommandSpec, processCommandToken } from "./internal-command.mjs";
|
|
14
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const die = (message) => { console.error(`✗ ${message}`); process.exit(1); };
|
|
16
|
+
export function selectedAgentIds(argv, available) {
|
|
17
|
+
const flag = (name) => {
|
|
18
|
+
const index = argv.indexOf(name);
|
|
19
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
20
|
+
};
|
|
21
|
+
const requested = flag("--agent");
|
|
22
|
+
const requestedMany = flag("--agents");
|
|
23
|
+
if (requested && requestedMany)
|
|
24
|
+
throw new Error("--agent 与 --agents 不能同时使用");
|
|
25
|
+
const names = requestedMany
|
|
26
|
+
? [...new Set(requestedMany.split(",").map((name) => name.trim()).filter(Boolean))]
|
|
27
|
+
: requested ? [requested] : available;
|
|
28
|
+
if (!names.length)
|
|
29
|
+
throw new Error("--agents 至少需要一个 Agent ID");
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
function writeSupervisorStatus(file, supervisor, children) {
|
|
33
|
+
fs.writeFileSync(file, `${JSON.stringify({
|
|
34
|
+
...supervisor,
|
|
35
|
+
...children,
|
|
36
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
function childExit(child) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
child.once("error", reject);
|
|
41
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function waitForChildExit(child, timeoutMs) {
|
|
45
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
46
|
+
return true;
|
|
47
|
+
return await new Promise((resolve) => {
|
|
48
|
+
const onExit = () => { clearTimeout(timer); resolve(true); };
|
|
49
|
+
const timer = setTimeout(() => { child.off("exit", onExit); resolve(false); }, timeoutMs);
|
|
50
|
+
child.once("exit", onExit);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function stopDashboardWithinBound(child, configDir, signal = "SIGTERM", timeoutMs = 5_000) {
|
|
54
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
55
|
+
return "stopped";
|
|
56
|
+
child.kill(signal);
|
|
57
|
+
if (await waitForChildExit(child, timeoutMs))
|
|
58
|
+
return "stopped";
|
|
59
|
+
const owned = readProcessState(configDir).dashboard;
|
|
60
|
+
if (owned.state !== "owned" || Number(owned.pid) !== child.pid)
|
|
61
|
+
return "unowned";
|
|
62
|
+
terminateOwnedProcess(owned, "SIGKILL");
|
|
63
|
+
return await waitForChildExit(child, Math.max(250, Math.min(2_000, timeoutMs))) ? "killed" : "unowned";
|
|
64
|
+
}
|
|
65
|
+
export async function main() {
|
|
66
|
+
const argv = process.argv.slice(2);
|
|
67
|
+
const { configDir, file: configFile, config } = larkinConfig.loadConfig(process.env);
|
|
68
|
+
if (!fs.existsSync(configFile))
|
|
69
|
+
die(`没找到配置 ${configFile},先跑 larkin setup`);
|
|
70
|
+
if (!config.serverId || !Object.keys(config.agents).length)
|
|
71
|
+
die("配置缺少 serverId/agents,请运行 larkin setup");
|
|
72
|
+
let names;
|
|
73
|
+
try {
|
|
74
|
+
names = selectedAgentIds(argv, Object.keys(config.agents));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return die(error.message);
|
|
78
|
+
}
|
|
79
|
+
const before = readProcessState(configDir);
|
|
80
|
+
if (before.supervisor.state === "owned") {
|
|
81
|
+
if (before.dashboard.state !== "owned") {
|
|
82
|
+
const response = await requestDashboardRecovery({ larkinHome: configDir });
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
die(`dashboard 补拉请求失败:${response.error || "unknown error"}`);
|
|
85
|
+
console.log(`✓ larkin 已由统一 supervisor 运行(pid ${before.supervisor.pid});已通过认证控制面请求 dashboard(${response.state || "accepted"}),不重复启动 daemon`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`✓ larkin 已由统一 supervisor 运行(pid ${before.supervisor.pid};dashboard=ready),不重复启动`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (before.supervisor.state === "unknown")
|
|
93
|
+
die(`supervisor PID ${before.supervisor.pid} 身份无法确认;拒绝并行启动`);
|
|
94
|
+
if (before.daemon.state === "owned")
|
|
95
|
+
die(`发现未受统一 supervisor 管理的 daemon PID ${before.daemon.pid};请先优雅停止旧进程再运行 larkin start`);
|
|
96
|
+
if (before.daemon.state === "unknown")
|
|
97
|
+
die(`daemon PID ${before.daemon.pid} 身份无法确认;拒绝并行启动`);
|
|
98
|
+
const supervisorCommandToken = processCommandToken("supervisor", "app/run.mjs");
|
|
99
|
+
let launchLock;
|
|
100
|
+
try {
|
|
101
|
+
launchLock = acquireProcessLock(path.join(configDir, "supervisor-launch.lock.json"), supervisorCommandToken);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const observed = readProcessState(configDir).supervisor;
|
|
105
|
+
if (observed.state === "owned") {
|
|
106
|
+
console.log(`✓ larkin 已由统一 supervisor 运行(pid ${observed.pid}),不重复启动`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
die(`无法取得 supervisor 启动锁:${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
const statusFile = path.join(configDir, "supervisor-status.json");
|
|
112
|
+
const supervisor = {
|
|
113
|
+
...currentProcessMetadata(supervisorCommandToken), pid: process.pid,
|
|
114
|
+
nonce: crypto.randomUUID(), startedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
writeSupervisorStatus(statusFile, supervisor, {});
|
|
117
|
+
const supervisorStartToken = String(supervisor.processStartToken || "");
|
|
118
|
+
if (!supervisorStartToken) {
|
|
119
|
+
launchLock.release();
|
|
120
|
+
die("无法取得 supervisor start identity");
|
|
121
|
+
}
|
|
122
|
+
const controlToken = initializeControlAuthority(configDir, { pid: process.pid, processStartToken: supervisorStartToken });
|
|
123
|
+
let ensureDashboardHandler = () => "starting";
|
|
124
|
+
const supervisorControl = createSupervisorControlServer({
|
|
125
|
+
larkinHome: configDir,
|
|
126
|
+
authorityToken: controlToken,
|
|
127
|
+
ensureDashboard: () => ensureDashboardHandler(),
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
await supervisorControl.start();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
launchLock.release();
|
|
134
|
+
try {
|
|
135
|
+
removeControlAuthority(configDir, controlToken);
|
|
136
|
+
}
|
|
137
|
+
catch { /* best effort */ }
|
|
138
|
+
die(`supervisor control 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
const agents = [];
|
|
141
|
+
try {
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
const stored = config.agents[name];
|
|
144
|
+
if (!stored)
|
|
145
|
+
throw new Error(`Agent ${name} 不存在`);
|
|
146
|
+
const agent = hydrateRuntimeAgent(configDir, stored);
|
|
147
|
+
syncAgentProfile(agent, { ...process.env, LARKIN_CONFIG_DIR: configDir });
|
|
148
|
+
agents.push(agent);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
await supervisorControl.close().catch(() => { });
|
|
153
|
+
try {
|
|
154
|
+
removeControlAuthority(configDir, controlToken);
|
|
155
|
+
}
|
|
156
|
+
catch { /* best effort */ }
|
|
157
|
+
launchLock.release();
|
|
158
|
+
die(`Agent 启动校验失败:${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
}
|
|
160
|
+
if (before.dashboard.state === "owned") {
|
|
161
|
+
console.error(`[start] 正在把旧 dashboard PID ${before.dashboard.pid} 收归统一 supervisor…`);
|
|
162
|
+
terminateOwnedProcess(before.dashboard, "SIGTERM");
|
|
163
|
+
if (!await waitForProcessExit(before.dashboard)) {
|
|
164
|
+
await supervisorControl.close().catch(() => { });
|
|
165
|
+
try {
|
|
166
|
+
removeControlAuthority(configDir, controlToken);
|
|
167
|
+
}
|
|
168
|
+
catch { /* best effort */ }
|
|
169
|
+
launchLock.release();
|
|
170
|
+
die("旧 dashboard 10 秒内未退出;拒绝并行启动");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (before.dashboard.state === "unknown") {
|
|
174
|
+
await supervisorControl.close().catch(() => { });
|
|
175
|
+
try {
|
|
176
|
+
removeControlAuthority(configDir, controlToken);
|
|
177
|
+
}
|
|
178
|
+
catch { /* best effort */ }
|
|
179
|
+
launchLock.release();
|
|
180
|
+
die(`dashboard PID ${before.dashboard.pid} 身份无法确认;拒绝替换`);
|
|
181
|
+
}
|
|
182
|
+
const serverId = config.serverId;
|
|
183
|
+
if (!serverId)
|
|
184
|
+
die("配置缺少 serverId");
|
|
185
|
+
const runtimeEnv = {
|
|
186
|
+
...process.env,
|
|
187
|
+
LARKIN_HOME: configDir,
|
|
188
|
+
LARKIN_CONFIG_DIR: configDir,
|
|
189
|
+
LARKIN_AGENT_TRANSPORT_MODULE: path.join(HERE, "..", "agent", "agent-transport.cjs"),
|
|
190
|
+
LARKIN_SERVER_ID: String(serverId),
|
|
191
|
+
LARKIN_AGENTS_CONFIG: JSON.stringify(agents),
|
|
192
|
+
LARKIN_CONTROL_AUTHORIZATION: controlToken,
|
|
193
|
+
};
|
|
194
|
+
if (argv.includes("--dry-run"))
|
|
195
|
+
runtimeEnv.LARKIN_FEISHU_DRYRUN = "1";
|
|
196
|
+
let stopping = false;
|
|
197
|
+
let dashboard = null;
|
|
198
|
+
let dashboardRestartTimer = null;
|
|
199
|
+
const dashboardCrashes = [];
|
|
200
|
+
const dashboardScript = process.env.LARKIN_FEISHU_DRYRUN === "1" && process.env.LARKIN_TEST_DASHBOARD_SCRIPT
|
|
201
|
+
? path.resolve(process.env.LARKIN_TEST_DASHBOARD_SCRIPT) : path.join(HERE, "dashboard.mjs");
|
|
202
|
+
const daemonSpec = argv.includes("--dry-run") && process.env.LARKIN_TEST_DAEMON_SCRIPT
|
|
203
|
+
? { command: process.execPath, args: [path.resolve(process.env.LARKIN_TEST_DAEMON_SCRIPT)] }
|
|
204
|
+
: internalCommandSpec("runtime-process", [], runtimeEnv);
|
|
205
|
+
let daemon;
|
|
206
|
+
const daemonCrashes = [];
|
|
207
|
+
let daemonRestartTimer = null;
|
|
208
|
+
let resolveDaemonFinal = null;
|
|
209
|
+
const daemonFinal = new Promise((resolve) => { resolveDaemonFinal = resolve; });
|
|
210
|
+
// Erlang-style supervision: the supervisor restarts the daemon with a bounded
|
|
211
|
+
// recovery budget (3 restarts per 60s window, exponential backoff) instead of
|
|
212
|
+
// exiting on transient failures (e.g. network/proxy ECONNRESET during Feishu
|
|
213
|
+
// auth). Only when the budget is exhausted does the supervisor exit so the
|
|
214
|
+
// external process manager (launchd) takes over as the final respawn layer.
|
|
215
|
+
const launchDaemon = () => {
|
|
216
|
+
daemon = spawn(daemonSpec.command, daemonSpec.args, { env: runtimeEnv, stdio: "inherit" });
|
|
217
|
+
daemon.once("exit", (code, signal) => {
|
|
218
|
+
if (stopping) {
|
|
219
|
+
resolveDaemonFinal?.({ code, signal });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
daemonCrashes.push(now);
|
|
224
|
+
while (daemonCrashes[0] < now - 60_000)
|
|
225
|
+
daemonCrashes.shift();
|
|
226
|
+
if (daemonCrashes.length > 3) {
|
|
227
|
+
console.error(`✗ daemon 60 秒内连续退出超过 3 次;统一 supervisor 退出,交由外部进程管理器重启`);
|
|
228
|
+
resolveDaemonFinal?.({ code, signal });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const delay = 250 * 2 ** (daemonCrashes.length - 1);
|
|
232
|
+
console.error(`[start] daemon 异常退出,${delay}ms 后重启(${daemonCrashes.length}/3)`);
|
|
233
|
+
daemonRestartTimer = setTimeout(() => {
|
|
234
|
+
daemonRestartTimer = null;
|
|
235
|
+
if (stopping)
|
|
236
|
+
return;
|
|
237
|
+
// The fresh daemon re-registers its control authority; drop the stale one.
|
|
238
|
+
try {
|
|
239
|
+
removeControlAuthority(configDir, controlToken);
|
|
240
|
+
}
|
|
241
|
+
catch { /* best effort */ }
|
|
242
|
+
// Dashboard state is coupled to the daemon; restart it alongside.
|
|
243
|
+
const oldDashboard = dashboard;
|
|
244
|
+
if (oldDashboard && oldDashboard.exitCode === null && oldDashboard.signalCode === null) {
|
|
245
|
+
oldDashboard.kill("SIGTERM");
|
|
246
|
+
}
|
|
247
|
+
if (dashboardRestartTimer) {
|
|
248
|
+
clearTimeout(dashboardRestartTimer);
|
|
249
|
+
dashboardRestartTimer = null;
|
|
250
|
+
}
|
|
251
|
+
dashboardCrashes.length = 0;
|
|
252
|
+
launchDaemon();
|
|
253
|
+
writeSupervisorStatus(statusFile, supervisor, { daemonPid: daemon.pid });
|
|
254
|
+
launchDashboard();
|
|
255
|
+
}, delay);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
launchDaemon();
|
|
259
|
+
const launchDashboard = () => {
|
|
260
|
+
const dashboardEnv = { ...process.env, LARKIN_HOME: configDir, LARKIN_CONFIG_DIR: configDir, LARKIN_DASHBOARD_SUPERVISED: "1" };
|
|
261
|
+
const dashboardSpec = process.env.LARKIN_FEISHU_DRYRUN === "1" && process.env.LARKIN_TEST_DASHBOARD_SCRIPT
|
|
262
|
+
? { command: process.execPath, args: [dashboardScript] }
|
|
263
|
+
: internalCommandSpec("dashboard", [], dashboardEnv);
|
|
264
|
+
dashboard = spawn(dashboardSpec.command, dashboardSpec.args, {
|
|
265
|
+
env: dashboardEnv,
|
|
266
|
+
stdio: "inherit",
|
|
267
|
+
});
|
|
268
|
+
writeSupervisorStatus(statusFile, supervisor, { daemonPid: daemon.pid, dashboardPid: dashboard.pid });
|
|
269
|
+
const ownedChild = dashboard;
|
|
270
|
+
ownedChild.once("exit", () => {
|
|
271
|
+
if (dashboard === ownedChild)
|
|
272
|
+
dashboard = null;
|
|
273
|
+
if (stopping)
|
|
274
|
+
return;
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
dashboardCrashes.push(now);
|
|
277
|
+
while (dashboardCrashes[0] < now - 60_000)
|
|
278
|
+
dashboardCrashes.shift();
|
|
279
|
+
if (dashboardCrashes.length > 3) {
|
|
280
|
+
console.error("✗ dashboard 60 秒内连续退出超过 3 次;停止重启 dashboard,daemon 保持运行");
|
|
281
|
+
writeSupervisorStatus(statusFile, supervisor, { daemonPid: daemon.pid });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const delay = 250 * 2 ** (dashboardCrashes.length - 1);
|
|
285
|
+
console.error(`[start] dashboard 异常退出,${delay}ms 后重启(${dashboardCrashes.length}/3)`);
|
|
286
|
+
dashboardRestartTimer = setTimeout(() => {
|
|
287
|
+
dashboardRestartTimer = null;
|
|
288
|
+
if (!stopping && daemon.exitCode === null)
|
|
289
|
+
launchDashboard();
|
|
290
|
+
}, delay);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
const ensureDashboard = () => {
|
|
294
|
+
if (stopping || daemon.exitCode !== null)
|
|
295
|
+
return "stopping";
|
|
296
|
+
if (dashboard && dashboard.exitCode === null && dashboard.signalCode === null)
|
|
297
|
+
return "ready";
|
|
298
|
+
if (dashboardRestartTimer) {
|
|
299
|
+
clearTimeout(dashboardRestartTimer);
|
|
300
|
+
dashboardRestartTimer = null;
|
|
301
|
+
}
|
|
302
|
+
dashboardCrashes.length = 0;
|
|
303
|
+
console.error("[start] 收到 dashboard 补拉请求;已重置有界恢复预算");
|
|
304
|
+
launchDashboard();
|
|
305
|
+
return "started";
|
|
306
|
+
};
|
|
307
|
+
ensureDashboardHandler = ensureDashboard;
|
|
308
|
+
launchDashboard();
|
|
309
|
+
launchLock.release();
|
|
310
|
+
console.error(`[start] unified supervisor pid=${process.pid} daemon=${daemon.pid} agents=${names.join(",")}`);
|
|
311
|
+
console.error("[start] dashboard 已随服务启动;不会自动打开浏览器");
|
|
312
|
+
const requestStop = (signal) => {
|
|
313
|
+
if (stopping)
|
|
314
|
+
return;
|
|
315
|
+
stopping = true;
|
|
316
|
+
if (daemon.exitCode === null && daemon.signalCode === null)
|
|
317
|
+
daemon.kill(signal);
|
|
318
|
+
};
|
|
319
|
+
process.once("SIGINT", () => requestStop("SIGINT"));
|
|
320
|
+
process.once("SIGTERM", () => requestStop("SIGTERM"));
|
|
321
|
+
const daemonResult = await daemonFinal;
|
|
322
|
+
try {
|
|
323
|
+
cleanupStaleAgentControlSocket(configDir, controlToken);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error(`✗ daemon control socket 清理失败:${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
+
}
|
|
328
|
+
const intentional = stopping;
|
|
329
|
+
stopping = true;
|
|
330
|
+
if (dashboardRestartTimer) {
|
|
331
|
+
clearTimeout(dashboardRestartTimer);
|
|
332
|
+
dashboardRestartTimer = null;
|
|
333
|
+
}
|
|
334
|
+
const finalDashboard = dashboard;
|
|
335
|
+
if (finalDashboard) {
|
|
336
|
+
const timeoutMs = Math.max(100, Number(process.env.LARKIN_DASHBOARD_STOP_TIMEOUT_MS || 5_000) || 5_000);
|
|
337
|
+
const result = await stopDashboardWithinBound(finalDashboard, configDir, "SIGTERM", timeoutMs).catch(() => "unowned");
|
|
338
|
+
if (result === "killed")
|
|
339
|
+
console.error("[start] dashboard 未在期限内退出,已在 ownership 校验后 SIGKILL");
|
|
340
|
+
if (result === "unowned")
|
|
341
|
+
console.error("✗ dashboard 未在期限内退出且 ownership 无法确认;supervisor fail-safe 退出");
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const status = JSON.parse(fs.readFileSync(statusFile, "utf8"));
|
|
345
|
+
if (status.pid === process.pid)
|
|
346
|
+
fs.unlinkSync(statusFile);
|
|
347
|
+
}
|
|
348
|
+
catch { /* best effort */ }
|
|
349
|
+
await supervisorControl.close().catch(() => { });
|
|
350
|
+
try {
|
|
351
|
+
removeControlAuthority(configDir, controlToken);
|
|
352
|
+
}
|
|
353
|
+
catch { /* best effort */ }
|
|
354
|
+
if (!intentional) {
|
|
355
|
+
console.error(`✗ daemon 异常退出(${daemonResult.code ?? daemonResult.signal});统一 supervisor 退出,交由外部进程管理器重启`);
|
|
356
|
+
process.exitCode = daemonResult.code && daemonResult.code !== 0 ? daemonResult.code : 1;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
process.exitCode = daemonResult.signal === "SIGINT" ? 130 : daemonResult.signal === "SIGTERM" ? 143 : daemonResult.code ?? 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
363
|
+
main().catch((error) => { console.error(`✗ ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });
|
|
364
|
+
}
|