把 write-back 步骤拆成独立模块

新增 steps/write_back.rs,承载保存 State、追加 write-back / outdated 行与漂移文案
把 rebuild_target_state 从 run_workflow.rs 移到 steps/mod.rs,作为已记录 DTO 的重放入口
run_workflow.rs 只保留输入输出类型、三步编排与检查点恢复判定
把 DRIFT_REASON 与两条漂移文案收敛到 write_back 模块,避免编排层和写回层各写一套
同步两份 UI 编辑器技术方案的模块布局与代码地图
This commit is contained in:
2026-09-24 13:09:57 +08:00
parent 6ef64c3bcf
commit 0262e82572
5 changed files with 120 additions and 77 deletions
@@ -9,24 +9,22 @@ use super::checkpoint::{
checkpoint_timestamp, CheckpointDocument, CheckpointLine, RoundOutcome, WorkflowLog,
WorkflowRound,
};
use super::steps::recognize::apply_recognition;
use super::steps::separate::{add_sprite_assets, apply_separation, register_cut_image_sprites};
use super::steps::write_back::{
checkpoint_drift_message, record_write_back, WriteBackTarget, DRIFT_REASON,
};
use super::steps::{rebuild_target_state, recognize::apply_recognition};
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,
};
use crate::ui_editor::persistence::{
load_ui_design_document_snapshot_at, save_ui_design_state_at, SaveUiDesignStateInput,
SaveUiDesignStateResult, UiDesignStateSnapshot,
};
use crate::ui_editor::persistence::{load_ui_design_document_snapshot_at, UiDesignStateSnapshot};
use crate::ui_editor::state::State;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
const DRIFT_REASON: &str = "doc-state-drift";
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct RunUiDesignDocWorkflowInput {
@@ -139,13 +137,15 @@ pub(crate) async fn run_ui_design_doc_workflow(
));
let revision = if progress.already_written_back {
log.append(&CheckpointLine::WriteBack {
at: checkpoint_timestamp(),
revision: snapshot.revision,
})?;
record_write_back(&log, snapshot.revision)?;
snapshot.revision
} else {
write_back(&input, &log, snapshot.revision, &progress.state)?
WriteBackTarget {
project_path: input.project_path.trim(),
expected_project_id: &input.expected_project_id,
asset_id: &input.asset_id,
}
.commit(&log, snapshot.revision, &progress.state)?
};
// 与前端切分链路一致:干净跑完才清理 sidecar;有回填问题或问题节点时保留,
// 交给编辑器显示恢复入口。切分 op 内部的细粒度进度仍由 SeparationState 承担。
@@ -212,7 +212,7 @@ fn resume_progress(
at: checkpoint_timestamp(),
reason: DRIFT_REASON.to_string(),
})?;
return Err(drift_message(snapshot.revision, round));
return Err(checkpoint_drift_message(snapshot.revision, round));
}
Ok(WorkflowProgress {
state,
@@ -223,66 +223,6 @@ fn resume_progress(
})
}
/// 用已记录的 DTO 重建切分后的 State,供写回幂等判定复用。
fn rebuild_target_state(
root: &Path,
base: &State,
recognize: Option<&RecognitionDTO>,
separate: &SeparationDTO,
) -> Result<(State, Vec<String>), String> {
let mut state = base.clone();
if let Some(dto) = recognize {
apply_recognition(&mut state, dto)?;
}
let cut_images = register_cut_image_sprites(root, separate)?;
let mut backfill_errors = cut_images.errors;
state = add_sprite_assets(&state, &cut_images.sprites)?;
backfill_errors.extend(apply_separation(&mut state, separate, &cut_images.by_path));
Ok((state, backfill_errors))
}
fn write_back(
input: &RunUiDesignDocWorkflowInput,
log: &WorkflowLog,
expected_revision: u64,
state: &State,
) -> Result<u64, String> {
match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: input.project_path.trim().to_string(),
expected_project_id: input.expected_project_id.clone(),
asset_id: input.asset_id.clone(),
expected_revision,
state: state.clone(),
})? {
SaveUiDesignStateResult::Saved { revision, .. }
| SaveUiDesignStateResult::Unchanged { revision, .. } => {
log.append(&CheckpointLine::WriteBack {
at: checkpoint_timestamp(),
revision,
})?;
Ok(revision)
}
SaveUiDesignStateResult::Conflict { current } => {
log.append(&CheckpointLine::Outdated {
at: checkpoint_timestamp(),
reason: DRIFT_REASON.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}"))
}
@@ -1,2 +1,29 @@
pub(crate) mod recognize;
pub(crate) mod separate;
pub(crate) mod write_back;
use crate::ui_editor::commands::{RecognitionDTO, SeparationDTO};
use crate::ui_editor::state::State;
use std::path::Path;
/// 用已记录的 DTO 重放本轮的三步变换,得到目标 State;供写回幂等判定与恢复复用。
pub(crate) fn rebuild_target_state(
root: &Path,
base: &State,
recognize: Option<&RecognitionDTO>,
separate: &SeparationDTO,
) -> Result<(State, Vec<String>), String> {
let mut state = base.clone();
if let Some(dto) = recognize {
recognize::apply_recognition(&mut state, dto)?;
}
let cut_images = separate::register_cut_image_sprites(root, separate)?;
let mut backfill_errors = cut_images.errors;
state = separate::add_sprite_assets(&state, &cut_images.sprites)?;
backfill_errors.extend(separate::apply_separation(
&mut state,
separate,
&cut_images.by_path,
));
Ok((state, backfill_errors))
}
@@ -0,0 +1,74 @@
//! `write-back` 步骤:把本轮重建出的 State 保存进设计文档,并在检查点记下结果。
//!
//! 文档被外部改动时追加 `outdated` 行并报错,不在同一次调用里自动重开新一轮;
//! 恢复侧发现文档已等于目标 State 时只补 `write-back` 行,不重复保存。
use super::super::checkpoint::{checkpoint_timestamp, CheckpointLine, WorkflowLog, WorkflowRound};
use crate::ui_editor::persistence::{
save_ui_design_state_at, SaveUiDesignStateInput, SaveUiDesignStateResult,
};
use crate::ui_editor::state::State;
/// 文档中途漂移,本轮作废的检查点原因。
pub(crate) const DRIFT_REASON: &str = "doc-state-drift";
/// 本轮要写回的文档。
pub(crate) struct WriteBackTarget<'a> {
pub(crate) project_path: &'a str,
pub(crate) expected_project_id: &'a str,
pub(crate) asset_id: &'a str,
}
impl WriteBackTarget<'_> {
/// 保存 State 并追加 `write-back` 行;`Unchanged` 也记行,恢复判定只看行。
pub(crate) fn commit(
&self,
log: &WorkflowLog,
expected_revision: u64,
state: &State,
) -> Result<u64, String> {
match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: self.project_path.to_string(),
expected_project_id: self.expected_project_id.to_string(),
asset_id: self.asset_id.to_string(),
expected_revision,
state: state.clone(),
})? {
SaveUiDesignStateResult::Saved { revision, .. }
| SaveUiDesignStateResult::Unchanged { revision, .. } => {
record_write_back(log, revision)?;
Ok(revision)
}
SaveUiDesignStateResult::Conflict { current } => {
log.append(&CheckpointLine::Outdated {
at: checkpoint_timestamp(),
reason: DRIFT_REASON.to_string(),
})?;
Err(conflict_message(expected_revision, current.revision))
}
}
}
}
/// 文档已经是目标 State(保存成功但缺 `write-back` 行)时只补行。
pub(crate) fn record_write_back(log: &WorkflowLog, revision: u64) -> Result<(), String> {
log.append(&CheckpointLine::WriteBack {
at: checkpoint_timestamp(),
revision,
})
}
/// 保存时 revision 已经变了:说明文档在本轮中途被外部保存。
pub(crate) fn conflict_message(expected_revision: u64, current_revision: u64) -> String {
format!(
"UI 设计文档在本次工作流中途被改动(写回期望 revision {expected_revision},当前 {current_revision}),本轮已作废;请重新调用 run-workflow 开新一轮"
)
}
/// 恢复前的基线漂移:带上检查点轮次,便于判断是外部保存还是本轮重放。
pub(crate) fn checkpoint_drift_message(current_revision: u64, round: &WorkflowRound) -> String {
format!(
"UI 设计文档在本次工作流中途被改动(检查点第 {} 轮基线 revision {},当前 revision {current_revision}),本轮已作废;请重新调用 run-workflow 开新一轮",
round.at, round.base_revision
)
}
@@ -71,10 +71,12 @@ src/ui_editor/design_doc/
├─ checkpoint.rs JSONL 追加、读取、轮次判定
├─ run_workflow.rs recognize → separate → write-back 编排与恢复
├─ steps/
│ ├─ mod.rs 用已记录 DTO 重放目标 State
│ ├─ recognize.rs 识别 DTO 落 State
│ └─ separate/
│ ├─ mod.rs 切分 DTO 落 State(回填、清状态、写 NeedReview)
│ └─ cut_images.rs 切图图片登记与 SpriteAsset 构造
│ ├─ separate/
│ │ ├─ mod.rs 切分 DTO 落 State(回填、清状态、写 NeedReview)
│ │ └─ cut_images.rs 切图图片登记与 SpriteAsset 构造
│ └─ write_back.rs 保存 State、记 write-back / outdated 行、漂移文案
└─ test_support.rs design_doc 单测共用夹具
src/agent/runtime_tools/ui_design_doc.rs 工具参数解析与 Runtime 侧调用
```
@@ -27,7 +27,7 @@ view/ui-editor (页面/组件)
| `component/` | 组件枚举 `Component::{Image, Text}`、`NodeComponent`(LLM 工具载荷的 `PureNode`/`WithComponent` 判别式) |
| `resource/` | 界面图(`path` / `pixel_size` / `pixels_per_unit`)、sprite(含 `SpriteBorder` 九宫格)、字体(格式/媒体类型/CSS format)资源描述 |
| `persistence.rs` | 文档读写、`revision` 乐观并发保存、领域校验(重复 ID、树/资源引用、组件状态)、代码生成写盘 |
| `design_doc/` | Agent 工具链路:`creation.rs` 用一至四张设计图新建文档(登记未登记图片、按 `ui/UI 设计 N.json` 取号、持项目写锁装 revision 0、失败回滚)、`checkpoint.rs` JSONL 检查点日志、`run_workflow.rs` 三步编排与崩溃恢复、`steps/` 逐步落 State(`recognize.rs` 识别、`separate/` 切分:`cut_images.rs` 登记切图与 `SpriteAsset` 构造、`mod.rs` 回填与问题状态) |
| `design_doc/` | Agent 工具链路:`creation.rs` 用一至四张设计图新建文档(登记未登记图片、按 `ui/UI 设计 N.json` 取号、持项目写锁装 revision 0、失败回滚)、`checkpoint.rs` JSONL 检查点日志、`run_workflow.rs` 三步编排与崩溃恢复、`steps/` 逐步落 State(`mod.rs` 用已记录 DTO 重放目标 State、`recognize.rs` 识别、`separate/` 切分:`cut_images.rs` 登记切图与 `SpriteAsset` 构造、`mod.rs` 回填与问题状态、`write_back.rs` 保存与漂移文案) |
| `html_renderer/` | 由 State 生成 HTML 片段与 JS(maud + 布局/组件 CSS 映射),供预览与 `ui/generated-*.js` |
| `commands/` | LLM 工具链:`recognition`(结构识别)、`separation/`(自动切分素材)、`utils.rs`(LLM 请求、重试、`required_tool_arguments`) |
| `commands/separation/` | 切分批处理、截图/预切、sidecar 恢复(inspect / finalize / discard)、patch 回写 |