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 3f4293087..1f51a98ae 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -337,6 +337,30 @@ async fn separate_ui( ui_editor::commands::separate_ui_impl(project_path, asset_id, state).await } +#[tauri::command] +fn inspect_separation_recovery( + project_path: String, + asset_id: String, +) -> Result { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "asset.list")?; + ui_editor::commands::separation::inspect_separation_recovery(root, &asset_id) +} + +#[tauri::command] +fn finalize_separation(project_path: String, asset_id: String) -> Result<(), String> { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "asset.register")?; + ui_editor::commands::separation::finalize_separation(root, &asset_id) +} + +#[tauri::command] +fn discard_separation_recovery(project_path: String, asset_id: String) -> Result<(), String> { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "asset.register")?; + ui_editor::commands::separation::finalize_separation(root, &asset_id) +} + #[tauri::command] async fn merge_ui(state: ui_editor::state::State) -> Result { ui_editor::commands::merge_ui_impl(state).await @@ -2569,6 +2593,9 @@ fn main() { suggest_ui_design_semantic, recognize_ui, separate_ui, + inspect_separation_recovery, + finalize_separation, + discard_separation_recovery, merge_ui, bind_components, load_ui_design_state, diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs index 181931f2d..d85432e07 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs @@ -12,6 +12,6 @@ pub(crate) use merge::{merge_ui_impl, merge_ui_impl_with_provider}; pub use recognition::RecognitionDTO; pub(crate) use recognition::{recognize_ui_impl, recognize_ui_impl_with_provider}; pub(crate) use separation::separate_ui_impl; -pub use separation::SeparationDTO; +pub use separation::{SeparationDTO, SeparationRecoveryDTO}; pub(crate) use ui_design_suggestion::suggest_ui_design_semantic_impl; pub use ui_design_suggestion::UIDesignSuggestionTreeNode; diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model.rs index 985e8fda3..12fca349c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model.rs @@ -84,6 +84,15 @@ pub struct SeparationDTO { pub problematic_nodes: Vec, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] +pub struct SeparationRecoveryDTO { + pub exists: bool, + pub bound_node_count: usize, + pub problematic_node_count: usize, + pub has_pending_tree: bool, +} + #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct BindingArea { diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/persistence.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/persistence.rs index e9d7c9296..3ec6aa8fb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/persistence.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/persistence.rs @@ -25,6 +25,21 @@ pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result Result { + Ok(separation_sidecar_dir(root, asset_id)?.join("state.json")) +} + +pub fn project_relative_path(root: &Path, path: &Path) -> Result { + let relative = path + .strip_prefix(root) + .map_err(|_| "separation 产物必须位于项目目录内".to_string())?; + let value = relative.to_string_lossy().replace('\\', "/"); + if value.is_empty() || value.starts_with('/') || value.split('/').any(|part| part == "..") { + return Err("separation 产物相对路径无效".to_string()); + } + Ok(value) +} + pub fn write_separation_state(path: &Path, state: &SeparationState) -> Result<(), String> { if state.schema_version != SEPARATION_STATE_SCHEMA_VERSION { app_log!("ui_separation.error stage=state_write reason=schema_mismatch"); @@ -116,3 +131,36 @@ pub fn separation_dto(state: &SeparationState) -> SeparationDTO { problematic_nodes: state.problematic_nodes.clone(), } } + +pub fn inspect_separation_recovery( + root: &Path, + asset_id: &str, +) -> Result { + let state_path = separation_state_path(root, asset_id)?; + if !state_path.exists() { + return Ok(SeparationRecoveryDTO { + exists: false, + bound_node_count: 0, + problematic_node_count: 0, + has_pending_tree: false, + }); + } + let state = read_separation_state(&state_path)?; + Ok(SeparationRecoveryDTO { + exists: true, + bound_node_count: state.bound.len(), + problematic_node_count: state.problematic_nodes.len(), + has_pending_tree: state.trees.iter().any(|tree| { + !tree.root.children.is_empty() || tree.root_extractable + }), + }) +} + +pub fn finalize_separation(root: &Path, asset_id: &str) -> Result<(), String> { + let state_path = separation_state_path(root, asset_id)?; + match fs::remove_file(&state_path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(format!("删除 separation state 失败:{error}")), + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs index 2c3a629bb..da3a1288e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs @@ -367,7 +367,7 @@ pub(crate) async fn separate_ui_impl( ); format!("创建 separation sidecar 失败:{e}") })?; - let state_path = sidecar.join("state.json"); + let state_path = separation_state_path(root, &asset_id)?; let restored = state_path.exists(); let mut separation = if restored { app_log!("ui_separation.state_restore.start asset_id={}", asset_id); @@ -538,8 +538,10 @@ pub(crate) async fn separate_ui_impl( .await { Ok(()) => { - cut_paths - .insert(to_node.clone(), cut_path.to_string_lossy().to_string()); + cut_paths.insert( + to_node.clone(), + project_relative_path(root, &cut_path)?, + ); } Err(error) => { app_log!( @@ -577,19 +579,6 @@ pub(crate) async fn separate_ui_impl( batch_index += 1; } } - match fs::remove_file(&state_path) { - Ok(()) => app_log!("ui_separation.state_removed asset_id={}", asset_id), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - app_log!( - "ui_separation.state_remove_skipped asset_id={} reason=not_found", - asset_id - ) - } - Err(error) => app_log!( - "ui_separation.error stage=state_remove asset_id={} error={error}", - asset_id - ), - } app_log!( "ui_separation.completed asset_id={} bound_nodes={} problematic_nodes={}", asset_id,