补充分离流程排障日志

记录 separation sidecar 状态读写与恢复阶段

记录树构造、批次选择、视觉绑定、裁切和 patch 结果

避免输出 base64、完整提示词和敏感凭据
This commit is contained in:
2026-09-09 12:06:45 +08:00
parent 15e30acb0f
commit 99c19e3ed9
3 changed files with 454 additions and 41 deletions
@@ -4,6 +4,7 @@ use std::fs;
use std::path::{Path, PathBuf};
pub fn separation_sidecar_dir(root: &Path, asset_id: &str) -> Result<PathBuf, String> {
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<PathBuf, St
crate::ui_editor::persistence::generated_file_stem(asset_id)
));
if !dir.starts_with(root) {
app_log!("ui_separation.error stage=sidecar_dir reason=path_escape");
return Err("separation sidecar 路径越界".to_string());
}
app_log!(
"ui_separation.sidecar_resolved asset_id={} directory={}",
asset_id,
dir.file_name()
.and_then(|name| name.to_str())
.unwrap_or("<unknown>")
);
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("<unknown>"),
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("<unknown>"),
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<SeparationState, String> {
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("<unknown>")
);
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(),
@@ -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::<Vec<_>>();
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(())
}
File diff suppressed because it is too large Load Diff