@keyou007/agent-bot 0.1.12 → 0.1.13-alpha.1

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 (91) hide show
  1. package/.env.example +2 -2
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +32 -13
  4. package/README.zh.md +32 -13
  5. package/config.example.yaml +0 -24
  6. package/dist/cli/AgentPrerequisites.js +262 -0
  7. package/dist/cli/AgentPrerequisites.js.map +1 -0
  8. package/dist/cli/DefaultAgentSelection.js +47 -0
  9. package/dist/cli/DefaultAgentSelection.js.map +1 -0
  10. package/dist/cli/Initializer.js +165 -2
  11. package/dist/cli/Initializer.js.map +1 -1
  12. package/dist/cli/ServerStarter.js +17 -2
  13. package/dist/cli/ServerStarter.js.map +1 -1
  14. package/dist/cli/controlProtocol.js.map +1 -1
  15. package/dist/cli/help.js +16 -6
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli.js +233 -4
  18. package/dist/cli.js.map +1 -1
  19. package/dist/codex/AppServerConnection.js +3 -3
  20. package/dist/codex/AppServerConnection.js.map +1 -1
  21. package/dist/codex/CodexProcessManager.js +4 -4
  22. package/dist/codex/CodexProcessManager.js.map +1 -1
  23. package/dist/codex/CodexRuntime.js +28 -16
  24. package/dist/codex/CodexRuntime.js.map +1 -1
  25. package/dist/commands/CommandRouter.js +5 -0
  26. package/dist/commands/CommandRouter.js.map +1 -1
  27. package/dist/config/FeishuUserOpenIdStore.js +76 -0
  28. package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
  29. package/dist/config/loadConfig.js +3 -24
  30. package/dist/config/loadConfig.js.map +1 -1
  31. package/dist/console/ConsoleConnector.js +2 -2
  32. package/dist/console/ConsoleConnector.js.map +1 -1
  33. package/dist/feishu/CardRenderer.js +90 -30
  34. package/dist/feishu/CardRenderer.js.map +1 -1
  35. package/dist/index.js +104 -24
  36. package/dist/index.js.map +1 -1
  37. package/dist/presentation/turnGraph.js +36 -0
  38. package/dist/presentation/turnGraph.js.map +1 -0
  39. package/dist/proxy/ProxySessionController.js +321 -98
  40. package/dist/proxy/ProxySessionController.js.map +1 -1
  41. package/dist/runtime/AcpRuntimeAdapter.js +3 -2
  42. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  43. package/dist/startup/StartupNotifier.js +16 -7
  44. package/dist/startup/StartupNotifier.js.map +1 -1
  45. package/dist/startup/startFeishu.js +2 -2
  46. package/dist/startup/startFeishu.js.map +1 -1
  47. package/dist/state/StateStore.js +169 -0
  48. package/dist/state/StateStore.js.map +1 -1
  49. package/dist/state/migrations.js +21 -0
  50. package/dist/state/migrations.js.map +1 -1
  51. package/dist/supervision/SafeRestartNotifier.js +46 -3
  52. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  53. package/dist/supervision/SafeRestartScheduler.js +65 -6
  54. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  55. package/dist/supervision/replacementSupervisor.js +27 -2
  56. package/dist/supervision/replacementSupervisor.js.map +1 -1
  57. package/dist/supervisor.js +27 -2
  58. package/dist/supervisor.js.map +1 -1
  59. package/docs/technical-reference.md +42 -20
  60. package/docs/technical-reference.zh.md +42 -20
  61. package/npm-shrinkwrap.json +2 -2
  62. package/package.json +7 -3
  63. package/skills/agent-bot/SKILL.md +21 -9
  64. package/src/cli/AgentPrerequisites.ts +377 -0
  65. package/src/cli/DefaultAgentSelection.ts +56 -0
  66. package/src/cli/Initializer.ts +204 -3
  67. package/src/cli/ServerStarter.ts +25 -2
  68. package/src/cli/controlProtocol.ts +6 -1
  69. package/src/cli/help.ts +16 -6
  70. package/src/cli.ts +350 -5
  71. package/src/codex/AppServerConnection.ts +3 -3
  72. package/src/codex/CodexProcessManager.ts +4 -4
  73. package/src/codex/CodexRuntime.ts +38 -18
  74. package/src/commands/CommandRouter.ts +4 -0
  75. package/src/commands/commandTypes.ts +1 -0
  76. package/src/config/FeishuUserOpenIdStore.ts +81 -0
  77. package/src/config/loadConfig.ts +3 -24
  78. package/src/console/ConsoleConnector.ts +2 -2
  79. package/src/feishu/CardRenderer.ts +117 -30
  80. package/src/index.ts +162 -22
  81. package/src/presentation/turnGraph.ts +48 -0
  82. package/src/proxy/ProxySessionController.ts +375 -107
  83. package/src/runtime/AcpRuntimeAdapter.ts +3 -1
  84. package/src/startup/StartupNotifier.ts +23 -7
  85. package/src/startup/startFeishu.ts +3 -2
  86. package/src/state/StateStore.ts +221 -0
  87. package/src/state/migrations.ts +21 -0
  88. package/src/supervision/SafeRestartNotifier.ts +53 -4
  89. package/src/supervision/SafeRestartScheduler.ts +78 -7
  90. package/src/supervision/replacementSupervisor.ts +27 -1
  91. package/src/supervisor.ts +33 -2
@@ -11,7 +11,11 @@ import {
11
11
  import { refreshedSystemEnvironment } from "../supervision/systemEnvironment.js";
12
12
  import { controlEndpoint } from "./controlProtocol.js";
13
13
  import { cliText } from "./i18n.js";
14
- import { isServerReachable, isServerRunning } from "./LocalControlClient.js";
14
+ import {
15
+ isServerReachable,
16
+ isServerRunning,
17
+ sendControlRequest,
18
+ } from "./LocalControlClient.js";
15
19
 
16
20
  export interface ServerStartResult {
17
21
  status: "started" | "already-running";
@@ -19,6 +23,7 @@ export interface ServerStartResult {
19
23
 
20
24
  export type InitializationServerResult =
21
25
  | ServerStartResult
26
+ | { status: "restart-scheduled" }
22
27
  | { status: "skipped"; reason: "feishu-skipped" };
23
28
 
24
29
  export interface ServerStarterDependencies {
@@ -33,6 +38,7 @@ const SERVER_START_TIMEOUT_MS = 45_000;
33
38
  interface InitializationServerDependencies {
34
39
  loadConfig(configPath?: string): AppConfig;
35
40
  startServer(config: AppConfig): Promise<ServerStartResult>;
41
+ restartServer(config: AppConfig): Promise<void>;
36
42
  }
37
43
 
38
44
  export async function startInitializedServer(
@@ -42,7 +48,11 @@ export async function startInitializedServer(
42
48
  if (options.skipFeishu) {
43
49
  return { status: "skipped", reason: "feishu-skipped" };
44
50
  }
45
- return dependencies.startServer(dependencies.loadConfig(options.configPath));
51
+ const config = dependencies.loadConfig(options.configPath);
52
+ const result = await dependencies.startServer(config);
53
+ if (result.status !== "already-running") return result;
54
+ await dependencies.restartServer(config);
55
+ return { status: "restart-scheduled" };
46
56
  }
47
57
 
48
58
  export async function startServer(
@@ -116,6 +126,19 @@ const defaultDependencies: ServerStarterDependencies = {
116
126
  const defaultInitializationDependencies: InitializationServerDependencies = {
117
127
  loadConfig,
118
128
  startServer: (config) => startServer(config),
129
+ restartServer: async (config) => {
130
+ const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
131
+ action: "server_restart",
132
+ mode: "safe",
133
+ reason: "Agent Bot 初始化完成,需要加载当前版本",
134
+ });
135
+ if (!response.ok) {
136
+ throw new Error(response.message?.trim() || cliText(
137
+ "Could not schedule the Agent Bot restart after initialization.",
138
+ "初始化完成后无法安排 Agent Bot 重启。",
139
+ ));
140
+ }
141
+ },
119
142
  };
120
143
 
121
144
  async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
@@ -6,7 +6,12 @@ import type { SessionRecord } from "../state/StateStore.js";
6
6
 
7
7
  export type ControlRequest =
8
8
  | { action: "health" }
9
- | { action: "server_restart"; mode: "safe" | "immediate"; reason: string }
9
+ | {
10
+ action: "server_restart";
11
+ mode: "safe" | "immediate";
12
+ reason: string;
13
+ notificationSessionId?: string;
14
+ }
10
15
  | { action: "server_stop" }
11
16
  | { action: "task_status"; localSessionId: string }
12
17
  | { action: "task_stop"; localSessionId: string }
package/src/cli/help.ts CHANGED
@@ -3,18 +3,18 @@ import { cliLanguage, type CliLanguage } from "./i18n.js";
3
3
  export function renderCliHelp(version: string, language: CliLanguage = cliLanguage): string {
4
4
  if (language === "zh") {
5
5
  return `Agent Bot ${version}
6
- 通过飞书使用本地 Codex 和 ACP Agent。
6
+ 通过飞书使用本地 Codex、TraeX 和兼容 ACP 的 Agent。
7
7
 
8
8
  用法:
9
9
  agentbot [选项] <命令>
10
10
 
11
11
  常用命令:
12
- init 初始化飞书应用并启动服务
12
+ init 检查并选择 Agent,初始化飞书应用和服务
13
13
  init --reset 重置并重新配置显式指定的 Profile
14
14
  console 打开本地控制台
15
15
  server start 在后台启动 Agent Bot
16
16
  server status 显示服务、飞书 App ID 和安全重启状态
17
- server restart 安排安全重启
17
+ server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
18
18
  server stop 停止 Agent Bot
19
19
  task list 列出任务
20
20
  task status <任务> 显示任务状态
@@ -39,6 +39,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
39
39
  --skip-feishu 仅初始化 Console 环境
40
40
  --reconfigure-feishu 仅替换现有飞书凭据
41
41
 
42
+ 重启选项:
43
+ --task <任务> 将安全重启状态发回任务所在会话
44
+ --reason <原因> 设置重启原因
45
+ --immediate 立即重启,可能中断任务
46
+
42
47
  示例:
43
48
  agentbot init
44
49
  agentbot --profile ~/.agent-bot init --reset
@@ -51,18 +56,18 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
51
56
  }
52
57
 
53
58
  return `Agent Bot ${version}
54
- Use local Codex and ACP agents from Lark/Feishu.
59
+ Use Codex, TraeX, and compatible ACP agents from Lark/Feishu.
55
60
 
56
61
  Usage:
57
62
  agentbot [options] <command>
58
63
 
59
64
  Common commands:
60
- init Initialize the Lark app and start the service
65
+ init Check and select an Agent; initialize the Lark app and service
61
66
  init --reset Reset and reconfigure an explicit profile
62
67
  console Open the local console
63
68
  server start Start Agent Bot in the background
64
69
  server status Show the server, Lark App ID, and safe-restart status
65
- server restart Schedule a safe restart
70
+ server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
66
71
  server stop Stop Agent Bot
67
72
  task list List tasks
68
73
  task status <task> Show task status
@@ -87,6 +92,11 @@ Init options:
87
92
  --skip-feishu Initialize for Console only
88
93
  --reconfigure-feishu Replace only the existing Lark credentials
89
94
 
95
+ Restart options:
96
+ --task <task> Return safe-restart status to the task's conversation
97
+ --reason <reason> Set the restart reason
98
+ --immediate Restart immediately and possibly interrupt tasks
99
+
90
100
  Examples:
91
101
  agentbot init
92
102
  agentbot --profile ~/.agent-bot init --reset
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { createInterface } from "node:readline";
5
6
  import { loadConfig, loadConfigWithoutEnvironmentMutation } from "./config/loadConfig.js";
6
7
  import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.js";
7
8
  import {
@@ -14,8 +15,10 @@ import {
14
15
  acquireInitializationLock,
15
16
  cleanupFeishuCredentialTemporaryFiles,
16
17
  initializeAgentBot,
18
+ readConfiguredAgentSelection,
17
19
  readFeishuCredentials,
18
20
  shouldCreateFeishuApp,
21
+ writeDefaultAgent,
19
22
  writeFeishuCredentials,
20
23
  type InitializationResult,
21
24
  type InitializationStatus,
@@ -38,6 +41,18 @@ import {
38
41
  } from "./cli/FeishuAppConfiguration.js";
39
42
  import { renderCliHelp } from "./cli/help.js";
40
43
  import { cliLanguage, cliText, localizeCliErrorMessage } from "./cli/i18n.js";
44
+ import {
45
+ inspectSupportedAgent,
46
+ inspectSupportedAgents,
47
+ runSupportedAgentMaintenance,
48
+ type SupportedAgentInspection,
49
+ } from "./cli/AgentPrerequisites.js";
50
+ import {
51
+ parseMaintenanceSelection,
52
+ resolveDefaultAgentChoice,
53
+ selectableDefaultAgents,
54
+ type SelectableDefaultAgent,
55
+ } from "./cli/DefaultAgentSelection.js";
41
56
  import { formatServerStatus, withConfiguredFeishuAppId } from "./cli/serverStatus.js";
42
57
  import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
43
58
  import { readPackageVersion } from "./cli/packageVersion.js";
@@ -50,6 +65,8 @@ import {
50
65
  type ServerStartResult,
51
66
  } from "./cli/ServerStarter.js";
52
67
  import { taskChatRoute } from "./cli/taskChatRoute.js";
68
+ import { isThreadContextKey } from "./feishu/contextKey.js";
69
+ import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
53
70
  import {
54
71
  parseTaskForkGroupOptions,
55
72
  parseTaskNewGroupOptions,
@@ -112,14 +129,27 @@ async function main(input: string[]): Promise<void> {
112
129
  type FeishuInitializationStatus = "created" | "existing" | "skipped";
113
130
 
114
131
  interface InitCommandResult extends InitializationResult {
132
+ agents: InitializedAgent[];
133
+ defaultAgent: {
134
+ name: string;
135
+ status: "selected" | "existing";
136
+ };
115
137
  feishu: {
116
138
  status: FeishuInitializationStatus;
117
139
  appId?: string;
140
+ userOpenIdStatus?: "configured" | "pending";
118
141
  configuration?: EnsureFeishuAppConfigurationResult;
119
142
  };
120
143
  server: InitializationServerResult;
121
144
  }
122
145
 
146
+ interface InitializedAgent extends SupportedAgentInspection {
147
+ assistance?: {
148
+ status: "completed" | "skipped" | "unavailable" | "failed";
149
+ error?: string;
150
+ };
151
+ }
152
+
123
153
  async function initCommand(
124
154
  input: string[],
125
155
  configPath: string | undefined,
@@ -135,13 +165,15 @@ async function initCommand(
135
165
  if (options.reset) await assertResetProfileServerStopped(configPath);
136
166
  paths = initializeAgentBot({ configPath, reset: options.reset });
137
167
  if (!options.json) printInitializationPaths(paths);
168
+ const agents = await initializeSupportedAgents(options.json);
169
+ const defaultAgent = await selectDefaultAgent(paths.config.path, agents, options.json);
138
170
  initializationLock ??= acquireInitializationLock(paths.home.path);
139
171
  cleanupFeishuCredentialTemporaryFiles(paths.env.path);
140
172
  const feishu = await initializeFeishu(
141
173
  paths,
142
174
  options,
143
175
  );
144
- initialized = { ...paths, feishu };
176
+ initialized = { ...paths, agents, defaultAgent, feishu };
145
177
  } finally {
146
178
  initializationLock?.release();
147
179
  }
@@ -156,6 +188,259 @@ async function initCommand(
156
188
  else printInitializationServerResult(server);
157
189
  }
158
190
 
191
+ async function initializeSupportedAgents(json: boolean): Promise<InitializedAgent[]> {
192
+ const output = json ? process.stderr : process.stdout;
193
+ output.write(cliText(
194
+ "\nAgent setup\nChecking Codex and TraeX...\n",
195
+ "\nAgent 设置\n正在检查 Codex 和 TraeX...\n",
196
+ ));
197
+ const inspections = await inspectSupportedAgents();
198
+ for (const inspection of inspections) printAgentInspection(inspection, output);
199
+ const actionable = inspections.filter((inspection) => inspection.action);
200
+ if (actionable.length === 0) return inspections;
201
+
202
+ output.write(cliText("\nAvailable actions:\n", "\n可执行的操作:\n"));
203
+ for (const [index, inspection] of actionable.entries()) {
204
+ output.write(cliText(
205
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "Install" : "Upgrade"} ${inspection.name}: ${inspection.action?.command}\n`,
206
+ ` ${index + 1}. ${inspection.action?.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action?.command}\n`,
207
+ ));
208
+ }
209
+
210
+ let selected = new Set<number>();
211
+ if (!process.stdin.isTTY) {
212
+ output.write(cliText(
213
+ "No interactive terminal is available. Run the commands above manually if needed.\n",
214
+ "当前没有交互式终端,如有需要请手动执行上面的命令。\n",
215
+ ));
216
+ } else {
217
+ selected = new Set(await promptForAgentMaintenance(actionable.length));
218
+ if (selected.size === 0) {
219
+ output.write(cliText("No Agent changes selected.\n", "已跳过 Agent 安装和升级。\n"));
220
+ }
221
+ }
222
+
223
+ const initialized: InitializedAgent[] = [];
224
+ for (const inspection of inspections) {
225
+ if (!inspection.action) {
226
+ initialized.push(inspection);
227
+ continue;
228
+ }
229
+ const actionIndex = actionable.indexOf(inspection);
230
+ if (!selected.has(actionIndex)) {
231
+ initialized.push({
232
+ ...inspection,
233
+ assistance: { status: process.stdin.isTTY ? "skipped" : "unavailable" },
234
+ });
235
+ continue;
236
+ }
237
+
238
+ output.write(cliText(
239
+ `\nRunning ${inspection.name} ${inspection.action.kind}: ${inspection.action.command}\n`,
240
+ `\n正在${inspection.action.kind === "install" ? "安装" : "升级"} ${inspection.name}:${inspection.action.command}\n`,
241
+ ));
242
+ const maintenance = await runSupportedAgentMaintenance(inspection.id, inspection.action.kind);
243
+ if (maintenance.status !== 0 || maintenance.error) {
244
+ const error = maintenance.error ?? cliText(
245
+ `Command exited with code ${maintenance.status ?? "unknown"}.`,
246
+ `命令退出码为 ${maintenance.status ?? "未知"}。`,
247
+ );
248
+ output.write(cliText(
249
+ `${inspection.name} ${inspection.action.kind} did not complete; initialization will continue.\n`,
250
+ `${inspection.name}${inspection.action.kind === "install" ? "安装" : "升级"}未完成,初始化将继续。\n`,
251
+ ));
252
+ initialized.push({ ...inspection, assistance: { status: "failed", error } });
253
+ continue;
254
+ }
255
+
256
+ refreshCurrentProcessPath();
257
+ const refreshed = await inspectSupportedAgent(inspection.id);
258
+ if (refreshed.installedVersion) {
259
+ output.write(cliText(
260
+ `${inspection.name} is ready (${refreshed.installedVersion}).\n`,
261
+ `${inspection.name} 已就绪(${refreshed.installedVersion})。\n`,
262
+ ));
263
+ } else {
264
+ output.write(cliText(
265
+ `${inspection.name} command completed, but the CLI is not visible in this process. Open a new terminal and run agentbot init again.\n`,
266
+ `${inspection.name} 命令已完成,但当前进程仍无法找到该 CLI。请打开新终端后重新运行 agentbot init。\n`,
267
+ ));
268
+ }
269
+ initialized.push({ ...refreshed, assistance: { status: "completed" } });
270
+ }
271
+ return initialized;
272
+ }
273
+
274
+ async function selectDefaultAgent(
275
+ configPath: string,
276
+ inspections: InitializedAgent[],
277
+ json: boolean,
278
+ ): Promise<InitCommandResult["defaultAgent"]> {
279
+ const output = json ? process.stderr : process.stdout;
280
+ const configured = readConfiguredAgentSelection(configPath);
281
+ const choices = selectableDefaultAgents(configured.agents, inspections);
282
+ if (!process.stdin.isTTY) {
283
+ if (!configured.defaultAgent || !configured.agents.some((agent) => agent.name === configured.defaultAgent)) {
284
+ throw new Error(cliText(
285
+ "A default Agent is not configured. Run agentbot init in an interactive terminal and select one.",
286
+ "尚未配置默认 Agent。请在交互式终端中运行 agentbot init 并选择一个 Agent。",
287
+ ));
288
+ }
289
+ output.write(cliText(
290
+ `Default Agent: ${configured.defaultAgent} (kept because no interactive terminal is available)\n`,
291
+ `默认 Agent:${configured.defaultAgent}(当前没有交互式终端,已保留现有设置)\n`,
292
+ ));
293
+ return { name: configured.defaultAgent, status: "existing" };
294
+ }
295
+ if (choices.length === 0) {
296
+ throw new Error(cliText(
297
+ "No available configured Agent can be selected. Install Codex or TraeX, then run agentbot init again.",
298
+ "没有可选择的已配置 Agent。请安装 Codex 或 TraeX,然后重新运行 agentbot init。",
299
+ ));
300
+ }
301
+
302
+ output.write(cliText("\nSelect the default Agent:\n", "\n请选择默认 Agent:\n"));
303
+ for (const [index, choice] of choices.entries()) {
304
+ const version = choice.installedVersion ? ` (${choice.installedVersion})` : "";
305
+ const current = choice.name === configured.defaultAgent
306
+ ? cliText(" [current]", " [当前]")
307
+ : "";
308
+ output.write(` ${index + 1}. ${choice.name} - ${choice.title}${version}${current}\n`);
309
+ }
310
+ const selected = choices[await promptForDefaultAgent(choices, configured.defaultAgent)];
311
+ if (!selected) throw new Error(cliText("Default Agent selection failed.", "默认 Agent 选择失败。"));
312
+ const updated = writeDefaultAgent(configPath, selected.name);
313
+ output.write(updated
314
+ ? cliText(`Saved default Agent: ${selected.name}\n`, `已保存默认 Agent:${selected.name}\n`)
315
+ : cliText(`Default Agent unchanged: ${selected.name}\n`, `默认 Agent 保持不变:${selected.name}\n`));
316
+ return { name: selected.name, status: updated ? "selected" : "existing" };
317
+ }
318
+
319
+ function promptForAgentMaintenance(choiceCount: number): Promise<number[]> {
320
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
321
+ return new Promise((resolve, reject) => {
322
+ let settled = false;
323
+ const finish = (selection: number[]): void => {
324
+ settled = true;
325
+ readline.close();
326
+ resolve(selection);
327
+ };
328
+ const ask = (): void => {
329
+ readline.question(cliText(
330
+ "Enter action numbers separated by commas, type all, or press Enter to skip: ",
331
+ "请输入要执行的操作编号(多个用逗号分隔),输入 all 选择全部,直接回车跳过:",
332
+ ), (answer) => {
333
+ const selection = parseMaintenanceSelection(answer, choiceCount);
334
+ if (selection) {
335
+ finish(selection);
336
+ return;
337
+ }
338
+ process.stderr.write(cliText(
339
+ `Enter numbers from 1 to ${choiceCount}, all, or press Enter to skip.\n`,
340
+ `请输入 1 到 ${choiceCount} 之间的编号、all,或直接回车跳过。\n`,
341
+ ));
342
+ ask();
343
+ });
344
+ };
345
+ listenForPromptCancellation(readline, () => {
346
+ settled = true;
347
+ reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
348
+ }, () => settled);
349
+ ask();
350
+ });
351
+ }
352
+
353
+ function promptForDefaultAgent(
354
+ choices: SelectableDefaultAgent[],
355
+ currentAgent?: string,
356
+ ): Promise<number> {
357
+ const readline = createInterface({ input: process.stdin, output: process.stderr });
358
+ return new Promise((resolve, reject) => {
359
+ let settled = false;
360
+ const finish = (index: number): void => {
361
+ settled = true;
362
+ readline.close();
363
+ resolve(index);
364
+ };
365
+ const ask = (): void => {
366
+ const currentAvailable = choices.some((choice) => choice.name === currentAgent);
367
+ readline.question(cliText(
368
+ `Enter a number or Agent name${currentAvailable ? ` [${currentAgent}]` : ""}: `,
369
+ `请输入编号或 Agent 标准名${currentAvailable ? ` [${currentAgent}]` : ""}:`,
370
+ ), (answer) => {
371
+ const index = resolveDefaultAgentChoice(answer, choices, currentAgent);
372
+ if (index !== undefined) {
373
+ finish(index);
374
+ return;
375
+ }
376
+ process.stderr.write(cliText(
377
+ `Enter a number from 1 to ${choices.length} or an Agent standard name.\n`,
378
+ `请输入 1 到 ${choices.length} 之间的编号,或 Agent 标准名。\n`,
379
+ ));
380
+ ask();
381
+ });
382
+ };
383
+ listenForPromptCancellation(readline, () => {
384
+ settled = true;
385
+ reject(new Error(cliText("Initialization was cancelled.", "初始化已取消。")));
386
+ }, () => settled);
387
+ ask();
388
+ });
389
+ }
390
+
391
+ function listenForPromptCancellation(
392
+ readline: ReturnType<typeof createInterface>,
393
+ cancel: () => void,
394
+ isSettled: () => boolean,
395
+ ): void {
396
+ readline.once("SIGINT", () => {
397
+ if (!isSettled()) cancel();
398
+ readline.close();
399
+ });
400
+ readline.once("close", () => {
401
+ if (!isSettled()) cancel();
402
+ });
403
+ }
404
+
405
+ function printAgentInspection(
406
+ inspection: SupportedAgentInspection,
407
+ output: NodeJS.WriteStream,
408
+ ): void {
409
+ if (inspection.state === "missing") {
410
+ output.write(cliText(
411
+ ` ${inspection.name}: not installed\n`,
412
+ ` ${inspection.name}:未安装\n`,
413
+ ));
414
+ return;
415
+ }
416
+ if (inspection.state === "outdated") {
417
+ output.write(cliText(
418
+ ` ${inspection.name}: ${inspection.installedVersion}; update available (${inspection.latestVersion})\n`,
419
+ ` ${inspection.name}:${inspection.installedVersion};可升级到 ${inspection.latestVersion}\n`,
420
+ ));
421
+ return;
422
+ }
423
+ if (inspection.latestCheckFailed) {
424
+ output.write(cliText(
425
+ ` ${inspection.name}: ${inspection.installedVersion}; latest-version check unavailable\n`,
426
+ ` ${inspection.name}:${inspection.installedVersion};无法检查最新版本\n`,
427
+ ));
428
+ return;
429
+ }
430
+ output.write(cliText(
431
+ ` ${inspection.name}: ${inspection.installedVersion} (up to date)\n`,
432
+ ` ${inspection.name}:${inspection.installedVersion}(已是最新)\n`,
433
+ ));
434
+ }
435
+
436
+ function refreshCurrentProcessPath(): void {
437
+ const refreshed = refreshedSystemEnvironment();
438
+ if (!refreshed.refreshed) return;
439
+ const pathEntry = Object.entries(refreshed.environment)
440
+ .find(([name]) => name.toLowerCase() === "path");
441
+ if (pathEntry?.[1]) process.env.PATH = pathEntry[1];
442
+ }
443
+
159
444
  async function initializeFeishu(
160
445
  paths: InitializationResult,
161
446
  options: InitCommandOptions,
@@ -165,8 +450,14 @@ async function initializeFeishu(
165
450
  return {
166
451
  status: "skipped",
167
452
  ...(existing.appId ? { appId: existing.appId } : {}),
453
+ ...(existing.appId && existing.appSecret
454
+ ? { userOpenIdStatus: existing.userOpenId ? "configured" as const : "pending" as const }
455
+ : {}),
168
456
  };
169
457
  }
458
+ if (!options.json) {
459
+ process.stdout.write(cliText("\nLark setup\n", "\n飞书设置\n"));
460
+ }
170
461
 
171
462
  const controller = new AbortController();
172
463
  const onInterrupt = (): void => controller.abort(new Error(cliText(
@@ -202,8 +493,8 @@ async function initializeFeishu(
202
493
 
203
494
  if (!options.json) {
204
495
  process.stdout.write(cliText(
205
- "\nChecking Lark app permissions, events, and callbacks...\n",
206
- "\n正在检查飞书应用权限、事件和回调...\n",
496
+ "Checking app permissions, events, and callbacks...\n",
497
+ "正在检查应用权限、事件和回调...\n",
207
498
  ));
208
499
  }
209
500
  const manualPermissionSkip = new AbortController();
@@ -227,6 +518,7 @@ async function initializeFeishu(
227
518
  return {
228
519
  status,
229
520
  appId: credentials.appId,
521
+ userOpenIdStatus: credentials.userOpenId ? "configured" : "pending",
230
522
  configuration,
231
523
  };
232
524
  } finally {
@@ -373,12 +665,14 @@ async function serverCommand(input: string[]): Promise<void> {
373
665
  }
374
666
  if (action === "restart") {
375
667
  const immediate = rest.includes("--immediate") || rest.includes("--force");
668
+ const notificationSessionId = resolveRestartNotificationSessionId(config.storage.sqlitePath, rest);
376
669
  const reason = optionValue(rest, "--reason")
377
670
  ?? (immediate ? "通过 Agent Bot CLI 立即重启" : "通过 Agent Bot CLI 安全重启");
378
671
  ensureOk(await sendControlRequest(endpoint, {
379
672
  action: "server_restart",
380
673
  mode: immediate ? "immediate" : "safe",
381
674
  reason,
675
+ notificationSessionId,
382
676
  }));
383
677
  process.stdout.write(immediate
384
678
  ? cliText("Immediate restart requested.\n", "已请求立即重启。\n")
@@ -388,6 +682,42 @@ async function serverCommand(input: string[]): Promise<void> {
388
682
  throw new Error(cliText(`Unknown server command: ${action}`, `未知的 server 命令:${action}`));
389
683
  }
390
684
 
685
+ function resolveRestartNotificationSessionId(sqlitePath: string, args: string[]): string | undefined {
686
+ const store = new StateStore(sqlitePath);
687
+ try {
688
+ const sessions = store.listAllSessions();
689
+ if (args.includes("--task")) {
690
+ const reference = optionValue(args, "--task")?.trim();
691
+ if (!reference || reference.startsWith("--")) throw new Error(cliText(
692
+ "server restart --task requires a task number or task ID.",
693
+ "server restart --task 需要任务序号或任务 ID。",
694
+ ));
695
+ const session = resolveTask(sessions, reference);
696
+ if (
697
+ isThreadContextKey(session.contextKey)
698
+ && !store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
699
+ && !store.findLatestMessageIdForContext(session.contextKey)
700
+ ) {
701
+ throw new Error(cliText(
702
+ "The task topic has no message anchor for restart notifications. Send /restart in that topic instead.",
703
+ "该任务话题没有可用于重启通知的消息锚点,请直接在该话题中发送 /restart。",
704
+ ));
705
+ }
706
+ return session.localSessionId;
707
+ }
708
+ const runningContexts = new Set(
709
+ sessions.filter((session) => session.status === "running").map((session) => session.contextKey),
710
+ );
711
+ if (runningContexts.size > 1) throw new Error(cliText(
712
+ "Multiple conversations are running. Pass --task <task> so restart notifications return to the requester.",
713
+ "当前有多个会话正在运行。请传入 --task <任务>,确保重启通知返回发起会话。",
714
+ ));
715
+ return undefined;
716
+ } finally {
717
+ store.close();
718
+ }
719
+ }
720
+
391
721
  async function taskCommand(input: string[]): Promise<void> {
392
722
  const [action = "list", ...rest] = input;
393
723
  const config = loadConfig();
@@ -624,8 +954,8 @@ function printSkillStatus(status: SkillRegistrationStatus): void {
624
954
 
625
955
  function printInitializationPaths(result: InitializationResult): void {
626
956
  process.stdout.write(cliText(
627
- "Preparing the Agent Bot user environment...\n",
628
- "正在准备 Agent Bot 用户环境...\n",
957
+ "Profile setup\n",
958
+ "Profile 设置\n",
629
959
  ));
630
960
  if (result.reset) {
631
961
  process.stdout.write(`${cliText("Reset backup: ", "重置备份:")}${result.reset.backupPath}\n`);
@@ -642,6 +972,7 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
642
972
  "\nAgent Bot initialization completed.\n",
643
973
  "\nAgent Bot 初始化完成。\n",
644
974
  ));
975
+ process.stdout.write(`${cliText("Default Agent: ", "默认 Agent:")}${result.defaultAgent.name}\n`);
645
976
  if (result.feishu.status === "created") {
646
977
  process.stdout.write(cliText(
647
978
  `Lark app: created and credentials saved (${result.feishu.appId})\n`,
@@ -658,6 +989,12 @@ function printInitializationResult(result: Omit<InitCommandResult, "server">): v
658
989
  "飞书应用:已跳过;请使用 Console 模式或稍后重新运行 init。\n",
659
990
  ));
660
991
  }
992
+ if (result.feishu.userOpenIdStatus === "pending") {
993
+ process.stdout.write(cliText(
994
+ "Lark user: pending; send the bot a direct message to save FEISHU_USER_OPEN_ID\n",
995
+ "飞书用户:等待补全;请私聊机器人以保存 FEISHU_USER_OPEN_ID\n",
996
+ ));
997
+ }
661
998
  if (result.feishu.configuration?.status === "updated") {
662
999
  const added = result.feishu.configuration.added;
663
1000
  process.stdout.write(cliText(
@@ -697,6 +1034,13 @@ function printInitializationServerResult(result: InitCommandResult["server"]): v
697
1034
  ));
698
1035
  return;
699
1036
  }
1037
+ if (result.status === "restart-scheduled") {
1038
+ process.stdout.write(cliText(
1039
+ "Agent Bot server: safe restart scheduled to load the current version.\n",
1040
+ "Agent Bot 服务:已安排安全重启,以加载当前版本。\n",
1041
+ ));
1042
+ return;
1043
+ }
700
1044
  printServerStartResult(result);
701
1045
  }
702
1046
 
@@ -852,6 +1196,7 @@ function feishuFeatureWarnings(missing: FeishuConfigurationChallenge["missing"])
852
1196
 
853
1197
  function initializationStatusLabel(status: InitializationStatus): string {
854
1198
  if (status === "created") return cliText("created", "已创建");
1199
+ if (status === "updated") return cliText("updated with missing settings", "已补全缺失配置");
855
1200
  if (status === "reset") return cliText("reset from template", "已从模板重置");
856
1201
  return cliText("already exists; unchanged", "已存在,未修改");
857
1202
  }
@@ -108,7 +108,7 @@ export class AppServerConnection {
108
108
  try {
109
109
  message = JSON.parse(line) as AppServerMessage;
110
110
  } catch (error) {
111
- this.logger.warn({ line, error }, "Ignoring non-JSON Codex App Server stdout line.");
111
+ this.logger.warn({ line, error }, "Ignoring non-JSON App Server stdout line.");
112
112
  return;
113
113
  }
114
114
  this.handleMessage(message);
@@ -128,7 +128,7 @@ export class AppServerConnection {
128
128
  for (const listener of this.notificationListeners) listener(message.method, message.params);
129
129
  return;
130
130
  }
131
- this.logger.warn({ message }, "Ignoring unknown Codex App Server message shape.");
131
+ this.logger.warn({ message }, "Ignoring unknown App Server message shape.");
132
132
  }
133
133
 
134
134
  private handleResponse(message: AppServerResponse): void {
@@ -139,7 +139,7 @@ export class AppServerConnection {
139
139
  responseId: message.id,
140
140
  responseKind: "error" in message ? "error" : "result",
141
141
  },
142
- "Received Codex App Server response for unknown request.",
142
+ "Received App Server response for unknown request.",
143
143
  );
144
144
  return;
145
145
  }
@@ -27,15 +27,15 @@ export class CodexProcessManager implements AppServerClientProvider {
27
27
  const client = new AppServerConnection(child, this.logger.child({ component: "codex-app-server" }));
28
28
  this.client = client;
29
29
  readline.createInterface({ input: child.stderr, crlfDelay: Infinity }).on("line", (line) => {
30
- this.logger.debug({ line }, "Codex App Server stderr.");
30
+ this.logger.debug({ line }, "App Server stderr.");
31
31
  });
32
- child.once("error", (error) => this.logger.error({ error }, "Codex App Server process error."));
32
+ child.once("error", (error) => this.logger.error({ error }, "App Server process error."));
33
33
  child.once("exit", (code, signal) => {
34
- this.logger.warn({ code, signal }, "Codex App Server process exited.");
34
+ this.logger.warn({ code, signal }, "App Server process exited.");
35
35
  client.close();
36
36
  if (this.client === client) this.client = undefined;
37
37
  if (this.child === child) this.child = undefined;
38
- const error = new Error(`Codex App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
38
+ const error = new Error(`App Server exited (code=${code ?? "null"}, signal=${signal ?? "null"}).`);
39
39
  for (const listener of this.disconnectListeners) listener(error);
40
40
  });
41
41
  await client.request("initialize", {