@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
@@ -17,11 +17,15 @@ import {
17
17
  CardRenderer,
18
18
  type CardSection,
19
19
  type ExecutionSettingsTab,
20
+ type HelpCardSection,
21
+ type ResetHistoryCardEntry,
20
22
  type TaskListCardAction,
23
+ type TaskListCardEntry,
21
24
  } from "../feishu/CardRenderer.js";
22
25
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
23
26
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
24
27
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
28
+ import { buildTurnGraphRows } from "../presentation/turnGraph.js";
25
29
  import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
26
30
  import type {
27
31
  AgentRuntime,
@@ -61,8 +65,126 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
61
65
  const MESSAGE_FAILED_REACTION = "ERROR";
62
66
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
63
67
  const SESSION_PAGE_SIZE = 5;
68
+ const RESET_HISTORY_PAGE_SIZE = 10;
64
69
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
65
70
 
71
+ interface HelpCommandDefinition {
72
+ command: string;
73
+ usage?: string;
74
+ description: string;
75
+ requiresArgument?: boolean;
76
+ }
77
+
78
+ const HELP_COMMAND_SECTIONS: Array<{
79
+ title: string;
80
+ commands: HelpCommandDefinition[];
81
+ }> = [
82
+ {
83
+ title: "任务管理",
84
+ commands: [
85
+ {
86
+ command: "/new",
87
+ usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
88
+ description: "使用默认 Agent 创建任务;--nodir 创建 Projectless 任务",
89
+ },
90
+ {
91
+ command: "/newgroup",
92
+ usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
93
+ description: "创建飞书群和新任务",
94
+ },
95
+ {
96
+ command: "/forkgroup",
97
+ usage: "[title]",
98
+ description: "从当前任务最近完成轮次创建分支群",
99
+ },
100
+ {
101
+ command: "/fork",
102
+ usage: "[序号或任务 ID]",
103
+ description: "从当前或指定任务最近完成轮次创建分支",
104
+ },
105
+ { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
106
+ {
107
+ command: "/title",
108
+ usage: "&#60;新标题&#62;",
109
+ description: "修改当前任务标题",
110
+ requiresArgument: true,
111
+ },
112
+ { command: "/sessions", usage: "[关键词]", description: "查找本机任务" },
113
+ {
114
+ command: "/switch",
115
+ usage: "[序号或任务 ID]",
116
+ description: "切换任务;不填参数切回上一个任务",
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ title: "执行设置",
122
+ commands: [
123
+ {
124
+ command: "!",
125
+ usage: "&#60;命令&#62;",
126
+ description: "在当前任务目录执行本地命令",
127
+ requiresArgument: true,
128
+ },
129
+ { command: "/stop", description: "停止当前执行" },
130
+ {
131
+ command: "/queue",
132
+ usage: "&#60;prompt&#62;",
133
+ description: "将 Prompt 排队为后续轮次",
134
+ requiresArgument: true,
135
+ },
136
+ {
137
+ command: "/nosteer",
138
+ usage: "&#60;prompt&#62;",
139
+ description: "与 /queue 相同",
140
+ requiresArgument: true,
141
+ },
142
+ {
143
+ command: "/goal",
144
+ usage: "&#60;目标或操作&#62;",
145
+ description: "查看或创建长任务 Goal;支持 pause、resume、edit、clear",
146
+ requiresArgument: true,
147
+ },
148
+ { command: "/provider", description: "选择 AI 服务提供商" },
149
+ { command: "/model", description: "选择当前任务使用的模型" },
150
+ { command: "/thinking", description: "设置模型的思考强度" },
151
+ { command: "/permissions", description: "设置执行工具前是否需要确认" },
152
+ ],
153
+ },
154
+ {
155
+ title: "Agent",
156
+ commands: [
157
+ {
158
+ command: "/agent",
159
+ usage: "[name]",
160
+ description: "选择新任务使用的默认 Agent",
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ title: "系统",
166
+ commands: [
167
+ {
168
+ command: "/status",
169
+ usage: "[序号或任务 ID]",
170
+ description: "查看当前或指定任务状态",
171
+ },
172
+ {
173
+ command: "/restart",
174
+ usage: "[--force]",
175
+ description: "默认安全重启;--force 立即重启",
176
+ },
177
+ { command: "/help", description: "显示本帮助" },
178
+ ],
179
+ },
180
+ ];
181
+
182
+ const HELP_DEFAULT_COMMANDS = new Set(
183
+ HELP_COMMAND_SECTIONS.flatMap((section) => section.commands)
184
+ .filter((command) => !command.requiresArgument)
185
+ .map((command) => command.command),
186
+ );
187
+
66
188
  interface AgentRemoteSession {
67
189
  agentName: string;
68
190
  runtime: AgentRuntime;
@@ -77,7 +199,7 @@ interface AgentRemoteSessionSummary {
77
199
  interface SessionsCardOptions {
78
200
  updateMessageId?: string;
79
201
  forceSwitchTaskId?: string;
80
- visibleCount?: number;
202
+ page?: number;
81
203
  }
82
204
 
83
205
  interface StatusCardOptions {
@@ -85,6 +207,12 @@ interface StatusCardOptions {
85
207
  forceSwitchTaskId?: string;
86
208
  }
87
209
 
210
+ interface ResetHistoryCardOptions {
211
+ expectedSessionId?: string;
212
+ updateMessageId?: string;
213
+ page?: number;
214
+ }
215
+
88
216
  interface ModelCardOptions {
89
217
  sessionId?: string;
90
218
  updateMessageId?: string;
@@ -167,8 +295,9 @@ export interface ControlTaskGroupResult {
167
295
 
168
296
  export interface ProxyLifecycle {
169
297
  supervised?: boolean;
170
- restart(contextKey: string, force: boolean): Promise<void>;
298
+ restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
171
299
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
300
+ rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
172
301
  }
173
302
 
174
303
  export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
@@ -181,6 +310,7 @@ export class ProxySessionController {
181
310
  private readonly queuedPromptStarts = new Map<string, Promise<void>>();
182
311
  private readonly queuedPromptCards = new Map<string, Map<string, string>>();
183
312
  private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
313
+ private readonly sessionResets = new Map<string, Promise<void>>();
184
314
  private readonly lastSessionListings = new Map<string, string[]>();
185
315
  private readonly threadInitializations = new Map<string, Promise<void>>();
186
316
  private readonly unsubscribe: Array<() => void> = [];
@@ -240,8 +370,20 @@ export class ProxySessionController {
240
370
  "Failed to acknowledge the incoming Feishu message with a reaction.",
241
371
  );
242
372
  }
373
+ if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
374
+ try {
375
+ await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
376
+ } catch (error) {
377
+ this.logger.warn(
378
+ { error, messageId: message.messageId },
379
+ "Failed to persist the first private-chat Lark user Open ID.",
380
+ );
381
+ }
382
+ }
243
383
  if (message.chatId && message.chatType) {
244
- this.store.recordChatContext(baseChatContextKey(message.contextKey), message.chatType);
384
+ const chatContextKey = baseChatContextKey(message.contextKey);
385
+ this.store.recordChatContext(chatContextKey, message.chatType);
386
+ this.store.markChatActive(chatContextKey);
245
387
  }
246
388
  const replyTarget = message.replyInThread
247
389
  ? { messageId: message.messageId, replyInThread: true as const }
@@ -252,9 +394,6 @@ export class ProxySessionController {
252
394
  text: message.text,
253
395
  ...(imageCount > 0 ? { imageCount } : {}),
254
396
  });
255
- if (imageCount > 0 && message.chatId && message.chatType) {
256
- this.store.markChatActive(baseChatContextKey(message.contextKey));
257
- }
258
397
  let localImagePaths: string[] | undefined;
259
398
  if (imageCount > 0) {
260
399
  try {
@@ -284,17 +423,6 @@ export class ProxySessionController {
284
423
  );
285
424
  return;
286
425
  }
287
- if (
288
- message.chatId
289
- && message.chatType
290
- && (
291
- command.type === "shell"
292
- || (command.type === "prompt" && !message.text.trimStart().startsWith("/"))
293
- )
294
- ) {
295
- this.store.markChatActive(baseChatContextKey(message.contextKey));
296
- }
297
-
298
426
  // Operational and read-only commands must remain available even if a prompt operation is slow.
299
427
  if (isQueueIndependentCommand(command)) {
300
428
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
@@ -355,7 +483,9 @@ export class ProxySessionController {
355
483
  await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
356
484
  try {
357
485
  const kind = String(scopedAction.value.action ?? "");
358
- if (kind === "turn_details") {
486
+ if (kind === "help_command") {
487
+ await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
488
+ } else if (kind === "turn_details") {
359
489
  await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
360
490
  } else if (kind === "activity_history") {
361
491
  const requestedPage = String(scopedAction.value.page ?? "0");
@@ -369,6 +499,26 @@ export class ProxySessionController {
369
499
  } else if (kind === "turn_cancel") {
370
500
  const sessionId = String(scopedAction.value.sessionId ?? "");
371
501
  await this.cancelSession(this.requireSession(contextKey, sessionId));
502
+ } else if (kind === "turn_reset") {
503
+ const sessionId = String(scopedAction.value.sessionId ?? "");
504
+ await this.resetCurrentSessionToTurn(
505
+ contextKey,
506
+ sessionId,
507
+ String(scopedAction.value.turnId ?? ""),
508
+ );
509
+ if (scopedAction.value.cardView === "reset_history") {
510
+ await this.openResetHistory(contextKey, {
511
+ expectedSessionId: sessionId,
512
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
513
+ page: resetHistoryPageValue(scopedAction.value.page),
514
+ });
515
+ }
516
+ } else if (kind === "turn_reset_page") {
517
+ await this.openResetHistory(contextKey, {
518
+ expectedSessionId: String(scopedAction.value.sessionId ?? ""),
519
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
520
+ page: resetHistoryPageValue(scopedAction.value.page),
521
+ });
372
522
  } else if (kind === "queued_prompt_cancel") {
373
523
  await this.cancelQueuedPrompt(scopedAction);
374
524
  } else if (kind === "safe_restart_cancel") {
@@ -383,17 +533,17 @@ export class ProxySessionController {
383
533
  if (!cancelled) {
384
534
  await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
385
535
  }
386
- } else if (kind === "session_more") {
387
- await this.refreshSessionsCardFromAction(scopedAction, undefined, true);
536
+ } else if (kind === "session_page") {
537
+ await this.refreshSessionsCardFromAction(scopedAction);
388
538
  } else if (kind === "session_switch") {
389
539
  const sessionId = String(scopedAction.value.sessionId ?? "");
390
540
  await this.switchSession(contextKey, sessionId);
391
541
  if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction);
392
- else await this.refreshSessionsCardFromAction(scopedAction);
542
+ else await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
393
543
  } else if (kind === "session_fork") {
394
544
  const sessionId = String(scopedAction.value.sessionId ?? "");
395
545
  await this.forkSessionReference(contextKey, sessionId);
396
- await this.refreshSessionsCardFromAction(scopedAction);
546
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
397
547
  } else if (kind === "session_fork_group") {
398
548
  const sessionId = String(scopedAction.value.sessionId ?? "");
399
549
  await this.forkSessionReferenceToFeishuGroup(contextKey, sessionId, scopedAction.userId);
@@ -401,7 +551,7 @@ export class ProxySessionController {
401
551
  } else if (kind === "session_new") {
402
552
  const sessionId = String(scopedAction.value.sessionId ?? "");
403
553
  await this.createProjectSessionFromReference(contextKey, sessionId);
404
- await this.refreshSessionsCardFromAction(scopedAction);
554
+ await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
405
555
  } else if (kind === "session_new_group") {
406
556
  const sessionId = String(scopedAction.value.sessionId ?? "");
407
557
  await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
@@ -410,7 +560,7 @@ export class ProxySessionController {
410
560
  const sessionId = String(scopedAction.value.sessionId ?? "");
411
561
  await this.stopSessionReference(contextKey, sessionId);
412
562
  if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
413
- else await this.refreshSessionsCardFromAction(scopedAction, sessionId);
563
+ else await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
414
564
  } else if (kind === "session_status") {
415
565
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
416
566
  } else if (kind === "session_status_refresh") {
@@ -541,6 +691,7 @@ export class ProxySessionController {
541
691
  this.lastSessionListings.clear();
542
692
  this.queuedPromptCards.clear();
543
693
  this.queuedPromptCardWrites.clear();
694
+ this.sessionResets.clear();
544
695
  }
545
696
 
546
697
  async controlStopTask(localSessionId: string): Promise<string> {
@@ -831,7 +982,10 @@ export class ProxySessionController {
831
982
  return;
832
983
  case "restart":
833
984
  if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
834
- await this.lifecycle.restart(contextKey, command.force === true);
985
+ await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
986
+ return;
987
+ case "turns":
988
+ await this.openResetHistory(contextKey);
835
989
  return;
836
990
  case "model":
837
991
  await this.openExecutionSettings(contextKey, "model");
@@ -2063,6 +2217,24 @@ export class ProxySessionController {
2063
2217
  }
2064
2218
 
2065
2219
  private async handleRuntimeEvent(event: RuntimeEvent): Promise<void> {
2220
+ if ("turnId" in event) {
2221
+ const source = this.store.getSession(event.sessionId);
2222
+ if (source?.remoteSessionId && this.isCodexSession(source)) {
2223
+ this.store.saveTurnRuntimeOrigin(
2224
+ event.turnId,
2225
+ source.localSessionId,
2226
+ source.agentName,
2227
+ source.remoteSessionId,
2228
+ );
2229
+ }
2230
+ if (event.type === "turn_started") {
2231
+ this.store.saveTurnParent(
2232
+ event.turnId,
2233
+ event.sessionId,
2234
+ this.completedParentTurnId(source),
2235
+ );
2236
+ }
2237
+ }
2066
2238
  if (event.type === "session_metadata_updated") {
2067
2239
  this.store.updateRuntimeSession(event.sessionId, { title: event.title });
2068
2240
  this.outbound.updateSessionTitle(event.sessionId, event.title);
@@ -2157,6 +2329,13 @@ export class ProxySessionController {
2157
2329
  }
2158
2330
  }
2159
2331
 
2332
+ private completedParentTurnId(source: SessionRecord | undefined): string | undefined {
2333
+ if (!source?.lastTurnId) return undefined;
2334
+ if (source.lastTurnStatus === "completed") return source.lastTurnId;
2335
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(source.lastTurnId));
2336
+ return snapshot?.status === "completed" ? source.lastTurnId : undefined;
2337
+ }
2338
+
2160
2339
  private async finalizeStandaloneMessageReaction(
2161
2340
  messageId: string,
2162
2341
  status: "completed" | "failed" | "cancelled",
@@ -2271,6 +2450,111 @@ export class ProxySessionController {
2271
2450
  await loaded.runtime.cancelTurn(record.localSessionId, turnId);
2272
2451
  }
2273
2452
 
2453
+ private async resetCurrentSessionToTurn(
2454
+ contextKey: string,
2455
+ sessionId: string,
2456
+ turnId: string,
2457
+ ): Promise<void> {
2458
+ if (!sessionId || !turnId) throw new Error("无效的 Reset 请求。请使用最新的思考卡片重试。");
2459
+ const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
2460
+ const reset = previous.catch(() => undefined).then(() =>
2461
+ this.performCurrentSessionReset(contextKey, sessionId, turnId));
2462
+ this.sessionResets.set(sessionId, reset);
2463
+ try {
2464
+ await reset;
2465
+ } finally {
2466
+ if (this.sessionResets.get(sessionId) === reset) this.sessionResets.delete(sessionId);
2467
+ }
2468
+ }
2469
+
2470
+ private async performCurrentSessionReset(
2471
+ contextKey: string,
2472
+ sessionId: string,
2473
+ turnId: string,
2474
+ ): Promise<void> {
2475
+ const current = this.requireCurrentSession(contextKey);
2476
+ if (current.localSessionId !== sessionId) {
2477
+ throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
2478
+ }
2479
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
2480
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2481
+ }
2482
+
2483
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
2484
+ if (!snapshot || snapshot.sessionId !== sessionId || snapshot.status !== "completed") {
2485
+ throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
2486
+ }
2487
+ if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
2488
+ await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
2489
+ return;
2490
+ }
2491
+
2492
+ const runtime = this.runtimes.forAgent(current.agentName);
2493
+ if (runtime.kind !== "codex" || !runtime.forkSession) {
2494
+ throw new Error("当前 App Server Agent 不支持 Reset。");
2495
+ }
2496
+ const loaded = await this.loadSession(current);
2497
+ const activity = await this.assertSessionTurnOwnership(current, runtime);
2498
+ const active = activity?.active
2499
+ ?? Boolean(loaded.session.activeTurnId || current.status === "running" || current.lastTurnStatus === "running");
2500
+ if (active) {
2501
+ throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
2502
+ }
2503
+
2504
+ const origin = this.store.getTurnRuntimeOrigin(turnId);
2505
+ if (origin && (origin.localSessionId !== sessionId || origin.agentName !== current.agentName)) {
2506
+ throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
2507
+ }
2508
+ const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
2509
+ const previousRemoteSessionId = current.remoteSessionId;
2510
+ const forked = await runtime.forkSession({
2511
+ localSessionId: current.localSessionId,
2512
+ remoteSessionId: sourceRemoteSessionId,
2513
+ lastTurnId: turnId,
2514
+ agentName: current.agentName,
2515
+ cwd: current.cwd,
2516
+ title: current.title,
2517
+ modelProvider: current.modelProvider,
2518
+ model: current.model,
2519
+ reasoningEffort: current.reasoningEffort,
2520
+ permissionMode: current.permissionMode ?? "auto",
2521
+ });
2522
+ this.persistRuntimeSession(current, forked, "ready");
2523
+ this.store.updateRuntimeSession(current.localSessionId, {
2524
+ lastTurnId: turnId,
2525
+ lastTurnStatus: "completed",
2526
+ });
2527
+ this.outbound.registerSession(
2528
+ current.localSessionId,
2529
+ contextKey,
2530
+ forked.title ?? current.title,
2531
+ current.cwd,
2532
+ this.agentLabel(current.agentName),
2533
+ );
2534
+ this.store.audit(contextKey, "session_reset_to_turn", {
2535
+ localSessionId: current.localSessionId,
2536
+ previousRemoteSessionId,
2537
+ sourceRemoteSessionId,
2538
+ resetTurnId: turnId,
2539
+ forkedRemoteSessionId: forked.remoteSessionId,
2540
+ });
2541
+ const targetSummary = truncateText(
2542
+ (snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(),
2543
+ 100,
2544
+ ) || "未记录对话内容";
2545
+ const targetTime = snapshot.completedAt ?? snapshot.startedAt;
2546
+ await this.outbound.sendText(
2547
+ contextKey,
2548
+ [
2549
+ "已将当前任务重置到:",
2550
+ targetSummary,
2551
+ `完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
2552
+ `Turn ID:${turnId}`,
2553
+ "后续对话将从该轮完成后的状态继续;本地文件没有回退。",
2554
+ ].join("\n"),
2555
+ );
2556
+ }
2557
+
2274
2558
  private async resolveApproval(action: CardAction): Promise<void> {
2275
2559
  const sessionId = String(action.value.sessionId ?? "");
2276
2560
  const requestId = String(action.value.requestId ?? "");
@@ -2824,6 +3108,92 @@ export class ProxySessionController {
2824
3108
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
2825
3109
  }
2826
3110
 
3111
+ private async openResetHistory(
3112
+ contextKey: string,
3113
+ options: ResetHistoryCardOptions = {},
3114
+ ): Promise<void> {
3115
+ const current = this.requireCurrentSession(contextKey);
3116
+ if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
3117
+ throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
3118
+ }
3119
+ if (!current.remoteSessionId || !this.isCodexSession(current)) {
3120
+ throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3121
+ }
3122
+ const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
3123
+ const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
3124
+ turnId: turn.turnId,
3125
+ parentTurnId: turn.parentTurnId,
3126
+ })));
3127
+ const total = allTurns.length;
3128
+ const totalPages = Math.max(1, Math.ceil(total / RESET_HISTORY_PAGE_SIZE));
3129
+ const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
3130
+ const offset = page * RESET_HISTORY_PAGE_SIZE;
3131
+ const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
3132
+ const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
3133
+ const snapshot = turnViewSnapshot(turn.snapshot);
3134
+ if (!snapshot) return [];
3135
+ const graph = graphRows[offset + index]!;
3136
+ const summary = truncateText(
3137
+ (snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(),
3138
+ 100,
3139
+ );
3140
+ const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
3141
+ return [{
3142
+ sequence: graph.sequence,
3143
+ graphNodeLine: graph.nodeLine,
3144
+ graphConnectorLine: graph.connectorLine,
3145
+ lines: [
3146
+ `**${cardText(summary || "未记录对话内容")}**`,
3147
+ `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardCode(turn.turnId)}`,
3148
+ ],
3149
+ current: isCurrent,
3150
+ actions: isCurrent ? undefined : [{
3151
+ text: "Reset",
3152
+ value: {
3153
+ action: "turn_reset",
3154
+ cardView: "reset_history",
3155
+ sessionId: current.localSessionId,
3156
+ turnId: turn.turnId,
3157
+ contextKey,
3158
+ page: String(page),
3159
+ },
3160
+ }],
3161
+ }];
3162
+ });
3163
+ const pageActions: TaskListCardAction[] = [
3164
+ ...(page > 0 ? [{
3165
+ text: "Previous",
3166
+ value: {
3167
+ action: "turn_reset_page",
3168
+ sessionId: current.localSessionId,
3169
+ contextKey,
3170
+ page: String(page - 1),
3171
+ },
3172
+ }] : []),
3173
+ ...(page < totalPages - 1 ? [{
3174
+ text: "Next",
3175
+ value: {
3176
+ action: "turn_reset_page",
3177
+ sessionId: current.localSessionId,
3178
+ contextKey,
3179
+ page: String(page + 1),
3180
+ },
3181
+ }] : []),
3182
+ ];
3183
+ const card = this.cardRenderer.renderResetHistoryCard({
3184
+ entries,
3185
+ footerLines: [
3186
+ `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
3187
+ ],
3188
+ pageActions,
3189
+ });
3190
+ if (options.updateMessageId) {
3191
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
3192
+ } else {
3193
+ await this.outbound.sendInteractiveCard(contextKey, card);
3194
+ }
3195
+ }
3196
+
2827
3197
  private async listSessions(
2828
3198
  contextKey: string,
2829
3199
  searchTerm?: string,
@@ -2831,7 +3201,8 @@ export class ProxySessionController {
2831
3201
  ): Promise<void> {
2832
3202
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
2833
3203
  const normalizedSearch = searchTerm?.trim().toLowerCase();
2834
- const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
3204
+ const requestedPage = Math.max(0, Math.trunc(options.page ?? 0));
3205
+ const fetchLimit = (requestedPage + 1) * SESSION_PAGE_SIZE;
2835
3206
  const localSessions = this.store.listSessions(contextKey).filter((session) =>
2836
3207
  session.status !== "closed" && this.isCodexSession(session),
2837
3208
  );
@@ -2841,9 +3212,9 @@ export class ProxySessionController {
2841
3212
  await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2842
3213
  if (!runtime.listRemoteSessions) return;
2843
3214
  try {
2844
- const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
2845
- remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2846
- remoteHasMore ||= Boolean(page.nextCursor);
3215
+ const result = await runtime.listRemoteSessions({ searchTerm, limit: fetchLimit });
3216
+ remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
3217
+ remoteHasMore ||= Boolean(result.nextCursor);
2847
3218
  } catch (error) {
2848
3219
  this.logger.warn({ error, contextKey, agentName }, "Failed to list App Server sessions for an Agent.");
2849
3220
  remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
@@ -2861,9 +3232,13 @@ export class ProxySessionController {
2861
3232
  || Number(right.active) - Number(left.active)
2862
3233
  || right.updatedAt - left.updatedAt);
2863
3234
  const activeCount = entries.filter((entry) => entry.active).length;
2864
- const visibleEntries = entries.slice(0, visibleCount);
2865
- const hasMore = remoteHasMore || entries.length > visibleCount;
2866
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
3235
+ const lastKnownPage = Math.max(0, Math.ceil(entries.length / SESSION_PAGE_SIZE) - 1);
3236
+ const page = remoteHasMore ? requestedPage : Math.min(requestedPage, lastKnownPage);
3237
+ const offset = page * SESSION_PAGE_SIZE;
3238
+ const visibleEntries = entries.slice(offset, offset + SESSION_PAGE_SIZE);
3239
+ const hasPrevious = page > 0;
3240
+ const hasNext = remoteHasMore || entries.length > offset + SESSION_PAGE_SIZE;
3241
+ this.lastSessionListings.set(contextKey, entries.map((entry) => entry.reference));
2867
3242
  const cardEntries = visibleEntries.map((entry, index) => {
2868
3243
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
2869
3244
  const showStop = entry.status === "外部执行中"
@@ -2876,7 +3251,7 @@ export class ProxySessionController {
2876
3251
  action: showStop ? "session_stop" : "session_switch",
2877
3252
  sessionId: entry.reference,
2878
3253
  ...(searchTerm ? { searchTerm } : {}),
2879
- visibleCount: String(visibleCount),
3254
+ page: String(page),
2880
3255
  contextKey,
2881
3256
  },
2882
3257
  }];
@@ -2886,7 +3261,7 @@ export class ProxySessionController {
2886
3261
  action: "session_new",
2887
3262
  sessionId: entry.reference,
2888
3263
  ...(searchTerm ? { searchTerm } : {}),
2889
- visibleCount: String(visibleCount),
3264
+ page: String(page),
2890
3265
  contextKey,
2891
3266
  },
2892
3267
  });
@@ -2896,7 +3271,7 @@ export class ProxySessionController {
2896
3271
  action: "session_new_group",
2897
3272
  sessionId: entry.reference,
2898
3273
  ...(searchTerm ? { searchTerm } : {}),
2899
- visibleCount: String(visibleCount),
3274
+ page: String(page),
2900
3275
  contextKey,
2901
3276
  },
2902
3277
  });
@@ -2906,7 +3281,7 @@ export class ProxySessionController {
2906
3281
  action: "session_fork",
2907
3282
  sessionId: entry.reference,
2908
3283
  ...(searchTerm ? { searchTerm } : {}),
2909
- visibleCount: String(visibleCount),
3284
+ page: String(page),
2910
3285
  contextKey,
2911
3286
  },
2912
3287
  });
@@ -2916,7 +3291,7 @@ export class ProxySessionController {
2916
3291
  action: "session_fork_group",
2917
3292
  sessionId: entry.reference,
2918
3293
  ...(searchTerm ? { searchTerm } : {}),
2919
- visibleCount: String(visibleCount),
3294
+ page: String(page),
2920
3295
  contextKey,
2921
3296
  },
2922
3297
  });
@@ -2930,7 +3305,7 @@ export class ProxySessionController {
2930
3305
  });
2931
3306
  return {
2932
3307
  lines: [
2933
- `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
3308
+ `**${offset + index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
2934
3309
  `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
2935
3310
  ],
2936
3311
  actions,
@@ -2942,22 +3317,31 @@ export class ProxySessionController {
2942
3317
  cardEntries,
2943
3318
  [
2944
3319
  ...(remoteHint ? [remoteHint] : []),
2945
- "点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
2946
- "点击 **NewGroup** 在对应任务的项目中创建新群和新任务;点击 **ForkGroup** 从对应任务最新已完成轮次创建分支群。",
2947
- "也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
2948
- "发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
2949
- "点击 **Status**,或发送 **/status [序号或任务 ID]**,查看当前或指定任务状态。",
3320
+ `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
3321
+ "",
3322
+ "> **Switch** 切换任务;**Stop** 停止运行;**Status** 查看状态。",
3323
+ "> **New** / **Fork** 新建 / 分支任务;**NewGroup** / **ForkGroup** 新建 / 分支群。",
3324
+ ],
3325
+ [
3326
+ ...(hasPrevious ? [{
3327
+ text: "Previous",
3328
+ value: {
3329
+ action: "session_page",
3330
+ ...(searchTerm ? { searchTerm } : {}),
3331
+ page: String(page - 1),
3332
+ contextKey,
3333
+ },
3334
+ }] : []),
3335
+ ...(hasNext ? [{
3336
+ text: "Next",
3337
+ value: {
3338
+ action: "session_page",
3339
+ ...(searchTerm ? { searchTerm } : {}),
3340
+ page: String(page + 1),
3341
+ contextKey,
3342
+ },
3343
+ }] : []),
2950
3344
  ],
2951
- hasMore ? {
2952
- text: "More",
2953
- type: "primary",
2954
- value: {
2955
- action: "session_more",
2956
- ...(searchTerm ? { searchTerm } : {}),
2957
- visibleCount: String(visibleCount),
2958
- contextKey,
2959
- },
2960
- } : undefined,
2961
3345
  );
2962
3346
  if (options.updateMessageId) {
2963
3347
  await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
@@ -2968,18 +3352,16 @@ export class ProxySessionController {
2968
3352
 
2969
3353
  private async refreshSessionsCardFromAction(
2970
3354
  action: CardAction,
2971
- forceSwitchTaskId?: string,
2972
- loadMore = false,
3355
+ options: { forceSwitchTaskId?: string; page?: number } = {},
2973
3356
  ): Promise<void> {
2974
3357
  if (!action.messageId) return;
2975
3358
  const searchTerm = typeof action.value.searchTerm === "string" && action.value.searchTerm.trim()
2976
3359
  ? action.value.searchTerm
2977
3360
  : undefined;
2978
- const currentVisibleCount = parseSessionVisibleCount(action.value.visibleCount);
2979
3361
  await this.listSessions(action.contextKey, searchTerm, {
2980
3362
  updateMessageId: action.messageId,
2981
- forceSwitchTaskId,
2982
- visibleCount: currentVisibleCount + (loadMore ? SESSION_PAGE_SIZE : 0),
3363
+ forceSwitchTaskId: options.forceSwitchTaskId,
3364
+ page: options.page ?? parseSessionPage(action.value.page),
2983
3365
  });
2984
3366
  }
2985
3367
 
@@ -3400,58 +3782,61 @@ export class ProxySessionController {
3400
3782
  else await this.outbound.sendInteractiveCard(contextKey, card);
3401
3783
  }
3402
3784
 
3785
+ private async executeHelpCommandAction(
3786
+ action: CardAction,
3787
+ contextKey: string,
3788
+ replyTarget?: MessageReplyTarget,
3789
+ ): Promise<void> {
3790
+ const commandText = typeof action.value.command === "string"
3791
+ ? action.value.command.trim()
3792
+ : "";
3793
+ if (!HELP_DEFAULT_COMMANDS.has(commandText)) {
3794
+ throw new Error("帮助卡片中的命令无效,请发送 /help 获取最新卡片。");
3795
+ }
3796
+ const command = this.router.parse(commandText);
3797
+ await this.execute(
3798
+ contextKey,
3799
+ command,
3800
+ undefined,
3801
+ replyTarget,
3802
+ undefined,
3803
+ action.userId,
3804
+ );
3805
+ }
3806
+
3403
3807
  private async help(contextKey: string): Promise<void> {
3404
- const sections: CardSection[] = [
3405
- {
3406
- lines: [
3407
- "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
3408
- "> 命令前缀示例:/sess 等同于 /sessions。",
3409
- "> 命令缩写示例:/fg 等同于 /forkgroup。",
3410
- "前缀或缩写匹配多个命令时,需要输入更长的形式。",
3411
- "**[参数]** 可选 **&#60;参数&#62;** 必填",
3412
- ],
3413
- },
3414
- {
3415
- title: "任务管理",
3416
- lines: [
3417
- "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
3418
- "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
3419
- "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
3420
- "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
3421
- "**/title &#60;新标题&#62;** 修改当前任务的标题",
3422
- "**/sessions [关键词]** 查找本机任务",
3423
- "**/switch [序号或任务 ID]** 不填参数切回上一个任务",
3424
- ],
3425
- },
3426
- {
3427
- title: "执行设置",
3428
- lines: [
3429
- "**! &#60;命令&#62;** 在当前任务目录直接执行本地命令",
3430
- "**/stop** 停止当前执行",
3431
- "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
3432
- "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
3433
- "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
3434
- "**/model** 选择当前任务使用的模型",
3435
- "**/thinking** 设置模型的思考强度",
3436
- "**/permissions** 设置执行工具前是否需要确认",
3437
- ],
3438
- },
3439
- {
3440
- title: "Agent",
3441
- lines: [
3442
- "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
3443
- ],
3444
- },
3445
- {
3446
- title: "系统",
3447
- lines: [
3448
- "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
3449
- "**/restart [--force]** 默认安全重启;--force 立即重启",
3450
- "**/help** 显示本帮助",
3451
- ],
3452
- },
3453
- ];
3454
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Bot 使用帮助", sections));
3808
+ const sections: HelpCardSection[] = HELP_COMMAND_SECTIONS.map((section) => ({
3809
+ title: section.title,
3810
+ commands: section.commands.map((command) => ({
3811
+ text: command.command,
3812
+ ...(command.requiresArgument
3813
+ ? {}
3814
+ : {
3815
+ action: {
3816
+ text: command.command,
3817
+ value: {
3818
+ action: "help_command",
3819
+ command: command.command,
3820
+ contextKey,
3821
+ },
3822
+ },
3823
+ }),
3824
+ ...(command.usage ? { usage: command.usage } : {}),
3825
+ description: command.description,
3826
+ })),
3827
+ }));
3828
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
3829
+ "Agent Bot 使用帮助",
3830
+ [
3831
+ "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
3832
+ "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
3833
+ "> 命令前缀示例:/sess 等同于 /sessions。",
3834
+ "> 命令缩写示例:/fg 等同于 /forkgroup。",
3835
+ "前缀或缩写匹配多个命令时,需要输入更长的形式。",
3836
+ "**[参数]** 可选 **&#60;参数&#62;** 必填",
3837
+ ],
3838
+ sections,
3839
+ ));
3455
3840
  }
3456
3841
 
3457
3842
  private ensureAgent(agentName: string) {
@@ -3825,6 +4210,10 @@ function formatStatusTime(value: string): string {
3825
4210
  }).format(date);
3826
4211
  }
3827
4212
 
4213
+ function formatResetTurnTime(value?: number): string {
4214
+ return value === undefined ? "完成时间未知" : formatStatusTime(new Date(value).toISOString());
4215
+ }
4216
+
3828
4217
  function cardText(value: string): string {
3829
4218
  return value.replaceAll("&", "&amp;").replaceAll("`", "&#96;").replaceAll("<", "&#60;").replaceAll(">", "&#62;");
3830
4219
  }
@@ -3843,9 +4232,15 @@ function executionSettingsTabValue(value: unknown): ExecutionSettingsTab {
3843
4232
  throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3844
4233
  }
3845
4234
 
4235
+ function resetHistoryPageValue(value: unknown): number {
4236
+ const page = Number(value);
4237
+ if (!Number.isSafeInteger(page) || page < 0) throw new Error("历史轮次卡片页码无效,请重新发送 /turns。");
4238
+ return page;
4239
+ }
4240
+
3846
4241
  function requiredCardMessageId(value: string | undefined): string {
3847
4242
  if (value) return value;
3848
- throw new Error("无法更新设置卡片,请重新发送设置命令。");
4243
+ throw new Error("无法更新卡片,请重新发送对应命令。");
3849
4244
  }
3850
4245
 
3851
4246
  function remoteSessionStatusLabel(status: RemoteSessionSummary["status"]): string {
@@ -3947,9 +4342,9 @@ function isQueueIndependentCommand(command: Command): boolean {
3947
4342
  return false;
3948
4343
  }
3949
4344
 
3950
- function parseSessionVisibleCount(value: unknown): number {
4345
+ function parseSessionPage(value: unknown): number {
3951
4346
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
3952
- return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
4347
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
3953
4348
  }
3954
4349
 
3955
4350
  function isPromptCommand(command: Command): boolean {