From 33369f118027535368ceff777e6146032a505468 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 15 Sep 2026 10:37:47 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=AE=9E=E6=97=B6=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 兼容 Windows 普通、verbatim 与 UNC 项目路径的失效事件匹配 让普通生图成功后立即发送 manifest 刷新通知 补充资源卡片与桥接通知回归测试 --- .../src-tauri/src/agent/direct_tool_bridge.rs | 1 + .../src-tauri/src/tests/project.rs | 84 +++++++++++++++++++ apps/ai-game-creator-shell/src/App.tsx | 12 ++- .../features/project-summary/projectPath.ts | 21 +++++ .../project-summary/projectSummary.ts | 1 + .../tests/appSurface/harness.ts | 4 +- .../tests/appSurface/home.suite.ts | 15 +++- .../tests/projectPathNotification.test.ts | 26 ++++++ docs/project-memory/shared-memory/pitfalls.md | 4 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 10 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/projectPathNotification.test.ts diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 8832c1267..e185c8cac 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -2159,6 +2159,7 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value) generate_platform_art_asset_with_options_at(&state.root, &prompt, &[], &options), ) .await?; + emit_game_creator_manifest_invalidated(&state.root, "direct-codex-art"); let resources = bridge_art_resources( &state.root, std::slice::from_ref(&generated.asset.local_path), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 52ad3d47d..4fa58e0e2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -1088,6 +1088,90 @@ async fn canonical_art_spec_and_ui_requests_use_the_shared_reference_chain() { fs::remove_dir_all(ui_config_dir).ok(); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn direct_image_generation_notifies_after_manifest_commit() { + let root = unique_project_path(); + let config_dir = unique_project_path(); + let canvas_base_url = spawn_mock_external_canvas_generation_api_server(None); + let _session = crate::platform_session::install_test_platform_session( + "direct-image-refresh-user", + "editor-runtime-key", + &canvas_base_url, + ); + fs::create_dir_all(&config_dir).expect("create config directory"); + fs::write( + config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), + serde_json::json!({ + "editorApi": { "baseUrl": canvas_base_url, "apiKey": "editor-runtime-key" } + }) + .to_string(), + ) + .expect("write config"); + let _config = use_test_runtime_config_dir(config_dir.clone()); + init_local_game_project_at(&root, "direct-image-refresh", "生成图片刷新测试") + .expect("init project"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), + }, + ) + .expect("allow generation"); + let listener = + TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).expect("bind event receiver"); + let sink = acquire_game_creator_manifest_invalidation_event_sink_test_guard(); + sink.configure(listener.local_addr().unwrap().port(), &"d".repeat(64)) + .expect("configure event receiver"); + let bridge = start_direct_tool_bridge(&root, false) + .await + .expect("start tool bridge"); + let client = reqwest::Client::new(); + let result: Value = client.post(bridge.url()).json(&serde_json::json!({ + "tool": "agc_generate_image", + "arguments": { "prompt": "像素月光主角", "kind": "icon-spec", "outputPath": "assets/art-spec.png" } + })).send().await.expect("generate through bridge").json().await.expect("read tool result"); + assert_eq!(result["isError"], false, "{result}"); + let manifest = read_existing_manifest_for_project(&root).expect("read committed manifest"); + assert!(manifest + .assets + .iter() + .any(|asset| asset.local_path == "assets/art-spec.png")); + assert!(root.join("assets/art-spec.png").is_file()); + let payload = read_manifest_invalidation_relay_payload_with_deadline(&listener) + .expect("generation must notify the client"); + let envelope: GameCreatorManifestInvalidationRelayEnvelope = + serde_json::from_slice(&payload).expect("event envelope"); + assert_eq!( + envelope.event.project_path, + fs::canonicalize(&root).unwrap().to_string_lossy() + ); + assert_eq!(envelope.event.agent_id, "direct-codex-art"); + + let rejected: Value = client + .post(bridge.url()) + .json(&serde_json::json!({ + "tool": "agc_generate_image", "arguments": { "prompt": "", "kind": "icon-spec" } + })) + .send() + .await + .expect("send rejected request") + .json() + .await + .expect("read rejected result"); + assert_eq!(rejected["isError"], true); + assert_eq!( + read_manifest_invalidation_relay_payload_with_deadline(&listener) + .expect_err("rejected generation must not emit a commit") + .kind(), + io::ErrorKind::TimedOut + ); + drop(bridge); + fs::remove_dir_all(root).ok(); + fs::remove_dir_all(config_dir).ok(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn platform_art_external_request_does_not_hold_project_lock_or_overwrite_manifest() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 58526a3db..d1b1cf38c 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -190,6 +190,7 @@ import { missingChatCommandArgumentMessage, projectFileActionDrafts, projectPathHasControlCharacter, + projectPathsMatchForInvalidation, readableArtifactsFromAgentRunTrace, sortCheckpointManifestFiles, summarizeAgentRunSupportFileReadDrafts, @@ -2192,10 +2193,17 @@ export function App({ void subscribeTauriEvent( 'game-creator-manifest-invalidated', (event) => { - if (event.payload.projectPath !== localProjectPathRef.current) { + const activeProjectPath = localProjectPathRef.current; + if ( + !activeProjectPath || + !projectPathsMatchForInvalidation( + event.payload.projectPath, + activeProjectPath, + ) + ) { return; } - void refreshManifest(event.payload.projectPath); + void refreshManifest(activeProjectPath); }, ) .then((unlisten) => { diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts index b0a61e6c5..d8a54f66a 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts @@ -17,6 +17,27 @@ export function projectPathHasControlCharacter(value: string) { }); } +// 失效事件只是重读提示:匹配 Windows 的普通 / verbatim 路径后,调用方仍用当前项目路径 +// 读取权威清单。此比较不解析链接,也不作为文件访问授权依据。 +export function projectPathsMatchForInvalidation( + eventPath: string, + activePath: string | null, +) { + if (!eventPath || !activePath) return false; + function normalize(path: string) { + if (/^\\\\\?\\UNC\\/i.test(path)) { + path = `\\\\${path.slice(8)}`; + } else if (/^\\\\\?\\[a-z]:\\/i.test(path)) { + path = path.slice(4); + } + if (/^[a-z]:[\\/]/i.test(path) || /^\\\\[^?.\\][^\\]*\\[^\\]+/.test(path)) { + return path.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase(); + } + return path; + } + return normalize(eventPath) === normalize(activePath); +} + export function isSafeProjectRelativePath(value: string) { const path = value.trim(); return ( diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts index 160f2e563..97b981535 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts @@ -80,6 +80,7 @@ export { isAbsoluteProjectPath, isSafeProjectRelativePath, projectPathHasControlCharacter, + projectPathsMatchForInvalidation, } from './projectPath'; export { summarizeProjectDependencyMap, diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index f683a7fa7..5dbedafbf 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -1212,10 +1212,10 @@ function createProjectSupervisorRuntimeHarness({ }, }); }, - emitManifestInvalidated(agentId: string) { + emitManifestInvalidated(agentId: string, eventProjectPath = projectPath) { manifestInvalidatedHandler?.({ payload: { - projectPath, + projectPath: eventProjectPath, agentId, }, }); diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 8f09ca84c..af621b7b5 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -2257,6 +2257,7 @@ export function registerHomeProjectCreationTests() { it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => { const projectPath = 'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\live-direct-art'; + const eventProjectPath = `\\\\?\\${projectPath}`; const manifest = createGameCreationAppManifest( 'live-direct-art', '直连美术实时刷新', @@ -2347,7 +2348,7 @@ export function registerHomeProjectCreationTests() { taskId: 'direct-codex-art-art-spritesheet', }, }, - ]; + ].map((asset) => ({ ...asset, category: 'ui-interaction' as const })); for (let index = 0; index < committedAssets.length; index += 1) { const refreshCountBeforeEvent = invoke.mock.calls.filter( @@ -2357,8 +2358,12 @@ export function registerHomeProjectCreationTests() { ...currentManifest, assets: committedAssets.slice(0, index + 1), }; + runtimeHarness.setProjectRevision(index + 1); act(() => { - runtimeHarness.emitManifestInvalidated('direct-codex-art'); + runtimeHarness.emitManifestInvalidated( + 'direct-codex-art', + eventProjectPath, + ); }); await waitFor(() => { expect( @@ -2373,6 +2378,12 @@ export function registerHomeProjectCreationTests() { ), ).toHaveLength(1); } + await openResourceBookCategory('UI 交互'); + expect(getResourceSelectButton('art-spec.png')).not.toBeNull(); + expect( + getResourceSelectButton('direct-game-background.png'), + ).not.toBeNull(); + expect(getResourceSelectButton('art-spritesheet.png')).not.toBeNull(); const refreshCountBeforeFailure = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', diff --git a/apps/ai-game-creator-shell/tests/projectPathNotification.test.ts b/apps/ai-game-creator-shell/tests/projectPathNotification.test.ts new file mode 100644 index 000000000..6de9adad1 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/projectPathNotification.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { projectPathsMatchForInvalidation } from '../src/features/project-summary/projectPath'; + +describe('项目刷新事件路径', () => { + it.each([ + ['C:\\Projects\\game', '\\\\?\\C:\\Projects\\game'], + ['\\\\?\\C:\\Projects\\game', 'c:/Projects/game/'], + ['\\\\server\\share\\game', '\\\\?\\UNC\\server\\share\\game'], + ['/tmp/game', '/tmp/game'], + ])('识别同一项目 %s 与 %s', (eventPath, activePath) => { + expect(projectPathsMatchForInvalidation(eventPath, activePath)).toBe(true); + }); + + it.each([ + ['\\\\?\\C:\\Projects\\game-other', 'C:\\Projects\\game'], + ['\\\\?\\C:\\Projects\\game\\child', 'C:\\Projects\\game'], + ['\\\\?\\UNC\\other\\share\\game', '\\\\server\\share\\game'], + ['\\\\.\\C:\\Projects\\game', 'C:\\Projects\\game'], + ['/tmp/Game', '/tmp/game'], + ['', ''], + ['C:\\Projects\\game', null], + ])('拒绝其它项目或空作用域 %s 与 %s', (eventPath, activePath) => { + expect(projectPathsMatchForInvalidation(eventPath, activePath)).toBe(false); + }); +}); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 3db0f4784..ebe4f1d83 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,9 @@ # 踩坑与排障记录 +## Windows 已登记生图资产未刷新 + +Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\` / `\\?\UNC\`,而前端项目路径仍是普通盘符或 UNC。失效监听不能直接比较原始字符串;识别为同一项目后,用当前项目路径重读 manifest,保留项目切换与 revision 门禁。普通 `agc_generate_image` 成功提交也必须发出失效通知,不能依赖整轮 Agent 结束。回归需覆盖两种 Windows 前缀、其它项目事件拒收,以及 Agent 尚未结束和后续失败时已登记图片卡片仍可见。 + ## 2026-09-14 严格 IPC 桩缺登记新命令时,症状可能是「unhandled rejection + 不相干的提示断言」,而不是同一处报错 - **现象**:`ProjectDevelopmentView` 新增「项目打开时读生成任务账本」(`list_local_project_asset_generations`)后,两个**别的关注点**的用例同时红:`resourceCanvasManualLayout.test.tsx` 报 `AssertionError: expected [ Array(1) ] to deeply equal []`(严格桩把新命令记进 `unexpectedCommands`),并伴随 7 条 `Unhandled Rejection: TypeError: Cannot read properties of undefined (reading 'map')`;`appSurface/project-development.suite.ts` 的「布局读时提示」用例则因为新命令被当成 unexpected invoke 抛错、触发了新的提示条,导致 `queryBySelector('.game-resource-live-notice')` 断言失败。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2a3504a0e..adc56a555 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1167,7 +1167,7 @@ game-project/ - `.agent/manifest.json` 的存储写边界使用同目录持久文件锁跨线程、跨进程串行化;锁必须覆盖旧 manifest 读取、不可变版本前缀校验、临时文件安装和安装后回读一致性校验。锁文件拒绝符号链接、非普通文件和异常所有权 / 硬链接;Windows 使用不共享写句柄,Unix 使用 `O_NOFOLLOW + flock`。旧快照在新版本安装后只能被拒绝,不能覆盖已追加版本。 - 后台 Agent 的 manifest 变化以共用 Runtime 状态投影 / 终态 emitter 作为失效因果点:`game-creator-agent-runtime-update` 的 Rust / TypeScript DTO 固定携带 `manifestInvalidated`,且 App 必须在 Supervisor、selected agent、session 和 run 身份的任何 early return 之前处理失效。GUI 进程内 Runtime 直接发该事件;External Runner 是独立进程、没有 GUI `AppHandle`,因此 Runner 协议 v5 的 `runner.attach_gui_owner` 必须登记 GUI 创建的随机 loopback 端口和 64 位随机令牌,Runner 的同一 emitter 通过受令牌保护的短连接转发 `game-creator-manifest-invalidated`。两条路径都只传项目路径与 Agent 身份,不复制 manifest,也不靠轮询补偿。 -- Direct Codex 不伪造普通 Agent Runtime state。每张平台美术在本地文件与 manifest 提交成功后,统一通过 standalone `game-creator-manifest-invalidated` 发送 `projectPath + direct-codex-art`;只读恢复的已付费源图同样在 `register_local_asset_at` 成功后发送,下载、解码、文件写入或登记失败时不得发送成功失效。前端仍把 `game-creator-agent-progress` 仅用于进度文案;Direct Codex 整体命令成功、失败或超时 reject 后都追加一次 manifest 最终对账,只有完整成功才启动本地预览。 +- Direct Codex 不伪造普通 Agent Runtime state。每张平台美术在本地文件与 manifest 提交成功后,统一通过 standalone `game-creator-manifest-invalidated` 发送 `projectPath + direct-codex-art`;普通 `agc_generate_image` 同样在生成通道成功返回后、工具结果组装前发出通知,不能只覆盖标准美术包。只读恢复的已付费源图同样在 `register_local_asset_at` 成功后发送,下载、解码、文件写入或登记失败时不得发送成功失效。失效事件匹配当前项目时统一 Windows 盘符、UNC 与对应 verbatim 前缀的写法,实际重读始终使用当前项目保存的路径;该比较仅用于刷新提示,不替代后端路径与权限校验。前端仍把 `game-creator-agent-progress` 仅用于进度文案;Direct Codex 整体命令成功、失败或超时 reject 后都追加一次 manifest 最终对账,只有完整成功才启动本地预览。 - App 收到当前项目的 Runtime / relay 失效后重新调用 `get_local_game_manifest`。重读按项目 single-flight 合并事件风暴;读取中再到达失效只追加一轮串行重读,不并发提交同项目响应。应用结果同时校验组件仍挂载、当前项目路径和项目 scope version;项目切换、组件卸载或旧 scope 的迟到响应不得覆盖新项目。Project Supervisor 对外发布前以“revision 前读 -> manifest -> revision 后读”取得一致快照,再通过 `onManifestChange(projectPath, manifest, metadata)` 携带 `projectId + revision + source`;启动器按 `projectPath + projectId` 只接受更高 revision,同 revision 只接受内容一致的重复,旧轮询和同 revision 分叉都不得覆盖。资源列表、依赖图输入、任务状态、运行入口和正式版本卡必须在当前页面实时重投影,不要求关闭或重开项目。集成测试记录“事件未重新打开项目”的调用基线前,必须先等待项目写入最近列表后触发的只读目录状态刷新完成,不能把这项合法后台检查误算成失效事件副作用。 - `.agent/agent.db` 有界尾部读取报告截断时,审计 producer 映射失败关闭,不生成基于不完整审计的 producer、task flow 或对应任务环。前端收到截断 DTO 时只剔除 `producerAssignments`、`taskFlows` 与对应 `cyclicTaskIds`;Rust 根据当前 manifest、精确资源引用和仍可信任务深度下限返回的 `dependencyDepths` 继续保留,前端只校验资源仍存在且深度为非负安全整数,不得自行重算或压平权威深度。精确引用边、reference connection index、`cyclicResourceIds` 与 unresolved references 同样继续保留。 -- 资源依赖 SVG 继续作为不可交互装饰层隐藏,但 dependency 画布通过 `aria-describedby` 提供当前可见精确引用和任务流的文本等价列表。中央资源聚焦按稳定 `resourceId` 驱动焦点状态:仅 `null -> id` 或 `idA -> idB` 聚焦详情 region,同一 ID 的 manifest 重投影不得抢走音频、视频、链接或关闭按钮焦点;显式收起和 Escape 恢复画布滚动并优先聚焦原触发卡片。聚焦资源被删除时清理 stale focused / selected ID,关闭详情并把焦点落到资源搜索框;项目切换或运行视图切换清除旧恢复意图,不得恢复旧项目卡片。橙色引用线及箭头使用对 `#fffdfa` 画布达到至少 `3:1` 的颜色。 From fbf8d34f45bf1ebc5856fa98203cff7b661832cd Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 15 Sep 2026 12:14:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20AGC=20=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E7=AD=96=E5=88=92=E8=A1=A5=E5=85=A8=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首页做游戏入口增加策划补全勾选项 勾选后复用 planning Agent Runtime 启动模式 补充首页启动模式回归测试与实施计划 --- .../src/view/home/homeStartMode.ts | 12 +++++++ .../src/view/home/index.tsx | 31 ++++++++++++++++--- .../tests/homeStartMode.test.ts | 18 +++++++++++ ...实施计划】AGC首页策划补全入口-2026-09-15.md | 16 ++++++++++ ...里程碑】AGC首页策划补全入口-2026-09-15.md | 20 ++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/view/home/homeStartMode.ts create mode 100644 apps/ai-game-creator-shell/tests/homeStartMode.test.ts create mode 100644 docs/project-memory/plans/【实施计划】AGC首页策划补全入口-2026-09-15.md create mode 100644 docs/project-memory/plans/【里程碑】AGC首页策划补全入口-2026-09-15.md diff --git a/apps/ai-game-creator-shell/src/view/home/homeStartMode.ts b/apps/ai-game-creator-shell/src/view/home/homeStartMode.ts new file mode 100644 index 000000000..4724c1066 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/home/homeStartMode.ts @@ -0,0 +1,12 @@ +import type { ProjectStartMode } from '../../app/types'; +import type { HomeCreationType } from './useHomeDraftStore'; + +export function resolveHomeStartMode( + creationType: HomeCreationType, + planningCompletionEnabled: boolean, +): ProjectStartMode { + return creationType === 'doc' || + (creationType === 'game' && planningCompletionEnabled) + ? 'planning' + : 'direct-build'; +} diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index d1470cb25..74f596b64 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -20,6 +20,7 @@ import { richTextToAttachments, richTextToPrompt, } from './components/RichInputArea/richTextToPrompt'; +import { resolveHomeStartMode } from './homeStartMode'; import InspirationGallery from './InspirationGallery'; import { type HomeCreationType, @@ -145,14 +146,18 @@ export default function HomeView({ (state) => state.setRichText, ); const [homeCreationBusy, setHomeCreationBusy] = useState(false); + const [planningCompletionEnabled, setPlanningCompletionEnabled] = + useState(false); const homeCreationBusyRef = useRef(false); const activeCreationType = HOME_CREATION_TYPE_ITEMS.find( (item) => item.creationType === homeCreationType, ) ?? HOME_CREATION_TYPE_ITEMS[0]!; // 做方案走立项策划链路,做游戏 / 做素材维持既有的直接开建路由。 - const startMode: ProjectStartMode = - homeCreationType === 'doc' ? 'planning' : 'direct-build'; + const startMode = resolveHomeStartMode( + homeCreationType, + planningCompletionEnabled, + ); async function createFromHome() { if (homeCreationBusyRef.current || creationBusy) { @@ -241,7 +246,12 @@ export default function HomeView({ type="button" key={item.creationType} aria-pressed={isActive} - onClick={() => setHomeCreationType(item.creationType)} + onClick={() => { + setHomeCreationType(item.creationType); + if (item.creationType !== 'game') { + setPlanningCompletionEnabled(false); + } + }} >