From ec2180f281ad6f8531d82f2fdf9027fce352eacb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 15 Sep 2026 06:38:29 +0000 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AD=96=E5=88=92V2=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E7=8A=B6=E6=80=81=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 App 内策划 V2 会话、GDD 同步与审批状态 删除策划 V2 类型转换模块和退役 GDD 状态类型 保留现役设计 Agent 会话恢复与项目运行时入口 --- apps/ai-game-creator-shell/src/App.tsx | 592 +---------------- apps/ai-game-creator-shell/src/app/types.ts | 164 ----- .../project-workspace/planningSessionV2.ts | 614 ------------------ 3 files changed, 24 insertions(+), 1346 deletions(-) delete mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index ec6f00905..80d6a1a0d 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -90,8 +90,6 @@ import type { OpenCanvasProjectResult, PendingCommand, PendingUiConfirmation, - PlanGddDecisionAction, - PlanGddStateViewV1, ProjectPermissionPolicy, ProjectPermissionPolicyView, SyncCanvasProjectAssetsResult, @@ -230,14 +228,6 @@ import { parseRememberInput, } from './features/project-workspace/memoryCommands'; import { pendingCommandDetail } from './features/project-workspace/pendingCommandPresentation'; -import { - type PlanningApprovalCommandResultV2, - planningMessagesToChatMessages, - planningResultDisplayText, - type PlanningSessionCommandResultV2, - planningSessionToPlanGddState, - planningSessionToRuntime, -} from './features/project-workspace/planningSessionV2'; import { isAgentTraceFilePath, isProjectPolicyConfirmableCommandId, @@ -584,11 +574,8 @@ export function App({ onMakeGameFromApprovedGdd, }: AppProps = {}) { const { setTitle: setWindowTitle } = useWindowChrome(); - const [planningV2Active, setPlanningV2Active] = useState(false); - const planningV2ActiveRef = useRef(false); const [designAgentActive, setDesignAgentActive] = useState(planningStartMode); const designAgentActiveRef = useRef(planningStartMode); - planningV2ActiveRef.current = planningV2Active; designAgentActiveRef.current = designAgentActive; const [designAgentView, setDesignAgentView] = useState( null, @@ -603,17 +590,13 @@ export function App({ const designAgentEventSubscriptionResolveRef = useRef<(() => void) | null>( null, ); - // 做方案入口独立成链:立项策划需要委派、澄清 pending 与 GDD 审批,这些只存在于 - // Supervisor Runtime;direct-codex 是单回合「生成→试玩→修」循环,没有对应机制。 - // 因此策划入口不走产品默认的 direct-codex,做游戏与做素材保持 master 的新默认。 const directCodexProductRuntime = DIRECT_CODEX_PRODUCT_RUNTIME && projectSupervisorOnly && !supervisorChatOnly && !planningStartMode && - !planningV2Active && !designAgentActive; - const planningLaneActive = planningV2Active || designAgentActive; + const planningLaneActive = designAgentActive; const [devMode] = useState(() => projectSupervisorOnly ? false : isDeveloperMode(), ); @@ -815,21 +798,6 @@ export function App({ useState(null); const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] = useState(''); - const [planGddState, setPlanGddState] = useState( - null, - ); - const planGddStateRef = useRef(null); - planGddStateRef.current = planGddState; - const [planningV2Session, setPlanningV2Session] = - useState(null); - const planningV2SessionRef = useRef( - null, - ); - planningV2SessionRef.current = planningV2Session; - const [planningV2TransientReply, setPlanningV2TransientReplyVisible] = - useState(''); - const planningV2TransientReplyTargetRef = useRef(''); - const planningV2VisibleReplyRef = useRef(''); const [designAgentTransientReply, setDesignAgentTransientReplyVisible] = useState(''); const designAgentTransientReplyTargetRef = useRef(''); @@ -845,32 +813,6 @@ export function App({ clientTurnId: string; text: string; } | null>(null); - const planningV2TurnRef = useRef<{ - projectPath: string; - clientTurnId: string; - } | null>(null); - const planGddHydrateSequenceRef = useRef(0); - const [planGddHydrateBusy, setPlanGddHydrateBusy] = useState(false); - const [planGddDecisionBusy, setPlanGddDecisionBusy] = useState(false); - const [planGddError, setPlanGddError] = useState(null); - const planGddDecisionResponseIdsRef = useRef( - new Map< - string, - { - action: PlanGddDecisionAction; - comment: string | null; - responseId: string; - } - >(), - ); - - function setPlanningV2TransientReplyTarget(next: string) { - planningV2TransientReplyTargetRef.current = next; - if (!next) { - planningV2VisibleReplyRef.current = ''; - setPlanningV2TransientReplyVisible(''); - } - } function setDesignAgentTransientReplyTarget(next: string) { designAgentTransientReplyTargetRef.current = next; @@ -902,29 +844,6 @@ export function App({ designAgentEventSubscriptionResolveRef.current = null; } - useEffect(() => { - const timer = window.setInterval(() => { - const target = planningV2TransientReplyTargetRef.current; - setPlanningV2TransientReplyVisible((current) => { - if (!target) { - planningV2VisibleReplyRef.current = ''; - return ''; - } - const prefix = target.startsWith(current) ? current : ''; - if (prefix === target) { - planningV2VisibleReplyRef.current = target; - return target; - } - const remaining = target.length - prefix.length; - const step = remaining > 160 ? 4 : remaining > 48 ? 2 : 1; - const next = target.slice(0, prefix.length + step); - planningV2VisibleReplyRef.current = next; - return next; - }); - }, 50); - return () => window.clearInterval(timer); - }, []); - useEffect(() => { const timer = window.setInterval(() => { const target = designAgentTransientReplyTargetRef.current; @@ -948,117 +867,6 @@ export function App({ return () => window.clearInterval(timer); }, []); - function applyPlanningV2CommandResult( - result: PlanningSessionCommandResultV2, - clientTurnId?: string, - ) { - planningV2ActiveRef.current = true; - setPlanningV2Active(true); - designAgentActiveRef.current = false; - setDesignAgentActive(false); - planningV2SessionRef.current = result; - setPlanningV2Session(result); - projectSupervisorSessionIdRef.current = result.session.sessionId; - setProjectSupervisorSessionId(result.session.sessionId); - setPlanGddState( - planningSessionToPlanGddState(result.session, result.currentArtifact), - ); - updateProjectSupervisorRuntime(planningSessionToRuntime(result.session)); - const resultError = - result.result?.kind === 'error' - ? (() => { - const payload = result.result.payload; - if ( - payload && - typeof payload === 'object' && - typeof (payload as { summary?: unknown }).summary === 'string' - ) { - return (payload as { summary: string }).summary.trim(); - } - return ''; - })() - : ''; - const sessionError = result.session.lastError?.summary ?? resultError; - setProjectSupervisorRuntimeError(sessionError); - if (result.conversation) { - const conversationMessages = planningMessagesToChatMessages( - result.conversation, - ); - setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); - setMessages(conversationMessages); - savedConversationProjectPathRef.current = localProjectPathRef.current; - savedConversationCountRef.current = conversationMessages.length; - latestMessagesRef.current = conversationMessages; - } else if (result.result && clientTurnId) { - const displayText = planningResultDisplayText( - result.result, - result.currentArtifact, - ); - if (displayText.trim()) { - const messageId = `planning-v2:${clientTurnId}:assistant`; - setMessages((current) => { - const existingIndex = current.findIndex( - (message) => message.messageId === messageId, - ); - const nextMessage: ChatMessage = { - role: 'assistant', - text: displayText, - runtimeOwned: true, - messageId, - updatedAt: Date.now(), - }; - if (existingIndex < 0) { - return [...current, nextMessage]; - } - return current.map((message, index) => - index === existingIndex ? nextMessage : message, - ); - }); - } - } - } - - async function hydratePlanningV2Session( - nextProjectPath: string, - requestSequence: number, - ) { - const invoke = resolveTauriInvoke(); - if (!invoke || !nextProjectPath.trim()) { - return null; - } - const sessionId = planningV2SessionRef.current?.session.sessionId; - const result = await invoke( - 'hydrate_planning_session_v2', - { - projectPath: nextProjectPath, - ...(sessionId ? { sessionId } : {}), - }, - ); - if ( - requestSequence !== planGddHydrateSequenceRef.current || - localProjectPathRef.current !== nextProjectPath - ) { - return null; - } - if (!result) { - planningV2SessionRef.current = null; - setPlanningV2Session(null); - setPlanGddState(null); - if (planningStartMode) { - planningV2ActiveRef.current = false; - setPlanningV2Active(false); - designAgentActiveRef.current = true; - setDesignAgentActive(true); - } - return null; - } - designAgentActiveRef.current = false; - setDesignAgentActive(false); - setDesignAgentView(null); - applyPlanningV2CommandResult(result); - return result; - } - function designMessagesToChat(view: DesignView): ChatMessage[] { const reasoningByMessageId = new Map(); for (const entry of view.reasoningEntries ?? []) { @@ -1085,7 +893,7 @@ export function App({ designAgentActiveRef.current = true; setDesignAgentActive(true); setDesignAgentView(view); - setPlanGddError(view.session.lastError); + setProjectSupervisorRuntimeError(view.session.lastError ?? ''); setChatAgentBusy(view.running); const conversation = designMessagesToChat(view); setMessages(conversation); @@ -1218,7 +1026,6 @@ export function App({ requestRuntimeConfigOpen(); } setProjectSupervisorRuntimeError(message); - setPlanGddError(message); } finally { if (!designAgentPendingViewRef.current) { designAgentTurnRef.current = null; @@ -1228,154 +1035,6 @@ export function App({ } } - const hydratePlanGddState = useCallback( - async (nextProjectPath?: string) => { - const targetProjectPath = - nextProjectPath?.trim() || localProjectPathRef.current || projectPath; - const invoke = resolveTauriInvoke(); - if (!invoke || !targetProjectPath.trim()) { - setPlanGddState(null); - return; - } - const requestSequence = ++planGddHydrateSequenceRef.current; - setPlanGddHydrateBusy(true); - setPlanGddError(null); - try { - await hydratePlanningV2Session(targetProjectPath, requestSequence); - } catch (error) { - // 项目写锁争用是瞬时的:后端已经等过一个短窗口,仍然没抢到只说明此刻 - // 运行时正在写盘。这条 effect 每次监工状态变化都会再跑一次,下一拍就能 - // 拿到,所以保留上一份状态、不动错误位——否则一次刚落盘成功的审批会在 - // 卡里显示成失败(decide 成功后紧跟的这次 hydrate 正是最容易撞上的时刻)。 - const transientContention = - String(error).includes('项目正在被其他写操作占用:'); - if ( - !transientContention && - requestSequence === planGddHydrateSequenceRef.current && - localProjectPathRef.current === targetProjectPath - ) { - setPlanGddError(String(error)); - } - } finally { - if ( - requestSequence === planGddHydrateSequenceRef.current && - localProjectPathRef.current === targetProjectPath - ) { - setPlanGddHydrateBusy(false); - } - } - }, - // V2 helpers intentionally read current refs/setters; keeping this callback - // stable prevents hydrate effects from running on every render. - // eslint-disable-next-line react-hooks/exhaustive-deps - [projectPath], - ); - - const decidePlanGdd = useCallback( - async (action: PlanGddDecisionAction, comment: string | null) => { - const current = planGddStateRef.current; - const pending = current?.pendingApproval; - const targetProjectPath = - resolveChatProjectPath(localProject) ?? projectPath; - const invoke = resolveTauriInvoke(); - if (!pending || !current?.displayGdd || !invoke || !targetProjectPath) { - throw new Error('当前没有可提交的 GDD 审批决定'); - } - // 方案 §13.2:busy、超时与网络重试复用同一 responseId,但用户改变 action 或 - // comment 后必须换新的。旧键只含 `approvalRequestId:action`,改写修改意见时会 - // 带着旧 responseId 提交,命中后端「同 responseId 的审批意图不一致」硬错误。 - // 判据方向是宁可多换不可少换:多换的最坏后果是 receipt 已存在时把 replayed 降级 - // 成 already-decided,两者都是 Ok;少换是硬错误。 - const previousDecision = planGddDecisionResponseIdsRef.current.get( - pending.approvalRequestId, - ); - const responseId = - previousDecision && - previousDecision.action === action && - previousDecision.comment === comment - ? previousDecision.responseId - : `gdd-response-${crypto.randomUUID()}`; - planGddDecisionResponseIdsRef.current.set(pending.approvalRequestId, { - action, - comment, - responseId, - }); - setPlanGddDecisionBusy(true); - setPlanGddError(null); - try { - const planningSessionId = current.session?.sessionId; - if (!planningSessionId) { - throw new Error('Planning V2 Session 不存在'); - } - const approval = await invoke( - 'decide_planning_artifact_v2', - { - projectPath: targetProjectPath, - sessionId: planningSessionId, - artifactId: pending.gddRef.gddId, - version: pending.gddRef.version, - fingerprint: pending.gddRef.fingerprint, - decisionId: responseId, - action, - comment, - }, - ); - applyPlanningV2CommandResult({ - session: approval.session, - result: null, - currentArtifact: approval.currentArtifact, - replayed: approval.replayed, - }); - setPlanGddError(null); - if (action === 'revise' && comment?.trim()) { - await executePlanningV2Turn( - targetProjectPath, - `审批:revise ${comment.trim()}`, - `planning-v2-revision-${crypto.randomUUID()}`, - ); - } - } catch (error) { - // 方案 §18.3 要求 decision 返回后以 hydrate 对权威文件的重验为准,失败分支同样 - // 适用:不重灌就会让卡片停在已失效的 pending 身份上,三个决定按钮仍可点,且 - // `recoveryPending` 永远翻不成真、「重试同步」入口不渲染,卡内没有出路。 - // 两句顺序不能反——`hydratePlanGddState` 入口会 `setPlanGddError(null)`, - // 先写错误再 hydrate 等于把这条错误擦掉。它自身从不抛出,不需要再包一层。 - await hydratePlanGddState(targetProjectPath); - setPlanGddError(String(error)); - throw error; - } finally { - setPlanGddDecisionBusy(false); - } - }, - // V2 decision handling also reads the current session through refs. - // eslint-disable-next-line react-hooks/exhaustive-deps - [hydratePlanGddState, localProject, projectPath], - ); - - useEffect(() => { - const targetProjectPath = localProject?.projectPath; - if (!targetProjectPath) { - setPlanGddState(null); - setPlanGddError(null); - return; - } - void hydratePlanGddState(targetProjectPath); - }, [hydratePlanGddState, localProject?.projectPath]); - - useEffect(() => { - const hydrateOnResume = () => { - if (document.visibilityState === 'hidden' || !localProject?.projectPath) { - return; - } - void hydratePlanGddState(localProject.projectPath); - }; - window.addEventListener('focus', hydrateOnResume); - document.addEventListener('visibilitychange', hydrateOnResume); - return () => { - window.removeEventListener('focus', hydrateOnResume); - document.removeEventListener('visibilitychange', hydrateOnResume); - }; - }, [hydratePlanGddState, localProject?.projectPath]); const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] = useState(null); const chatInputRef = useRef(null); @@ -1672,21 +1331,15 @@ export function App({ projectSupervisorRuntimeRef.current = null; projectSupervisorExpectedRunIdRef.current = null; projectSupervisorResponseStreamRef.current = null; - planningV2SessionRef.current = null; - planningV2TurnRef.current = null; projectSupervisorRuntimeSyncingRef.current.clear(); setProjectSupervisorSessionId(null); setProjectSupervisorRuntime(null); setProjectSupervisorExpectedRunId(null); setProjectSupervisorResponseStream(null); setProjectSupervisorRuntimeError(''); - setPlanningV2Session(null); - setPlanningV2TransientReplyTarget(''); designAgentPendingViewRef.current = null; designAgentReasoningTurnRef.current = null; setDesignAgentReasoning(''); - setPlanningV2Active(false); - planningV2ActiveRef.current = false; setDesignAgentActive(planningStartMode); designAgentActiveRef.current = planningStartMode; designAgentTurnRef.current = null; @@ -3328,11 +2981,7 @@ export function App({ const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1; projectSupervisorHistoryLoadVersionRef.current = loadVersion; try { - // V2 projects do not have a Supervisor run or legacy conversation. Probe the - // V2 authority first; a missing V2 session returns null and preserves the - // existing open-project behavior for older projects. - let planningV2: PlanningSessionCommandResultV2 | null = null; - if (projectSupervisorOnly || planningStartMode) { + if (planningStartMode) { try { const design = await hydrateDesignAgentSession(nextProjectPath); if (design) { @@ -3349,46 +2998,9 @@ export function App({ ); return; } - } catch (error) { - if (planningStartMode) { - setPlanGddError( - `策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`, - ); - } - } - try { - planningV2 = await invoke( - 'hydrate_planning_session_v2', - { projectPath: nextProjectPath }, - ); - } catch (error) { - if (planningStartMode) { - throw error; - } - // Older projects/binaries may not expose V2 yet; continue with the - // legacy read path unless the caller explicitly entered planning V2. - } - } - if (planningStartMode || (projectSupervisorOnly && planningV2)) { - if ( - projectSupervisorHistoryLoadVersionRef.current !== loadVersion || - localProjectPathRef.current !== nextProjectPath - ) { - return; - } - planningV2ActiveRef.current = Boolean(planningV2); - setPlanningV2Active(Boolean(planningV2)); - if (planningV2) { - designAgentActiveRef.current = false; - setDesignAgentActive(false); - setDesignAgentView(null); - applyPlanningV2CommandResult(planningV2); - } else { designAgentActiveRef.current = true; setDesignAgentActive(true); - planningV2SessionRef.current = null; - setPlanningV2Session(null); - setPlanGddState(null); + setDesignAgentView(null); projectSupervisorSessionIdRef.current = null; setProjectSupervisorSessionId(null); updateProjectSupervisorRuntime(null); @@ -3396,16 +3008,18 @@ export function App({ savedConversationProjectPathRef.current = nextProjectPath; savedConversationCountRef.current = 0; latestMessagesRef.current = []; + setWorkspaceStatus((workspaceStatus) => + workspaceStatus === '等待确认' + ? `已打开:${nextProjectPath}` + : workspaceStatus, + ); + return; + } catch (error) { + setProjectSupervisorRuntimeError( + `策划会话无法恢复:${error instanceof Error ? error.message : String(error)}`, + ); + return; } - setProjectSupervisorRuntimeError( - planningV2?.session.lastError?.summary ?? '', - ); - setWorkspaceStatus((workspaceStatus) => - workspaceStatus === '等待确认' - ? `已打开:${nextProjectPath}` - : workspaceStatus, - ); - return; } const resumeError = directCodexProductRuntime ? '' @@ -6160,86 +5774,6 @@ export function App({ } } - async function executePlanningV2Turn( - nextProjectPath: string, - prompt: string, - clientTurnId = createAgentChatRunId('planning-v2-turn'), - questionId = planningV2SessionRef.current?.session.currentQuestion?.id, - ) { - const invoke = resolveTauriInvoke(); - const normalizedPrompt = prompt.trim(); - if (!invoke) { - setProjectSupervisorRuntimeError('需要在 Tauri App 内运行。'); - return; - } - if (!nextProjectPath.trim() || !normalizedPrompt) { - return; - } - const currentSessionId = planningV2SessionRef.current?.session.sessionId; - planningV2TurnRef.current = { - projectPath: nextProjectPath, - clientTurnId, - }; - setChatAgentBusy(true); - setProjectSupervisorRuntimeError(''); - setPlanningV2TransientReplyTarget(''); - try { - const result = currentSessionId - ? await invoke( - 'continue_planning_session_v2', - { - projectPath: nextProjectPath, - sessionId: currentSessionId, - clientTurnId, - input: { - text: normalizedPrompt, - ...(questionId ? { questionId } : {}), - }, - }, - ) - : await invoke( - 'start_planning_session_v2', - { - projectPath: nextProjectPath, - clientTurnId, - prompt: normalizedPrompt, - mode: 'gdd', - }, - ); - if (localProjectPathRef.current !== nextProjectPath) { - return; - } - applyPlanningV2CommandResult(result, clientTurnId); - setCommandLog((current) => [ - ...current, - currentSessionId ? 'planning.v2.continue' : 'planning.v2.start', - ]); - } catch (error) { - if (localProjectPathRef.current !== nextProjectPath) { - return; - } - const message = error instanceof Error ? error.message : String(error); - if (isRuntimeConfigMissingError(message)) { - requestRuntimeConfigOpen(); - } - setProjectSupervisorRuntimeError(message); - setMessages((current) => [ - ...current, - { - role: 'assistant', - text: message, - runtimeOwned: true, - messageId: `planning-v2:${clientTurnId}:error`, - updatedAt: Date.now(), - }, - ]); - } finally { - planningV2TurnRef.current = null; - setPlanningV2TransientReplyTarget(''); - setChatAgentBusy(false); - } - } - async function executeChatAgentReply({ prompt, clientTurnId: directConversationTurnId, @@ -6248,11 +5782,7 @@ export function App({ directPolicyChecked = false, references, }: ExecuteChatAgentReplyInput) { - if ( - planningV2ActiveRef.current || - designAgentActiveRef.current || - planningStartMode - ) { + if (designAgentActiveRef.current) { const nextProjectPath = resolveChatProjectPath(localProject); if (!nextProjectPath) { setProjectSupervisorRuntimeError('请先初始化本地项目'); @@ -6266,11 +5796,6 @@ export function App({ ); return; } - await executePlanningV2Turn( - nextProjectPath, - prompt, - directConversationTurnId ?? createAgentChatRunId('planning-v2-turn'), - ); return; } // Product default: send the conversation directly to Codex app-server. @@ -7122,42 +6647,6 @@ export function App({ answers: Record, ) { const nextProjectPath = resolveChatProjectPath(localProject); - if (planningV2ActiveRef.current) { - const current = planningV2SessionRef.current; - const currentQuestion = current?.session.currentQuestion; - const answer = Object.values(answers) - .map((value) => value.trim()) - .find((value) => value.length > 0); - if ( - !nextProjectPath || - !current || - !currentQuestion || - request.sessionId !== current.session.sessionId || - request.questions[0]?.id !== currentQuestion.id || - !answer || - chatAgentBusy - ) { - setProjectSupervisorRuntimeError('待回答问题已变更,请刷新策划状态'); - return; - } - setMessages((current) => [ - ...current, - { - role: 'user', - text: answer, - runtimeOwned: true, - messageId: `planning-v2:${responseId}:user`, - updatedAt: Date.now(), - }, - ]); - await executePlanningV2Turn( - nextProjectPath, - answer, - responseId, - request.questions[0].id, - ); - return; - } const runtime = projectSupervisorRuntimeRef.current; const sessionId = projectSupervisorSessionIdRef.current; const currentRequest = runtime?.userInputRequest; @@ -11764,26 +11253,6 @@ export function App({ void loadProjectConversation(nextProjectPath, false, 'replace'); return; } - if (planningV2ActiveRef.current || planningStartMode) { - if (!prompt || chatAgentBusy) { - return; - } - supervisorChatShouldFollowLatestRef.current = true; - const clientTurnId = createAgentChatRunId('planning-v2-turn'); - setChatInput(''); - setMessages((current) => [ - ...current, - { - role: 'user', - text: prompt, - runtimeOwned: true, - messageId: `planning-v2:${clientTurnId}:user`, - updatedAt: Date.now(), - }, - ]); - void executeChatAgentReply({ prompt, clientTurnId }); - return; - } if (supervisorChatOnly || directCodexProductRuntime) { supervisorChatShouldFollowLatestRef.current = true; } @@ -11821,8 +11290,7 @@ export function App({ agent.id !== PROJECT_SUPERVISOR_AGENT_ID && (agent.runtimeStatus !== null || agent.hasRecentEvidence), ); - const useDesignAgentSurface = - Boolean(designAgentView) || (designAgentActive && !planningV2Session); + const useDesignAgentSurface = Boolean(designAgentView) || designAgentActive; if (projectSupervisorOnly && supervisorChatOnly) { const supervisorProjectPath = @@ -11857,11 +11325,9 @@ export function App({ transientReply={ designAgentActive ? designAgentTransientReply - : planningV2Active - ? planningV2TransientReply - : directCodexProductRuntime - ? directCodexTransientReply - : projectSupervisorTransientReply + : directCodexProductRuntime + ? directCodexTransientReply + : projectSupervisorTransientReply } hasConversationControls={projectSupervisorHasConversationControls} hiddenConversationCount={hiddenConversationCount} @@ -11909,11 +11375,9 @@ export function App({ transientReply={ designAgentActive ? designAgentTransientReply - : planningV2Active - ? planningV2TransientReply - : directCodexProductRuntime - ? directCodexTransientReply - : projectSupervisorTransientReply + : directCodexProductRuntime + ? directCodexTransientReply + : projectSupervisorTransientReply } showDesignReasoning={designAgentActive} designReasoning={designAgentReasoning} @@ -11926,12 +11390,6 @@ export function App({ !directCodexProductRuntime && orchestrationMode === 'professional-dag' } workspaceStatus={workspaceStatus} - planGddState={planGddState} - planGddHydrateBusy={planGddHydrateBusy} - planGddDecisionBusy={planGddDecisionBusy} - planGddError={planGddError} - onPlanGddRefresh={() => void hydratePlanGddState()} - onPlanGddDecision={decidePlanGdd} planningLane={planningLaneActive} designView={useDesignAgentSurface ? designAgentView : null} onDesignApprove={ @@ -11956,7 +11414,6 @@ export function App({ setDesignAgentTransientReplyTarget(''); setDesignAgentReasoning(''); setChatAgentBusy(true); - setPlanGddDecisionBusy(true); void designAgentEventSubscriptionReady() .then(() => invoke('decide_design_phase', { @@ -11990,7 +11447,7 @@ export function App({ ) { return; } - setPlanGddError(String(error)); + setProjectSupervisorRuntimeError(String(error)); }) .finally(() => { if ( @@ -12006,7 +11463,6 @@ export function App({ setDesignAgentTransientReplyTarget(''); } setChatAgentBusy(false); - setPlanGddDecisionBusy(false); }); } : undefined diff --git a/apps/ai-game-creator-shell/src/app/types.ts b/apps/ai-game-creator-shell/src/app/types.ts index 64a79bfab..f92269aac 100644 --- a/apps/ai-game-creator-shell/src/app/types.ts +++ b/apps/ai-game-creator-shell/src/app/types.ts @@ -217,170 +217,6 @@ export interface GenerateLocalGameDraftResult { manifest: GameCreationAppManifest; } -export type PlanGddDecisionAction = 'approve' | 'revise' | 'reject'; - -export interface PlanGddStateViewV1 { - schemaVersion: 'plan-gdd-state-view.v1'; - projectId: string; - gddId: string | null; - state: - | 'not_started' - | 'draft' - | 'ready_for_approval' - | 'revision_requested' - | 'approved' - | 'rejected'; - session: { - sessionId: string; - sessionRevision: number; - sessionFingerprint: string; - phase: - | 'collecting' - | 'awaiting_user_input' - | 'awaiting_gdd_approval' - | 'revision_requested' - | 'approved' - | 'rejected' - | 'recovery_required'; - clarificationRound: number; - questionLimit?: number | null; - repairDepth: number; - accumulatedAgentMillis: number; - activeRunId: string | null; - awaitingAnswerFor: { - delegationId: string; - requestId: string; - questionId: string; - round: number; - } | null; - decisionStateCounts: { - confirmed: number; - defaultPending: number; - assumptionPending?: number; - prototypePending: number; - }; - } | null; - versions: Array<{ - gddRef: { gddId: string; version: number; fingerprint: string }; - status: - | 'ready_for_approval' - | 'revision_requested' - | 'approved' - | 'rejected' - | 'superseded'; - approvalRequestId: string; - createdAtUtc: string; - decision: { action: PlanGddDecisionAction; decidedAtUtc: string } | null; - }>; - displayGdd: { - schemaVersion: string; - projectId: string; - gddId: string; - version: number; - submissionId: string; - approvalRequestId: string; - actionFingerprint: string; - agentId: string; - source: string; - runProfile: string; - runProfileBindingFingerprint: string; - rootAgentId: string; - rootRunId: string; - delegationId: string; - sessionId: string; - sourceSessionRevision: number; - sourceSessionFingerprint: string; - createdByRunId: string; - createdAtUtc: string; - fingerprint: string; - game: { - title: string; - oneLiner: string; - genre: { primary: string; fusion: string | null }; - artStyle: { - visualType: string; - keywords: string[]; - moodAndColor: string; - mvpArtBoundary: string; - }; - pillars: Array<{ - name: string; - playerFeel: string; - mechanism: string; - decisionState: string; - basis: null; - }>; - coreLoop: string[]; - targetUsers: { - coreUsers: string; - preferences: string; - sessionLength: string; - referenceGames: string[]; - }; - platformFacts: { - runtime: string; - viewports: string[]; - inputs: string[]; - preview: string; - }; - mvpSystems: Array<{ - system: string; - minimalFunction: string; - whyRequired: string; - verifyMethod: string; - decisionState: string; - basis: null; - }>; - outOfScope: string[]; - creatorTips: { - doFirst: string; - deferForNow: string; - howToVerify: string; - expandWhen: string; - }; - }; - decisions: Array<{ - id: string; - topic: string; - state: - | 'confirmed' - | 'assumption_pending' - | 'default_pending' - | 'prototype_pending'; - answerSource: - | 'user_option' - | 'user_freeform' - | 'user_revision' - | 'agent_inferred' - | 'default'; - round: number; - answerSummary: string; - basis: null; - }>; - prototypeValidationItems: Array<{ - id: string; - question: string; - microPrototype: string; - observation: string; - passCriterion: string; - }>; - } | null; - pendingApproval: { - gddRef: { gddId: string; version: number; fingerprint: string }; - pendingActionId: string; - actionFingerprint: string; - approvalRequestId: string; - sessionId: string; - runId: string; - } | null; - approvedGddRef: { - gddId: string; - version: number; - fingerprint: string; - } | null; - recoveryPending: boolean; -} - export interface GameCreatorChatAgentReply { replyText: string; } diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts b/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts deleted file mode 100644 index cb49d1c9f..000000000 --- a/apps/ai-game-creator-shell/src/features/project-workspace/planningSessionV2.ts +++ /dev/null @@ -1,614 +0,0 @@ -import { - PROJECT_SUPERVISOR_AGENT_ID, - PROJECT_SUPERVISOR_PLAN_SOURCE, -} from '../../app/constants'; -import type { - AgentRuntimeState, - AgentRuntimeUserInputRequest, - ChatMessage, - PlanGddDecisionAction, - PlanGddStateViewV1, -} from '../../app/types'; - -export type PlanningQuestionV2 = { - id: string; - header: string; - question: string; - options: Array<{ label: string; description: string }>; -}; - -export type PlanningSessionV2 = { - schemaVersion: 'planning-session.v2'; - engine: 'planning-session-v2'; - sessionId: string; - projectId: string; - mode: string; - status: string; - turnIndex: number; - questionCount: number; - questionLimit: number | null; - revisionCount: number; - currentArtifactVersion: number | null; - currentQuestion: PlanningQuestionV2 | null; - capabilities: { tools: string[]; skills: string[] }; - processingSeconds: number; - createdAtUtc: string; - updatedAtUtc: string; - lastError: { code: string; summary: string } | null; -}; - -export type PlanningMessageV2 = { - schemaVersion: 'planning-message.v2'; - messageId: string; - clientTurnId: string; - turnIndex: number; - atUtc: string; - role: 'user' | 'assistant' | 'system' | 'tool'; - kind: string; - payload: Record; -}; - -export type PlanningTurnResultV2 = { - schemaVersion: 'planning-turn-result.v2'; - kind: 'question' | 'artifact' | 'assistant_text' | 'error' | string; - payload: Record; -}; - -export type PlanningGddPayloadV2 = { - schemaVersion: string; - projectId: string; - gddId: string; - version: number; - createdAtUtc: string; - game: { - title: string; - oneLiner: string; - genre: { primary: string; fusion: string | null }; - artStyle: { - visualType: string; - keywords: string[]; - moodAndColor: string; - mvpArtBoundary: string; - }; - pillars: Array<{ - name: string; - playerFeel: string; - mechanism: string; - decisionState: string; - basis: null; - }>; - coreLoop: string[]; - targetUsers: { - coreUsers: string; - preferences: string; - sessionLength: string; - referenceGames: string[]; - }; - platformFacts: { - runtime: string; - viewports: string[]; - inputs: string[]; - preview: string; - }; - mvpSystems: Array<{ - system: string; - minimalFunction: string; - whyRequired: string; - verifyMethod: string; - decisionState: string; - basis: null; - }>; - outOfScope: string[]; - creatorTips: { - doFirst: string; - deferForNow: string; - howToVerify: string; - expandWhen: string; - }; - }; - decisions: Array<{ - id: string; - topic: string; - state: 'confirmed' | 'assumption_pending' | 'prototype_pending'; - answerSource: - | 'user_option' - | 'user_freeform' - | 'user_revision' - | 'agent_inferred'; - round: number; - answerSummary: string; - basis: null; - }>; - prototypeValidationItems: Array<{ - id: string; - question: string; - microPrototype: string; - observation: string; - passCriterion: string; - }>; - fingerprint: string; -}; - -export type PlanningArtifactV2 = { - artifactId: string; - kind: 'gdd' | string; - version: number; - status: string; - fingerprint: string; - payload: PlanningGddPayloadV2; -}; - -export type PlanningSessionCommandResultV2 = { - session: PlanningSessionV2; - result: PlanningTurnResultV2 | null; - currentArtifact: PlanningArtifactV2 | null; - replayed: boolean; - conversation?: PlanningMessageV2[]; -}; - -export type PlanningApprovalCommandResultV2 = { - approval: { - schemaVersion: string; - projectId: string; - sessionId: string; - artifactId: string; - version: number; - fingerprint: string; - decisionId: string; - action: PlanGddDecisionAction; - comment: string | null; - decidedAtUtc: string; - receiptFingerprint: string; - }; - session: PlanningSessionV2; - currentArtifact: PlanningArtifactV2 | null; - replayed: boolean; -}; - -export type PlanningSessionStreamEventV2 = { - sessionId: string; - clientTurnId: string; - status: 'started' | 'delta' | 'completed' | 'failed' | string; - deltaText: string; - accumulatedText: string; - finishReason: string | null; - result: PlanningTurnResultV2 | null; - error: { code: string; summary: string } | null; -}; - -function asRecord(value: unknown): Record | null { - return value && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : null; -} - -function questionFromPayload(payloadValue: unknown) { - const payload = asRecord(payloadValue); - // Turn results wrap the question in `payload.question`; persisted - // conversation messages store the question object directly in `payload`. - const question = asRecord(payload?.question) ?? payload; - if (!question) { - return null; - } - const options = Array.isArray(question.options) - ? question.options - .map((option) => { - const item = asRecord(option); - return item && - typeof item.label === 'string' && - typeof item.description === 'string' - ? { label: item.label, description: item.description } - : null; - }) - .filter((option): option is { label: string; description: string } => - Boolean(option), - ) - : []; - if ( - typeof question.id !== 'string' || - typeof question.header !== 'string' || - typeof question.question !== 'string' || - options.length === 0 - ) { - return null; - } - return { - id: question.id, - header: question.header, - question: question.question, - options, - } satisfies PlanningQuestionV2; -} - -function resultQuestion(result: PlanningTurnResultV2 | null) { - return questionFromPayload(result?.payload); -} - -export function planningResultText(result: PlanningTurnResultV2 | null) { - const text = result?.payload?.text; - return typeof text === 'string' ? text : ''; -} - -function questionDisplayText(question: PlanningQuestionV2) { - const options = question.options - .map( - (option, index) => - `${String.fromCharCode(65 + index)}. ${option.label}\n ${option.description}`, - ) - .join('\n'); - return `${question.header}\n${question.question}\n${options}`; -} - -function artifactDisplayText(artifact: PlanningArtifactV2) { - const title = artifact.payload.game.title.trim(); - const oneLiner = artifact.payload.game.oneLiner.trim(); - return `已生成 GDD v${artifact.version}${title ? `:${title}` : ''}${ - oneLiner ? `\n${oneLiner}` : '' - }`; -} - -export function planningResultDisplayText( - result: PlanningTurnResultV2 | null, - artifact: PlanningArtifactV2 | null, -) { - const question = resultQuestion(result); - if (question) { - return questionDisplayText(question); - } - if ( - artifact && - (result?.kind === 'artifact' || - result?.payload?.kind === 'gdd' || - typeof result?.payload?.artifactId === 'string') - ) { - return artifactDisplayText(artifact); - } - if (result?.kind === 'error') { - const payload = asRecord(result.payload); - const summary = - typeof payload?.summary === 'string' ? payload.summary.trim() : ''; - return summary - ? `后台任务失败:${summary}` - : '后台任务失败:策划回合未完成'; - } - return planningResultText(result); -} - -function messageDisplayText(message: PlanningMessageV2) { - const payload = asRecord(message.payload); - if (typeof payload?.text === 'string') { - return payload.text; - } - if (message.kind === 'question') { - const question = questionFromPayload(payload); - return question ? questionDisplayText(question) : ''; - } - if (message.kind === 'artifact') { - const artifact = asRecord(payload); - const nested = asRecord(artifact?.payload); - const game = asRecord(nested?.game); - const title = typeof game?.title === 'string' ? game.title.trim() : ''; - const version = - typeof artifact?.version === 'number' ? artifact.version : null; - const oneLiner = - typeof game?.oneLiner === 'string' ? game.oneLiner.trim() : ''; - const decisions = Array.isArray(nested?.decisions) - ? nested.decisions - .map((value) => { - const decision = asRecord(value); - const topic = - typeof decision?.topic === 'string' ? decision.topic.trim() : ''; - const summary = - typeof decision?.answerSummary === 'string' - ? decision.answerSummary.trim() - : ''; - return topic && summary ? `- ${topic}:${summary}` : null; - }) - .filter((value): value is string => Boolean(value)) - : []; - return [ - `已生成 GDD${version === null ? '' : ` v${version}`}${title ? `:${title}` : ''}`, - oneLiner, - decisions.length > 0 ? `本轮决策:\n${decisions.join('\n')}` : '', - ] - .filter(Boolean) - .join('\n'); - } - if (message.kind === 'error') { - const summary = typeof payload?.summary === 'string' ? payload.summary : ''; - return summary - ? `后台任务失败:${summary}` - : '后台任务失败:策划回合未完成'; - } - return ''; -} - -function timestampToMillis(value: string) { - const parsed = Date.parse(value); - return Number.isFinite(parsed) ? parsed : Date.now(); -} - -export function planningMessagesToChatMessages( - messages: PlanningMessageV2[] | undefined, -): ChatMessage[] { - return (messages ?? []) - .filter( - ( - message, - ): message is PlanningMessageV2 & { role: 'user' | 'assistant' } => - (message.role === 'user' || message.role === 'assistant') && - message.kind !== 'error', - ) - .map((message) => { - const text = messageDisplayText(message); - return { - role: message.role, - text, - runtimeOwned: true, - messageId: `planning-v2:${message.messageId}`, - updatedAt: timestampToMillis(message.atUtc), - } satisfies ChatMessage; - }) - .filter((message) => message.text.trim().length > 0); -} - -type PlanningSessionPhase = NonNullable['phase']; - -function sessionPhase(session: PlanningSessionV2): PlanningSessionPhase { - switch (session.status) { - case 'awaiting_user': - return 'awaiting_user_input'; - case 'awaiting_approval': - return 'awaiting_gdd_approval'; - case 'revision_requested': - return 'revision_requested'; - case 'approved': - return 'approved'; - case 'rejected': - return 'rejected'; - case 'provider_failed': - return 'recovery_required'; - default: - return 'collecting'; - } -} - -function gddState(status: string): PlanGddStateViewV1['state'] { - switch (status) { - case 'approved': - return 'approved'; - case 'rejected': - return 'rejected'; - case 'revision_requested': - return 'revision_requested'; - case 'ready_for_approval': - return 'ready_for_approval'; - default: - return 'draft'; - } -} - -function gddDisplayFromArtifact(artifact: PlanningArtifactV2) { - const payload = artifact.payload; - return { - schemaVersion: payload.schemaVersion, - projectId: payload.projectId, - gddId: payload.gddId, - version: payload.version, - submissionId: `planning-v2-submission-${payload.gddId}`, - approvalRequestId: `planning-v2-approval-${payload.gddId}-v${payload.version}`, - actionFingerprint: artifact.fingerprint, - agentId: 'planning-agent-v2', - source: PROJECT_SUPERVISOR_PLAN_SOURCE, - runProfile: 'standard', - runProfileBindingFingerprint: '', - rootAgentId: PROJECT_SUPERVISOR_AGENT_ID, - rootRunId: `planning-v2-${payload.projectId}`, - delegationId: '', - sessionId: `planning-v2-${payload.projectId}`, - sourceSessionRevision: 0, - sourceSessionFingerprint: '', - createdByRunId: `planning-v2-${payload.gddId}`, - createdAtUtc: payload.createdAtUtc, - fingerprint: payload.fingerprint, - game: payload.game, - decisions: payload.decisions.map((decision) => ({ - ...decision, - state: decision.state as - | 'confirmed' - | 'assumption_pending' - | 'prototype_pending', - answerSource: decision.answerSource as - | 'user_option' - | 'user_freeform' - | 'user_revision' - | 'agent_inferred', - })), - prototypeValidationItems: payload.prototypeValidationItems, - } satisfies NonNullable; -} - -export function planningSessionToPlanGddState( - session: PlanningSessionV2, - artifact: PlanningArtifactV2 | null, -): PlanGddStateViewV1 { - const artifactStatus = artifact?.status ?? ''; - const displayGdd = artifact ? gddDisplayFromArtifact(artifact) : null; - const awaitingQuestion = session.currentQuestion; - const questionCount = Math.max(0, Math.trunc(session.questionCount)); - const clarificationRound = awaitingQuestion - ? Math.max(0, questionCount - 1) - : questionCount; - const sessionView = { - sessionId: session.sessionId, - sessionRevision: Math.max(1, session.turnIndex), - sessionFingerprint: '', - phase: sessionPhase(session), - clarificationRound, - questionLimit: session.questionLimit, - repairDepth: session.revisionCount, - accumulatedAgentMillis: Math.round(session.processingSeconds * 1000), - activeRunId: session.status === 'planning' ? session.sessionId : null, - awaitingAnswerFor: awaitingQuestion - ? { - delegationId: `planning-v2-${session.sessionId}`, - requestId: `planning-v2-question-${session.turnIndex}`, - questionId: awaitingQuestion.id, - round: clarificationRound, - } - : null, - decisionStateCounts: { - confirmed: - displayGdd?.decisions.filter( - (decision) => decision.state === 'confirmed', - ).length ?? 0, - defaultPending: 0, - assumptionPending: - displayGdd?.decisions.filter( - (decision) => decision.state === 'assumption_pending', - ).length ?? 0, - prototypePending: - displayGdd?.decisions.filter( - (decision) => decision.state === 'prototype_pending', - ).length ?? 0, - }, - } satisfies NonNullable; - const state = artifact - ? gddState(artifactStatus) - : session.status === 'approved' - ? 'approved' - : session.status === 'rejected' - ? 'rejected' - : 'draft'; - const gddRef = artifact - ? { - gddId: artifact.artifactId, - version: artifact.version, - fingerprint: artifact.fingerprint, - } - : null; - const approvalRequestId = gddRef - ? `planning-v2-approval-${gddRef.gddId}-v${gddRef.version}` - : ''; - return { - schemaVersion: 'plan-gdd-state-view.v1', - projectId: session.projectId, - gddId: artifact?.artifactId ?? null, - state, - session: sessionView, - versions: gddRef - ? [ - { - gddRef, - status: - state === 'approved' - ? 'approved' - : state === 'rejected' - ? 'rejected' - : state === 'revision_requested' - ? 'revision_requested' - : 'ready_for_approval', - approvalRequestId, - createdAtUtc: - artifact?.payload.createdAtUtc ?? session.updatedAtUtc, - decision: null, - }, - ] - : [], - displayGdd, - pendingApproval: - artifact && gddRef && state === 'ready_for_approval' - ? { - gddRef, - pendingActionId: `planning-v2-action-${artifact.artifactId}-v${artifact.version}`, - actionFingerprint: artifact.fingerprint, - approvalRequestId, - sessionId: session.sessionId, - runId: session.sessionId, - } - : null, - approvedGddRef: state === 'approved' ? gddRef : null, - recoveryPending: false, - }; -} - -export function planningSessionToRuntime( - session: PlanningSessionV2, -): AgentRuntimeState { - const isQuestion = - session.status === 'awaiting_user' && session.currentQuestion; - const status = - session.status === 'planning' - ? 'running' - : isQuestion - ? 'waiting-for-user-input' - : session.status === 'provider_failed' - ? 'failed' - : session.status === 'approved' || session.status === 'rejected' - ? 'completed' - : 'idle'; - const phase = isQuestion - ? 'waiting-for-user-input' - : session.status === 'planning' - ? 'planning' - : session.status === 'provider_failed' - ? 'failed' - : status === 'completed' - ? 'completed' - : 'idle'; - const question = session.currentQuestion; - const userInputRequest: AgentRuntimeUserInputRequest | null = question - ? { - schemaVersion: 'planning-session-v2-user-input.v1', - requestId: `planning-v2-question-${session.turnIndex}`, - agentId: PROJECT_SUPERVISOR_AGENT_ID, - taskId: session.sessionId, - sessionId: session.sessionId, - runId: session.sessionId, - actionId: `planning-v2-question-${session.turnIndex}`, - status: 'pending', - questions: [question], - allowFreeform: true, - responseId: null, - requestedAt: timestampToMillis(session.updatedAtUtc), - updatedAt: timestampToMillis(session.updatedAtUtc), - } - : null; - return { - schemaVersion: 'planning-session-v2-runtime.v1', - agentId: PROJECT_SUPERVISOR_AGENT_ID, - taskId: session.sessionId, - sessionId: session.sessionId, - runId: session.sessionId, - source: PROJECT_SUPERVISOR_PLAN_SOURCE, - runProfile: 'standard', - status, - phase, - currentTask: '立项策划', - currentAction: session.status === 'planning' ? '正在整理策划方案' : '', - waitingOn: isQuestion ? '等待用户回答策划问题' : '', - nextStep: isQuestion ? '提交回答后继续策划' : '', - plan: [], - observations: [], - allowedTools: [], - pendingToolAction: null, - userInputRequest, - taskQueue: { - total: session.status === 'planning' ? 1 : 0, - pending: 0, - running: session.status === 'planning' ? 1 : 0, - waitingForConfirmation: 0, - waitingForUserInput: isQuestion ? 1 : 0, - paused: 0, - cancelled: 0, - completed: status === 'completed' ? 1 : 0, - failed: 0, - latestRunId: session.sessionId, - updatedAt: timestampToMillis(session.updatedAtUtc), - }, - lastResponse: null, - error: session.lastError?.summary ?? null, - startedAt: timestampToMillis(session.createdAtUtc), - updatedAt: timestampToMillis(session.updatedAtUtc), - }; -}