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

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 (100) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +33 -5
  3. package/README.zh.md +32 -5
  4. package/dist/acp/AcpJsonRpcConnection.js +3 -1
  5. package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
  6. package/dist/acp/AcpSessionManager.js +0 -14
  7. package/dist/acp/AcpSessionManager.js.map +1 -1
  8. package/dist/acp/acpTypes.js.map +1 -1
  9. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  10. package/dist/cli/SelfUpdater.js +2 -0
  11. package/dist/cli/SelfUpdater.js.map +1 -1
  12. package/dist/codex/AppServerConnection.js +3 -1
  13. package/dist/codex/AppServerConnection.js.map +1 -1
  14. package/dist/codex/CodexEventMapper.js +28 -1
  15. package/dist/codex/CodexEventMapper.js.map +1 -1
  16. package/dist/codex/CodexRuntime.js +92 -0
  17. package/dist/codex/CodexRuntime.js.map +1 -1
  18. package/dist/commands/CommandRouter.js +5 -3
  19. package/dist/commands/CommandRouter.js.map +1 -1
  20. package/dist/feishu/CardRenderer.js +45 -23
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +2 -0
  23. package/dist/feishu/FeishuConnector.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 +497 -62
  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 +155 -6
  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 +466 -38
  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 +12 -1
  67. package/src/acp/AcpJsonRpcConnection.ts +3 -1
  68. package/src/acp/AcpSessionManager.ts +0 -17
  69. package/src/acp/acpTypes.ts +0 -1
  70. package/src/cli/SelfUpdateRunner.ts +1 -0
  71. package/src/cli/SelfUpdater.ts +2 -0
  72. package/src/codex/AppServerConnection.ts +3 -1
  73. package/src/codex/CodexEventMapper.ts +29 -1
  74. package/src/codex/CodexRuntime.ts +87 -0
  75. package/src/commands/CommandRouter.ts +4 -3
  76. package/src/commands/commandTypes.ts +1 -0
  77. package/src/feishu/CardRenderer.ts +43 -21
  78. package/src/feishu/FeishuConnector.ts +2 -0
  79. package/src/feishu/FeishuMessageClient.ts +44 -0
  80. package/src/feishu/FeishuTurnPresenter.ts +107 -31
  81. package/src/feishu/RecentMessageContext.ts +90 -0
  82. package/src/feishu/types.ts +5 -0
  83. package/src/index.ts +62 -22
  84. package/src/local-files/DiagramPreview.ts +173 -0
  85. package/src/local-files/LocalFileViewerServer.ts +129 -2
  86. package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
  87. package/src/local-files/TurnPreviewPage.ts +537 -59
  88. package/src/presentation/OutboundRouter.ts +25 -7
  89. package/src/presentation/TurnStateReducer.ts +151 -7
  90. package/src/presentation/turnReasoning.ts +21 -0
  91. package/src/presentation/turnViewTypes.ts +19 -1
  92. package/src/proxy/ProxySessionController.ts +457 -27
  93. package/src/runtime/types.ts +21 -0
  94. package/src/state/StateStore.ts +45 -2
  95. package/src/state/TurnPreviewJournal.ts +398 -0
  96. package/src/state/migrations.ts +10 -0
  97. package/src/updates/AutomaticUpdatePreparer.ts +21 -7
  98. package/src/updates/ManualUpdateController.ts +116 -0
  99. package/src/updates/PublishedRelease.ts +49 -3
  100. 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;
@@ -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,
@@ -4180,7 +4399,7 @@ export class ProxySessionController {
4180
4399
  }
4181
4400
  };
4182
4401
  const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
4183
- this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
4402
+ this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
4184
4403
  return session;
4185
4404
  }
4186
4405
  async assertProviderModelSettings(loaded, modelProvider, model, effort) {
@@ -4242,7 +4461,7 @@ export class ProxySessionController {
4242
4461
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
4243
4462
  }
4244
4463
  this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
4245
- this.outbound.updateSessionModel(loaded.record.localSessionId, model);
4464
+ this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
4246
4465
  await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
4247
4466
  const effortMessage = nextEffort && nextEffort !== currentEffort
4248
4467
  ? `,思考强度已自动调整为 ${nextEffort}`
@@ -4585,11 +4804,13 @@ export class ProxySessionController {
4585
4804
  const legacyTitle = legacy && record.agentName.toLowerCase() === legacy.agentName.toLowerCase()
4586
4805
  ? legacy.title
4587
4806
  : undefined;
4588
- if (isDefaultGroupNameFormat(nameFormat) && isLegacyGroupPrefixOnly(event.afterName))
4589
- return;
4590
- const title = normalizeTaskTitle(isDefaultGroupNameFormat(nameFormat)
4807
+ const defaultFormat = isDefaultGroupNameFormat(nameFormat);
4808
+ let parsedTitle = defaultFormat
4591
4809
  ? legacyTitle ?? templateTitle
4592
- : templateTitle ?? legacyTitle);
4810
+ : templateTitle ?? legacyTitle;
4811
+ if (defaultFormat && isLegacyGroupPrefixOnly(event.afterName))
4812
+ parsedTitle = undefined;
4813
+ const title = normalizeTaskTitle(parsedTitle) ?? normalizeTaskTitle(event.afterName);
4593
4814
  if (!title || title === record.title)
4594
4815
  return;
4595
4816
  const loaded = await this.loadSession(record);
@@ -4692,6 +4913,87 @@ export class ProxySessionController {
4692
4913
  assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
4693
4914
  };
4694
4915
  }
4916
+ async loadTurnDetails(turnId) {
4917
+ const existing = this.turnDetailHydrations.get(turnId);
4918
+ if (existing)
4919
+ return existing;
4920
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
4921
+ if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(turnId))
4922
+ return this.store.previews.load(turnId);
4923
+ if (snapshot && !needsHistoricalDetails(snapshot))
4924
+ return snapshot;
4925
+ const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
4926
+ if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn))
4927
+ return undefined;
4928
+ const hydration = snapshot ? this.hydrateTurnDetails(snapshot) : this.loadUnrecordedTurnPreview(origin);
4929
+ this.turnDetailHydrations.set(turnId, hydration);
4930
+ try {
4931
+ return await hydration;
4932
+ }
4933
+ finally {
4934
+ if (this.turnDetailHydrations.get(turnId) === hydration)
4935
+ this.turnDetailHydrations.delete(turnId);
4936
+ }
4937
+ }
4938
+ async loadUnrecordedTurnPreview(origin) {
4939
+ const runtime = this.runtimes.forAgent(origin.agentName);
4940
+ if (!runtime.readRemoteTurn)
4941
+ throw new Error("此 Agent 不支持读取历史执行详情。");
4942
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, origin.turnId);
4943
+ if (details.turnId !== origin.turnId)
4944
+ throw new Error("Agent 返回的轮次与请求不一致,请重试。");
4945
+ let remote;
4946
+ try {
4947
+ remote = await runtime.readRemoteSession?.(origin.remoteSessionId, "metadata");
4948
+ }
4949
+ catch (error) {
4950
+ this.logger.warn({ error, turnId: origin.turnId }, "Failed to read Preview task metadata; retaining full Turn details.");
4951
+ }
4952
+ // An Agent Bot turn may have arrived while the read-only request was in flight.
4953
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
4954
+ if (latest)
4955
+ return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
4956
+ const state = hydrateTurnViewState(createTurnViewState(origin.localSessionId, origin.turnId, details.startedAt ?? 0, remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName)), details);
4957
+ // Viewing external work must not create a task, a chat binding or a pending final reply.
4958
+ this.store.previews.seed(state);
4959
+ this.store.previews.release(state.turnId);
4960
+ return state;
4961
+ }
4962
+ async hydrateTurnDetails(snapshot) {
4963
+ let updated;
4964
+ try {
4965
+ const origin = this.store.getTurnRuntimeOrigin(snapshot.turnId);
4966
+ if (!origin)
4967
+ throw new Error("未保存本轮的原始 Agent 会话信息,无法读取历史执行详情。");
4968
+ const runtime = this.runtimes.forAgent(origin.agentName);
4969
+ if (!runtime.readRemoteTurn)
4970
+ throw new Error("此 Agent 不支持读取历史执行详情。");
4971
+ const details = await runtime.readRemoteTurn(origin.remoteSessionId, snapshot.turnId);
4972
+ if (details.turnId !== snapshot.turnId)
4973
+ throw new Error("Agent 返回的轮次与请求不一致,请重试。");
4974
+ const session = this.store.getSession(snapshot.sessionId);
4975
+ updated = hydrateTurnViewState({
4976
+ ...snapshot,
4977
+ projectCwd: snapshot.projectCwd ?? session?.cwd,
4978
+ agentLabel: snapshot.agentLabel ?? this.agentLabel(origin.agentName),
4979
+ }, details);
4980
+ }
4981
+ catch (error) {
4982
+ const message = error instanceof Error ? error.message : String(error);
4983
+ this.logger.warn({ error, turnId: snapshot.turnId }, "Failed to load historical Turn details.");
4984
+ updated = { ...snapshot, historyDetail: "summary", historyDetailError: message };
4985
+ }
4986
+ const latest = turnViewSnapshot(this.store.getTurnSnapshot(snapshot.turnId));
4987
+ if (!latest || !needsHistoricalDetails(latest))
4988
+ return latest;
4989
+ if (updated.historyDetail === "full") {
4990
+ this.store.previews.seed(updated);
4991
+ this.store.previews.flush(updated.turnId, true);
4992
+ updated = { ...updated, previewJournal: true };
4993
+ }
4994
+ this.store.saveTurnSnapshot(snapshot.turnId, snapshot.sessionId, updated);
4995
+ return updated;
4996
+ }
4695
4997
  async openTurnDetails(contextKey, reference) {
4696
4998
  const current = this.requireCurrentSession(contextKey);
4697
4999
  let turnId;
@@ -4734,10 +5036,23 @@ export class ProxySessionController {
4734
5036
  await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(this.runningTurnSnapshot(current, turnId)));
4735
5037
  return;
4736
5038
  }
5039
+ await this.loadTurnDetails(turnId);
4737
5040
  await this.outbound.showDetails(contextKey, turnId);
4738
5041
  }
4739
5042
  async openResetHistory(contextKey, options = {}) {
4740
- const current = this.requireCurrentSession(contextKey);
5043
+ const taskId = options.sessionReference
5044
+ ? this.resolveSessionReference(contextKey, options.sessionReference)
5045
+ : undefined;
5046
+ const current = taskId
5047
+ ? this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId)
5048
+ : this.requireCurrentSession(contextKey);
5049
+ if (!current && taskId) {
5050
+ await this.openRemoteTurnHistory(contextKey, taskId, options);
5051
+ return;
5052
+ }
5053
+ if (!current || current.status === "closed")
5054
+ throw new Error(`找不到任务:${taskId}`);
5055
+ const readOnly = this.currentSession(contextKey)?.localSessionId !== current.localSessionId;
4741
5056
  if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
4742
5057
  throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
4743
5058
  }
@@ -4795,7 +5110,7 @@ export class ProxySessionController {
4795
5110
  current: isCurrent,
4796
5111
  running: isRunning,
4797
5112
  resetting: isResetting,
4798
- actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
5113
+ actions: readOnly || isCurrent || isRunning || resettingTurnId ? undefined : [{
4799
5114
  text: "Reset",
4800
5115
  value: {
4801
5116
  action: "turn_reset",
@@ -4812,8 +5127,8 @@ export class ProxySessionController {
4812
5127
  ...(page > 0 ? [{
4813
5128
  text: "Previous",
4814
5129
  value: {
4815
- action: "turn_reset_page",
4816
- sessionId: current.localSessionId,
5130
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
5131
+ sessionId: options.sessionReference ?? current.localSessionId,
4817
5132
  contextKey,
4818
5133
  page: String(page - 1),
4819
5134
  },
@@ -4821,14 +5136,15 @@ export class ProxySessionController {
4821
5136
  ...(page < totalPages - 1 ? [{
4822
5137
  text: "Next",
4823
5138
  value: {
4824
- action: "turn_reset_page",
4825
- sessionId: current.localSessionId,
5139
+ action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
5140
+ sessionId: options.sessionReference ?? current.localSessionId,
4826
5141
  contextKey,
4827
5142
  page: String(page + 1),
4828
5143
  },
4829
5144
  }] : []),
4830
5145
  ];
4831
5146
  const card = this.cardRenderer.renderResetHistoryCard({
5147
+ ...(options.sessionReference ? { taskTitle: current.title ?? current.remoteSessionId, readOnly } : {}),
4832
5148
  entries,
4833
5149
  footerLines: [
4834
5150
  hasMoreHistory
@@ -4847,6 +5163,75 @@ export class ProxySessionController {
4847
5163
  await this.outbound.sendInteractiveCard(contextKey, card);
4848
5164
  }
4849
5165
  }
5166
+ async openRemoteTurnHistory(contextKey, reference, options) {
5167
+ const { runtime, remote } = await this.resolveRemoteCodexSession(reference);
5168
+ if (!runtime.listRemoteTurnSummaries)
5169
+ throw new Error("当前 Agent 不支持读取历史轮次。");
5170
+ const page = Math.max(0, Math.trunc(options.page ?? 0));
5171
+ const running = remote.lastTurnStatus === "inProgress" && remote.lastTurnId;
5172
+ let cursor = options.cursor;
5173
+ // Next carries its cursor; only backwards navigation needs to locate a page again.
5174
+ if (!cursor && page > 0) {
5175
+ const seen = new Set();
5176
+ for (let index = 0; index < page; index += 1) {
5177
+ const previous = await runtime.listRemoteTurnSummaries(remote.id, {
5178
+ cursor,
5179
+ limit: RESET_HISTORY_PAGE_SIZE - (index === 0 && running ? 1 : 0),
5180
+ });
5181
+ if (!previous.nextCursor || seen.has(previous.nextCursor)) {
5182
+ throw new Error("任务轮次已变化,请从 /sessions 重新打开 Turns。");
5183
+ }
5184
+ cursor = previous.nextCursor;
5185
+ seen.add(cursor);
5186
+ }
5187
+ }
5188
+ const result = await runtime.listRemoteTurnSummaries(remote.id, {
5189
+ cursor,
5190
+ limit: RESET_HISTORY_PAGE_SIZE - (page === 0 && running ? 1 : 0),
5191
+ });
5192
+ if (result.nextCursor && result.nextCursor === cursor) {
5193
+ throw new Error("历史轮次分页游标无效,请稍后重试。");
5194
+ }
5195
+ const turns = [
5196
+ ...(page === 0 && running ? [{ id: running, prompt: remote.lastUserPrompt }] : []),
5197
+ ...result.turns,
5198
+ ];
5199
+ const entries = turns.map((turn, index) => ({
5200
+ sequence: page * RESET_HISTORY_PAGE_SIZE + index + 1,
5201
+ graphNodeLine: "●",
5202
+ graphConnectorLine: index < turns.length - 1 ? "│" : undefined,
5203
+ lines: [cardText(truncateText((turn.prompt ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100))],
5204
+ timestamp: formatResetTurnTime(turn.completedAt ?? turn.startedAt),
5205
+ current: remote.lastTurnStatus === "completed" && remote.lastTurnId === turn.id,
5206
+ running: running === turn.id,
5207
+ }));
5208
+ const pageAction = (text, targetPage, targetCursor) => ({
5209
+ text,
5210
+ value: {
5211
+ action: "session_turns_page",
5212
+ sessionId: reference,
5213
+ contextKey,
5214
+ page: String(targetPage),
5215
+ ...(targetCursor ? { cursor: targetCursor } : {}),
5216
+ },
5217
+ });
5218
+ const card = this.cardRenderer.renderResetHistoryCard({
5219
+ taskTitle: remote.title ?? remote.preview ?? remote.id,
5220
+ readOnly: true,
5221
+ entries,
5222
+ footerLines: [`第 ${page + 1} 页 · 本页 ${entries.length} 个 turn`],
5223
+ pageActions: [
5224
+ ...(page > 0 ? [pageAction("Previous", page - 1)] : []),
5225
+ ...(result.nextCursor ? [pageAction("Next", page + 1, result.nextCursor)] : []),
5226
+ ],
5227
+ });
5228
+ if (options.updateMessageId) {
5229
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
5230
+ }
5231
+ else {
5232
+ await this.outbound.sendInteractiveCard(contextKey, card);
5233
+ }
5234
+ }
4850
5235
  async openDirectoryBrowser(contextKey, requestedDirectory, options = {}) {
4851
5236
  if (requestedDirectory === WINDOWS_DRIVES_DIRECTORY) {
4852
5237
  await this.openWindowsDriveBrowser(contextKey, options);
@@ -5118,6 +5503,16 @@ export class ProxySessionController {
5118
5503
  contextKey,
5119
5504
  },
5120
5505
  })];
5506
+ actions.push(bindCardAction({
5507
+ text: "SwitchGroup",
5508
+ value: {
5509
+ action: "session_switch_group",
5510
+ sessionId: entry.reference,
5511
+ ...(searchTerm ? { searchTerm } : {}),
5512
+ page: String(page),
5513
+ contextKey,
5514
+ },
5515
+ }));
5121
5516
  const projectActions = [bindCardAction({
5122
5517
  text: "New",
5123
5518
  value: {
@@ -5165,6 +5560,14 @@ export class ProxySessionController {
5165
5560
  contextKey,
5166
5561
  },
5167
5562
  }));
5563
+ actions.push(bindCardAction({
5564
+ text: "Turns",
5565
+ value: {
5566
+ action: "session_turns",
5567
+ sessionId: entry.reference,
5568
+ contextKey,
5569
+ },
5570
+ }));
5168
5571
  actions.push(bindCardAction({
5169
5572
  text: "Archive",
5170
5573
  value: {
@@ -5358,7 +5761,7 @@ export class ProxySessionController {
5358
5761
  await this.assertSessionTurnOwnership(existing, runtime);
5359
5762
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
5360
5763
  this.store.setCurrentSession(contextKey, existing.localSessionId);
5361
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model);
5764
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName), existing.model, existing.modelProvider);
5362
5765
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
5363
5766
  return;
5364
5767
  }
@@ -5385,7 +5788,7 @@ export class ProxySessionController {
5385
5788
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
5386
5789
  });
5387
5790
  this.store.setCurrentSession(contextKey, localSessionId);
5388
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model);
5791
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName), remote.model, remote.modelProvider);
5389
5792
  await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
5390
5793
  }
5391
5794
  async stopSessionReference(contextKey, taskId) {
@@ -5770,6 +6173,9 @@ export class ProxySessionController {
5770
6173
  const actions = [
5771
6174
  statusRefreshAction(taskId, contextKey),
5772
6175
  ...taskActions,
6176
+ ...this.statusPreviewActions(contextKey, activeTurnId ?? current?.lastTurnId, current?.remoteSessionId ? {
6177
+ agentName: current.agentName, remoteSessionId: current.remoteSessionId, localSessionId: current.localSessionId,
6178
+ } : undefined),
5773
6179
  ];
5774
6180
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
5775
6181
  if (options.updateMessageId)
@@ -5777,6 +6183,15 @@ export class ProxySessionController {
5777
6183
  else
5778
6184
  await this.outbound.sendInteractiveCard(contextKey, card);
5779
6185
  }
6186
+ statusPreviewActions(contextKey, turnId, source) {
6187
+ if (!turnId || turnId.startsWith("pending_"))
6188
+ return [];
6189
+ if (source && this.runtimes.forAgent(source.agentName).readRemoteTurn) {
6190
+ this.store.saveTurnRuntimeOrigin(turnId, source.localSessionId ?? remoteSessionReference(source.agentName, source.remoteSessionId), source.agentName, source.remoteSessionId);
6191
+ }
6192
+ const url = this.outbound.getTurnPreviewUrl(contextKey, turnId);
6193
+ return url ? [{ text: "Preview", value: {}, url }] : [];
6194
+ }
5780
6195
  async statusForCodexTask(contextKey, reference, options = {}) {
5781
6196
  const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
5782
6197
  const runtime = this.runtimes.forAgent(agentName);
@@ -5824,6 +6239,7 @@ export class ProxySessionController {
5824
6239
  const actions = [
5825
6240
  statusRefreshAction(actionReference, contextKey),
5826
6241
  statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", actionReference, contextKey),
6242
+ ...this.statusPreviewActions(contextKey, remote.lastTurnId, { agentName, remoteSessionId: remote.id }),
5827
6243
  ];
5828
6244
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
5829
6245
  if (options.updateMessageId)
@@ -5891,7 +6307,7 @@ export class ProxySessionController {
5891
6307
  }
5892
6308
  this.store.setChatRequiresMention(chatContextKey, enabled);
5893
6309
  await this.outbound.sendText(contextKey, enabled
5894
- ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;发送 @机器人 /mute off 可恢复自动响应。"
6310
+ ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;普通提问会补充当前群/话题近期文字和图片(最多 20 条、6 张图片)。发送 @机器人 /mute off 可恢复自动响应并关闭自动补充。"
5895
6311
  : "已关闭当前群的静音模式。机器人将恢复自动响应群消息。");
5896
6312
  }
5897
6313
  requireControlSession(localSessionId) {
@@ -6624,6 +7040,18 @@ function isUnmaterializedCodexThreadError(error) {
6624
7040
  const message = error instanceof Error ? error.message : String(error);
6625
7041
  return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
6626
7042
  }
7043
+ function needsHistoricalDetails(state) {
7044
+ if (!isTerminalTurnViewStatus(state.status) || state.historyDetail === "full")
7045
+ return false;
7046
+ if (state.historyDetail === "summary")
7047
+ return true;
7048
+ return state.status === "completed" && state.finalResponse === undefined && !state.assistantText
7049
+ && !state.error && !state.progressText && !state.approval && !state.activeTool
7050
+ && !state.activities?.length && !state.plan?.length && !state.reasoningItems?.length
7051
+ && !state.completedTools?.length && !state.failedTools?.length && !state.fileSummary?.length
7052
+ && !state.totalToolCount && !state.totalTokens
7053
+ && state.fullToolOutputs === undefined && state.fullToolErrors === undefined && state.toolStatuses === undefined;
7054
+ }
6627
7055
  function turnViewSnapshot(value) {
6628
7056
  if (!value || typeof value !== "object")
6629
7057
  return undefined;
@@ -6937,7 +7365,7 @@ function isBotOwnedActiveTurn(record, remote) {
6937
7365
  && record.lastTurnId === remote.lastTurnId;
6938
7366
  }
6939
7367
  function isQueueIndependentCommand(command) {
6940
- if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
7368
+ if (["archive", "dismiss", "stop", "status", "restart", "update", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
6941
7369
  return true;
6942
7370
  if (command.type === "agent")
6943
7371
  return command.agent === undefined;