@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
@@ -6,6 +6,7 @@ import { CommandRouter } from "../commands/CommandRouter.js";
6
6
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
7
7
  import { CardRenderer, } from "../feishu/CardRenderer.js";
8
8
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
9
+ import { buildTurnGraphRows } from "../presentation/turnGraph.js";
9
10
  import { createId } from "../utils/id.js";
10
11
  import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
11
12
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
@@ -15,7 +16,111 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
15
16
  const MESSAGE_FAILED_REACTION = "ERROR";
16
17
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
17
18
  const SESSION_PAGE_SIZE = 5;
19
+ const RESET_HISTORY_PAGE_SIZE = 10;
18
20
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
21
+ const HELP_COMMAND_SECTIONS = [
22
+ {
23
+ title: "任务管理",
24
+ commands: [
25
+ {
26
+ command: "/new",
27
+ usage: "[title] [--dir <cwd> | --nodir]",
28
+ description: "使用默认 Agent 创建任务;--nodir 创建 Projectless 任务",
29
+ },
30
+ {
31
+ command: "/newgroup",
32
+ usage: "[title] [--dir <cwd> | --nodir]",
33
+ description: "创建飞书群和新任务",
34
+ },
35
+ {
36
+ command: "/forkgroup",
37
+ usage: "[title]",
38
+ description: "从当前任务最近完成轮次创建分支群",
39
+ },
40
+ {
41
+ command: "/fork",
42
+ usage: "[序号或任务 ID]",
43
+ description: "从当前或指定任务最近完成轮次创建分支",
44
+ },
45
+ { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
46
+ {
47
+ command: "/title",
48
+ usage: "<新标题>",
49
+ description: "修改当前任务标题",
50
+ requiresArgument: true,
51
+ },
52
+ { command: "/sessions", usage: "[关键词]", description: "查找本机任务" },
53
+ {
54
+ command: "/switch",
55
+ usage: "[序号或任务 ID]",
56
+ description: "切换任务;不填参数切回上一个任务",
57
+ },
58
+ ],
59
+ },
60
+ {
61
+ title: "执行设置",
62
+ commands: [
63
+ {
64
+ command: "!",
65
+ usage: "<命令>",
66
+ description: "在当前任务目录执行本地命令",
67
+ requiresArgument: true,
68
+ },
69
+ { command: "/stop", description: "停止当前执行" },
70
+ {
71
+ command: "/queue",
72
+ usage: "<prompt>",
73
+ description: "将 Prompt 排队为后续轮次",
74
+ requiresArgument: true,
75
+ },
76
+ {
77
+ command: "/nosteer",
78
+ usage: "<prompt>",
79
+ description: "与 /queue 相同",
80
+ requiresArgument: true,
81
+ },
82
+ {
83
+ command: "/goal",
84
+ usage: "<目标或操作>",
85
+ description: "查看或创建长任务 Goal;支持 pause、resume、edit、clear",
86
+ requiresArgument: true,
87
+ },
88
+ { command: "/provider", description: "选择 AI 服务提供商" },
89
+ { command: "/model", description: "选择当前任务使用的模型" },
90
+ { command: "/thinking", description: "设置模型的思考强度" },
91
+ { command: "/permissions", description: "设置执行工具前是否需要确认" },
92
+ ],
93
+ },
94
+ {
95
+ title: "Agent",
96
+ commands: [
97
+ {
98
+ command: "/agent",
99
+ usage: "[name]",
100
+ description: "选择新任务使用的默认 Agent",
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ title: "系统",
106
+ commands: [
107
+ {
108
+ command: "/status",
109
+ usage: "[序号或任务 ID]",
110
+ description: "查看当前或指定任务状态",
111
+ },
112
+ {
113
+ command: "/restart",
114
+ usage: "[--force]",
115
+ description: "默认安全重启;--force 立即重启",
116
+ },
117
+ { command: "/help", description: "显示本帮助" },
118
+ ],
119
+ },
120
+ ];
121
+ const HELP_DEFAULT_COMMANDS = new Set(HELP_COMMAND_SECTIONS.flatMap((section) => section.commands)
122
+ .filter((command) => !command.requiresArgument)
123
+ .map((command) => command.command));
19
124
  export class ProxySessionController {
20
125
  config;
21
126
  store;
@@ -31,6 +136,7 @@ export class ProxySessionController {
31
136
  queuedPromptStarts = new Map();
32
137
  queuedPromptCards = new Map();
33
138
  queuedPromptCardWrites = new Map();
139
+ sessionResets = new Map();
34
140
  lastSessionListings = new Map();
35
141
  threadInitializations = new Map();
36
142
  unsubscribe = [];
@@ -75,8 +181,18 @@ export class ProxySessionController {
75
181
  catch (error) {
76
182
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
77
183
  }
184
+ if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
185
+ try {
186
+ await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
187
+ }
188
+ catch (error) {
189
+ this.logger.warn({ error, messageId: message.messageId }, "Failed to persist the first private-chat Lark user Open ID.");
190
+ }
191
+ }
78
192
  if (message.chatId && message.chatType) {
79
- this.store.recordChatContext(baseChatContextKey(message.contextKey), message.chatType);
193
+ const chatContextKey = baseChatContextKey(message.contextKey);
194
+ this.store.recordChatContext(chatContextKey, message.chatType);
195
+ this.store.markChatActive(chatContextKey);
80
196
  }
81
197
  const replyTarget = message.replyInThread
82
198
  ? { messageId: message.messageId, replyInThread: true }
@@ -87,9 +203,6 @@ export class ProxySessionController {
87
203
  text: message.text,
88
204
  ...(imageCount > 0 ? { imageCount } : {}),
89
205
  });
90
- if (imageCount > 0 && message.chatId && message.chatType) {
91
- this.store.markChatActive(baseChatContextKey(message.contextKey));
92
- }
93
206
  let localImagePaths;
94
207
  if (imageCount > 0) {
95
208
  try {
@@ -112,12 +225,6 @@ export class ProxySessionController {
112
225
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
113
226
  return;
114
227
  }
115
- if (message.chatId
116
- && message.chatType
117
- && (command.type === "shell"
118
- || (command.type === "prompt" && !message.text.trimStart().startsWith("/")))) {
119
- this.store.markChatActive(baseChatContextKey(message.contextKey));
120
- }
121
228
  // Operational and read-only commands must remain available even if a prompt operation is slow.
122
229
  if (isQueueIndependentCommand(command)) {
123
230
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
@@ -165,7 +272,10 @@ export class ProxySessionController {
165
272
  await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
166
273
  try {
167
274
  const kind = String(scopedAction.value.action ?? "");
168
- if (kind === "turn_details") {
275
+ if (kind === "help_command") {
276
+ await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
277
+ }
278
+ else if (kind === "turn_details") {
169
279
  await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
170
280
  }
171
281
  else if (kind === "activity_history") {
@@ -177,6 +287,24 @@ export class ProxySessionController {
177
287
  const sessionId = String(scopedAction.value.sessionId ?? "");
178
288
  await this.cancelSession(this.requireSession(contextKey, sessionId));
179
289
  }
290
+ else if (kind === "turn_reset") {
291
+ const sessionId = String(scopedAction.value.sessionId ?? "");
292
+ await this.resetCurrentSessionToTurn(contextKey, sessionId, String(scopedAction.value.turnId ?? ""));
293
+ if (scopedAction.value.cardView === "reset_history") {
294
+ await this.openResetHistory(contextKey, {
295
+ expectedSessionId: sessionId,
296
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
297
+ page: resetHistoryPageValue(scopedAction.value.page),
298
+ });
299
+ }
300
+ }
301
+ else if (kind === "turn_reset_page") {
302
+ await this.openResetHistory(contextKey, {
303
+ expectedSessionId: String(scopedAction.value.sessionId ?? ""),
304
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
305
+ page: resetHistoryPageValue(scopedAction.value.page),
306
+ });
307
+ }
180
308
  else if (kind === "queued_prompt_cancel") {
181
309
  await this.cancelQueuedPrompt(scopedAction);
182
310
  }
@@ -193,8 +321,8 @@ export class ProxySessionController {
193
321
  await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
194
322
  }
195
323
  }
196
- else if (kind === "session_more") {
197
- await this.refreshSessionsCardFromAction(scopedAction, undefined, true);
324
+ else if (kind === "session_page") {
325
+ await this.refreshSessionsCardFromAction(scopedAction);
198
326
  }
199
327
  else if (kind === "session_switch") {
200
328
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -202,12 +330,12 @@ export class ProxySessionController {
202
330
  if (scopedAction.value.cardView === "status")
203
331
  await this.refreshStatusCardFromAction(scopedAction);
204
332
  else
205
- await this.refreshSessionsCardFromAction(scopedAction);
333
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
206
334
  }
207
335
  else if (kind === "session_fork") {
208
336
  const sessionId = String(scopedAction.value.sessionId ?? "");
209
337
  await this.forkSessionReference(contextKey, sessionId);
210
- await this.refreshSessionsCardFromAction(scopedAction);
338
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
211
339
  }
212
340
  else if (kind === "session_fork_group") {
213
341
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -217,7 +345,7 @@ export class ProxySessionController {
217
345
  else if (kind === "session_new") {
218
346
  const sessionId = String(scopedAction.value.sessionId ?? "");
219
347
  await this.createProjectSessionFromReference(contextKey, sessionId);
220
- await this.refreshSessionsCardFromAction(scopedAction);
348
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
221
349
  }
222
350
  else if (kind === "session_new_group") {
223
351
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -230,7 +358,7 @@ export class ProxySessionController {
230
358
  if (scopedAction.value.cardView === "status")
231
359
  await this.refreshStatusCardFromAction(scopedAction, sessionId);
232
360
  else
233
- await this.refreshSessionsCardFromAction(scopedAction, sessionId);
361
+ await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
234
362
  }
235
363
  else if (kind === "session_status") {
236
364
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
@@ -355,6 +483,7 @@ export class ProxySessionController {
355
483
  this.lastSessionListings.clear();
356
484
  this.queuedPromptCards.clear();
357
485
  this.queuedPromptCardWrites.clear();
486
+ this.sessionResets.clear();
358
487
  }
359
488
  async controlStopTask(localSessionId) {
360
489
  const record = this.store.getSession(localSessionId);
@@ -588,7 +717,10 @@ export class ProxySessionController {
588
717
  case "restart":
589
718
  if (!this.lifecycle)
590
719
  throw new Error("当前运行方式不支持自动重启。");
591
- await this.lifecycle.restart(contextKey, command.force === true);
720
+ await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
721
+ return;
722
+ case "turns":
723
+ await this.openResetHistory(contextKey);
592
724
  return;
593
725
  case "model":
594
726
  await this.openExecutionSettings(contextKey, "model");
@@ -1532,6 +1664,15 @@ export class ProxySessionController {
1532
1664
  });
1533
1665
  }
1534
1666
  async handleRuntimeEvent(event) {
1667
+ if ("turnId" in event) {
1668
+ const source = this.store.getSession(event.sessionId);
1669
+ if (source?.remoteSessionId && this.isCodexSession(source)) {
1670
+ this.store.saveTurnRuntimeOrigin(event.turnId, source.localSessionId, source.agentName, source.remoteSessionId);
1671
+ }
1672
+ if (event.type === "turn_started") {
1673
+ this.store.saveTurnParent(event.turnId, event.sessionId, this.completedParentTurnId(source));
1674
+ }
1675
+ }
1535
1676
  if (event.type === "session_metadata_updated") {
1536
1677
  this.store.updateRuntimeSession(event.sessionId, { title: event.title });
1537
1678
  this.outbound.updateSessionTitle(event.sessionId, event.title);
@@ -1623,6 +1764,14 @@ export class ProxySessionController {
1623
1764
  }
1624
1765
  }
1625
1766
  }
1767
+ completedParentTurnId(source) {
1768
+ if (!source?.lastTurnId)
1769
+ return undefined;
1770
+ if (source.lastTurnStatus === "completed")
1771
+ return source.lastTurnId;
1772
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(source.lastTurnId));
1773
+ return snapshot?.status === "completed" ? source.lastTurnId : undefined;
1774
+ }
1626
1775
  async finalizeStandaloneMessageReaction(messageId, status) {
1627
1776
  const reaction = this.store.claimMessageReaction(messageId);
1628
1777
  if (reaction)
@@ -1718,6 +1867,88 @@ export class ProxySessionController {
1718
1867
  }
1719
1868
  await loaded.runtime.cancelTurn(record.localSessionId, turnId);
1720
1869
  }
1870
+ async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
1871
+ if (!sessionId || !turnId)
1872
+ throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
1873
+ const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
1874
+ const reset = previous.catch(() => undefined).then(() => this.performCurrentSessionReset(contextKey, sessionId, turnId));
1875
+ this.sessionResets.set(sessionId, reset);
1876
+ try {
1877
+ await reset;
1878
+ }
1879
+ finally {
1880
+ if (this.sessionResets.get(sessionId) === reset)
1881
+ this.sessionResets.delete(sessionId);
1882
+ }
1883
+ }
1884
+ async performCurrentSessionReset(contextKey, sessionId, turnId) {
1885
+ const current = this.requireCurrentSession(contextKey);
1886
+ if (current.localSessionId !== sessionId) {
1887
+ throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
1888
+ }
1889
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
1890
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
1891
+ }
1892
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
1893
+ if (!snapshot || snapshot.sessionId !== sessionId || snapshot.status !== "completed") {
1894
+ throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
1895
+ }
1896
+ if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
1897
+ await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
1898
+ return;
1899
+ }
1900
+ const runtime = this.runtimes.forAgent(current.agentName);
1901
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
1902
+ throw new Error("当前 App Server Agent 不支持 Reset。");
1903
+ }
1904
+ const loaded = await this.loadSession(current);
1905
+ const activity = await this.assertSessionTurnOwnership(current, runtime);
1906
+ const active = activity?.active
1907
+ ?? Boolean(loaded.session.activeTurnId || current.status === "running" || current.lastTurnStatus === "running");
1908
+ if (active) {
1909
+ throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
1910
+ }
1911
+ const origin = this.store.getTurnRuntimeOrigin(turnId);
1912
+ if (origin && (origin.localSessionId !== sessionId || origin.agentName !== current.agentName)) {
1913
+ throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
1914
+ }
1915
+ const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
1916
+ const previousRemoteSessionId = current.remoteSessionId;
1917
+ const forked = await runtime.forkSession({
1918
+ localSessionId: current.localSessionId,
1919
+ remoteSessionId: sourceRemoteSessionId,
1920
+ lastTurnId: turnId,
1921
+ agentName: current.agentName,
1922
+ cwd: current.cwd,
1923
+ title: current.title,
1924
+ modelProvider: current.modelProvider,
1925
+ model: current.model,
1926
+ reasoningEffort: current.reasoningEffort,
1927
+ permissionMode: current.permissionMode ?? "auto",
1928
+ });
1929
+ this.persistRuntimeSession(current, forked, "ready");
1930
+ this.store.updateRuntimeSession(current.localSessionId, {
1931
+ lastTurnId: turnId,
1932
+ lastTurnStatus: "completed",
1933
+ });
1934
+ this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName));
1935
+ this.store.audit(contextKey, "session_reset_to_turn", {
1936
+ localSessionId: current.localSessionId,
1937
+ previousRemoteSessionId,
1938
+ sourceRemoteSessionId,
1939
+ resetTurnId: turnId,
1940
+ forkedRemoteSessionId: forked.remoteSessionId,
1941
+ });
1942
+ const targetSummary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100) || "未记录对话内容";
1943
+ const targetTime = snapshot.completedAt ?? snapshot.startedAt;
1944
+ await this.outbound.sendText(contextKey, [
1945
+ "已将当前任务重置到:",
1946
+ targetSummary,
1947
+ `完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
1948
+ `Turn ID:${turnId}`,
1949
+ "后续对话将从该轮完成后的状态继续;本地文件没有回退。",
1950
+ ].join("\n"));
1951
+ }
1721
1952
  async resolveApproval(action) {
1722
1953
  const sessionId = String(action.value.sessionId ?? "");
1723
1954
  const requestId = String(action.value.requestId ?? "");
@@ -2185,10 +2416,92 @@ export class ProxySessionController {
2185
2416
  }
2186
2417
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
2187
2418
  }
2419
+ async openResetHistory(contextKey, options = {}) {
2420
+ const current = this.requireCurrentSession(contextKey);
2421
+ if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
2422
+ throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
2423
+ }
2424
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
2425
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2426
+ }
2427
+ const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
2428
+ const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
2429
+ turnId: turn.turnId,
2430
+ parentTurnId: turn.parentTurnId,
2431
+ })));
2432
+ const total = allTurns.length;
2433
+ const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
2434
+ const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
2435
+ const offset = page * RESET_HISTORY_PAGE_SIZE;
2436
+ const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
2437
+ const entries = turns.flatMap((turn, index) => {
2438
+ const snapshot = turnViewSnapshot(turn.snapshot);
2439
+ if (!snapshot)
2440
+ return [];
2441
+ const graph = graphRows[offset + index];
2442
+ const summary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100);
2443
+ const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
2444
+ return [{
2445
+ sequence: graph.sequence,
2446
+ graphNodeLine: graph.nodeLine,
2447
+ graphConnectorLine: graph.connectorLine,
2448
+ lines: [
2449
+ `**${cardText(summary || "未记录对话内容")}**`,
2450
+ `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
2451
+ ],
2452
+ current: isCurrent,
2453
+ actions: isCurrent ? undefined : [{
2454
+ text: "Reset",
2455
+ value: {
2456
+ action: "turn_reset",
2457
+ cardView: "reset_history",
2458
+ sessionId: current.localSessionId,
2459
+ turnId: turn.turnId,
2460
+ contextKey,
2461
+ page: String(page),
2462
+ },
2463
+ }],
2464
+ }];
2465
+ });
2466
+ const pageActions = [
2467
+ ...(page > 0 ? [{
2468
+ text: "Previous",
2469
+ value: {
2470
+ action: "turn_reset_page",
2471
+ sessionId: current.localSessionId,
2472
+ contextKey,
2473
+ page: String(page - 1),
2474
+ },
2475
+ }] : []),
2476
+ ...(page < totalPages - 1 ? [{
2477
+ text: "Next",
2478
+ value: {
2479
+ action: "turn_reset_page",
2480
+ sessionId: current.localSessionId,
2481
+ contextKey,
2482
+ page: String(page + 1),
2483
+ },
2484
+ }] : []),
2485
+ ];
2486
+ const card = this.cardRenderer.renderResetHistoryCard({
2487
+ entries,
2488
+ footerLines: [
2489
+ `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
2490
+ ],
2491
+ pageActions,
2492
+ });
2493
+ if (options.updateMessageId) {
2494
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2495
+ }
2496
+ else {
2497
+ await this.outbound.sendInteractiveCard(contextKey, card);
2498
+ }
2499
+ }
2188
2500
  async listSessions(contextKey, searchTerm, options = {}) {
2189
2501
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2190
2502
  const normalizedSearch = searchTerm?.trim().toLowerCase();
2191
- const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
2503
+ const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
2504
+ const fetchLimit = (requestedPage + 1) * SESSION_PAGE_SIZE;
2192
2505
  const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
2193
2506
  const remoteSessions = [];
2194
2507
  const remoteErrors = [];
@@ -2197,9 +2510,9 @@ export class ProxySessionController {
2197
2510
  if (!runtime.listRemoteSessions)
2198
2511
  return;
2199
2512
  try {
2200
- const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
2201
- remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2202
- remoteHasMore ||= Boolean(page.nextCursor);
2513
+ const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
2514
+ remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
2515
+ remoteHasMore ||= Boolean(result.nextCursor);
2203
2516
  }
2204
2517
  catch (error) {
2205
2518
  this.logger.warn({ error, contextKey, agentName }, "Failed to list App Server sessions for an Agent.");
@@ -2217,9 +2530,13 @@ export class ProxySessionController {
2217
2530
  || Number(right.active) - Number(left.active)
2218
2531
  || right.updatedAt - left.updatedAt);
2219
2532
  const activeCount = entries.filter((entry) => entry.active).length;
2220
- const visibleEntries = entries.slice(0, visibleCount);
2221
- const hasMore = remoteHasMore || entries.length > visibleCount;
2222
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
2533
+ const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
2534
+ const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
2535
+ const offset = page * SESSION_PAGE_SIZE;
2536
+ const visibleEntries = entries.slice(offset, offset + SESSION_PAGE_SIZE);
2537
+ const hasPrevious = page > 0;
2538
+ const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
2539
+ this.lastSessionListings.set(contextKey, entries.map((entry) => entry.reference));
2223
2540
  const cardEntries = visibleEntries.map((entry, index) => {
2224
2541
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
2225
2542
  const showStop = entry.status === "外部执行中"
@@ -2232,7 +2549,7 @@ export class ProxySessionController {
2232
2549
  action: showStop ? "session_stop" : "session_switch",
2233
2550
  sessionId: entry.reference,
2234
2551
  ...(searchTerm ? { searchTerm } : {}),
2235
- visibleCount: String(visibleCount),
2552
+ page: String(page),
2236
2553
  contextKey,
2237
2554
  },
2238
2555
  }];
@@ -2242,7 +2559,7 @@ export class ProxySessionController {
2242
2559
  action: "session_new",
2243
2560
  sessionId: entry.reference,
2244
2561
  ...(searchTerm ? { searchTerm } : {}),
2245
- visibleCount: String(visibleCount),
2562
+ page: String(page),
2246
2563
  contextKey,
2247
2564
  },
2248
2565
  });
@@ -2252,7 +2569,7 @@ export class ProxySessionController {
2252
2569
  action: "session_new_group",
2253
2570
  sessionId: entry.reference,
2254
2571
  ...(searchTerm ? { searchTerm } : {}),
2255
- visibleCount: String(visibleCount),
2572
+ page: String(page),
2256
2573
  contextKey,
2257
2574
  },
2258
2575
  });
@@ -2262,7 +2579,7 @@ export class ProxySessionController {
2262
2579
  action: "session_fork",
2263
2580
  sessionId: entry.reference,
2264
2581
  ...(searchTerm ? { searchTerm } : {}),
2265
- visibleCount: String(visibleCount),
2582
+ page: String(page),
2266
2583
  contextKey,
2267
2584
  },
2268
2585
  });
@@ -2272,7 +2589,7 @@ export class ProxySessionController {
2272
2589
  action: "session_fork_group",
2273
2590
  sessionId: entry.reference,
2274
2591
  ...(searchTerm ? { searchTerm } : {}),
2275
- visibleCount: String(visibleCount),
2592
+ page: String(page),
2276
2593
  contextKey,
2277
2594
  },
2278
2595
  });
@@ -2286,7 +2603,7 @@ export class ProxySessionController {
2286
2603
  });
2287
2604
  return {
2288
2605
  lines: [
2289
- `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2606
+ `**${offset + index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2290
2607
  `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
2291
2608
  ],
2292
2609
  actions,
@@ -2294,21 +2611,30 @@ export class ProxySessionController {
2294
2611
  });
2295
2612
  const card = this.cardRenderer.renderTaskListCard(searchTerm ? `App Server 任务:${searchTerm}` : "App Server 任务", activeCount > 0 ? `任务(${activeCount} 个活跃)` : "任务", cardEntries, [
2296
2613
  ...(remoteHint ? [remoteHint] : []),
2297
- "点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
2298
- "点击 **NewGroup** 在对应任务的项目中创建新群和新任务;点击 **ForkGroup** 从对应任务最新已完成轮次创建分支群。",
2299
- "也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
2300
- "发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
2301
- "点击 **Status**,或发送 **/status [序号或任务 ID]**,查看当前或指定任务状态。",
2302
- ], hasMore ? {
2303
- text: "More",
2304
- type: "primary",
2305
- value: {
2306
- action: "session_more",
2307
- ...(searchTerm ? { searchTerm } : {}),
2308
- visibleCount: String(visibleCount),
2309
- contextKey,
2310
- },
2311
- } : undefined);
2614
+ `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
2615
+ "",
2616
+ "> **Switch** 切换任务;**Stop** 停止运行;**Status** 查看状态。",
2617
+ "> **New** / **Fork** 新建 / 分支任务;**NewGroup** / **ForkGroup** 新建 / 分支群。",
2618
+ ], [
2619
+ ...(hasPrevious ? [{
2620
+ text: "Previous",
2621
+ value: {
2622
+ action: "session_page",
2623
+ ...(searchTerm ? { searchTerm } : {}),
2624
+ page: String(page - 1),
2625
+ contextKey,
2626
+ },
2627
+ }] : []),
2628
+ ...(hasNext ? [{
2629
+ text: "Next",
2630
+ value: {
2631
+ action: "session_page",
2632
+ ...(searchTerm ? { searchTerm } : {}),
2633
+ page: String(page + 1),
2634
+ contextKey,
2635
+ },
2636
+ }] : []),
2637
+ ]);
2312
2638
  if (options.updateMessageId) {
2313
2639
  await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2314
2640
  }
@@ -2316,17 +2642,16 @@ export class ProxySessionController {
2316
2642
  await this.outbound.sendInteractiveCard(contextKey, card);
2317
2643
  }
2318
2644
  }
2319
- async refreshSessionsCardFromAction(action, forceSwitchTaskId, loadMore = false) {
2645
+ async refreshSessionsCardFromAction(action, options = {}) {
2320
2646
  if (!action.messageId)
2321
2647
  return;
2322
2648
  const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
2323
2649
  ? action.value.searchTerm
2324
2650
  : undefined;
2325
- const currentVisibleCount = parseSessionVisibleCount(action.value.visibleCount);
2326
2651
  await this.listSessions(action.contextKey, searchTerm, {
2327
2652
  updateMessageId: action.messageId,
2328
- forceSwitchTaskId,
2329
- visibleCount: currentVisibleCount + (loadMore ? SESSION_PAGE_SIZE : 0),
2653
+ forceSwitchTaskId: options.forceSwitchTaskId,
2654
+ page: options.page ?? parseSessionPage(action.value.page),
2330
2655
  });
2331
2656
  }
2332
2657
  async refreshStatusCardFromAction(action, forceSwitchTaskId) {
@@ -2702,58 +3027,45 @@ export class ProxySessionController {
2702
3027
  else
2703
3028
  await this.outbound.sendInteractiveCard(contextKey, card);
2704
3029
  }
3030
+ async executeHelpCommandAction(action, contextKey, replyTarget) {
3031
+ const commandText = typeof action.value.command === "string"
3032
+ ? action.value.command.trim()
3033
+ : "";
3034
+ if (!HELP_DEFAULT_COMMANDS.has(commandText)) {
3035
+ throw new Error("帮助卡片中的命令无效,请发送 /help 获取最新卡片。");
3036
+ }
3037
+ const command = this.router.parse(commandText);
3038
+ await this.execute(contextKey, command, undefined, replyTarget, undefined, action.userId);
3039
+ }
2705
3040
  async help(contextKey) {
2706
- const sections = [
2707
- {
2708
- lines: [
2709
- "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
2710
- "> 命令前缀示例:/sess 等同于 /sessions。",
2711
- "> 命令缩写示例:/fg 等同于 /forkgroup。",
2712
- "前缀或缩写匹配多个命令时,需要输入更长的形式。",
2713
- "**[参数]** 可选 **&#60;参数&#62;** 必填",
2714
- ],
2715
- },
2716
- {
2717
- title: "任务管理",
2718
- lines: [
2719
- "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2720
- "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2721
- "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2722
- "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2723
- "**/title &#60;新标题&#62;** 修改当前任务的标题",
2724
- "**/sessions [关键词]** 查找本机任务",
2725
- "**/switch [序号或任务 ID]** 不填参数切回上一个任务",
2726
- ],
2727
- },
2728
- {
2729
- title: "执行设置",
2730
- lines: [
2731
- "**! &#60;命令&#62;** 在当前任务目录直接执行本地命令",
2732
- "**/stop** 停止当前执行",
2733
- "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2734
- "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2735
- "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
2736
- "**/model** 选择当前任务使用的模型",
2737
- "**/thinking** 设置模型的思考强度",
2738
- "**/permissions** 设置执行工具前是否需要确认",
2739
- ],
2740
- },
2741
- {
2742
- title: "Agent",
2743
- lines: [
2744
- "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2745
- ],
2746
- },
2747
- {
2748
- title: "系统",
2749
- lines: [
2750
- "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2751
- "**/restart [--force]** 默认安全重启;--force 立即重启",
2752
- "**/help** 显示本帮助",
2753
- ],
2754
- },
2755
- ];
2756
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Bot 使用帮助", sections));
3041
+ const sections = HELP_COMMAND_SECTIONS.map((section) => ({
3042
+ title: section.title,
3043
+ commands: section.commands.map((command) => ({
3044
+ text: command.command,
3045
+ ...(command.requiresArgument
3046
+ ? {}
3047
+ : {
3048
+ action: {
3049
+ text: command.command,
3050
+ value: {
3051
+ action: "help_command",
3052
+ command: command.command,
3053
+ contextKey,
3054
+ },
3055
+ },
3056
+ }),
3057
+ ...(command.usage ? { usage: command.usage } : {}),
3058
+ description: command.description,
3059
+ })),
3060
+ }));
3061
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard("Agent Bot 使用帮助", [
3062
+ "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
3063
+ "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
3064
+ "> 命令前缀示例:/sess 等同于 /sessions。",
3065
+ "> 命令缩写示例:/fg 等同于 /forkgroup。",
3066
+ "前缀或缩写匹配多个命令时,需要输入更长的形式。",
3067
+ "**[参数]** 可选 **&#60;参数&#62;** 必填",
3068
+ ], sections));
2757
3069
  }
2758
3070
  ensureAgent(agentName) {
2759
3071
  const agent = this.config.agents[agentName];
@@ -3090,6 +3402,9 @@ function formatStatusTime(value) {
3090
3402
  hourCycle: "h23",
3091
3403
  }).format(date);
3092
3404
  }
3405
+ function formatResetTurnTime(value) {
3406
+ return value === undefined ? "完成时间未知" : formatStatusTime(new Date(value).toISOString());
3407
+ }
3093
3408
  function cardText(value) {
3094
3409
  return value.replaceAll("&", "&amp;").replaceAll("`", "&#96;").replaceAll("<", "&#60;").replaceAll(">", "&#62;");
3095
3410
  }
@@ -3106,10 +3421,16 @@ function executionSettingsTabValue(value) {
3106
3421
  return value;
3107
3422
  throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3108
3423
  }
3424
+ function resetHistoryPageValue(value) {
3425
+ const page = Number(value);
3426
+ if (!Number.isSafeInteger(page) || page < 0)
3427
+ throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
3428
+ return page;
3429
+ }
3109
3430
  function requiredCardMessageId(value) {
3110
3431
  if (value)
3111
3432
  return value;
3112
- throw new Error("无法更新设置卡片,请重新发送设置命令。");
3433
+ throw new Error("无法更新卡片,请重新发送对应命令。");
3113
3434
  }
3114
3435
  function remoteSessionStatusLabel(status) {
3115
3436
  const labels = {
@@ -3206,9 +3527,9 @@ function isQueueIndependentCommand(command) {
3206
3527
  return true;
3207
3528
  return false;
3208
3529
  }
3209
- function parseSessionVisibleCount(value) {
3530
+ function parseSessionPage(value) {
3210
3531
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
3211
- return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
3532
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
3212
3533
  }
3213
3534
  function isPromptCommand(command) {
3214
3535
  return command.type === "prompt" || command.type === "nosteer";