From ba3bd59f2476545c72c5214c342c4fa7738a95bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 21 Sep 2026 12:21:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E7=AD=96=E5=88=92=20Agent=20?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E4=BA=8B=E4=BB=B6=E8=AE=A2=E9=98=85=EF=BC=88?= =?UTF-8?q?876529e66=20=E8=AF=AF=E5=88=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apps/ai-game-creator-shell/src/App.tsx 重新订阅 `design-agent-update`:本轮正文与工具提示回到 transient reply,`reasoningText` 回到 `planningV2Reasoning`,`view` 回到 applyDesignAgentViewAfterTransient;Rust 侧一直照旧发射这些事件,前端却不再监听 - 补回 designAgentEventSubscriptionReadyRef 的建/解函数与 designAgentEventSubscriptionResolveRef,让 `executeDesignAgentTurn` 等待监听器挂好再发起回合,开局事件不丢 - 新增 designAgentReasoningTurnRef:回合结束(designAgentTurnRef 清空)后仍认本轮 reasoning,最后一条思考不会被视图落盘前的空隙吃掉;切项目时随 resetChatState 清空 --- apps/ai-game-creator-shell/src/App.tsx | 121 +++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index cd6dae9eb..b5ac0dc9f 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -33,6 +33,7 @@ import type { ChatMessage, DesignAgentInput, DesignClarificationRequest, + DesignEvent, DesignView, InitLocalProjectResult, LauncherImportedAttachment, @@ -102,6 +103,10 @@ import { setAgcPluginProjectPath, startAvailableAgcPlugin, } from './services/pluginHost'; +import { + canSubscribeTauriEvents, + subscribeTauriEvent, +} from './services/tauriEventSubscription'; import type { HomeCreationType } from './view/home'; import { type ProjectAgentResultSummary, @@ -243,6 +248,20 @@ export function App({ const designAgentEventSubscriptionReadyRef = useRef | null>( null, ); + const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>( + null, + ); + /** + * 本轮策划回合的思考归属。 + * + * 与 `designAgentTurnRef` 分开:那个记录回合本身,回合结束就清空;这个只用来判定 + * 「这条 reasoning 事件是不是本轮的」,回合结束后仍保留,最后一条思考不会在视图 + * 落盘前的空隙里被丢掉。切项目时随其它聊天状态一起清空。 + */ + const designAgentReasoningTurnRef = useRef<{ + projectPath: string; + clientTurnId: string; + } | null>(null); // 项目开发工作台的普通项目固定使用 DirectProject,立项策划项目走策划聊天;两条 // 链路各自独立,不通过兼容分支互相承载。 const directProjectMode = !planningV2Active; @@ -419,6 +438,22 @@ export function App({ return designAgentEventSubscriptionReadyRef.current ?? Promise.resolve(); } + function createDesignAgentEventSubscriptionReady() { + if (!designAgentEventSubscriptionReadyRef.current) { + designAgentEventSubscriptionReadyRef.current = new Promise( + (resolve) => { + designAgentEventSubscriptionResolveRef.current = resolve; + }, + ); + } + return designAgentEventSubscriptionReadyRef.current; + } + + function resolveDesignAgentEventSubscriptionReady() { + designAgentEventSubscriptionResolveRef.current?.(); + designAgentEventSubscriptionResolveRef.current = null; + } + useEffect(() => { const timer = window.setInterval(() => { const target = planningV2TransientReplyTargetRef.current; @@ -442,6 +477,83 @@ export function App({ return () => window.clearInterval(timer); }, []); + /** + * 策划 Agent 的实时事件流:本轮流式正文、思考过程和回合中途的视图都靠它推给界面。 + * + * 订阅建立是异步的,而回合由一个 invoke 发起;`designAgentEventSubscriptionReady()` + * 让回合等监听器挂好再开始,避免开头几个事件丢掉。事件只认当前项目;有在跑的回合时 + * 还要认本轮 `clientTurnId`,迟到的上一轮事件不会画到这一轮上。 + */ + useEffect(() => { + const ready = createDesignAgentEventSubscriptionReady(); + if (!canSubscribeTauriEvents() || !planningV2Active) { + resolveDesignAgentEventSubscriptionReady(); + return () => { + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + } + let cleanup: (() => void) | null = null; + let disposed = false; + void subscribeTauriEvent('design-agent-update', (event) => { + const payload = event.payload; + const tracked = designAgentTurnRef.current; + if ( + payload.projectPath !== localProjectPathRef.current || + (tracked && payload.clientTurnId !== tracked.clientTurnId) + ) { + return; + } + if ( + (payload.kind === 'text' || payload.kind === 'tool') && + payload.text + ) { + setPlanningV2TransientReplyTarget(payload.text); + } + if (payload.reasoningText != null) { + const reasoningTurn = designAgentReasoningTurnRef.current; + if ( + reasoningTurn?.projectPath === payload.projectPath && + reasoningTurn.clientTurnId === payload.clientTurnId + ) { + setPlanningV2Reasoning(payload.reasoningText); + } + } + if (payload.view) { + applyDesignAgentViewAfterTransient( + payload.view, + payload.projectPath, + payload.clientTurnId, + ); + } + }) + .then((unlisten) => { + resolveDesignAgentEventSubscriptionReady(); + if (disposed) { + unlisten(); + return; + } + cleanup = unlisten; + }) + .catch(() => { + resolveDesignAgentEventSubscriptionReady(); + }); + return () => { + disposed = true; + cleanup?.(); + resolveDesignAgentEventSubscriptionReady(); + if (designAgentEventSubscriptionReadyRef.current === ready) { + designAgentEventSubscriptionReadyRef.current = null; + createDesignAgentEventSubscriptionReady(); + } + }; + // 订阅只跟着「是否走策划 Agent」这条链路;回调里的项目/回合判据都走 refs, + // 把它们写进依赖会在每轮回复时重订事件。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [planningV2Active]); + function applyPlanningV2CommandResult( result: PlanningSessionCommandResultV2, clientTurnId?: string, @@ -712,6 +824,10 @@ export function App({ projectPath: nextProjectPath, clientTurnId, }; + designAgentReasoningTurnRef.current = { + projectPath: nextProjectPath, + clientTurnId, + }; designAgentPendingViewRef.current = null; await designAgentEventSubscriptionReady(); setChatAgentBusy(true); @@ -1582,6 +1698,7 @@ export function App({ planningV2ActiveRef.current = planningStartMode; designAgentLaneRef.current = planningStartMode; designAgentTurnRef.current = null; + designAgentReasoningTurnRef.current = null; setDesignAgentView(null); } @@ -2689,6 +2806,10 @@ export function App({ projectPath: nextProjectPath, clientTurnId, }; + designAgentReasoningTurnRef.current = { + projectPath: nextProjectPath, + clientTurnId, + }; designAgentPendingViewRef.current = null; setPlanningV2TransientReplyTarget(''); setPlanningV2Reasoning('');