回写自动切分问题节点状态
扩展 problematic DTO 携带完整返工历史 分离流程回写 NeedReview 并清理成功节点旧状态 补充概览计数定位与状态覆盖测试 同步 UI workflow 规范文档
This commit is contained in:
@@ -287,6 +287,10 @@ mod tests {
|
||||
separation.problematic_nodes[0].rework_count,
|
||||
MAX_REWORK_COUNT
|
||||
);
|
||||
assert_eq!(
|
||||
separation.problematic_nodes[0].problem_history,
|
||||
vec!["第一次意见", "第二次意见", "最后一次意见"]
|
||||
);
|
||||
assert_eq!(node.rework_count, MAX_REWORK_COUNT);
|
||||
assert!(next_image_batch(&separation, &separation.trees[0]).is_empty());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ pub struct BoundNode {
|
||||
pub struct ProblematicNode {
|
||||
pub node_id: NodeId,
|
||||
pub problem_description: String,
|
||||
#[serde(default)]
|
||||
pub problem_history: Vec<String>,
|
||||
pub rework_count: u32,
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -61,9 +61,11 @@ pub fn apply_batch_patch(
|
||||
let count = rework_counts.get(to_node).copied().unwrap_or(0) + 1;
|
||||
increment_rework_count(&mut tree.root, to_node, count);
|
||||
if count >= MAX_REWORK_COUNT {
|
||||
let problem_history = rework_history(&tree.root, to_node);
|
||||
state.problematic_nodes.push(ProblematicNode {
|
||||
node_id: to_node.clone(),
|
||||
problem_description: problem_description.clone(),
|
||||
problem_history,
|
||||
rework_count: count,
|
||||
});
|
||||
}
|
||||
@@ -99,3 +101,16 @@ fn increment_rework_count(node: &mut SeparationNode, id: &NodeId, count: u32) {
|
||||
increment_rework_count(child, id, count);
|
||||
}
|
||||
}
|
||||
|
||||
fn rework_history(node: &SeparationNode, id: &NodeId) -> Vec<String> {
|
||||
if node.id == *id {
|
||||
return node.note.rework_notes.clone();
|
||||
}
|
||||
node.children
|
||||
.iter()
|
||||
.find_map(|child| {
|
||||
let history = rework_history(child, id);
|
||||
(!history.is_empty()).then_some(history)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { collectUiTreeNodeTargets } from './stageStatusOverview';
|
||||
import type { Node as UiNode } from './types/Node';
|
||||
import type { ProblematicNode } from './types/ProblematicNode';
|
||||
import type { UITree } from './types/UITree';
|
||||
|
||||
export function separationProblemReason(problematic: ProblematicNode): string {
|
||||
const history = problematic.problem_history
|
||||
.filter((item) => item.trim())
|
||||
.join('\n');
|
||||
const details = history || problematic.problem_description;
|
||||
return `自动切分重试已达上限(${problematic.rework_count} 次)${details ? `\n${details}` : ''}`;
|
||||
}
|
||||
|
||||
export function clearSeparationComponentStatus(node: UiNode): void {
|
||||
node.metadata.component_status = 'NoProblem';
|
||||
}
|
||||
|
||||
export function applySeparationProblematicStatuses(
|
||||
uiTrees: UITree[],
|
||||
problematicNodes: ProblematicNode[],
|
||||
): string[] {
|
||||
const targets = new Map(
|
||||
collectUiTreeNodeTargets(uiTrees).map((target) => [target.node.id, target]),
|
||||
);
|
||||
const errors: string[] = [];
|
||||
for (const problematic of problematicNodes) {
|
||||
const target = targets.get(problematic.node_id);
|
||||
if (!target) {
|
||||
errors.push(`问题节点 ${problematic.node_id} 已不存在,已保留问题记录`);
|
||||
continue;
|
||||
}
|
||||
if (!target.node.component || !('Image' in target.node.component)) {
|
||||
errors.push(
|
||||
`问题节点 ${problematic.node_id} 不是可处理的 Image 组件,已保留问题记录`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
target.node.metadata.component_status = {
|
||||
NeedReview: separationProblemReason(problematic),
|
||||
};
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -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 { NodeId } from "./NodeId";
|
||||
|
||||
export type ProblematicNode = { node_id: NodeId, problem_description: string, rework_count: number, };
|
||||
export type ProblematicNode = { node_id: NodeId, problem_description: string, problem_history: Array<string>, rework_count: number, };
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
} from '../../features/ui-editor/importAdapter';
|
||||
import { applyMergeResult } from '../../features/ui-editor/merge';
|
||||
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
|
||||
import {
|
||||
applySeparationProblematicStatuses,
|
||||
clearSeparationComponentStatus,
|
||||
} from '../../features/ui-editor/separationStatus';
|
||||
import {
|
||||
getStageStatusOverview,
|
||||
type StageStatusField,
|
||||
@@ -1170,6 +1174,7 @@ export function useUiEditorSession(
|
||||
continue;
|
||||
}
|
||||
if (imageComponent.Image.target_graphic === sprite.asset_id) {
|
||||
clearSeparationComponentStatus(location.node);
|
||||
continue;
|
||||
}
|
||||
if (imageComponent.Image.target_graphic !== null) {
|
||||
@@ -1179,7 +1184,14 @@ export function useUiEditorSession(
|
||||
continue;
|
||||
}
|
||||
imageComponent.Image.target_graphic = sprite.asset_id;
|
||||
clearSeparationComponentStatus(location.node);
|
||||
}
|
||||
backfillErrors.push(
|
||||
...applySeparationProblematicStatuses(
|
||||
next.ui_trees,
|
||||
result.problematic_nodes,
|
||||
),
|
||||
);
|
||||
editor.replaceState(next);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
nodeHasPendingSeparation,
|
||||
nodeNeedsComponentReview,
|
||||
} from '../src/features/ui-editor/separationOverview';
|
||||
import {
|
||||
applySeparationProblematicStatuses,
|
||||
clearSeparationComponentStatus,
|
||||
} from '../src/features/ui-editor/separationStatus';
|
||||
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';
|
||||
@@ -131,3 +135,41 @@ describe('getSeparationOverview', () => {
|
||||
).toBe('blocked');
|
||||
});
|
||||
});
|
||||
|
||||
describe('separation status writeback', () => {
|
||||
it('writes problematic history into NeedReview and overwrites Blocked', () => {
|
||||
const problematicTree: UITree[] = [
|
||||
{
|
||||
src_ui_design: 'page-a',
|
||||
root: node('root', null, 'NoProblem', [
|
||||
node('problematic', image(null), { Blocked: '旧阻塞' }),
|
||||
]),
|
||||
},
|
||||
];
|
||||
expect(
|
||||
applySeparationProblematicStatuses(problematicTree, [
|
||||
{
|
||||
node_id: 'problematic',
|
||||
problem_description: '当前建议',
|
||||
problem_history: ['第一次建议', '最后建议'],
|
||||
rework_count: 3,
|
||||
},
|
||||
]),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
problematicTree[0].root.children[0]?.metadata.component_status,
|
||||
).toEqual({
|
||||
NeedReview: '自动切分重试已达上限(3 次)\n第一次建议\n最后建议',
|
||||
});
|
||||
expect(getSeparationOverview(problematicTree, {})).toMatchObject({
|
||||
needsAttention: 1,
|
||||
blocked: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the old status after a successful bound writeback', () => {
|
||||
const boundNode = node('bound', image(null), { Blocked: '旧阻塞' });
|
||||
clearSeparationComponentStatus(boundNode);
|
||||
expect(boundNode.metadata.component_status).toBe('NoProblem');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1276,7 +1276,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 编辑器 `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-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 冒充完成;自动切分达到返工上限的 problematic 节点则回写 UI State 的 `component_status = NeedReview(...)`,作为已尽力完成、交由用户在 UI 编辑器中处理的结果。工作台点击 `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 编排覆盖
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ ui-workflow.completed
|
||||
|
||||
最终回执写入 `.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 `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 未配置、请求失败、工具调用缺失、结果不匹配、未知字体引用或绑定没有可渲染组件时,完成阶段不会推进;自动切分达到返工上限的 problematic 节点属于“已尽力完成”的可人工收尾结果,由 UI State 回写 `component_status = NeedReview(...)` 并交给编辑器概览定位,不阻止本地分离流程报告成功。已落盘的中间阶段仍通过 manifest invalidation 更新客户端,不再使用 deterministic seed 冒充语义处理通过。
|
||||
|
||||
UI 编辑器的分离提示会对节点描述和返工备注做长度与控制字符清洗,避免用户/模型文本改变提示结构;远端图片的 Base64 解码、处理图尺寸及 multipart 上传解析均受有界检查和准备截止时间约束。切分 sidecar 产物使用 UUID 文件名,失败检查点只记录附加错误并保留原始业务错误,便于恢复与排障。
|
||||
|
||||
@@ -71,6 +71,8 @@ UI 编辑器的分离提示会对节点描述和返工备注做长度与控制
|
||||
|
||||
点击已有 `UI` 资源直接打开 UI 编辑器。若 manifest 阶段为 `ui-workflow.completed`,工作台自动打开该资源的 `asset-separation` 阶段(最远步骤为 2),交给用户做最终检查和手动调整。
|
||||
|
||||
自动切分达到返工上限的 problematic 节点会随分离 DTO 返回每节点的 `problem_history`,并由编辑器回写为 `component_status = NeedReview(...)`。`SeparationOverview` 只读取 UI State 中的状态来计数和定位;该结果仍按“已尽力完成”报告成功并执行既有 finalize,剩余节点由用户在概览定位后手动处理或再次发起分离。
|
||||
|
||||
## 诚实完成门禁
|
||||
|
||||
`ui-prototype` 图片登记、UI JSON 创建、页面 State 保存、结构树生成、游戏应用标记和最终路由是不同证据。Agent 只有拿到所有页面的 `completed` 状态与 `finalStageRoute` 才能报告完成;只生成图片、只创建空 JSON、只写计划或只打开普通图片画布均不算完成。
|
||||
|
||||
Reference in New Issue
Block a user