diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index 11ca2fcb7..1fd1b3489 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -2197,17 +2197,8 @@ pub(crate) fn create_ui_design_resource( if manifest.project_id != expected_project_id.trim() { return Err("project-identity-conflict".to_string()); } - let next_index = manifest - .assets - .iter() - .filter(|asset| { - asset.kind == crate::ui_editor::persistence::UI_DESIGN_DOC_ASSET_KIND - && asset.media_type == crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE - }) - .count() - + 1; - let resource_name = format!("UI 设计 {next_index}"); - let relative_path = format!("ui/{resource_name}.json"); + let (resource_name, relative_path) = + crate::ui_editor::resource_bridge::next_ui_design_path(root, &manifest)?; let absolute_path = resolve_local_project_path(root, &relative_path)?; if let Some(parent) = absolute_path.parent() { ensure_game_creator_private_directory_tree(parent, "UI 资源目录")?; @@ -2243,7 +2234,10 @@ pub(crate) fn create_ui_design_resource( GameCreationAppAssetSource { kind: GameCreationAppAssetSourceKind::Generated, canvas_project_id: None, - resource_id: Some(format!("ui:{next_index}")), + resource_id: Some(format!( + "ui:{}", + resource_name.trim_start_matches("UI 设计 ") + )), asset_object_id: None, task_id: None, prompt: None, 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 f96b864ea..c78e8f242 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 @@ -2259,6 +2259,37 @@ fn register_local_asset_derives_category_from_real_write_side_kinds() { fs::remove_dir_all(root).ok(); } +/// 已存在的 `ui/UI 设计 N.json` 不能让新建 UI 设计资源直接失败。 +/// +/// 编号按已登记的 UI 文档数推导,旧项目里 kind 已被收口为 `unknown` 的文档不再计入, +/// 只按计数取名就会撞上仍然存在的同名文件并报「路径已存在」。两个写入侧必须共用 +/// `ui_editor::resource_bridge` 的「第一个空闲编号」规则,既不改名既有文件也不覆盖。 +#[test] +fn create_ui_design_resource_skips_a_taken_ui_design_path() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "UI 设计编号避让").expect("project init"); + write_local_project_file_at(&root, "ui/UI 设计 1.json", "{}").expect("ui asset file"); + + let result = crate::commands::create_ui_design_resource( + root.to_string_lossy().to_string(), + "project-1".to_string(), + ) + .expect("create UI design resource"); + + assert_eq!(result.asset.local_path, "ui/UI 设计 2.json"); + let registered = result + .manifest + .assets + .iter() + .find(|asset| asset.id == result.asset.id) + .expect("registered UI design asset"); + assert_eq!(registered.source.resource_id.as_deref(), Some("ui:2")); + assert_eq!(registered.kind, GameCreationAppAssetKind::UiDesignDoc); + assert!(root.join("ui/UI 设计 1.json").is_file()); + + fs::remove_dir_all(root).ok(); +} + #[test] fn register_local_asset_rejects_missing_or_unsafe_path() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs index bde9b6b34..dc76ce37c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs @@ -190,7 +190,9 @@ pub(crate) fn ensure_ui_design_resource_for_prototype( }) } -fn next_ui_design_path( +/// UI 设计文档的确定性命名:从已登记文档数 + 1 起找第一个既未被占用、 +/// 也未登记进 manifest 的 `ui/UI 设计 N.json`,不覆盖任何既有文件。 +pub(crate) fn next_ui_design_path( root: &Path, manifest: &GameCreationAppManifest, ) -> Result<(String, String), String> {