diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs index b698e4c7f..c7f9a4a0d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs @@ -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> { - 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 { - 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(value: &T) -> Result { serde_json::to_value(value).map_err(|error| format!("序列化工作流检查点行失败:{error}")) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/mod.rs index 7ef20d8a8..a6993e09e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/mod.rs @@ -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> { + 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)) +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/write_back.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/write_back.rs new file mode 100644 index 000000000..4e0e9e917 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/write_back.rs @@ -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 { + 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 + ) +} diff --git a/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md b/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md index c7111cdfc..f93394529 100644 --- a/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md +++ b/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md @@ -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 侧调用 ``` diff --git a/docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md b/docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md index 98241f0d0..81e4399eb 100644 --- a/docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md +++ b/docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md @@ -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 回写 |