diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs index 3bb71b767..5bdf90008 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs @@ -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()); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model/result.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model/result.rs index 82b34bdcd..8dbb38383 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model/result.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/model/result.rs @@ -14,6 +14,8 @@ pub struct BoundNode { pub struct ProblematicNode { pub node_id: NodeId, pub problem_description: String, + #[serde(default)] + pub problem_history: Vec, pub rework_count: u32, } diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/patch.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/patch.rs index d8998f1c3..5bdc82a23 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/patch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow/patch.rs @@ -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 { + 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() +} diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/separationStatus.ts b/apps/ai-game-creator-shell/src/features/ui-editor/separationStatus.ts new file mode 100644 index 000000000..d789e7045 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/ui-editor/separationStatus.ts @@ -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; +} diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/types/ProblematicNode.ts b/apps/ai-game-creator-shell/src/features/ui-editor/types/ProblematicNode.ts index c77e0a49b..0badc8890 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/types/ProblematicNode.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/types/ProblematicNode.ts @@ -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, rework_count: number, }; diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts index 50f26cee6..e2c690b06 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts @@ -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); }); diff --git a/apps/ai-game-creator-shell/tests/separationOverview.test.ts b/apps/ai-game-creator-shell/tests/separationOverview.test.ts index 91b39ca72..4761832ad 100644 --- a/apps/ai-game-creator-shell/tests/separationOverview.test.ts +++ b/apps/ai-game-creator-shell/tests/separationOverview.test.ts @@ -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'); + }); +}); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 079d6890b..3c4eaefc5 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -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 编排覆盖 diff --git a/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md b/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md index 69a91b0b6..f618c73f0 100644 --- a/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md +++ b/docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md @@ -55,7 +55,7 @@ ui-workflow.completed 最终回执写入 `.agent/ui-workflows/.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、只写计划或只打开普通图片画布均不算完成。