统一自动切分素材工作流命名

将 UI 编辑器第三步和内部流程 ID 统一为自动切分素材。
重命名前端切分概览模块并更新完成、恢复和导航文案。
同步 Runtime 最终编辑器路由、测试与相关技术文档。
This commit is contained in:
2026-09-10 15:11:29 +08:00
parent 1c992c69fe
commit 1509235c1b
21 changed files with 107 additions and 99 deletions
@@ -1832,7 +1832,7 @@ mod tests {
assert!(with_canvas.contains("由 Runtime 在收束门内同时核对固定 owner 文档"));
assert!(!with_canvas.contains("成功动作本身就是当前 revision 的验证"));
assert!(with_canvas.contains("调用 ui.workflow.run"));
assert!(with_canvas.contains("visual-binding 最终编辑器路由"));
assert!(with_canvas.contains("asset-separation 最终编辑器路由"));
assert!(with_canvas.contains("每个功能页面各写一行 @genarrative-ui-page"));
assert!(with_canvas.contains("ui.workflow.run 的 discover"));
assert!(with_canvas.contains("assets/ui-pages/{pageId}.png"));
@@ -801,7 +801,7 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
let initial_step = route.get("initialStep")?.as_str()?;
let render_mode = route.get("renderMode")?.as_str()?;
if resource_id.is_empty()
|| initial_step != "visual-binding"
|| initial_step != "asset-separation"
|| render_mode != "final-preview"
{
return None;
@@ -296,7 +296,7 @@ pub(crate) async fn run_ui_workflow_at_with_provider(
) {
let route = UiWorkflowFinalStageRoute {
resource_id: statuses[0].ui_asset_id.clone(),
initial_step: "visual-binding".to_string(),
initial_step: "asset-separation".to_string(),
render_mode: "final-preview".to_string(),
};
if finalized {
@@ -3,7 +3,7 @@ import type { State } from './types/State';
/**
* 识别结果是整棵结构草稿树的替换结果,不与旧树逐节点合并。
* 识别阶段有意不携带视觉组件;组件绑定由后续 visual-binding 阶段完成
* 识别阶段有意不携带 SpriteAsset;视觉素材由后续 asset-separation 阶段自动切分并回填
*/
export function applyRecognitionResult(
state: State,
@@ -8,27 +8,27 @@ import type { Component } from './types/Component';
import type { SpriteAsset } from './types/SpriteAsset';
import type { UITree } from './types/UITree';
export type ComponentBindingCounts = {
export type ComponentSeparationCounts = {
componentsNeedingAssets: number;
assetSlots: number;
boundSlots: number;
pendingSlots: number;
};
export type BindingOverview = ComponentBindingCounts & {
export type SeparationOverview = ComponentSeparationCounts & {
needsAttention: number;
blocked: number;
independentAssets: number;
};
const EMPTY_COMPONENT_BINDING_COUNTS: ComponentBindingCounts = {
const EMPTY_COMPONENT_SEPARATION_COUNTS: ComponentSeparationCounts = {
componentsNeedingAssets: 0,
assetSlots: 0,
boundSlots: 0,
pendingSlots: 0,
};
function countRequiredAssetSlot(isBound: boolean): ComponentBindingCounts {
function countRequiredAssetSlot(isBound: boolean): ComponentSeparationCounts {
return {
componentsNeedingAssets: 1,
assetSlots: 1,
@@ -37,40 +37,40 @@ function countRequiredAssetSlot(isBound: boolean): ComponentBindingCounts {
};
}
export function getImageBindingCounts(
export function getImageSeparationCounts(
component: Extract<Component, { Image: unknown }>['Image'],
): ComponentBindingCounts {
): ComponentSeparationCounts {
return countRequiredAssetSlot(component.target_graphic !== null);
}
export function getTextBindingCounts(
export function getTextSeparationCounts(
component: Extract<Component, { Text: unknown }>['Text'],
): ComponentBindingCounts {
): ComponentSeparationCounts {
if (
typeof component.font !== 'object' ||
component.font === null ||
!('Bound' in component.font)
) {
return EMPTY_COMPONENT_BINDING_COUNTS;
return EMPTY_COMPONENT_SEPARATION_COUNTS;
}
return countRequiredAssetSlot(true);
}
export function getComponentBindingCounts(
export function getComponentSeparationCounts(
component: Component,
): ComponentBindingCounts {
): ComponentSeparationCounts {
if ('Image' in component) {
return getImageBindingCounts(component.Image);
return getImageSeparationCounts(component.Image);
}
if ('Text' in component) {
return getTextBindingCounts(component.Text);
return getTextSeparationCounts(component.Text);
}
return EMPTY_COMPONENT_BINDING_COUNTS;
return EMPTY_COMPONENT_SEPARATION_COUNTS;
}
function addBindingCounts(
overview: ComponentBindingCounts,
counts: ComponentBindingCounts,
function addSeparationCounts(
overview: ComponentSeparationCounts,
counts: ComponentSeparationCounts,
) {
overview.componentsNeedingAssets += counts.componentsNeedingAssets;
overview.assetSlots += counts.assetSlots;
@@ -78,12 +78,12 @@ function addBindingCounts(
overview.pendingSlots += counts.pendingSlots;
}
export function getBindingOverview(
export function getSeparationOverview(
uiTrees: UITree[],
spriteAssets: Record<string, SpriteAsset>,
): BindingOverview {
const overview: BindingOverview = {
...EMPTY_COMPONENT_BINDING_COUNTS,
): SeparationOverview {
const overview: SeparationOverview = {
...EMPTY_COMPONENT_SEPARATION_COUNTS,
needsAttention: 0,
blocked: 0,
independentAssets: Object.keys(spriteAssets).length,
@@ -96,17 +96,20 @@ export function getBindingOverview(
overview.needsAttention += 1;
}
if (node.component) {
addBindingCounts(overview, getComponentBindingCounts(node.component));
addSeparationCounts(
overview,
getComponentSeparationCounts(node.component),
);
}
}
return overview;
}
export function nodeHasPendingBinding(target: UiTreeNodeTarget): boolean {
export function nodeHasPendingSeparation(target: UiTreeNodeTarget): boolean {
return (
target.node.component !== null &&
getComponentBindingCounts(target.node.component).pendingSlots > 0
getComponentSeparationCounts(target.node.component).pendingSlots > 0
);
}
@@ -3335,7 +3335,7 @@ export default function ProjectDevelopmentView({
resource.label,
...(result.asset.source.generationKind === 'ui-workflow.completed'
? {
initialStep: 'visual-binding',
initialStep: 'asset-separation',
initialFurthestStepIndex: 2,
}
: {}),
@@ -3376,7 +3376,7 @@ export default function ProjectDevelopmentView({
(asset) => asset.id === resource.manifestAssetId,
)?.source.generationKind === 'ui-workflow.completed'
? {
initialStep: 'visual-binding' as const,
initialStep: 'asset-separation' as const,
initialFurthestStepIndex: 2,
}
: {}),
@@ -3445,7 +3445,7 @@ export default function ProjectDevelopmentView({
resourceLabel:
completed.localPath.split(/[\\/]/u).filter(Boolean).pop() ??
'UI 设计资源',
initialStep: 'visual-binding',
initialStep: 'asset-separation',
initialFurthestStepIndex: 2,
});
}, [advanceFocusGeneration, manifest.assets, uiEditorRoute]);
@@ -1,17 +1,17 @@
import { useMemo } from 'react';
import {
getBindingOverview,
getSeparationOverview,
nodeHasBlockedComponents,
nodeNeedsComponentReview,
} from '../../../features/ui-editor/bindingOverview';
} from '../../../features/ui-editor/separationOverview';
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 SeparationOverview({
uiTrees,
sprites,
onFocusStatusNode,
@@ -21,7 +21,7 @@ export function BindingOverview({
onFocusStatusNode: (treeId: UIDesignImageId, nodeId: NodeId) => void;
}) {
const overview = useMemo(
() => getBindingOverview(uiTrees, sprites),
() => getSeparationOverview(uiTrees, sprites),
[sprites, uiTrees],
);
const attentionCycle = useUiTreeNodeCycle({
@@ -38,20 +38,20 @@ export function BindingOverview({
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="需要素材的组件"
label="需要切分素材的组件"
value={overview.componentsNeedingAssets}
/>
<OverviewValue label="素材槽位" value={overview.assetSlots} />
<OverviewValue label="已绑定" value={overview.boundSlots} />
<OverviewValue label="待处理" value={overview.pendingSlots} />
<OverviewValue label="已切分并回填" value={overview.boundSlots} />
<OverviewValue label="待切分" value={overview.pendingSlots} />
<OverviewAction
label="待用户检查"
value={overview.needsAttention}
@@ -11,7 +11,7 @@ export function WorkflowActionCard({
(workflow.activeStep === 'reference-analysis' && workflow.isSuggesting) ||
(workflow.activeStep === 'structure-recognition' &&
workflow.isRecognizing) ||
(workflow.activeStep === 'visual-binding' && workflow.isSeparating);
(workflow.activeStep === 'asset-separation' && workflow.isSeparating);
const status =
workflow.activeStep === 'reference-analysis'
? workflow.suggestionStatus
@@ -59,17 +59,19 @@ export function WorkflowActionCard({
</div>
<ThemedModal
open={workflow.separationRecovery !== null}
ariaLabel="恢复自动分离"
ariaLabel="恢复自动切分素材"
onClose={workflow.cancelSeparationRecovery}
closeOnBackdrop={false}
closeOnEscape={false}
panelClassName="w-full max-w-md rounded-2xl p-5 shadow-2xl"
>
<h3 className="m-0 text-base font-semibold"></h3>
<h3 className="m-0 text-base font-semibold">
</h3>
<p className="mt-3 text-sm text-(--platform-text-soft)">
{' '}
{' '}
{workflow.separationRecovery?.bound_node_count ?? 0}{' '}
</p>
<div className="mt-5 flex justify-end gap-2">
<button
@@ -84,14 +86,14 @@ export function WorkflowActionCard({
className="rounded-lg border border-orange-500 px-3 py-1.5 text-xs"
onClick={() => void workflow.restartSeparation()}
>
</button>
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-xs font-semibold text-white"
onClick={() => void workflow.continueSeparation()}
>
</button>
</div>
</ThemedModal>
@@ -115,8 +117,8 @@ function getStepAction(workflow: UiEditorWorkflowProjection) {
};
}
return {
label: '自动分离并绑定视觉素材',
runningLabel: '自动分离中…',
label: '自动分素材',
runningLabel: '素材切分中…',
action: workflow.separateUi,
};
}
@@ -21,7 +21,7 @@ export function prerequisiteIssuesForStep(
return [];
case 'structure-recognition':
return validateComponentRecognitionPrerequisites(state);
case 'visual-binding':
case 'asset-separation':
return validateAssetRecognitionPrerequisites(state);
}
}
@@ -35,7 +35,7 @@ export function postCheckIssuesForStep(
return validateReferenceAnalysisResult(state);
case 'structure-recognition':
return validateStructureRecognitionResult(state);
case 'visual-binding':
case 'asset-separation':
return validateVisualBindingResult(state);
}
}
@@ -59,7 +59,7 @@ export function activeStepPrerequisiteIssues(
return validateComponentRecognitionPrerequisites(state);
case 'structure-recognition':
return validateAssetRecognitionPrerequisites(state);
case 'visual-binding':
case 'asset-separation':
return validateLayoutReviewPrerequisites(state);
}
}
@@ -18,8 +18,8 @@ export function workflowStepLabel(step: UiEditorStepId): string {
return '分析参考图';
case 'structure-recognition':
return '识别界面结构';
case 'visual-binding':
return '绑定视觉素材';
case 'asset-separation':
return '自动切分素材';
}
const exhaustiveCheck: never = step;
return exhaustiveCheck;
@@ -7,13 +7,13 @@ 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 { SeparationOverview } from './components/SeparationOverview';
import { ToolNavigation } from './components/ToolNavigation';
import { WorkflowActionCard } from './components/WorkflowActionCard';
import { WorkflowCompletionModal } from './components/WorkflowCompletionModal';
@@ -265,8 +265,8 @@ export default function UiEditorPage({
session.input.highlightStatusField(nodeId, 'layout_status');
}}
/>
) : session.input.activeStep === 'visual-binding' ? (
<BindingOverview
) : session.input.activeStep === 'asset-separation' ? (
<SeparationOverview
uiTrees={session.input.uiTrees}
sprites={session.input.sprites}
onFocusStatusNode={(treeId, nodeId) => {
@@ -6,7 +6,7 @@ import type { RemovalImpact } from '../../features/ui-editor/useUiEditorState';
export type UiEditorStepId =
| 'reference-analysis'
| 'structure-recognition'
| 'visual-binding';
| 'asset-separation';
export type UiEditorImportKind = 'design-image' | 'font' | 'sprite';
export type UiEditorNodeFocusRequest = {
@@ -27,7 +27,7 @@ export const UI_EDITOR_STEPS: Array<{
}> = [
{ id: 'reference-analysis', label: '分析参考图' },
{ id: 'structure-recognition', label: '识别界面结构' },
{ id: 'visual-binding', label: '绑定视觉素材' },
{ id: 'asset-separation', label: '自动切分素材' },
];
export const UI_DESIGN_IMAGE_ROLES: Array<{
@@ -417,7 +417,7 @@ export function useUiEditorSession(
activeStep === 'reference-analysis'
? 'structure-recognition'
: activeStep === 'structure-recognition'
? 'visual-binding'
? 'asset-separation'
: null;
const spriteReferenceCounts = useMemo(() => {
@@ -1111,7 +1111,7 @@ export function useUiEditorSession(
(path) => !importedByPath.has(path),
);
backfillErrors = missingImports.map(
(path) => `未能登记分离图片:${path}`,
(path) => `未能登记自动切分素材图片:${path}`,
);
const importedAssets: ImportedAsset[] = [
...new Map(
@@ -1147,7 +1147,7 @@ export function useUiEditorSession(
const sprite = spriteByPath.get(path);
if (!sprite) {
backfillErrors.push(
`节点 ${bound.node_id} 缺少已登记的分离图片:${path}`,
`节点 ${bound.node_id} 缺少已登记的自动切分素材图片:${path}`,
);
continue;
}
@@ -1183,18 +1183,19 @@ export function useUiEditorSession(
]),
),
}));
if (separationResult === null) throw new Error('自动分离没有返回结果');
if (separationResult === null)
throw new Error('自动切分素材没有返回结果');
const completedResult = separationResult as SeparationDTO;
if (!(await save())) {
throw new Error(
'分离结果已写入编辑器,但 State 保存失败;sidecar 已保留,可继续恢复。',
'自动切分素材结果已写入编辑器,但 State 保存失败;sidecar 已保留,可继续恢复。',
);
}
if (backfillErrors.length > 0) {
reportWorkflowCompletion(
'visual-binding',
'asset-separation',
'failure',
`自动分离已完成,但有 ${backfillErrors.length} 项未能回填;已登记素材并保留恢复状态。\n${backfillErrors.join('\n')}`,
`自动切分素材已完成,但有 ${backfillErrors.length} 项未能回填;已登记素材并保留恢复状态。\n${backfillErrors.join('\n')}`,
setSeparationStatus,
);
return;
@@ -1205,14 +1206,14 @@ export function useUiEditorSession(
});
setHasSeparated(true);
reportWorkflowCompletion(
'visual-binding',
'asset-separation',
'success',
`自动分离完成:${completedResult.bound_nodes.length} 个已绑定${completedResult.problematic_nodes.length} 个待处理。`,
`自动切分素材完成:${completedResult.bound_nodes.length} 个已切分并回填${completedResult.problematic_nodes.length} 个待处理。`,
setSeparationStatus,
);
} catch (cause) {
reportWorkflowCompletion(
'visual-binding',
'asset-separation',
'failure',
cause instanceof Error ? cause.message : String(cause),
setSeparationStatus,
@@ -1,11 +1,11 @@
import { describe, expect, it } from 'vitest';
import {
getBindingOverview,
getSeparationOverview,
nodeHasBlockedComponents,
nodeHasPendingBinding,
nodeHasPendingSeparation,
nodeNeedsComponentReview,
} from '../src/features/ui-editor/bindingOverview';
} from '../src/features/ui-editor/separationOverview';
import { getNextMatchingUiTreeNodeTarget } from '../src/features/ui-editor/stageStatusOverview';
import type { Component } from '../src/features/ui-editor/types/Component';
import type { Node } from '../src/features/ui-editor/types/Node';
@@ -97,9 +97,9 @@ const trees: UITree[] = [
},
];
describe('getBindingOverview', () => {
describe('getSeparationOverview', () => {
it('uses per-component helpers to include both image and text slots', () => {
expect(getBindingOverview(trees, sprites)).toEqual({
expect(getSeparationOverview(trees, sprites)).toEqual({
componentsNeedingAssets: 2,
assetSlots: 2,
boundSlots: 1,
@@ -110,15 +110,15 @@ describe('getBindingOverview', () => {
});
});
it('reuses the common preorder next-target search for every binding queue', () => {
it('reuses the common preorder next-target search for every separation queue', () => {
expect(
getNextMatchingUiTreeNodeTarget(trees, null, (target) =>
nodeHasPendingBinding(target),
nodeHasPendingSeparation(target),
)?.node.id,
).toBe('review');
expect(
getNextMatchingUiTreeNodeTarget(trees, 'review', (target) =>
nodeHasPendingBinding(target),
nodeHasPendingSeparation(target),
)?.node.id,
).toBe('review');
expect(
@@ -347,12 +347,14 @@ describe('UiEditorPage', () => {
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(screen.getByRole('heading', { name: '识别概览' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
fireEvent.click(screen.getByRole('button', { name: /自动切分素材/ }));
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(screen.getByRole('heading', { name: '绑定概览' })).toBeTruthy();
expect(
screen.getByRole('heading', { name: '自动切分素材概览' }),
).toBeTruthy();
});
it('opens a completed workflow directly at the visual binding review stage', async () => {
it('opens a completed workflow directly at the asset separation review stage', async () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue({
revision: 3,
@@ -367,25 +369,25 @@ describe('UiEditorPage', () => {
projectPath: '/tmp/ui-editor-final-review',
resourceId: 'ui-resource',
stateStore,
initialStep: 'visual-binding',
initialStep: 'asset-separation',
initialFurthestStepIndex: 2,
}),
);
expect(
await screen.findByRole('heading', { name: '绑定概览' }),
await screen.findByRole('heading', { name: '自动切分素材概览' }),
).toBeTruthy();
expect(
screen
.getByRole('navigation', { name: 'UI 编辑流程' })
.querySelector('button[aria-current="step"]')?.textContent,
).toContain('绑定视觉素材');
).toContain('自动切分素材');
});
it('keeps the pending binding count informational instead of navigable', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
fireEvent.click(screen.getByRole('button', { name: /自动切分素材/ }));
fireEvent.click(screen.getByRole('button', { name: '仍然继续' }));
expect(
@@ -396,7 +398,7 @@ describe('UiEditorPage', () => {
it('switches tools freely without inventing completed workflow state', () => {
render(createElement(UiEditorPage, { projectPath: '/tmp/ui-editor' }));
fireEvent.click(screen.getByRole('button', { name: /绑定视觉素材/ }));
fireEvent.click(screen.getByRole('button', { name: /自动切分素材/ }));
expect(screen.getByRole('heading', { name: '检查发现问题' })).toBeTruthy();
});
@@ -15,6 +15,6 @@ describe('workflow completion notice helpers', () => {
it('maps every workflow step to a user-facing label', () => {
expect(workflowStepLabel('reference-analysis')).toBe('分析参考图');
expect(workflowStepLabel('structure-recognition')).toBe('识别界面结构');
expect(workflowStepLabel('visual-binding')).toBe('绑定视觉素材');
expect(workflowStepLabel('asset-separation')).toBe('自动切分素材');
});
});
@@ -7903,7 +7903,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,并立即刷新 manifest;关联查找按 canonical resource identity 且优先已完成 workflow 资源。全部页面完成后,工作台自动打开首个页面的 UI 编辑器 `visual-binding` 最终阶段。
- 客户端:画布点击 `ui-prototype` 先幂等桥接到 `UI` JSON,并立即刷新 manifest;关联查找按 canonical resource identity 且优先已完成 workflow 资源。全部页面完成后,工作台自动打开首个页面的 UI 编辑器 `asset-separation` 最终阶段。
- 完成门:`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 尚未具备可用会话,保持未验证。
@@ -106,7 +106,7 @@ UI Editor Inspector 的全局只读状态唯一来源是 `controller.editor.isLo
## 2026-08-18 UI Editor 结构识别、合并与增量导入边界
UI Editor 当前把“识别界面结构”定义为结构草稿阶段,而不是完整视觉还原阶段。识别 DTO 只负责输出节点层级、几何、名称、描述和置信度;节点组件暂为空,由后续“绑定视觉素材”阶段补齐 Image / Text 组件。`applyRecognitionResult` 可以整体替换当前 `ui_trees`,但该替换只代表结构结果,不能宣称已经保留截图中的视觉内容;组件状态使用 `NoProblem`,前置检查仍会根据空组件和素材绑定情况阻止跳过绑定阶段。
UI Editor 当前把“识别界面结构”定义为结构草稿阶段,而不是完整视觉还原阶段。识别 DTO 只负责输出节点层级、几何、名称、描述和置信度;节点组件暂为空,由后续“自动切分素材”阶段补齐 Image / Text 组件。`applyRecognitionResult` 可以整体替换当前 `ui_trees`,但该替换只代表结构结果,不能宣称已经保留截图中的视觉内容;组件状态使用 `NoProblem`,前置检查仍会根据空组件和素材切分情况阻止跳过自动切分阶段。
结构识别、界面语义建议、多图合并和组件绑定只接受不超过 `1 MiB` 的 LLM 工具调用 arguments,并在递归业务类型反序列化前先解析为通用 JSON、迭代检查结构预算。结构识别按每棵返回树独立限制为最多 `512` 个 LLM 节点和 `32` 层,不汇总多棵树的节点数,也不计 Rust 自动补建的页面根;界面语义建议最多 `4` 个节点和 `4` 层;合并计划最多 `512` 个计划节点和 `32` 层,`Simple.children``Merged.merged_from` 使用同一计数和深度口径;组件绑定 `changes` 不得超过当前可编辑节点数且绝对上限为 `10,000`,每个 change 的完整组件栈最多 `64` 个组件。任何超限结果均整次拒绝,不截断、不返回部分结果,也不把工具 arguments 正文写入日志。
@@ -1286,7 +1286,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 结构识别、多树合并与分批自动切分素材,持久化 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 编辑器 `asset-separation` 最终阶段,交给用户检查和手动调整。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 编排覆盖
@@ -1,10 +1,10 @@
# UI 编辑器自动分离工作流
# UI 编辑器自动切分素材工作流
更新时间:`2026-09-08`
## 目标
将 UI 编辑器现有“用户先提供独立图片/图标,再执行组件绑定”的入口替换为自动分离:结构识别阶段直接返回可渲染组件草稿,分阶段按整页叶节点批次调用图片编辑模型,再由视觉模型确认处理图中的区域与目标节点。
将 UI 编辑器现有“用户先提供独立图片/图标,再执行组件绑定”的入口替换为自动切分素材:结构识别阶段直接返回可渲染组件草稿,分阶段按整页叶节点批次调用图片编辑模型,再由视觉模型确认处理图中的区域与目标节点。
## 识别结果
@@ -84,8 +84,8 @@ Rust 在接收并校验工具结果后,才把这两个枚举变体映射为正
## 前端正式接入
- UI 编辑器点击“自动分离”时,前端只调用一次 `separate_ui`,消费完整 `SeparationDTO`separation 内部 batch 不向前端暴露,也不在 UI 中显示 batch 进度。
- 如果 sidecar 已存在未完成的 `state.json`,点击入口时先打开独立弹窗,由用户选择“继续上次分离”或“开始新的分离”。继续复用 sidecar 的 pending tree;重新开始只替换当前 `state.json`,不删除 sidecar 图片。
- UI 编辑器点击“自动切分素材”时,前端只调用一次 `separate_ui`,消费完整 `SeparationDTO`separation 内部 batch 不向前端暴露,也不在 UI 中显示 batch 进度。
- 如果 sidecar 已存在未完成的 `state.json`,点击入口时先打开独立弹窗,由用户选择“继续上次自动切分素材”或“开始新的自动切分素材”。继续复用 sidecar 的 pending tree;重新开始只替换当前 `state.json`,不删除 sidecar 图片。
- `BoundNode.cut_image_path` 必须是项目根相对路径。前端使用现有 `import_local_project_image_assets` 登记 cut 图片;由于该通用命令单次最多 100 个路径,前端可以在资源登记阶段按 100 条分组调用,但这不属于 separation batch,也不向用户展示。
- 现有本地资源导入按清洗后的文件名 stem 与内容摘要生成目标路径;相同目标路径直接复用已有 manifest asset ID,内容不同则拒绝覆盖或生成不同摘要路径。前端不自行猜测 SpriteAsset 是否存在,也不从 NodeId 派生 SpriteAssetId。
- 全部可登记图片完成导入后,前端在一个 `runWithStateLocked` 中复用 `addSpriteAssets` 的内部 State 变换逻辑,加入返回的 SpriteAsset 并回填仍匹配 Node 的唯一未绑定 Image component,最后一次性提交 State。公开 `addSpriteAssets` 的普通 mutation guard 不放宽。
@@ -104,7 +104,7 @@ Rust 在接收并校验工具结果后,才把这两个枚举变体映射为正
## TODO
- 正在执行 batch 的持久化和恢复。
- 前端自动分离接入已实现;仍需补齐真实 Tauri/前端联调回归测试与失败注入测试。
- 前端自动切分素材接入已实现;仍需补齐真实 Tauri/前端联调回归测试与失败注入测试。
- 临时图片清理/归档策略。
- 手动抠图能力。
- problematic 对更高层 workflow 完成门禁的最终定义。
@@ -2,7 +2,7 @@
## 目标
UI 编辑器的“分析参考图”“识别界面结构”“绑定视觉素材”三个工作流动作在每次运行结束后,用独立的阻塞通知弹窗明确反馈结果,避免仅依赖卡片内一行状态文本而被忽略。
UI 编辑器的“分析参考图”“识别界面结构”“自动切分素材”三个工作流动作在每次运行结束后,用独立的阻塞通知弹窗明确反馈结果,避免仅依赖卡片内一行状态文本而被忽略。
## 交互约定
@@ -15,13 +15,13 @@ UI 编辑器的“分析参考图”“识别界面结构”“绑定视觉素
## 文案
弹窗标题由步骤名和结果态组成,例如“识别界面结构完成”或“绑定视觉素材失败”。正文复用卡片状态文本,并逐条扩展结果信息,统一以“请检查”收尾。
弹窗标题由步骤名和结果态组成,例如“识别界面结构完成”或“自动切分素材失败”。正文复用卡片状态文本,并逐条扩展结果信息,统一以“请检查”收尾。
成功状态的基线文案:
- 分析参考图:保留已应用的语义建议数量;若现有状态可可靠取得问题/待确认数量,则一并展示。
- 识别界面结构:保留替换的界面树数量,并展示识别结果中的待检查/必须修复数量(若可取得)。
- 绑定视觉素材:保留现有 `B/B` 批次计数,改为用户可读的绑定结果。
- 自动切分素材:保留现有 `B/B` 批次计数,改为用户可读的切分结果。
失败状态保留实际错误文本,仅在弹窗标题中补充步骤和失败上下文,正文同样以“请检查”收尾。
@@ -38,7 +38,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`,重复执行从最近真实阶段恢复。
3. `status` 只回读 State、页面阶段和 blockers,不推进项目 revision。
4. `finalize` 只接受 `game/` 下的真实 UTF-8 文件,写入与 UI State revision 绑定的应用标记;所有页面通过应用门禁后才返回 `visual-binding` 最终阶段路由。缺少页面、资源、组件或应用标记时拒绝伪造完成。
4. `finalize` 只接受 `game/` 下的真实 UTF-8 文件,写入与 UI State revision 绑定的应用标记;所有页面通过应用门禁后才返回 `asset-separation` 最终阶段路由。缺少页面、资源、组件或应用标记时拒绝伪造完成。
每次 State 或 manifest 阶段变化都推进项目 revision。Runtime 回执带有 `revisionAdvanceCount`,用于并发项目 revision 门禁;manifest 资产的 `source.generationKind` 依次记录:
@@ -67,7 +67,7 @@ ui-workflow.completed
- 没有关联时原子创建 `ui/UI 设计 N.json`,登记 `kind=UI``application/json`,并把原型图作为首张页面设计图载入 State。
- 成功后通过 `onManifestChange` 更新客户端资源投影,再打开 UI 编辑器;普通桥接从 `reference-analysis` 开始。
点击已有 `UI` 资源直接打开 UI 编辑器。若 manifest 阶段为 `ui-workflow.completed`,工作台自动打开该资源的 `visual-binding` 阶段(最远步骤为 2),交给用户做最终检查和手动调整。
点击已有 `UI` 资源直接打开 UI 编辑器。若 manifest 阶段为 `ui-workflow.completed`,工作台自动打开该资源的 `asset-separation` 阶段(最远步骤为 2),交给用户做最终检查和手动调整。
## 诚实完成门禁