@keyou007/agent-bot 0.1.2 → 0.1.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/.env.example +3 -1
- package/CHANGELOG.md +9 -0
- package/README.md +20 -7
- package/README.zh.md +20 -7
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppRegistration.js +12 -2
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +10 -4
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/ServerStarter.js +69 -0
- package/dist/cli/ServerStarter.js.map +1 -0
- package/dist/cli/help.js +6 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/profile.js +40 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli.js +40 -56
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.js +7 -2
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/paths.js +1 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +21 -3
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +7 -76
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +18 -3
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +13 -2
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +1 -1
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +23 -7
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +20 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +209 -0
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -0
- package/dist/supervisor.js +47 -12
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +23 -5
- package/docs/technical-reference.zh.md +23 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +23 -3
- package/src/cli/FeishuAppRegistration.ts +14 -1
- package/src/cli/Initializer.ts +10 -3
- package/src/cli/ServerStarter.ts +106 -0
- package/src/cli/help.ts +6 -2
- package/src/cli/profile.ts +59 -0
- package/src/cli.ts +51 -52
- package/src/config/loadConfig.ts +12 -2
- package/src/config/paths.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/feishu/CardRenderer.ts +25 -3
- package/src/feishu/FeishuConnector.ts +6 -87
- package/src/index.ts +17 -5
- package/src/proxy/ProxySessionController.ts +18 -3
- package/src/startup/StartupNotifier.ts +21 -4
- package/src/startup/startFeishu.ts +1 -1
- package/src/supervision/SafeRestartNotifier.ts +22 -7
- package/src/supervision/SafeRestartScheduler.ts +27 -3
- package/src/supervision/SupervisorDiagnostics.ts +268 -0
- package/src/supervisor.ts +46 -11
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import qrcode from "qrcode-terminal";
|
|
5
5
|
import { loadConfig } from "./config/loadConfig.js";
|
|
6
|
-
import { sendControlRequest, isServerReachable
|
|
6
|
+
import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
|
|
7
7
|
import {
|
|
8
8
|
controlEndpoint,
|
|
9
9
|
type ControlResponse,
|
|
@@ -30,11 +30,22 @@ import {
|
|
|
30
30
|
type FeishuConfigurationChallenge,
|
|
31
31
|
} from "./cli/FeishuAppConfiguration.js";
|
|
32
32
|
import { renderCliHelp } from "./cli/help.js";
|
|
33
|
-
import { requireServerFeishuTransport } from "./feishu/transport.js";
|
|
34
33
|
import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
|
|
35
34
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
35
|
+
import { applyExplicitProfile, parseGlobalOptions } from "./cli/profile.js";
|
|
36
|
+
import {
|
|
37
|
+
startInitializedServer,
|
|
38
|
+
startServer,
|
|
39
|
+
type InitializationServerResult,
|
|
40
|
+
type ServerStartResult,
|
|
41
|
+
} from "./cli/ServerStarter.js";
|
|
36
42
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
37
43
|
import { StateStore, type SessionRecord } from "./state/StateStore.js";
|
|
44
|
+
import {
|
|
45
|
+
nodeDiagnosticReportArguments,
|
|
46
|
+
prepareCrashReportDirectory,
|
|
47
|
+
resolveSupervisorDiagnosticsPaths,
|
|
48
|
+
} from "./supervision/SupervisorDiagnostics.js";
|
|
38
49
|
|
|
39
50
|
const args = process.argv.slice(2);
|
|
40
51
|
|
|
@@ -45,7 +56,8 @@ void main(args).catch((error: unknown) => {
|
|
|
45
56
|
|
|
46
57
|
async function main(input: string[]): Promise<void> {
|
|
47
58
|
const parsed = parseGlobalOptions(input);
|
|
48
|
-
if (parsed.
|
|
59
|
+
if (parsed.profilePath) applyExplicitProfile(parsed.profilePath);
|
|
60
|
+
else if (parsed.configPath) process.env.AGENT_BOT_CONFIG = parsed.configPath;
|
|
49
61
|
const [command, ...rest] = parsed.args;
|
|
50
62
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
51
63
|
printHelp();
|
|
@@ -86,6 +98,7 @@ interface InitCommandResult extends InitializationResult {
|
|
|
86
98
|
appId?: string;
|
|
87
99
|
configuration?: EnsureFeishuAppConfigurationResult;
|
|
88
100
|
};
|
|
101
|
+
server: InitializationServerResult;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
async function initCommand(input: string[], configPath?: string): Promise<void> {
|
|
@@ -103,18 +116,24 @@ async function initCommand(input: string[], configPath?: string): Promise<void>
|
|
|
103
116
|
if (!json) printInitializationPaths(paths);
|
|
104
117
|
const initializationLock = acquireInitializationLock(paths.home.path);
|
|
105
118
|
|
|
119
|
+
let initialized: Omit<InitCommandResult, "server">;
|
|
106
120
|
try {
|
|
107
121
|
cleanupFeishuCredentialTemporaryFiles(paths.env.path);
|
|
108
122
|
const feishu = await initializeFeishu(
|
|
109
123
|
paths,
|
|
110
124
|
{ json, skipFeishu, reconfigureFeishu },
|
|
111
125
|
);
|
|
112
|
-
|
|
113
|
-
if (json) printJson(result);
|
|
114
|
-
else printInitializationResult(result);
|
|
126
|
+
initialized = { ...paths, feishu };
|
|
115
127
|
} finally {
|
|
116
128
|
initializationLock.release();
|
|
117
129
|
}
|
|
130
|
+
|
|
131
|
+
if (!json) printInitializationResult(initialized);
|
|
132
|
+
if (!json && !skipFeishu) process.stdout.write("\n正在启动 Agent Bot server...\n");
|
|
133
|
+
const server = await startInitializedServer({ skipFeishu, configPath });
|
|
134
|
+
const result: InitCommandResult = { ...initialized, server };
|
|
135
|
+
if (json) printJson(result);
|
|
136
|
+
else printInitializationServerResult(server);
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
async function initializeFeishu(
|
|
@@ -140,6 +159,7 @@ async function initializeFeishu(
|
|
|
140
159
|
credentials = {
|
|
141
160
|
appId: existing.appId!,
|
|
142
161
|
appSecret: existing.appSecret!,
|
|
162
|
+
userOpenId: existing.userOpenId,
|
|
143
163
|
};
|
|
144
164
|
status = "existing";
|
|
145
165
|
} else {
|
|
@@ -220,7 +240,12 @@ async function consoleCommand(input: string[]): Promise<void> {
|
|
|
220
240
|
throw new Error("agent-bot server 正在运行。为避免争用同一任务状态,请先停止 server,或明确使用 --force。");
|
|
221
241
|
}
|
|
222
242
|
const entry = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
223
|
-
const
|
|
243
|
+
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
244
|
+
prepareCrashReportDirectory(reportDirectory);
|
|
245
|
+
const result = spawnSync(process.execPath, [
|
|
246
|
+
...nodeDiagnosticReportArguments(reportDirectory),
|
|
247
|
+
entry,
|
|
248
|
+
], {
|
|
224
249
|
cwd: process.cwd(),
|
|
225
250
|
stdio: "inherit",
|
|
226
251
|
env: { ...process.env, AGENT_BOT_CONSOLE_ONLY: "1" },
|
|
@@ -247,29 +272,7 @@ async function serverCommand(input: string[]): Promise<void> {
|
|
|
247
272
|
return;
|
|
248
273
|
}
|
|
249
274
|
if (action === "start") {
|
|
250
|
-
|
|
251
|
-
if (await isServerRunning(endpoint)) {
|
|
252
|
-
process.stdout.write("agent-bot server 已在运行。\n");
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (await isServerReachable(endpoint)) {
|
|
256
|
-
const running = await waitForServer(endpoint, 45_000);
|
|
257
|
-
if (!running) throw new Error("agent-bot server 已启动,但未能连接飞书机器人。请检查日志。");
|
|
258
|
-
process.stdout.write("agent-bot server 已启动。\n");
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const entry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
262
|
-
const child = spawn(process.execPath, [entry], {
|
|
263
|
-
cwd: process.cwd(),
|
|
264
|
-
detached: true,
|
|
265
|
-
windowsHide: true,
|
|
266
|
-
stdio: "ignore",
|
|
267
|
-
env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
|
|
268
|
-
});
|
|
269
|
-
child.unref();
|
|
270
|
-
const running = await waitForServer(endpoint, 45_000);
|
|
271
|
-
if (!running) throw new Error("已启动 Supervisor,但 server 未在 45 秒内连接飞书机器人。请检查日志。");
|
|
272
|
-
process.stdout.write("agent-bot server 已启动。\n");
|
|
275
|
+
printServerStartResult(await startServer(config));
|
|
273
276
|
return;
|
|
274
277
|
}
|
|
275
278
|
if (action === "stop") {
|
|
@@ -374,16 +377,6 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
374
377
|
}
|
|
375
378
|
}
|
|
376
379
|
|
|
377
|
-
function parseGlobalOptions(input: string[]): { configPath?: string; args: string[] } {
|
|
378
|
-
const args = [...input];
|
|
379
|
-
const configIndex = args.indexOf("--config");
|
|
380
|
-
if (configIndex < 0) return { args };
|
|
381
|
-
const configPath = args[configIndex + 1];
|
|
382
|
-
if (!configPath) throw new Error("--config 需要配置文件路径。");
|
|
383
|
-
args.splice(configIndex, 2);
|
|
384
|
-
return { configPath, args };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
380
|
function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
|
|
388
381
|
const context = optionValue(args, "--context");
|
|
389
382
|
const status = optionValue(args, "--status");
|
|
@@ -481,7 +474,7 @@ function printInitializationPaths(result: InitializationResult): void {
|
|
|
481
474
|
process.stdout.write(`日志目录:${result.logs.path}(${initializationStatusLabel(result.logs.status)})\n`);
|
|
482
475
|
}
|
|
483
476
|
|
|
484
|
-
function printInitializationResult(result: InitCommandResult): void {
|
|
477
|
+
function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
|
|
485
478
|
process.stdout.write("\nAgent Bot 初始化完成。\n");
|
|
486
479
|
if (result.feishu.status === "created") {
|
|
487
480
|
process.stdout.write(`飞书应用:已创建并保存凭证(${result.feishu.appId})\n`);
|
|
@@ -511,7 +504,22 @@ function printInitializationResult(result: InitCommandResult): void {
|
|
|
511
504
|
process.stdout.write("飞书配置:权限、事件和回调均已就绪\n");
|
|
512
505
|
}
|
|
513
506
|
process.stdout.write(`配置文件:${result.config.path}\n`);
|
|
514
|
-
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function printInitializationServerResult(result: InitCommandResult["server"]): void {
|
|
510
|
+
if (result.status === "skipped") {
|
|
511
|
+
process.stdout.write("Agent Bot server:已跳过(未配置飞书,可使用 Console 模式)\n");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
printServerStartResult(result);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function printServerStartResult(result: ServerStartResult): void {
|
|
518
|
+
process.stdout.write(
|
|
519
|
+
result.status === "already-running"
|
|
520
|
+
? "agent-bot server 已在运行。\n"
|
|
521
|
+
: "agent-bot server 已启动。\n",
|
|
522
|
+
);
|
|
515
523
|
}
|
|
516
524
|
|
|
517
525
|
function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, json: boolean): void {
|
|
@@ -617,15 +625,6 @@ function optionValue(args: string[], name: string): string | undefined {
|
|
|
617
625
|
return index >= 0 ? args[index + 1] : undefined;
|
|
618
626
|
}
|
|
619
627
|
|
|
620
|
-
async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
|
|
621
|
-
const deadline = Date.now() + timeoutMs;
|
|
622
|
-
while (Date.now() < deadline) {
|
|
623
|
-
if (await isServerRunning(endpoint)) return true;
|
|
624
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
625
|
-
}
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
628
|
function printJson(value: unknown): void {
|
|
630
629
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
631
630
|
}
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -3,12 +3,18 @@ import path from "node:path";
|
|
|
3
3
|
import { config as loadDotEnv } from "dotenv";
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { appConfigSchema, type AppConfig } from "./schema.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AGENT_BOT_EXPLICIT_PROFILE_ENV,
|
|
8
|
+
defaultConfigPath,
|
|
9
|
+
defaultDotEnvPath,
|
|
10
|
+
resolveUserPath,
|
|
11
|
+
} from "./paths.js";
|
|
7
12
|
|
|
8
13
|
const ENV_PATTERN = /\$\{([A-Z0-9_]+)\}/gi;
|
|
9
14
|
const DEFAULT_CONFIG = `feishu:
|
|
10
15
|
appId: "\${FEISHU_APP_ID}"
|
|
11
16
|
appSecret: "\${FEISHU_APP_SECRET}"
|
|
17
|
+
userOpenId: "\${FEISHU_USER_OPEN_ID}"
|
|
12
18
|
|
|
13
19
|
console:
|
|
14
20
|
enabled: true
|
|
@@ -65,7 +71,11 @@ logging:
|
|
|
65
71
|
export function loadConfig(
|
|
66
72
|
configPath?: string,
|
|
67
73
|
): AppConfig {
|
|
68
|
-
loadDotEnv({
|
|
74
|
+
loadDotEnv({
|
|
75
|
+
path: defaultDotEnvPath(),
|
|
76
|
+
quiet: true,
|
|
77
|
+
override: process.env[AGENT_BOT_EXPLICIT_PROFILE_ENV] === "1",
|
|
78
|
+
});
|
|
69
79
|
|
|
70
80
|
const configuredPath = firstNonBlank(configPath, process.env.AGENT_BOT_CONFIG);
|
|
71
81
|
const absolutePath = resolveUserPath(configuredPath ?? defaultConfigPath());
|
package/src/config/paths.ts
CHANGED
|
@@ -2,6 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export const AGENT_BOT_HOME_ENV = "AGENT_BOT_HOME";
|
|
5
|
+
export const AGENT_BOT_EXPLICIT_PROFILE_ENV = "AGENT_BOT_EXPLICIT_PROFILE";
|
|
5
6
|
export const DEFAULT_CONFIG_FILE = "config.yaml";
|
|
6
7
|
export const DEFAULT_ENV_FILE = ".env";
|
|
7
8
|
export const DEFAULT_SQLITE_PATH = "./data/agent-bot.sqlite";
|
package/src/config/schema.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const appConfigSchema = z.object({
|
|
|
21
21
|
transport: z.enum(["auto", "sdk", "console"]).default("auto"),
|
|
22
22
|
appId: z.string().optional(),
|
|
23
23
|
appSecret: z.string().optional(),
|
|
24
|
+
userOpenId: z.string().optional(),
|
|
24
25
|
useConsoleWhenMissingCredentials: z.boolean().default(false),
|
|
25
26
|
}),
|
|
26
27
|
agents: z.record(z.string().min(1), agentConfigSchema).refine(
|
|
@@ -33,8 +33,9 @@ export interface StartupStatusView {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface SafeRestartStatusView {
|
|
36
|
+
scheduleId: number;
|
|
36
37
|
reason: string;
|
|
37
|
-
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting";
|
|
38
|
+
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled";
|
|
38
39
|
remainingMs?: number;
|
|
39
40
|
pendingFinalDeliveries: number;
|
|
40
41
|
waitingTasks: Array<{
|
|
@@ -270,6 +271,8 @@ export class CardRenderer {
|
|
|
270
271
|
? `${Math.max(0, Math.ceil((view.remainingMs ?? 0) / 1_000))}s`
|
|
271
272
|
: view.phase === "restarting"
|
|
272
273
|
? "0s"
|
|
274
|
+
: view.phase === "cancelled"
|
|
275
|
+
? "已取消"
|
|
273
276
|
: "等待阻塞项清空后开始";
|
|
274
277
|
const status = view.phase === "waiting_tasks"
|
|
275
278
|
? "🟠 等待任务完成"
|
|
@@ -277,7 +280,9 @@ export class CardRenderer {
|
|
|
277
280
|
? "🟠 等待最终结果投递"
|
|
278
281
|
: view.phase === "countdown"
|
|
279
282
|
? "🟡 空闲确认中"
|
|
280
|
-
:
|
|
283
|
+
: view.phase === "restarting"
|
|
284
|
+
? "🔄 正在重启"
|
|
285
|
+
: "⚪ 已取消";
|
|
281
286
|
const lines = [
|
|
282
287
|
`**状态**:${status}`,
|
|
283
288
|
`**重启原因**:${inlineCode(view.reason)}`,
|
|
@@ -296,7 +301,24 @@ export class CardRenderer {
|
|
|
296
301
|
} else {
|
|
297
302
|
elements.push({ tag: "hr" }, markdown("**当前等待的任务**:无"));
|
|
298
303
|
}
|
|
299
|
-
|
|
304
|
+
if (view.phase !== "restarting" && view.phase !== "cancelled") {
|
|
305
|
+
elements.push(
|
|
306
|
+
{ tag: "hr" },
|
|
307
|
+
taskActionRow([{
|
|
308
|
+
text: "Cancel",
|
|
309
|
+
value: {
|
|
310
|
+
action: "safe_restart_cancel",
|
|
311
|
+
scheduleId: String(view.scheduleId),
|
|
312
|
+
},
|
|
313
|
+
}]),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const template = view.phase === "restarting"
|
|
317
|
+
? "blue"
|
|
318
|
+
: view.phase === "cancelled"
|
|
319
|
+
? "grey"
|
|
320
|
+
: "orange";
|
|
321
|
+
return sectionCard("Agent Bot 安全重启", elements, template);
|
|
300
322
|
}
|
|
301
323
|
|
|
302
324
|
renderStartupStatus(view: StartupStatusView): Record<string, unknown> {
|
|
@@ -3,18 +3,11 @@ import type { AppConfig } from "../config/schema.js";
|
|
|
3
3
|
import { threadContextKey } from "./contextKey.js";
|
|
4
4
|
import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
7
|
-
|
|
8
|
-
interface FeishuConnectorOptions {
|
|
9
|
-
connectTimeoutMs?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
6
|
export class FeishuConnector {
|
|
13
7
|
constructor(
|
|
14
8
|
private readonly config: AppConfig,
|
|
15
9
|
private readonly handler: FeishuEventHandler,
|
|
16
10
|
private readonly logger: Logger,
|
|
17
|
-
private readonly options: FeishuConnectorOptions = {},
|
|
18
11
|
) {}
|
|
19
12
|
|
|
20
13
|
async start(): Promise<void> {
|
|
@@ -83,86 +76,12 @@ export class FeishuConnector {
|
|
|
83
76
|
};
|
|
84
77
|
},
|
|
85
78
|
});
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
loggerLevel: lark.LoggerLevel.debug,
|
|
93
|
-
});
|
|
94
|
-
await wsClient.start({ eventDispatcher });
|
|
95
|
-
await connected.promise;
|
|
96
|
-
this.logger.info("Feishu WebSocket connector connected.");
|
|
97
|
-
} finally {
|
|
98
|
-
connected.dispose();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function createConnectionWaiter(
|
|
104
|
-
logger: Logger,
|
|
105
|
-
timeoutMs: number,
|
|
106
|
-
): {
|
|
107
|
-
promise: Promise<void>;
|
|
108
|
-
sdkLogger: {
|
|
109
|
-
error: (...messages: unknown[]) => void;
|
|
110
|
-
warn: (...messages: unknown[]) => void;
|
|
111
|
-
info: (...messages: unknown[]) => void;
|
|
112
|
-
debug: (...messages: unknown[]) => void;
|
|
113
|
-
trace: (...messages: unknown[]) => void;
|
|
114
|
-
};
|
|
115
|
-
dispose: () => void;
|
|
116
|
-
} {
|
|
117
|
-
let resolveConnected!: () => void;
|
|
118
|
-
let rejectConnected!: (error: Error) => void;
|
|
119
|
-
let lastError: string | undefined;
|
|
120
|
-
let settled = false;
|
|
121
|
-
const promise = new Promise<void>((resolve, reject) => {
|
|
122
|
-
resolveConnected = resolve;
|
|
123
|
-
rejectConnected = reject;
|
|
124
|
-
});
|
|
125
|
-
const timer = setTimeout(() => {
|
|
126
|
-
if (settled) return;
|
|
127
|
-
settled = true;
|
|
128
|
-
const detail = lastError ? ` Last SDK error: ${lastError}` : "";
|
|
129
|
-
rejectConnected(new Error(`Timed out after ${timeoutMs}ms waiting for the Feishu WebSocket connection.${detail}`));
|
|
130
|
-
}, timeoutMs);
|
|
131
|
-
const logValues = (messages: unknown[]): string => messages.map(formatSdkLogValue).join(" ");
|
|
132
|
-
const sdkLogger = {
|
|
133
|
-
error: (...messages: unknown[]) => {
|
|
134
|
-
lastError = logValues(messages);
|
|
135
|
-
logger.error({ component: "feishu-websocket", sdkMessage: lastError }, "Feishu SDK error.");
|
|
136
|
-
},
|
|
137
|
-
warn: (...messages: unknown[]) => {
|
|
138
|
-
logger.warn({ component: "feishu-websocket", sdkMessage: logValues(messages) }, "Feishu SDK warning.");
|
|
139
|
-
},
|
|
140
|
-
info: (...messages: unknown[]) => {
|
|
141
|
-
logger.debug({ component: "feishu-websocket", sdkMessage: logValues(messages) }, "Feishu SDK status.");
|
|
142
|
-
},
|
|
143
|
-
debug: (...messages: unknown[]) => {
|
|
144
|
-
if (!messages.some((message) => message === "ws connect success")) return;
|
|
145
|
-
if (settled) return;
|
|
146
|
-
settled = true;
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
resolveConnected();
|
|
149
|
-
},
|
|
150
|
-
trace: () => undefined,
|
|
151
|
-
};
|
|
152
|
-
return {
|
|
153
|
-
promise,
|
|
154
|
-
sdkLogger,
|
|
155
|
-
dispose: () => clearTimeout(timer),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function formatSdkLogValue(value: unknown): string {
|
|
160
|
-
if (typeof value === "string") return value;
|
|
161
|
-
if (value instanceof Error) return value.message;
|
|
162
|
-
try {
|
|
163
|
-
return JSON.stringify(value);
|
|
164
|
-
} catch {
|
|
165
|
-
return String(value);
|
|
79
|
+
const wsClient = new lark.WSClient({
|
|
80
|
+
appId,
|
|
81
|
+
appSecret,
|
|
82
|
+
});
|
|
83
|
+
await wsClient.start({ eventDispatcher });
|
|
84
|
+
this.logger.info("Feishu WebSocket connector started.");
|
|
166
85
|
}
|
|
167
86
|
}
|
|
168
87
|
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,11 @@ import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
|
|
|
29
29
|
import { replacementSupervisorEnvironment } from "./supervision/replacementSupervisor.js";
|
|
30
30
|
import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
31
31
|
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
32
|
+
import {
|
|
33
|
+
nodeDiagnosticReportArguments,
|
|
34
|
+
prepareCrashReportDirectory,
|
|
35
|
+
resolveSupervisorDiagnosticsPaths,
|
|
36
|
+
} from "./supervision/SupervisorDiagnostics.js";
|
|
32
37
|
|
|
33
38
|
const processStartedAt = new Date();
|
|
34
39
|
const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
|
|
@@ -78,6 +83,7 @@ if (feishuOutbound) {
|
|
|
78
83
|
defaultAgentTitle: defaultAgent?.title ?? defaultAgentName,
|
|
79
84
|
cwd: path.resolve(config.defaults.cwd),
|
|
80
85
|
workspaceKind: defaultAgent?.kind === "codex" ? "projectless" : "project",
|
|
86
|
+
defaultUserOpenId: config.feishu.userOpenId,
|
|
81
87
|
}, metadataHydrator);
|
|
82
88
|
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger);
|
|
83
89
|
}
|
|
@@ -96,16 +102,17 @@ if (routes.length === 0) throw new Error("Neither Feishu nor console input is en
|
|
|
96
102
|
const outbound = new OutboundRouter(routes);
|
|
97
103
|
let shuttingDown = false;
|
|
98
104
|
let restartRequested = false;
|
|
99
|
-
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
100
|
-
supervised,
|
|
101
|
-
restart: requestRestart,
|
|
102
|
-
});
|
|
103
105
|
const safeRestart = new SafeRestartScheduler({
|
|
104
106
|
readActivity: () => store.getServerActivityState(),
|
|
105
107
|
onReady: (reason) => initiateRestart(reason),
|
|
106
108
|
onStatus: (status) => safeRestartNotifier?.update(status),
|
|
107
109
|
onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
|
|
108
110
|
});
|
|
111
|
+
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
112
|
+
supervised,
|
|
113
|
+
restart: requestRestart,
|
|
114
|
+
cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
|
|
115
|
+
});
|
|
109
116
|
|
|
110
117
|
if (feishuOutbound) feishuConnector = new FeishuConnector(config, controller, logger);
|
|
111
118
|
if (consoleEnabled) consoleConnector = new ConsoleConnector(controller, logger);
|
|
@@ -224,7 +231,12 @@ async function shutdown(exitCode: number, restartReason?: string): Promise<void>
|
|
|
224
231
|
|
|
225
232
|
function startReplacementSupervisor(restartReason: string): void {
|
|
226
233
|
const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
227
|
-
const
|
|
234
|
+
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
235
|
+
prepareCrashReportDirectory(reportDirectory);
|
|
236
|
+
const supervisor = spawn(process.execPath, [
|
|
237
|
+
...nodeDiagnosticReportArguments(reportDirectory),
|
|
238
|
+
supervisorEntry,
|
|
239
|
+
], {
|
|
228
240
|
cwd: process.cwd(),
|
|
229
241
|
detached: true,
|
|
230
242
|
windowsHide: true,
|
|
@@ -123,6 +123,7 @@ interface CreatedFeishuGroupContext {
|
|
|
123
123
|
export interface ProxyLifecycle {
|
|
124
124
|
supervised?: boolean;
|
|
125
125
|
restart(contextKey: string): Promise<void>;
|
|
126
|
+
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
|
|
@@ -176,9 +177,8 @@ export class ProxySessionController {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
async onMessage(message: IncomingMessage): Promise<void> {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
180
|
+
// The durable deduplication claim is the only operation allowed before acknowledgement.
|
|
181
|
+
// It prevents event retries from adding duplicate reactions.
|
|
182
182
|
if (!this.store.claimInboundEvent(message.messageId, "message")) return;
|
|
183
183
|
try {
|
|
184
184
|
const reactionId = await this.outbound.addReaction(
|
|
@@ -195,6 +195,9 @@ export class ProxySessionController {
|
|
|
195
195
|
"Failed to acknowledge the incoming Feishu message with a reaction.",
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
|
+
if (message.chatId && message.chatType) {
|
|
199
|
+
this.store.recordChatContext(baseChatContextKey(message.contextKey), message.chatType);
|
|
200
|
+
}
|
|
198
201
|
const replyTarget = message.replyInThread
|
|
199
202
|
? { messageId: message.messageId, replyInThread: true as const }
|
|
200
203
|
: undefined;
|
|
@@ -323,6 +326,18 @@ export class ProxySessionController {
|
|
|
323
326
|
await this.cancelSession(this.requireSession(contextKey, sessionId));
|
|
324
327
|
} else if (kind === "queued_prompt_cancel") {
|
|
325
328
|
await this.cancelQueuedPrompt(scopedAction);
|
|
329
|
+
} else if (kind === "safe_restart_cancel") {
|
|
330
|
+
const scheduleId = Number(scopedAction.value.scheduleId);
|
|
331
|
+
if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0) {
|
|
332
|
+
throw new Error("安全重启卡片无效,请使用最新的状态卡片。");
|
|
333
|
+
}
|
|
334
|
+
if (!this.lifecycle?.cancelSafeRestart) {
|
|
335
|
+
throw new Error("当前运行方式不支持取消安全重启。");
|
|
336
|
+
}
|
|
337
|
+
const cancelled = await this.lifecycle.cancelSafeRestart(scheduleId);
|
|
338
|
+
if (!cancelled) {
|
|
339
|
+
await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
|
|
340
|
+
}
|
|
326
341
|
} else if (kind === "session_more") {
|
|
327
342
|
await this.refreshSessionsCardFromAction(scopedAction, undefined, true);
|
|
328
343
|
} else if (kind === "session_switch") {
|
|
@@ -2,19 +2,25 @@ import type { Logger } from "pino";
|
|
|
2
2
|
import { detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
3
3
|
import { CardRenderer } from "../feishu/CardRenderer.js";
|
|
4
4
|
import type { FeishuOutbound } from "../feishu/types.js";
|
|
5
|
-
import type { SessionRecord, StateStore } from "../state/StateStore.js";
|
|
5
|
+
import type { SessionRecord, StateStore, UserContextRecord } from "../state/StateStore.js";
|
|
6
6
|
|
|
7
7
|
export interface StartupNotificationOptions {
|
|
8
8
|
defaultAgentName: string;
|
|
9
9
|
defaultAgentTitle: string;
|
|
10
10
|
cwd: string;
|
|
11
11
|
workspaceKind?: "project" | "projectless";
|
|
12
|
+
defaultUserOpenId?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface StartupTaskMetadataHydrator {
|
|
15
16
|
hydrate(session: SessionRecord): Promise<SessionRecord>;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
interface StartupNotificationTarget {
|
|
20
|
+
contextKey: string;
|
|
21
|
+
context?: UserContextRecord;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export class StartupNotifier {
|
|
19
25
|
constructor(
|
|
20
26
|
private readonly store: StateStore,
|
|
@@ -26,7 +32,7 @@ export class StartupNotifier {
|
|
|
26
32
|
) {}
|
|
27
33
|
|
|
28
34
|
async notify(startedAt: Date, restartReason: string): Promise<void> {
|
|
29
|
-
let targets;
|
|
35
|
+
let targets: StartupNotificationTarget[];
|
|
30
36
|
try {
|
|
31
37
|
const activeGroupSince = new Date(startedAt.getTime() - 3 * 60 * 1_000);
|
|
32
38
|
const chats = [
|
|
@@ -40,10 +46,16 @@ export class StartupNotifier {
|
|
|
40
46
|
}));
|
|
41
47
|
} catch (error) {
|
|
42
48
|
this.logger.warn({ error }, "Failed to load startup notification targets.");
|
|
43
|
-
|
|
49
|
+
throw new Error("Failed to load startup notification targets.", { cause: error });
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
if (targets.length === 0) {
|
|
53
|
+
const defaultUserOpenId = this.options.defaultUserOpenId?.trim();
|
|
54
|
+
if (!defaultUserOpenId) return;
|
|
55
|
+
targets = [{ contextKey: `open_id:${defaultUserOpenId}` }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const deliveries = await Promise.all(targets.map(async ({ contextKey, context }) => {
|
|
47
59
|
let session = context?.currentSessionId ? this.store.getSession(context.currentSessionId) : undefined;
|
|
48
60
|
if (session && this.metadataHydrator) {
|
|
49
61
|
try {
|
|
@@ -81,12 +93,17 @@ export class StartupNotifier {
|
|
|
81
93
|
});
|
|
82
94
|
try {
|
|
83
95
|
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
96
|
+
return true;
|
|
84
97
|
} catch (error) {
|
|
85
98
|
this.logger.warn(
|
|
86
99
|
{ error, contextKey },
|
|
87
100
|
"Failed to send startup status notification.",
|
|
88
101
|
);
|
|
102
|
+
return false;
|
|
89
103
|
}
|
|
90
104
|
}));
|
|
105
|
+
if (deliveries.length > 0 && !deliveries.some(Boolean)) {
|
|
106
|
+
throw new Error("Failed to send any startup status notification.");
|
|
107
|
+
}
|
|
91
108
|
}
|
|
92
109
|
}
|
|
@@ -14,6 +14,7 @@ export class SafeRestartNotifier {
|
|
|
14
14
|
private readonly messageIds = new Map<string, string>();
|
|
15
15
|
private readonly cardHashes = new Map<string, string>();
|
|
16
16
|
private activeScheduleId?: number;
|
|
17
|
+
private terminalScheduleId?: number;
|
|
17
18
|
private pendingInitialStatus?: SafeRestartStatus;
|
|
18
19
|
private initialCardTimer?: NodeJS.Timeout;
|
|
19
20
|
private queue = Promise.resolve();
|
|
@@ -27,13 +28,24 @@ export class SafeRestartNotifier {
|
|
|
27
28
|
) {}
|
|
28
29
|
|
|
29
30
|
update(status: SafeRestartStatus): Promise<void> {
|
|
31
|
+
if (this.terminalScheduleId !== undefined && status.scheduleId <= this.terminalScheduleId) {
|
|
32
|
+
if (status.phase !== "cancelled") return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
if (this.activeScheduleId !== undefined && status.scheduleId < this.activeScheduleId) {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
if (status.phase === "cancelled") {
|
|
38
|
+
this.terminalScheduleId = Math.max(this.terminalScheduleId ?? 0, status.scheduleId);
|
|
39
|
+
}
|
|
30
40
|
if (this.activeScheduleId !== status.scheduleId) {
|
|
31
41
|
this.activeScheduleId = status.scheduleId;
|
|
32
42
|
this.messageIds.clear();
|
|
33
43
|
this.cardHashes.clear();
|
|
34
44
|
this.clearInitialCardTimer();
|
|
35
45
|
this.pendingInitialStatus = status;
|
|
36
|
-
const delayMs =
|
|
46
|
+
const delayMs = status.phase === "cancelled"
|
|
47
|
+
? 0
|
|
48
|
+
: this.options.initialCardDelayMs ?? DEFAULT_INITIAL_CARD_DELAY_MS;
|
|
37
49
|
if (delayMs <= 0) return this.publishPendingInitialStatus();
|
|
38
50
|
this.initialCardTimer = setTimeout(() => {
|
|
39
51
|
this.initialCardTimer = undefined;
|
|
@@ -79,13 +91,16 @@ export class SafeRestartNotifier {
|
|
|
79
91
|
private async publish(status: SafeRestartStatus): Promise<void> {
|
|
80
92
|
const targets = this.store.listChatContexts("p2p");
|
|
81
93
|
if (targets.length === 0) return;
|
|
82
|
-
const waitingTasks =
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
const waitingTasks = status.phase === "cancelled"
|
|
95
|
+
? []
|
|
96
|
+
: this.store.listAllSessions()
|
|
97
|
+
.filter((session) => session.status === "running" || this.store.countQueuedPrompts(session.localSessionId) > 0)
|
|
98
|
+
.map((session) => ({
|
|
99
|
+
id: session.remoteSessionId ?? session.localSessionId,
|
|
100
|
+
title: session.title,
|
|
101
|
+
}));
|
|
88
102
|
const card = this.renderer.renderSafeRestartStatus({
|
|
103
|
+
scheduleId: status.scheduleId,
|
|
89
104
|
reason: status.reason,
|
|
90
105
|
phase: status.phase,
|
|
91
106
|
remainingMs: status.remainingMs,
|