@keyou007/agent-bot 0.1.24-alpha.1 → 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 (99) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +33 -7
  3. package/README.zh.md +32 -7
  4. package/dist/acp/AcpJsonRpcConnection.js +3 -1
  5. package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
  6. package/dist/cli/SelfUpdateRunner.js +2 -0
  7. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  8. package/dist/cli/SelfUpdater.js +2 -0
  9. package/dist/cli/SelfUpdater.js.map +1 -1
  10. package/dist/codex/AppServerConnection.js +3 -1
  11. package/dist/codex/AppServerConnection.js.map +1 -1
  12. package/dist/codex/CodexEventMapper.js +41 -3
  13. package/dist/codex/CodexEventMapper.js.map +1 -1
  14. package/dist/codex/CodexRuntime.js +95 -3
  15. package/dist/codex/CodexRuntime.js.map +1 -1
  16. package/dist/commands/CommandRouter.js +5 -0
  17. package/dist/commands/CommandRouter.js.map +1 -1
  18. package/dist/feishu/CardRenderer.js +49 -23
  19. package/dist/feishu/CardRenderer.js.map +1 -1
  20. package/dist/feishu/FeishuConnector.js +2 -0
  21. package/dist/feishu/FeishuConnector.js.map +1 -1
  22. package/dist/feishu/FeishuMarkdown.js +70 -4
  23. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  24. package/dist/feishu/FeishuMessageClient.js +44 -0
  25. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  26. package/dist/feishu/FeishuTurnPresenter.js +114 -37
  27. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  28. package/dist/feishu/RecentMessageContext.js +70 -0
  29. package/dist/feishu/RecentMessageContext.js.map +1 -0
  30. package/dist/index.js +62 -24
  31. package/dist/index.js.map +1 -1
  32. package/dist/local-files/DiagramPreview.js +171 -0
  33. package/dist/local-files/DiagramPreview.js.map +1 -0
  34. package/dist/local-files/LocalFileViewerServer.js +141 -3
  35. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  36. package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
  37. package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
  38. package/dist/local-files/TurnPreviewPage.js +499 -60
  39. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  40. package/dist/presentation/OutboundRouter.js +18 -9
  41. package/dist/presentation/OutboundRouter.js.map +1 -1
  42. package/dist/presentation/TurnStateReducer.js +157 -8
  43. package/dist/presentation/TurnStateReducer.js.map +1 -1
  44. package/dist/presentation/turnReasoning.js +23 -0
  45. package/dist/presentation/turnReasoning.js.map +1 -0
  46. package/dist/proxy/ProxySessionController.js +461 -49
  47. package/dist/proxy/ProxySessionController.js.map +1 -1
  48. package/dist/state/StateStore.js +43 -2
  49. package/dist/state/StateStore.js.map +1 -1
  50. package/dist/state/TurnPreviewJournal.js +469 -0
  51. package/dist/state/TurnPreviewJournal.js.map +1 -0
  52. package/dist/state/migrations.js +10 -0
  53. package/dist/state/migrations.js.map +1 -1
  54. package/dist/updates/AutomaticUpdatePreparer.js +16 -7
  55. package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
  56. package/dist/updates/ManualUpdateController.js +102 -0
  57. package/dist/updates/ManualUpdateController.js.map +1 -0
  58. package/dist/updates/PublishedRelease.js +42 -4
  59. package/dist/updates/PublishedRelease.js.map +1 -1
  60. package/dist/utils/ProtocolLineWarning.js +15 -0
  61. package/dist/utils/ProtocolLineWarning.js.map +1 -0
  62. package/docs/technical-reference.md +21 -1
  63. package/docs/technical-reference.zh.md +21 -1
  64. package/npm-shrinkwrap.json +1401 -10
  65. package/package.json +3 -1
  66. package/skills/agent-bot/SKILL.md +9 -2
  67. package/src/acp/AcpJsonRpcConnection.ts +3 -1
  68. package/src/cli/SelfUpdateRunner.ts +3 -0
  69. package/src/cli/SelfUpdater.ts +2 -0
  70. package/src/codex/AppServerConnection.ts +3 -1
  71. package/src/codex/CodexEventMapper.ts +40 -3
  72. package/src/codex/CodexRuntime.ts +91 -4
  73. package/src/commands/CommandRouter.ts +4 -0
  74. package/src/commands/commandTypes.ts +1 -0
  75. package/src/feishu/CardRenderer.ts +47 -21
  76. package/src/feishu/FeishuConnector.ts +2 -0
  77. package/src/feishu/FeishuMarkdown.ts +69 -3
  78. package/src/feishu/FeishuMessageClient.ts +44 -0
  79. package/src/feishu/FeishuTurnPresenter.ts +107 -31
  80. package/src/feishu/RecentMessageContext.ts +90 -0
  81. package/src/feishu/types.ts +5 -0
  82. package/src/index.ts +62 -22
  83. package/src/local-files/DiagramPreview.ts +173 -0
  84. package/src/local-files/LocalFileViewerServer.ts +129 -2
  85. package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
  86. package/src/local-files/TurnPreviewPage.ts +539 -57
  87. package/src/presentation/OutboundRouter.ts +25 -7
  88. package/src/presentation/TurnStateReducer.ts +153 -9
  89. package/src/presentation/turnReasoning.ts +21 -0
  90. package/src/presentation/turnViewTypes.ts +19 -1
  91. package/src/proxy/ProxySessionController.ts +453 -40
  92. package/src/runtime/types.ts +21 -0
  93. package/src/state/StateStore.ts +45 -2
  94. package/src/state/TurnPreviewJournal.ts +398 -0
  95. package/src/state/migrations.ts +10 -0
  96. package/src/updates/AutomaticUpdatePreparer.ts +21 -7
  97. package/src/updates/ManualUpdateController.ts +116 -0
  98. package/src/updates/PublishedRelease.ts +49 -3
  99. package/src/utils/ProtocolLineWarning.ts +13 -0
@@ -15,9 +15,11 @@ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
15
15
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
16
16
  import { formatGroupNameDate, formatNewGroupName, parseTaskNameFromGroupName, } from "../feishu/GroupNameFormatter.js";
17
17
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
18
+ import { RECENT_CONTEXT_LIMIT, RECENT_CONTEXT_IMAGE_LIMIT, RECENT_CONTEXT_TEXT_LIMIT, recentContextPrompt } from "../feishu/RecentMessageContext.js";
18
19
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
19
20
  import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
20
21
  import { errorLogValue } from "../logging/errorLogValue.js";
22
+ import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
21
23
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
22
24
  import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
23
25
  import { createId } from "../utils/id.js";
@@ -62,6 +64,7 @@ const FORK_GROUP_STARTED_MESSAGE = "正在创建分支群聊,请稍后。";
62
64
  const LIST_SESSIONS_STARTED_MESSAGE = "正在读取任务列表,请稍后。";
63
65
  const LIST_TURNS_STARTED_MESSAGE = "正在读取历史轮次,请稍后。";
64
66
  const SWITCH_TASK_STARTED_MESSAGE = "正在切换任务,请稍后。";
67
+ const SWITCH_GROUP_STARTED_MESSAGE = "正在创建群聊并绑定所选任务,请稍后。";
65
68
  const STATUS_STARTED_MESSAGE = "正在读取任务状态,请稍后。";
66
69
  const GOAL_STARTED_MESSAGE = "正在处理 Goal,请稍后。";
67
70
  const ARCHIVE_TASK_STARTED_MESSAGE = "正在归档任务,请稍后。";
@@ -190,6 +193,7 @@ const HELP_COMMAND_SECTIONS = [
190
193
  usage: "[--force]",
191
194
  description: "默认安全重启;--force 立即重启",
192
195
  },
196
+ { command: "/update", description: "查看正式版和 Alpha 版更新,选择版本安全更新" },
193
197
  {
194
198
  command: "/release",
195
199
  description: "释放 Agent Bot 占用的 App Server 任务",
@@ -220,6 +224,7 @@ export class ProxySessionController {
220
224
  router = new CommandRouter();
221
225
  cardRenderer = new CardRenderer();
222
226
  messageQueues = new Map();
227
+ switchGroupOperations = new Set();
223
228
  sessionLoads = new Map();
224
229
  queuedPromptStarts = new Map();
225
230
  queuedPromptCards = new Map();
@@ -228,6 +233,7 @@ export class ProxySessionController {
228
233
  resetHistoryOperations = new Map();
229
234
  remoteTurnHistoryRefreshes = new Map();
230
235
  turnHistoryHydrations = new Map();
236
+ turnDetailHydrations = new Map();
231
237
  forkHistoryCursors = new Map();
232
238
  lastSessionListings = new Map();
233
239
  threadInitializations = new Map();
@@ -536,15 +542,24 @@ export class ProxySessionController {
536
542
  let executableImagePaths = localImagePaths;
537
543
  let executableMessage = message;
538
544
  let injectedRootMessageId;
545
+ let recentContextIds = [];
539
546
  if (command.type === "prompt") {
540
547
  await this.ensureThreadFork(message);
541
548
  const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
542
- executableCommand = { ...command, text: prepared.text };
543
- executableImagePaths = prepared.localImagePaths;
549
+ const recent = await this.prepareMutedGroupContext(message, prepared.text, prepared.localImagePaths);
550
+ executableCommand = { ...command, text: recent.text };
551
+ executableImagePaths = recent.localImagePaths;
552
+ recentContextIds = recent.messageIds;
544
553
  executableMessage = { ...message, displayText: prepared.displayPrompt };
545
554
  injectedRootMessageId = prepared.injectedRootMessageId;
546
555
  }
547
556
  await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
557
+ if (command.type === "prompt" && message.chatType === "group" && message.mentionedBot
558
+ && this.store.chatRequiresMention(baseChatContextKey(message.contextKey))) {
559
+ const current = this.currentSession(message.contextKey);
560
+ if (current)
561
+ this.store.rememberRecentContextMessages(current.localSessionId, current.remoteSessionId ?? "", message.contextKey, message.messageId, [...recentContextIds, message.messageId, ...[referencedMessageId, injectedRootMessageId].filter((id) => Boolean(id))]);
562
+ }
548
563
  if (injectedRootMessageId) {
549
564
  const current = this.currentSession(message.contextKey);
550
565
  if (current) {
@@ -609,11 +624,18 @@ export class ProxySessionController {
609
624
  throw new Error("自动更新功能未启用。");
610
625
  await this.lifecycle.cancelAutomaticUpdate(scopedAction);
611
626
  }
627
+ else if (kind === "agentbot_update_select") {
628
+ if (!this.lifecycle?.selectUpdate)
629
+ throw new Error("当前运行方式不支持在线更新。");
630
+ await this.lifecycle.selectUpdate(scopedAction, replyTarget);
631
+ }
612
632
  else if (kind === "help_command") {
613
633
  await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
614
634
  }
615
635
  else if (kind === "turn_details") {
616
- await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
636
+ const turnId = String(scopedAction.value.turnId ?? "");
637
+ await this.loadTurnDetails(turnId);
638
+ await this.outbound.showDetails(contextKey, turnId);
617
639
  }
618
640
  else if (kind === "activity_history") {
619
641
  const requestedPage = String(scopedAction.value.page ?? "0");
@@ -745,6 +767,10 @@ export class ProxySessionController {
745
767
  await this.forkSessionReference(contextKey, sessionId);
746
768
  await this.refreshSessionsCardFromAction(scopedAction, { page: 0 });
747
769
  }
770
+ else if (kind === "session_switch_group") {
771
+ await this.switchSessionReferenceToFeishuGroup(contextKey, String(scopedAction.value.sessionId ?? ""), scopedAction.userId);
772
+ await this.refreshSessionsCardFromAction(scopedAction);
773
+ }
748
774
  else if (kind === "session_fork_group") {
749
775
  const sessionId = String(scopedAction.value.sessionId ?? "");
750
776
  await this.forkSessionReferenceToFeishuGroup(contextKey, sessionId, scopedAction.userId);
@@ -789,6 +815,21 @@ export class ProxySessionController {
789
815
  await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
790
816
  }
791
817
  }
818
+ else if (kind === "session_turns" || kind === "session_turns_page") {
819
+ const sessionReference = String(scopedAction.value.sessionId ?? "").trim();
820
+ if (!sessionReference)
821
+ throw new Error("任务轮次卡片无效,请重新发送 /sessions。");
822
+ if (kind === "session_turns")
823
+ await this.outbound.sendText(contextKey, LIST_TURNS_STARTED_MESSAGE);
824
+ await this.openResetHistory(contextKey, {
825
+ sessionReference,
826
+ ...(kind === "session_turns_page" ? {
827
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
828
+ page: resetHistoryPageValue(scopedAction.value.page),
829
+ cursor: typeof scopedAction.value.cursor === "string" ? scopedAction.value.cursor : undefined,
830
+ } : {}),
831
+ });
832
+ }
792
833
  else if (kind === "session_status") {
793
834
  await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
794
835
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
@@ -1054,7 +1095,7 @@ export class ProxySessionController {
1054
1095
  ?? { ...record, contextKey: responseContextKey };
1055
1096
  const previous = this.messageQueues.get(responseContextKey) ?? Promise.resolve();
1056
1097
  const next = previous.catch(() => undefined).then(async () => {
1057
- this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName), scopedRecord.model);
1098
+ this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName), scopedRecord.model, scopedRecord.modelProvider);
1058
1099
  const promptMessageId = await this.outbound.withReplyTarget(responseContextKey, existingReplyTarget, () => this.outbound.sendText(responseContextKey, promptText));
1059
1100
  const turnReplyTarget = isThreadContextKey(responseContextKey)
1060
1101
  ? promptMessageId
@@ -1256,7 +1297,7 @@ export class ProxySessionController {
1256
1297
  await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
1257
1298
  }
1258
1299
  this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
1259
- this.outbound.updateSessionModel(record.localSessionId, nextValue);
1300
+ this.outbound.updateSessionModel(record.localSessionId, nextValue, loaded.session.modelProvider);
1260
1301
  }
1261
1302
  else if (setting === "thinking" && nextValue) {
1262
1303
  const models = await loaded.runtime.listModels(loaded.session.modelProvider, loaded.session.model);
@@ -1580,6 +1621,11 @@ export class ProxySessionController {
1580
1621
  throw new Error("当前运行方式不支持自动重启。");
1581
1622
  await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
1582
1623
  return;
1624
+ case "update":
1625
+ if (!this.lifecycle?.showUpdates)
1626
+ throw new Error("当前运行方式不支持检查更新。");
1627
+ await this.lifecycle.showUpdates(contextKey, userId);
1628
+ return;
1583
1629
  case "release":
1584
1630
  await this.requestAppServerRelease(contextKey);
1585
1631
  return;
@@ -1600,7 +1646,7 @@ export class ProxySessionController {
1600
1646
  await this.openExecutionSettings(contextKey, "model");
1601
1647
  return;
1602
1648
  case "provider":
1603
- await this.openProviderSettings(contextKey);
1649
+ await this.openExecutionSettings(contextKey, "provider");
1604
1650
  return;
1605
1651
  case "thinking":
1606
1652
  await this.openExecutionSettings(contextKey, "thinking");
@@ -1628,7 +1674,7 @@ export class ProxySessionController {
1628
1674
  : context.contextKey;
1629
1675
  if (!this.outbound.canRoute(turnContextKey))
1630
1676
  continue;
1631
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1677
+ this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model, session.modelProvider);
1632
1678
  if (!session.lastTurnId)
1633
1679
  continue;
1634
1680
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error) => {
@@ -1702,7 +1748,7 @@ export class ProxySessionController {
1702
1748
  continue;
1703
1749
  }
1704
1750
  if (session) {
1705
- this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1751
+ this.outbound.registerSession(session.localSessionId, attempt.contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model, session.modelProvider);
1706
1752
  }
1707
1753
  try {
1708
1754
  await this.handleRuntimeEvent({
@@ -1814,7 +1860,7 @@ export class ProxySessionController {
1814
1860
  const replyTarget = attempt.replyMessageId
1815
1861
  ? { messageId: attempt.replyMessageId, replyInThread: true }
1816
1862
  : undefined;
1817
- this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model);
1863
+ this.outbound.registerSession(session.localSessionId, contextKey, session.title, session.cwd, this.agentLabel(session.agentName), session.model, session.modelProvider);
1818
1864
  if (announce) {
1819
1865
  await this.outbound.withReplyTarget(contextKey, replyTarget, () => this.outbound.sendText(contextKey, "检测到任务在 Agent Bot 重启前尚未完成,正在自动恢复。")).catch((error) => {
1820
1866
  this.logger.warn({ error, attemptId: attempt.attemptId, contextKey }, "Failed to send the task recovery notification.");
@@ -2016,8 +2062,8 @@ export class ProxySessionController {
2016
2062
  }
2017
2063
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
2018
2064
  if (!configuredRuntime.getSession(record.localSessionId)) {
2019
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
2020
- await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text);
2065
+ this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model, record.modelProvider);
2066
+ await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text, localImagePaths);
2021
2067
  }
2022
2068
  let loaded;
2023
2069
  try {
@@ -2060,7 +2106,7 @@ export class ProxySessionController {
2060
2106
  }
2061
2107
  try {
2062
2108
  await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
2063
- await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
2109
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId, localImagePaths);
2064
2110
  if (messageId)
2065
2111
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
2066
2112
  return;
@@ -2083,7 +2129,7 @@ export class ProxySessionController {
2083
2129
  if (current.activeTurnId !== activeTurnId) {
2084
2130
  try {
2085
2131
  await loaded.runtime.steerTurn(record.localSessionId, current.activeTurnId, runtimePrompt(text, localImagePaths));
2086
- await this.presentSteerMessage(record.localSessionId, current.activeTurnId, displayPrompt ?? text, messageId);
2132
+ await this.presentSteerMessage(record.localSessionId, current.activeTurnId, displayPrompt ?? text, messageId, localImagePaths);
2087
2133
  if (messageId)
2088
2134
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
2089
2135
  return;
@@ -2105,10 +2151,10 @@ export class ProxySessionController {
2105
2151
  if (messageId)
2106
2152
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
2107
2153
  }
2108
- async presentSteerMessage(localSessionId, turnId, text, messageId) {
2154
+ async presentSteerMessage(localSessionId, turnId, text, messageId, localImagePaths) {
2109
2155
  this.store.touchTurnAttempt(turnId);
2110
2156
  try {
2111
- await this.outbound.appendSteerMessage(localSessionId, turnId, text, messageId);
2157
+ await this.outbound.appendSteerMessage(localSessionId, turnId, text, messageId, localImagePaths);
2112
2158
  }
2113
2159
  catch (error) {
2114
2160
  this.logger.warn({ error, sessionId: localSessionId, turnId, messageId }, "Failed to insert a steer message into the thinking card.");
@@ -2239,6 +2285,72 @@ export class ProxySessionController {
2239
2285
  }
2240
2286
  }
2241
2287
  }
2288
+ async prepareMutedGroupContext(message, text, localImagePaths) {
2289
+ if (message.chatType !== "group" || !message.chatId || message.mentionedBot !== true
2290
+ || !this.store.chatRequiresMention(baseChatContextKey(message.contextKey))) {
2291
+ return { text, localImagePaths, messageIds: [] };
2292
+ }
2293
+ if (message.threadContext && !message.threadId)
2294
+ throw new Error("无法识别当前话题,已停止补充上下文以避免读取其他话题。");
2295
+ try {
2296
+ const current = this.currentSession(message.contextKey);
2297
+ const stopIds = current ? new Set([
2298
+ ...this.store.recentContextMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey, false),
2299
+ ...this.store.recentSubmittedMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey),
2300
+ ]) : new Set();
2301
+ const messages = await this.outbound.readRecentMessages(message.contextKey, {
2302
+ chatId: message.chatId,
2303
+ ...(message.threadId ? { threadId: message.threadId } : {}),
2304
+ beforeMessageId: message.messageId,
2305
+ beforeTimestamp: message.createdAt,
2306
+ ...(stopIds.size ? { stopBeforeMessageIds: [...stopIds] } : {}),
2307
+ });
2308
+ const seen = current ? this.store.recentContextMessageIds(current.localSessionId, current.remoteSessionId ?? "", message.contextKey) : new Set();
2309
+ seen.add(message.messageId);
2310
+ if (message.parentMessageId)
2311
+ seen.add(message.parentMessageId);
2312
+ if (message.threadContext && message.rootMessageId)
2313
+ seen.add(message.rootMessageId);
2314
+ const selected = [];
2315
+ const imagePaths = [...(localImagePaths ?? [])];
2316
+ const imageKeys = new Set();
2317
+ let remaining = RECENT_CONTEXT_TEXT_LIMIT;
2318
+ let imageCount = 0;
2319
+ for (const item of messages.slice(-RECENT_CONTEXT_LIMIT).reverse()) {
2320
+ if (stopIds.has(item.messageId) || remaining <= 0)
2321
+ break;
2322
+ if (seen.has(item.messageId))
2323
+ continue;
2324
+ seen.add(item.messageId);
2325
+ const limit = Math.min(2_000, remaining);
2326
+ let body = item.text.length > limit ? `${item.text.slice(0, limit)}\n[消息文本已截断]` : item.text;
2327
+ remaining -= Math.min(item.text.length, limit);
2328
+ for (const [index, image] of item.images.entries()) {
2329
+ if (imageKeys.has(image.imageKey))
2330
+ continue;
2331
+ imageKeys.add(image.imageKey);
2332
+ if (imageCount >= RECENT_CONTEXT_IMAGE_LIMIT) {
2333
+ body += `\n[图片 ${index + 1} 未附带:已达到近期图片上限]`;
2334
+ continue;
2335
+ }
2336
+ const imagePath = await this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey);
2337
+ imagePaths.push(imagePath);
2338
+ imageCount++;
2339
+ body += `\n[图片 ${index + 1} 已附带:${imagePath}]`;
2340
+ }
2341
+ selected.unshift({ ...item, text: body });
2342
+ }
2343
+ return {
2344
+ text: recentContextPrompt(text, selected),
2345
+ ...(imagePaths.length ? { localImagePaths: [...new Set(imagePaths)] } : {}),
2346
+ messageIds: selected.map((item) => item.messageId),
2347
+ };
2348
+ }
2349
+ catch (error) {
2350
+ const detail = error instanceof Error ? error.message : String(error);
2351
+ throw new Error(`无法补充近期群聊上下文(包括图片):${detail}。请检查飞书历史消息/群消息读取权限及机器人是否在群内,或附图重试。`);
2352
+ }
2353
+ }
2242
2354
  async prepareTopicRootPrompt(message, text, localImagePaths) {
2243
2355
  const displayPrompt = message.displayText ?? text;
2244
2356
  const rootMessageId = topicRootMessageId(message);
@@ -2309,7 +2421,7 @@ export class ProxySessionController {
2309
2421
  permissionMode: source.permissionMode ?? "auto",
2310
2422
  });
2311
2423
  this.store.setCurrentSession(message.contextKey, localSessionId);
2312
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName), source.model);
2424
+ this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName), source.model, source.modelProvider);
2313
2425
  try {
2314
2426
  const forked = await runtime.forkSession({
2315
2427
  localSessionId,
@@ -2607,7 +2719,7 @@ export class ProxySessionController {
2607
2719
  lastTurnId: plan.lastTurnId,
2608
2720
  lastTurnStatus: plan.lastTurnStatus,
2609
2721
  });
2610
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName), plan.model);
2722
+ this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName), forked.model ?? plan.model, forked.modelProvider ?? plan.modelProvider);
2611
2723
  this.store.audit(contextKey, "session_forked", {
2612
2724
  sourceLocalSessionId: plan.source?.localSessionId,
2613
2725
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -2647,6 +2759,7 @@ export class ProxySessionController {
2647
2759
  sessionId: input.localSessionId,
2648
2760
  turnId: turn.id,
2649
2761
  prompt: turn.prompt ?? "未记录对话内容",
2762
+ historyDetail: "summary",
2650
2763
  status: "completed",
2651
2764
  startedAt,
2652
2765
  completedAt,
@@ -2963,8 +3076,9 @@ export class ProxySessionController {
2963
3076
  while (true) {
2964
3077
  let pendingTurnId;
2965
3078
  try {
2966
- this.outbound.updateSessionModel(loaded.record.localSessionId, loaded.runtime.getSession(loaded.record.localSessionId)?.model ?? loaded.session.model);
2967
- pendingTurnId = await this.outbound.startPendingTurn(loaded.record.localSessionId, loaded.record.contextKey, title, replyTarget, cardPrompt);
3079
+ const session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
3080
+ this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
3081
+ pendingTurnId = await this.outbound.startPendingTurn(loaded.record.localSessionId, loaded.record.contextKey, title, replyTarget, cardPrompt, localImagePaths);
2968
3082
  }
2969
3083
  catch (error) {
2970
3084
  this.store.updateTurnAttempt(attemptId, {
@@ -3056,7 +3170,7 @@ export class ProxySessionController {
3056
3170
  }
3057
3171
  if (activate)
3058
3172
  this.store.setCurrentSession(contextKey, localSessionId);
3059
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName), resolvedExecutionSettings.model);
3173
+ this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName), resolvedExecutionSettings.model, resolvedExecutionSettings.modelProvider);
3060
3174
  const runtime = this.runtimes.forAgent(agentName);
3061
3175
  try {
3062
3176
  if (prepareTurn) {
@@ -3073,7 +3187,7 @@ export class ProxySessionController {
3073
3187
  permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
3074
3188
  });
3075
3189
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3076
- this.outbound.updateSessionModel(localSessionId, session.model);
3190
+ this.outbound.updateSessionModel(localSessionId, session.model, session.modelProvider);
3077
3191
  const saved = this.store.getSession(localSessionId) ?? record;
3078
3192
  if (announce) {
3079
3193
  const task = initialTitle ? `${initialTitle}(${session.remoteSessionId})` : session.remoteSessionId;
@@ -3109,6 +3223,111 @@ export class ProxySessionController {
3109
3223
  const boundProjectCwd = detectProjectlessWorkspace(resolved.cwd) ? undefined : resolved.cwd;
3110
3224
  await this.createFeishuGroupWithTask(sourceContextKey, resolved.agentName, undefined, userId, boundProjectCwd, resolved.executionSettings);
3111
3225
  }
3226
+ async switchSessionReferenceToFeishuGroup(sourceContextKey, reference, userId) {
3227
+ if (!userId?.startsWith("ou_"))
3228
+ throw new Error("SwitchGroup 只能由具有 open_id 的飞书用户触发。");
3229
+ if (!reference.trim())
3230
+ throw new Error("任务无效,请重新发送 /sessions。");
3231
+ await this.outbound.sendText(sourceContextKey, SWITCH_GROUP_STARTED_MESSAGE);
3232
+ const taskId = this.resolveSessionReference(sourceContextKey, reference);
3233
+ let task = this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId);
3234
+ if (task?.status === "closed")
3235
+ throw new Error(`找不到任务:${taskId}`);
3236
+ if (task && (!this.isCodexSession(task) || !task.remoteSessionId)) {
3237
+ throw new Error("指定任务不是可切换的 App Server 任务。");
3238
+ }
3239
+ let remote;
3240
+ let agentName;
3241
+ let runtime;
3242
+ if (task) {
3243
+ agentName = task.agentName;
3244
+ runtime = this.runtimes.forAgent(agentName);
3245
+ await this.assertSessionTurnOwnership(task, runtime);
3246
+ }
3247
+ else {
3248
+ const resolved = await this.resolveRemoteCodexSession(taskId);
3249
+ ({ agentName, runtime, remote } = resolved);
3250
+ if (isRemoteSessionActive(remote)) {
3251
+ throw new Error("这个任务正在外部 Agent 中执行,请等待完成后再使用 SwitchGroup。");
3252
+ }
3253
+ }
3254
+ const remoteSessionId = task?.remoteSessionId ?? remote.id;
3255
+ const operationKey = agentRemoteKey(agentName, remoteSessionId);
3256
+ if (this.switchGroupOperations.has(operationKey))
3257
+ throw new Error("正在为此任务创建群聊,请勿重复点击。");
3258
+ const cwd = task?.cwd ?? remote.cwd;
3259
+ if (!cwd)
3260
+ throw new Error("指定任务没有可用的工作目录,暂时无法创建群聊。");
3261
+ const title = task?.title ?? remote?.title ?? remote?.preview ?? remoteSessionId;
3262
+ const boundProjectCwd = detectProjectlessWorkspace(cwd) ? undefined : cwd;
3263
+ this.switchGroupOperations.add(operationKey);
3264
+ try {
3265
+ const group = await this.createFeishuGroupContext(sourceContextKey, agentName, title, userId, boundProjectCwd, "SwitchGroup");
3266
+ try {
3267
+ const latest = task ? undefined : await runtime.readRemoteSession(remoteSessionId);
3268
+ if (latest && isRemoteSessionActive(latest))
3269
+ throw new Error("所选任务已在外部 Agent 中开始执行,请等待完成后切换。");
3270
+ // Group creation may take time; reuse any canonical task linked in the meantime.
3271
+ task = this.store.findSessionByRemoteSessionId(remoteSessionId, undefined, agentName) ?? task;
3272
+ if (task) {
3273
+ task = this.store.getSession(task.localSessionId);
3274
+ if (!task || task.status === "closed")
3275
+ throw new Error("所选任务已归档,请重新选择任务。");
3276
+ await this.assertSessionTurnOwnership(task, runtime);
3277
+ task = this.store.getSession(task.localSessionId);
3278
+ if (!task || task.status === "closed" || task.remoteSessionId !== remoteSessionId) {
3279
+ throw new Error("所选任务已变化,请重新选择任务。");
3280
+ }
3281
+ this.store.attachSessionToContext(group.contextKey, task.localSessionId);
3282
+ }
3283
+ else {
3284
+ if (!latest)
3285
+ throw new Error("无法读取所选任务,请重试。");
3286
+ const localSessionId = createId("sess");
3287
+ this.store.createSession({ localSessionId, contextKey: group.contextKey, agentName, cwd, status: "ready" });
3288
+ this.store.updateRuntimeSession(localSessionId, {
3289
+ runtimeKind: "codex",
3290
+ remoteSessionId,
3291
+ title: latest.title ?? latest.preview ?? title,
3292
+ modelProvider: latest.modelProvider ?? remote?.modelProvider,
3293
+ model: latest.model ?? remote?.model,
3294
+ reasoningEffort: latest.reasoningEffort ?? remote?.reasoningEffort,
3295
+ permissionMode: latest.permissionMode ?? remote?.permissionMode ?? "auto",
3296
+ lastTurnId: latest.lastTurnId ?? remote?.lastTurnId,
3297
+ lastTurnStatus: mapRemoteTurnStatus(latest.lastTurnStatus ?? remote?.lastTurnStatus),
3298
+ });
3299
+ task = this.store.getSession(localSessionId);
3300
+ }
3301
+ this.store.setCurrentSession(group.contextKey, task.localSessionId);
3302
+ // A live Turn keeps its original delivery route. The new group's next Turn
3303
+ // establishes its own route through startPendingTurn.
3304
+ if (task.status !== "running" && !runtime.getSession(task.localSessionId)?.activeTurnId) {
3305
+ this.outbound.registerSession(task.localSessionId, group.contextKey, task.title, task.cwd, this.agentLabel(agentName), task.model, task.modelProvider);
3306
+ }
3307
+ this.store.audit(sourceContextKey, "session_switch_group", {
3308
+ localSessionId: task.localSessionId, remoteSessionId, groupContextKey: group.contextKey,
3309
+ });
3310
+ }
3311
+ catch (error) {
3312
+ await this.outbound.sendText(group.contextKey, `群已创建,但绑定任务失败:${runtimeErrorMessage(error)}。可使用 /sessions 重新选择任务。`)
3313
+ .catch((noticeError) => this.logger.warn({ error: noticeError, contextKey: group.contextKey }, "Failed to report SwitchGroup binding failure."));
3314
+ throw new Error(`群 ${group.name} 已创建,但绑定任务失败:${runtimeErrorMessage(error)}`);
3315
+ }
3316
+ await this.outbound.sendText(group.contextKey, [
3317
+ "已切换到已有任务,历史与原会话共享。",
3318
+ `当前任务:${task.title ?? title}(${remoteSessionId})`,
3319
+ `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
3320
+ `当前 Provider:${task.modelProvider ?? "Agent 默认"}`,
3321
+ `当前模型:${task.model ?? "默认"}`,
3322
+ `思考强度:${task.reasoningEffort ?? "自动"}`,
3323
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
3324
+ ].join("\n"));
3325
+ await this.outbound.sendText(sourceContextKey, `已创建飞书群:${group.name},并切换到任务 ${task.title ?? remoteSessionId}。原会话不变。`);
3326
+ }
3327
+ finally {
3328
+ this.switchGroupOperations.delete(operationKey);
3329
+ }
3330
+ }
3112
3331
  async createFeishuGroupWithTask(sourceContextKey, agentName, requestedTitle, userId, boundProjectCwd, executionSettings) {
3113
3332
  const explicitTitle = normalizeTaskTitle(requestedTitle);
3114
3333
  const taskTitle = explicitTitle ?? `新任务 (${formatGroupNameDate(new Date(), this.config.feishu?.groupNameFormat?.dateFormat ?? DEFAULT_GROUP_NAME_FORMAT.dateFormat)})`;
@@ -3298,7 +3517,7 @@ export class ProxySessionController {
3298
3517
  record: { ...loaded.record, contextKey: record.contextKey },
3299
3518
  };
3300
3519
  }
3301
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model);
3520
+ this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName), record.model, record.modelProvider);
3302
3521
  const loading = (async () => {
3303
3522
  if (record.lastTurnId) {
3304
3523
  try {
@@ -3949,7 +4168,7 @@ export class ProxySessionController {
3949
4168
  lastTurnId: turnId,
3950
4169
  lastTurnStatus: "completed",
3951
4170
  });
3952
- this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName), current.model);
4171
+ this.outbound.registerSession(current.localSessionId, contextKey, forked.title ?? current.title, current.cwd, this.agentLabel(current.agentName), forked.model ?? current.model, forked.modelProvider ?? current.modelProvider);
3953
4172
  this.store.audit(contextKey, "session_reset_to_turn", {
3954
4173
  localSessionId: current.localSessionId,
3955
4174
  previousRemoteSessionId,
@@ -4046,20 +4265,6 @@ export class ProxySessionController {
4046
4265
  }
4047
4266
  await this.openExecutionSettings(contextKey, "agent");
4048
4267
  }
4049
- async openProviderSettings(contextKey) {
4050
- const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
4051
- const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
4052
- ? await this.modelProviderOptions(loaded)
4053
- : [];
4054
- const current = loaded.session.modelProvider?.trim()
4055
- || providers.find((provider) => provider.isDefault)?.id
4056
- || "运行时默认";
4057
- if (providers.length <= 1) {
4058
- await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
4059
- return;
4060
- }
4061
- await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
4062
- }
4063
4268
  async selectProvider(contextKey, modelProvider, options = {}) {
4064
4269
  const record = options.sessionId
4065
4270
  ? this.requireSession(contextKey, options.sessionId)
@@ -4194,7 +4399,7 @@ export class ProxySessionController {
4194
4399
  }
4195
4400
  };
4196
4401
  const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
4197
- this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
4402
+ this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
4198
4403
  return session;
4199
4404
  }
4200
4405
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
@@ -4256,7 +4461,7 @@ export class ProxySessionController {
4256
4461
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
4257
4462
  }
4258
4463
  this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
4259
- this.outbound.updateSessionModel(loaded.record.localSessionId, model);
4464
+ this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
4260
4465
  await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
4261
4466
  const effortMessage = nextEffort && nextEffort !== currentEffort
4262
4467
  ? `,思考强度已自动调整为 ${nextEffort}`
@@ -4706,6 +4911,87 @@ export class ProxySessionController {
4706
4911
  assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
4707
4912
  };
4708
4913
  }
4914
+ async loadTurnDetails(turnId) {
4915
+ const existing = this.turnDetailHydrations.get(turnId);
4916
+ if (existing)
4917
+ return existing;
4918
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
4919
+ if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(turnId))
4920
+ return this.store.previews.load(turnId);
4921
+ if (snapshot && !needsHistoricalDetails(snapshot))
4922
+ return snapshot;
4923
+ const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
4924
+ if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn))
4925
+ return undefined;
4926
+ const hydration = snapshot ? this.hydrateTurnDetails(snapshot) : this.loadUnrecordedTurnPreview(origin);
4927
+ this.turnDetailHydrations.set(turnId, hydration);
4928
+ try {
4929
+ return await hydration;
4930
+ }
4931
+ finally {
4932
+ if (this.turnDetailHydrations.get(turnId) === hydration)
4933
+ this.turnDetailHydrations.delete(turnId);
4934
+ }
4935
+ }
4936
+ async loadUnrecordedTurnPreview(origin) {
4937
+ const runtime = this.runtimes.forAgent(origin.agentName);
4938
+ if (!runtime.readRemoteTurn)
4939
+ throw new Error("此 Agent 不支持读取历史执行详情。");
4940
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, origin.turnId);
4941
+ if (details.turnId !== origin.turnId)
4942
+ throw new Error("Agent 返回的轮次与请求不一致,请重试。");
4943
+ let remote;
4944
+ try {
4945
+ remote = await runtime.readRemoteSession?.(origin.remoteSessionId, "metadata");
4946
+ }
4947
+ catch (error) {
4948
+ this.logger.warn({ error, turnId: origin.turnId }, "Failed to read Preview task metadata; retaining full Turn details.");
4949
+ }
4950
+ // An Agent Bot turn may have arrived while the read-only request was in flight.
4951
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
4952
+ if (latest)
4953
+ return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
4954
+ const state = hydrateTurnViewState(createTurnViewState(origin.localSessionId, origin.turnId, details.startedAt ?? 0, remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName)), details);
4955
+ // Viewing external work must not create a task, a chat binding or a pending final reply.
4956
+ this.store.previews.seed(state);
4957
+ this.store.previews.release(state.turnId);
4958
+ return state;
4959
+ }
4960
+ async hydrateTurnDetails(snapshot) {
4961
+ let updated;
4962
+ try {
4963
+ const origin = this.store.getTurnRuntimeOrigin(snapshot.turnId);
4964
+ if (!origin)
4965
+ throw new Error("未保存本轮的原始 Agent 会话信息,无法读取历史执行详情。");
4966
+ const runtime = this.runtimes.forAgent(origin.agentName);
4967
+ if (!runtime.readRemoteTurn)
4968
+ throw new Error("此 Agent 不支持读取历史执行详情。");
4969
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, snapshot.turnId);
4970
+ if (details.turnId !== snapshot.turnId)
4971
+ throw new Error("Agent 返回的轮次与请求不一致,请重试。");
4972
+ const session = this.store.getSession(snapshot.sessionId);
4973
+ updated = hydrateTurnViewState({
4974
+ ...snapshot,
4975
+ projectCwd: snapshot.projectCwd ?? session?.cwd,
4976
+ agentLabel: snapshot.agentLabel ?? this.agentLabel(origin.agentName),
4977
+ }, details);
4978
+ }
4979
+ catch (error) {
4980
+ const message = error instanceof Error ? error.message : String(error);
4981
+ this.logger.warn({ error, turnId: snapshot.turnId }, "Failed to load historical Turn details.");
4982
+ updated = { ...snapshot, historyDetail: "summary", historyDetailError: message };
4983
+ }
4984
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(snapshot.turnId));
4985
+ if (!latest || !needsHistoricalDetails(latest))
4986
+ return latest;
4987
+ if (updated.historyDetail === "full") {
4988
+ this.store.previews.seed(updated);
4989
+ this.store.previews.flush(updated.turnId, true);
4990
+ updated = { ...updated, previewJournal: true };
4991
+ }
4992
+ this.store.saveTurnSnapshot(snapshot.turnId, snapshot.sessionId, updated);
4993
+ return updated;
4994
+ }
4709
4995
  async openTurnDetails(contextKey, reference) {
4710
4996
  const current = this.requireCurrentSession(contextKey);
4711
4997
  let turnId;
@@ -4748,10 +5034,23 @@ export class ProxySessionController {
4748
5034
  await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(this.runningTurnSnapshot(current, turnId)));
4749
5035
  return;
4750
5036
  }
5037
+ await this.loadTurnDetails(turnId);
4751
5038
  await this.outbound.showDetails(contextKey, turnId);
4752
5039
  }
4753
5040
  async openResetHistory(contextKey, options = {}) {
4754
- const current = this.requireCurrentSession(contextKey);
5041
+ const taskId = options.sessionReference
5042
+ ? this.resolveSessionReference(contextKey, options.sessionReference)
5043
+ : undefined;
5044
+ const current = taskId
5045
+ ? this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId)
5046
+ : this.requireCurrentSession(contextKey);
5047
+ if (!current && taskId) {
5048
+ await this.openRemoteTurnHistory(contextKey, taskId, options);
5049
+ return;
5050
+ }
5051
+ if (!current || current.status === "closed")
5052
+ throw new Error(`找不到任务:${taskId}`);
5053
+ const readOnly = this.currentSession(contextKey)?.localSessionId !== current.localSessionId;
4755
5054
  if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
4756
5055
  throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
4757
5056
  }
@@ -4809,7 +5108,7 @@ export class ProxySessionController {
4809
5108
  current: isCurrent,
4810
5109
  running: isRunning,
4811
5110
  resetting: isResetting,
4812
- actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
5111
+ actions: readOnly || isCurrent || isRunning || resettingTurnId ? undefined : [{
4813
5112
  text: "Reset",
4814
5113
  value: {
4815
5114
  action: "turn_reset",
@@ -4826,8 +5125,8 @@ export class ProxySessionController {
4826
5125
  ...(page > 0 ? [{
4827
5126
  text: "Previous",
4828
5127
  value: {
4829
- action: "turn_reset_page",
4830
- sessionId: current.localSessionId,
5128
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
5129
+ sessionId: options.sessionReference ?? current.localSessionId,
4831
5130
  contextKey,
4832
5131
  page: String(page - 1),
4833
5132
  },
@@ -4835,14 +5134,15 @@ export class ProxySessionController {
4835
5134
  ...(page < totalPages - 1 ? [{
4836
5135
  text: "Next",
4837
5136
  value: {
4838
- action: "turn_reset_page",
4839
- sessionId: current.localSessionId,
5137
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
5138
+ sessionId: options.sessionReference ?? current.localSessionId,
4840
5139
  contextKey,
4841
5140
  page: String(page + 1),
4842
5141
  },
4843
5142
  }] : []),
4844
5143
  ];
4845
5144
  const card = this.cardRenderer.renderResetHistoryCard({
5145
+ ...(options.sessionReference ? { taskTitle: current.title ?? current.remoteSessionId, readOnly } : {}),
4846
5146
  entries,
4847
5147
  footerLines: [
4848
5148
  hasMoreHistory
@@ -4861,6 +5161,75 @@ export class ProxySessionController {
4861
5161
  await this.outbound.sendInteractiveCard(contextKey, card);
4862
5162
  }
4863
5163
  }
5164
+ async openRemoteTurnHistory(contextKey, reference, options) {
5165
+ const { runtime, remote } = await this.resolveRemoteCodexSession(reference);
5166
+ if (!runtime.listRemoteTurnSummaries)
5167
+ throw new Error("当前 Agent 不支持读取历史轮次。");
5168
+ const page = Math.max(0, Math.trunc(options.page ?? 0));
5169
+ const running = remote.lastTurnStatus === "inProgress" && remote.lastTurnId;
5170
+ let cursor = options.cursor;
5171
+ // Next carries its cursor; only backwards navigation needs to locate a page again.
5172
+ if (!cursor && page > 0) {
5173
+ const seen = new Set();
5174
+ for (let index = 0; index < page; index += 1) {
5175
+ const previous = await runtime.listRemoteTurnSummaries(remote.id, {
5176
+ cursor,
5177
+ limit: RESET_HISTORY_PAGE_SIZE - (index === 0 && running ? 1 : 0),
5178
+ });
5179
+ if (!previous.nextCursor || seen.has(previous.nextCursor)) {
5180
+ throw new Error("任务轮次已变化,请从 /sessions 重新打开 Turns。");
5181
+ }
5182
+ cursor = previous.nextCursor;
5183
+ seen.add(cursor);
5184
+ }
5185
+ }
5186
+ const result = await runtime.listRemoteTurnSummaries(remote.id, {
5187
+ cursor,
5188
+ limit: RESET_HISTORY_PAGE_SIZE - (page === 0 && running ? 1 : 0),
5189
+ });
5190
+ if (result.nextCursor && result.nextCursor === cursor) {
5191
+ throw new Error("历史轮次分页游标无效,请稍后重试。");
5192
+ }
5193
+ const turns = [
5194
+ ...(page === 0 && running ? [{ id: running, prompt: remote.lastUserPrompt }] : []),
5195
+ ...result.turns,
5196
+ ];
5197
+ const entries = turns.map((turn, index) => ({
5198
+ sequence: page * RESET_HISTORY_PAGE_SIZE + index + 1,
5199
+ graphNodeLine: "●",
5200
+ graphConnectorLine: index < turns.length - 1 ? "│" : undefined,
5201
+ lines: [cardText(truncateText((turn.prompt ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100))],
5202
+ timestamp: formatResetTurnTime(turn.completedAt ?? turn.startedAt),
5203
+ current: remote.lastTurnStatus === "completed" && remote.lastTurnId === turn.id,
5204
+ running: running === turn.id,
5205
+ }));
5206
+ const pageAction = (text, targetPage, targetCursor) => ({
5207
+ text,
5208
+ value: {
5209
+ action: "session_turns_page",
5210
+ sessionId: reference,
5211
+ contextKey,
5212
+ page: String(targetPage),
5213
+ ...(targetCursor ? { cursor: targetCursor } : {}),
5214
+ },
5215
+ });
5216
+ const card = this.cardRenderer.renderResetHistoryCard({
5217
+ taskTitle: remote.title ?? remote.preview ?? remote.id,
5218
+ readOnly: true,
5219
+ entries,
5220
+ footerLines: [`第 ${page + 1} 页 · 本页 ${entries.length} 个 turn`],
5221
+ pageActions: [
5222
+ ...(page > 0 ? [pageAction("Previous", page - 1)] : []),
5223
+ ...(result.nextCursor ? [pageAction("Next", page + 1, result.nextCursor)] : []),
5224
+ ],
5225
+ });
5226
+ if (options.updateMessageId) {
5227
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
5228
+ }
5229
+ else {
5230
+ await this.outbound.sendInteractiveCard(contextKey, card);
5231
+ }
5232
+ }
4864
5233
  async openDirectoryBrowser(contextKey, requestedDirectory, options = {}) {
4865
5234
  if (requestedDirectory === WINDOWS_DRIVES_DIRECTORY) {
4866
5235
  await this.openWindowsDriveBrowser(contextKey, options);
@@ -5132,6 +5501,16 @@ export class ProxySessionController {
5132
5501
  contextKey,
5133
5502
  },
5134
5503
  })];
5504
+ actions.push(bindCardAction({
5505
+ text: "SwitchGroup",
5506
+ value: {
5507
+ action: "session_switch_group",
5508
+ sessionId: entry.reference,
5509
+ ...(searchTerm ? { searchTerm } : {}),
5510
+ page: String(page),
5511
+ contextKey,
5512
+ },
5513
+ }));
5135
5514
  const projectActions = [bindCardAction({
5136
5515
  text: "New",
5137
5516
  value: {
@@ -5179,6 +5558,14 @@ export class ProxySessionController {
5179
5558
  contextKey,
5180
5559
  },
5181
5560
  }));
5561
+ actions.push(bindCardAction({
5562
+ text: "Turns",
5563
+ value: {
5564
+ action: "session_turns",
5565
+ sessionId: entry.reference,
5566
+ contextKey,
5567
+ },
5568
+ }));
5182
5569
  actions.push(bindCardAction({
5183
5570
  text: "Archive",
5184
5571
  value: {
@@ -5372,7 +5759,7 @@ export class ProxySessionController {
5372
5759
  await this.assertSessionTurnOwnership(existing, runtime);
5373
5760
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
5374
5761
  this.store.setCurrentSession(contextKey, existing.localSessionId);
5375
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model);
5762
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model, existing.modelProvider);
5376
5763
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
5377
5764
  return;
5378
5765
  }
@@ -5399,7 +5786,7 @@ export class ProxySessionController {
5399
5786
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
5400
5787
  });
5401
5788
  this.store.setCurrentSession(contextKey, localSessionId);
5402
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model);
5789
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model, remote.modelProvider);
5403
5790
  await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
5404
5791
  }
5405
5792
  async stopSessionReference(contextKey, taskId) {
@@ -5784,6 +6171,9 @@ export class ProxySessionController {
5784
6171
  const actions = [
5785
6172
  statusRefreshAction(taskId, contextKey),
5786
6173
  ...taskActions,
6174
+ ...this.statusPreviewActions(contextKey, activeTurnId ?? current?.lastTurnId, current?.remoteSessionId ? {
6175
+ agentName: current.agentName, remoteSessionId: current.remoteSessionId, localSessionId: current.localSessionId,
6176
+ } : undefined),
5787
6177
  ];
5788
6178
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
5789
6179
  if (options.updateMessageId)
@@ -5791,6 +6181,15 @@ export class ProxySessionController {
5791
6181
  else
5792
6182
  await this.outbound.sendInteractiveCard(contextKey, card);
5793
6183
  }
6184
+ statusPreviewActions(contextKey, turnId, source) {
6185
+ if (!turnId || turnId.startsWith("pending_"))
6186
+ return [];
6187
+ if (source && this.runtimes.forAgent(source.agentName).readRemoteTurn) {
6188
+ this.store.saveTurnRuntimeOrigin(turnId, source.localSessionId ?? remoteSessionReference(source.agentName, source.remoteSessionId), source.agentName, source.remoteSessionId);
6189
+ }
6190
+ const url = this.outbound.getTurnPreviewUrl(contextKey, turnId);
6191
+ return url ? [{ text: "Preview", value: {}, url }] : [];
6192
+ }
5794
6193
  async statusForCodexTask(contextKey, reference, options = {}) {
5795
6194
  const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5796
6195
  const runtime = this.runtimes.forAgent(agentName);
@@ -5838,6 +6237,7 @@ export class ProxySessionController {
5838
6237
  const actions = [
5839
6238
  statusRefreshAction(actionReference, contextKey),
5840
6239
  statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", actionReference, contextKey),
6240
+ ...this.statusPreviewActions(contextKey, remote.lastTurnId, { agentName, remoteSessionId: remote.id }),
5841
6241
  ];
5842
6242
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
5843
6243
  if (options.updateMessageId)
@@ -5905,7 +6305,7 @@ export class ProxySessionController {
5905
6305
  }
5906
6306
  this.store.setChatRequiresMention(chatContextKey, enabled);
5907
6307
  await this.outbound.sendText(contextKey, enabled
5908
- ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;发送 @机器人 /mute off 可恢复自动响应。"
6308
+ ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;普通提问会补充当前群/话题近期文字和图片(最多 20 条、6 张图片)。发送 @机器人 /mute off 可恢复自动响应并关闭自动补充。"
5909
6309
  : "已关闭当前群的静音模式。机器人将恢复自动响应群消息。");
5910
6310
  }
5911
6311
  requireControlSession(localSessionId) {
@@ -6638,6 +7038,18 @@ function isUnmaterializedCodexThreadError(error) {
6638
7038
  const message = error instanceof Error ? error.message : String(error);
6639
7039
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
6640
7040
  }
7041
+ function needsHistoricalDetails(state) {
7042
+ if (!isTerminalTurnViewStatus(state.status) || state.historyDetail === "full")
7043
+ return false;
7044
+ if (state.historyDetail === "summary")
7045
+ return true;
7046
+ return state.status === "completed" && state.finalResponse === undefined && !state.assistantText
7047
+ && !state.error && !state.progressText && !state.approval && !state.activeTool
7048
+ && !state.activities?.length && !state.plan?.length && !state.reasoningItems?.length
7049
+ && !state.completedTools?.length && !state.failedTools?.length && !state.fileSummary?.length
7050
+ && !state.totalToolCount && !state.totalTokens
7051
+ && state.fullToolOutputs === undefined && state.fullToolErrors === undefined && state.toolStatuses === undefined;
7052
+ }
6641
7053
  function turnViewSnapshot(value) {
6642
7054
  if (!value || typeof value !== "object")
6643
7055
  return undefined;
@@ -6951,7 +7363,7 @@ function isBotOwnedActiveTurn(record, remote) {
6951
7363
  && record.lastTurnId === remote.lastTurnId;
6952
7364
  }
6953
7365
  function isQueueIndependentCommand(command) {
6954
- if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
7366
+ if (["archive", "dismiss", "stop", "status", "restart", "update", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
6955
7367
  return true;
6956
7368
  if (command.type === "agent")
6957
7369
  return command.agent === undefined;