@keyou007/agent-bot 0.1.14-alpha.0 → 0.1.14-alpha.2

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 (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +82 -55
  3. package/README.zh.md +82 -55
  4. package/config.example.yaml +5 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +17 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +92 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +643 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +16 -5
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +20 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/config/loadConfig.js +2 -0
  33. package/dist/config/loadConfig.js.map +1 -1
  34. package/dist/config/schema.js +2 -0
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/console/ConsoleTurnPresenter.js +2 -0
  37. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  38. package/dist/feishu/CardRenderer.js +584 -16
  39. package/dist/feishu/CardRenderer.js.map +1 -1
  40. package/dist/feishu/FeishuConnector.js +29 -8
  41. package/dist/feishu/FeishuConnector.js.map +1 -1
  42. package/dist/feishu/FeishuMarkdown.js +25 -1
  43. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  44. package/dist/feishu/FeishuMessageClient.js +43 -0
  45. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  46. package/dist/feishu/FeishuTurnPresenter.js +29 -1
  47. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  48. package/dist/feishu/ownerAccess.js +7 -0
  49. package/dist/feishu/ownerAccess.js.map +1 -0
  50. package/dist/index.js +148 -19
  51. package/dist/index.js.map +1 -1
  52. package/dist/presentation/OutboundRouter.js +19 -1
  53. package/dist/presentation/OutboundRouter.js.map +1 -1
  54. package/dist/proxy/ProxySessionController.js +1444 -66
  55. package/dist/proxy/ProxySessionController.js.map +1 -1
  56. package/dist/startup/StartupNotifier.js +65 -16
  57. package/dist/startup/StartupNotifier.js.map +1 -1
  58. package/dist/startup/startFeishu.js +2 -2
  59. package/dist/startup/startFeishu.js.map +1 -1
  60. package/dist/state/StateStore.js +268 -67
  61. package/dist/state/StateStore.js.map +1 -1
  62. package/dist/state/migrations.js +26 -0
  63. package/dist/state/migrations.js.map +1 -1
  64. package/dist/supervision/SafeRestartNotifier.js +17 -16
  65. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  66. package/dist/supervision/SafeRestartScheduler.js +12 -7
  67. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  68. package/dist/supervision/replacementSupervisor.js +45 -4
  69. package/dist/supervision/replacementSupervisor.js.map +1 -1
  70. package/dist/supervisor.js +30 -18
  71. package/dist/supervisor.js.map +1 -1
  72. package/docs/technical-reference.md +33 -10
  73. package/docs/technical-reference.zh.md +33 -10
  74. package/npm-shrinkwrap.json +2 -2
  75. package/package.json +2 -2
  76. package/skills/agent-bot/SKILL.md +107 -182
  77. package/src/cli/AutostartManager.ts +579 -0
  78. package/src/cli/FeishuAppConfiguration.ts +22 -7
  79. package/src/cli/Initializer.ts +57 -0
  80. package/src/cli/SelfUpdateRunner.ts +817 -0
  81. package/src/cli/SelfUpdater.ts +688 -0
  82. package/src/cli/autostartOutput.ts +75 -0
  83. package/src/cli/controlProtocol.ts +111 -1
  84. package/src/cli/help.ts +92 -18
  85. package/src/cli/taskGroupOptions.ts +27 -17
  86. package/src/cli/taskListOutput.ts +10 -0
  87. package/src/cli/taskTarget.ts +168 -0
  88. package/src/cli.ts +781 -65
  89. package/src/codex/CodexRuntime.ts +17 -5
  90. package/src/commands/CommandRouter.ts +19 -1
  91. package/src/commands/commandTypes.ts +3 -0
  92. package/src/config/loadConfig.ts +2 -0
  93. package/src/config/schema.ts +2 -0
  94. package/src/console/ConsoleTurnPresenter.ts +4 -1
  95. package/src/feishu/CardRenderer.ts +716 -16
  96. package/src/feishu/FeishuConnector.ts +44 -10
  97. package/src/feishu/FeishuMarkdown.ts +27 -1
  98. package/src/feishu/FeishuMessageClient.ts +58 -0
  99. package/src/feishu/FeishuTurnPresenter.ts +35 -2
  100. package/src/feishu/ownerAccess.ts +7 -0
  101. package/src/feishu/types.ts +7 -0
  102. package/src/index.ts +192 -22
  103. package/src/presentation/OutboundRouter.ts +28 -3
  104. package/src/proxy/ProxySessionController.ts +1707 -70
  105. package/src/runtime/types.ts +1 -0
  106. package/src/startup/StartupNotifier.ts +67 -17
  107. package/src/startup/startFeishu.ts +5 -3
  108. package/src/state/StateStore.ts +413 -65
  109. package/src/state/migrations.ts +26 -0
  110. package/src/supervision/SafeRestartNotifier.ts +17 -17
  111. package/src/supervision/SafeRestartScheduler.ts +13 -6
  112. package/src/supervision/replacementSupervisor.ts +50 -4
  113. package/src/supervisor.ts +35 -16
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
19
19
  import { FeishuConnector } from "./feishu/FeishuConnector.js";
20
20
  import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
21
21
  import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
22
- import { baseChatContextKey, isThreadContextKey } from "./feishu/contextKey.js";
22
+ import { isThreadContextKey } from "./feishu/contextKey.js";
23
23
  import type { MessageReplyTarget } from "./feishu/types.js";
24
24
  import { requireServerFeishuTransport } from "./feishu/transport.js";
25
25
  import { createLogger } from "./logging/logger.js";
@@ -34,8 +34,10 @@ import { startFeishu } from "./startup/startFeishu.js";
34
34
  import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
35
35
  import {
36
36
  RESTART_GROUP_CONTEXTS_ENV,
37
+ RESTART_NOTIFICATION_TARGETS_ENV,
37
38
  replacementSupervisorEnvironment,
38
39
  restartGroupContextKeysFromEnvironment,
40
+ restartNotificationTargetsFromEnvironment,
39
41
  } from "./supervision/replacementSupervisor.js";
40
42
  import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
41
43
  import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
@@ -46,14 +48,22 @@ import {
46
48
  resolveSupervisorDiagnosticsPaths,
47
49
  } from "./supervision/SupervisorDiagnostics.js";
48
50
  import { refreshedSystemEnvironment } from "./supervision/systemEnvironment.js";
51
+ import {
52
+ assertSelfUpdatePlanPath,
53
+ launchSelfUpdateRunner,
54
+ readPendingSelfUpdate,
55
+ releaseSelfUpdatePlan,
56
+ } from "./cli/SelfUpdater.js";
49
57
 
50
58
  const processStartedAt = new Date();
51
59
  const agentBotVersion = readPackageVersion();
52
60
  const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
53
61
  const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
54
62
  || (supervised ? "Supervisor 启动" : "直接启动");
55
- const restartGroupContextKeys = restartGroupContextKeysFromEnvironment(
56
- process.env[RESTART_GROUP_CONTEXTS_ENV],
63
+ const restartNotificationTargets = mergeRestartNotificationTargets(
64
+ restartNotificationTargetsFromEnvironment(process.env[RESTART_NOTIFICATION_TARGETS_ENV]),
65
+ restartGroupContextKeysFromEnvironment(process.env[RESTART_GROUP_CONTEXTS_ENV])
66
+ .map((contextKey) => ({ contextKey })),
57
67
  );
58
68
  const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
59
69
  const config = loadConfig();
@@ -80,7 +90,7 @@ let serverReady = false;
80
90
 
81
91
  const feishuOutbound = transport === "sdk" ? new FeishuMessageClient(config, logger) : undefined;
82
92
  if (feishuOutbound) {
83
- const renderer = new CardRenderer();
93
+ const renderer = new CardRenderer({ thinkingCardLayout: config.feishu.thinkingCardLayout });
84
94
  const presenter = new FeishuTurnPresenter(feishuOutbound, store, renderer, {
85
95
  normalIntervalMs: 2_000,
86
96
  criticalGapMs: 500,
@@ -115,16 +125,26 @@ if (routes.length === 0) throw new Error("Neither Feishu nor console input is en
115
125
  const outbound = new OutboundRouter(routes);
116
126
  let shuttingDown = false;
117
127
  let restartRequested = false;
128
+ let pendingSelfUpdatePlanPath: string | undefined;
118
129
  const safeRestart = new SafeRestartScheduler({
119
130
  readActivity: () => store.getServerActivityState(),
120
- onReady: (reason, notificationTargets) => initiateRestart(reason, notificationTargets),
131
+ onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
132
+ ? initiateSelfUpdate(reason, notificationTargets)
133
+ : initiateRestart(reason, notificationTargets),
121
134
  onStatus: (status) => safeRestartNotifier?.update(status),
122
135
  onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
123
136
  });
124
137
  const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
125
138
  supervised,
126
139
  restart: requestRestart,
127
- cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
140
+ cancelSafeRestart: async (scheduleId) => {
141
+ const cancelled = await safeRestart.cancelScheduled(scheduleId);
142
+ if (cancelled && pendingSelfUpdatePlanPath) {
143
+ releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
144
+ pendingSelfUpdatePlanPath = undefined;
145
+ }
146
+ return cancelled;
147
+ },
128
148
  rememberFeishuUserOpenId: (userOpenId) => {
129
149
  if (config.feishu.userOpenId?.startsWith("ou_")) return;
130
150
  const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
@@ -170,14 +190,38 @@ if (feishuConnector && startupNotifier) {
170
190
  startupReason,
171
191
  startControlServer,
172
192
  () => { serverReady = true; },
173
- restartGroupContextKeys,
193
+ restartNotificationTargets,
174
194
  );
175
195
  } else {
176
196
  await startControlServer();
177
197
  await feishuConnector?.start();
178
198
  serverReady = true;
179
199
  }
200
+ await controller.recoverInterruptedTasks().catch((error: unknown) => {
201
+ logger.error({ error }, "Startup task recovery failed; the server will remain available.");
202
+ });
180
203
  consoleConnector?.start();
204
+ recoverPendingSelfUpdate();
205
+
206
+ function recoverPendingSelfUpdate(): void {
207
+ const pending = readPendingSelfUpdate(agentBotHome());
208
+ if (!pending || pendingSelfUpdatePlanPath || restartRequested || safeRestart.scheduled) return;
209
+ let notificationTarget: RestartNotificationTarget | undefined;
210
+ try {
211
+ notificationTarget = inferControlRestartTarget(pending.plan.notificationSessionId);
212
+ } catch (error) {
213
+ logger.warn({ error }, "Could not restore the self-update notification target; update recovery will continue.");
214
+ }
215
+ pendingSelfUpdatePlanPath = pending.planPath;
216
+ safeRestart.schedule(
217
+ pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
218
+ notificationTarget,
219
+ );
220
+ logger.warn(
221
+ { planPath: pending.planPath, toVersion: pending.plan.toVersion },
222
+ "Recovered a prepared Agent Bot self-update after Worker restart.",
223
+ );
224
+ }
181
225
 
182
226
  async function requestRestart(
183
227
  contextKey: string,
@@ -192,6 +236,10 @@ async function requestRestart(
192
236
  await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
193
237
  return;
194
238
  }
239
+ if (pendingSelfUpdatePlanPath) {
240
+ await outbound.sendText(contextKey, "Agent Bot 更新已在等待中;更新完成后服务会自动恢复。").catch(() => undefined);
241
+ return;
242
+ }
195
243
  if (force) {
196
244
  await initiateRestart("用户执行 /restart --force 命令", [notificationTarget]);
197
245
  return;
@@ -216,13 +264,13 @@ async function initiateRestart(
216
264
  return;
217
265
  }
218
266
  restartRequested = true;
267
+ pendingSelfUpdatePlanPath = undefined;
219
268
  await safeRestart.cancelCurrent();
220
269
  await safeRestartNotifier?.flush();
221
270
  const allNotificationTargets = mergeRestartNotificationTargets(
222
271
  notificationTargets,
223
272
  safeRestartNotifier?.getNotificationTargets() ?? [],
224
273
  );
225
- const restartGroupContextKeys = groupContextKeysForRestartTargets(allNotificationTargets);
226
274
  await sendRestartTexts(
227
275
  allNotificationTargets,
228
276
  "Agent Bot 正在重启,恢复在线后会发送启动状态通知。",
@@ -230,10 +278,59 @@ async function initiateRestart(
230
278
  setTimeout(() => void shutdown(
231
279
  supervised ? STOP_EXIT_CODE : 0,
232
280
  reason,
233
- restartGroupContextKeys,
281
+ allNotificationTargets,
234
282
  ), 100);
235
283
  }
236
284
 
285
+ async function initiateSelfUpdate(
286
+ reason: string,
287
+ notificationTargets: RestartNotificationTarget[] = [],
288
+ ): Promise<void> {
289
+ if (restartRequested) {
290
+ await sendRestartTexts(notificationTargets, "Agent Bot 已在重启或更新中,请稍候。");
291
+ return;
292
+ }
293
+ const planPath = pendingSelfUpdatePlanPath;
294
+ if (!planPath) return;
295
+ restartRequested = true;
296
+ await safeRestartNotifier?.flush();
297
+ const allNotificationTargets = mergeRestartNotificationTargets(
298
+ notificationTargets,
299
+ safeRestartNotifier?.getNotificationTargets() ?? [],
300
+ );
301
+ await sendRestartTexts(
302
+ allNotificationTargets,
303
+ "Agent Bot 正在更新,完成或回滚后会自动恢复服务并发送启动状态通知。",
304
+ );
305
+ const environmentRefresh = refreshedSystemEnvironment();
306
+ if (environmentRefresh.error) {
307
+ logger.warn({ error: environmentRefresh.error }, "Failed to refresh the environment for Agent Bot update.");
308
+ }
309
+ try {
310
+ assertSelfUpdatePlanPath(planPath, agentBotHome());
311
+ launchSelfUpdateRunner(planPath, {
312
+ waitPids: [process.pid, ...(supervised ? [process.ppid] : [])],
313
+ environment: replacementSupervisorEnvironment(
314
+ reason,
315
+ environmentRefresh.environment,
316
+ allNotificationTargets,
317
+ ),
318
+ });
319
+ } catch (error) {
320
+ restartRequested = false;
321
+ releaseSelfUpdatePlan(planPath);
322
+ pendingSelfUpdatePlanPath = undefined;
323
+ logger.error({ error, planPath }, "Failed to launch Agent Bot self-update; keeping the current service online.");
324
+ await sendRestartTexts(
325
+ allNotificationTargets,
326
+ `Agent Bot 更新未启动,当前服务保持在线:${error instanceof Error ? error.message : String(error)}`,
327
+ );
328
+ return;
329
+ }
330
+ pendingSelfUpdatePlanPath = undefined;
331
+ setTimeout(() => void shutdown(STOP_EXIT_CODE), 100);
332
+ }
333
+
237
334
  async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
238
335
  switch (request.action) {
239
336
  case "health":
@@ -262,6 +359,9 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
262
359
  },
263
360
  };
264
361
  case "server_restart": {
362
+ if (pendingSelfUpdatePlanPath) {
363
+ return { ok: false, message: "An Agent Bot update is already pending." };
364
+ }
265
365
  const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
266
366
  if (request.mode === "safe") {
267
367
  const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
@@ -278,18 +378,90 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
278
378
  ), 25);
279
379
  return { ok: true, message: "Immediate restart requested." };
280
380
  }
381
+ case "server_update": {
382
+ if (restartRequested || pendingSelfUpdatePlanPath || safeRestart.scheduled) {
383
+ return { ok: false, message: "A restart or update is already pending." };
384
+ }
385
+ assertSelfUpdatePlanPath(request.planPath, agentBotHome());
386
+ const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
387
+ pendingSelfUpdatePlanPath = request.planPath;
388
+ safeRestart.schedule(request.reason, notificationTarget);
389
+ return {
390
+ ok: true,
391
+ message: "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
392
+ };
393
+ }
281
394
  case "server_stop":
282
395
  safeRestart.cancel();
396
+ if (pendingSelfUpdatePlanPath) releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
397
+ pendingSelfUpdatePlanPath = undefined;
283
398
  setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
284
399
  return { ok: true, message: "Agent Bot server stop requested." };
285
400
  case "task_status":
286
401
  return { ok: true, data: await controller.controlGetTaskStatus(request.localSessionId) };
287
402
  case "task_stop":
288
403
  return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
404
+ case "task_archive":
405
+ return { ok: true, data: await controller.controlArchiveTask(request.localSessionId) };
289
406
  case "task_title":
290
407
  return { ok: true, message: await controller.controlSetTaskTitle(request.localSessionId, request.title) };
291
408
  case "task_prompt":
292
409
  return { ok: true, message: await controller.controlSendTaskPrompt(request.localSessionId, request.text) };
410
+ case "task_new":
411
+ return {
412
+ ok: true,
413
+ data: await controller.controlCreateTask(
414
+ request.localSessionId,
415
+ request.title,
416
+ request.cwd,
417
+ request.projectless === true,
418
+ request.agentName,
419
+ ),
420
+ };
421
+ case "task_fork":
422
+ return { ok: true, data: await controller.controlForkTask(request.localSessionId) };
423
+ case "task_switch":
424
+ return {
425
+ ok: true,
426
+ data: await controller.controlSwitchTask(
427
+ request.localSessionId,
428
+ request.targetLocalSessionId,
429
+ request.previous === true,
430
+ ),
431
+ };
432
+ case "task_queue":
433
+ return { ok: true, data: await controller.controlQueueTaskPrompt(request.localSessionId, request.text) };
434
+ case "task_agent":
435
+ return { ok: true, data: controller.controlTaskAgent(request.localSessionId, request.agentName) };
436
+ case "task_settings":
437
+ return {
438
+ ok: true,
439
+ data: await controller.controlTaskSettings(request.localSessionId, request.setting, request.value),
440
+ };
441
+ case "task_goal":
442
+ return {
443
+ ok: true,
444
+ data: await controller.controlTaskGoal(
445
+ request.localSessionId,
446
+ request.goalAction,
447
+ request.objective,
448
+ ),
449
+ };
450
+ case "task_turns":
451
+ return { ok: true, data: controller.controlListTaskTurns(request.localSessionId) };
452
+ case "task_reset":
453
+ return { ok: true, data: await controller.controlResetTaskToTurn(request.localSessionId, request.turnId) };
454
+ case "task_mute":
455
+ return { ok: true, data: controller.controlTaskMute(request.localSessionId, request.enabled) };
456
+ case "task_shell":
457
+ return { ok: true, data: await controller.controlRunTaskShell(request.localSessionId, request.command) };
458
+ case "task_directory":
459
+ return {
460
+ ok: true,
461
+ data: await controller.controlListTaskDirectory(request.localSessionId, request.directory, request.page),
462
+ };
463
+ case "task_send_file":
464
+ return { ok: true, data: { messageId: await controller.controlSendTaskFile(request.localSessionId, request.filePath) } };
293
465
  case "task_new_group":
294
466
  return {
295
467
  ok: true,
@@ -317,10 +489,14 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
317
489
  async function shutdown(
318
490
  exitCode: number,
319
491
  restartReason?: string,
320
- restartGroupContextKeys: string[] = [],
492
+ restartNotificationTargets: RestartNotificationTarget[] = [],
321
493
  ): Promise<void> {
322
494
  if (shuttingDown) return;
323
495
  shuttingDown = true;
496
+ if (pendingSelfUpdatePlanPath) {
497
+ releaseSelfUpdatePlan(pendingSelfUpdatePlanPath);
498
+ pendingSelfUpdatePlanPath = undefined;
499
+ }
324
500
  logger.info({ exitCode }, "Shutting down Agent Bot.");
325
501
  safeRestart.cancel();
326
502
  feishuConnector?.stop();
@@ -333,11 +509,14 @@ async function shutdown(
333
509
  runtimes.close();
334
510
  await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
335
511
  store.close();
336
- if (restartReason) startReplacementSupervisor(restartReason, restartGroupContextKeys);
512
+ if (restartReason) startReplacementSupervisor(restartReason, restartNotificationTargets);
337
513
  process.exit(exitCode);
338
514
  }
339
515
 
340
- function startReplacementSupervisor(restartReason: string, restartGroupContextKeys: string[]): void {
516
+ function startReplacementSupervisor(
517
+ restartReason: string,
518
+ restartNotificationTargets: RestartNotificationTarget[],
519
+ ): void {
341
520
  const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
342
521
  const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
343
522
  prepareCrashReportDirectory(reportDirectory);
@@ -361,7 +540,7 @@ function startReplacementSupervisor(restartReason: string, restartGroupContextKe
361
540
  env: replacementSupervisorEnvironment(
362
541
  restartReason,
363
542
  environmentRefresh.environment,
364
- restartGroupContextKeys,
543
+ restartNotificationTargets,
365
544
  ),
366
545
  });
367
546
  supervisor.unref();
@@ -430,15 +609,6 @@ async function sendRestartTexts(targets: RestartNotificationTarget[], text: stri
430
609
  }));
431
610
  }
432
611
 
433
- function groupContextKeysForRestartTargets(targets: RestartNotificationTarget[]): string[] {
434
- const knownGroupContextKeys = new Set(
435
- store.listChatContexts("group").map((chat) => chat.contextKey),
436
- );
437
- return [...new Set(targets
438
- .map((target) => baseChatContextKey(target.contextKey))
439
- .filter((contextKey) => knownGroupContextKeys.has(contextKey)))];
440
- }
441
-
442
612
  function mergeRestartNotificationTargets(
443
613
  ...targetGroups: RestartNotificationTarget[][]
444
614
  ): RestartNotificationTarget[] {
@@ -23,8 +23,9 @@ export interface TurnPresenter {
23
23
  taskTitle?: string,
24
24
  replyTarget?: MessageReplyTarget,
25
25
  prompt?: string,
26
- ): Promise<void>;
26
+ ): Promise<string | undefined>;
27
27
  failPendingTurn(sessionId: string, message: string): Promise<void>;
28
+ interruptTurnForRecovery(sessionId: string, contextKey: string, turnId: string, message: string): Promise<void>;
28
29
  appendSteerMessage(sessionId: string, turnId: string, text: string, messageId?: string): Promise<void>;
29
30
  onEvent(event: AgentEvent): Promise<void>;
30
31
  showDetails(contextKey: string, turnId: string): Promise<void>;
@@ -75,6 +76,10 @@ export class OutboundRouter {
75
76
  return this.sessionReplyTargets.get(sessionId);
76
77
  }
77
78
 
79
+ canRoute(contextKey: string): boolean {
80
+ return this.routes.some((route) => route.matches(contextKey));
81
+ }
82
+
78
83
  updateSessionTitle(sessionId: string, taskTitle: string): void {
79
84
  this.sessionRoutes.get(sessionId)?.presenter.updateSessionTitle(sessionId, taskTitle);
80
85
  }
@@ -93,19 +98,28 @@ export class OutboundRouter {
93
98
  taskTitle?: string,
94
99
  replyTarget?: MessageReplyTarget,
95
100
  prompt?: string,
96
- ): Promise<void> {
101
+ ): Promise<string | undefined> {
97
102
  const route = this.route(contextKey);
98
103
  this.sessionRoutes.set(sessionId, route);
99
104
  this.sessionContextKeys.set(sessionId, contextKey);
100
105
  if (replyTarget) this.sessionReplyTargets.set(sessionId, replyTarget);
101
106
  else this.sessionReplyTargets.delete(sessionId);
102
- await route.presenter.startPendingTurn(sessionId, contextKey, taskTitle, replyTarget, prompt);
107
+ return route.presenter.startPendingTurn(sessionId, contextKey, taskTitle, replyTarget, prompt);
103
108
  }
104
109
 
105
110
  async failPendingTurn(sessionId: string, message: string): Promise<void> {
106
111
  await this.sessionRoutes.get(sessionId)?.presenter.failPendingTurn(sessionId, message);
107
112
  }
108
113
 
114
+ async interruptTurnForRecovery(
115
+ sessionId: string,
116
+ contextKey: string,
117
+ turnId: string,
118
+ message: string,
119
+ ): Promise<void> {
120
+ await this.route(contextKey).presenter.interruptTurnForRecovery(sessionId, contextKey, turnId, message);
121
+ }
122
+
109
123
  async appendSteerMessage(
110
124
  sessionId: string,
111
125
  turnId: string,
@@ -175,6 +189,17 @@ export class OutboundRouter {
175
189
  : outbound.sendText(contextKey, text);
176
190
  }
177
191
 
192
+ sendFile(contextKey: string, filePath: string): Promise<string | undefined> {
193
+ const outbound = this.route(contextKey).outbound;
194
+ const target = this.currentReplyTarget(contextKey);
195
+ if (target) {
196
+ if (!outbound.replyFile) throw new Error("当前消息通道不支持在话题中发送文件。");
197
+ return outbound.replyFile(contextKey, target, filePath);
198
+ }
199
+ if (!outbound.sendFile) throw new Error("当前消息通道不支持发送文件。");
200
+ return outbound.sendFile(contextKey, filePath);
201
+ }
202
+
178
203
  sendMarkdown(contextKey: string, markdown: string): Promise<string | undefined> {
179
204
  const outbound = this.route(contextKey).outbound;
180
205
  const target = this.currentReplyTarget(contextKey);