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