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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.env.example +2 -2
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +10 -4
  4. package/README.zh.md +10 -4
  5. package/dist/cli/ServerStarter.js +17 -2
  6. package/dist/cli/ServerStarter.js.map +1 -1
  7. package/dist/cli/controlProtocol.js.map +1 -1
  8. package/dist/cli/help.js +12 -2
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli.js +39 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/CommandRouter.js +5 -0
  13. package/dist/commands/CommandRouter.js.map +1 -1
  14. package/dist/config/FeishuUserOpenIdStore.js +76 -0
  15. package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
  16. package/dist/config/loadConfig.js +3 -0
  17. package/dist/config/loadConfig.js.map +1 -1
  18. package/dist/feishu/CardRenderer.js +82 -22
  19. package/dist/feishu/CardRenderer.js.map +1 -1
  20. package/dist/index.js +103 -23
  21. package/dist/index.js.map +1 -1
  22. package/dist/presentation/turnGraph.js +36 -0
  23. package/dist/presentation/turnGraph.js.map +1 -0
  24. package/dist/proxy/ProxySessionController.js +270 -47
  25. package/dist/proxy/ProxySessionController.js.map +1 -1
  26. package/dist/startup/StartupNotifier.js +16 -7
  27. package/dist/startup/StartupNotifier.js.map +1 -1
  28. package/dist/startup/startFeishu.js +2 -2
  29. package/dist/startup/startFeishu.js.map +1 -1
  30. package/dist/state/StateStore.js +169 -0
  31. package/dist/state/StateStore.js.map +1 -1
  32. package/dist/state/migrations.js +21 -0
  33. package/dist/state/migrations.js.map +1 -1
  34. package/dist/supervision/SafeRestartNotifier.js +46 -3
  35. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  36. package/dist/supervision/SafeRestartScheduler.js +65 -6
  37. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  38. package/dist/supervision/replacementSupervisor.js +27 -2
  39. package/dist/supervision/replacementSupervisor.js.map +1 -1
  40. package/dist/supervisor.js +27 -2
  41. package/dist/supervisor.js.map +1 -1
  42. package/docs/technical-reference.md +11 -4
  43. package/docs/technical-reference.zh.md +11 -4
  44. package/npm-shrinkwrap.json +2 -2
  45. package/package.json +1 -1
  46. package/skills/agent-bot/SKILL.md +10 -4
  47. package/src/cli/ServerStarter.ts +25 -2
  48. package/src/cli/controlProtocol.ts +6 -1
  49. package/src/cli/help.ts +12 -2
  50. package/src/cli.ts +57 -0
  51. package/src/commands/CommandRouter.ts +4 -0
  52. package/src/commands/commandTypes.ts +1 -0
  53. package/src/config/FeishuUserOpenIdStore.ts +81 -0
  54. package/src/config/loadConfig.ts +3 -0
  55. package/src/feishu/CardRenderer.ts +109 -22
  56. package/src/index.ts +161 -21
  57. package/src/presentation/turnGraph.ts +48 -0
  58. package/src/proxy/ProxySessionController.ts +323 -55
  59. package/src/startup/StartupNotifier.ts +23 -7
  60. package/src/startup/startFeishu.ts +3 -2
  61. package/src/state/StateStore.ts +221 -0
  62. package/src/state/migrations.ts +21 -0
  63. package/src/supervision/SafeRestartNotifier.ts +53 -4
  64. package/src/supervision/SafeRestartScheduler.ts +78 -7
  65. package/src/supervision/replacementSupervisor.ts +27 -1
  66. 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,6 +16,7 @@ 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:";
19
21
  export class ProxySessionController {
20
22
  config;
@@ -31,6 +33,7 @@ export class ProxySessionController {
31
33
  queuedPromptStarts = new Map();
32
34
  queuedPromptCards = new Map();
33
35
  queuedPromptCardWrites = new Map();
36
+ sessionResets = new Map();
34
37
  lastSessionListings = new Map();
35
38
  threadInitializations = new Map();
36
39
  unsubscribe = [];
@@ -75,8 +78,18 @@ export class ProxySessionController {
75
78
  catch (error) {
76
79
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
77
80
  }
81
+ if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
82
+ try {
83
+ await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
84
+ }
85
+ catch (error) {
86
+ this.logger.warn({ error, messageId: message.messageId }, "Failed to persist the first private-chat Lark user Open ID.");
87
+ }
88
+ }
78
89
  if (message.chatId && message.chatType) {
79
- this.store.recordChatContext(baseChatContextKey(message.contextKey), message.chatType);
90
+ const chatContextKey = baseChatContextKey(message.contextKey);
91
+ this.store.recordChatContext(chatContextKey, message.chatType);
92
+ this.store.markChatActive(chatContextKey);
80
93
  }
81
94
  const replyTarget = message.replyInThread
82
95
  ? { messageId: message.messageId, replyInThread: true }
@@ -87,9 +100,6 @@ export class ProxySessionController {
87
100
  text: message.text,
88
101
  ...(imageCount > 0 ? { imageCount } : {}),
89
102
  });
90
- if (imageCount > 0 && message.chatId && message.chatType) {
91
- this.store.markChatActive(baseChatContextKey(message.contextKey));
92
- }
93
103
  let localImagePaths;
94
104
  if (imageCount > 0) {
95
105
  try {
@@ -112,12 +122,6 @@ export class ProxySessionController {
112
122
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
113
123
  return;
114
124
  }
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
125
  // Operational and read-only commands must remain available even if a prompt operation is slow.
122
126
  if (isQueueIndependentCommand(command)) {
123
127
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
@@ -177,6 +181,24 @@ export class ProxySessionController {
177
181
  const sessionId = String(scopedAction.value.sessionId ?? "");
178
182
  await this.cancelSession(this.requireSession(contextKey, sessionId));
179
183
  }
184
+ else if (kind === "turn_reset") {
185
+ const sessionId = String(scopedAction.value.sessionId ?? "");
186
+ await this.resetCurrentSessionToTurn(contextKey, sessionId, String(scopedAction.value.turnId ?? ""));
187
+ if (scopedAction.value.cardView === "reset_history") {
188
+ await this.openResetHistory(contextKey, {
189
+ expectedSessionId: sessionId,
190
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
191
+ page: resetHistoryPageValue(scopedAction.value.page),
192
+ });
193
+ }
194
+ }
195
+ else if (kind === "turn_reset_page") {
196
+ await this.openResetHistory(contextKey, {
197
+ expectedSessionId: String(scopedAction.value.sessionId ?? ""),
198
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
199
+ page: resetHistoryPageValue(scopedAction.value.page),
200
+ });
201
+ }
180
202
  else if (kind === "queued_prompt_cancel") {
181
203
  await this.cancelQueuedPrompt(scopedAction);
182
204
  }
@@ -193,8 +215,8 @@ export class ProxySessionController {
193
215
  await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
194
216
  }
195
217
  }
196
- else if (kind === "session_more") {
197
- await this.refreshSessionsCardFromAction(scopedAction, undefined, true);
218
+ else if (kind === "session_page") {
219
+ await this.refreshSessionsCardFromAction(scopedAction);
198
220
  }
199
221
  else if (kind === "session_switch") {
200
222
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -202,12 +224,12 @@ export class ProxySessionController {
202
224
  if (scopedAction.value.cardView === "status")
203
225
  await this.refreshStatusCardFromAction(scopedAction);
204
226
  else
205
- await this.refreshSessionsCardFromAction(scopedAction);
227
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
206
228
  }
207
229
  else if (kind === "session_fork") {
208
230
  const sessionId = String(scopedAction.value.sessionId ?? "");
209
231
  await this.forkSessionReference(contextKey, sessionId);
210
- await this.refreshSessionsCardFromAction(scopedAction);
232
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
211
233
  }
212
234
  else if (kind === "session_fork_group") {
213
235
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -217,7 +239,7 @@ export class ProxySessionController {
217
239
  else if (kind === "session_new") {
218
240
  const sessionId = String(scopedAction.value.sessionId ?? "");
219
241
  await this.createProjectSessionFromReference(contextKey, sessionId);
220
- await this.refreshSessionsCardFromAction(scopedAction);
242
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
221
243
  }
222
244
  else if (kind === "session_new_group") {
223
245
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -230,7 +252,7 @@ export class ProxySessionController {
230
252
  if (scopedAction.value.cardView === "status")
231
253
  await this.refreshStatusCardFromAction(scopedAction, sessionId);
232
254
  else
233
- await this.refreshSessionsCardFromAction(scopedAction, sessionId);
255
+ await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
234
256
  }
235
257
  else if (kind === "session_status") {
236
258
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
@@ -355,6 +377,7 @@ export class ProxySessionController {
355
377
  this.lastSessionListings.clear();
356
378
  this.queuedPromptCards.clear();
357
379
  this.queuedPromptCardWrites.clear();
380
+ this.sessionResets.clear();
358
381
  }
359
382
  async controlStopTask(localSessionId) {
360
383
  const record = this.store.getSession(localSessionId);
@@ -588,7 +611,10 @@ export class ProxySessionController {
588
611
  case "restart":
589
612
  if (!this.lifecycle)
590
613
  throw new Error("当前运行方式不支持自动重启。");
591
- await this.lifecycle.restart(contextKey, command.force === true);
614
+ await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
615
+ return;
616
+ case "turns":
617
+ await this.openResetHistory(contextKey);
592
618
  return;
593
619
  case "model":
594
620
  await this.openExecutionSettings(contextKey, "model");
@@ -1532,6 +1558,15 @@ export class ProxySessionController {
1532
1558
  });
1533
1559
  }
1534
1560
  async handleRuntimeEvent(event) {
1561
+ if ("turnId" in event) {
1562
+ const source = this.store.getSession(event.sessionId);
1563
+ if (source?.remoteSessionId && this.isCodexSession(source)) {
1564
+ this.store.saveTurnRuntimeOrigin(event.turnId, source.localSessionId, source.agentName, source.remoteSessionId);
1565
+ }
1566
+ if (event.type === "turn_started") {
1567
+ this.store.saveTurnParent(event.turnId, event.sessionId, source?.lastTurnStatus === "completed" ? source.lastTurnId : undefined);
1568
+ }
1569
+ }
1535
1570
  if (event.type === "session_metadata_updated") {
1536
1571
  this.store.updateRuntimeSession(event.sessionId, { title: event.title });
1537
1572
  this.outbound.updateSessionTitle(event.sessionId, event.title);
@@ -1718,6 +1753,88 @@ export class ProxySessionController {
1718
1753
  }
1719
1754
  await loaded.runtime.cancelTurn(record.localSessionId, turnId);
1720
1755
  }
1756
+ async resetCurrentSessionToTurn(contextKey, sessionId, turnId) {
1757
+ if (!sessionId || !turnId)
1758
+ throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
1759
+ const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
1760
+ const reset = previous.catch(() => undefined).then(() => this.performCurrentSessionReset(contextKey, sessionId, turnId));
1761
+ this.sessionResets.set(sessionId, reset);
1762
+ try {
1763
+ await reset;
1764
+ }
1765
+ finally {
1766
+ if (this.sessionResets.get(sessionId) === reset)
1767
+ this.sessionResets.delete(sessionId);
1768
+ }
1769
+ }
1770
+ async performCurrentSessionReset(contextKey, sessionId, turnId) {
1771
+ const current = this.requireCurrentSession(contextKey);
1772
+ if (current.localSessionId !== sessionId) {
1773
+ throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
1774
+ }
1775
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
1776
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
1777
+ }
1778
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
1779
+ if (!snapshot || snapshot.sessionId !== sessionId || snapshot.status !== "completed") {
1780
+ throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
1781
+ }
1782
+ if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
1783
+ await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
1784
+ return;
1785
+ }
1786
+ const runtime = this.runtimes.forAgent(current.agentName);
1787
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
1788
+ throw new Error("当前 App Server Agent 不支持 Reset。");
1789
+ }
1790
+ const loaded = await this.loadSession(current);
1791
+ const activity = await this.assertSessionTurnOwnership(current, runtime);
1792
+ const active = activity?.active
1793
+ ?? Boolean(loaded.session.activeTurnId || current.status === "running" || current.lastTurnStatus === "running");
1794
+ if (active) {
1795
+ throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
1796
+ }
1797
+ const origin = this.store.getTurnRuntimeOrigin(turnId);
1798
+ if (origin && (origin.localSessionId !== sessionId || origin.agentName !== current.agentName)) {
1799
+ throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
1800
+ }
1801
+ const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
1802
+ const previousRemoteSessionId = current.remoteSessionId;
1803
+ const forked = await runtime.forkSession({
1804
+ localSessionId: current.localSessionId,
1805
+ remoteSessionId: sourceRemoteSessionId,
1806
+ lastTurnId: turnId,
1807
+ agentName: current.agentName,
1808
+ cwd: current.cwd,
1809
+ title: current.title,
1810
+ modelProvider: current.modelProvider,
1811
+ model: current.model,
1812
+ reasoningEffort: current.reasoningEffort,
1813
+ permissionMode: current.permissionMode ?? "auto",
1814
+ });
1815
+ this.persistRuntimeSession(current, forked, "ready");
1816
+ this.store.updateRuntimeSession(current.localSessionId, {
1817
+ lastTurnId: turnId,
1818
+ lastTurnStatus: "completed",
1819
+ });
1820
+ this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName));
1821
+ this.store.audit(contextKey, "session_reset_to_turn", {
1822
+ localSessionId: current.localSessionId,
1823
+ previousRemoteSessionId,
1824
+ sourceRemoteSessionId,
1825
+ resetTurnId: turnId,
1826
+ forkedRemoteSessionId: forked.remoteSessionId,
1827
+ });
1828
+ const targetSummary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100) || "未记录对话内容";
1829
+ const targetTime = snapshot.completedAt ?? snapshot.startedAt;
1830
+ await this.outbound.sendText(contextKey, [
1831
+ "已将当前任务重置到:",
1832
+ targetSummary,
1833
+ `完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
1834
+ `Turn ID:${turnId}`,
1835
+ "后续对话将从该轮完成后的状态继续;本地文件没有回退。",
1836
+ ].join("\n"));
1837
+ }
1721
1838
  async resolveApproval(action) {
1722
1839
  const sessionId = String(action.value.sessionId ?? "");
1723
1840
  const requestId = String(action.value.requestId ?? "");
@@ -2185,10 +2302,92 @@ export class ProxySessionController {
2185
2302
  }
2186
2303
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
2187
2304
  }
2305
+ async openResetHistory(contextKey, options = {}) {
2306
+ const current = this.requireCurrentSession(contextKey);
2307
+ if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
2308
+ throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
2309
+ }
2310
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
2311
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2312
+ }
2313
+ const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
2314
+ const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
2315
+ turnId: turn.turnId,
2316
+ parentTurnId: turn.parentTurnId,
2317
+ })));
2318
+ const total = allTurns.length;
2319
+ const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
2320
+ const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
2321
+ const offset = page * RESET_HISTORY_PAGE_SIZE;
2322
+ const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
2323
+ const entries = turns.flatMap((turn, index) => {
2324
+ const snapshot = turnViewSnapshot(turn.snapshot);
2325
+ if (!snapshot)
2326
+ return [];
2327
+ const graph = graphRows[offset + index];
2328
+ const summary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100);
2329
+ const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
2330
+ return [{
2331
+ sequence: graph.sequence,
2332
+ graphNodeLine: graph.nodeLine,
2333
+ graphConnectorLine: graph.connectorLine,
2334
+ lines: [
2335
+ `**${cardText(summary || "未记录对话内容")}**`,
2336
+ `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
2337
+ ],
2338
+ current: isCurrent,
2339
+ actions: isCurrent ? undefined : [{
2340
+ text: "Reset",
2341
+ value: {
2342
+ action: "turn_reset",
2343
+ cardView: "reset_history",
2344
+ sessionId: current.localSessionId,
2345
+ turnId: turn.turnId,
2346
+ contextKey,
2347
+ page: String(page),
2348
+ },
2349
+ }],
2350
+ }];
2351
+ });
2352
+ const pageActions = [
2353
+ ...(page > 0 ? [{
2354
+ text: "Previous",
2355
+ value: {
2356
+ action: "turn_reset_page",
2357
+ sessionId: current.localSessionId,
2358
+ contextKey,
2359
+ page: String(page - 1),
2360
+ },
2361
+ }] : []),
2362
+ ...(page < totalPages - 1 ? [{
2363
+ text: "Next",
2364
+ value: {
2365
+ action: "turn_reset_page",
2366
+ sessionId: current.localSessionId,
2367
+ contextKey,
2368
+ page: String(page + 1),
2369
+ },
2370
+ }] : []),
2371
+ ];
2372
+ const card = this.cardRenderer.renderResetHistoryCard({
2373
+ entries,
2374
+ footerLines: [
2375
+ `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
2376
+ ],
2377
+ pageActions,
2378
+ });
2379
+ if (options.updateMessageId) {
2380
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2381
+ }
2382
+ else {
2383
+ await this.outbound.sendInteractiveCard(contextKey, card);
2384
+ }
2385
+ }
2188
2386
  async listSessions(contextKey, searchTerm, options = {}) {
2189
2387
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2190
2388
  const normalizedSearch = searchTerm?.trim().toLowerCase();
2191
- const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
2389
+ const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
2390
+ const fetchLimit = (requestedPage + 1) * SESSION_PAGE_SIZE;
2192
2391
  const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
2193
2392
  const remoteSessions = [];
2194
2393
  const remoteErrors = [];
@@ -2197,9 +2396,9 @@ export class ProxySessionController {
2197
2396
  if (!runtime.listRemoteSessions)
2198
2397
  return;
2199
2398
  try {
2200
- const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
2201
- remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2202
- remoteHasMore ||= Boolean(page.nextCursor);
2399
+ const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
2400
+ remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
2401
+ remoteHasMore ||= Boolean(result.nextCursor);
2203
2402
  }
2204
2403
  catch (error) {
2205
2404
  this.logger.warn({ error, contextKey, agentName }, "Failed to list App Server sessions for an Agent.");
@@ -2217,9 +2416,13 @@ export class ProxySessionController {
2217
2416
  || Number(right.active) - Number(left.active)
2218
2417
  || right.updatedAt - left.updatedAt);
2219
2418
  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));
2419
+ const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
2420
+ const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
2421
+ const offset = page * SESSION_PAGE_SIZE;
2422
+ const visibleEntries = entries.slice(offset, offset + SESSION_PAGE_SIZE);
2423
+ const hasPrevious = page > 0;
2424
+ const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
2425
+ this.lastSessionListings.set(contextKey, entries.map((entry) => entry.reference));
2223
2426
  const cardEntries = visibleEntries.map((entry, index) => {
2224
2427
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
2225
2428
  const showStop = entry.status === "外部执行中"
@@ -2232,7 +2435,7 @@ export class ProxySessionController {
2232
2435
  action: showStop ? "session_stop" : "session_switch",
2233
2436
  sessionId: entry.reference,
2234
2437
  ...(searchTerm ? { searchTerm } : {}),
2235
- visibleCount: String(visibleCount),
2438
+ page: String(page),
2236
2439
  contextKey,
2237
2440
  },
2238
2441
  }];
@@ -2242,7 +2445,7 @@ export class ProxySessionController {
2242
2445
  action: "session_new",
2243
2446
  sessionId: entry.reference,
2244
2447
  ...(searchTerm ? { searchTerm } : {}),
2245
- visibleCount: String(visibleCount),
2448
+ page: String(page),
2246
2449
  contextKey,
2247
2450
  },
2248
2451
  });
@@ -2252,7 +2455,7 @@ export class ProxySessionController {
2252
2455
  action: "session_new_group",
2253
2456
  sessionId: entry.reference,
2254
2457
  ...(searchTerm ? { searchTerm } : {}),
2255
- visibleCount: String(visibleCount),
2458
+ page: String(page),
2256
2459
  contextKey,
2257
2460
  },
2258
2461
  });
@@ -2262,7 +2465,7 @@ export class ProxySessionController {
2262
2465
  action: "session_fork",
2263
2466
  sessionId: entry.reference,
2264
2467
  ...(searchTerm ? { searchTerm } : {}),
2265
- visibleCount: String(visibleCount),
2468
+ page: String(page),
2266
2469
  contextKey,
2267
2470
  },
2268
2471
  });
@@ -2272,7 +2475,7 @@ export class ProxySessionController {
2272
2475
  action: "session_fork_group",
2273
2476
  sessionId: entry.reference,
2274
2477
  ...(searchTerm ? { searchTerm } : {}),
2275
- visibleCount: String(visibleCount),
2478
+ page: String(page),
2276
2479
  contextKey,
2277
2480
  },
2278
2481
  });
@@ -2286,7 +2489,7 @@ export class ProxySessionController {
2286
2489
  });
2287
2490
  return {
2288
2491
  lines: [
2289
- `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2492
+ `**${offset + index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2290
2493
  `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
2291
2494
  ],
2292
2495
  actions,
@@ -2294,21 +2497,32 @@ export class ProxySessionController {
2294
2497
  });
2295
2498
  const card = this.cardRenderer.renderTaskListCard(searchTerm ? `App Server 任务:${searchTerm}` : "App Server 任务", activeCount > 0 ? `任务(${activeCount} 个活跃)` : "任务", cardEntries, [
2296
2499
  ...(remoteHint ? [remoteHint] : []),
2500
+ `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
2297
2501
  "点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
2298
2502
  "点击 **NewGroup** 在对应任务的项目中创建新群和新任务;点击 **ForkGroup** 从对应任务最新已完成轮次创建分支群。",
2299
2503
  "也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
2300
2504
  "发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
2301
2505
  "点击 **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);
2506
+ ], [
2507
+ ...(hasPrevious ? [{
2508
+ text: "Previous",
2509
+ value: {
2510
+ action: "session_page",
2511
+ ...(searchTerm ? { searchTerm } : {}),
2512
+ page: String(page - 1),
2513
+ contextKey,
2514
+ },
2515
+ }] : []),
2516
+ ...(hasNext ? [{
2517
+ text: "Next",
2518
+ value: {
2519
+ action: "session_page",
2520
+ ...(searchTerm ? { searchTerm } : {}),
2521
+ page: String(page + 1),
2522
+ contextKey,
2523
+ },
2524
+ }] : []),
2525
+ ]);
2312
2526
  if (options.updateMessageId) {
2313
2527
  await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
2314
2528
  }
@@ -2316,17 +2530,16 @@ export class ProxySessionController {
2316
2530
  await this.outbound.sendInteractiveCard(contextKey, card);
2317
2531
  }
2318
2532
  }
2319
- async refreshSessionsCardFromAction(action, forceSwitchTaskId, loadMore = false) {
2533
+ async refreshSessionsCardFromAction(action, options = {}) {
2320
2534
  if (!action.messageId)
2321
2535
  return;
2322
2536
  const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
2323
2537
  ? action.value.searchTerm
2324
2538
  : undefined;
2325
- const currentVisibleCount = parseSessionVisibleCount(action.value.visibleCount);
2326
2539
  await this.listSessions(action.contextKey, searchTerm, {
2327
2540
  updateMessageId: action.messageId,
2328
- forceSwitchTaskId,
2329
- visibleCount: currentVisibleCount + (loadMore ? SESSION_PAGE_SIZE : 0),
2541
+ forceSwitchTaskId: options.forceSwitchTaskId,
2542
+ page: options.page ?? parseSessionPage(action.value.page),
2330
2543
  });
2331
2544
  }
2332
2545
  async refreshStatusCardFromAction(action, forceSwitchTaskId) {
@@ -2720,6 +2933,7 @@ export class ProxySessionController {
2720
2933
  "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2721
2934
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2722
2935
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2936
+ "**/turns** 浏览当前任务的历史对话轮次,并可将对话上下文恢复到所选轮次",
2723
2937
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
2724
2938
  "**/sessions [关键词]** 查找本机任务",
2725
2939
  "**/switch [序号或任务 ID]** 不填参数切回上一个任务",
@@ -3090,6 +3304,9 @@ function formatStatusTime(value) {
3090
3304
  hourCycle: "h23",
3091
3305
  }).format(date);
3092
3306
  }
3307
+ function formatResetTurnTime(value) {
3308
+ return value === undefined ? "完成时间未知" : formatStatusTime(new Date(value).toISOString());
3309
+ }
3093
3310
  function cardText(value) {
3094
3311
  return value.replaceAll("&", "&amp;").replaceAll("`", "&#96;").replaceAll("<", "&#60;").replaceAll(">", "&#62;");
3095
3312
  }
@@ -3106,10 +3323,16 @@ function executionSettingsTabValue(value) {
3106
3323
  return value;
3107
3324
  throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3108
3325
  }
3326
+ function resetHistoryPageValue(value) {
3327
+ const page = Number(value);
3328
+ if (!Number.isSafeInteger(page) || page < 0)
3329
+ throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
3330
+ return page;
3331
+ }
3109
3332
  function requiredCardMessageId(value) {
3110
3333
  if (value)
3111
3334
  return value;
3112
- throw new Error("无法更新设置卡片,请重新发送设置命令。");
3335
+ throw new Error("无法更新卡片,请重新发送对应命令。");
3113
3336
  }
3114
3337
  function remoteSessionStatusLabel(status) {
3115
3338
  const labels = {
@@ -3206,9 +3429,9 @@ function isQueueIndependentCommand(command) {
3206
3429
  return true;
3207
3430
  return false;
3208
3431
  }
3209
- function parseSessionVisibleCount(value) {
3432
+ function parseSessionPage(value) {
3210
3433
  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;
3434
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
3212
3435
  }
3213
3436
  function isPromptCommand(command) {
3214
3437
  return command.type === "prompt" || command.type === "nosteer";