From 99c19e3ed92715e244a6398aff2033879adc7c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 9 Sep 2026 12:06:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=88=86=E7=A6=BB=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=8E=92=E9=9A=9C=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录 separation sidecar 状态读写与恢复阶段 记录树构造、批次选择、视觉绑定、裁切和 patch 结果 避免输出 base64、完整提示词和敏感凭据 --- .../commands/separation/persistence.rs | 91 ++++- .../src/ui_editor/commands/separation/tree.rs | 71 +++- .../ui_editor/commands/separation/workflow.rs | 333 ++++++++++++++++-- 3 files changed, 454 insertions(+), 41 deletions(-) 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 5d391e2e4..91ea757f0 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 @@ -4,6 +4,7 @@ use std::fs; use std::path::{Path, PathBuf}; pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result { if asset_id.trim().is_empty() || asset_id.trim() != asset_id { + app_log!("ui_separation.error stage=sidecar_dir reason=invalid_asset_id"); return Err("UI 资源 ID 无效".to_string()); } let dir = root.join("ui").join(format!( @@ -11,37 +12,105 @@ pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result") + ); Ok(dir) } 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"); return Err("不支持的 separation state schema".to_string()); } - let bytes = serde_json::to_vec_pretty(state) - .map_err(|error| format!("序列化 separation state 失败:{error}"))?; - let parent = path - .parent() - .ok_or_else(|| "separation state 路径缺少父目录".to_string())?; - fs::create_dir_all(parent).map_err(|error| format!("创建 separation sidecar 失败:{error}"))?; + app_log!( + "ui_separation.state_write.start file={} trees={} bound={} problematic={}", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + state.unprocessed_trees.len(), + state.bound.len(), + state.problematic_nodes.len() + ); + let bytes = serde_json::to_vec_pretty(state).map_err(|error| { + app_log!("ui_separation.error stage=state_write reason=serialize error={error}"); + format!("序列化 separation state 失败:{error}") + })?; + let parent = path.parent().ok_or_else(|| { + app_log!("ui_separation.error stage=state_write reason=missing_parent"); + "separation state 路径缺少父目录".to_string() + })?; + fs::create_dir_all(parent).map_err(|error| { + app_log!("ui_separation.error stage=state_write reason=create_parent error={error}"); + format!("创建 separation sidecar 失败:{error}") + })?; let temporary = path.with_extension("json.tmp"); - fs::write(&temporary, bytes).map_err(|error| format!("写入 separation state 失败:{error}"))?; - fs::rename(&temporary, path).map_err(|error| format!("安装 separation state 失败:{error}")) + fs::write(&temporary, bytes).map_err(|error| { + app_log!("ui_separation.error stage=state_write reason=write_temp error={error}"); + format!("写入 separation state 失败:{error}") + })?; + fs::rename(&temporary, path).map_err(|error| { + app_log!("ui_separation.error stage=state_write reason=install error={error}"); + format!("安装 separation state 失败:{error}") + })?; + app_log!( + "ui_separation.state_write.completed file={} bytes={} trees={} bound={} problematic={}", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0), + state.unprocessed_trees.len(), + state.bound.len(), + state.problematic_nodes.len() + ); + Ok(()) } pub fn read_separation_state(path: &Path) -> Result { - let bytes = fs::read(path).map_err(|error| format!("读取 separation state 失败:{error}"))?; - let state: SeparationState = serde_json::from_slice(&bytes) - .map_err(|error| format!("解析 separation state 失败:{error}"))?; + app_log!( + "ui_separation.state_read.start file={}", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") + ); + let bytes = fs::read(path).map_err(|error| { + app_log!("ui_separation.error stage=state_read reason=read error={error}"); + format!("读取 separation state 失败:{error}") + })?; + let state: SeparationState = serde_json::from_slice(&bytes).map_err(|error| { + app_log!("ui_separation.error stage=state_read reason=parse error={error}"); + format!("解析 separation state 失败:{error}") + })?; if state.schema_version != SEPARATION_STATE_SCHEMA_VERSION { + app_log!("ui_separation.error stage=state_read reason=schema_mismatch"); return Err("不支持的 separation state schema".to_string()); } + app_log!( + "ui_separation.state_read.completed bytes={} trees={} bound={} problematic={}", + bytes.len(), + state.unprocessed_trees.len(), + state.bound.len(), + state.problematic_nodes.len() + ); Ok(state) } pub fn separation_dto(state: &SeparationState) -> SeparationDTO { + app_log!( + "ui_separation.dto bound_nodes={} problematic_nodes={} remaining_trees={}", + state.bound.len(), + state.problematic_nodes.len(), + state.unprocessed_trees.len() + ); SeparationDTO { bound_nodes: state.bound.clone(), problematic_nodes: state.problematic_nodes.clone(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs index b600c48e1..fe8414e7f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs @@ -1,5 +1,5 @@ use super::model::*; -use crate::ui_editor::component::{Component, image::ImageComponent}; +use crate::ui_editor::component::{image::ImageComponent, Component}; use crate::ui_editor::layout::node::Node; use crate::ui_editor::state::{State, UITree}; use crate::ui_editor::utils::NodeId; @@ -71,17 +71,35 @@ fn collect_todo_nodes( } pub fn construct_separation_state(state: &State) -> SeparationState { + app_log!( + "ui_separation.tree_construct.start ui_trees={} ui_images={}", + state.ui_trees.len(), + state.ui_design_images.len() + ); let unprocessed_trees = state .ui_trees .iter() .filter_map(|tree| { - let image = state.ui_design_images.get(&tree.src_ui_design)?; + let Some(image) = state.ui_design_images.get(&tree.src_ui_design) else { + app_log!( + "ui_separation.error stage=tree_construct reason=missing_ui_image image_id={}", + tree.src_ui_design.as_str() + ); + return None; + }; let ppu = image.pixels_per_unit.get(); let size = image.pixel_size / ppu; let root_rect = crate::ui_editor::layout::dimension::UIRect::new(nalgebra::Point2::origin(), size); let mut children = Vec::new(); collect_todo_nodes(&tree.root, &root_rect, ppu, &mut children); + app_log!( + "ui_separation.tree_construct.tree image_id={} todo_nodes={} pixel_width={} pixel_height={}", + tree.src_ui_design.as_str(), + count_nodes(&children), + image.pixel_size.x.round() as u32, + image.pixel_size.y.round() as u32 + ); (!children.is_empty()).then(|| SeparationTree { src_ui_design: tree.src_ui_design.clone(), root: SeparationNode { @@ -100,13 +118,25 @@ pub fn construct_separation_state(state: &State) -> SeparationState { }, }) }) - .collect(); - SeparationState { + .collect::>(); + let result = SeparationState { schema_version: SEPARATION_STATE_SCHEMA_VERSION.to_string(), unprocessed_trees, bound: Vec::new(), problematic_nodes: Vec::new(), - } + }; + app_log!( + "ui_separation.tree_construct.completed trees={}", + result.unprocessed_trees.len() + ); + result +} + +fn count_nodes(nodes: &[SeparationNode]) -> usize { + nodes + .iter() + .map(|node| 1 + count_nodes(&node.children)) + .sum() } pub fn next_leaf_batch(tree: &SeparationTree) -> Vec<&SeparationNode> { @@ -121,6 +151,11 @@ pub fn next_leaf_batch(tree: &SeparationTree) -> Vec<&SeparationNode> { } let mut output = Vec::new(); leaves(&tree.root, &mut output); + app_log!( + "ui_separation.batch_selected image_id={} leaf_nodes={}", + tree.src_ui_design.as_str(), + output.len() + ); output } @@ -128,6 +163,11 @@ pub fn validate_binding_response( response: &BindingResp, batch: &[&SeparationNode], ) -> Result<(), String> { + app_log!( + "ui_separation.binding_validate.start expected_nodes={} decisions={}", + batch.len(), + response.decisions.len() + ); let expected = batch .iter() .map(|node| node.id.clone()) @@ -140,9 +180,17 @@ pub fn validate_binding_response( } }; if !expected.contains(node_id) { + app_log!( + "ui_separation.error stage=binding_validate reason=unknown_node node_id={}", + node_id.as_str() + ); return Err(format!("视觉绑定返回了未知节点:{}", node_id.as_str())); } if !seen.insert(node_id.clone()) { + app_log!( + "ui_separation.error stage=binding_validate reason=duplicate_node node_id={}", + node_id.as_str() + ); return Err(format!("视觉绑定重复返回节点:{}", node_id.as_str())); } if let BindingDecision::NeedRework { @@ -151,12 +199,25 @@ pub fn validate_binding_response( } = decision { if problem_description.trim().is_empty() { + app_log!( + "ui_separation.error stage=binding_validate reason=empty_problem_description node_id={}", + node_id.as_str() + ); return Err("NeedRework 必须包含问题描述".to_string()); } } } if seen.len() != expected.len() { + app_log!( + "ui_separation.error stage=binding_validate reason=incomplete_coverage expected={} seen={}", + expected.len(), + seen.len() + ); return Err("视觉绑定未覆盖当前 batch 的全部节点".to_string()); } + app_log!( + "ui_separation.binding_validate.completed covered_nodes={}", + seen.len() + ); Ok(()) } 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 19e1ee531..dabc5f1a7 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 @@ -23,6 +23,12 @@ pub fn apply_batch_patch( decisions: &[BindingDecision], cut_paths: &std::collections::HashMap, ) -> Result<(), String> { + app_log!( + "ui_separation.batch_patch.start tree_index={} decisions={} cut_paths={}", + tree_index, + decisions.len(), + cut_paths.len() + ); let tree = state .unprocessed_trees .get_mut(tree_index) @@ -70,6 +76,14 @@ pub fn apply_batch_patch( } } remove_ids(&mut tree.root, &ids); + app_log!( + "ui_separation.batch_patch.completed tree_index={} removed_nodes={} bound={} problematic={} pending_root_children={}", + tree_index, + ids.len(), + state.bound.len(), + state.problematic_nodes.len(), + tree.root.children.len() + ); Ok(()) } @@ -79,6 +93,12 @@ fn mark_batch_problematic( batch: &[&SeparationNode], error: String, ) { + app_log!( + "ui_separation.batch_problematic tree_index={} nodes={} error={}", + tree_index, + batch.len(), + error + ); let ids = batch .iter() .map(|node| node.id.clone()) @@ -121,6 +141,12 @@ async fn raw_image_edit( width: u32, height: u32, ) -> Result { + app_log!( + "ui_separation.image_edit.start width={} height={} prompt_chars={}", + width, + height, + prompt.chars().count() + ); let (mime, data) = image_data_url .split_once(",") .ok_or_else(|| "界面图 data URL 无效".to_string())?; @@ -151,20 +177,37 @@ async fn raw_image_edit( ) .send() .await - .map_err(|e| format!("图片分离请求失败:{e}"))?; + .map_err(|e| { + app_log!("ui_separation.error stage=image_edit reason=send error={e}"); + format!("图片分离请求失败:{e}") + })?; if !response.status().is_success() { + app_log!( + "ui_separation.error stage=image_edit reason=http_status status={}", + response.status() + ); return Err(format!("图片分离请求失败(HTTP {})", response.status())); } - let payload = response - .json::() - .await - .map_err(|e| format!("解析图片分离响应失败:{e}"))?; - payload + let payload = response.json::().await.map_err(|e| { + app_log!("ui_separation.error stage=image_edit reason=parse_response error={e}"); + format!("解析图片分离响应失败:{e}") + })?; + let result = payload .data .into_iter() .next() .map(|item| format!("data:image/png;base64,{}", item.b64_json)) - .ok_or_else(|| "图片分离响应没有图像".to_string()) + .ok_or_else(|| "图片分离响应没有图像".to_string()); + match &result { + Ok(value) => app_log!( + "ui_separation.image_edit.completed data_url_chars={}", + value.chars().count() + ), + Err(error) => { + app_log!("ui_separation.error stage=image_edit reason=empty_result error={error}") + } + } + result } async fn build_marked_image( @@ -172,6 +215,14 @@ async fn build_marked_image( nodes: Vec, target: PathBuf, ) -> Result { + app_log!( + "ui_separation.mark_image.start nodes={} target_file={}", + nodes.len(), + target + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") + ); tokio::task::spawn_blocking(move || { let areas = nodes .iter() @@ -207,6 +258,12 @@ fn build_marked_image_blocking( .to_rgba8(); let width = image.width(); let height = image.height(); + app_log!( + "ui_separation.mark_image.decoded nodes={} width={} height={}", + nodes.len(), + width, + height + ); for &(global_pos_x_px, global_pos_y_px, node_width_px, node_height_px) in nodes { let x0 = global_pos_x_px.min(width.saturating_sub(1)); let y0 = global_pos_y_px.min(height.saturating_sub(1)); @@ -244,13 +301,26 @@ fn build_marked_image_blocking( image::DynamicImage::ImageRgba8(image) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .map_err(|e| format!("编码标记图失败:{e}"))?; - Ok(format!( + let result = format!( "data:image/png;base64,{}", base64::engine::general_purpose::STANDARD.encode(png) - )) + ); + app_log!( + "ui_separation.mark_image.completed data_url_chars={}", + result.len() + ); + Ok(result) } async fn write_processed_image(processed_url: String, target: PathBuf) -> Result<(), String> { + app_log!( + "ui_separation.processed_image.write.start target_file={} data_url_chars={}", + target + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + processed_url.chars().count() + ); tokio::task::spawn_blocking(move || { let processed_bytes = base64::engine::general_purpose::STANDARD .decode( @@ -260,8 +330,19 @@ async fn write_processed_image(processed_url: String, target: PathBuf) -> Result .unwrap_or_default(), ) .map_err(|error| format!("解析处理图失败:{error}"))?; + let byte_len = processed_bytes.len(); fs::write(&target, processed_bytes) .map_err(|error| format!("写入处理图失败:{}: {error}", target.display())) + .map(|_| { + app_log!( + "ui_separation.processed_image.write.completed target_file={} bytes={}", + target + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + byte_len + ); + }) }) .await .map_err(|error| format!("写入处理图任务失败:{error}"))? @@ -272,12 +353,27 @@ async fn visual_binding( processed_url: String, nodes: &[&SeparationNode], ) -> Result { + app_log!( + "ui_separation.visual_binding.start nodes={} source_url_chars={} processed_url_chars={}", + nodes.len(), + source_url.chars().count(), + processed_url.chars().count() + ); let llm_config = load_game_creator_app_config() - .map_err(|e| e.to_string())? + .map_err(|e| { + app_log!("ui_separation.error stage=visual_binding reason=load_config error={e}"); + e.to_string() + })? .llm; - let client = build_game_creator_llm_client_from_llm_config(&llm_config, "llm") - .map_err(|e| e.to_string())?; - let schema = strict_json_schema::()?; + let client = + build_game_creator_llm_client_from_llm_config(&llm_config, "llm").map_err(|e| { + app_log!("ui_separation.error stage=visual_binding reason=build_client error={e}"); + e.to_string() + })?; + let schema = strict_json_schema::().map_err(|error| { + app_log!("ui_separation.error stage=visual_binding reason=build_schema error={error}"); + error + })?; let tool = LlmFunctionTool::new( "bind_ui_elements", "确认处理图中的区域对应哪些 UI 节点", @@ -285,7 +381,7 @@ async fn visual_binding( ) .with_strict(true); let base_prompt = gen_binding_prompt(nodes.to_vec()); - request_with_feedback( + let result = request_with_feedback( 2, |feedback| { let prompt = feedback.map_or_else( @@ -333,7 +429,19 @@ async fn visual_binding( }, |value: &BindingResp| validate_binding_response(value, nodes), ) - .await + .await; + match &result { + Ok(value) => app_log!( + "ui_separation.visual_binding.completed nodes={} decisions={}", + nodes.len(), + value.decisions.len() + ), + Err(error) => app_log!( + "ui_separation.error stage=visual_binding reason=failed nodes={} error={error}", + nodes.len() + ), + } + result } pub(crate) async fn separate_ui_impl( @@ -341,25 +449,85 @@ pub(crate) async fn separate_ui_impl( asset_id: String, state: State, ) -> Result { + app_log!( + "ui_separation.start asset_id={} ui_trees={} ui_images={} sprites={}", + asset_id, + state.ui_trees.len(), + state.ui_design_images.len(), + state.sprite_assets.len() + ); let session = current_platform_session().ok_or_else(|| "请先登录平台账号".to_string())?; let root = Path::new(project_path.trim()); - let sidecar = separation_sidecar_dir(root, &asset_id)?; - fs::create_dir_all(&sidecar).map_err(|e| format!("创建 separation sidecar 失败:{e}"))?; + let sidecar = separation_sidecar_dir(root, &asset_id).map_err(|error| { + app_log!( + "ui_separation.error stage=sidecar_dir asset_id={} error={error}", + asset_id + ); + error + })?; + fs::create_dir_all(&sidecar).map_err(|e| { + app_log!( + "ui_separation.error stage=sidecar_create asset_id={} error={e}", + asset_id + ); + format!("创建 separation sidecar 失败:{e}") + })?; let state_path = sidecar.join("state.json"); - let mut separation = if state_path.exists() { - read_separation_state(&state_path)? + let restored = state_path.exists(); + let mut separation = if restored { + app_log!("ui_separation.state_restore.start asset_id={}", asset_id); + read_separation_state(&state_path).map_err(|error| { + app_log!( + "ui_separation.error stage=state_restore asset_id={} error={error}", + asset_id + ); + error + })? } else { + app_log!("ui_separation.state_construct.start asset_id={}", asset_id); construct_separation_state(&state) }; + app_log!( + "ui_separation.state_ready asset_id={} restored={} trees={} bound={} problematic={}", + asset_id, + restored, + separation.unprocessed_trees.len(), + separation.bound.len(), + separation.problematic_nodes.len() + ); for tree_index in 0..separation.unprocessed_trees.len() { let tree = &separation.unprocessed_trees[tree_index]; + let image_id = tree.src_ui_design.clone(); let image = state .ui_design_images - .get(&tree.src_ui_design) + .get(&image_id) .ok_or_else(|| "缺少源界面图".to_string())?; let source_path = crate::project::resolve_local_project_path(root, &image.path)?; - let source_url = read_ui_reference_image_data_url(source_path).await?; - write_separation_state(&state_path, &separation)?; + let source_url = read_ui_reference_image_data_url(source_path) + .await + .map_err(|error| { + app_log!( + "ui_separation.error stage=read_source tree_index={} image_id={} error={error}", + tree_index, + image_id.as_str() + ); + error + })?; + app_log!( + "ui_separation.tree_start tree_index={} image_id={} width={} height={}", + tree_index, + image_id.as_str(), + image.pixel_size.x.round() as u32, + image.pixel_size.y.round() as u32 + ); + write_separation_state(&state_path, &separation).map_err(|error| { + app_log!( + "ui_separation.error stage=state_checkpoint tree_index={} error={error}", + tree_index + ); + error + })?; + let mut batch_index = 0usize; loop { let Some(current_tree) = separation.unprocessed_trees.get(tree_index) else { break; @@ -369,11 +537,26 @@ pub(crate) async fn separate_ui_impl( .cloned() .collect::>(); if batch_nodes.is_empty() { + app_log!( + "ui_separation.tree_completed tree_index={} image_id={} bound={} problematic={}", + tree_index, + image_id.as_str(), + separation.bound.len(), + separation.problematic_nodes.len() + ); break; } let batch = batch_nodes.iter().collect::>(); let prompt = gen_extract_prompt(batch.iter().map(|n| n.note.clone()).collect::>()); + app_log!( + "ui_separation.batch_start tree_index={} batch_index={} nodes={} prompt_chars={} rework_total={}", + tree_index, + batch_index, + batch.len(), + prompt.chars().count(), + batch.iter().map(|node| node.rework_count).sum::() + ); let marker_path = sidecar.join(format!("marked-{}.png", separation.bound.len())); let marked_url = match build_marked_image( source_url.clone(), @@ -384,8 +567,14 @@ pub(crate) async fn separate_ui_impl( { Ok(value) => value, Err(error) => { + app_log!( + "ui_separation.error stage=mark_image tree_index={} batch_index={} error={error}", + tree_index, + batch_index + ); mark_batch_problematic(&mut separation, tree_index, &batch, error); write_separation_state(&state_path, &separation)?; + batch_index += 1; continue; } }; @@ -400,21 +589,51 @@ pub(crate) async fn separate_ui_impl( { Ok(value) => value, Err(error) => { + app_log!( + "ui_separation.error stage=image_edit tree_index={} batch_index={} error={error}", + tree_index, + batch_index + ); mark_batch_problematic(&mut separation, tree_index, &batch, error); write_separation_state(&state_path, &separation)?; + batch_index += 1; continue; } }; let processed_path = sidecar.join(format!("processed-{}.png", separation.bound.len())); - write_processed_image(processed_url.clone(), processed_path.clone()).await?; + if let Err(error) = + write_processed_image(processed_url.clone(), processed_path.clone()).await + { + app_log!( + "ui_separation.error stage=processed_image_write tree_index={} batch_index={} error={error}", + tree_index, + batch_index + ); + mark_batch_problematic(&mut separation, tree_index, &batch, error); + write_separation_state(&state_path, &separation)?; + batch_index += 1; + continue; + } let binding = match visual_binding(source_url.clone(), processed_url, &batch).await { Ok(value) => value, Err(error) => { + app_log!( + "ui_separation.error stage=visual_binding tree_index={} batch_index={} error={error}", + tree_index, + batch_index + ); mark_batch_problematic(&mut separation, tree_index, &batch, error); write_separation_state(&state_path, &separation)?; + batch_index += 1; continue; } }; + app_log!( + "ui_separation.binding_decisions tree_index={} batch_index={} decisions={}", + tree_index, + batch_index, + binding.decisions.len() + ); let mut cut_paths = std::collections::HashMap::new(); let mut cut_error = None; for decision in &binding.decisions { @@ -436,6 +655,12 @@ pub(crate) async fn separate_ui_impl( .insert(to_node.clone(), cut_path.to_string_lossy().to_string()); } Err(error) => { + app_log!( + "ui_separation.error stage=cut_image tree_index={} batch_index={} node_id={} error={error}", + tree_index, + batch_index, + to_node.as_str() + ); cut_error = Some(format!("节点 {} 的分离区域无效:{error}", to_node.as_str())); break; @@ -444,15 +669,48 @@ pub(crate) async fn separate_ui_impl( } } if let Some(error) = cut_error { + app_log!( + "ui_separation.error stage=cut_batch tree_index={} batch_index={} error={error}", + tree_index, + batch_index + ); mark_batch_problematic(&mut separation, tree_index, &batch, error); write_separation_state(&state_path, &separation)?; + batch_index += 1; continue; } apply_batch_patch(&mut separation, tree_index, &binding.decisions, &cut_paths)?; write_separation_state(&state_path, &separation)?; + app_log!( + "ui_separation.batch_completed tree_index={} batch_index={} cuts={} bound={} problematic={}", + tree_index, + batch_index, + cut_paths.len(), + separation.bound.len(), + separation.problematic_nodes.len() + ); + batch_index += 1; } } - let _ = fs::remove_file(state_path); + 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, + separation.bound.len(), + separation.problematic_nodes.len() + ); Ok(separation_dto(&separation)) } @@ -461,6 +719,21 @@ async fn cut_processed_image( area: BindingArea, target: PathBuf, ) -> Result<(), String> { + app_log!( + "ui_separation.cut_image.start source_file={} target_file={} area=({}, {}, {}, {})", + source + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + target + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + area.global_pos_x_px, + area.global_pos_y_px, + area.width_px, + area.height_px + ); tokio::task::spawn_blocking(move || cut_processed_image_blocking(&source, &area, &target)) .await .map_err(|error| format!("裁切处理图任务失败:{error}"))? @@ -481,7 +754,17 @@ fn cut_processed_image_blocking( ); cropped .save_with_format(target, ImageFormat::Png) - .map_err(|e| format!("写入 cut 图片失败:{e}")) + .map_err(|e| format!("写入 cut 图片失败:{e}"))?; + app_log!( + "ui_separation.cut_image.completed target_file={} width={} height={}", + target + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""), + area.width_px, + area.height_px + ); + Ok(()) } fn remove_ids(node: &mut SeparationNode, ids: &std::collections::HashSet) {