收敛 design_doc 未使用接口并补齐切分 sidecar 收尾

- run-workflow 干净跑完后调用 finalize_separation 清理 sidecar,有回填问题时保留给编辑器恢复,与前端切分链路一致
- 检查点恢复校验轮次归属文档,漂移与写回冲突说明带上基线 revision
- WorkflowLog 删除仅供测试的 path 访问器,测试直接用 workflow_log_path
- mod.rs 不再导出仅内部使用的 next_ui_design_path,消除未使用告警
- 技术方案同步 sidecar 收尾口径
This commit is contained in:
2026-09-23 21:44:45 +08:00
parent 544f9ac040
commit 784f6f0e32
4 changed files with 43 additions and 23 deletions
@@ -85,10 +85,6 @@ impl WorkflowLog {
})
}
pub(crate) fn path(&self) -> &Path {
&self.path
}
/// 读取最后一轮。文件不存在或没有完整 `run` 行时返回 `None`。
pub(crate) fn last_round(&self) -> Result<Option<WorkflowRound>, String> {
let Some(content) = read_log_content(&self.path)? else {
@@ -274,6 +270,10 @@ mod tests {
use super::*;
use serde_json::json;
fn log_path(directory: &tempfile::TempDir) -> PathBuf {
workflow_log_path(directory.path(), "ui/UI 设计 1.json").expect("workflow log path")
}
fn log(directory: &tempfile::TempDir) -> WorkflowLog {
WorkflowLog::open(directory.path(), "ui/UI 设计 1.json").expect("open workflow log")
}
@@ -368,7 +368,7 @@ mod tests {
{
let mut file = OpenOptions::new()
.append(true)
.open(log.path())
.open(log_path(&directory))
.expect("open raw log");
file.write_all(b"{\"type\":\"separate\",\"at\":12,\"dt")
.expect("write torn tail");
@@ -383,7 +383,7 @@ mod tests {
.expect("append after torn tail");
let round = log.last_round().expect("scan").expect("round");
assert_eq!(round.outcome, RoundOutcome::WrittenBack { revision: 2 });
let content = std::fs::read_to_string(log.path()).expect("read log");
let content = std::fs::read_to_string(log_path(&directory)).expect("read log");
assert!(!content.contains("\"dt"));
assert!(content.ends_with('\n'));
}
@@ -392,8 +392,9 @@ mod tests {
fn malformed_complete_line_fails_closed() {
let directory = tempfile::tempdir().expect("temp dir");
let log = log(&directory);
std::fs::create_dir_all(log.path().parent().expect("parent")).expect("create dir");
std::fs::write(log.path(), "{\"type\":\"nonsense\"}\n").expect("write bad line");
std::fs::create_dir_all(log_path(&directory).parent().expect("parent"))
.expect("create dir");
std::fs::write(log_path(&directory), "{\"type\":\"nonsense\"}\n").expect("write bad line");
assert!(log.last_round().is_err());
}
}
@@ -8,7 +8,7 @@ mod steps;
mod test_support;
pub(crate) use creation::{
create_ui_design_doc_from_images, next_ui_design_path, CreateUiDesignDocFromImagesInput,
UiDesignDocCreated, UiDesignImageReference,
create_ui_design_doc_from_images, CreateUiDesignDocFromImagesInput, UiDesignDocCreated,
UiDesignImageReference,
};
pub(crate) use run_workflow::{run_ui_design_doc_workflow, RunUiDesignDocWorkflowInput};
@@ -13,6 +13,7 @@ use super::cut_images::register_cut_image_sprites;
use super::steps::recognize::apply_recognition;
use super::steps::separate::{add_sprite_assets, apply_separation};
use crate::enforce_project_permission_policy;
use crate::ui_editor::commands::separation::finalize_separation;
use crate::ui_editor::commands::{
recognize_ui_impl_with_provider, separate_ui_impl, RecognitionDTO, SeparationDTO,
};
@@ -73,7 +74,7 @@ pub(crate) async fn run_ui_design_doc_workflow(
.filter(|round| round.outcome == RoundOutcome::Open);
let recovered_from_checkpoint = resume.is_some();
let mut progress = match &resume {
Some(round) => resume_progress(root, &log, &snapshot, round)?,
Some(round) => resume_progress(root, &log, &snapshot, round, &input.asset_id)?,
None => {
let state = snapshot.state.clone();
log.append(&CheckpointLine::Run {
@@ -145,8 +146,13 @@ pub(crate) async fn run_ui_design_doc_workflow(
})?;
snapshot.revision
} else {
write_back(root, &input, &log, snapshot.revision, &progress.state)?
write_back(&input, &log, snapshot.revision, &progress.state)?
};
// 与前端切分链路一致:干净跑完才清理 sidecar;有回填问题或问题节点时保留,
// 交给编辑器显示恢复入口。切分 op 内部的细粒度进度仍由 SeparationState 承担。
if progress.backfill_errors.is_empty() && separate_dto.problematic_nodes.is_empty() {
finalize_separation(root, &input.asset_id)?;
}
Ok(RunUiDesignDocWorkflowOutput {
asset_id: input.asset_id,
relative_path,
@@ -167,7 +173,14 @@ fn resume_progress(
log: &WorkflowLog,
snapshot: &UiDesignStateSnapshot,
round: &WorkflowRound,
asset_id: &str,
) -> Result<WorkflowProgress, String> {
if round.document_asset_id != asset_id {
return Err(format!(
"工作流检查点属于其它 UI 设计文档({}),拒绝按本轮恢复",
round.document_asset_id
));
}
let state: State = serde_json::from_value(round.base_state.clone())
.map_err(|error| format!("工作流检查点的起始 State 无法还原:{error}"))?;
let recorded_recognize = round
@@ -200,10 +213,7 @@ fn resume_progress(
at: checkpoint_timestamp(),
reason: DRIFT_REASON.to_string(),
})?;
return Err(
"UI 设计文档在本次工作流中途被改动,本轮已作废;请重新调用 run-workflow 开新一轮"
.to_string(),
);
return Err(drift_message(snapshot.revision, round));
}
Ok(WorkflowProgress {
state,
@@ -233,7 +243,6 @@ fn rebuild_target_state(
}
fn write_back(
root: &Path,
input: &RunUiDesignDocWorkflowInput,
log: &WorkflowLog,
expected_revision: u64,
@@ -254,19 +263,27 @@ fn write_back(
})?;
Ok(revision)
}
SaveUiDesignStateResult::Conflict { .. } => {
SaveUiDesignStateResult::Conflict { current } => {
log.append(&CheckpointLine::Outdated {
at: checkpoint_timestamp(),
reason: DRIFT_REASON.to_string(),
})?;
Err(
"UI 设计文档在本次工作流中途被改动,本轮已作废;请重新调用 run-workflow 开新一轮"
.to_string(),
)
Err(format!(
"UI 设计文档在本次工作流中途被改动(写回期望 revision {expected_revision},当前 {},本轮已作废;请重新调用 run-workflow 开新一轮",
current.revision
))
}
}
}
/// 漂移说明带上检查点轮次的基线,便于判断是外部保存还是本轮重放。
fn drift_message(current_revision: u64, round: &WorkflowRound) -> String {
format!(
"UI 设计文档在本次工作流中途被改动(检查点第 {} 轮基线 revision {},当前 revision {current_revision}),本轮已作废;请重新调用 run-workflow 开新一轮",
round.at, round.base_revision
)
}
fn to_checkpoint_value<T: Serialize>(value: &T) -> Result<Value, String> {
serde_json::to_value(value).map_err(|error| format!("序列化工作流检查点行失败:{error}"))
}
@@ -35,7 +35,7 @@
| --- | --- | --- |
| `recognize` | `recognize_ui_impl_with_provider` | `ui_trees`(每棵树的 `src_ui_design` 指向文档内设计图) |
| `separate` | `separate_ui_impl` | 切分图落盘 → 登记 asset → 转 `SpriteAsset` → 写入 State → 回填 `target_graphic`、清 `component_status`、写 `NeedReview` |
| `write-back` | `save_ui_design_state_at` | 文档新 `revision` |
| `write-back` | `save_ui_design_state_at` | 文档新 `revision`;没有回填问题且没有问题节点时再 `finalize_separation` 清理 sidecar |
不再有合并、组件绑定与页面级 profile/finalize 阶段;`recognize` 之前不做任何前置发现。
@@ -55,6 +55,8 @@
- 漂移:文档在轮次中途被改动(当前 State 与 `run` 行快照不一致)时,追加一行 `outdated` 并**返回错误**,不在同一次调用里自动重开新一轮;下一次调用看到 `outdated` 才从头开新一轮,且以当前文档为基准。
- 写回幂等:`save` 成功但 `write-back` 行没追加时,重放会重建出同一份目标 State,此时按「目标 State 与文档当前 State 相等」判定为已写完,直接补 `write-back` 行并返回成功。这条判据成立的前提是工作流这条路不产生随机身份:识别树的根节点 id 来自 DTO,切图资源 id 走 manifest 的 by-path 复用。
- 切图资源不回滚:切分产出的图片与已登记资源在失败后保留,重放靠 by-path 复用接上,不做回滚清理。
- 写回成功后才清理 sidecar:与前端切分链路一致,`backfill_errors` 为空且 `problematic_nodes` 为空时调用
`finalize_separation`;有回填问题或问题节点时保留 sidecar,交给编辑器显示恢复入口。
- 切分 op 内部的细粒度恢复仍由 `SeparationState` 承担(`ui-editor-separation-state.v2` sidecar),日志只记录工作流层面的步骤完成,不复制它的进度。
- 不引入跨语言 fixture 比对:镜像的四个 seam 都是简单变换,靠同语义实现与各自单测覆盖,不为它们额外维护一套 golden。