@keyou007/agent-bot 0.1.1 → 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.
Files changed (73) hide show
  1. package/.env.example +3 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +23 -6
  4. package/README.zh.md +23 -6
  5. package/config.example.yaml +1 -2
  6. package/dist/cli/FeishuAppRegistration.js +12 -2
  7. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  8. package/dist/cli/Initializer.js +164 -14
  9. package/dist/cli/Initializer.js.map +1 -1
  10. package/dist/cli/LocalControlClient.js +14 -1
  11. package/dist/cli/LocalControlClient.js.map +1 -1
  12. package/dist/cli/ServerStarter.js +69 -0
  13. package/dist/cli/ServerStarter.js.map +1 -0
  14. package/dist/cli/help.js +6 -2
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/profile.js +40 -0
  17. package/dist/cli/profile.js.map +1 -0
  18. package/dist/cli.js +93 -89
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config/loadConfig.js +7 -4
  21. package/dist/config/loadConfig.js.map +1 -1
  22. package/dist/config/paths.js +1 -0
  23. package/dist/config/paths.js.map +1 -1
  24. package/dist/config/schema.js +2 -1
  25. package/dist/config/schema.js.map +1 -1
  26. package/dist/feishu/CardRenderer.js +21 -3
  27. package/dist/feishu/CardRenderer.js.map +1 -1
  28. package/dist/feishu/FeishuConnector.js +8 -11
  29. package/dist/feishu/FeishuConnector.js.map +1 -1
  30. package/dist/feishu/transport.js +4 -11
  31. package/dist/feishu/transport.js.map +1 -1
  32. package/dist/index.js +20 -8
  33. package/dist/index.js.map +1 -1
  34. package/dist/proxy/ProxySessionController.js +18 -3
  35. package/dist/proxy/ProxySessionController.js.map +1 -1
  36. package/dist/startup/StartupNotifier.js +13 -2
  37. package/dist/startup/StartupNotifier.js.map +1 -1
  38. package/dist/startup/startFeishu.js +2 -1
  39. package/dist/startup/startFeishu.js.map +1 -1
  40. package/dist/supervision/SafeRestartNotifier.js +23 -7
  41. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  42. package/dist/supervision/SafeRestartScheduler.js +20 -2
  43. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  44. package/dist/supervision/SupervisorDiagnostics.js +209 -0
  45. package/dist/supervision/SupervisorDiagnostics.js.map +1 -0
  46. package/dist/supervisor.js +54 -16
  47. package/dist/supervisor.js.map +1 -1
  48. package/docs/technical-reference.md +28 -14
  49. package/docs/technical-reference.zh.md +28 -14
  50. package/npm-shrinkwrap.json +2 -2
  51. package/package.json +1 -1
  52. package/skills/agent-bot/SKILL.md +24 -4
  53. package/src/cli/FeishuAppRegistration.ts +14 -1
  54. package/src/cli/Initializer.ts +174 -14
  55. package/src/cli/LocalControlClient.ts +12 -1
  56. package/src/cli/ServerStarter.ts +106 -0
  57. package/src/cli/help.ts +6 -2
  58. package/src/cli/profile.ts +59 -0
  59. package/src/cli.ts +114 -84
  60. package/src/config/loadConfig.ts +12 -4
  61. package/src/config/paths.ts +1 -0
  62. package/src/config/schema.ts +2 -1
  63. package/src/feishu/CardRenderer.ts +25 -3
  64. package/src/feishu/FeishuConnector.ts +8 -12
  65. package/src/feishu/transport.ts +7 -16
  66. package/src/index.ts +31 -8
  67. package/src/proxy/ProxySessionController.ts +18 -3
  68. package/src/startup/StartupNotifier.ts +21 -4
  69. package/src/startup/startFeishu.ts +2 -0
  70. package/src/supervision/SafeRestartNotifier.ts +22 -7
  71. package/src/supervision/SafeRestartScheduler.ts +27 -3
  72. package/src/supervision/SupervisorDiagnostics.ts +268 -0
  73. package/src/supervisor.ts +53 -15
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { loadConfig } from "../config/loadConfig.js";
4
+ import type { AppConfig } from "../config/schema.js";
5
+ import { requireServerFeishuTransport } from "../feishu/transport.js";
6
+ import {
7
+ nodeDiagnosticReportArguments,
8
+ prepareCrashReportDirectory,
9
+ resolveSupervisorDiagnosticsPaths,
10
+ } from "../supervision/SupervisorDiagnostics.js";
11
+ import { controlEndpoint } from "./controlProtocol.js";
12
+ import { isServerReachable, isServerRunning } from "./LocalControlClient.js";
13
+
14
+ export interface ServerStartResult {
15
+ status: "started" | "already-running";
16
+ }
17
+
18
+ export type InitializationServerResult =
19
+ | ServerStartResult
20
+ | { status: "skipped"; reason: "feishu-skipped" };
21
+
22
+ export interface ServerStarterDependencies {
23
+ isRunning(endpoint: string): Promise<boolean>;
24
+ isReachable(endpoint: string): Promise<boolean>;
25
+ waitUntilRunning(endpoint: string, timeoutMs: number): Promise<boolean>;
26
+ spawnSupervisor(config: AppConfig): void;
27
+ }
28
+
29
+ const SERVER_START_TIMEOUT_MS = 45_000;
30
+
31
+ interface InitializationServerDependencies {
32
+ loadConfig(configPath?: string): AppConfig;
33
+ startServer(config: AppConfig): Promise<ServerStartResult>;
34
+ }
35
+
36
+ export async function startInitializedServer(
37
+ options: { skipFeishu: boolean; configPath?: string },
38
+ dependencies: InitializationServerDependencies = defaultInitializationDependencies,
39
+ ): Promise<InitializationServerResult> {
40
+ if (options.skipFeishu) {
41
+ return { status: "skipped", reason: "feishu-skipped" };
42
+ }
43
+ return dependencies.startServer(dependencies.loadConfig(options.configPath));
44
+ }
45
+
46
+ export async function startServer(
47
+ config: AppConfig,
48
+ dependencies: ServerStarterDependencies = defaultDependencies,
49
+ ): Promise<ServerStartResult> {
50
+ requireServerFeishuTransport(config.feishu);
51
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
52
+
53
+ if (await dependencies.isRunning(endpoint)) {
54
+ return { status: "already-running" };
55
+ }
56
+ if (await dependencies.isReachable(endpoint)) {
57
+ const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
58
+ if (!running) {
59
+ throw new Error("agent-bot server 已启动,但未能连接飞书机器人。请检查日志。");
60
+ }
61
+ return { status: "started" };
62
+ }
63
+
64
+ dependencies.spawnSupervisor(config);
65
+ const running = await dependencies.waitUntilRunning(endpoint, SERVER_START_TIMEOUT_MS);
66
+ if (!running) {
67
+ throw new Error("已启动 Supervisor,但 server 未在 45 秒内连接飞书机器人。请检查日志。");
68
+ }
69
+ return { status: "started" };
70
+ }
71
+
72
+ const defaultDependencies: ServerStarterDependencies = {
73
+ isRunning: isServerRunning,
74
+ isReachable: isServerReachable,
75
+ waitUntilRunning: waitForServer,
76
+ spawnSupervisor: (config) => {
77
+ const entry = fileURLToPath(new URL("../supervisor.js", import.meta.url));
78
+ const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
79
+ prepareCrashReportDirectory(reportDirectory);
80
+ const child = spawn(process.execPath, [
81
+ ...nodeDiagnosticReportArguments(reportDirectory),
82
+ entry,
83
+ ], {
84
+ cwd: process.cwd(),
85
+ detached: true,
86
+ windowsHide: true,
87
+ stdio: "ignore",
88
+ env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
89
+ });
90
+ child.unref();
91
+ },
92
+ };
93
+
94
+ const defaultInitializationDependencies: InitializationServerDependencies = {
95
+ loadConfig,
96
+ startServer: (config) => startServer(config),
97
+ };
98
+
99
+ async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
100
+ const deadline = Date.now() + timeoutMs;
101
+ while (Date.now() < deadline) {
102
+ if (await isServerRunning(endpoint)) return true;
103
+ await new Promise((resolve) => setTimeout(resolve, 200));
104
+ }
105
+ return false;
106
+ }
package/src/cli/help.ts CHANGED
@@ -25,7 +25,7 @@ function renderChineseHelp(version: string): string {
25
25
  agent-bot [选项] <命令>
26
26
 
27
27
  常用命令:
28
- init 初始化配置、数据目录和飞书应用
28
+ init 初始化配置和飞书应用,并启动服务
29
29
  console 打开本地 Console
30
30
  server start 在后台启动 Agent Bot
31
31
  server status 查看服务和安全重启状态
@@ -41,6 +41,7 @@ function renderChineseHelp(version: string): string {
41
41
  skills install 安装内置 Skill
42
42
 
43
43
  选项:
44
+ --profile <directory> 使用指定目录中的独立 Profile
44
45
  --config <path> 使用指定配置文件
45
46
  --json 在支持的命令中输出 JSON
46
47
  -h, --help 显示帮助
@@ -48,6 +49,7 @@ function renderChineseHelp(version: string): string {
48
49
 
49
50
  示例:
50
51
  agent-bot init
52
+ agent-bot --profile ~/.agent-bot-rescue init
51
53
  agent-bot server start
52
54
  agent-bot server status
53
55
  agent-bot task list
@@ -63,7 +65,7 @@ Usage:
63
65
  agent-bot [options] <command>
64
66
 
65
67
  Common commands:
66
- init Initialize config, data directories, and the Lark app
68
+ init Initialize the Lark app and start the service
67
69
  console Open the local console
68
70
  server start Start Agent Bot in the background
69
71
  server status Show server and safe-restart status
@@ -79,6 +81,7 @@ Common commands:
79
81
  skills install Install the built-in Skill
80
82
 
81
83
  Options:
84
+ --profile <directory> Use an isolated profile directory
82
85
  --config <path> Use a specific config file
83
86
  --json Print JSON when supported
84
87
  -h, --help Show help
@@ -86,6 +89,7 @@ Options:
86
89
 
87
90
  Examples:
88
91
  agent-bot init
92
+ agent-bot --profile ~/.agent-bot-rescue init
89
93
  agent-bot server start
90
94
  agent-bot server status
91
95
  agent-bot task list
@@ -0,0 +1,59 @@
1
+ import path from "node:path";
2
+ import {
3
+ AGENT_BOT_EXPLICIT_PROFILE_ENV,
4
+ AGENT_BOT_HOME_ENV,
5
+ DEFAULT_CONFIG_FILE,
6
+ resolveUserPath,
7
+ } from "../config/paths.js";
8
+
9
+ export interface ParsedGlobalOptions {
10
+ args: string[];
11
+ configPath?: string;
12
+ profilePath?: string;
13
+ }
14
+
15
+ export interface AppliedProfile {
16
+ homePath: string;
17
+ configPath: string;
18
+ }
19
+
20
+ export function parseGlobalOptions(input: string[]): ParsedGlobalOptions {
21
+ const args = [...input];
22
+ const configPath = removeOption(args, "--config");
23
+ const profilePath = removeOption(args, "--profile");
24
+ if (configPath && profilePath) {
25
+ throw new Error("--profile 和 --config 不能同时使用。Profile 固定使用其目录下的 config.yaml。");
26
+ }
27
+ return {
28
+ args,
29
+ ...(configPath ? { configPath } : {}),
30
+ ...(profilePath ? { profilePath } : {}),
31
+ };
32
+ }
33
+
34
+ export function applyExplicitProfile(
35
+ profilePath: string,
36
+ env: NodeJS.ProcessEnv = process.env,
37
+ baseDirectory = process.cwd(),
38
+ ): AppliedProfile {
39
+ const homePath = resolveUserPath(profilePath, baseDirectory);
40
+ const configPath = path.join(homePath, DEFAULT_CONFIG_FILE);
41
+ env[AGENT_BOT_HOME_ENV] = homePath;
42
+ env.AGENT_BOT_CONFIG = configPath;
43
+ env[AGENT_BOT_EXPLICIT_PROFILE_ENV] = "1";
44
+ delete env.FEISHU_APP_ID;
45
+ delete env.FEISHU_APP_SECRET;
46
+ delete env.FEISHU_USER_OPEN_ID;
47
+ return { homePath, configPath };
48
+ }
49
+
50
+ function removeOption(args: string[], option: string): string | undefined {
51
+ const indexes = args.flatMap((value, index) => value === option ? [index] : []);
52
+ if (indexes.length > 1) throw new Error(`${option} 只能指定一次。`);
53
+ const index = indexes[0];
54
+ if (index === undefined) return undefined;
55
+ const value = args[index + 1];
56
+ if (!value || value.startsWith("--")) throw new Error(`${option} 需要目录或文件路径。`);
57
+ args.splice(index, 2);
58
+ return value;
59
+ }
package/src/cli.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from "node:child_process";
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, isServerRunning } from "./cli/LocalControlClient.js";
6
+ import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
7
7
  import {
8
8
  controlEndpoint,
9
9
  type ControlResponse,
10
10
  type TaskStatusControlData,
11
11
  } from "./cli/controlProtocol.js";
12
12
  import {
13
+ acquireInitializationLock,
14
+ cleanupFeishuCredentialTemporaryFiles,
13
15
  initializeAgentBot,
14
16
  readFeishuCredentials,
17
+ shouldCreateFeishuApp,
15
18
  writeFeishuCredentials,
16
19
  type InitializationResult,
17
20
  type InitializationStatus,
@@ -29,8 +32,20 @@ import {
29
32
  import { renderCliHelp } from "./cli/help.js";
30
33
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
31
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";
32
42
  import { taskChatRoute } from "./cli/taskChatRoute.js";
33
43
  import { StateStore, type SessionRecord } from "./state/StateStore.js";
44
+ import {
45
+ nodeDiagnosticReportArguments,
46
+ prepareCrashReportDirectory,
47
+ resolveSupervisorDiagnosticsPaths,
48
+ } from "./supervision/SupervisorDiagnostics.js";
34
49
 
35
50
  const args = process.argv.slice(2);
36
51
 
@@ -41,7 +56,8 @@ void main(args).catch((error: unknown) => {
41
56
 
42
57
  async function main(input: string[]): Promise<void> {
43
58
  const parsed = parseGlobalOptions(input);
44
- if (parsed.configPath) process.env.AGENT_BOT_CONFIG = parsed.configPath;
59
+ if (parsed.profilePath) applyExplicitProfile(parsed.profilePath);
60
+ else if (parsed.configPath) process.env.AGENT_BOT_CONFIG = parsed.configPath;
45
61
  const [command, ...rest] = parsed.args;
46
62
  if (!command || command === "help" || command === "--help" || command === "-h") {
47
63
  printHelp();
@@ -82,6 +98,7 @@ interface InitCommandResult extends InitializationResult {
82
98
  appId?: string;
83
99
  configuration?: EnsureFeishuAppConfigurationResult;
84
100
  };
101
+ server: InitializationServerResult;
85
102
  }
86
103
 
87
104
  async function initCommand(input: string[], configPath?: string): Promise<void> {
@@ -97,57 +114,80 @@ async function initCommand(input: string[], configPath?: string): Promise<void>
97
114
 
98
115
  const paths = initializeAgentBot({ configPath });
99
116
  if (!json) printInitializationPaths(paths);
117
+ const initializationLock = acquireInitializationLock(paths.home.path);
118
+
119
+ let initialized: Omit<InitCommandResult, "server">;
120
+ try {
121
+ cleanupFeishuCredentialTemporaryFiles(paths.env.path);
122
+ const feishu = await initializeFeishu(
123
+ paths,
124
+ { json, skipFeishu, reconfigureFeishu },
125
+ );
126
+ initialized = { ...paths, feishu };
127
+ } finally {
128
+ initializationLock.release();
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);
137
+ }
100
138
 
139
+ async function initializeFeishu(
140
+ paths: InitializationResult,
141
+ options: { json: boolean; skipFeishu: boolean; reconfigureFeishu: boolean },
142
+ ): Promise<InitCommandResult["feishu"]> {
101
143
  const existing = readFeishuCredentials(paths.env.path);
102
- let feishu: InitCommandResult["feishu"];
103
- if (skipFeishu) {
104
- feishu = {
144
+ if (options.skipFeishu) {
145
+ return {
105
146
  status: "skipped",
106
147
  ...(existing.appId ? { appId: existing.appId } : {}),
107
148
  };
108
- } else {
109
- const controller = new AbortController();
110
- const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
111
- process.once("SIGINT", onInterrupt);
112
- try {
113
- let credentials: FeishuAppCredentials;
114
- let status: Exclude<FeishuInitializationStatus, "skipped">;
115
- if (existing.status === "configured" && !reconfigureFeishu) {
116
- credentials = {
117
- appId: existing.appId!,
118
- appSecret: existing.appSecret!,
119
- };
120
- status = "existing";
121
- } else {
122
- if (existing.status === "incomplete" && !reconfigureFeishu) {
123
- throw new Error(`检测到不完整的飞书凭证:请补全 ${paths.env.path},或使用 --reconfigure-feishu 重新创建应用。`);
124
- }
125
- credentials = await registerFeishuApp({
126
- signal: controller.signal,
127
- onVerification: (challenge) => printFeishuVerification(challenge, json),
128
- });
129
- writeFeishuCredentials(paths.env.path, credentials);
130
- status = "created";
131
- }
149
+ }
132
150
 
133
- if (!json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
134
- const configuration = await ensureFeishuAppConfiguration(credentials, {
151
+ const controller = new AbortController();
152
+ const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
153
+ process.once("SIGINT", onInterrupt);
154
+ process.once("SIGTERM", onInterrupt);
155
+ try {
156
+ let credentials: FeishuAppCredentials;
157
+ let status: Exclude<FeishuInitializationStatus, "skipped">;
158
+ if (!shouldCreateFeishuApp(existing, options.reconfigureFeishu)) {
159
+ credentials = {
160
+ appId: existing.appId!,
161
+ appSecret: existing.appSecret!,
162
+ userOpenId: existing.userOpenId,
163
+ };
164
+ status = "existing";
165
+ } else {
166
+ if (existing.status === "incomplete" && !options.json) {
167
+ process.stdout.write("\n检测到未完整保存的飞书凭据,将重新创建机器人。\n");
168
+ }
169
+ credentials = await registerFeishuApp({
135
170
  signal: controller.signal,
136
- onVerification: (challenge) => printFeishuConfigurationVerification(challenge, json),
171
+ onVerification: (challenge) => printFeishuVerification(challenge, options.json),
137
172
  });
138
- feishu = {
139
- status,
140
- appId: credentials.appId,
141
- configuration,
142
- };
143
- } finally {
144
- process.removeListener("SIGINT", onInterrupt);
173
+ writeFeishuCredentials(paths.env.path, credentials);
174
+ status = "created";
145
175
  }
146
- }
147
176
 
148
- const result: InitCommandResult = { ...paths, feishu };
149
- if (json) printJson(result);
150
- else printInitializationResult(result);
177
+ if (!options.json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
178
+ const configuration = await ensureFeishuAppConfiguration(credentials, {
179
+ signal: controller.signal,
180
+ onVerification: (challenge) => printFeishuConfigurationVerification(challenge, options.json),
181
+ });
182
+ return {
183
+ status,
184
+ appId: credentials.appId,
185
+ configuration,
186
+ };
187
+ } finally {
188
+ process.removeListener("SIGINT", onInterrupt);
189
+ process.removeListener("SIGTERM", onInterrupt);
190
+ }
151
191
  }
152
192
 
153
193
  function skillsCommand(input: string[]): void {
@@ -196,11 +236,16 @@ async function consoleCommand(input: string[]): Promise<void> {
196
236
  const config = loadConfig();
197
237
  const endpoint = controlEndpoint(config.storage.sqlitePath);
198
238
  const force = input.includes("--force");
199
- if (!force && await isServerRunning(endpoint)) {
239
+ if (!force && await isServerReachable(endpoint)) {
200
240
  throw new Error("agent-bot server 正在运行。为避免争用同一任务状态,请先停止 server,或明确使用 --force。");
201
241
  }
202
242
  const entry = fileURLToPath(new URL("./index.js", import.meta.url));
203
- const result = spawnSync(process.execPath, [entry], {
243
+ const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
244
+ prepareCrashReportDirectory(reportDirectory);
245
+ const result = spawnSync(process.execPath, [
246
+ ...nodeDiagnosticReportArguments(reportDirectory),
247
+ entry,
248
+ ], {
204
249
  cwd: process.cwd(),
205
250
  stdio: "inherit",
206
251
  env: { ...process.env, AGENT_BOT_CONSOLE_ONLY: "1" },
@@ -227,22 +272,7 @@ async function serverCommand(input: string[]): Promise<void> {
227
272
  return;
228
273
  }
229
274
  if (action === "start") {
230
- if (await isServerRunning(endpoint)) {
231
- process.stdout.write("agent-bot server 已在运行。\n");
232
- return;
233
- }
234
- const entry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
235
- const child = spawn(process.execPath, [entry], {
236
- cwd: process.cwd(),
237
- detached: true,
238
- windowsHide: true,
239
- stdio: "ignore",
240
- env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
241
- });
242
- child.unref();
243
- const running = await waitForServer(endpoint, 30_000);
244
- if (!running) throw new Error("已启动 Supervisor,但 server 未在 30 秒内就绪。请检查日志。");
245
- process.stdout.write("agent-bot server 已启动。\n");
275
+ printServerStartResult(await startServer(config));
246
276
  return;
247
277
  }
248
278
  if (action === "stop") {
@@ -347,16 +377,6 @@ async function taskCommand(input: string[]): Promise<void> {
347
377
  }
348
378
  }
349
379
 
350
- function parseGlobalOptions(input: string[]): { configPath?: string; args: string[] } {
351
- const args = [...input];
352
- const configIndex = args.indexOf("--config");
353
- if (configIndex < 0) return { args };
354
- const configPath = args[configIndex + 1];
355
- if (!configPath) throw new Error("--config 需要配置文件路径。");
356
- args.splice(configIndex, 2);
357
- return { configPath, args };
358
- }
359
-
360
380
  function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
361
381
  const context = optionValue(args, "--context");
362
382
  const status = optionValue(args, "--status");
@@ -422,7 +442,11 @@ function printServerStatus(data: unknown): void {
422
442
  const activity = value.activity && typeof value.activity === "object"
423
443
  ? value.activity as Record<string, unknown>
424
444
  : {};
425
- process.stdout.write("agent-bot server:运行中\n");
445
+ process.stdout.write(
446
+ value.ready === false
447
+ ? "agent-bot server:启动中(正在连接飞书机器人)\n"
448
+ : "agent-bot server:运行中\n",
449
+ );
426
450
  process.stdout.write(`PID:${value.pid ?? "-"}\n`);
427
451
  process.stdout.write(`启动时间:${value.startedAt ?? "-"}\n`);
428
452
  process.stdout.write(`Supervisor:${value.supervised ? "已启用" : "未启用"}\n`);
@@ -450,7 +474,7 @@ function printInitializationPaths(result: InitializationResult): void {
450
474
  process.stdout.write(`日志目录:${result.logs.path}(${initializationStatusLabel(result.logs.status)})\n`);
451
475
  }
452
476
 
453
- function printInitializationResult(result: InitCommandResult): void {
477
+ function printInitializationResult(result: Omit<InitCommandResult, "server">): void {
454
478
  process.stdout.write("\nAgent Bot 初始化完成。\n");
455
479
  if (result.feishu.status === "created") {
456
480
  process.stdout.write(`飞书应用:已创建并保存凭证(${result.feishu.appId})\n`);
@@ -480,7 +504,22 @@ function printInitializationResult(result: InitCommandResult): void {
480
504
  process.stdout.write("飞书配置:权限、事件和回调均已就绪\n");
481
505
  }
482
506
  process.stdout.write(`配置文件:${result.config.path}\n`);
483
- process.stdout.write("下一步:运行 agent-bot server start。\n");
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
+ );
484
523
  }
485
524
 
486
525
  function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, json: boolean): void {
@@ -586,15 +625,6 @@ function optionValue(args: string[], name: string): string | undefined {
586
625
  return index >= 0 ? args[index + 1] : undefined;
587
626
  }
588
627
 
589
- async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
590
- const deadline = Date.now() + timeoutMs;
591
- while (Date.now() < deadline) {
592
- if (await isServerRunning(endpoint)) return true;
593
- await new Promise((resolve) => setTimeout(resolve, 200));
594
- }
595
- return false;
596
- }
597
-
598
628
  function printJson(value: unknown): void {
599
629
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
600
630
  }
@@ -3,14 +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 { defaultConfigPath, defaultDotEnvPath, resolveUserPath } from "./paths.js";
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
- transport: "auto"
11
15
  appId: "\${FEISHU_APP_ID}"
12
16
  appSecret: "\${FEISHU_APP_SECRET}"
13
- useConsoleWhenMissingCredentials: true
17
+ userOpenId: "\${FEISHU_USER_OPEN_ID}"
14
18
 
15
19
  console:
16
20
  enabled: true
@@ -67,7 +71,11 @@ logging:
67
71
  export function loadConfig(
68
72
  configPath?: string,
69
73
  ): AppConfig {
70
- loadDotEnv({ path: defaultDotEnvPath(), quiet: true });
74
+ loadDotEnv({
75
+ path: defaultDotEnvPath(),
76
+ quiet: true,
77
+ override: process.env[AGENT_BOT_EXPLICIT_PROFILE_ENV] === "1",
78
+ });
71
79
 
72
80
  const configuredPath = firstNonBlank(configPath, process.env.AGENT_BOT_CONFIG);
73
81
  const absolutePath = resolveUserPath(configuredPath ?? defaultConfigPath());
@@ -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";
@@ -21,7 +21,8 @@ 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
- useConsoleWhenMissingCredentials: z.boolean().default(true),
24
+ userOpenId: z.string().optional(),
25
+ useConsoleWhenMissingCredentials: z.boolean().default(false),
25
26
  }),
26
27
  agents: z.record(z.string().min(1), agentConfigSchema).refine(
27
28
  (agents) => Object.keys(agents).length > 0,
@@ -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
- return sectionCard("Agent Bot 安全重启", elements, view.phase === "restarting" ? "blue" : "orange");
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> {
@@ -25,7 +25,6 @@ export class FeishuConnector {
25
25
 
26
26
  private async startFeishuWs(appId: string, appSecret: string): Promise<void> {
27
27
  const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
28
- const wsClient = new lark.WSClient({ appId, appSecret });
29
28
  const eventDispatcher = new lark.EventDispatcher({}).register({
30
29
  "im.message.receive_v1": async (data: unknown) => {
31
30
  const message = toIncomingMessage(data);
@@ -77,8 +76,11 @@ export class FeishuConnector {
77
76
  };
78
77
  },
79
78
  });
80
-
81
- wsClient.start({ eventDispatcher });
79
+ const wsClient = new lark.WSClient({
80
+ appId,
81
+ appSecret,
82
+ });
83
+ await wsClient.start({ eventDispatcher });
82
84
  this.logger.info("Feishu WebSocket connector started.");
83
85
  }
84
86
  }
@@ -86,12 +88,8 @@ export class FeishuConnector {
86
88
  function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
87
89
  const event = getFeishuEvent(data);
88
90
  const chatId = typeof event?.chat_id === "string" ? event.chat_id : undefined;
89
- const beforeName = typeof event?.before_change?.name === "string"
90
- ? event.before_change.name
91
- : undefined;
92
- const afterName = typeof event?.after_change?.name === "string"
93
- ? event.after_change.name
94
- : undefined;
91
+ const beforeName = typeof event?.before_change?.name === "string" ? event.before_change.name : undefined;
92
+ const afterName = typeof event?.after_change?.name === "string" ? event.after_change.name : undefined;
95
93
  if (!chatId || !afterName || beforeName === afterName) return undefined;
96
94
  return {
97
95
  chatId,
@@ -118,9 +116,7 @@ function toIncomingMessage(data: unknown): IncomingMessage | undefined {
118
116
  const parentMessageId = typeof message.parent_id === "string" && message.parent_id ? message.parent_id : undefined;
119
117
  const threadContext = Boolean(chatId && threadId);
120
118
  const senderId =
121
- event.sender?.sender_id?.open_id ??
122
- event.sender?.sender_id?.user_id ??
123
- event.sender?.sender_id?.union_id;
119
+ event.sender?.sender_id?.open_id ?? event.sender?.sender_id?.user_id ?? event.sender?.sender_id?.union_id;
124
120
 
125
121
  if (!chatId && !senderId) {
126
122
  return undefined;