@keyou007/agent-bot 0.1.24-alpha.2 → 0.1.24-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +33 -5
- package/README.zh.md +32 -5
- package/dist/acp/AcpJsonRpcConnection.js +3 -1
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -1
- package/dist/acp/AcpSessionManager.js +0 -14
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/acp/acpTypes.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 -3
- 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 +497 -62
- 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 +466 -38
- 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 +12 -1
- package/src/acp/AcpJsonRpcConnection.ts +3 -1
- package/src/acp/AcpSessionManager.ts +0 -17
- package/src/acp/acpTypes.ts +0 -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 -3
- 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 +537 -59
- 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 +457 -27
- 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;
|
|
@@ -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,
|
|
@@ -5353,7 +5574,7 @@ export class ProxySessionController {
|
|
|
5353
5574
|
}
|
|
5354
5575
|
};
|
|
5355
5576
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5356
|
-
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model);
|
|
5577
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5357
5578
|
return session;
|
|
5358
5579
|
}
|
|
5359
5580
|
|
|
@@ -5424,7 +5645,7 @@ export class ProxySessionController {
|
|
|
5424
5645
|
await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
|
|
5425
5646
|
}
|
|
5426
5647
|
this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
|
|
5427
|
-
this.outbound.updateSessionModel(loaded.record.localSessionId, model);
|
|
5648
|
+
this.outbound.updateSessionModel(loaded.record.localSessionId, model, loaded.session.modelProvider);
|
|
5428
5649
|
await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
|
|
5429
5650
|
const effortMessage = nextEffort && nextEffort !== currentEffort
|
|
5430
5651
|
? `,思考强度已自动调整为 ${nextEffort}`
|
|
@@ -5818,10 +6039,12 @@ export class ProxySessionController {
|
|
|
5818
6039
|
const legacyTitle = legacy && record.agentName.toLowerCase() === legacy.agentName.toLowerCase()
|
|
5819
6040
|
? legacy.title
|
|
5820
6041
|
: undefined;
|
|
5821
|
-
|
|
5822
|
-
|
|
6042
|
+
const defaultFormat = isDefaultGroupNameFormat(nameFormat);
|
|
6043
|
+
let parsedTitle = defaultFormat
|
|
5823
6044
|
? legacyTitle ?? templateTitle
|
|
5824
|
-
: templateTitle ?? legacyTitle
|
|
6045
|
+
: templateTitle ?? legacyTitle;
|
|
6046
|
+
if (defaultFormat && isLegacyGroupPrefixOnly(event.afterName)) parsedTitle = undefined;
|
|
6047
|
+
const title = normalizeTaskTitle(parsedTitle) ?? normalizeTaskTitle(event.afterName);
|
|
5825
6048
|
if (!title || title === record.title) return;
|
|
5826
6049
|
|
|
5827
6050
|
const loaded = await this.loadSession(record);
|
|
@@ -5936,6 +6159,78 @@ export class ProxySessionController {
|
|
|
5936
6159
|
};
|
|
5937
6160
|
}
|
|
5938
6161
|
|
|
6162
|
+
async loadTurnDetails(turnId: string): Promise<TurnViewState | undefined> {
|
|
6163
|
+
const existing = this.turnDetailHydrations.get(turnId);
|
|
6164
|
+
if (existing) return existing;
|
|
6165
|
+
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
|
|
6166
|
+
if ((!snapshot || snapshot.previewJournal) && this.store.previews.has(turnId)) return this.store.previews.load(turnId);
|
|
6167
|
+
if (snapshot && !needsHistoricalDetails(snapshot)) return snapshot;
|
|
6168
|
+
const origin = !snapshot ? this.store.getTurnRuntimeOrigin(turnId) : undefined;
|
|
6169
|
+
if (!snapshot && (!origin || !this.runtimes.forAgent(origin.agentName).readRemoteTurn)) return undefined;
|
|
6170
|
+
const hydration = snapshot ? this.hydrateTurnDetails(snapshot) : this.loadUnrecordedTurnPreview(origin!);
|
|
6171
|
+
this.turnDetailHydrations.set(turnId, hydration);
|
|
6172
|
+
try {
|
|
6173
|
+
return await hydration;
|
|
6174
|
+
} finally {
|
|
6175
|
+
if (this.turnDetailHydrations.get(turnId) === hydration) this.turnDetailHydrations.delete(turnId);
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
|
|
6179
|
+
private async loadUnrecordedTurnPreview(origin: TurnRuntimeOriginRecord): Promise<TurnViewState> {
|
|
6180
|
+
const runtime = this.runtimes.forAgent(origin.agentName);
|
|
6181
|
+
if (!runtime.readRemoteTurn) throw new Error("此 Agent 不支持读取历史执行详情。");
|
|
6182
|
+
const details = await runtime.readRemoteTurn(origin.remoteSessionId, origin.turnId);
|
|
6183
|
+
if (details.turnId !== origin.turnId) throw new Error("Agent 返回的轮次与请求不一致,请重试。");
|
|
6184
|
+
let remote: RemoteSessionSummary | undefined;
|
|
6185
|
+
try {
|
|
6186
|
+
remote = await runtime.readRemoteSession?.(origin.remoteSessionId, "metadata");
|
|
6187
|
+
} catch (error) {
|
|
6188
|
+
this.logger.warn({ error, turnId: origin.turnId }, "Failed to read Preview task metadata; retaining full Turn details.");
|
|
6189
|
+
}
|
|
6190
|
+
// An Agent Bot turn may have arrived while the read-only request was in flight.
|
|
6191
|
+
const latest = turnViewSnapshot(this.store.getTurnSnapshot(origin.turnId));
|
|
6192
|
+
if (latest) return latest.previewJournal ? this.store.previews.load(origin.turnId) ?? latest : latest;
|
|
6193
|
+
const state = hydrateTurnViewState(createTurnViewState(
|
|
6194
|
+
origin.localSessionId, origin.turnId, details.startedAt ?? 0,
|
|
6195
|
+
remote?.title, undefined, remote?.cwd, undefined, this.agentLabel(origin.agentName),
|
|
6196
|
+
), details);
|
|
6197
|
+
// Viewing external work must not create a task, a chat binding or a pending final reply.
|
|
6198
|
+
this.store.previews.seed(state);
|
|
6199
|
+
this.store.previews.release(state.turnId);
|
|
6200
|
+
return state;
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
private async hydrateTurnDetails(snapshot: TurnViewState): Promise<TurnViewState | undefined> {
|
|
6204
|
+
let updated: TurnViewState;
|
|
6205
|
+
try {
|
|
6206
|
+
const origin = this.store.getTurnRuntimeOrigin(snapshot.turnId);
|
|
6207
|
+
if (!origin) throw new Error("未保存本轮的原始 Agent 会话信息,无法读取历史执行详情。");
|
|
6208
|
+
const runtime = this.runtimes.forAgent(origin.agentName);
|
|
6209
|
+
if (!runtime.readRemoteTurn) throw new Error("此 Agent 不支持读取历史执行详情。");
|
|
6210
|
+
const details = await runtime.readRemoteTurn(origin.remoteSessionId, snapshot.turnId);
|
|
6211
|
+
if (details.turnId !== snapshot.turnId) throw new Error("Agent 返回的轮次与请求不一致,请重试。");
|
|
6212
|
+
const session = this.store.getSession(snapshot.sessionId);
|
|
6213
|
+
updated = hydrateTurnViewState({
|
|
6214
|
+
...snapshot,
|
|
6215
|
+
projectCwd: snapshot.projectCwd ?? session?.cwd,
|
|
6216
|
+
agentLabel: snapshot.agentLabel ?? this.agentLabel(origin.agentName),
|
|
6217
|
+
}, details);
|
|
6218
|
+
} catch (error) {
|
|
6219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6220
|
+
this.logger.warn({ error, turnId: snapshot.turnId }, "Failed to load historical Turn details.");
|
|
6221
|
+
updated = { ...snapshot, historyDetail: "summary", historyDetailError: message };
|
|
6222
|
+
}
|
|
6223
|
+
const latest = turnViewSnapshot(this.store.getTurnSnapshot(snapshot.turnId));
|
|
6224
|
+
if (!latest || !needsHistoricalDetails(latest)) return latest;
|
|
6225
|
+
if (updated.historyDetail === "full") {
|
|
6226
|
+
this.store.previews.seed(updated);
|
|
6227
|
+
this.store.previews.flush(updated.turnId, true);
|
|
6228
|
+
updated = { ...updated, previewJournal: true };
|
|
6229
|
+
}
|
|
6230
|
+
this.store.saveTurnSnapshot(snapshot.turnId, snapshot.sessionId, updated);
|
|
6231
|
+
return updated;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
5939
6234
|
private async openTurnDetails(contextKey: string, reference: string): Promise<void> {
|
|
5940
6235
|
const current = this.requireCurrentSession(contextKey);
|
|
5941
6236
|
let turnId: string | undefined;
|
|
@@ -5974,6 +6269,7 @@ export class ProxySessionController {
|
|
|
5974
6269
|
));
|
|
5975
6270
|
return;
|
|
5976
6271
|
}
|
|
6272
|
+
await this.loadTurnDetails(turnId);
|
|
5977
6273
|
await this.outbound.showDetails(contextKey, turnId);
|
|
5978
6274
|
}
|
|
5979
6275
|
|
|
@@ -5981,7 +6277,18 @@ export class ProxySessionController {
|
|
|
5981
6277
|
contextKey: string,
|
|
5982
6278
|
options: ResetHistoryCardOptions = {},
|
|
5983
6279
|
): Promise<void> {
|
|
5984
|
-
const
|
|
6280
|
+
const taskId = options.sessionReference
|
|
6281
|
+
? this.resolveSessionReference(contextKey, options.sessionReference)
|
|
6282
|
+
: undefined;
|
|
6283
|
+
const current = taskId
|
|
6284
|
+
? this.store.getSession(taskId) ?? this.findStoredSessionByReference(taskId)
|
|
6285
|
+
: this.requireCurrentSession(contextKey);
|
|
6286
|
+
if (!current && taskId) {
|
|
6287
|
+
await this.openRemoteTurnHistory(contextKey, taskId, options);
|
|
6288
|
+
return;
|
|
6289
|
+
}
|
|
6290
|
+
if (!current || current.status === "closed") throw new Error(`找不到任务:${taskId}`);
|
|
6291
|
+
const readOnly = this.currentSession(contextKey)?.localSessionId !== current.localSessionId;
|
|
5985
6292
|
if (options.expectedSessionId && current.localSessionId !== options.expectedSessionId) {
|
|
5986
6293
|
throw new Error("这张历史轮次卡片不属于当前任务。请先切换回对应任务,再重新发送 /turns。");
|
|
5987
6294
|
}
|
|
@@ -6041,7 +6348,7 @@ export class ProxySessionController {
|
|
|
6041
6348
|
current: isCurrent,
|
|
6042
6349
|
running: isRunning,
|
|
6043
6350
|
resetting: isResetting,
|
|
6044
|
-
actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
|
|
6351
|
+
actions: readOnly || isCurrent || isRunning || resettingTurnId ? undefined : [{
|
|
6045
6352
|
text: "Reset",
|
|
6046
6353
|
value: {
|
|
6047
6354
|
action: "turn_reset",
|
|
@@ -6058,8 +6365,8 @@ export class ProxySessionController {
|
|
|
6058
6365
|
...(page > 0 ? [{
|
|
6059
6366
|
text: "Previous",
|
|
6060
6367
|
value: {
|
|
6061
|
-
action: "turn_reset_page",
|
|
6062
|
-
sessionId: current.localSessionId,
|
|
6368
|
+
action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
|
|
6369
|
+
sessionId: options.sessionReference ?? current.localSessionId,
|
|
6063
6370
|
contextKey,
|
|
6064
6371
|
page: String(page - 1),
|
|
6065
6372
|
},
|
|
@@ -6067,14 +6374,15 @@ export class ProxySessionController {
|
|
|
6067
6374
|
...(page < totalPages - 1 ? [{
|
|
6068
6375
|
text: "Next",
|
|
6069
6376
|
value: {
|
|
6070
|
-
action: "turn_reset_page",
|
|
6071
|
-
sessionId: current.localSessionId,
|
|
6377
|
+
action: options.sessionReference ? "session_turns_page" : "turn_reset_page",
|
|
6378
|
+
sessionId: options.sessionReference ?? current.localSessionId,
|
|
6072
6379
|
contextKey,
|
|
6073
6380
|
page: String(page + 1),
|
|
6074
6381
|
},
|
|
6075
6382
|
}] : []),
|
|
6076
6383
|
];
|
|
6077
6384
|
const card = this.cardRenderer.renderResetHistoryCard({
|
|
6385
|
+
...(options.sessionReference ? { taskTitle: current.title ?? current.remoteSessionId, readOnly } : {}),
|
|
6078
6386
|
entries,
|
|
6079
6387
|
footerLines: [
|
|
6080
6388
|
hasMoreHistory
|
|
@@ -6093,6 +6401,78 @@ export class ProxySessionController {
|
|
|
6093
6401
|
}
|
|
6094
6402
|
}
|
|
6095
6403
|
|
|
6404
|
+
private async openRemoteTurnHistory(
|
|
6405
|
+
contextKey: string,
|
|
6406
|
+
reference: string,
|
|
6407
|
+
options: ResetHistoryCardOptions,
|
|
6408
|
+
): Promise<void> {
|
|
6409
|
+
const { runtime, remote } = await this.resolveRemoteCodexSession(reference);
|
|
6410
|
+
if (!runtime.listRemoteTurnSummaries) throw new Error("当前 Agent 不支持读取历史轮次。");
|
|
6411
|
+
const page = Math.max(0, Math.trunc(options.page ?? 0));
|
|
6412
|
+
const running = remote.lastTurnStatus === "inProgress" && remote.lastTurnId;
|
|
6413
|
+
let cursor = options.cursor;
|
|
6414
|
+
// Next carries its cursor; only backwards navigation needs to locate a page again.
|
|
6415
|
+
if (!cursor && page > 0) {
|
|
6416
|
+
const seen = new Set<string>();
|
|
6417
|
+
for (let index = 0; index < page; index += 1) {
|
|
6418
|
+
const previous = await runtime.listRemoteTurnSummaries(remote.id, {
|
|
6419
|
+
cursor,
|
|
6420
|
+
limit: RESET_HISTORY_PAGE_SIZE - (index === 0 && running ? 1 : 0),
|
|
6421
|
+
});
|
|
6422
|
+
if (!previous.nextCursor || seen.has(previous.nextCursor)) {
|
|
6423
|
+
throw new Error("任务轮次已变化,请从 /sessions 重新打开 Turns。");
|
|
6424
|
+
}
|
|
6425
|
+
cursor = previous.nextCursor;
|
|
6426
|
+
seen.add(cursor);
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
const result = await runtime.listRemoteTurnSummaries(remote.id, {
|
|
6430
|
+
cursor,
|
|
6431
|
+
limit: RESET_HISTORY_PAGE_SIZE - (page === 0 && running ? 1 : 0),
|
|
6432
|
+
});
|
|
6433
|
+
if (result.nextCursor && result.nextCursor === cursor) {
|
|
6434
|
+
throw new Error("历史轮次分页游标无效,请稍后重试。");
|
|
6435
|
+
}
|
|
6436
|
+
const turns = [
|
|
6437
|
+
...(page === 0 && running ? [{ id: running, prompt: remote.lastUserPrompt }] : []),
|
|
6438
|
+
...result.turns,
|
|
6439
|
+
];
|
|
6440
|
+
const entries: ResetHistoryCardEntry[] = turns.map((turn, index) => ({
|
|
6441
|
+
sequence: page * RESET_HISTORY_PAGE_SIZE + index + 1,
|
|
6442
|
+
graphNodeLine: "●",
|
|
6443
|
+
graphConnectorLine: index < turns.length - 1 ? "│" : undefined,
|
|
6444
|
+
lines: [cardText(truncateText((turn.prompt ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100))],
|
|
6445
|
+
timestamp: formatResetTurnTime(turn.completedAt ?? turn.startedAt),
|
|
6446
|
+
current: remote.lastTurnStatus === "completed" && remote.lastTurnId === turn.id,
|
|
6447
|
+
running: running === turn.id,
|
|
6448
|
+
}));
|
|
6449
|
+
const pageAction = (text: string, targetPage: number, targetCursor?: string): TaskListCardAction => ({
|
|
6450
|
+
text,
|
|
6451
|
+
value: {
|
|
6452
|
+
action: "session_turns_page",
|
|
6453
|
+
sessionId: reference,
|
|
6454
|
+
contextKey,
|
|
6455
|
+
page: String(targetPage),
|
|
6456
|
+
...(targetCursor ? { cursor: targetCursor } : {}),
|
|
6457
|
+
},
|
|
6458
|
+
});
|
|
6459
|
+
const card = this.cardRenderer.renderResetHistoryCard({
|
|
6460
|
+
taskTitle: remote.title ?? remote.preview ?? remote.id,
|
|
6461
|
+
readOnly: true,
|
|
6462
|
+
entries,
|
|
6463
|
+
footerLines: [`第 ${page + 1} 页 · 本页 ${entries.length} 个 turn`],
|
|
6464
|
+
pageActions: [
|
|
6465
|
+
...(page > 0 ? [pageAction("Previous", page - 1)] : []),
|
|
6466
|
+
...(result.nextCursor ? [pageAction("Next", page + 1, result.nextCursor)] : []),
|
|
6467
|
+
],
|
|
6468
|
+
});
|
|
6469
|
+
if (options.updateMessageId) {
|
|
6470
|
+
await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
6471
|
+
} else {
|
|
6472
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6096
6476
|
private async openDirectoryBrowser(
|
|
6097
6477
|
contextKey: string,
|
|
6098
6478
|
requestedDirectory?: string,
|
|
@@ -6388,6 +6768,16 @@ export class ProxySessionController {
|
|
|
6388
6768
|
contextKey,
|
|
6389
6769
|
},
|
|
6390
6770
|
})];
|
|
6771
|
+
actions.push(bindCardAction({
|
|
6772
|
+
text: "SwitchGroup",
|
|
6773
|
+
value: {
|
|
6774
|
+
action: "session_switch_group",
|
|
6775
|
+
sessionId: entry.reference,
|
|
6776
|
+
...(searchTerm ? { searchTerm } : {}),
|
|
6777
|
+
page: String(page),
|
|
6778
|
+
contextKey,
|
|
6779
|
+
},
|
|
6780
|
+
}));
|
|
6391
6781
|
const projectActions: TaskListCardAction[] = [bindCardAction({
|
|
6392
6782
|
text: "New",
|
|
6393
6783
|
value: {
|
|
@@ -6435,6 +6825,14 @@ export class ProxySessionController {
|
|
|
6435
6825
|
contextKey,
|
|
6436
6826
|
},
|
|
6437
6827
|
}));
|
|
6828
|
+
actions.push(bindCardAction({
|
|
6829
|
+
text: "Turns",
|
|
6830
|
+
value: {
|
|
6831
|
+
action: "session_turns",
|
|
6832
|
+
sessionId: entry.reference,
|
|
6833
|
+
contextKey,
|
|
6834
|
+
},
|
|
6835
|
+
}));
|
|
6438
6836
|
actions.push(bindCardAction({
|
|
6439
6837
|
text: "Archive",
|
|
6440
6838
|
value: {
|
|
@@ -6652,6 +7050,7 @@ export class ProxySessionController {
|
|
|
6652
7050
|
existing.cwd,
|
|
6653
7051
|
this.agentLabel(existing.agentName),
|
|
6654
7052
|
existing.model,
|
|
7053
|
+
existing.modelProvider,
|
|
6655
7054
|
);
|
|
6656
7055
|
await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
|
|
6657
7056
|
return;
|
|
@@ -6686,6 +7085,7 @@ export class ProxySessionController {
|
|
|
6686
7085
|
remote.cwd,
|
|
6687
7086
|
this.agentLabel(agentName),
|
|
6688
7087
|
remote.model,
|
|
7088
|
+
remote.modelProvider,
|
|
6689
7089
|
);
|
|
6690
7090
|
await this.outbound.sendText(
|
|
6691
7091
|
contextKey,
|
|
@@ -7116,12 +7516,30 @@ export class ProxySessionController {
|
|
|
7116
7516
|
const actions: TaskListCardAction[] = [
|
|
7117
7517
|
statusRefreshAction(taskId, contextKey),
|
|
7118
7518
|
...taskActions,
|
|
7519
|
+
...this.statusPreviewActions(contextKey, activeTurnId ?? current?.lastTurnId, current?.remoteSessionId ? {
|
|
7520
|
+
agentName: current.agentName, remoteSessionId: current.remoteSessionId, localSessionId: current.localSessionId,
|
|
7521
|
+
} : undefined),
|
|
7119
7522
|
];
|
|
7120
7523
|
const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
|
|
7121
7524
|
if (options.updateMessageId) await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
7122
7525
|
else await this.outbound.sendInteractiveCard(contextKey, card);
|
|
7123
7526
|
}
|
|
7124
7527
|
|
|
7528
|
+
private statusPreviewActions(
|
|
7529
|
+
contextKey: string,
|
|
7530
|
+
turnId?: string,
|
|
7531
|
+
source?: { agentName: string; remoteSessionId: string; localSessionId?: string },
|
|
7532
|
+
): TaskListCardAction[] {
|
|
7533
|
+
if (!turnId || turnId.startsWith("pending_")) return [];
|
|
7534
|
+
if (source && this.runtimes.forAgent(source.agentName).readRemoteTurn) {
|
|
7535
|
+
this.store.saveTurnRuntimeOrigin(turnId,
|
|
7536
|
+
source.localSessionId ?? remoteSessionReference(source.agentName, source.remoteSessionId),
|
|
7537
|
+
source.agentName, source.remoteSessionId);
|
|
7538
|
+
}
|
|
7539
|
+
const url = this.outbound.getTurnPreviewUrl(contextKey, turnId);
|
|
7540
|
+
return url ? [{ text: "Preview", value: {}, url }] : [];
|
|
7541
|
+
}
|
|
7542
|
+
|
|
7125
7543
|
private async statusForCodexTask(
|
|
7126
7544
|
contextKey: string,
|
|
7127
7545
|
reference: string,
|
|
@@ -7178,6 +7596,7 @@ export class ProxySessionController {
|
|
|
7178
7596
|
actionReference,
|
|
7179
7597
|
contextKey,
|
|
7180
7598
|
),
|
|
7599
|
+
...this.statusPreviewActions(contextKey, remote.lastTurnId, { agentName, remoteSessionId: remote.id }),
|
|
7181
7600
|
];
|
|
7182
7601
|
const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
|
|
7183
7602
|
if (options.updateMessageId) await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
|
|
@@ -7267,7 +7686,7 @@ export class ProxySessionController {
|
|
|
7267
7686
|
await this.outbound.sendText(
|
|
7268
7687
|
contextKey,
|
|
7269
7688
|
enabled
|
|
7270
|
-
? "已开启当前群的静音模式。之后只有 @
|
|
7689
|
+
? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;普通提问会补充当前群/话题近期文字和图片(最多 20 条、6 张图片)。发送 @机器人 /mute off 可恢复自动响应并关闭自动补充。"
|
|
7271
7690
|
: "已关闭当前群的静音模式。机器人将恢复自动响应群消息。",
|
|
7272
7691
|
);
|
|
7273
7692
|
}
|
|
@@ -8202,6 +8621,17 @@ function isUnmaterializedCodexThreadError(error: unknown): boolean {
|
|
|
8202
8621
|
return /thread\/read failed:[^\n]*(?:not materialized|not loaded|includeTurns is unavailable before first user message)/i.test(message);
|
|
8203
8622
|
}
|
|
8204
8623
|
|
|
8624
|
+
function needsHistoricalDetails(state: TurnViewState): boolean {
|
|
8625
|
+
if (!isTerminalTurnViewStatus(state.status) || state.historyDetail === "full") return false;
|
|
8626
|
+
if (state.historyDetail === "summary") return true;
|
|
8627
|
+
return state.status === "completed" && state.finalResponse === undefined && !state.assistantText
|
|
8628
|
+
&& !state.error && !state.progressText && !state.approval && !state.activeTool
|
|
8629
|
+
&& !state.activities?.length && !state.plan?.length && !state.reasoningItems?.length
|
|
8630
|
+
&& !state.completedTools?.length && !state.failedTools?.length && !state.fileSummary?.length
|
|
8631
|
+
&& !state.totalToolCount && !state.totalTokens
|
|
8632
|
+
&& state.fullToolOutputs === undefined && state.fullToolErrors === undefined && state.toolStatuses === undefined;
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8205
8635
|
function turnViewSnapshot(value: unknown): TurnViewState | undefined {
|
|
8206
8636
|
if (!value || typeof value !== "object") return undefined;
|
|
8207
8637
|
const candidate = value as Partial<TurnViewState>;
|
|
@@ -8528,7 +8958,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
8528
8958
|
}
|
|
8529
8959
|
|
|
8530
8960
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
8531
|
-
if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
8961
|
+
if (["archive", "dismiss", "stop", "status", "restart", "update", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
8532
8962
|
if (command.type === "agent") return command.agent === undefined;
|
|
8533
8963
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
8534
8964
|
return false;
|