@keyou007/agent-bot 0.1.13-alpha.0 → 0.1.13-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 (97) hide show
  1. package/.env.example +2 -2
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +107 -98
  4. package/README.zh.md +81 -72
  5. package/dist/acp/AcpProcessManager.js +8 -3
  6. package/dist/acp/AcpProcessManager.js.map +1 -1
  7. package/dist/acp/AcpSessionManager.js +15 -0
  8. package/dist/acp/AcpSessionManager.js.map +1 -1
  9. package/dist/cli/ServerStarter.js +17 -2
  10. package/dist/cli/ServerStarter.js.map +1 -1
  11. package/dist/cli/controlProtocol.js.map +1 -1
  12. package/dist/cli/help.js +16 -4
  13. package/dist/cli/help.js.map +1 -1
  14. package/dist/cli/serverStatus.js +27 -0
  15. package/dist/cli/serverStatus.js.map +1 -1
  16. package/dist/cli/taskListOutput.js +72 -0
  17. package/dist/cli/taskListOutput.js.map +1 -0
  18. package/dist/cli.js +88 -54
  19. package/dist/cli.js.map +1 -1
  20. package/dist/codex/CodexProcessManager.js +34 -4
  21. package/dist/codex/CodexProcessManager.js.map +1 -1
  22. package/dist/codex/CodexRuntime.js +3 -0
  23. package/dist/codex/CodexRuntime.js.map +1 -1
  24. package/dist/commands/CommandRouter.js +5 -0
  25. package/dist/commands/CommandRouter.js.map +1 -1
  26. package/dist/config/FeishuUserOpenIdStore.js +76 -0
  27. package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
  28. package/dist/config/loadConfig.js +3 -0
  29. package/dist/config/loadConfig.js.map +1 -1
  30. package/dist/feishu/CardRenderer.js +123 -22
  31. package/dist/feishu/CardRenderer.js.map +1 -1
  32. package/dist/feishu/FeishuConnector.js +16 -5
  33. package/dist/feishu/FeishuConnector.js.map +1 -1
  34. package/dist/index.js +125 -26
  35. package/dist/index.js.map +1 -1
  36. package/dist/presentation/turnGraph.js +36 -0
  37. package/dist/presentation/turnGraph.js.map +1 -0
  38. package/dist/proxy/ProxySessionController.js +425 -104
  39. package/dist/proxy/ProxySessionController.js.map +1 -1
  40. package/dist/runtime/AcpRuntimeAdapter.js +3 -0
  41. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  42. package/dist/runtime/agentEnvironment.js +24 -4
  43. package/dist/runtime/agentEnvironment.js.map +1 -1
  44. package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
  45. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  46. package/dist/startup/StartupNotifier.js +16 -7
  47. package/dist/startup/StartupNotifier.js.map +1 -1
  48. package/dist/startup/startFeishu.js +2 -2
  49. package/dist/startup/startFeishu.js.map +1 -1
  50. package/dist/state/StateStore.js +172 -0
  51. package/dist/state/StateStore.js.map +1 -1
  52. package/dist/state/migrations.js +21 -0
  53. package/dist/state/migrations.js.map +1 -1
  54. package/dist/supervision/SafeRestartNotifier.js +46 -3
  55. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  56. package/dist/supervision/SafeRestartScheduler.js +65 -6
  57. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  58. package/dist/supervision/replacementSupervisor.js +27 -2
  59. package/dist/supervision/replacementSupervisor.js.map +1 -1
  60. package/dist/supervisor.js +27 -2
  61. package/dist/supervisor.js.map +1 -1
  62. package/docs/technical-reference.md +20 -7
  63. package/docs/technical-reference.zh.md +20 -7
  64. package/npm-shrinkwrap.json +2 -2
  65. package/package.json +1 -1
  66. package/skills/agent-bot/SKILL.md +63 -26
  67. package/src/acp/AcpProcessManager.ts +16 -3
  68. package/src/acp/AcpSessionManager.ts +14 -0
  69. package/src/cli/ServerStarter.ts +25 -2
  70. package/src/cli/controlProtocol.ts +6 -1
  71. package/src/cli/help.ts +16 -4
  72. package/src/cli/serverStatus.ts +28 -0
  73. package/src/cli/taskListOutput.ts +91 -0
  74. package/src/cli.ts +123 -51
  75. package/src/codex/CodexProcessManager.ts +38 -3
  76. package/src/codex/CodexRuntime.ts +6 -0
  77. package/src/commands/CommandRouter.ts +4 -0
  78. package/src/commands/commandTypes.ts +1 -0
  79. package/src/config/FeishuUserOpenIdStore.ts +81 -0
  80. package/src/config/loadConfig.ts +3 -0
  81. package/src/feishu/CardRenderer.ts +169 -22
  82. package/src/feishu/FeishuConnector.ts +13 -4
  83. package/src/index.ts +195 -23
  84. package/src/presentation/turnGraph.ts +48 -0
  85. package/src/proxy/ProxySessionController.ts +507 -112
  86. package/src/runtime/AcpRuntimeAdapter.ts +5 -0
  87. package/src/runtime/agentEnvironment.ts +41 -3
  88. package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
  89. package/src/runtime/types.ts +5 -0
  90. package/src/startup/StartupNotifier.ts +23 -7
  91. package/src/startup/startFeishu.ts +3 -2
  92. package/src/state/StateStore.ts +224 -0
  93. package/src/state/migrations.ts +21 -0
  94. package/src/supervision/SafeRestartNotifier.ts +53 -4
  95. package/src/supervision/SafeRestartScheduler.ts +78 -7
  96. package/src/supervision/replacementSupervisor.ts +27 -1
  97. package/src/supervisor.ts +33 -2
@@ -139,9 +139,34 @@ export interface TaskListCardAction {
139
139
  value: Record<string, string>;
140
140
  }
141
141
 
142
+ export interface HelpCardCommand {
143
+ text: string;
144
+ action?: TaskListCardAction;
145
+ usage?: string;
146
+ description: string;
147
+ }
148
+
149
+ export interface HelpCardSection {
150
+ title: string;
151
+ commands: HelpCardCommand[];
152
+ }
153
+
142
154
  export interface TaskListCardEntry {
143
155
  lines: string[];
144
156
  actions?: TaskListCardAction[];
157
+ current?: boolean;
158
+ }
159
+
160
+ export interface ResetHistoryCardEntry extends TaskListCardEntry {
161
+ sequence: number;
162
+ graphNodeLine: string;
163
+ graphConnectorLine?: string;
164
+ }
165
+
166
+ export interface ResetHistoryCardView {
167
+ entries: ResetHistoryCardEntry[];
168
+ footerLines: string[];
169
+ pageActions: TaskListCardAction[];
145
170
  }
146
171
 
147
172
  export class CardRenderer {
@@ -662,6 +687,18 @@ export class CardRenderer {
662
687
  type: "danger",
663
688
  value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
664
689
  }]));
690
+ } else if (state.status === "completed") {
691
+ elements.push(
692
+ { tag: "hr" },
693
+ taskActionRow([{
694
+ text: "Reset",
695
+ value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
696
+ }]),
697
+ {
698
+ ...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到本轮完成时;不会回退本地文件。</font>"),
699
+ text_size: "notation",
700
+ },
701
+ );
665
702
  }
666
703
  return turnCard(
667
704
  turnTitle(state.status, state.prompt ?? state.taskTitle),
@@ -769,12 +806,25 @@ export class CardRenderer {
769
806
  return sectionCard(title, elements);
770
807
  }
771
808
 
809
+ renderHelpCard(
810
+ title: string,
811
+ introLines: string[],
812
+ sections: HelpCardSection[],
813
+ ): Record<string, unknown> {
814
+ const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
815
+ for (const section of sections) {
816
+ elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
817
+ elements.push(...section.commands.map(helpCommandRow));
818
+ }
819
+ return sectionCard(title, elements);
820
+ }
821
+
772
822
  renderTaskListCard(
773
823
  title: string,
774
824
  sectionTitle: string,
775
825
  entries: TaskListCardEntry[],
776
826
  footerLines: string[],
777
- footerAction?: TaskListCardAction,
827
+ footerActions: TaskListCardAction[] = [],
778
828
  ): Record<string, unknown> {
779
829
  const elements: Record<string, unknown>[] = [markdown(`**${sectionTitle}**`)];
780
830
  if (entries.length === 0) {
@@ -788,30 +838,12 @@ export class CardRenderer {
788
838
  if (index < entries.length - 1) elements.push({ tag: "hr" });
789
839
  });
790
840
  }
841
+ if (footerActions.length > 0) {
842
+ elements.push({ tag: "hr" }, taskActionRow(footerActions));
843
+ }
791
844
  if (footerLines.length > 0) {
792
845
  elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
793
846
  }
794
- if (footerAction) {
795
- elements.push({
796
- tag: "column_set",
797
- flex_mode: "none",
798
- columns: [{
799
- tag: "column",
800
- width: "weighted",
801
- weight: 1,
802
- elements: [{
803
- tag: "button",
804
- text: { tag: "plain_text", content: footerAction.text },
805
- type: footerAction.type ?? "default",
806
- width: "fill",
807
- behaviors: [{
808
- type: "callback",
809
- value: footerAction.value,
810
- }],
811
- }],
812
- }],
813
- });
814
- }
815
847
  return {
816
848
  schema: "2.0",
817
849
  config: {
@@ -831,6 +863,39 @@ export class CardRenderer {
831
863
  };
832
864
  }
833
865
 
866
+ renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
867
+ const elements: Record<string, unknown>[] = [
868
+ {
869
+ ...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到所选轮次完成时;不会回退本地文件。</font>"),
870
+ text_size: "notation",
871
+ },
872
+ { tag: "hr" },
873
+ markdown("**历史对话轮次**"),
874
+ ];
875
+ if (view.entries.length === 0) {
876
+ elements.push(markdown("当前任务还没有成功完成的 turn。"));
877
+ } else {
878
+ view.entries.forEach((entry) => {
879
+ const action = entry.actions?.[0];
880
+ elements.push(resetHistoryEntryRow(
881
+ entry.graphNodeLine,
882
+ entry.graphConnectorLine,
883
+ entry.lines,
884
+ action,
885
+ entry.current === true,
886
+ ));
887
+ });
888
+ }
889
+ if (view.footerLines.length > 0) {
890
+ elements.push({ tag: "hr" }, {
891
+ ...markdown(view.footerLines.join("\n")),
892
+ text_size: "notation",
893
+ });
894
+ }
895
+ if (view.pageActions.length > 0) elements.push(taskActionRow(view.pageActions));
896
+ return sectionCard("历史对话轮次", elements);
897
+ }
898
+
834
899
  private baseCard(title: string, template: string, elements: unknown[]): Record<string, unknown> {
835
900
  return {
836
901
  config: {
@@ -1470,6 +1535,55 @@ function settingsOptionRow(input: {
1470
1535
  };
1471
1536
  }
1472
1537
 
1538
+ function resetHistoryEntryRow(
1539
+ graphNodeLine: string,
1540
+ graphConnectorLine: string | undefined,
1541
+ lines: string[],
1542
+ action: TaskListCardAction | undefined,
1543
+ current: boolean,
1544
+ ): Record<string, unknown> {
1545
+ const graph = [
1546
+ `<font color='${current ? "green" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
1547
+ ...(graphConnectorLine
1548
+ ? [`<font color='grey'>${escapeCardHtml(graphConnectorLine)}</font>`]
1549
+ : []),
1550
+ ].join("\n");
1551
+ return {
1552
+ tag: "column_set",
1553
+ flex_mode: "none",
1554
+ horizontal_spacing: "12px",
1555
+ vertical_align: "top",
1556
+ columns: [
1557
+ {
1558
+ tag: "column",
1559
+ width: "auto",
1560
+ vertical_align: "top",
1561
+ elements: [{
1562
+ ...markdown(graph),
1563
+ text_align: "center",
1564
+ }],
1565
+ },
1566
+ {
1567
+ tag: "column",
1568
+ width: "weighted",
1569
+ weight: 1,
1570
+ vertical_align: "top",
1571
+ elements: [markdown(lines.join("\n"))],
1572
+ },
1573
+ {
1574
+ tag: "column",
1575
+ width: "auto",
1576
+ vertical_align: "top",
1577
+ elements: current
1578
+ ? [markdown("✅ 当前")]
1579
+ : action
1580
+ ? [taskActionElement(action)]
1581
+ : [],
1582
+ },
1583
+ ],
1584
+ };
1585
+ }
1586
+
1473
1587
  function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
1474
1588
  return {
1475
1589
  tag: "column_set",
@@ -1501,6 +1615,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
1501
1615
  };
1502
1616
  }
1503
1617
 
1618
+ function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
1619
+ const details = [
1620
+ command.usage ? `**${command.usage}**` : undefined,
1621
+ command.description,
1622
+ ].filter((line): line is string => Boolean(line)).join(" ");
1623
+ return {
1624
+ tag: "column_set",
1625
+ flex_mode: "none",
1626
+ horizontal_spacing: "12px",
1627
+ vertical_align: "top",
1628
+ margin: "2px 0 0 0",
1629
+ columns: [
1630
+ {
1631
+ tag: "column",
1632
+ width: "auto",
1633
+ vertical_align: "top",
1634
+ elements: [
1635
+ command.action
1636
+ ? taskActionElement(command.action)
1637
+ : markdown(`**${escapeCardHtml(command.text)}**`),
1638
+ ],
1639
+ },
1640
+ {
1641
+ tag: "column",
1642
+ width: "weighted",
1643
+ weight: 1,
1644
+ vertical_align: "top",
1645
+ elements: [markdown(details)],
1646
+ },
1647
+ ],
1648
+ };
1649
+ }
1650
+
1504
1651
  function turnCard(
1505
1652
  title: string,
1506
1653
  template: string,
@@ -5,6 +5,7 @@ import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
5
5
  import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
6
6
 
7
7
  type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
8
+ type BotOpenIdListener = (botOpenId: string) => void;
8
9
 
9
10
  export class FeishuConnector {
10
11
  constructor(
@@ -12,6 +13,7 @@ export class FeishuConnector {
12
13
  private readonly handler: FeishuEventHandler,
13
14
  private readonly logger: Logger,
14
15
  private readonly botOpenIdResolver: BotOpenIdResolver = resolveFeishuBotOpenId,
16
+ private readonly onBotOpenId?: BotOpenIdListener,
15
17
  ) {}
16
18
 
17
19
  async start(): Promise<void> {
@@ -21,10 +23,17 @@ export class FeishuConnector {
21
23
  }
22
24
 
23
25
  const respondToAllGroupMessages = this.config.feishu.respondToAllGroupMessages !== false;
24
- const botOpenId = respondToAllGroupMessages
25
- ? undefined
26
- : await this.botOpenIdResolver(appId, appSecret);
27
- await this.startFeishuWs(appId, appSecret, botOpenId);
26
+ let botOpenId: string | undefined;
27
+ if (!respondToAllGroupMessages || this.onBotOpenId) {
28
+ try {
29
+ botOpenId = await this.botOpenIdResolver(appId, appSecret);
30
+ this.onBotOpenId?.(botOpenId);
31
+ } catch (error) {
32
+ if (!respondToAllGroupMessages) throw error;
33
+ this.logger.warn({ error }, "Failed to resolve the Lark bot Open ID for the Agent environment.");
34
+ }
35
+ }
36
+ await this.startFeishuWs(appId, appSecret, respondToAllGroupMessages ? undefined : botOpenId);
28
37
  }
29
38
 
30
39
  stop(): void {
package/src/index.ts CHANGED
@@ -5,6 +5,13 @@ import { LocalControlServer } from "./cli/LocalControlServer.js";
5
5
  import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
6
6
  import { readPackageVersion } from "./cli/packageVersion.js";
7
7
  import { loadConfig } from "./config/loadConfig.js";
8
+ import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
9
+ import {
10
+ agentBotHome,
11
+ defaultConfigPath,
12
+ defaultDotEnvPath,
13
+ resolveUserPath,
14
+ } from "./config/paths.js";
8
15
  import { ConsoleConnector } from "./console/ConsoleConnector.js";
9
16
  import { ConsoleTurnPresenter } from "./console/ConsoleTurnPresenter.js";
10
17
  import { CardRenderer } from "./feishu/CardRenderer.js";
@@ -12,18 +19,26 @@ import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
12
19
  import { FeishuConnector } from "./feishu/FeishuConnector.js";
13
20
  import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
14
21
  import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
22
+ import { baseChatContextKey, isThreadContextKey } from "./feishu/contextKey.js";
23
+ import type { MessageReplyTarget } from "./feishu/types.js";
15
24
  import { requireServerFeishuTransport } from "./feishu/transport.js";
16
25
  import { createLogger } from "./logging/logger.js";
17
26
  import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
18
27
  import { ProxySessionController } from "./proxy/ProxySessionController.js";
28
+ import type { AgentEnvironmentContext } from "./runtime/agentEnvironment.js";
19
29
  import { createAgentRuntimeRegistry } from "./runtime/createAgentRuntimeRegistry.js";
20
30
  import { StateStore } from "./state/StateStore.js";
21
31
  import { StartupNotifier } from "./startup/StartupNotifier.js";
22
32
  import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
23
33
  import { startFeishu } from "./startup/startFeishu.js";
24
34
  import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
25
- import { replacementSupervisorEnvironment } from "./supervision/replacementSupervisor.js";
35
+ import {
36
+ RESTART_GROUP_CONTEXTS_ENV,
37
+ replacementSupervisorEnvironment,
38
+ restartGroupContextKeysFromEnvironment,
39
+ } from "./supervision/replacementSupervisor.js";
26
40
  import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
41
+ import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
27
42
  import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
28
43
  import {
29
44
  nodeDiagnosticReportArguments,
@@ -37,13 +52,23 @@ const agentBotVersion = readPackageVersion();
37
52
  const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
38
53
  const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
39
54
  || (supervised ? "Supervisor 启动" : "直接启动");
55
+ const restartGroupContextKeys = restartGroupContextKeysFromEnvironment(
56
+ process.env[RESTART_GROUP_CONTEXTS_ENV],
57
+ );
40
58
  const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
41
59
  const config = loadConfig();
42
60
  const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
43
61
  const logger = createLogger(config);
44
62
  const store = new StateStore(config.storage.sqlitePath);
45
63
 
46
- const runtimes = createAgentRuntimeRegistry(config, logger);
64
+ const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
65
+ const agentEnvironmentContext: AgentEnvironmentContext = {
66
+ profilePath: agentBotHome(),
67
+ configPath: configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath(),
68
+ larkAppId: config.feishu.appId,
69
+ larkUserOpenId: config.feishu.userOpenId,
70
+ };
71
+ const runtimes = createAgentRuntimeRegistry(config, logger, agentEnvironmentContext);
47
72
 
48
73
  const routes: OutboundRoute[] = [];
49
74
  let feishuConnector: FeishuConnector | undefined;
@@ -92,7 +117,7 @@ let shuttingDown = false;
92
117
  let restartRequested = false;
93
118
  const safeRestart = new SafeRestartScheduler({
94
119
  readActivity: () => store.getServerActivityState(),
95
- onReady: (reason) => initiateRestart(reason),
120
+ onReady: (reason, notificationTargets) => initiateRestart(reason, notificationTargets),
96
121
  onStatus: (status) => safeRestartNotifier?.update(status),
97
122
  onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
98
123
  });
@@ -100,9 +125,28 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
100
125
  supervised,
101
126
  restart: requestRestart,
102
127
  cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
128
+ rememberFeishuUserOpenId: (userOpenId) => {
129
+ if (config.feishu.userOpenId?.startsWith("ou_")) return;
130
+ const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
131
+ config.feishu.userOpenId = persisted.userOpenId;
132
+ agentEnvironmentContext.larkUserOpenId = persisted.userOpenId;
133
+ process.env.FEISHU_USER_OPEN_ID = persisted.userOpenId;
134
+ logger.info(
135
+ { status: persisted.status },
136
+ "Stored the default Lark user Open ID from the first private chat message.",
137
+ );
138
+ },
103
139
  });
104
140
 
105
- if (feishuOutbound) feishuConnector = new FeishuConnector(config, controller, logger);
141
+ if (feishuOutbound) {
142
+ feishuConnector = new FeishuConnector(
143
+ config,
144
+ controller,
145
+ logger,
146
+ undefined,
147
+ (botOpenId) => { agentEnvironmentContext.larkBotOpenId = botOpenId; },
148
+ );
149
+ }
106
150
  if (consoleEnabled) consoleConnector = new ConsoleConnector(controller, logger);
107
151
 
108
152
  process.on("SIGINT", () => void shutdown(0));
@@ -126,6 +170,7 @@ if (feishuConnector && startupNotifier) {
126
170
  startupReason,
127
171
  startControlServer,
128
172
  () => { serverReady = true; },
173
+ restartGroupContextKeys,
129
174
  );
130
175
  } else {
131
176
  await startControlServer();
@@ -134,40 +179,59 @@ if (feishuConnector && startupNotifier) {
134
179
  }
135
180
  consoleConnector?.start();
136
181
 
137
- async function requestRestart(contextKey: string, force: boolean): Promise<void> {
182
+ async function requestRestart(
183
+ contextKey: string,
184
+ force: boolean,
185
+ replyTarget?: MessageReplyTarget,
186
+ ): Promise<void> {
187
+ const notificationTarget: RestartNotificationTarget = {
188
+ contextKey,
189
+ ...(replyTarget ? { replyMessageId: replyTarget.messageId } : {}),
190
+ };
138
191
  if (restartRequested) {
139
192
  await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
140
193
  return;
141
194
  }
142
195
  if (force) {
143
- await initiateRestart("用户执行 /restart --force 命令", contextKey);
196
+ await initiateRestart("用户执行 /restart --force 命令", [notificationTarget]);
144
197
  return;
145
198
  }
146
- const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令");
199
+ const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令", notificationTarget);
147
200
  await outbound.sendText(
148
201
  contextKey,
149
202
  newlyScheduled
150
203
  ? "已安排安全重启。Agent Bot 会等待所有任务完成、最终结果投递完成,并保持 15 秒无新消息后重启。"
151
- : "安全重启已在等待中,已更新重启原因。",
204
+ : "安全重启已在等待中;当前会话已加入通知范围,并已更新重启原因。",
152
205
  ).catch((error: unknown) => {
153
206
  logger.warn({ error, contextKey }, "Failed to send safe restart acknowledgement.");
154
207
  });
155
208
  }
156
209
 
157
- async function initiateRestart(reason: string, contextKey?: string): Promise<void> {
210
+ async function initiateRestart(
211
+ reason: string,
212
+ notificationTargets: RestartNotificationTarget[] = [],
213
+ ): Promise<void> {
158
214
  if (restartRequested) {
159
- if (contextKey) await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
215
+ await sendRestartTexts(notificationTargets, "Agent Bot 已在重启中,请稍候。");
160
216
  return;
161
217
  }
162
218
  restartRequested = true;
163
219
  await safeRestart.cancelCurrent();
164
220
  await safeRestartNotifier?.flush();
165
- if (contextKey) {
166
- await outbound.sendText(contextKey, "Agent Bot 正在重启,恢复在线后会发送启动状态通知。").catch((error: unknown) => {
167
- logger.warn({ error, contextKey }, "Failed to send restart acknowledgement.");
168
- });
169
- }
170
- setTimeout(() => void shutdown(supervised ? STOP_EXIT_CODE : 0, reason), 100);
221
+ const allNotificationTargets = mergeRestartNotificationTargets(
222
+ notificationTargets,
223
+ safeRestartNotifier?.getNotificationTargets() ?? [],
224
+ );
225
+ const restartGroupContextKeys = groupContextKeysForRestartTargets(allNotificationTargets);
226
+ await sendRestartTexts(
227
+ allNotificationTargets,
228
+ "Agent Bot 正在重启,恢复在线后会发送启动状态通知。",
229
+ );
230
+ setTimeout(() => void shutdown(
231
+ supervised ? STOP_EXIT_CODE : 0,
232
+ reason,
233
+ restartGroupContextKeys,
234
+ ), 100);
171
235
  }
172
236
 
173
237
  async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
@@ -182,14 +246,25 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
182
246
  startedAt: processStartedAt.toISOString(),
183
247
  supervised,
184
248
  feishuAppId: config.feishu.appId ?? null,
249
+ agents: runtimes.entries().map(([name, runtime]) => {
250
+ const processInfo = runtime.getProcessInfo?.() ?? {};
251
+ return {
252
+ name,
253
+ title: config.agents[name]?.title ?? name,
254
+ kind: config.agents[name]?.kind ?? runtime.kind,
255
+ pid: processInfo.pid ?? null,
256
+ version: processInfo.version ?? null,
257
+ };
258
+ }),
185
259
  safeRestartScheduled: safeRestart.scheduled,
186
260
  safeRestartReason: safeRestart.pendingReason,
187
261
  activity: store.getServerActivityState(),
188
262
  },
189
263
  };
190
- case "server_restart":
264
+ case "server_restart": {
265
+ const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
191
266
  if (request.mode === "safe") {
192
- const newlyScheduled = safeRestart.schedule(request.reason);
267
+ const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
193
268
  return {
194
269
  ok: true,
195
270
  message: newlyScheduled
@@ -197,8 +272,12 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
197
272
  : "A safe restart is already pending. Its reason has been updated.",
198
273
  };
199
274
  }
200
- setTimeout(() => void initiateRestart(request.reason), 25);
275
+ setTimeout(() => void initiateRestart(
276
+ request.reason,
277
+ notificationTarget ? [notificationTarget] : [],
278
+ ), 25);
201
279
  return { ok: true, message: "Immediate restart requested." };
280
+ }
202
281
  case "server_stop":
203
282
  safeRestart.cancel();
204
283
  setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
@@ -235,7 +314,11 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
235
314
  }
236
315
  }
237
316
 
238
- async function shutdown(exitCode: number, restartReason?: string): Promise<void> {
317
+ async function shutdown(
318
+ exitCode: number,
319
+ restartReason?: string,
320
+ restartGroupContextKeys: string[] = [],
321
+ ): Promise<void> {
239
322
  if (shuttingDown) return;
240
323
  shuttingDown = true;
241
324
  logger.info({ exitCode }, "Shutting down Agent Bot.");
@@ -250,11 +333,11 @@ async function shutdown(exitCode: number, restartReason?: string): Promise<void>
250
333
  runtimes.close();
251
334
  await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
252
335
  store.close();
253
- if (restartReason) startReplacementSupervisor(restartReason);
336
+ if (restartReason) startReplacementSupervisor(restartReason, restartGroupContextKeys);
254
337
  process.exit(exitCode);
255
338
  }
256
339
 
257
- function startReplacementSupervisor(restartReason: string): void {
340
+ function startReplacementSupervisor(restartReason: string, restartGroupContextKeys: string[]): void {
258
341
  const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
259
342
  const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
260
343
  prepareCrashReportDirectory(reportDirectory);
@@ -275,11 +358,100 @@ function startReplacementSupervisor(restartReason: string): void {
275
358
  detached: true,
276
359
  windowsHide: true,
277
360
  stdio: "ignore",
278
- env: replacementSupervisorEnvironment(restartReason, environmentRefresh.environment),
361
+ env: replacementSupervisorEnvironment(
362
+ restartReason,
363
+ environmentRefresh.environment,
364
+ restartGroupContextKeys,
365
+ ),
279
366
  });
280
367
  supervisor.unref();
281
368
  }
282
369
 
370
+ function inferControlRestartTarget(notificationSessionId?: string): RestartNotificationTarget | undefined {
371
+ const requestedSessionId = notificationSessionId?.trim();
372
+ if (requestedSessionId) {
373
+ const requested = store.getSession(requestedSessionId)
374
+ ?? store.findSessionByRemoteSessionId(requestedSessionId);
375
+ if (!requested) throw new Error(`Cannot route restart notifications: unknown task ${requestedSessionId}.`);
376
+ return restartNotificationTargetForSession(requested);
377
+ }
378
+
379
+ const runningSessions = store.listAllSessions().filter((session) => session.status === "running");
380
+ const runningContexts = new Set(runningSessions.map((session) => session.contextKey));
381
+ if (runningContexts.size > 1) {
382
+ throw new Error(
383
+ "Cannot determine which conversation requested the restart because multiple conversations are running. Pass --task <task>.",
384
+ );
385
+ }
386
+ const contextKey = runningContexts.size === 1 ? runningContexts.values().next().value : undefined;
387
+ const session = contextKey
388
+ ? runningSessions.find((candidate) => candidate.contextKey === contextKey)
389
+ : undefined;
390
+ return session ? restartNotificationTargetForSession(session) : undefined;
391
+ }
392
+
393
+ function restartNotificationTargetForSession(
394
+ session: { localSessionId: string; contextKey: string },
395
+ ): RestartNotificationTarget {
396
+ const replyMessageId = isThreadContextKey(session.contextKey)
397
+ ? store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
398
+ ?? store.findLatestMessageIdForContext(session.contextKey)
399
+ : undefined;
400
+ if (isThreadContextKey(session.contextKey) && !replyMessageId) {
401
+ throw new Error(
402
+ "Cannot route restart notifications to the task topic because it has no message anchor. Send /restart in that topic instead.",
403
+ );
404
+ }
405
+ return {
406
+ contextKey: session.contextKey,
407
+ ...(replyMessageId ? { replyMessageId } : {}),
408
+ };
409
+ }
410
+
411
+ async function sendRestartText(target: RestartNotificationTarget, text: string): Promise<string | undefined> {
412
+ if (isThreadContextKey(target.contextKey) && !target.replyMessageId) {
413
+ throw new Error("Cannot send a restart notice to a topic without a reply message anchor.");
414
+ }
415
+ const replyTarget = target.replyMessageId
416
+ ? { messageId: target.replyMessageId, replyInThread: true as const }
417
+ : undefined;
418
+ return outbound.withReplyTarget(target.contextKey, replyTarget, () =>
419
+ outbound.sendText(target.contextKey, text));
420
+ }
421
+
422
+ async function sendRestartTexts(targets: RestartNotificationTarget[], text: string): Promise<void> {
423
+ await Promise.all(targets.map(async (target) => {
424
+ await sendRestartText(target, text).catch((error: unknown) => {
425
+ logger.warn(
426
+ { error, contextKey: target.contextKey },
427
+ "Failed to send restart acknowledgement.",
428
+ );
429
+ });
430
+ }));
431
+ }
432
+
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
+ function mergeRestartNotificationTargets(
443
+ ...targetGroups: RestartNotificationTarget[][]
444
+ ): RestartNotificationTarget[] {
445
+ const targets = new Map<string, RestartNotificationTarget>();
446
+ for (const target of targetGroups.flat()) {
447
+ const existing = targets.get(target.contextKey);
448
+ if (!existing || (!existing.replyMessageId && target.replyMessageId)) {
449
+ targets.set(target.contextKey, { ...target });
450
+ }
451
+ }
452
+ return [...targets.values()];
453
+ }
454
+
283
455
  function delay(milliseconds: number): Promise<void> {
284
456
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
285
457
  }
@@ -0,0 +1,48 @@
1
+ export interface TurnGraphNode {
2
+ turnId: string;
3
+ parentTurnId?: string;
4
+ }
5
+
6
+ export interface TurnGraphRow {
7
+ sequence: number;
8
+ nodeLine: string;
9
+ connectorLine?: string;
10
+ }
11
+
12
+ export function buildTurnGraphRows(nodes: readonly TurnGraphNode[]): TurnGraphRow[] {
13
+ const visibleTurnIds = new Set(nodes.map((node) => node.turnId));
14
+ const lanes: string[] = [];
15
+
16
+ return nodes.map((node, index) => {
17
+ let lane = lanes.indexOf(node.turnId);
18
+ if (lane < 0) {
19
+ lane = lanes.length;
20
+ lanes.push(node.turnId);
21
+ }
22
+
23
+ const sequence = index + 1;
24
+ const nodeLine = `${lanes.map((_, laneIndex) => laneIndex === lane ? "●" : "│").join(" ")} ${sequence}`;
25
+ const parentTurnId = node.parentTurnId && visibleTurnIds.has(node.parentTurnId)
26
+ ? node.parentTurnId
27
+ : undefined;
28
+ let connectorLine: string | undefined;
29
+
30
+ if (!parentTurnId) {
31
+ lanes.splice(lane, 1);
32
+ connectorLine = lanes.length > 0 ? lanes.map(() => "│").join(" ") : undefined;
33
+ } else {
34
+ const existingParentLane = lanes.indexOf(parentTurnId);
35
+ if (existingParentLane < 0 || existingParentLane === lane) {
36
+ lanes[lane] = parentTurnId;
37
+ connectorLine = lanes.map(() => "│").join(" ");
38
+ } else {
39
+ const connectorLanes = lanes.map(() => "│");
40
+ connectorLanes[lane] = lane > existingParentLane ? "╱" : "╲";
41
+ connectorLine = connectorLanes.join(" ");
42
+ lanes.splice(lane, 1);
43
+ }
44
+ }
45
+
46
+ return { sequence, nodeLine, ...(connectorLine ? { connectorLine } : {}) };
47
+ });
48
+ }