新增 run-workflow 编排、切图素材登记与检查点工具函数
- 新增 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 的说明
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<SpriteAsset>,
|
||||
/// 切分路径(含导入后回填的路径)到素材的映射,供回填按路径查找。
|
||||
pub(crate) by_path: HashMap<String, SpriteAsset>,
|
||||
/// 未能登记的路径说明。
|
||||
pub(crate) errors: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn register_cut_image_sprites(
|
||||
root: &Path,
|
||||
dto: &SeparationDTO,
|
||||
) -> Result<CutImageSprites, String> {
|
||||
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<String, SpriteAsset> = 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<String> {
|
||||
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<SpriteAsset, String> {
|
||||
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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
/// 一轮已经推进到哪一步:已记录的 DTO 直接复用,缺失的步骤才真正执行。
|
||||
struct WorkflowProgress {
|
||||
state: State,
|
||||
recorded_recognize: Option<RecognitionDTO>,
|
||||
recorded_separate: Option<SeparationDTO>,
|
||||
backfill_errors: Vec<String>,
|
||||
/// 文档已经是重建出的目标 State,只差补 `write-back` 行。
|
||||
already_written_back: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn run_ui_design_doc_workflow(
|
||||
input: RunUiDesignDocWorkflowInput,
|
||||
provider_identity: Option<(&str, &str)>,
|
||||
) -> Result<RunUiDesignDocWorkflowOutput, String> {
|
||||
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<WorkflowProgress, String> {
|
||||
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::<RecognitionDTO>)
|
||||
.transpose()
|
||||
.map_err(|error| format!("工作流检查点的识别结果无法还原:{error}"))?;
|
||||
let recorded_separate = round
|
||||
.separate_dto
|
||||
.clone()
|
||||
.map(serde_json::from_value::<SeparationDTO>)
|
||||
.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>), 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<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 { .. } => {
|
||||
log.append(&CheckpointLine::Outdated {
|
||||
at: checkpoint_timestamp(),
|
||||
reason: DRIFT_REASON.to_string(),
|
||||
})?;
|
||||
Err(
|
||||
"UI 设计文档在本次工作流中途被改动,本轮已作废;请重新调用 run-workflow 开新一轮"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_checkpoint_value<T: Serialize>(value: &T) -> Result<Value, String> {
|
||||
serde_json::to_value(value).map_err(|error| format!("序列化工作流检查点行失败:{error}"))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ fn backfill_bound_node(
|
||||
sprite_by_path: &HashMap<String, SpriteAsset>,
|
||||
errors: &mut Vec<String>,
|
||||
) {
|
||||
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());
|
||||
|
||||
@@ -169,16 +169,28 @@ pub(crate) fn load_ui_design_state_at(
|
||||
input: LoadUiDesignStateInput,
|
||||
) -> Result<UiDesignStateSnapshot, String> {
|
||||
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(
|
||||
|
||||
@@ -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-<stem>-<digest>.js` 且不推进项目 revision。
|
||||
|
||||
每个文件只承担一件事;`checkpoint.rs` 不感知切分,`creation.rs` 不感知识别。
|
||||
|
||||
### 提示词目录模块
|
||||
|
||||
Reference in New Issue
Block a user