Compare commits

...

21 Commits

Author SHA1 Message Date
k88936 1d6e7057bc 移除识别绑定旧计算命名
清理请求错误、结果校验和日志中的旧计算及联合文案
2026-09-05 15:39:17 +08:00
k88936 6daff74c85 更新并简化 UI 识别与绑定系统提示 2026-09-05 15:22:26 +08:00
k88936 ad49056d36 更新 UI workflow 阶段决策记录
移除历史阶段描述并记录现行阶段与无迁移约束
2026-09-05 14:14:37 +08:00
k88936 28af87e3ad 统一 UI workflow 阶段并拒绝未知值
现行阶段原地使用并移除旧阶段 fallback 与迁移兼容
2026-09-05 14:11:00 +08:00
k88936 d994ccaa78 更正独立素材上限语义
将128限制恢复为独立素材数量且不计入页面参考图
2026-09-05 13:33:52 +08:00
k88936 42f1109b90 限制联合图片上下文总量
将页面参考图与独立素材的单次请求总数限制为128张
2026-09-05 13:30:03 +08:00
k88936 b772ec3efb 记录联合识别重跑与阶段排名语义
说明重复识别会生成新结果并明确 workflow_stage_rank 的后端持久化边界
2026-09-05 13:25:38 +08:00
k88936 d014cb1d06 限制联合请求独立素材总量
将单次识别绑定请求的独立素材数量限制为128个
2026-09-05 13:24:58 +08:00
k88936 e34ae89db3 移除废弃的独立识别路径
让联合识别绑定成为 UI 编辑器唯一实现并清理旧状态投影
2026-09-05 13:11:38 +08:00
k88936 88853d0a30 清理联合识别后的节点选择
替换生成新树后清除已失效的旧节点选中状态
2026-09-05 13:09:40 +08:00
k88936 fabc64df6c 避免工作流渲染校验重复克隆状态
直接复用联合结果写入后的 State 完成组件校验
2026-09-05 13:08:50 +08:00
k88936 27803a6e78 明确联合结果按图片返回树
让提示与每张输入图一棵树的校验契约一致
2026-09-05 13:07:27 +08:00
k88936 828b3e1173 消除联合请求初始化崩溃路径
将 provider client 与配置 unwrap 改为显式错误
2026-09-05 13:02:38 +08:00
k88936 deb60065a5 校验联合结果素材引用
拒绝不存在的图片素材组件引用

拒绝不存在的字体绑定引用
2026-09-05 12:58:49 +08:00
k88936 5c6a940b9c 稳定联合识别图片上下文顺序
页面角色优先作为 ROOT

其余界面图按 ID 排序保持请求可复现
2026-09-05 12:49:56 +08:00
k88936 8df5ebff69 简化联合工作流动作状态
移除旧识别状态回退与重复分支

统一联合步骤的运行中和完成态显示
2026-09-05 12:47:43 +08:00
k88936 041b51adc1 归一化UI编辑器完成态入口
将旧 visual-binding 初始步骤归一到联合识别绑定步骤

简化工作流步骤索引计算
2026-09-05 12:46:55 +08:00
k88936 5a31809e21 修正联合概览状态定位
结构问题高亮 layout_status

组件问题高亮 components_status
2026-09-05 12:44:26 +08:00
k88936 44bebae0a9 统一UI编辑器联合计算入口
工作流导航收敛为参考分析与一键识别绑定两步

联合概览同时展示结构与组件状态
2026-09-05 12:01:54 +08:00
k88936 62793ac445 正式合并UI识别与组件绑定
删除旧 recognition 与 binding 命令及前端 DTO

新增 recognition_and_binding 单次 workflow 与联合概览

移除 debug 超时特判并直达 binding-ready
2026-09-05 11:55:47 +08:00
k88936 f630c518f3 重构UI工作流联合识别绑定契约
更新 UI workflow 以 recognition_and_binding 单次计算为正式识别路径

保留 merge 能力但移出联合识别绑定流程

声明联合提示词 placeholder 与 binding-ready 直达阶段
2026-09-05 11:07:42 +08:00
22 changed files with 392 additions and 1405 deletions
@@ -321,11 +321,11 @@ async fn suggest_ui_design_semantic(
}
#[tauri::command]
async fn recognize_ui(
async fn recognition_and_binding(
project_path: String,
state: ui_editor::state::State,
) -> Result<ui_editor::commands::RecognitionDTO, String> {
ui_editor::commands::recognize_ui_impl(project_path, state).await
) -> Result<ui_editor::commands::RecognitionAndBindingDTO, String> {
ui_editor::commands::recognition_and_binding_impl(project_path, state).await
}
#[tauri::command]
@@ -333,15 +333,6 @@ async fn merge_ui(state: ui_editor::state::State) -> Result<ui_editor::commands:
ui_editor::commands::merge_ui_impl(state).await
}
#[tauri::command]
async fn bind_components(
project_path: String,
state: ui_editor::state::State,
sprite_ids: Vec<String>,
) -> Result<ui_editor::commands::BindingDTO, String> {
ui_editor::commands::bind_components_impl(project_path, state, sprite_ids).await
}
#[tauri::command]
fn load_ui_design_state(
input: ui_editor::persistence::LoadUiDesignStateInput,
@@ -2540,9 +2531,8 @@ fn main() {
read_ui_editor_font_bytes,
check_ui_editor_font_glyph_coverage,
suggest_ui_design_semantic,
recognize_ui,
recognition_and_binding,
merge_ui,
bind_components,
load_ui_design_state,
save_ui_design_state,
generate_ui_design_code,
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,13 @@
pub mod binding;
pub mod merge;
pub mod recognition;
pub mod recognition_and_binding;
pub mod ui_design_suggestion;
pub mod utils;
pub use binding::BindingDTO;
pub(crate) use binding::{bind_components_impl, bind_components_impl_with_provider};
pub(crate) use merge::merge_ui_impl;
pub use merge::MergeDTO;
pub(crate) use merge::{merge_ui_impl, merge_ui_impl_with_provider};
pub use recognition::RecognitionDTO;
pub(crate) use recognition::{recognize_ui_impl, recognize_ui_impl_with_provider};
pub use recognition_and_binding::RecognitionAndBindingDTO;
pub(crate) use recognition_and_binding::{
recognition_and_binding_impl, recognition_and_binding_impl_with_provider,
};
pub(crate) use ui_design_suggestion::suggest_ui_design_semantic_impl;
pub use ui_design_suggestion::UIDesignSuggestionTreeNode;
@@ -1,8 +1,4 @@
use crate::ui_editor::commands::binding::BindingChange;
use crate::ui_editor::commands::{
bind_components_impl_with_provider, merge_ui_impl_with_provider,
recognize_ui_impl_with_provider,
};
use crate::ui_editor::commands::recognition_and_binding_impl_with_provider;
use crate::ui_editor::layout::node::{Node, StageStatus};
use crate::ui_editor::persistence::{
initialize_ui_design_state_at, load_ui_design_state_at, save_ui_design_state_at,
@@ -96,7 +92,6 @@ struct UiWorkflowPageDeclaration {
#[serde(rename_all = "kebab-case")]
pub(crate) enum UiWorkflowPageStage {
ReferenceReady,
StructureReady,
BindingReady,
ApplicationReady,
Completed,
@@ -258,7 +253,7 @@ pub(crate) async fn run_ui_workflow_at_with_provider(
// binding commands as the editor. There is intentionally no
// deterministic fallback here: a missing provider or malformed
// response is returned to the caller and leaves the durable stage
// at reference-ready/structure-ready rather than claiming UI
// at reference-ready rather than claiming UI
// semantics were recognized.
recognize_page_semantics(root, &manifest.project_id, page, provider_identity).await?;
}
@@ -689,6 +684,17 @@ fn find_page_ui_resource(
{
return Err(format!("页面 {} 的 UI workflow 资源身份冲突", page.page_id));
}
let generation_kind = asset
.source
.generation_kind
.as_deref()
.ok_or_else(|| format!("页面 {} 的 UI workflow 阶段缺失", page.page_id))?;
if workflow_stage_rank(generation_kind).is_none() {
return Err(format!(
"页面 {} 的 UI workflow 阶段无效:{generation_kind}",
page.page_id
));
}
let mut expected_references = vec![
canonical_resource_id(source),
canonical_resource_id(design_asset),
@@ -760,7 +766,7 @@ fn ensure_page_ui_resource(
prompt: None,
model: None,
generation_route: None,
generation_kind: Some("ui-workflow".to_string()),
generation_kind: Some("ui-workflow.reference-ready".to_string()),
reference_resource_ids: {
let mut references = vec![
canonical_resource_id(source),
@@ -924,10 +930,9 @@ fn install_page_component_assets(
/// Runs the provider-backed editor pipeline for one workflow page.
///
/// `recognize_ui_impl` owns multimodal semantic recognition and strict tool
/// response validation. `bind_components_impl` owns visual component binding
/// and its allowlisted sprite validation. This wrapper only persists their
/// DTOs under the UI State revision gate; it never manufactures a tree when a
/// `recognition_and_binding_impl` owns the single multimodal request, strict
/// response validation, and allowlisted sprite validation. This wrapper only
/// persists its DTO under the UI State revision gate; it never manufactures a tree when a
/// provider is unavailable or returns an invalid result.
async fn recognize_page_semantics(
root: &Path,
@@ -942,225 +947,44 @@ async fn recognize_page_semantics(
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
})?;
let mut stage = page
.ui_asset
.source
.generation_kind
.as_deref()
.unwrap_or("ui-workflow")
.to_string();
let has_page_tree = snapshot
.state
.ui_trees
.iter()
.filter(|tree| tree.src_ui_design == image_id)
.count()
== 1;
// Every provider-backed phase is persisted independently. A retry resumes
// from the latest truthful manifest stage instead of repeating completed
// calls or manufacturing fallback output.
if !matches!(
stage.as_str(),
"ui-workflow.structure-ready" | "ui-workflow.merge-ready" | "ui-workflow.binding-ready"
) {
let project_path = root.to_string_lossy().into_owned();
let recognition = recognize_ui_impl_with_provider(
project_path,
snapshot.state.clone(),
provider_identity,
)
.await
.map_err(|error| format!("页面 {} UI 语义识别失败:{error}", page.input.page_id))?;
if recognition.ui_trees.len() != 1 || recognition.ui_trees[0].src_ui_design != image_id {
return Err(format!(
"页面 {} UI 语义识别返回的树与页面设计图不匹配",
page.input.page_id
));
}
let mut state = snapshot.state;
state.ui_trees = recognition.ui_trees;
match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
expected_revision: snapshot.revision,
state,
})? {
SaveUiDesignStateResult::Saved { .. } | SaveUiDesignStateResult::Unchanged { .. } => {}
SaveUiDesignStateResult::Conflict { .. } => {
return Err(format!(
"页面 {} UI 语义识别保存 revision 冲突,请重试",
page.input.page_id
));
}
}
update_page_manifest_stage(root, &page.ui_asset.id, "structure-ready")?;
stage = "ui-workflow.structure-ready".to_string();
} else if !has_page_tree {
let result = recognition_and_binding_impl_with_provider(
root.to_string_lossy().into_owned(),
snapshot.state.clone(),
provider_identity,
)
.await
.map_err(|error| format!("页面 {} UI 联合识别绑定失败:{error}", page.input.page_id))?;
if result.ui_trees.len() != 1 || result.ui_trees[0].src_ui_design != image_id {
return Err(format!(
"页面 {} manifest 已记录语义识别阶段,但 UI State 缺少唯一结构树",
"页面 {} UI 联合识别绑定返回的树与页面设计图不匹配",
page.input.page_id
));
}
if stage == "ui-workflow.binding-ready" {
return Ok(());
}
if stage == "ui-workflow.structure-ready" {
let merge_snapshot = load_ui_design_state_at(LoadUiDesignStateInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
})?;
let merged = merge_ui_impl_with_provider(
root.to_string_lossy().into_owned(),
merge_snapshot.state.clone(),
provider_identity,
)
.await
.map_err(|error| format!("页面 {} UI 多树合并失败:{error}", page.input.page_id))?;
if merged.ui_tree.src_ui_design != image_id {
return Err(format!(
"页面 {} UI 多树合并结果未绑定主页面设计图",
page.input.page_id
));
}
let mut state = merge_snapshot.state;
state.ui_trees = vec![merged.ui_tree];
match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
expected_revision: merge_snapshot.revision,
state,
})? {
SaveUiDesignStateResult::Saved { .. } | SaveUiDesignStateResult::Unchanged { .. } => {}
SaveUiDesignStateResult::Conflict { .. } => {
return Err(format!(
"页面 {} UI 多树合并保存 revision 冲突,请重试",
page.input.page_id
));
}
}
update_page_manifest_stage(root, &page.ui_asset.id, "merge-ready")?;
stage = "ui-workflow.merge-ready".to_string();
}
if stage != "ui-workflow.merge-ready" {
let mut state = snapshot.state;
state.ui_trees = result.ui_trees;
if !state_has_renderable_component(&state) {
return Err(format!(
"页面 {} UI workflow 阶段无法进入组件绑定:{stage}",
"页面 {} UI 联合识别绑定未形成可渲染组件",
page.input.page_id
));
}
let mut binding_snapshot = load_ui_design_state_at(LoadUiDesignStateInput {
match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
})?;
let mut sprite_ids = binding_snapshot
.state
.sprite_assets
.keys()
.map(|id| id.as_str().to_string())
.collect::<Vec<_>>();
sprite_ids.sort();
if sprite_ids.is_empty() {
return Err(format!(
"页面 {} UI 语义识别已完成,但没有可用于组件绑定的页面素材",
page.input.page_id
));
}
let mut changed_nodes = 0usize;
for batch in sprite_ids.chunks(crate::ui_editor::commands::binding::ASSET_BATCH_SIZE) {
let binding = bind_components_impl_with_provider(
root.to_string_lossy().into_owned(),
binding_snapshot.state.clone(),
batch.to_vec(),
provider_identity,
)
.await
.map_err(|error| format!("页面 {} UI 组件语义绑定失败:{error}", page.input.page_id))?;
if binding.changes.is_empty() {
continue;
expected_revision: snapshot.revision,
state,
})? {
SaveUiDesignStateResult::Saved { .. } | SaveUiDesignStateResult::Unchanged { .. } => {}
SaveUiDesignStateResult::Conflict { .. } => {
return Err(format!(
"页面 {} UI 联合识别绑定保存 revision 冲突,请重试",
page.input.page_id
));
}
let mut state = binding_snapshot.state.clone();
let changes = binding
.changes
.into_iter()
.map(|change| (change.node_id.clone(), change))
.collect::<HashMap<_, _>>();
changed_nodes += apply_binding_changes(&mut state.ui_trees, &changes);
binding_snapshot = match save_ui_design_state_at(SaveUiDesignStateInput {
project_path: root.to_string_lossy().into_owned(),
expected_project_id: project_id.to_string(),
asset_id: page.ui_asset.id.clone(),
expected_revision: binding_snapshot.revision,
state,
})? {
SaveUiDesignStateResult::Saved {
state, revision, ..
}
| SaveUiDesignStateResult::Unchanged {
state, revision, ..
} => crate::ui_editor::persistence::UiDesignStateSnapshot { state, revision },
SaveUiDesignStateResult::Conflict { .. } => {
return Err(format!(
"页面 {} UI 组件绑定保存 revision 冲突,请重试",
page.input.page_id
));
}
};
}
if changed_nodes == 0 || !state_has_renderable_component(&binding_snapshot.state) {
return Err(format!(
"页面 {} UI 组件语义绑定未形成可渲染组件,拒绝进入 binding-ready",
page.input.page_id
));
}
let mut binding_blockers = Vec::new();
let mut component_count = 0usize;
for tree in &binding_snapshot.state.ui_trees {
collect_binding_blockers(&tree.root, &mut component_count, &mut binding_blockers);
}
if !binding_blockers.is_empty() {
// Keep the provider result available for review, but do not claim the
// binding stage. A subsequent recognize operation can retry binding
// from the durable structure-ready state.
return Ok(());
}
update_page_manifest_stage(root, &page.ui_asset.id, "binding-ready")
}
fn apply_binding_changes(
trees: &mut [crate::ui_editor::state::UITree],
changes: &HashMap<crate::ui_editor::utils::NodeId, BindingChange>,
) -> usize {
fn apply_node(
node: &mut Node,
changes: &HashMap<crate::ui_editor::utils::NodeId, BindingChange>,
) -> usize {
let mut changed = 0;
if let Some(change) = changes.get(&node.id) {
node.components = change.components.clone();
node.metadata.components_status = change.components_status.clone();
changed += 1;
}
for child in &mut node.children {
changed += apply_node(child, changes);
}
changed
}
trees
.iter_mut()
.map(|tree| apply_node(&mut tree.root, changes))
.sum()
}
fn state_has_renderable_component(state: &crate::ui_editor::state::State) -> bool {
fn has_component(node: &Node) -> bool {
!node.components.is_empty() || node.children.iter().any(has_component)
@@ -1183,13 +1007,16 @@ fn update_page_manifest_stage(root: &Path, asset_id: &str, stage: &str) -> Resul
return Err(format!("UI workflow 资源 {} 类型不匹配", asset_id));
}
let next_kind = format!("ui-workflow.{stage}");
let current_rank = asset
let current_kind = asset
.source
.generation_kind
.as_deref()
.and_then(workflow_stage_rank)
.unwrap_or(0);
let next_rank = workflow_stage_rank(&next_kind).unwrap_or(0);
.ok_or_else(|| format!("UI workflow 资源 {} 阶段缺失", asset_id))?;
let current_rank = workflow_stage_rank(current_kind).ok_or_else(|| {
format!("UI workflow 资源 {} 阶段无效:{current_kind}", asset_id)
})?;
let next_rank = workflow_stage_rank(&next_kind)
.ok_or_else(|| format!("UI workflow 下一阶段无效:{next_kind}"))?;
if current_rank >= next_rank {
return Ok(false);
}
@@ -1207,11 +1034,9 @@ fn update_page_manifest_stage(root: &Path, asset_id: &str, stage: &str) -> Resul
fn workflow_stage_rank(kind: &str) -> Option<u8> {
match kind {
"ui-workflow.reference-ready" => Some(1),
"ui-workflow.structure-ready" => Some(2),
"ui-workflow.merge-ready" => Some(3),
"ui-workflow.binding-ready" => Some(4),
"ui-workflow.application-ready" => Some(5),
"ui-workflow.completed" => Some(6),
"ui-workflow.binding-ready" => Some(2),
"ui-workflow.application-ready" => Some(3),
"ui-workflow.completed" => Some(4),
_ => None,
}
}
@@ -1268,7 +1093,6 @@ fn derive_page_status(
if matching_trees.len() != 1 {
blockers.push("尚未形成唯一的页面 UI 结构树".to_string());
} else {
stage = UiWorkflowPageStage::StructureReady;
let mut component_count = 0usize;
collect_binding_blockers(&matching_trees[0].root, &mut component_count, &mut blockers);
if component_count == 0 {
@@ -1,21 +0,0 @@
import type { BindingDTO } from './types/BindingDTO';
import type { Node } from './types/Node';
import type { State } from './types/State';
function applyChanges(node: Node, result: BindingDTO): void {
const change = result.changes.find(
(candidate) => candidate.node_id === node.id,
);
if (change) {
node.components = structuredClone(change.components);
node.metadata.components_status = structuredClone(change.components_status);
}
for (const child of node.children) applyChanges(child, result);
}
/** Applies only explicit component changes; omitted nodes remain untouched. */
export function applyBindingResult(state: State, result: BindingDTO): State {
const next = structuredClone(state);
for (const tree of next.ui_trees) applyChanges(tree.root, result);
return next;
}
@@ -1,16 +0,0 @@
import type { RecognitionDTO } from './types/RecognitionDTO';
import type { State } from './types/State';
/**
* 识别结果是整棵结构草稿树的替换结果,不与旧树逐节点合并。
* 识别阶段有意不携带视觉组件;组件绑定由后续 visual-binding 阶段完成。
*/
export function applyRecognitionResult(
state: State,
result: RecognitionDTO,
): State {
return {
...structuredClone(state),
ui_trees: structuredClone(result.ui_trees),
};
}
@@ -0,0 +1,15 @@
import type { RecognitionAndBindingDTO } from './types/RecognitionAndBindingDTO';
import type { State } from './types/State';
/**
* 联合识别绑定结果是整棵最终树的替换结果,不与旧树逐节点合并。
*/
export function applyRecognitionAndBindingResult(
state: State,
result: RecognitionAndBindingDTO,
): State {
return {
...structuredClone(state),
ui_trees: structuredClone(result.ui_trees),
};
}
@@ -1,6 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Component } from "./Component";
import type { NodeId } from "./NodeId";
import type { StageStatus } from "./StageStatus";
export type BindingChange = { node_id: NodeId, components: Array<Component>, components_status: StageStatus, };
@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindingChange } from "./BindingChange";
export type BindingDTO = { changes: Array<BindingChange>, };
@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UITree } from "./UITree";
export type RecognitionDTO = { ui_trees: Array<UITree>, };
export type RecognitionAndBindingDTO = { ui_trees: Array<UITree>, };
@@ -5,70 +5,117 @@ import {
nodeHasBlockedComponents,
nodeNeedsComponentReview,
} from '../../../features/ui-editor/bindingOverview';
import type { StageStatusField } from '../../../features/ui-editor/stageStatusOverview';
import {
getStageStatusOverview,
isBlocked,
isNeedReview,
} from '../../../features/ui-editor/stageStatusOverview';
import type { NodeId } from '../../../features/ui-editor/types/NodeId';
import type { SpriteAsset } from '../../../features/ui-editor/types/SpriteAsset';
import type { UIDesignImageId } from '../../../features/ui-editor/types/UIDesignImageId';
import type { UITree } from '../../../features/ui-editor/types/UITree';
import { useUiTreeNodeCycle } from '../useUiTreeNodeCycle';
export function BindingOverview({
export function RecognitionAndBindingOverview({
uiTrees,
sprites,
onFocusStatusNode,
}: {
uiTrees: UITree[];
sprites: Record<string, SpriteAsset>;
onFocusStatusNode: (treeId: UIDesignImageId, nodeId: NodeId) => void;
onFocusStatusNode: (
treeId: UIDesignImageId,
nodeId: NodeId,
field: StageStatusField,
) => void;
}) {
const overview = useMemo(
const recognition = useMemo(
() => getStageStatusOverview(uiTrees, 'layout_status'),
[uiTrees],
);
const binding = useMemo(
() => getBindingOverview(uiTrees, sprites),
[sprites, uiTrees],
);
const attentionCycle = useUiTreeNodeCycle({
const layoutAttentionCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: onFocusStatusNode,
onFocusNode: (treeId, nodeId) =>
onFocusStatusNode(treeId, nodeId, 'layout_status'),
matches: ({ node }) => {
const status = node.metadata.layout_status;
return isBlocked(status) || isNeedReview(status);
},
});
const layoutBlockedCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: (treeId, nodeId) =>
onFocusStatusNode(treeId, nodeId, 'layout_status'),
matches: ({ node }) => isBlocked(node.metadata.layout_status),
});
const componentAttentionCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: (treeId, nodeId) =>
onFocusStatusNode(treeId, nodeId, 'components_status'),
matches: nodeNeedsComponentReview,
});
const blockedCycle = useUiTreeNodeCycle({
const componentBlockedCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: onFocusStatusNode,
onFocusNode: (treeId, nodeId) =>
onFocusStatusNode(treeId, nodeId, 'components_status'),
matches: nodeHasBlockedComponents,
});
return (
<section
className="shrink-0 border-b border-l border-(--platform-subpanel-border) bg-white/35 px-4 pb-3"
aria-label="绑定概览"
aria-label="识别与绑定概览"
>
<span className="text-[10px] font-semibold tracking-wider text-(--platform-text-soft) uppercase">
Overview
</span>
<h2 className="m-0 text-sm font-semibold"></h2>
<h2 className="m-0 text-sm font-semibold"></h2>
<div className="mt-3 grid grid-cols-2 gap-2">
<OverviewValue label="已识别 Node" value={recognition.total} />
<OverviewValue label="无问题 Node" value={recognition.noProblem} />
<OverviewAction
label="结构待检查"
value={recognition.needsAttention}
tone="warning"
disabled={recognition.needsAttention === 0}
onClick={layoutAttentionCycle.focusNext}
/>
<OverviewAction
label="结构必须修复"
value={recognition.blocked}
tone="danger"
disabled={recognition.blocked === 0}
onClick={layoutBlockedCycle.focusNext}
/>
<OverviewValue
label="需要素材的组件"
value={overview.componentsNeedingAssets}
value={binding.componentsNeedingAssets}
/>
<OverviewValue label="素材槽位" value={overview.assetSlots} />
<OverviewValue label="已绑定" value={overview.boundSlots} />
<OverviewValue label="待处理" value={overview.pendingSlots} />
<OverviewValue label="素材槽位" value={binding.assetSlots} />
<OverviewValue label="已绑定" value={binding.boundSlots} />
<OverviewValue label="待处理" value={binding.pendingSlots} />
<OverviewAction
label="待用户检查"
value={overview.needsAttention}
label="组件待检查"
value={binding.needsAttention}
tone="warning"
disabled={overview.needsAttention === 0}
onClick={attentionCycle.focusNext}
disabled={binding.needsAttention === 0}
onClick={componentAttentionCycle.focusNext}
/>
<OverviewAction
label="必须修复"
value={overview.blocked}
label="组件必须修复"
value={binding.blocked}
tone="danger"
disabled={overview.blocked === 0}
onClick={blockedCycle.focusNext}
disabled={binding.blocked === 0}
onClick={componentBlockedCycle.focusNext}
/>
<OverviewValue
label="独立素材"
value={overview.independentAssets}
value={binding.independentAssets}
wide
/>
</div>
@@ -1,112 +0,0 @@
import { useMemo } from 'react';
import {
getStageStatusOverview,
isBlocked,
isNeedReview,
} from '../../../features/ui-editor/stageStatusOverview';
import type { NodeId } from '../../../features/ui-editor/types/NodeId';
import type { UIDesignImageId } from '../../../features/ui-editor/types/UIDesignImageId';
import type { UITree } from '../../../features/ui-editor/types/UITree';
import { useUiTreeNodeCycle } from '../useUiTreeNodeCycle';
export function RecognitionOverview({
uiTrees,
onFocusStatusNode,
}: {
uiTrees: UITree[];
onFocusStatusNode: (treeId: UIDesignImageId, nodeId: NodeId) => void;
}) {
const overview = useMemo(
() => getStageStatusOverview(uiTrees, 'layout_status'),
[uiTrees],
);
const attentionCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: onFocusStatusNode,
matches: ({ node }) => {
const status = node.metadata.layout_status;
return isBlocked(status) || isNeedReview(status);
},
});
const blockedCycle = useUiTreeNodeCycle({
uiTrees,
onFocusNode: onFocusStatusNode,
matches: ({ node }) => isBlocked(node.metadata.layout_status),
});
return (
<section
className="shrink-0 border-b border-l border-(--platform-subpanel-border) bg-white/35 px-4 pb-3"
aria-label="识别概览"
>
<span className="text-[10px] font-semibold tracking-wider text-(--platform-text-soft) uppercase">
Overview
</span>
<h2 className="m-0 text-sm font-semibold"></h2>
<div className="mt-3 grid grid-cols-2 gap-2">
<OverviewValue label="已识别 Node" value={overview.total} />
<OverviewAction
label="待用户检查"
value={overview.needsAttention}
tone="warning"
disabled={overview.needsAttention === 0}
onClick={attentionCycle.focusNext}
/>
<OverviewValue label="无问题" value={overview.noProblem} />
<OverviewAction
label="必须修复"
value={overview.blocked}
tone="danger"
disabled={overview.blocked === 0}
onClick={blockedCycle.focusNext}
/>
</div>
</section>
);
}
function OverviewValue({ label, value }: { label: string; value: number }) {
return (
<div className="rounded-lg border border-(--platform-subpanel-border) bg-white/45 p-2 text-center">
<strong className="block text-base font-semibold text-(--platform-text-strong)">
{value}
</strong>
<span className="block text-[10px] text-(--platform-text-soft)">
{label}
</span>
</div>
);
}
function OverviewAction({
label,
value,
tone,
disabled,
onClick,
}: {
label: string;
value: number;
tone: 'warning' | 'danger';
disabled: boolean;
onClick: () => void;
}) {
const toneClass =
tone === 'danger'
? 'border-red-200 bg-red-50 text-red-800 hover:border-red-300 hover:bg-red-100'
: 'border-amber-200 bg-amber-50 text-amber-900 hover:border-amber-300 hover:bg-amber-100';
return (
<button
type="button"
className={`rounded-lg border p-2 text-center transition disabled:cursor-default disabled:opacity-45 ${toneClass}`}
disabled={disabled}
onClick={onClick}
aria-label={`${label} ${value},定位下一项`}
>
<strong className="block text-base font-semibold">{value}</strong>
<span className="block text-[10px]">{label}</span>
</button>
);
}
@@ -13,7 +13,7 @@ export function ToolNavigation({
}) {
return (
<nav
className="grid shrink-0 grid-cols-3 gap-2 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3"
className="grid shrink-0 grid-cols-2 gap-2 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) p-3"
aria-label="UI 编辑流程"
>
{UI_EDITOR_STEPS.map((step, index) => {
@@ -6,23 +6,15 @@ export function WorkflowActionCard({
workflow: UiEditorWorkflowProjection;
}) {
const action = getStepAction(workflow);
const isReferenceStep = workflow.activeStep === 'reference-analysis';
const isCombinedStep = workflow.activeStep === 'structure-recognition';
const running =
(workflow.activeStep === 'reference-analysis' && workflow.isSuggesting) ||
(workflow.activeStep === 'structure-recognition' &&
workflow.isRecognizing) ||
(workflow.activeStep === 'visual-binding' && workflow.isBinding);
const status =
workflow.activeStep === 'reference-analysis'
? workflow.suggestionStatus
: workflow.activeStep === 'structure-recognition'
? workflow.recognitionStatus
: workflow.bindingStatus;
const hasRun =
workflow.activeStep === 'reference-analysis'
? workflow.hasSuggested
: workflow.activeStep === 'structure-recognition'
? workflow.hasRecognized
: workflow.hasBound;
(isReferenceStep && workflow.isSuggesting) ||
(isCombinedStep && workflow.isBinding);
const status = isReferenceStep
? workflow.suggestionStatus
: workflow.bindingStatus;
const hasRun = isReferenceStep ? workflow.hasSuggested : workflow.hasBound;
return (
<section className="shrink-0 border-b border-(--platform-subpanel-border) bg-(--platform-subpanel-fill) px-4 py-3">
@@ -70,14 +62,14 @@ function getStepAction(workflow: UiEditorWorkflowProjection) {
}
if (workflow.activeStep === 'structure-recognition') {
return {
label: '识别界面结构',
runningLabel: '识别中…',
action: workflow.recognizeUi,
label: '一键识别并绑定',
runningLabel: '计算中…',
action: workflow.bindComponents,
};
}
return {
label: '绑定视觉素材',
runningLabel: '绑定中…',
label: '一键识别并绑定',
runningLabel: '计算中…',
action: workflow.bindComponents,
};
}
@@ -7,13 +7,12 @@ import {
type IUiDesignStateStore,
uiDesignStateStore,
} from '../../features/ui-editor/uiDesignStateStore';
import { BindingOverview } from './components/BindingOverview';
import { EditorDialogs } from './components/EditorDialogs';
import { ImportOverview } from './components/ImportOverview';
import { InputSidebar } from './components/InputSidebar';
import { InspectorSidebar } from './components/Inspector/InspectorSidebar';
import { PreviewWorkspace } from './components/preview/PreviewWorkspace';
import { RecognitionOverview } from './components/RecognitionOverview';
import { RecognitionAndBindingOverview } from './components/RecognitionAndBindingOverview';
import { ToolNavigation } from './components/ToolNavigation';
import { WorkflowActionCard } from './components/WorkflowActionCard';
import { UI_EDITOR_STEPS, type UiEditorStepId } from './model';
@@ -270,21 +269,14 @@ export default function UiEditorPage({
<PreviewWorkspace canvas={session.canvas} />
</div>
<div className="flex min-h-0 min-w-0 flex-col overflow-hidden">
{session.input.activeStep === 'structure-recognition' ? (
<RecognitionOverview
uiTrees={session.input.uiTrees}
onFocusStatusNode={(treeId, nodeId) => {
session.input.focusNode(treeId, nodeId);
session.input.highlightStatusField(nodeId, 'layout_status');
}}
/>
) : session.input.activeStep === 'visual-binding' ? (
<BindingOverview
{session.input.activeStep === 'structure-recognition' ||
session.input.activeStep === 'visual-binding' ? (
<RecognitionAndBindingOverview
uiTrees={session.input.uiTrees}
sprites={session.input.sprites}
onFocusStatusNode={(treeId, nodeId) => {
onFocusStatusNode={(treeId, nodeId, field) => {
session.input.focusNode(treeId, nodeId);
session.input.highlightStatusField(nodeId, 'components_status');
session.input.highlightStatusField(nodeId, field);
}}
/>
) : (
@@ -26,8 +26,7 @@ export const UI_EDITOR_STEPS: Array<{
label: string;
}> = [
{ id: 'reference-analysis', label: '分析参考图' },
{ id: 'structure-recognition', label: '识别界面结构' },
{ id: 'visual-binding', label: '绑定视觉素材' },
{ id: 'structure-recognition', label: '一键识别并绑定' },
];
export const UI_DESIGN_IMAGE_ROLES: Array<{
@@ -2,24 +2,22 @@ import { invoke } from '@tauri-apps/api/core';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ImportedAsset } from '../../components/AssetImporter';
import { applyBindingResult } from '../../features/ui-editor/binding';
import {
prepareDesignImageBatch,
prepareFontAssetBatch,
prepareSpriteAssetBatch,
} from '../../features/ui-editor/importAdapter';
import { applyMergeResult } from '../../features/ui-editor/merge';
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
import { applyRecognitionAndBindingResult } from '../../features/ui-editor/recognitionAndBinding';
import type { StageStatusField } from '../../features/ui-editor/stageStatusOverview';
import { collectUiNodeIds } from '../../features/ui-editor/treeUtils';
import type { BindingDTO } from '../../features/ui-editor/types/BindingDTO';
import type { ChildrenDisplayMode } from '../../features/ui-editor/types/ChildrenDisplayMode';
import type { Component } from '../../features/ui-editor/types/Component';
import type { FontAssetId } from '../../features/ui-editor/types/FontAssetId';
import type { MergeDTO } from '../../features/ui-editor/types/MergeDTO';
import type { Node as UiNode } from '../../features/ui-editor/types/Node';
import type { NodeId } from '../../features/ui-editor/types/NodeId';
import type { RecognitionDTO } from '../../features/ui-editor/types/RecognitionDTO';
import type { RecognitionAndBindingDTO } from '../../features/ui-editor/types/RecognitionAndBindingDTO';
import type { SpriteAssetId } from '../../features/ui-editor/types/SpriteAssetId';
import type { SpriteBorder } from '../../features/ui-editor/types/SpriteBorder';
import type { State } from '../../features/ui-editor/types/State';
@@ -63,8 +61,6 @@ import {
} from './model';
import { useUiEditorNodeFocus } from './useUiEditorNodeFocus';
const ASSET_BATCH_SIZE = 5;
type StatusFieldHighlight = {
nodeId: NodeId;
field: StageStatusField;
@@ -188,17 +184,17 @@ export function useUiEditorSession(
const [isSaving, setIsSaving] = useState(false);
const [generateError, setGenerateError] = useState<string | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const normalizedInitialStep =
initialStep === 'visual-binding' ? 'structure-recognition' : initialStep;
const normalizedInitialStepIndex =
initialStep === 'reference-analysis'
? 0
: initialStep === 'structure-recognition'
? 1
: 2;
normalizedInitialStep === 'reference-analysis' ? 0 : 1;
const normalizedInitialFurthestStepIndex = Math.max(
normalizedInitialStepIndex,
Math.min(2, Math.max(0, Math.trunc(initialFurthestStepIndex))),
Math.min(1, Math.max(0, Math.trunc(initialFurthestStepIndex))),
);
const [activeStep, setActiveStep] = useState<UiEditorStepId>(
normalizedInitialStep,
);
const [activeStep, setActiveStep] = useState<UiEditorStepId>(initialStep);
const [furthestStepIndex, setFurthestStepIndex] = useState(
normalizedInitialFurthestStepIndex,
);
@@ -235,20 +231,15 @@ export function useUiEditorSession(
useState<PendingResourceRemoval | null>(null);
const [isSuggesting, setIsSuggesting] = useState(false);
const [suggestionStatus, setSuggestionStatus] = useState<string | null>(null);
const [isRecognizing, setIsRecognizing] = useState(false);
const [recognitionStatus, setRecognitionStatus] = useState<string | null>(
null,
);
const [isMerging, setIsMerging] = useState(false);
const [mergeStatus, setMergeStatus] = useState<string | null>(null);
const [isBinding, setIsBinding] = useState(false);
const [bindingStatus, setBindingStatus] = useState<string | null>(null);
const [hasSuggested, setHasSuggested] = useState(false);
const [hasRecognized, setHasRecognized] = useState(false);
const [hasBound, setHasBound] = useState(false);
useEffect(() => {
setActiveStep(initialStep);
setActiveStep(normalizedInitialStep);
setFurthestStepIndex(normalizedInitialFurthestStepIndex);
setPendingWorkflowStepChange(null);
if (!resourceId) {
@@ -352,6 +343,7 @@ export function useUiEditorSession(
};
}, [
initialStep,
normalizedInitialStep,
normalizedInitialFurthestStepIndex,
projectPath,
replaceEditorState,
@@ -371,7 +363,7 @@ export function useUiEditorSession(
image.metadata.role === 'Page' &&
!isSlaveToDescendant(images, id as UIDesignImageId, activeImageId),
);
const isAiRunning = isSuggesting || isRecognizing || isBinding || isMerging;
const isAiRunning = isSuggesting || isBinding || isMerging;
const isWorkflowBusy =
isAiRunning || isSaving || isGenerating || isLoading || editor.isLocked;
const stateSignature = JSON.stringify(editor.state);
@@ -380,11 +372,7 @@ export function useUiEditorSession(
savedStateSignature !== null &&
savedStateSignature !== stateSignature;
const nextStep: UiEditorStepId | null =
activeStep === 'reference-analysis'
? 'structure-recognition'
: activeStep === 'structure-recognition'
? 'visual-binding'
: null;
activeStep === 'reference-analysis' ? 'structure-recognition' : null;
const spriteReferenceCounts = useMemo(() => {
const counts: Record<string, number> = {};
@@ -657,12 +645,7 @@ export function useUiEditorSession(
function enterStep(step: UiEditorStepId) {
setActiveStep(step);
const index =
step === 'reference-analysis'
? 0
: step === 'structure-recognition'
? 1
: 2;
const index = step === 'reference-analysis' ? 0 : 1;
setFurthestStepIndex((current) => Math.max(current, index));
}
@@ -960,30 +943,6 @@ export function useUiEditorSession(
}
}
async function recognizeUi() {
if (isRecognizing || isWorkflowBusy) return;
setRecognitionStatus(null);
setIsRecognizing(true);
try {
await editor.runWithStateLocked(async (snapshot) => {
const result = await invoke<RecognitionDTO>('recognize_ui', {
projectPath,
state: snapshot,
});
editor.replaceState(applyRecognitionResult(snapshot, result));
setHasRecognized(true);
setSelectedNodeId(null);
setRecognitionStatus(`已替换 ${result.ui_trees.length} 棵界面树。`);
});
} catch (cause) {
setRecognitionStatus(
cause instanceof Error ? cause.message : String(cause),
);
} finally {
setIsRecognizing(false);
}
}
async function mergeUi() {
// TODO: This experimental operation is intentionally outside the formal workflow.
if (isMerging || isWorkflowBusy) return;
@@ -1009,32 +968,17 @@ export function useUiEditorSession(
setIsBinding(true);
try {
await editor.runWithStateLocked(async (snapshot) => {
const allSpriteIds = Object.keys(snapshot.sprite_assets);
const batches: string[][] = [];
for (
let index = 0;
index < allSpriteIds.length;
index += ASSET_BATCH_SIZE
) {
batches.push(allSpriteIds.slice(index, index + ASSET_BATCH_SIZE));
}
if (batches.length === 0) batches.push([]);
let current = snapshot;
for (const [index, spriteIds] of batches.entries()) {
setBindingStatus(`绑定组件中(${index + 1}/${batches.length})…`);
const result = await invoke<BindingDTO>('bind_components', {
setBindingStatus('计算 UI 中…');
const result = await invoke<RecognitionAndBindingDTO>(
'recognition_and_binding',
{
projectPath,
state: current,
spriteIds,
});
current = applyBindingResult(current, result);
editor.replaceState(current, {
history: index < batches.length - 1 ? 'skip' : 'record',
});
}
setBindingStatus(
`组件绑定完成(${batches.length}/${batches.length})。`,
state: snapshot,
},
);
editor.replaceState(applyRecognitionAndBindingResult(snapshot, result));
setSelectedNodeId(null);
setBindingStatus('组件绑定完成。');
setHasBound(true);
});
} catch (cause) {
@@ -1179,17 +1123,14 @@ export function useUiEditorSession(
operations: {
isBinding,
isMerging,
isRecognizing,
isSuggesting,
bindingStatus,
mergeStatus,
recognitionStatus,
suggestionStatus,
},
checkPrerequisites,
bindComponents,
mergeUi,
recognizeUi,
suggestUiDesignSemantics,
openImporter: setImportKind,
selectDesignImage,
@@ -1296,10 +1237,6 @@ export function useUiEditorSession(
hasSuggested,
suggestionStatus,
suggestUiDesignSemantics,
isRecognizing,
hasRecognized,
recognitionStatus,
recognizeUi,
isBinding,
hasBound,
bindingStatus,
@@ -7890,7 +7890,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-08-24 AGC UI 原型桥接与自主 UI workflow
- 决策:`ui-prototype` 图片与 `UI` JSON 编辑资源保持两种正式类型。Agent 通过受控 `ui.workflow.run``prepare -> recognize -> status -> finalize` 创建页面资源、关联源图、持久化 UI State 和 manifest 阶段;`recognize` 直接复用 UI Editor 的 provider-backed 结构识别、多树合并与组件绑定命令,按 `reference-ready -> structure-ready -> merge-ready -> binding-ready` 逐阶段写入并推进项目 revision。页面可显式关联已登记图片/图标和字体,图片/图标按 5 项一批绑定,字体安全元数据进入绑定上下文且未知引用失败关闭。Runtime 回执携带 `revisionAdvanceCount`;Provider 未配置、请求失败、工具调用缺失、结果不匹配、未产出可渲染组件或仍有待审节点时保留最近真实阶段,禁止用 deterministic seed 冒充语义处理完成。
- 决策:`ui-prototype` 图片与 `UI` JSON 编辑资源保持两种正式类型。Agent 通过受控 `ui.workflow.run``prepare -> recognize -> status -> finalize` 创建页面资源、关联源图、持久化 UI State 和 manifest 阶段;`recognize` 直接复用 UI Editor 的 provider-backed 联合识别与组件绑定命令,按 `reference-ready -> binding-ready -> application-ready -> completed` 写入并推进项目 revision。每次显式 `recognize` 都重新执行识别+绑定并生成新的 State revision,不复用旧结果或自动 finalize。页面可显式关联已登记图片/图标和字体,独立素材单次联合请求最多 128 个,字体安全元数据进入绑定上下文且未知引用失败关闭。Runtime 回执携带 `revisionAdvanceCount`;Provider 未配置、请求失败、工具调用缺失、结果不匹配、未产出可渲染组件或仍有待审节点时保留最近真实阶段,禁止用 deterministic seed 冒充语义处理完成;manifest 阶段只接受现行值,未知值直接失败,不做 fallback 或 migration
- 客户端:画布点击 `ui-prototype` 先幂等桥接到 `UI` JSON,并立即刷新 manifest;关联查找按 canonical resource identity 且优先已完成 workflow 资源。全部页面完成后,工作台自动打开首个页面的 UI 编辑器 `visual-binding` 最终阶段。
- 完成门:`finalize` 必须为每个页面提供 `game/` 下真实 UTF-8 应用文件并安装当前 UI State revision 标记;缺少结构、组件、页面或标记时拒绝完成。详细输入、阶段与恢复契约见 [`docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`](../../【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md)。
- 验证:前端 bridge 6/6、资源实时集成 19/19、AppSurface 410/410、AGC typecheck、Rust workflow 定向测试覆盖 provider 前的 reference 阶段与真实调用失败关闭、Rust bridge 1/1、编码、格式和 diff 门禁通过;认证登录与真实 Provider 生成的桌面端 E2E 尚未具备可用会话,保持未验证。
@@ -54,7 +54,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
- 通用 Agent Rust 分层为 `agent-runtime-core`catalog、执行生命周期、ToolHost/spawn/all-join/Provider 契约)、`agent-runtime-orchestration`(动态无环任务图、ready、依赖波次、返工下游闭包和受限自主扩图提案)与 `platform-agent` 游戏适配器;循环返工通过新 pass / epoch 表达,不在单张依赖图中建立回边。LLM 可经宿主结构化 function call 提出新增节点/边,编排层只生成经校验的新候选图,epoch 与持久化仍由宿主掌控。
- DirectProject 始终连接客户端内置的 `agc_tools` STDIO MCP,并在启动时额外读取客户端扩展仓库中已启用的第三方 MCP 独立项。第三方 STDIO/HTTP 配置只写入本次隔离 `CODEX_HOME`,单项非 required,启停、重命名和内容指纹进入 app-server pool identity;完整 Plugin Runtime、hooks/apps 和单文件脚本手动指定入口仍关闭。Skill 正文与 references 由 Codex 原生按需读取;`agc_tools` 负责标准美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`;付费资源调用仍由客户端绑定回合、幂等账本、请求上限和投影权威。
- DirectProject 的 Codex 原生文件、搜索、命令、图片查看和 Skill 仅在真实 `game/` cwd 与 `workspaceWrite(writableRoots=[game])` 内可用;原生命令网络保持关闭。多 Agent、Apps、插件、hooks、图片生成、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制保持关闭。app-server 使用隔离 `CODEX_HOME`provider 凭据只由 AGC 客户端代理持有,不能进入模型上下文或 shell 环境。
- `ui-prototype`(设计图片)与 UI 编辑器 `UI` JSON 是不同资源。白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`,由 provider-backed 识别、合并和组件绑定持久化 State/revision,并把 `reference-ready → structure-ready → merge-ready → binding-ready → application-ready → completed` 投影到 manifest。Provider 缺失、请求失败、工具缺失、结果不匹配或仍有待审节点时保留真实阶段并返回 blocker,不得用 deterministic seed 伪造完成。
- `ui-prototype`(设计图片)与 UI 编辑器 `UI` JSON 是不同资源。白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`,由 provider-backed 联合识别与组件绑定持久化 State/revision,并把 `reference-ready → binding-ready → application-ready → completed` 投影到 manifest。每次显式 `recognize` 都是新的识别+绑定请求,会替换 UI 树并产生新的 State revision;不会自动恢复旧结果或自动 finalize。Provider 缺失、请求失败、工具缺失、结果不匹配或仍有待审节点时保留真实阶段并返回 blocker,不得用 deterministic seed 伪造完成。
- UI workflow 的资源桥接与 Runtime 边界以 `docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md` 和 AGC 实施计划的 2026-08-24 覆盖段为准;只生成图片、登记空 JSON 或进入普通图片画布都不构成 workflow 完成。
## 退役边界
@@ -1267,7 +1267,7 @@ game-project/
DirectProject 使用 `approvalPolicy=never`,避免每次原生调用再经过泛化 ToolHost 包装;原生命令网络保持关闭,联网资料继续走受控 `agc_web_search`。多 Agent、Apps、完整插件 Runtime、hooks、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制仍关闭,避免绕过 AGC durable delegation、浏览器证据和副作用审计;图片生成通过客户端审核的 `agc_tools.agc_generate_image` 暴露普通单图、角色图、视觉规范图和 UI 设计图,完整游戏美术包继续使用 `agc_tools.taonier_prepare_game_art`,两者都复用同一客户端登录态、幂等账本、下载校验和 manifest/revision 投影,不开放 Codex 原生 image tool。app-server 使用隔离 `CODEX_HOME`:内置 `agc_tools` 由客户端启动参数注入,用户在客户端扩展列表启用的独立第三方 MCP 以原生配置写入该次隔离 home;全局 Codex MCP、禁用项、Plugin hooks/apps 和其它插件能力不进入 DirectProject。第三方项固定非 required,配置或启动失败只记录该项,不替换 `agc_tools`provider session token、工具桥地址和受控搜索标记不得通过第三方 MCP 的环境转发字段泄露。配置了 AGC LLM Key 或可解析的 `OPENAI_API_KEY` 登录态时,真实 provider 凭据只由 AGC 本地 provider proxy 持有,Codex 仅使用连接级随机代理令牌;无法安全代理的 OAuth `auth.json` 继续关闭 native shell/unified exec。`agc_tools` 的平台授权由 AGC 客户端当前登录会话和受控后端完成,普通客户端不得把 DirectProject 请求改成外部 API Key 请求;401/403 只投影为客户端登录或权限异常,不向用户索要凭据或暴露内部 URL。shell 子进程采用 `shell_environment_policy` core 继承及 secret/proxy/bridge 排除,provider key 和桥接凭据不得进入命令环境。系统提示词不再预注入项目源码快照或 Skill 正文,Codex 按需读取当前 cwd 文件。
## 2026-08-24 AGC UI 原型桥接与自主 UI workflow
- 2026-08-24 起,`ui-prototype` 与 UI 编辑器的 `UI` JSON 资源明确分离。设计图生成后必须由白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`:为每个功能页面创建并关联 `UI` JSON,载入页面设计图和已登记图片/图标/字体,调用 UI Editor 的 provider-backed 结构识别、多树合并与分批组件绑定,持久化 State/revision,写入 `game/` 应用标记,并把 `reference-ready → structure-ready → merge-ready → binding-ready → application-ready → completed` 各阶段的 `generationKind` 和 manifest revision 投影给客户端。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、未产出可渲染组件或仍有待审节点时保留最近真实阶段并返回 blocker,不得使用 deterministic seed 冒充完成。工作台点击 `ui-prototype` 时通过 `ensure_ui_design_resource_for_prototype` 幂等补齐关联资源;工作流完成后自动打开首个页面的 UI 编辑器 `visual-binding` 最终阶段,交给用户检查和手动调整。UI 编辑器独立的语义建议请求也必须复用统一 LLM 传输选择,`llm.stream=true` 时发送 `stream=true` 并聚合完整工具调用后再校验结果。只生成图片、登记空 JSON 或进入普通图片画布均不构成 UI 工作流完成,详见 [`【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`](../【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md)。
- 2026-08-24 起,`ui-prototype` 与 UI 编辑器的 `UI` JSON 资源明确分离。设计图生成后必须由白名单 `ui.workflow.run` 按页面执行 `prepare → recognize → status → finalize`:为每个功能页面创建并关联 `UI` JSON,载入页面设计图和已登记图片/图标/字体,调用 UI Editor 的 provider-backed `recognition_and_binding` 单次联合计算,持久化 State/revision,写入 `game/` 应用标记,并把 `reference-ready → binding-ready → application-ready → completed` 各阶段的 `generationKind` 和 manifest revision 投影给客户端。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、未产出可渲染组件或仍有待审节点时保留最近真实阶段并返回 blocker,不得使用 deterministic seed 冒充完成。工作台点击 `ui-prototype` 时通过 `ensure_ui_design_resource_for_prototype` 幂等补齐关联资源;工作流完成后自动打开首个页面的 UI 编辑器 `visual-binding` 最终阶段,交给用户检查和手动调整。UI 编辑器独立的语义建议请求也必须复用统一 LLM 传输选择,`llm.stream=true` 时发送 `stream=true` 并聚合完整工具调用后再校验结果。只生成图片、登记空 JSON 或进入普通图片画布均不构成 UI 工作流完成,详见 [`【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`](../【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md)。
## 2026-08-28 AGC 自主构建 relaxed 编排覆盖
@@ -36,7 +36,7 @@ Agent 通过白名单工具 `ui.workflow.run` 发起工作流。项目路径由
处理规则:
1. `prepare` 为每个页面创建确定性的 `kind=UI` JSON 资源,引用源 `ui-prototype` 和页面设计图,载入设计图尺寸与相对路径到 `ui_design_images`,并保存 State。
2. `recognize` 依次执行 Provider 多模态结构识别、现有多树合并器、最多每批 5 项的图片/图标组件绑定,并把已登记字体的安全元数据提供给绑定器;阶段分别持久化为 `structure-ready``merge-ready``binding-ready`,重复执行从最近真实阶段恢复
2. `recognize` 直接执行一次 `recognition_and_binding` Provider 多模态请求,返回同时包含结构和视觉组件的完整 UI 树,并把已登记字体的安全元数据提供给模型;成功后直接持久化为 `binding-ready`,不再进入旧的结构识别、合并或分批绑定阶段
3. `status` 只回读 State、页面阶段和 blockers,不推进项目 revision。
4. `finalize` 只接受 `game/` 下的真实 UTF-8 文件,写入与 UI State revision 绑定的应用标记;所有页面通过应用门禁后才返回 `visual-binding` 最终阶段路由。缺少页面、资源、组件或应用标记时拒绝伪造完成。
@@ -46,16 +46,20 @@ UI 编辑器“生成代码”只把导出的 `ui/generated-*.js` 写入用户
```text
ui-workflow.reference-ready
ui-workflow.structure-ready
ui-workflow.merge-ready
ui-workflow.binding-ready
ui-workflow.application-ready
ui-workflow.completed
```
`recognize` 是显式的重新计算动作,不做幂等短路:用户或 Runtime 再次点击/调用时,始终重新执行一次识别+绑定请求,并用新生成的 UI 树替换旧树、写入新的 UI State revision。它不会复用上一次 LLM 结果,也不会自动替用户重新执行 `finalize`;如果页面已经有 `application-ready`/`completed` 标记,重新识别后应重新检查状态并按需再次 finalize,使应用标记与最新 State revision 对齐。该行为是有意保留的 demo/workbench 语义,避免“重新点击”被悄悄解释成恢复旧结果。
联合请求一次携带全部页面参考图和已登记独立素材;页面参考图最多 4 张,独立素材最多 128 个,页面参考图不占用这 128 个独立素材额度。超过独立素材上限时在发起 Provider 请求前直接失败,不会退回旧的素材 batching 路径。
`workflow_stage_rank` 不是前端内存状态,也不是 UI 编辑器的步骤索引。它是 Rust workflow 模块里的内部比较函数:读取 UI JSON manifest 资产上持久化的 `source.generationKind``ui-workflow.reference-ready``ui-workflow.binding-ready``ui-workflow.application-ready``ui-workflow.completed`),把阶段映射为数字 rank,用于 `update_page_manifest_stage` 的单调推进保护,避免较早阶段覆盖较晚阶段。阶段值在项目 manifest 中持久化,并随项目 revision 更新。当前实现只接受这组现行阶段;缺失或未知阶段直接报错,不做旧值 fallback,也不做迁移。
最终回执写入 `.agent/ui-workflows/<source-hash>.json`,客户端可据此恢复页面清单和最终编辑器路由。
`recognize` 现在直接复用 UI Editor 的 provider-backed `recognize_ui_impl``merge_ui_impl``bind_components_impl`:先对页面设计图执行多模态结构识别,再落盘合并后的唯一页面树,最后按 5 项一批绑定已登记图片/图标,并向模型提供 State 内已验证字体的 ID、family、face、weight 与 style。由 Agent Runtime 调用时,这三个阶段携带当前 `agent_id/run_id`,统一走活动 Provider 的 mode、请求快照、重试和恢复链路,不再从工作流偷偷创建另一套传统 HTTP client。Codex app-server 会把输入图片暂存到该连接的隔离工作区 `input-images/`,通过原生 `localImage` 输入发送;文本提示只保留图片占位符,避免把 base64 复制进提示词或 JSON-RPC。所有 LLM 工具参数仍沿用 UI Editor 的严格 schema、节点/深度/素材和字体白名单及有界输入校验。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、绑定没有可渲染组件或仍有 `NeedReview/Blocked` 时,完成阶段不会推进;已落盘的中间阶段仍通过 manifest invalidation 更新客户端,不再使用 deterministic seed 冒充语义处理通过
`recognize` 现在直接复用 UI Editor 的 provider-backed `recognition_and_binding_impl`:一次请求同时分析页面设计图和全部已登记图片/图标,返回完整 UI 树及节点组件,并向模型提供 State 内已验证字体的 ID、family、face、weight 与 style。联合提示词由 `recognition_and_binding.rs` 中的 `RECOGNITION_AND_BINDING_SYSTEM_PROMPT` 常量提供,当前保留为实现占位符。由 Agent Runtime 调用时携带当前 `agent_id/run_id`,统一走活动 Provider 的 mode、请求快照、重试和恢复链路,不再从工作流偷偷创建另一套传统 HTTP client。Codex app-server 会把输入图片暂存到该连接的隔离工作区 `input-images/`,通过原生 `localImage` 输入发送;文本提示只保留图片占位符,避免把 base64 复制进提示词或 JSON-RPC。所有 LLM 工具参数仍沿用 UI Editor 的严格 schema、节点/深度/素材和字体白名单及有界输入校验。Provider 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用、绑定没有可渲染组件或仍有 `NeedReview/Blocked` 时,完成阶段不会推进;旧的 `merge_ui_impl` 仍作为未开放前端的独立能力保留,但不再参与本次 `recognize` 路径
真实 Provider 鉴权失败时,Codex app-server 可能只返回 `codexErrorInfo=other`,而把上游 `401/403` 放在错误正文中。Runtime 必须从受控错误字段识别为 `codex-app-server-error:unauthorized`(公共摘要为 `codex-app-server-unauthorized`),只向公共运行记录暴露错误类别和指纹,不记录 Token 或上游原文。此错误不能伪造为 UI 工作流阶段完成;修复凭据后应从原有 run 的恢复边界重新执行。