diff --git a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs index 0ce7e6a2b..3ab0f2ced 100644 --- a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs +++ b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs @@ -22,7 +22,6 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url)); const repoRoot = resolve(appRoot, '../..'); const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js'); const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG'; -const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG'; const designDebugEnabled = process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1'; @@ -137,7 +136,6 @@ async function runTauriDev( env: { ...withAgcDevEndpointEnv(endpoint), [AGC_DESIGN_DEBUG_ENV]: designDebugEnabled, - [AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled, }, }); const childResult = waitForCli(child); @@ -191,7 +189,6 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) { cwd: repoRoot, env: { ...withAgcDevEndpointEnv(endpoint), - [AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled, }, }, ); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index 53142ca6a..60e923ab8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -606,11 +606,7 @@ fn build_design_request( // 调试队列只接收副本,写盘慢或失败时丢弃,不参与会话恢复。 fn design_debug(root: &Path, kind: &str, data: Value) { - if std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG") - .ok() - .as_deref() - != Some("1") - { + if !design_debug_enabled() { return; } type Entry = (PathBuf, Value); @@ -1128,6 +1124,18 @@ fn resolve_design_runtime_mode(root: &Path) -> Result, })) } +fn design_debug_enabled() -> bool { + std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG") + .ok() + .as_deref() + == Some("1") +} + +#[tauri::command] +pub(crate) fn is_design_agent_debug_enabled() -> bool { + cfg!(debug_assertions) && design_debug_enabled() +} + #[tauri::command] pub(crate) fn set_design_agent_runtime_mode( project_path: String, @@ -1154,12 +1162,7 @@ pub(crate) fn debug_fast_forward_design_session( project_path: String, target_phase: String, ) -> Result { - if !cfg!(debug_assertions) - || std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG") - .ok() - .as_deref() - != Some("1") - { + if !is_design_agent_debug_enabled() { return Err("策划 Agent 快速推进仅可用于 Debug 构建".to_string()); } let root = Path::new(project_path.trim()); 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/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 4ff699d3c..f3924a219 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2668,6 +2668,7 @@ fn main() { hydrate_design_agent_session, reset_design_agent_session, get_design_agent_runtime_mode, + is_design_agent_debug_enabled, set_design_agent_runtime_mode, debug_fast_forward_design_session, continue_design_agent_session, 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 eb5b121c1..014217e10 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, @@ -2101,10 +2102,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/src/features/project-workspace/DesignWorkspacePanel.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx index 53d01cbe1..c651a5764 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/DesignWorkspacePanel.tsx @@ -185,6 +185,7 @@ export function DesignWorkspacePanel({ const [loading, setLoading] = useState(true); const [previewLoading, setPreviewLoading] = useState(false); const [debugPreparing, setDebugPreparing] = useState(false); + const [designDebugEnabled, setDesignDebugEnabled] = useState(false); const [error, setError] = useState(''); const [expandedPaths, setExpandedPaths] = useState>( () => new Set(), @@ -264,6 +265,16 @@ export function DesignWorkspacePanel({ void loadWorkspace({ showLoading: true, hydrateSession: true }); }, [loadWorkspace]); + useEffect(() => { + const invoke = resolveTauriInvoke(); + if (!invoke) { + return; + } + void invoke('is_design_agent_debug_enabled') + .then(setDesignDebugEnabled) + .catch(() => setDesignDebugEnabled(false)); + }, []); + useEffect(() => { if (!canSubscribeTauriEvents() || !projectPath.trim()) { return; @@ -360,7 +371,7 @@ export function DesignWorkspacePanel({

Agent 产生的策划文档会显示在这里。

- {import.meta.env.VITE_GENARRATIVE_AGC_DESIGN_DEBUG === '1' ? ( + {designDebugEnabled ? (