@keyou007/agent-bot 0.1.24-alpha.2 → 0.1.24-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/README.md +27 -5
- package/README.zh.md +26 -5
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/cli/SelfUpdater.js +2 -0
- package/dist/cli/SelfUpdater.js.map +1 -1
- package/dist/codex/AppServerConnection.js +3 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +28 -1
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +92 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +45 -23
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +2 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +44 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +114 -37
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/RecentMessageContext.js +70 -0
- package/dist/feishu/RecentMessageContext.js.map +1 -0
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/local-files/DiagramPreview.js +171 -0
- package/dist/local-files/DiagramPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +141 -3
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/PreviewSyntaxHighlight.js +57 -0
- package/dist/local-files/PreviewSyntaxHighlight.js.map +1 -0
- package/dist/local-files/TurnPreviewPage.js +495 -60
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +18 -9
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +155 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/presentation/turnReasoning.js +23 -0
- package/dist/presentation/turnReasoning.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +460 -34
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +43 -2
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +469 -0
- package/dist/state/TurnPreviewJournal.js.map +1 -0
- package/dist/state/migrations.js +10 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/updates/AutomaticUpdatePreparer.js +16 -7
- package/dist/updates/AutomaticUpdatePreparer.js.map +1 -1
- package/dist/updates/ManualUpdateController.js +102 -0
- package/dist/updates/ManualUpdateController.js.map +1 -0
- package/dist/updates/PublishedRelease.js +42 -4
- package/dist/updates/PublishedRelease.js.map +1 -1
- package/dist/utils/ProtocolLineWarning.js +15 -0
- package/dist/utils/ProtocolLineWarning.js.map +1 -0
- package/docs/technical-reference.md +21 -1
- package/docs/technical-reference.zh.md +21 -1
- package/npm-shrinkwrap.json +1401 -10
- package/package.json +3 -1
- package/skills/agent-bot/SKILL.md +8 -1
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/cli/SelfUpdateRunner.ts +1 -0
- package/src/cli/SelfUpdater.ts +2 -0
- package/src/codex/AppServerConnection.ts +3 -1
- package/src/codex/CodexEventMapper.ts +29 -1
- package/src/codex/CodexRuntime.ts +87 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +43 -21
- package/src/feishu/FeishuConnector.ts +2 -0
- package/src/feishu/FeishuMessageClient.ts +44 -0
- package/src/feishu/FeishuTurnPresenter.ts +107 -31
- package/src/feishu/RecentMessageContext.ts +90 -0
- package/src/feishu/types.ts +5 -0
- package/src/index.ts +62 -22
- package/src/local-files/DiagramPreview.ts +173 -0
- package/src/local-files/LocalFileViewerServer.ts +129 -2
- package/src/local-files/PreviewSyntaxHighlight.ts +58 -0
- package/src/local-files/TurnPreviewPage.ts +535 -57
- package/src/presentation/OutboundRouter.ts +25 -7
- package/src/presentation/TurnStateReducer.ts +151 -7
- package/src/presentation/turnReasoning.ts +21 -0
- package/src/presentation/turnViewTypes.ts +19 -1
- package/src/proxy/ProxySessionController.ts +452 -24
- package/src/runtime/types.ts +21 -0
- package/src/state/StateStore.ts +45 -2
- package/src/state/TurnPreviewJournal.ts +398 -0
- package/src/state/migrations.ts +10 -0
- package/src/updates/AutomaticUpdatePreparer.ts +21 -7
- package/src/updates/ManualUpdateController.ts +116 -0
- package/src/updates/PublishedRelease.ts +49 -3
- 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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2967
|
-
|
|
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}`
|
|
@@ -4692,6 +4911,87 @@ export class ProxySessionController {
|
|
|
4692
4911
|
assistantText: "", plan: [], activities: [], completedTools: [], failedTools: [], fileSummary: [],
|
|
4693
4912
|
};
|
|
4694
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
|
+
}
|
|
4695
4995
|
async openTurnDetails(contextKey, reference) {
|
|
4696
4996
|
const current = this.requireCurrentSession(contextKey);
|
|
4697
4997
|
let turnId;
|
|
@@ -4734,10 +5034,23 @@ export class ProxySessionController {
|
|
|
4734
5034
|
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderTurnDetails(this.runningTurnSnapshot(current, turnId)));
|
|
4735
5035
|
return;
|
|
4736
5036
|
}
|
|
5037
|
+
await this.loadTurnDetails(turnId);
|
|
4737
5038
|
await this.outbound.showDetails(contextKey, turnId);
|
|
4738
5039
|
}
|
|
4739
5040
|
async openResetHistory(contextKey, options = {}) {
|
|
4740
|
-
const
|
|
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;
|
|
4741
5054
|
if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
|
|
4742
5055
|
throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
|
|
4743
5056
|
}
|
|
@@ -4795,7 +5108,7 @@ export class ProxySessionController {
|
|
|
4795
5108
|
current: isCurrent,
|
|
4796
5109
|
running: isRunning,
|
|
4797
5110
|
resetting: isResetting,
|
|
4798
|
-
actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
|
|
5111
|
+
actions: readOnly || isCurrent || isRunning || resettingTurnId ? undefined : [{
|
|
4799
5112
|
text: "Reset",
|
|
4800
5113
|
value: {
|
|
4801
5114
|
action: "turn_reset",
|
|
@@ -4812,8 +5125,8 @@ export class ProxySessionController {
|
|
|
4812
5125
|
...(page > 0 ? [{
|
|
4813
5126
|
text: "Previous",
|
|
4814
5127
|
value: {
|
|
4815
|
-
action: "turn_reset_page",
|
|
4816
|
-
sessionId: current.localSessionId,
|
|
5128
|
+
action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
|
|
5129
|
+
sessionId: options.sessionReference ?? current.localSessionId,
|
|
4817
5130
|
contextKey,
|
|
4818
5131
|
page: String(page - 1),
|
|
4819
5132
|
},
|
|
@@ -4821,14 +5134,15 @@ export class ProxySessionController {
|
|
|
4821
5134
|
...(page < totalPages - 1 ? [{
|
|
4822
5135
|
text: "Next",
|
|
4823
5136
|
value: {
|
|
4824
|
-
action: "turn_reset_page",
|
|
4825
|
-
sessionId: current.localSessionId,
|
|
5137
|
+
action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
|
|
5138
|
+
sessionId: options.sessionReference ?? current.localSessionId,
|
|
4826
5139
|
contextKey,
|
|
4827
5140
|
page: String(page + 1),
|
|
4828
5141
|
},
|
|
4829
5142
|
}] : []),
|
|
4830
5143
|
];
|
|
4831
5144
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
5145
|
+
...(options.sessionReference ? { taskTitle: current.title ?? current.remoteSessionId, readOnly } : {}),
|
|
4832
5146
|
entries,
|
|
4833
5147
|
footerLines: [
|
|
4834
5148
|
hasMoreHistory
|
|
@@ -4847,6 +5161,75 @@ export class ProxySessionController {
|
|
|
4847
5161
|
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
4848
5162
|
}
|
|
4849
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
|
+
}
|
|
4850
5233
|
async openDirectoryBrowser(contextKey, requestedDirectory, options = {}) {
|
|
4851
5234
|
if (requestedDirectory === WINDOWS_DRIVES_DIRECTORY) {
|
|
4852
5235
|
await this.openWindowsDriveBrowser(contextKey, options);
|
|
@@ -5118,6 +5501,16 @@ export class ProxySessionController {
|
|
|
5118
5501
|
contextKey,
|
|
5119
5502
|
},
|
|
5120
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
|
+
}));
|
|
5121
5514
|
const projectActions = [bindCardAction({
|
|
5122
5515
|
text: "New",
|
|
5123
5516
|
value: {
|
|
@@ -5165,6 +5558,14 @@ export class ProxySessionController {
|
|
|
5165
5558
|
contextKey,
|
|
5166
5559
|
},
|
|
5167
5560
|
}));
|
|
5561
|
+
actions.push(bindCardAction({
|
|
5562
|
+
text: "Turns",
|
|
5563
|
+
value: {
|
|
5564
|
+
action: "session_turns",
|
|
5565
|
+
sessionId: entry.reference,
|
|
5566
|
+
contextKey,
|
|
5567
|
+
},
|
|
5568
|
+
}));
|
|
5168
5569
|
actions.push(bindCardAction({
|
|
5169
5570
|
text: "Archive",
|
|
5170
5571
|
value: {
|
|
@@ -5358,7 +5759,7 @@ export class ProxySessionController {
|
|
|
5358
5759
|
await this.assertSessionTurnOwnership(existing, runtime);
|
|
5359
5760
|
this.store.attachSessionToContext(contextKey, existing.localSessionId);
|
|
5360
5761
|
this.store.setCurrentSession(contextKey, existing.localSessionId);
|
|
5361
|
-
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);
|
|
5362
5763
|
await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
|
|
5363
5764
|
return;
|
|
5364
5765
|
}
|
|
@@ -5385,7 +5786,7 @@ export class ProxySessionController {
|
|
|
5385
5786
|
lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
|
|
5386
5787
|
});
|
|
5387
5788
|
this.store.setCurrentSession(contextKey, localSessionId);
|
|
5388
|
-
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);
|
|
5389
5790
|
await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
|
|
5390
5791
|
}
|
|
5391
5792
|
async stopSessionReference(contextKey, taskId) {
|
|
@@ -5770,6 +6171,9 @@ export class ProxySessionController {
|
|
|
5770
6171
|
const actions = [
|
|
5771
6172
|
statusRefreshAction(taskId, contextKey),
|
|
5772
6173
|
...taskActions,
|
|
6174
|
+
...this.statusPreviewActions(contextKey, activeTurnId ?? current?.lastTurnId, current?.remoteSessionId ? {
|
|
6175
|
+
agentName: current.agentName, remoteSessionId: current.remoteSessionId, localSessionId: current.localSessionId,
|
|
6176
|
+
} : undefined),
|
|
5773
6177
|
];
|
|
5774
6178
|
const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
|
|
5775
6179
|
if (options.updateMessageId)
|
|
@@ -5777,6 +6181,15 @@ export class ProxySessionController {
|
|
|
5777
6181
|
else
|
|
5778
6182
|
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5779
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
|
+
}
|
|
5780
6193
|
async statusForCodexTask(contextKey, reference, options = {}) {
|
|
5781
6194
|
const { agentName, remote } = await this.resolveRemoteCodexSession(reference, { view: "latest-full" });
|
|
5782
6195
|
const runtime = this.runtimes.forAgent(agentName);
|
|
@@ -5824,6 +6237,7 @@ export class ProxySessionController {
|
|
|
5824
6237
|
const actions = [
|
|
5825
6238
|
statusRefreshAction(actionReference, contextKey),
|
|
5826
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 }),
|
|
5827
6241
|
];
|
|
5828
6242
|
const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
|
|
5829
6243
|
if (options.updateMessageId)
|
|
@@ -5891,7 +6305,7 @@ export class ProxySessionController {
|
|
|
5891
6305
|
}
|
|
5892
6306
|
this.store.setChatRequiresMention(chatContextKey, enabled);
|
|
5893
6307
|
await this.outbound.sendText(contextKey, enabled
|
|
5894
|
-
? "已开启当前群的静音模式。之后只有 @
|
|
6308
|
+
? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;普通提问会补充当前群/话题近期文字和图片(最多 20 条、6 张图片)。发送 @机器人 /mute off 可恢复自动响应并关闭自动补充。"
|
|
5895
6309
|
: "已关闭当前群的静音模式。机器人将恢复自动响应群消息。");
|
|
5896
6310
|
}
|
|
5897
6311
|
requireControlSession(localSessionId) {
|
|
@@ -6624,6 +7038,18 @@ function isUnmaterializedCodexThreadError(error) {
|
|
|
6624
7038
|
const message = error instanceof Error ? error.message : String(error);
|
|
6625
7039
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|
|
6626
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
|
+
}
|
|
6627
7053
|
function turnViewSnapshot(value) {
|
|
6628
7054
|
if (!value || typeof value !== "object")
|
|
6629
7055
|
return undefined;
|
|
@@ -6937,7 +7363,7 @@ function isBotOwnedActiveTurn(record, remote) {
|
|
|
6937
7363
|
&& record.lastTurnId === remote.lastTurnId;
|
|
6938
7364
|
}
|
|
6939
7365
|
function isQueueIndependentCommand(command) {
|
|
6940
|
-
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))
|
|
6941
7367
|
return true;
|
|
6942
7368
|
if (command.type === "agent")
|
|
6943
7369
|
return command.agent === undefined;
|