补充分离流程排障日志
记录 separation sidecar 状态读写与恢复阶段 记录树构造、批次选择、视觉绑定、裁切和 patch 结果 避免输出 base64、完整提示词和敏感凭据
This commit is contained in:
+80
-11
@@ -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(())
|
||||
}
|
||||
|
||||
+308
-25
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user