@keyou007/agent-bot 0.1.24-alpha.2 → 0.1.24-alpha.3

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 (95) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +27 -5
  3. package/README.zh.md +26 -5
  4. package/dist/acp/AcpJsonRpcConnection.js +3 -1
  5. package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
  6. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  7. package/dist/cli/SelfUpdater.js +2 -0
  8. package/dist/cli/SelfUpdater.js.map +1 -1
  9. package/dist/codex/AppServerConnection.js +3 -1
  10. package/dist/codex/AppServerConnection.js.map +1 -1
  11. package/dist/codex/CodexEventMapper.js +28 -1
  12. package/dist/codex/CodexEventMapper.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +92 -0
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +5 -0
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/feishu/CardRenderer.js +45 -23
  18. package/dist/feishu/CardRenderer.js.map +1 -1
  19. package/dist/feishu/FeishuConnector.js +2 -0
  20. package/dist/feishu/FeishuConnector.js.map +1 -1
  21. package/dist/feishu/FeishuMessageClient.js +44 -0
  22. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  23. package/dist/feishu/FeishuTurnPresenter.js +114 -37
  24. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  25. package/dist/feishu/RecentMessageContext.js +70 -0
  26. package/dist/feishu/RecentMessageContext.js.map +1 -0
  27. package/dist/index.js +62 -24
  28. package/dist/index.js.map +1 -1
  29. package/dist/local-files/DiagramPreview.js +171 -0
  30. package/dist/local-files/DiagramPreview.js.map +1 -0
  31. package/dist/local-files/LocalFileViewerServer.js +141 -3
  32. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  33. package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
  34. package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
  35. package/dist/local-files/TurnPreviewPage.js +495 -60
  36. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  37. package/dist/presentation/OutboundRouter.js +18 -9
  38. package/dist/presentation/OutboundRouter.js.map +1 -1
  39. package/dist/presentation/TurnStateReducer.js +155 -6
  40. package/dist/presentation/TurnStateReducer.js.map +1 -1
  41. package/dist/presentation/turnReasoning.js +23 -0
  42. package/dist/presentation/turnReasoning.js.map +1 -0
  43. package/dist/proxy/ProxySessionController.js +460 -34
  44. package/dist/proxy/ProxySessionController.js.map +1 -1
  45. package/dist/state/StateStore.js +43 -2
  46. package/dist/state/StateStore.js.map +1 -1
  47. package/dist/state/TurnPreviewJournal.js +469 -0
  48. package/dist/state/TurnPreviewJournal.js.map +1 -0
  49. package/dist/state/migrations.js +10 -0
  50. package/dist/state/migrations.js.map +1 -1
  51. package/dist/updates/AutomaticUpdatePreparer.js +16 -7
  52. package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
  53. package/dist/updates/ManualUpdateController.js +102 -0
  54. package/dist/updates/ManualUpdateController.js.map +1 -0
  55. package/dist/updates/PublishedRelease.js +42 -4
  56. package/dist/updates/PublishedRelease.js.map +1 -1
  57. package/dist/utils/ProtocolLineWarning.js +15 -0
  58. package/dist/utils/ProtocolLineWarning.js.map +1 -0
  59. package/docs/technical-reference.md +21 -1
  60. package/docs/technical-reference.zh.md +21 -1
  61. package/npm-shrinkwrap.json +1401 -10
  62. package/package.json +3 -1
  63. package/skills/agent-bot/SKILL.md +8 -1
  64. package/src/acp/AcpJsonRpcConnection.ts +3 -1
  65. package/src/cli/SelfUpdateRunner.ts +1 -0
  66. package/src/cli/SelfUpdater.ts +2 -0
  67. package/src/codex/AppServerConnection.ts +3 -1
  68. package/src/codex/CodexEventMapper.ts +29 -1
  69. package/src/codex/CodexRuntime.ts +87 -0
  70. package/src/commands/CommandRouter.ts +4 -0
  71. package/src/commands/commandTypes.ts +1 -0
  72. package/src/feishu/CardRenderer.ts +43 -21
  73. package/src/feishu/FeishuConnector.ts +2 -0
  74. package/src/feishu/FeishuMessageClient.ts +44 -0
  75. package/src/feishu/FeishuTurnPresenter.ts +107 -31
  76. package/src/feishu/RecentMessageContext.ts +90 -0
  77. package/src/feishu/types.ts +5 -0
  78. package/src/index.ts +62 -22
  79. package/src/local-files/DiagramPreview.ts +173 -0
  80. package/src/local-files/LocalFileViewerServer.ts +129 -2
  81. package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
  82. package/src/local-files/TurnPreviewPage.ts +535 -57
  83. package/src/presentation/OutboundRouter.ts +25 -7
  84. package/src/presentation/TurnStateReducer.ts +151 -7
  85. package/src/presentation/turnReasoning.ts +21 -0
  86. package/src/presentation/turnViewTypes.ts +19 -1
  87. package/src/proxy/ProxySessionController.ts +452 -24
  88. package/src/runtime/types.ts +21 -0
  89. package/src/state/StateStore.ts +45 -2
  90. package/src/state/TurnPreviewJournal.ts +398 -0
  91. package/src/state/migrations.ts +10 -0
  92. package/src/updates/AutomaticUpdatePreparer.ts +21 -7
  93. package/src/updates/ManualUpdateController.ts +116 -0
  94. package/src/updates/PublishedRelease.ts +49 -3
  95. package/src/utils/ProtocolLineWarning.ts +13 -0
@@ -26,6 +26,7 @@ import type {
26
26
  IncomingMessage,
27
27
  MessageReplyTarget,
28
28
  ReferencedMessageContent,
29
+ RecentContextMessage,
29
30
  } from "../feishu/types.js";
30
31
  import {
31
32
  type AppServerReleaseCardView,
@@ -49,11 +50,13 @@ import {
49
50
  parseTaskNameFromGroupName,
50
51
  } from "../feishu/GroupNameFormatter.js";
51
52
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
53
+ import { RECENT_CONTEXT_LIMIT, RECENT_CONTEXT_IMAGE_LIMIT, RECENT_CONTEXT_TEXT_LIMIT, recentContextPrompt } from "../feishu/RecentMessageContext.js";
52
54
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
53
55
  import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
54
56
  import { errorLogValue } from "../logging/errorLogValue.js";
55
57
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
56
58
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
59
+ import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
57
60
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
58
61
  import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
59
62
  import type {
@@ -90,6 +93,7 @@ import {
90
93
  type SessionRecord,
91
94
  type TurnAttemptRecord,
92
95
  type TurnAnchorRecord,
96
+ type TurnRuntimeOriginRecord,
93
97
  } from "../state/StateStore.js";
94
98
  import { createId } from "../utils/id.js";
95
99
  import { formatStorageSize } from "../utils/formatStorageSize.js";
@@ -205,6 +209,7 @@ const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
205
209
  const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
206
210
  const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
207
211
  const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
212
+ const SWITCH_GROUP_STARTED_MESSAGE = "正在创建群聊并绑定所选任务,请稍后。";
208
213
  const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
209
214
  const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
210
215
  const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
@@ -344,6 +349,7 @@ const HELP_COMMAND_SECTIONS: Array<{
344
349
  usage: "[--force]",
345
350
  description: "默认安全重启;--force 立即重启",
346
351
  },
352
+ { command: "/update", description: "查看正式版和 Alpha 版更新,选择版本安全更新" },
347
353
  {
348
354
  command: "/release",
349
355
  description: "释放 Agent Bot 占用的 App Server 任务",
@@ -392,6 +398,8 @@ interface StatusCardOptions {
392
398
  }
393
399
 
394
400
  interface ResetHistoryCardOptions {
401
+ sessionReference?: string;
402
+ cursor?: string;
395
403
  expectedSessionId?: string;
396
404
  updateMessageId?: string;
397
405
  page?: number;
@@ -503,6 +511,8 @@ export interface ProxyLifecycle {
503
511
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
504
512
  forceSafeRestart?(scheduleId: number): Promise<boolean>;
505
513
  cancelAutomaticUpdate?(action: CardAction): Promise<void>;
514
+ showUpdates?(contextKey: string, userId?: string): Promise<void>;
515
+ selectUpdate?(action: CardAction, replyTarget?: MessageReplyTarget): Promise<void>;
506
516
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
507
517
  persistAgentExecutionDefaults?(
508
518
  agentName: string,
@@ -527,6 +537,7 @@ export class ProxySessionController {
527
537
  private readonly router = new CommandRouter();
528
538
  private readonly cardRenderer = new CardRenderer();
529
539
  private readonly messageQueues = new Map<string, Promise<void>>();
540
+ private readonly switchGroupOperations = new Set<string>();
530
541
  private readonly sessionLoads = new Map<string, Promise<LoadedSession>>();
531
542
  private readonly queuedPromptStarts = new Map<string, Promise<void>>();
532
543
  private readonly queuedPromptCards = new Map<string, Map<string, string>>();
@@ -535,6 +546,7 @@ export class ProxySessionController {
535
546
  private readonly resetHistoryOperations = new Map<string, string>();
536
547
  private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
537
548
  private readonly turnHistoryHydrations = new Map<string, Promise<boolean>>();
549
+ private readonly turnDetailHydrations = new Map<string, Promise<TurnViewState | undefined>>();
538
550
  private readonly forkHistoryCursors = new Map<string, ForkHistoryCursor>();
539
551
  private readonly lastSessionListings = new Map<string, string[]>();
540
552
  private readonly threadInitializations = new Map<string, Promise<void>>();
@@ -914,11 +926,14 @@ export class ProxySessionController {
914
926
  let executableImagePaths = localImagePaths;
915
927
  let executableMessage = message;
916
928
  let injectedRootMessageId: string | undefined;
929
+ let recentContextIds: string[] = [];
917
930
  if (command.type === "prompt") {
918
931
  await this.ensureThreadFork(message);
919
932
  const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
920
- executableCommand = { ...command, text: prepared.text };
921
- executableImagePaths = prepared.localImagePaths;
933
+ const recent = await this.prepareMutedGroupContext(message, prepared.text, prepared.localImagePaths);
934
+ executableCommand = { ...command, text: recent.text };
935
+ executableImagePaths = recent.localImagePaths;
936
+ recentContextIds = recent.messageIds;
922
937
  executableMessage = { ...message, displayText: prepared.displayPrompt };
923
938
  injectedRootMessageId = prepared.injectedRootMessageId;
924
939
  }
@@ -931,6 +946,12 @@ export class ProxySessionController {
931
946
  message.userId,
932
947
  executableMessage,
933
948
  );
949
+ if (command.type === "prompt" && message.chatType === "group" && message.mentionedBot
950
+ && this.store.chatRequiresMention(baseChatContextKey(message.contextKey))) {
951
+ const current = this.currentSession(message.contextKey);
952
+ if (current) this.store.rememberRecentContextMessages(current.localSessionId, current.remoteSessionId ?? "", message.contextKey, message.messageId,
953
+ [...recentContextIds, message.messageId, ...[referencedMessageId, injectedRootMessageId].filter((id): id is string => Boolean(id))]);
954
+ }
934
955
  if (injectedRootMessageId) {
935
956
  const current = this.currentSession(message.contextKey);
936
957
  if (current) {
@@ -994,10 +1015,15 @@ export class ProxySessionController {
994
1015
  if (kind === "agentbot_update_cancel") {
995
1016
  if (!this.lifecycle?.cancelAutomaticUpdate) throw new Error("自动更新功能未启用。");
996
1017
  await this.lifecycle.cancelAutomaticUpdate(scopedAction);
1018
+ } else if (kind === "agentbot_update_select") {
1019
+ if (!this.lifecycle?.selectUpdate) throw new Error("当前运行方式不支持在线更新。");
1020
+ await this.lifecycle.selectUpdate(scopedAction, replyTarget);
997
1021
  } else if (kind === "help_command") {
998
1022
  await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
999
1023
  } else if (kind === "turn_details") {
1000
- await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
1024
+ const turnId = String(scopedAction.value.turnId ?? "");
1025
+ await this.loadTurnDetails(turnId);
1026
+ await this.outbound.showDetails(contextKey, turnId);
1001
1027
  } else if (kind === "activity_history") {
1002
1028
  const requestedPage = String(scopedAction.value.page ?? "0");
1003
1029
  const numericPage = Number(requestedPage);
@@ -1137,6 +1163,9 @@ export class ProxySessionController {
1137
1163
  const sessionId = String(scopedAction.value.sessionId ?? "");
1138
1164
  await this.forkSessionReference(contextKey, sessionId);
1139
1165
  await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
1166
+ } else if (kind === "session_switch_group") {
1167
+ await this.switchSessionReferenceToFeishuGroup(contextKey, String(scopedAction.value.sessionId ?? ""), scopedAction.userId);
1168
+ await this.refreshSessionsCardFromAction(scopedAction);
1140
1169
  } else if (kind === "session_fork_group") {
1141
1170
  const sessionId = String(scopedAction.value.sessionId ?? "");
1142
1171
  await this.forkSessionReferenceToFeishuGroup(contextKey, sessionId, scopedAction.userId);
@@ -1181,6 +1210,18 @@ export class ProxySessionController {
1181
1210
  else if (scopedAction.value.cardView !== "safe_restart") {
1182
1211
  await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
1183
1212
  }
1213
+ } else if (kind === "session_turns" || kind === "session_turns_page") {
1214
+ const sessionReference = String(scopedAction.value.sessionId ?? "").trim();
1215
+ if (!sessionReference) throw new Error("任务轮次卡片无效,请重新发送 /sessions。");
1216
+ if (kind === "session_turns") await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
1217
+ await this.openResetHistory(contextKey, {
1218
+ sessionReference,
1219
+ ...(kind === "session_turns_page" ? {
1220
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
1221
+ page: resetHistoryPageValue(scopedAction.value.page),
1222
+ cursor: typeof scopedAction.value.cursor === "string" ? scopedAction.value.cursor : undefined,
1223
+ } : {}),
1224
+ });
1184
1225
  } else if (kind === "session_status") {
1185
1226
  await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
1186
1227
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
@@ -1470,6 +1511,7 @@ export class ProxySessionController {
1470
1511
  scopedRecord.cwd,
1471
1512
  this.agentLabel(scopedRecord.agentName),
1472
1513
  scopedRecord.model,
1514
+ scopedRecord.modelProvider,
1473
1515
  );
1474
1516
  const promptMessageId = await this.outbound.withReplyTarget(
1475
1517
  responseContextKey,
@@ -1757,7 +1799,7 @@ export class ProxySessionController {
1757
1799
  await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
1758
1800
  }
1759
1801
  this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
1760
- this.outbound.updateSessionModel(record.localSessionId, nextValue);
1802
+ this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
1761
1803
  } else if (setting === "thinking" && nextValue) {
1762
1804
  const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
1763
1805
  const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
@@ -2139,6 +2181,10 @@ export class ProxySessionController {
2139
2181
  if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
2140
2182
  await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
2141
2183
  return;
2184
+ case "update":
2185
+ if (!this.lifecycle?.showUpdates) throw new Error("当前运行方式不支持检查更新。");
2186
+ await this.lifecycle.showUpdates(contextKey, userId);
2187
+ return;
2142
2188
  case "release":
2143
2189
  await this.requestAppServerRelease(contextKey);
2144
2190
  return;
@@ -2191,6 +2237,7 @@ export class ProxySessionController {
2191
2237
  session.cwd,
2192
2238
  this.agentLabel(session.agentName),
2193
2239
  session.model,
2240
+ session.modelProvider,
2194
2241
  );
2195
2242
  if (!session.lastTurnId) continue;
2196
2243
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error: unknown) => {
@@ -2278,6 +2325,7 @@ export class ProxySessionController {
2278
2325
  session.cwd,
2279
2326
  this.agentLabel(session.agentName),
2280
2327
  session.model,
2328
+ session.modelProvider,
2281
2329
  );
2282
2330
  }
2283
2331
  try {
@@ -2407,6 +2455,7 @@ export class ProxySessionController {
2407
2455
  session.cwd,
2408
2456
  this.agentLabel(session.agentName),
2409
2457
  session.model,
2458
+ session.modelProvider,
2410
2459
  );
2411
2460
  if (announce) {
2412
2461
  await this.outbound.withReplyTarget(contextKey, replyTarget, () =>
@@ -2689,6 +2738,7 @@ export class ProxySessionController {
2689
2738
  record.cwd,
2690
2739
  this.agentLabel(record.agentName),
2691
2740
  record.model,
2741
+ record.modelProvider,
2692
2742
  );
2693
2743
  await this.outbound.startPendingTurn(
2694
2744
  record.localSessionId,
@@ -2696,6 +2746,7 @@ export class ProxySessionController {
2696
2746
  record.title,
2697
2747
  replyTarget,
2698
2748
  displayPrompt ?? text,
2749
+ localImagePaths,
2699
2750
  );
2700
2751
  }
2701
2752
  let loaded: LoadedSession;
@@ -2736,7 +2787,7 @@ export class ProxySessionController {
2736
2787
  }
2737
2788
  try {
2738
2789
  await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
2739
- await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
2790
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId, localImagePaths);
2740
2791
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
2741
2792
  return;
2742
2793
  } catch (error) {
@@ -2764,6 +2815,7 @@ export class ProxySessionController {
2764
2815
  current.activeTurnId,
2765
2816
  displayPrompt ?? text,
2766
2817
  messageId,
2818
+ localImagePaths,
2767
2819
  );
2768
2820
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
2769
2821
  return;
@@ -2792,10 +2844,11 @@ export class ProxySessionController {
2792
2844
  turnId: string,
2793
2845
  text: string,
2794
2846
  messageId?: string,
2847
+ localImagePaths?: string[],
2795
2848
  ): Promise<void> {
2796
2849
  this.store.touchTurnAttempt(turnId);
2797
2850
  try {
2798
- await this.outbound.appendSteerMessage(localSessionId, turnId, text, messageId);
2851
+ await this.outbound.appendSteerMessage(localSessionId, turnId, text, messageId, localImagePaths);
2799
2852
  } catch (error) {
2800
2853
  this.logger.warn(
2801
2854
  { error, sessionId: localSessionId, turnId, messageId },
@@ -2958,6 +3011,70 @@ export class ProxySessionController {
2958
3011
  }
2959
3012
  }
2960
3013
 
3014
+ private async prepareMutedGroupContext(
3015
+ message: IncomingMessage,
3016
+ text: string,
3017
+ localImagePaths?: string[],
3018
+ ): Promise<{ text: string; localImagePaths?: string[]; messageIds: string[] }> {
3019
+ if (message.chatType !== "group" || !message.chatId || message.mentionedBot !== true
3020
+ || !this.store.chatRequiresMention(baseChatContextKey(message.contextKey))) {
3021
+ return { text, localImagePaths, messageIds: [] };
3022
+ }
3023
+ if (message.threadContext && !message.threadId) throw new Error("无法识别当前话题,已停止补充上下文以避免读取其他话题。");
3024
+ try {
3025
+ const current = this.currentSession(message.contextKey);
3026
+ const stopIds = current ? new Set([
3027
+ ...this.store.recentContextMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey, false),
3028
+ ...this.store.recentSubmittedMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey),
3029
+ ]) : new Set<string>();
3030
+ const messages = await this.outbound.readRecentMessages(message.contextKey, {
3031
+ chatId: message.chatId,
3032
+ ...(message.threadId ? { threadId: message.threadId } : {}),
3033
+ beforeMessageId: message.messageId,
3034
+ beforeTimestamp: message.createdAt,
3035
+ ...(stopIds.size ? { stopBeforeMessageIds: [...stopIds] } : {}),
3036
+ });
3037
+ const seen = current ? this.store.recentContextMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey) : new Set<string>();
3038
+ seen.add(message.messageId);
3039
+ if (message.parentMessageId) seen.add(message.parentMessageId);
3040
+ if (message.threadContext && message.rootMessageId) seen.add(message.rootMessageId);
3041
+ const selected: RecentContextMessage[] = [];
3042
+ const imagePaths = [...(localImagePaths ?? [])];
3043
+ const imageKeys = new Set<string>();
3044
+ let remaining = RECENT_CONTEXT_TEXT_LIMIT;
3045
+ let imageCount = 0;
3046
+ for (const item of messages.slice(-RECENT_CONTEXT_LIMIT).reverse()) {
3047
+ if (stopIds.has(item.messageId) || remaining <= 0) break;
3048
+ if (seen.has(item.messageId)) continue;
3049
+ seen.add(item.messageId);
3050
+ const limit = Math.min(2_000, remaining);
3051
+ let body = item.text.length > limit ? `${item.text.slice(0, limit)}\n[消息文本已截断]` : item.text;
3052
+ remaining -= Math.min(item.text.length, limit);
3053
+ for (const [index, image] of item.images.entries()) {
3054
+ if (imageKeys.has(image.imageKey)) continue;
3055
+ imageKeys.add(image.imageKey);
3056
+ if (imageCount >= RECENT_CONTEXT_IMAGE_LIMIT) {
3057
+ body += `\n[图片 ${index + 1} 未附带:已达到近期图片上限]`;
3058
+ continue;
3059
+ }
3060
+ const imagePath = await this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey);
3061
+ imagePaths.push(imagePath);
3062
+ imageCount++;
3063
+ body += `\n[图片 ${index + 1} 已附带:${imagePath}]`;
3064
+ }
3065
+ selected.unshift({ ...item, text: body });
3066
+ }
3067
+ return {
3068
+ text: recentContextPrompt(text, selected),
3069
+ ...(imagePaths.length ? { localImagePaths: [...new Set(imagePaths)] } : {}),
3070
+ messageIds: selected.map((item) => item.messageId),
3071
+ };
3072
+ } catch (error) {
3073
+ const detail = error instanceof Error ? error.message : String(error);
3074
+ throw new Error(`无法补充近期群聊上下文(包括图片):${detail}。请检查飞书历史消息/群消息读取权限及机器人是否在群内,或附图重试。`);
3075
+ }
3076
+ }
3077
+
2961
3078
  private async prepareTopicRootPrompt(
2962
3079
  message: IncomingMessage,
2963
3080
  text: string,
@@ -3048,6 +3165,7 @@ export class ProxySessionController {
3048
3165
  source.cwd,
3049
3166
  this.agentLabel(source.agentName),
3050
3167
  source.model,
3168
+ source.modelProvider,
3051
3169
  );
3052
3170
 
3053
3171
  try {
@@ -3409,7 +3527,8 @@ export class ProxySessionController {
3409
3527
  forked.title ?? plan.forkTitle,
3410
3528
  plan.cwd,
3411
3529
  this.agentLabel(plan.agentName),
3412
- plan.model,
3530
+ forked.model ?? plan.model,
3531
+ forked.modelProvider ?? plan.modelProvider,
3413
3532
  );
3414
3533
  this.store.audit(contextKey, "session_forked", {
3415
3534
  sourceLocalSessionId: plan.source?.localSessionId,
@@ -3458,6 +3577,7 @@ export class ProxySessionController {
3458
3577
  sessionId: input.localSessionId,
3459
3578
  turnId: turn.id,
3460
3579
  prompt: turn.prompt ?? "未记录对话内容",
3580
+ historyDetail: "summary",
3461
3581
  status: "completed",
3462
3582
  startedAt,
3463
3583
  completedAt,
@@ -3807,16 +3927,15 @@ export class ProxySessionController {
3807
3927
  while (true) {
3808
3928
  let pendingTurnId: string | undefined;
3809
3929
  try {
3810
- this.outbound.updateSessionModel(
3811
- loaded.record.localSessionId,
3812
- loaded.runtime.getSession(loaded.record.localSessionId)?.model ?? loaded.session.model,
3813
- );
3930
+ const session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
3931
+ this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
3814
3932
  pendingTurnId = await this.outbound.startPendingTurn(
3815
3933
  loaded.record.localSessionId,
3816
3934
  loaded.record.contextKey,
3817
3935
  title,
3818
3936
  replyTarget,
3819
3937
  cardPrompt,
3938
+ localImagePaths,
3820
3939
  );
3821
3940
  } catch (error) {
3822
3941
  this.store.updateTurnAttempt(attemptId, {
@@ -3933,6 +4052,7 @@ export class ProxySessionController {
3933
4052
  sessionCwd,
3934
4053
  this.agentLabel(agentName),
3935
4054
  resolvedExecutionSettings.model,
4055
+ resolvedExecutionSettings.modelProvider,
3936
4056
  );
3937
4057
  const runtime = this.runtimes.forAgent(agentName);
3938
4058
  try {
@@ -3950,7 +4070,7 @@ export class ProxySessionController {
3950
4070
  permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
3951
4071
  });
3952
4072
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3953
- this.outbound.updateSessionModel(localSessionId, session.model);
4073
+ this.outbound.updateSessionModel(localSessionId, session.model, session.modelProvider);
3954
4074
  const saved = this.store.getSession(localSessionId) ?? record;
3955
4075
  if (announce) {
3956
4076
  const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
@@ -4013,6 +4133,105 @@ export class ProxySessionController {
4013
4133
  );
4014
4134
  }
4015
4135
 
4136
+ private async switchSessionReferenceToFeishuGroup(
4137
+ sourceContextKey: string,
4138
+ reference: string,
4139
+ userId: string | undefined,
4140
+ ): Promise<void> {
4141
+ if (!userId?.startsWith("ou_")) throw new Error("SwitchGroup 只能由具有 open_id 的飞书用户触发。");
4142
+ if (!reference.trim()) throw new Error("任务无效,请重新发送 /sessions。");
4143
+ await this.outbound.sendText(sourceContextKey, SWITCH_GROUP_STARTED_MESSAGE);
4144
+ const taskId = this.resolveSessionReference(sourceContextKey, reference);
4145
+ let task = this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId);
4146
+ if (task?.status === "closed") throw new Error(`找不到任务:${taskId}`);
4147
+ if (task && (!this.isCodexSession(task) || !task.remoteSessionId)) {
4148
+ throw new Error("指定任务不是可切换的 App Server 任务。");
4149
+ }
4150
+ let remote: RemoteSessionSummary | undefined;
4151
+ let agentName: string;
4152
+ let runtime: AgentRuntime;
4153
+ if (task) {
4154
+ agentName = task.agentName;
4155
+ runtime = this.runtimes.forAgent(agentName);
4156
+ await this.assertSessionTurnOwnership(task, runtime);
4157
+ } else {
4158
+ const resolved = await this.resolveRemoteCodexSession(taskId);
4159
+ ({ agentName, runtime, remote } = resolved);
4160
+ if (isRemoteSessionActive(remote)) {
4161
+ throw new Error("这个任务正在外部 Agent 中执行,请等待完成后再使用 SwitchGroup。");
4162
+ }
4163
+ }
4164
+ const remoteSessionId = task?.remoteSessionId ?? remote!.id;
4165
+ const operationKey = agentRemoteKey(agentName, remoteSessionId);
4166
+ if (this.switchGroupOperations.has(operationKey)) throw new Error("正在为此任务创建群聊,请勿重复点击。");
4167
+ const cwd = task?.cwd ?? remote!.cwd;
4168
+ if (!cwd) throw new Error("指定任务没有可用的工作目录,暂时无法创建群聊。");
4169
+ const title = task?.title ?? remote?.title ?? remote?.preview ?? remoteSessionId;
4170
+ const boundProjectCwd = detectProjectlessWorkspace(cwd) ? undefined : cwd;
4171
+ this.switchGroupOperations.add(operationKey);
4172
+ try {
4173
+ const group = await this.createFeishuGroupContext(sourceContextKey, agentName, title, userId, boundProjectCwd, "SwitchGroup");
4174
+ try {
4175
+ const latest = task ? undefined : await runtime.readRemoteSession!(remoteSessionId);
4176
+ if (latest && isRemoteSessionActive(latest)) throw new Error("所选任务已在外部 Agent 中开始执行,请等待完成后切换。");
4177
+ // Group creation may take time; reuse any canonical task linked in the meantime.
4178
+ task = this.store.findSessionByRemoteSessionId(remoteSessionId, undefined, agentName) ?? task;
4179
+ if (task) {
4180
+ task = this.store.getSession(task.localSessionId);
4181
+ if (!task || task.status === "closed") throw new Error("所选任务已归档,请重新选择任务。");
4182
+ await this.assertSessionTurnOwnership(task, runtime);
4183
+ task = this.store.getSession(task.localSessionId);
4184
+ if (!task || task.status === "closed" || task.remoteSessionId !== remoteSessionId) {
4185
+ throw new Error("所选任务已变化,请重新选择任务。");
4186
+ }
4187
+ this.store.attachSessionToContext(group.contextKey, task.localSessionId);
4188
+ } else {
4189
+ if (!latest) throw new Error("无法读取所选任务,请重试。");
4190
+ const localSessionId = createId("sess");
4191
+ this.store.createSession({ localSessionId, contextKey: group.contextKey, agentName, cwd, status: "ready" });
4192
+ this.store.updateRuntimeSession(localSessionId, {
4193
+ runtimeKind: "codex",
4194
+ remoteSessionId,
4195
+ title: latest.title ?? latest.preview ?? title,
4196
+ modelProvider: latest.modelProvider ?? remote?.modelProvider,
4197
+ model: latest.model ?? remote?.model,
4198
+ reasoningEffort: latest.reasoningEffort ?? remote?.reasoningEffort,
4199
+ permissionMode: latest.permissionMode ?? remote?.permissionMode ?? "auto",
4200
+ lastTurnId: latest.lastTurnId ?? remote?.lastTurnId,
4201
+ lastTurnStatus: mapRemoteTurnStatus(latest.lastTurnStatus ?? remote?.lastTurnStatus),
4202
+ });
4203
+ task = this.store.getSession(localSessionId)!;
4204
+ }
4205
+ this.store.setCurrentSession(group.contextKey, task.localSessionId);
4206
+ // A live Turn keeps its original delivery route. The new group's next Turn
4207
+ // establishes its own route through startPendingTurn.
4208
+ if (task.status !== "running" && !runtime.getSession(task.localSessionId)?.activeTurnId) {
4209
+ this.outbound.registerSession(task.localSessionId, group.contextKey, task.title, task.cwd, this.agentLabel(agentName), task.model, task.modelProvider);
4210
+ }
4211
+ this.store.audit(sourceContextKey, "session_switch_group", {
4212
+ localSessionId: task.localSessionId, remoteSessionId, groupContextKey: group.contextKey,
4213
+ });
4214
+ } catch (error) {
4215
+ await this.outbound.sendText(group.contextKey,
4216
+ `群已创建,但绑定任务失败:${runtimeErrorMessage(error)}。可使用 /sessions 重新选择任务。`)
4217
+ .catch((noticeError: unknown) => this.logger.warn({ error: noticeError, contextKey: group.contextKey }, "Failed to report SwitchGroup binding failure."));
4218
+ throw new Error(`群 ${group.name} 已创建,但绑定任务失败:${runtimeErrorMessage(error)}`);
4219
+ }
4220
+ await this.outbound.sendText(group.contextKey, [
4221
+ "已切换到已有任务,历史与原会话共享。",
4222
+ `当前任务:${task.title ?? title}(${remoteSessionId})`,
4223
+ `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
4224
+ `当前 Provider:${task.modelProvider ?? "Agent 默认"}`,
4225
+ `当前模型:${task.model ?? "默认"}`,
4226
+ `思考强度:${task.reasoningEffort ?? "自动"}`,
4227
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
4228
+ ].join("\n"));
4229
+ await this.outbound.sendText(sourceContextKey, `已创建飞书群:${group.name},并切换到任务 ${task.title ?? remoteSessionId}。原会话不变。`);
4230
+ } finally {
4231
+ this.switchGroupOperations.delete(operationKey);
4232
+ }
4233
+ }
4234
+
4016
4235
  private async createFeishuGroupWithTask(
4017
4236
  sourceContextKey: string,
4018
4237
  agentName: string,
@@ -4275,7 +4494,7 @@ export class ProxySessionController {
4275
4494
  taskTitle: string,
4276
4495
  userId: string | undefined,
4277
4496
  boundProjectCwd: string | undefined,
4278
- commandName: "/newgroup" | "/forkgroup" | "/clonegroup",
4497
+ commandName: "/newgroup" | "/forkgroup" | "/clonegroup" | "SwitchGroup",
4279
4498
  ): Promise<CreatedFeishuGroupContext> {
4280
4499
  if (!userId?.startsWith("ou_")) {
4281
4500
  throw new Error(`${commandName} 只能由具有 open_id 的飞书用户消息触发。`);
@@ -4323,6 +4542,7 @@ export class ProxySessionController {
4323
4542
  record.cwd,
4324
4543
  this.agentLabel(record.agentName),
4325
4544
  record.model,
4545
+ record.modelProvider,
4326
4546
  );
4327
4547
  const loading = (async (): Promise<LoadedSession> => {
4328
4548
  if (record.lastTurnId) {
@@ -5065,7 +5285,8 @@ export class ProxySessionController {
5065
5285
  forked.title ?? current.title,
5066
5286
  current.cwd,
5067
5287
  this.agentLabel(current.agentName),
5068
- current.model,
5288
+ forked.model ?? current.model,
5289
+ forked.modelProvider ?? current.modelProvider,
5069
5290
  );
5070
5291
  this.store.audit(contextKey, "session_reset_to_turn", {
5071
5292
  localSessionId: current.localSessionId,
@@ -5353,7 +5574,7 @@ export class ProxySessionController {
5353
5574
  }
5354
5575
  };
5355
5576
  const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
5356
- this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
5577
+ this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
5357
5578
  return session;
5358
5579
  }
5359
5580
 
@@ -5424,7 +5645,7 @@ export class ProxySessionController {
5424
5645
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
5425
5646
  }
5426
5647
  this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
5427
- this.outbound.updateSessionModel(loaded.record.localSessionId, model);
5648
+ this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
5428
5649
  await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
5429
5650
  const effortMessage = nextEffort && nextEffort !== currentEffort
5430
5651
  ? `,思考强度已自动调整为 ${nextEffort}`
@@ -5936,6 +6157,78 @@ export class ProxySessionController {
5936
6157
  };
5937
6158
  }
5938
6159
 
6160
+ async loadTurnDetails(turnId: string): Promise<TurnViewState | undefined> {
6161
+ const existing = this.turnDetailHydrations.get(turnId);
6162
+ if (existing) return existing;
6163
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
6164
+ if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(turnId)) return this.store.previews.load(turnId);
6165
+ if (snapshot && !needsHistoricalDetails(snapshot)) return snapshot;
6166
+ const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
6167
+ if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn)) return undefined;
6168
+ const hydration = snapshot ? this.hydrateTurnDetails(snapshot) : this.loadUnrecordedTurnPreview(origin!);
6169
+ this.turnDetailHydrations.set(turnId, hydration);
6170
+ try {
6171
+ return await hydration;
6172
+ } finally {
6173
+ if (this.turnDetailHydrations.get(turnId) === hydration) this.turnDetailHydrations.delete(turnId);
6174
+ }
6175
+ }
6176
+
6177
+ private async loadUnrecordedTurnPreview(origin: TurnRuntimeOriginRecord): Promise<TurnViewState> {
6178
+ const runtime = this.runtimes.forAgent(origin.agentName);
6179
+ if (!runtime.readRemoteTurn) throw new Error("此 Agent 不支持读取历史执行详情。");
6180
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, origin.turnId);
6181
+ if (details.turnId !== origin.turnId) throw new Error("Agent 返回的轮次与请求不一致,请重试。");
6182
+ let remote: RemoteSessionSummary | undefined;
6183
+ try {
6184
+ remote = await runtime.readRemoteSession?.(origin.remoteSessionId, "metadata");
6185
+ } catch (error) {
6186
+ this.logger.warn({ error, turnId: origin.turnId }, "Failed to read Preview task metadata; retaining full Turn details.");
6187
+ }
6188
+ // An Agent Bot turn may have arrived while the read-only request was in flight.
6189
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
6190
+ if (latest) return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
6191
+ const state = hydrateTurnViewState(createTurnViewState(
6192
+ origin.localSessionId, origin.turnId, details.startedAt ?? 0,
6193
+ remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName),
6194
+ ), details);
6195
+ // Viewing external work must not create a task, a chat binding or a pending final reply.
6196
+ this.store.previews.seed(state);
6197
+ this.store.previews.release(state.turnId);
6198
+ return state;
6199
+ }
6200
+
6201
+ private async hydrateTurnDetails(snapshot: TurnViewState): Promise<TurnViewState | undefined> {
6202
+ let updated: TurnViewState;
6203
+ try {
6204
+ const origin = this.store.getTurnRuntimeOrigin(snapshot.turnId);
6205
+ if (!origin) throw new Error("未保存本轮的原始 Agent 会话信息,无法读取历史执行详情。");
6206
+ const runtime = this.runtimes.forAgent(origin.agentName);
6207
+ if (!runtime.readRemoteTurn) throw new Error("此 Agent 不支持读取历史执行详情。");
6208
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, snapshot.turnId);
6209
+ if (details.turnId !== snapshot.turnId) throw new Error("Agent 返回的轮次与请求不一致,请重试。");
6210
+ const session = this.store.getSession(snapshot.sessionId);
6211
+ updated = hydrateTurnViewState({
6212
+ ...snapshot,
6213
+ projectCwd: snapshot.projectCwd ?? session?.cwd,
6214
+ agentLabel: snapshot.agentLabel ?? this.agentLabel(origin.agentName),
6215
+ }, details);
6216
+ } catch (error) {
6217
+ const message = error instanceof Error ? error.message : String(error);
6218
+ this.logger.warn({ error, turnId: snapshot.turnId }, "Failed to load historical Turn details.");
6219
+ updated = { ...snapshot, historyDetail: "summary", historyDetailError: message };
6220
+ }
6221
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(snapshot.turnId));
6222
+ if (!latest || !needsHistoricalDetails(latest)) return latest;
6223
+ if (updated.historyDetail === "full") {
6224
+ this.store.previews.seed(updated);
6225
+ this.store.previews.flush(updated.turnId, true);
6226
+ updated = { ...updated, previewJournal: true };
6227
+ }
6228
+ this.store.saveTurnSnapshot(snapshot.turnId, snapshot.sessionId, updated);
6229
+ return updated;
6230
+ }
6231
+
5939
6232
  private async openTurnDetails(contextKey: string, reference: string): Promise<void> {
5940
6233
  const current = this.requireCurrentSession(contextKey);
5941
6234
  let turnId: string | undefined;
@@ -5974,6 +6267,7 @@ export class ProxySessionController {
5974
6267
  ));
5975
6268
  return;
5976
6269
  }
6270
+ await this.loadTurnDetails(turnId);
5977
6271
  await this.outbound.showDetails(contextKey, turnId);
5978
6272
  }
5979
6273
 
@@ -5981,7 +6275,18 @@ export class ProxySessionController {
5981
6275
  contextKey: string,
5982
6276
  options: ResetHistoryCardOptions = {},
5983
6277
  ): Promise<void> {
5984
- const current = this.requireCurrentSession(contextKey);
6278
+ const taskId = options.sessionReference
6279
+ ? this.resolveSessionReference(contextKey, options.sessionReference)
6280
+ : undefined;
6281
+ const current = taskId
6282
+ ? this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId)
6283
+ : this.requireCurrentSession(contextKey);
6284
+ if (!current && taskId) {
6285
+ await this.openRemoteTurnHistory(contextKey, taskId, options);
6286
+ return;
6287
+ }
6288
+ if (!current || current.status === "closed") throw new Error(`找不到任务:${taskId}`);
6289
+ const readOnly = this.currentSession(contextKey)?.localSessionId !== current.localSessionId;
5985
6290
  if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
5986
6291
  throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
5987
6292
  }
@@ -6041,7 +6346,7 @@ export class ProxySessionController {
6041
6346
  current: isCurrent,
6042
6347
  running: isRunning,
6043
6348
  resetting: isResetting,
6044
- actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
6349
+ actions: readOnly || isCurrent || isRunning || resettingTurnId ? undefined : [{
6045
6350
  text: "Reset",
6046
6351
  value: {
6047
6352
  action: "turn_reset",
@@ -6058,8 +6363,8 @@ export class ProxySessionController {
6058
6363
  ...(page > 0 ? [{
6059
6364
  text: "Previous",
6060
6365
  value: {
6061
- action: "turn_reset_page",
6062
- sessionId: current.localSessionId,
6366
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
6367
+ sessionId: options.sessionReference ?? current.localSessionId,
6063
6368
  contextKey,
6064
6369
  page: String(page - 1),
6065
6370
  },
@@ -6067,14 +6372,15 @@ export class ProxySessionController {
6067
6372
  ...(page < totalPages - 1 ? [{
6068
6373
  text: "Next",
6069
6374
  value: {
6070
- action: "turn_reset_page",
6071
- sessionId: current.localSessionId,
6375
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
6376
+ sessionId: options.sessionReference ?? current.localSessionId,
6072
6377
  contextKey,
6073
6378
  page: String(page + 1),
6074
6379
  },
6075
6380
  }] : []),
6076
6381
  ];
6077
6382
  const card = this.cardRenderer.renderResetHistoryCard({
6383
+ ...(options.sessionReference ? { taskTitle: current.title ?? current.remoteSessionId, readOnly } : {}),
6078
6384
  entries,
6079
6385
  footerLines: [
6080
6386
  hasMoreHistory
@@ -6093,6 +6399,78 @@ export class ProxySessionController {
6093
6399
  }
6094
6400
  }
6095
6401
 
6402
+ private async openRemoteTurnHistory(
6403
+ contextKey: string,
6404
+ reference: string,
6405
+ options: ResetHistoryCardOptions,
6406
+ ): Promise<void> {
6407
+ const { runtime, remote } = await this.resolveRemoteCodexSession(reference);
6408
+ if (!runtime.listRemoteTurnSummaries) throw new Error("当前 Agent 不支持读取历史轮次。");
6409
+ const page = Math.max(0, Math.trunc(options.page ?? 0));
6410
+ const running = remote.lastTurnStatus === "inProgress" && remote.lastTurnId;
6411
+ let cursor = options.cursor;
6412
+ // Next carries its cursor; only backwards navigation needs to locate a page again.
6413
+ if (!cursor && page > 0) {
6414
+ const seen = new Set<string>();
6415
+ for (let index = 0; index < page; index += 1) {
6416
+ const previous = await runtime.listRemoteTurnSummaries(remote.id, {
6417
+ cursor,
6418
+ limit: RESET_HISTORY_PAGE_SIZE - (index === 0 && running ? 1 : 0),
6419
+ });
6420
+ if (!previous.nextCursor || seen.has(previous.nextCursor)) {
6421
+ throw new Error("任务轮次已变化,请从 /sessions 重新打开 Turns。");
6422
+ }
6423
+ cursor = previous.nextCursor;
6424
+ seen.add(cursor);
6425
+ }
6426
+ }
6427
+ const result = await runtime.listRemoteTurnSummaries(remote.id, {
6428
+ cursor,
6429
+ limit: RESET_HISTORY_PAGE_SIZE - (page === 0 && running ? 1 : 0),
6430
+ });
6431
+ if (result.nextCursor && result.nextCursor === cursor) {
6432
+ throw new Error("历史轮次分页游标无效,请稍后重试。");
6433
+ }
6434
+ const turns = [
6435
+ ...(page === 0 && running ? [{ id: running, prompt: remote.lastUserPrompt }] : []),
6436
+ ...result.turns,
6437
+ ];
6438
+ const entries: ResetHistoryCardEntry[] = turns.map((turn, index) => ({
6439
+ sequence: page * RESET_HISTORY_PAGE_SIZE + index + 1,
6440
+ graphNodeLine: "●",
6441
+ graphConnectorLine: index < turns.length - 1 ? "│" : undefined,
6442
+ lines: [cardText(truncateText((turn.prompt ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100))],
6443
+ timestamp: formatResetTurnTime(turn.completedAt ?? turn.startedAt),
6444
+ current: remote.lastTurnStatus === "completed" && remote.lastTurnId === turn.id,
6445
+ running: running === turn.id,
6446
+ }));
6447
+ const pageAction = (text: string, targetPage: number, targetCursor?: string): TaskListCardAction => ({
6448
+ text,
6449
+ value: {
6450
+ action: "session_turns_page",
6451
+ sessionId: reference,
6452
+ contextKey,
6453
+ page: String(targetPage),
6454
+ ...(targetCursor ? { cursor: targetCursor } : {}),
6455
+ },
6456
+ });
6457
+ const card = this.cardRenderer.renderResetHistoryCard({
6458
+ taskTitle: remote.title ?? remote.preview ?? remote.id,
6459
+ readOnly: true,
6460
+ entries,
6461
+ footerLines: [`第 ${page + 1} 页 · 本页 ${entries.length} 个 turn`],
6462
+ pageActions: [
6463
+ ...(page > 0 ? [pageAction("Previous", page - 1)] : []),
6464
+ ...(result.nextCursor ? [pageAction("Next", page + 1, result.nextCursor)] : []),
6465
+ ],
6466
+ });
6467
+ if (options.updateMessageId) {
6468
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
6469
+ } else {
6470
+ await this.outbound.sendInteractiveCard(contextKey, card);
6471
+ }
6472
+ }
6473
+
6096
6474
  private async openDirectoryBrowser(
6097
6475
  contextKey: string,
6098
6476
  requestedDirectory?: string,
@@ -6388,6 +6766,16 @@ export class ProxySessionController {
6388
6766
  contextKey,
6389
6767
  },
6390
6768
  })];
6769
+ actions.push(bindCardAction({
6770
+ text: "SwitchGroup",
6771
+ value: {
6772
+ action: "session_switch_group",
6773
+ sessionId: entry.reference,
6774
+ ...(searchTerm ? { searchTerm } : {}),
6775
+ page: String(page),
6776
+ contextKey,
6777
+ },
6778
+ }));
6391
6779
  const projectActions: TaskListCardAction[] = [bindCardAction({
6392
6780
  text: "New",
6393
6781
  value: {
@@ -6435,6 +6823,14 @@ export class ProxySessionController {
6435
6823
  contextKey,
6436
6824
  },
6437
6825
  }));
6826
+ actions.push(bindCardAction({
6827
+ text: "Turns",
6828
+ value: {
6829
+ action: "session_turns",
6830
+ sessionId: entry.reference,
6831
+ contextKey,
6832
+ },
6833
+ }));
6438
6834
  actions.push(bindCardAction({
6439
6835
  text: "Archive",
6440
6836
  value: {
@@ -6652,6 +7048,7 @@ export class ProxySessionController {
6652
7048
  existing.cwd,
6653
7049
  this.agentLabel(existing.agentName),
6654
7050
  existing.model,
7051
+ existing.modelProvider,
6655
7052
  );
6656
7053
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
6657
7054
  return;
@@ -6686,6 +7083,7 @@ export class ProxySessionController {
6686
7083
  remote.cwd,
6687
7084
  this.agentLabel(agentName),
6688
7085
  remote.model,
7086
+ remote.modelProvider,
6689
7087
  );
6690
7088
  await this.outbound.sendText(
6691
7089
  contextKey,
@@ -7116,12 +7514,30 @@ export class ProxySessionController {
7116
7514
  const actions: TaskListCardAction[] = [
7117
7515
  statusRefreshAction(taskId, contextKey),
7118
7516
  ...taskActions,
7517
+ ...this.statusPreviewActions(contextKey, activeTurnId ?? current?.lastTurnId, current?.remoteSessionId ? {
7518
+ agentName: current.agentName, remoteSessionId: current.remoteSessionId, localSessionId: current.localSessionId,
7519
+ } : undefined),
7119
7520
  ];
7120
7521
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
7121
7522
  if (options.updateMessageId) await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
7122
7523
  else await this.outbound.sendInteractiveCard(contextKey, card);
7123
7524
  }
7124
7525
 
7526
+ private statusPreviewActions(
7527
+ contextKey: string,
7528
+ turnId?: string,
7529
+ source?: { agentName: string; remoteSessionId: string; localSessionId?: string },
7530
+ ): TaskListCardAction[] {
7531
+ if (!turnId || turnId.startsWith("pending_")) return [];
7532
+ if (source && this.runtimes.forAgent(source.agentName).readRemoteTurn) {
7533
+ this.store.saveTurnRuntimeOrigin(turnId,
7534
+ source.localSessionId ?? remoteSessionReference(source.agentName, source.remoteSessionId),
7535
+ source.agentName, source.remoteSessionId);
7536
+ }
7537
+ const url = this.outbound.getTurnPreviewUrl(contextKey, turnId);
7538
+ return url ? [{ text: "Preview", value: {}, url }] : [];
7539
+ }
7540
+
7125
7541
  private async statusForCodexTask(
7126
7542
  contextKey: string,
7127
7543
  reference: string,
@@ -7178,6 +7594,7 @@ export class ProxySessionController {
7178
7594
  actionReference,
7179
7595
  contextKey,
7180
7596
  ),
7597
+ ...this.statusPreviewActions(contextKey, remote.lastTurnId, { agentName, remoteSessionId: remote.id }),
7181
7598
  ];
7182
7599
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
7183
7600
  if (options.updateMessageId) await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
@@ -7267,7 +7684,7 @@ export class ProxySessionController {
7267
7684
  await this.outbound.sendText(
7268
7685
  contextKey,
7269
7686
  enabled
7270
- ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;发送 @机器人 /mute off 可恢复自动响应。"
7687
+ ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;普通提问会补充当前群/话题近期文字和图片(最多 20 条、6 张图片)。发送 @机器人 /mute off 可恢复自动响应并关闭自动补充。"
7271
7688
  : "已关闭当前群的静音模式。机器人将恢复自动响应群消息。",
7272
7689
  );
7273
7690
  }
@@ -8202,6 +8619,17 @@ function isUnmaterializedCodexThreadError(error: unknown): boolean {
8202
8619
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
8203
8620
  }
8204
8621
 
8622
+ function needsHistoricalDetails(state: TurnViewState): boolean {
8623
+ if (!isTerminalTurnViewStatus(state.status) || state.historyDetail === "full") return false;
8624
+ if (state.historyDetail === "summary") return true;
8625
+ return state.status === "completed" && state.finalResponse === undefined && !state.assistantText
8626
+ && !state.error && !state.progressText && !state.approval && !state.activeTool
8627
+ && !state.activities?.length && !state.plan?.length && !state.reasoningItems?.length
8628
+ && !state.completedTools?.length && !state.failedTools?.length && !state.fileSummary?.length
8629
+ && !state.totalToolCount && !state.totalTokens
8630
+ && state.fullToolOutputs === undefined && state.fullToolErrors === undefined && state.toolStatuses === undefined;
8631
+ }
8632
+
8205
8633
  function turnViewSnapshot(value: unknown): TurnViewState | undefined {
8206
8634
  if (!value || typeof value !== "object") return undefined;
8207
8635
  const candidate = value as Partial<TurnViewState>;
@@ -8528,7 +8956,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
8528
8956
  }
8529
8957
 
8530
8958
  function isQueueIndependentCommand(command: Command): boolean {
8531
- if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
8959
+ if (["archive", "dismiss", "stop", "status", "restart", "update", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
8532
8960
  if (command.type === "agent") return command.agent === undefined;
8533
8961
  if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
8534
8962
  return false;