From 116d9d594006beb9f6e3c945f71280ab0f4aab2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 21:08:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20run-workflow=20=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E3=80=81=E5=88=87=E5=9B=BE=E7=B4=A0=E6=9D=90=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E4=B8=8E=E6=A3=80=E6=9F=A5=E7=82=B9=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 design_doc/run_workflow.rs:recognize → separate → write-back 编排,按检查点行恢复,文档漂移追加 outdated 并报错 - 新增 design_doc/cut_images.rs:按切分路径登记图片并构造 SpriteAsset,按路径复用 manifest 条目、失败不回滚 - persistence 新增 load_ui_design_document_snapshot_at 并让 load_ui_design_state_at 复用它 - checkpoint 新增 checkpoint_timestamp 秒级时间戳 - 切分路径归一下沉到 steps/mod.rs,separate 步骤改用共享实现 - 技术方案同步 design_doc 模块布局与 into-js 复用 persistence 的说明 --- .../src/ui_editor/design_doc/checkpoint.rs | 9 + .../src/ui_editor/design_doc/cut_images.rs | 157 ++++++++++ .../src-tauri/src/ui_editor/design_doc/mod.rs | 3 + .../src/ui_editor/design_doc/run_workflow.rs | 272 ++++++++++++++++++ .../src/ui_editor/design_doc/steps/mod.rs | 5 + .../ui_editor/design_doc/steps/separate.rs | 7 +- .../src-tauri/src/ui_editor/persistence.rs | 24 +- ...œ¯方案】UI编辑器Agent工具化重写-2026-09-23.md | 17 +- 8 files changed, 477 insertions(+), 17 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/cut_images.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/checkpoint.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/checkpoint.rs index 656aa8a0c..ac122e83a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/checkpoint.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/checkpoint.rs @@ -9,6 +9,7 @@ use serde_json::Value; use std::fs::OpenOptions; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; const WORKFLOW_LOG_SUFFIX: &str = "-workflow.jsonl"; const TORN_TAIL_SCAN_BYTES: u64 = 64 * 1024; @@ -124,6 +125,14 @@ impl WorkflowLog { } } +/// 检查点行的时间戳:秒级 Unix 时间。 +pub(crate) fn checkpoint_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|elapsed| elapsed.as_secs()) + .unwrap_or(0) +} + /// 文档旁的同级隐藏日志:`ui/UI 设计 1.json` → `ui/.UI 设计 1-workflow.jsonl`。 pub(crate) fn workflow_log_path( root: &Path, diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/cut_images.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/cut_images.rs new file mode 100644 index 000000000..af64b1f9a --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/cut_images.rs @@ -0,0 +1,157 @@ +//! 登记自动切分产出的图片并构造切分素材。镜像前端 `useUiEditorPage.ts` 的 +//! `import_local_project_image_assets` → `prepareSpriteAssetBatch` 两段。 +//! +//! 登记按路径复用已有 manifest 条目,所以重放不会重复登记;登记失败的路径只回报 +//! 说明,不回滚已经登记成功的资源。 + +use super::steps::normalize_cut_image_path; +use crate::import_local_project_assets_for_agent; +use crate::ui_editor::commands::SeparationDTO; +use crate::ui_editor::resource::sprite::SpriteAsset; +use crate::ui_editor::utils::SpriteAssetId; +use crate::{resolve_local_project_path, ImportedAsset}; +use image::GenericImageView; +use nalgebra::Vector2; +use std::collections::HashMap; +use std::path::Path; +use typed_floats::tf32::StrictlyPositiveFinite; + +/// 单批登记上限,与前端 `SEPARATION_IMPORT_BATCH_SIZE` 一致。 +const CUT_IMAGE_IMPORT_BATCH_SIZE: usize = 100; + +pub(crate) struct CutImageSprites { + /// 本次切分产出的素材(按资源 id 去重)。 + pub(crate) sprites: Vec, + /// 切分路径(含导入后回填的路径)到素材的映射,供回填按路径查找。 + pub(crate) by_path: HashMap, + /// 未能登记的路径说明。 + pub(crate) errors: Vec, +} + +pub(crate) fn register_cut_image_sprites( + root: &Path, + dto: &SeparationDTO, +) -> Result { + let unique_paths = unique_cut_image_paths(dto); + if unique_paths.is_empty() { + return Ok(CutImageSprites { + sprites: Vec::new(), + by_path: HashMap::new(), + errors: Vec::new(), + }); + } + let mut imported_by_path: Vec<(String, ImportedAsset)> = Vec::new(); + for batch in unique_paths.chunks(CUT_IMAGE_IMPORT_BATCH_SIZE) { + let imported = import_local_project_assets_for_agent(root, batch)?; + if imported.assets.len() != batch.len() { + return Err(format!( + "本地资源登记结果数量不匹配:请求 {} 个,返回 {} 个", + batch.len(), + imported.assets.len() + )); + } + for (index, asset) in imported.assets.into_iter().enumerate() { + let normalized = normalize_cut_image_path(&asset.local_path); + if let Some(requested) = batch.get(index) { + // 导入可能把文件复制到 assets/uploads,切分路径与素材路径都要能查到。 + imported_by_path.push((requested.clone(), asset.clone())); + } + imported_by_path.push((normalized, asset)); + } + } + let mut errors = Vec::new(); + for path in &unique_paths { + if !imported_by_path + .iter() + .any(|(imported, _)| imported == path) + { + errors.push(format!("未能登记自动切分素材图片:{path}")); + } + } + let mut sprites = Vec::new(); + let mut sprite_by_id: HashMap = HashMap::new(); + for (_, asset) in &imported_by_path { + if sprite_by_id.contains_key(&asset.id) { + continue; + } + let sprite = sprite_from_registered_image(root, asset)?; + sprite_by_id.insert(asset.id.clone(), sprite.clone()); + sprites.push(sprite); + } + let by_path = imported_by_path + .iter() + .filter_map(|(path, asset)| { + sprite_by_id + .get(&asset.id) + .map(|sprite| (path.clone(), sprite.clone())) + }) + .collect(); + Ok(CutImageSprites { + sprites, + by_path, + errors, + }) +} + +/// 按 `bound_nodes` 顺序取去重后的切分路径,镜像前端的 `new Set(map(...))`。 +pub(crate) fn unique_cut_image_paths(dto: &SeparationDTO) -> Vec { + let mut paths = Vec::new(); + for bound in &dto.bound_nodes { + let path = normalize_cut_image_path(&bound.cut_image_path); + if !paths.contains(&path) { + paths.push(path); + } + } + paths +} + +fn sprite_from_registered_image(root: &Path, asset: &ImportedAsset) -> Result { + let absolute_path = resolve_local_project_path(root, &asset.local_path)?; + let dimensions = image::open(&absolute_path) + .map_err(|error| format!("读取自动切分素材失败:{}:{error}", asset.local_path))? + .dimensions(); + if dimensions.0 == 0 || dimensions.1 == 0 { + return Err(format!("自动切分素材尺寸无效:{}", asset.local_path)); + } + let asset_id = SpriteAssetId::new(asset.id.clone()) + .map_err(|error| format!("自动切分素材 ID 无效:{}:{error}", asset.id))?; + let pixels_per_unit = + StrictlyPositiveFinite::new(1.0).map_err(|_| "自动切分素材像素单位无效".to_string())?; + SpriteAsset::from_registered_image( + asset_id, + normalize_cut_image_path(&asset.local_path), + Vector2::new(dimensions.0 as f32, dimensions.1 as f32), + pixels_per_unit, + ) + .map_err(|error| format!("自动切分素材无效:{}:{error}", asset.local_path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui_editor::commands::separation::BoundNode; + use crate::ui_editor::utils::NodeId; + + fn bound(cut_path: &str) -> BoundNode { + BoundNode { + node_id: NodeId::new("node-a".to_string()).expect("node id"), + cut_image_path: cut_path.to_string(), + } + } + + #[test] + fn unique_paths_normalize_separators_and_drop_duplicates() { + let dto = SeparationDTO { + bound_nodes: vec![ + bound("/ui/cut 1.png"), + bound("ui\\cut 1.png"), + bound("ui/cut 2.png"), + ], + problematic_nodes: Vec::new(), + }; + assert_eq!( + unique_cut_image_paths(&dto), + vec!["ui/cut 1.png".to_string(), "ui/cut 2.png".to_string()] + ); + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/mod.rs index b44383e96..66ecc22cc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/mod.rs @@ -1,5 +1,7 @@ mod checkpoint; mod creation; +mod cut_images; +mod run_workflow; mod steps; #[cfg(test)] @@ -9,3 +11,4 @@ pub(crate) use creation::{ create_ui_design_doc_from_images, next_ui_design_path, CreateUiDesignDocFromImagesInput, UiDesignDocCreated, UiDesignImageReference, }; +pub(crate) use run_workflow::{run_ui_design_doc_workflow, RunUiDesignDocWorkflowInput}; 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 new file mode 100644 index 000000000..48089eed2 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/run_workflow.rs @@ -0,0 +1,272 @@ +//! `ui-design-doc.run-workflow` 的编排:`recognize` → `separate` → `write-back`, +//! 以及基于 JSONL 检查点的崩溃恢复。 +//! +//! 每步只做自己的事:识别结果落 State、切分结果落 State、最后写回文档。恢复判据 +//! 只有一条——这一步有没有对应的检查点行;文档中途漂移时追加 `outdated` 并报错, +//! 不在同一次调用里自动重开新一轮。 + +use super::checkpoint::{ + checkpoint_timestamp, CheckpointDocument, CheckpointLine, RoundOutcome, WorkflowLog, + WorkflowRound, +}; +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::{ + 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::state::State; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::path::Path; + +const WORKFLOW_COMMAND_ID: &str = "ui_design.workflow"; +const DRIFT_REASON: &str = "doc-state-drift"; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub(crate) struct RunUiDesignDocWorkflowInput { + pub(crate) project_path: String, + pub(crate) expected_project_id: String, + pub(crate) asset_id: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RunUiDesignDocWorkflowOutput { + pub(crate) asset_id: String, + pub(crate) relative_path: String, + pub(crate) revision: u64, + pub(crate) recovered_from_checkpoint: bool, + pub(crate) recognized_tree_count: usize, + pub(crate) bound_node_count: usize, + pub(crate) problematic_node_count: usize, + pub(crate) backfill_errors: Vec, +} + +/// 一轮已经推进到哪一步:已记录的 DTO 直接复用,缺失的步骤才真正执行。 +struct WorkflowProgress { + state: State, + recorded_recognize: Option, + recorded_separate: Option, + backfill_errors: Vec, + /// 文档已经是重建出的目标 State,只差补 `write-back` 行。 + already_written_back: bool, +} + +pub(crate) async fn run_ui_design_doc_workflow( + input: RunUiDesignDocWorkflowInput, + provider_identity: Option<(&str, &str)>, +) -> Result { + let root = Path::new(input.project_path.trim()); + enforce_project_permission_policy(root, WORKFLOW_COMMAND_ID)?; + let (relative_path, snapshot) = + load_ui_design_document_snapshot_at(root, &input.expected_project_id, &input.asset_id)?; + let log = WorkflowLog::open(root, &relative_path)?; + let resume = log + .last_round()? + .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)?, + None => { + let state = snapshot.state.clone(); + log.append(&CheckpointLine::Run { + at: checkpoint_timestamp(), + doc: CheckpointDocument { + asset_id: input.asset_id.clone(), + }, + revision: snapshot.revision, + state: to_checkpoint_value(&state)?, + })?; + WorkflowProgress { + state, + recorded_recognize: None, + recorded_separate: None, + backfill_errors: Vec::new(), + already_written_back: false, + } + } + }; + + let recognize_dto = match progress.recorded_recognize.take() { + Some(dto) => dto, + None => { + let dto = recognize_ui_impl_with_provider( + input.project_path.trim().to_string(), + progress.state.clone(), + provider_identity, + ) + .await?; + log.append(&CheckpointLine::Recognize { + at: checkpoint_timestamp(), + dto: to_checkpoint_value(&dto)?, + })?; + dto + } + }; + apply_recognition(&mut progress.state, &recognize_dto)?; + + let separate_dto = match progress.recorded_separate.take() { + Some(dto) => dto, + None => { + let dto = separate_ui_impl( + input.project_path.trim().to_string(), + input.asset_id.clone(), + progress.state.clone(), + ) + .await?; + log.append(&CheckpointLine::Separate { + at: checkpoint_timestamp(), + dto: to_checkpoint_value(&dto)?, + })?; + dto + } + }; + // 切图登记按路径复用 manifest 条目;失败只回报说明,不回滚已登记资源。 + let cut_images = register_cut_image_sprites(root, &separate_dto)?; + progress.backfill_errors.extend(cut_images.errors); + progress.state = add_sprite_assets(&progress.state, &cut_images.sprites)?; + progress.backfill_errors.extend(apply_separation( + &mut progress.state, + &separate_dto, + &cut_images.by_path, + )); + + let revision = if progress.already_written_back { + log.append(&CheckpointLine::WriteBack { + at: checkpoint_timestamp(), + revision: snapshot.revision, + })?; + snapshot.revision + } else { + write_back(root, &input, &log, snapshot.revision, &progress.state)? + }; + Ok(RunUiDesignDocWorkflowOutput { + asset_id: input.asset_id, + relative_path, + revision, + recovered_from_checkpoint, + recognized_tree_count: recognize_dto.ui_trees.len(), + bound_node_count: separate_dto.bound_nodes.len(), + problematic_node_count: separate_dto.problematic_nodes.len(), + backfill_errors: progress.backfill_errors, + }) +} + +/// 恢复一轮未完成的工作流:先按记录重建目标 State,若文档已经等于目标 State, +/// 说明保存成功但缺 `write-back` 行,直接补行;否则文档必须仍是本轮起点,否则判 +/// 漂移并追加 `outdated`。 +fn resume_progress( + root: &Path, + log: &WorkflowLog, + snapshot: &UiDesignStateSnapshot, + round: &WorkflowRound, +) -> Result { + let state: State = serde_json::from_value(round.base_state.clone()) + .map_err(|error| format!("工作流检查点的起始 State 无法还原:{error}"))?; + let recorded_recognize = round + .recognize_dto + .clone() + .map(serde_json::from_value::) + .transpose() + .map_err(|error| format!("工作流检查点的识别结果无法还原:{error}"))?; + let recorded_separate = round + .separate_dto + .clone() + .map(serde_json::from_value::) + .transpose() + .map_err(|error| format!("工作流检查点的切分结果无法还原:{error}"))?; + if let Some(separate) = &recorded_separate { + let (target, backfill_errors) = + rebuild_target_state(root, &state, recorded_recognize.as_ref(), separate)?; + if snapshot.state == target { + return Ok(WorkflowProgress { + state: target, + recorded_recognize, + recorded_separate, + backfill_errors, + already_written_back: true, + }); + } + } + if snapshot.state != state { + log.append(&CheckpointLine::Outdated { + at: checkpoint_timestamp(), + reason: DRIFT_REASON.to_string(), + })?; + return Err( + "UI 设计文档在本次工作流中途被改动,本轮已作废;请重新调用 run-workflow 开新一轮" + .to_string(), + ); + } + Ok(WorkflowProgress { + state, + recorded_recognize, + recorded_separate, + backfill_errors: Vec::new(), + already_written_back: false, + }) +} + +/// 用已记录的 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( + root: &Path, + 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 { .. } => { + log.append(&CheckpointLine::Outdated { + at: checkpoint_timestamp(), + reason: DRIFT_REASON.to_string(), + })?; + Err( + "UI 设计文档在本次工作流中途被改动,本轮已作废;请重新调用 run-workflow 开新一轮" + .to_string(), + ) + } + } +} + +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..6525ad603 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,7 @@ pub(crate) mod recognize; pub(crate) mod separate; + +/// 镜像前端 `normalizeProjectRelativePath`:只做分隔符与根斜杠归一,不做路径校验。 +pub(crate) fn normalize_cut_image_path(path: &str) -> String { + path.replace('\\', "/").trim_start_matches('/').to_string() +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/separate.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/separate.rs index 9ba1a3aeb..8eaebffad 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/separate.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/design_doc/steps/separate.rs @@ -121,7 +121,7 @@ fn backfill_bound_node( sprite_by_path: &HashMap, errors: &mut Vec, ) { - let path = normalize_cut_path(&bound.cut_image_path); + let path = super::normalize_cut_image_path(&bound.cut_image_path); let Some(sprite) = sprite_by_path.get(&path) else { errors.push(format!( "节点 {} 缺少已登记的自动切分素材图片:{path}", @@ -166,11 +166,6 @@ fn backfill_bound_node( } } -/// 镜像前端 `normalizeProjectRelativePath`:只做分隔符与根斜杠归一,不做路径校验。 -fn normalize_cut_path(path: &str) -> String { - path.replace('\\', "/").trim_start_matches('/').to_string() -} - fn sprite_asset_error(sprite: &SpriteAsset) -> Result<(), String> { if sprite.asset_id().as_str().trim().is_empty() { return Err("缺少素材 ID".to_string()); diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs index 06c443a9e..ad3d7101e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs @@ -169,16 +169,28 @@ pub(crate) fn load_ui_design_state_at( input: LoadUiDesignStateInput, ) -> Result { let root = Path::new(input.project_path.trim()); - let expected_project_id = required_identifier(&input.expected_project_id, "expectedProjectId")?; - let asset_id = required_identifier(&input.asset_id, "assetId")?; + Ok(load_ui_design_document_snapshot_at(root, &input.expected_project_id, &input.asset_id)?.1) +} + +/// 读取文档的登记路径与当前快照。工作流编排要据此定位检查点日志与起始 revision。 +pub(crate) fn load_ui_design_document_snapshot_at( + root: &Path, + expected_project_id: &str, + asset_id: &str, +) -> Result<(String, UiDesignStateSnapshot), String> { + let expected_project_id = required_identifier(expected_project_id, "expectedProjectId")?; + let asset_id = required_identifier(asset_id, "assetId")?; let asset = ui_design_asset(root, &expected_project_id, &asset_id)?; let document = read_ui_design_document(root, &asset.local_path, &expected_project_id, &asset_id)?; validate_document(&document, &expected_project_id, &asset_id)?; - Ok(UiDesignStateSnapshot { - revision: document.revision, - state: document.state, - }) + Ok(( + asset.local_path, + UiDesignStateSnapshot { + revision: document.revision, + state: document.state, + }, + )) } pub(crate) fn generate_ui_design_code_at( diff --git a/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md b/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md index a7ecbddf9..a0cd25d80 100644 --- a/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md +++ b/docs/technical/【技术方案】UI编辑器Agent工具化重写-2026-09-23.md @@ -64,14 +64,21 @@ ```text src/ui_editor/design_doc/ -├─ mod.rs 三个工具的对外入口与入参校验 -├─ creation.rs from-images:登记图片、建文档、manifest 注册、命名取号 -├─ checkpoint.rs JSONL 追加、读取、轮次判定 -├─ run_workflow.rs recognize → separate → write-back 编排与恢复 -└─ into_js.rs 复用 persistence 的代码生成 +├─ mod.rs 模块声明与三个工具的对外导出 +├─ creation.rs from-images:登记图片、建文档、manifest 注册、命名取号 +├─ checkpoint.rs JSONL 追加、读取、轮次判定 +├─ cut_images.rs 切图图片登记与 SpriteAsset 构造 +├─ run_workflow.rs recognize → separate → write-back 编排与恢复 +├─ steps/ +│ ├─ recognize.rs 识别 DTO 落 State +│ └─ separate.rs 切分 DTO 落 State(回填、清状态、写 NeedReview) +└─ test_support.rs design_doc 单测共用夹具 src/agent/runtime_tools/ui_design_doc.rs 工具参数解析与 Runtime 侧调用 ``` +`into-js` 不需要独立模块:它直接复用 `persistence.rs` 的 `generate_ui_design_code_at`, +该入口本来就只渲染 `ui/generated--.js` 且不推进项目 revision。 + 每个文件只承担一件事;`checkpoint.rs` 不感知切分,`creation.rs` 不感知识别。 ### 提示词目录模块