From 74609e2a5e137eaf9d7b46d6e17b64975db3faf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 24 Sep 2026 18:58:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=20UI=20=E8=AE=BE=E8=AE=A1=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=89=E4=B8=AA=E5=B7=A5=E5=85=B7=E7=AE=97=E8=BF=9B?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=8F=98=E6=9B=B4=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 项目变更判定与项目版本号推进判定补上 UI 设计文档的三个工具:成功返回即算改过项目 - 只重写派生产物的代码生成工具不推进项目版本号,避免虚报推进量、把"项目被别处改动"的误报再造出来 - 新建文档与跑工作流这两个工具在观察明细里报出真实推进格数,失败路径也报,避免切图素材已登记却漏计 - 非法人写的读明细逻辑收敛成一个helper,三处调用共用 - 补六条门禁用例覆盖成功即算、只出代码不算推进、真实推进量优先、失败但已推进才算 - 同步共享记忆的项目概览与决策记录 --- .../agent/runtime_actions/project_gates.rs | 177 +++++++++++++++--- .../src/agent/runtime_tools/ui_design_doc.rs | 61 +++++- .../shared-memory/decision-log.md | 9 + .../shared-memory/project-overview.md | 2 +- 4 files changed, 215 insertions(+), 34 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs index 44d022feb..75ef70891 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs @@ -649,12 +649,16 @@ pub(crate) fn is_agent_runtime_project_mutation_observation( if observation.tool == "project.patchset" { return agent_runtime_patchset_advanced_project_revision(observation); } + if agent_runtime_ui_design_doc_tool(&observation.tool) { + // 三个 UI 设计文档工具都在返回时改项目:`from-images` 登记设计图与文档,`run-workflow` 登记 + // 切图并保存文档,`into-js` 重写 `ui/generated-*.js`。成功返回即算改过项目;失败路径只有明细 + // 报出真实推进量时才算,避免漏掉"调用失败但素材已经登记进去"。 + return observation.status == "ok" + || agent_runtime_observation_detail_revision_advance_count(observation) + .is_some_and(|count| count > 0); + } if observation.tool == "canvas.asset_import" { - return observation - .detail - .as_deref() - .and_then(|detail| serde_json::from_str::(detail).ok()) - .and_then(|value| value.get("revisionAdvanceCount")?.as_u64()) + return agent_runtime_observation_detail_revision_advance_count(observation) .is_some_and(|count| count > 0); } observation.status == "ok" @@ -669,6 +673,25 @@ pub(crate) fn is_agent_runtime_project_mutation_observation( ) } +/// UI 设计文档链路当前的三个 Agent 工具(`ui.workflow.run` 退役后由它们接管)。 +fn agent_runtime_ui_design_doc_tool(tool: &str) -> bool { + matches!( + tool, + "ui-design-doc.from-images" | "ui-design-doc.run-workflow" | "ui-design-doc.into-js" + ) +} + +/// 明细里的真实项目 revision 推进量:登记类工具(`canvas.asset_import` 与 UI 设计文档工具)会写这个字段。 +fn agent_runtime_observation_detail_revision_advance_count( + observation: &AgentRuntimeToolObservation, +) -> Option { + observation + .detail + .as_deref() + .and_then(|detail| serde_json::from_str::(detail).ok()) + .and_then(|value| value.get("revisionAdvanceCount")?.as_u64()) +} + pub(in crate::agent) fn agent_runtime_command_start_advanced_project_revision( observation: &AgentRuntimeToolObservation, ) -> bool { @@ -731,16 +754,17 @@ pub(crate) fn agent_runtime_observation_advances_project_revision( if agent_runtime_patchset_advanced_project_revision(observation) { return true; } + if agent_runtime_ui_design_doc_tool(&observation.tool) { + // `into-js` 只重写派生产物,不推进 revision;另外两个(登记设计图/文档、登记切图并写回)会推进。 + return matches!( + observation.tool.as_str(), + "ui-design-doc.from-images" | "ui-design-doc.run-workflow" + ) && (observation.status == "ok" + || agent_runtime_observation_detail_revision_advance_count(observation) + .is_some_and(|count| count > 0)); + } if observation.status != "ok" { - return observation - .detail - .as_deref() - .and_then(|detail| serde_json::from_str::(detail).ok()) - .and_then(|value| { - value - .get("revisionAdvanceCount") - .and_then(serde_json::Value::as_u64) - }) + return agent_runtime_observation_detail_revision_advance_count(observation) .is_some_and(|count| count > 0); } match observation.tool.as_str() { @@ -751,12 +775,10 @@ pub(crate) fn agent_runtime_observation_advances_project_revision( | "project.restore" | "blackboard.write" | "canvas.asset_generate" => true, - "canvas.asset_import" => observation - .detail - .as_deref() - .and_then(|detail| serde_json::from_str::(detail).ok()) - .and_then(|value| value.get("revisionAdvanceCount")?.as_u64()) - .is_some_and(|count| count > 0), + "canvas.asset_import" => { + agent_runtime_observation_detail_revision_advance_count(observation) + .is_some_and(|count| count > 0) + } "memory.write" => true, _ => false, } @@ -765,16 +787,7 @@ pub(crate) fn agent_runtime_observation_advances_project_revision( fn agent_runtime_observation_project_revision_advance_count( observation: &AgentRuntimeToolObservation, ) -> u64 { - if let Some(count) = observation - .detail - .as_deref() - .and_then(|detail| serde_json::from_str::(detail).ok()) - .and_then(|value| { - value - .get("revisionAdvanceCount") - .and_then(serde_json::Value::as_u64) - }) - { + if let Some(count) = agent_runtime_observation_detail_revision_advance_count(observation) { return count; } if agent_runtime_observation_advances_project_revision(observation) { @@ -1777,3 +1790,107 @@ mod static_delegate_barrier_detail_gate_tests { } } } + +#[cfg(test)] +mod ui_design_doc_project_mutation_gate_tests { + use super::*; + + fn observation(tool: &str, status: &str, detail: Option<&str>) -> AgentRuntimeToolObservation { + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: status.to_string(), + summary: String::new(), + detail: detail.map(str::to_string), + } + } + + #[test] + fn ui_design_doc_tools_count_as_project_mutation_when_they_return() { + for tool in [ + "ui-design-doc.from-images", + "ui-design-doc.run-workflow", + "ui-design-doc.into-js", + ] { + let observation = observation(tool, "ok", None); + assert!( + is_agent_runtime_project_mutation_observation(&observation), + "{tool} 成功返回后必须算改过项目" + ); + } + } + + #[test] + fn into_js_does_not_advance_project_revision() { + let observation = observation("ui-design-doc.into-js", "ok", None); + assert!(!agent_runtime_observation_advances_project_revision( + &observation + )); + assert_eq!( + agent_runtime_observation_project_revision_advance_count(&observation), + 0, + "只重写派生产物时不能虚报 revision 推进,否则会误报项目被别处改动" + ); + } + + #[test] + fn registered_ui_design_doc_tools_advance_project_revision() { + for tool in ["ui-design-doc.from-images", "ui-design-doc.run-workflow"] { + let observation = observation(tool, "ok", None); + assert!(agent_runtime_observation_advances_project_revision( + &observation + )); + assert_eq!( + agent_runtime_observation_project_revision_advance_count(&observation), + 1 + ); + } + } + + #[test] + fn reported_revision_advance_count_is_used_verbatim() { + let observation = observation( + "ui-design-doc.run-workflow", + "ok", + Some(r#"{"revisionAdvanceCount":3}"#), + ); + assert_eq!( + agent_runtime_observation_project_revision_advance_count(&observation), + 3, + "登记类工具在明细里报出的真实推进量必须优先于兜底的 1" + ); + } + + #[test] + fn failed_ui_design_doc_tool_counts_only_when_it_already_advanced_revision() { + let rolled_back = observation("ui-design-doc.from-images", "error", None); + assert!(!is_agent_runtime_project_mutation_observation(&rolled_back)); + assert!(!agent_runtime_observation_advances_project_revision( + &rolled_back + )); + + let partially_advanced = observation( + "ui-design-doc.run-workflow", + "error", + Some(r#"{"revisionAdvanceCount":2}"#), + ); + assert!(is_agent_runtime_project_mutation_observation( + &partially_advanced + )); + assert!(agent_runtime_observation_advances_project_revision( + &partially_advanced + )); + assert_eq!( + agent_runtime_observation_project_revision_advance_count(&partially_advanced), + 2 + ); + } + + #[test] + fn non_project_tools_are_unaffected() { + let observation = observation("file.read", "ok", Some(r#"{"lineCount":12}"#)); + assert!(!is_agent_runtime_project_mutation_observation(&observation)); + assert!(!agent_runtime_observation_advances_project_revision( + &observation + )); + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs index 4017acbc9..1cd763194 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/ui_design_doc.rs @@ -64,6 +64,8 @@ pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_from_images( }; match created { Ok(created) => { + let revision_advance = revision_before + .map(|before| created.committed_project_revision.saturating_sub(before)); if revision_before.is_none_or(|before| created.committed_project_revision > before) { emit_game_creator_manifest_invalidated(root, tool); } @@ -80,11 +82,17 @@ pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_from_images( "assetId": created.asset.id, "relativePath": created.relative_path, "imageIds": created.image_ids, + "revisionAdvanceCount": revision_advance, })) .ok(), } } - Err(error) => error_observation(root, tool, error), + Err(error) => error_observation_with_revision_advance( + root, + tool, + error, + project_revision_advance_since(root, revision_before), + ), } } @@ -122,6 +130,9 @@ pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_run_workflow( let revision_after = read_game_creator_agent_runtime_project_revision(root) .ok() .map(|snapshot| snapshot.revision); + let revision_advance = revision_before + .zip(revision_after) + .map(|(before, after)| after.saturating_sub(before)); if revision_before .is_none_or(|before| revision_after.is_none_or(|after| after > before)) { @@ -142,10 +153,15 @@ pub(in crate::agent) async fn observe_agent_runtime_ui_design_doc_run_workflow( String::new() } ), - detail: serde_json::to_string(&result).ok(), + detail: workflow_detail_with_revision_advance(&result, revision_advance), } } - Err(error) => error_observation(root, tool, error), + Err(error) => error_observation_with_revision_advance( + root, + tool, + error, + project_revision_advance_since(root, revision_before), + ), } } @@ -212,6 +228,45 @@ fn error_observation(root: &Path, tool: &str, error: String) -> AgentRuntimeTool } } +/// 失败路径也可能已经改过项目(切图素材先登记、后面的步骤才失败,且按约定不回滚),所以重读一次项目 +/// 版本号,把真实推进量写进明细交给门禁;推进量为 0 时不写字段,明细保持原样。 +fn project_revision_advance_since(root: &Path, revision_before: Option) -> u64 { + let Some(before) = revision_before else { + return 0; + }; + read_game_creator_agent_runtime_project_revision(root) + .map(|snapshot| snapshot.revision.saturating_sub(before)) + .unwrap_or(0) +} + +fn error_observation_with_revision_advance( + root: &Path, + tool: &str, + error: String, + revision_advance: u64, +) -> AgentRuntimeToolObservation { + let mut observation = error_observation(root, tool, error); + if revision_advance > 0 { + observation.detail = + Some(serde_json::json!({ "revisionAdvanceCount": revision_advance }).to_string()); + } + observation +} + +/// 工作流结果明细原样保留,只追加一项真实推进量,供项目变更门禁读取。 +fn workflow_detail_with_revision_advance( + result: &T, + revision_advance: Option, +) -> Option { + let mut value = serde_json::to_value(result).ok()?; + if let Some(revision_advance) = revision_advance { + value + .as_object_mut()? + .insert("revisionAdvanceCount".to_string(), revision_advance.into()); + } + serde_json::to_string(&value).ok() +} + #[cfg(test)] mod tests { use super::*; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f4b572cca..69189efdd 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9275,6 +9275,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 影响面:`apps/ai-game-creator-shell/src-tauri/src/{main.rs,ui_editor/**}`、`src/features/ui-editor/**`、`src/view/ui-editor/**`、`src/view/project-development/index.tsx`、`tests/{uiEditorPage,uiEditorState,previewWorkspaceZoom}.test.*`、`docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md`、`docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md`、`docs/technical/【设计】UI编辑器工作流完成通知弹窗-2026-09-04.md`、`docs/technical/【前端架构】UI编辑会话模块边界-2026-08-19.md`。 - 验证:`cargo check` 与 `cargo test --bin genarrative-ai-game-creator-shell ui_editor`(160 passed)通过,ts-rs 重新导出 `types/UIDesignImage.ts` 并删除三个已退役类型;`npx vitest run` 定向 `uiEditorPage` / `uiEditorState` / `uiDesignStateStore` / `previewWorkspaceZoom` / `appSurface` 全绿;AGC `tsc --noEmit`、改动文件 eslint、`cargo fmt --check`、`check:encoding`、`git diff --check` 通过。整套 Rust 测试在本容器仍有 60 条环境性失败(`/sbin -> usr/bin` 让 `command.exec` 沙箱 merged-usr 预检失败),与本次改动无关。 +## 2026-09-24 UI 设计文档三个工具纳入项目变更门禁(成功返回即算改过项目) + +- 背景:`ui.workflow.run` 退役时,项目变更门禁的两处工具名单(`agent/runtime_actions/project_gates.rs` 的 `is_agent_runtime_project_mutation_observation` 与 `agent_runtime_observation_advances_project_revision`)只删未补,新接管的 `ui-design-doc.from-images` / `ui-design-doc.run-workflow` / `ui-design-doc.into-js` 都没登记。后果:改完项目可能被判定"没改过",于是不要求验证就判完成、自动模式的 liveness 判据看不到进展;`agent_runtime_pending_expected_project_revision` 少算推进量又会误报 `pending_project_revision_drift`("并行项目变更使旧动作过期")。 +- 口径(产品确认):三个工具都在**成功返回时**改项目——`from-images` 登记设计图与文档、`run-workflow` 登记切图并保存文档、`into-js` 重写 `ui/generated-*.js`;调用中途不产生需要门禁额外追踪的中间态。 +- 决策:门禁按"工具名 + 成功返回"判定三者都算项目变更;其中 `into-js` 只重写派生产物、**不推进 revision**,所以不进 revision 推进名单——进去会虚报推进量,正好把要修的误报再造出来。 +- 决策(真实推进量):`from-images` / `run-workflow` 的观察明细带上 `revisionAdvanceCount`(沿用 `canvas.asset_import` 的既有字段),失败路径也带——切图素材先登记、后面步骤才失败时按约定不回滚,仍要如实计数,避免门禁把真实推进当成"别处改动"。 +- 代价与取舍:失败路径要多读一次项目 revision;`into-js` 属于"改了东西但项目 revision 没动"的少数派,与写文件类工具口径一致。 +- 验证方式:新增 `project_gates::ui_design_doc_project_mutation_gate_tests` 六条用例(成功即算变更、`into-js` 不推进 revision、登记类兜底推进量为 1、明细里的真实推进量优先、失败但已推进才算、无关工具不受影响)。 + ## 2026-09-23 UI 编辑器 Agent 工具化重写 - 背景:UI 编辑器的 Agent 链路原本只有一个 `ui.workflow.run`,把发现页面、桥接设计图、结构识别、多树合并、组件绑定、finalize 全塞进一个工具,工具参数本身就是工作流状态;识别与切分的产物由前端 `useUiEditorPage.ts` 落 State 再保存,Agent 侧没有任何恢复点,任一步失败只能整轮重来。 diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index 716123c42..a519a9249 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -67,7 +67,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐 - 2026-09-09 起,AGC 已新增遵循 OpenAI Agent Plugins 组合模型的通用 Plugin Host/SDK:Plugin、Skill 和 MCP 进入统一扩展 catalog;插件生命周期、行分隔 JSON-RPC、UI 面板、Capability Registry、权限和审计由 `plugin_host` 统一承接,Skill/MCP 仍分别交给各自现有 loader/transport;目标编辑器只通过通用 `EditorAdapter` 扩展点接入。详见 `docs/technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md`。 - Unity 编辑器能力以 `plugins/agc-unity-editor` 内置插件提供,固定复用 Apache-2.0 的 DotCraft Attach 核心;Windows x64 / Unity Mono 接入不安装项目包。GUI、Runtime 与 DirectProject 通过现有 Runner 统一执行归属,跨进程回执与持久不确定阻断统一处理。首次打开 Unity 工程只初始化 AGC `.agent` 元数据,保留原引擎工程;详见 `docs/technical/【技术方案】AGC Unity编辑器插件接入-2026-09-18.md`。 - DirectProject 的 Codex 原生文件、搜索、命令、图片查看和 Skill 仅在用户项目 cwd 与 `workspaceWrite(writableRoots=[project])` 内可用;原生命令允许联网以支持 npm 安装,npm 缓存位于项目内 `.npm-cache/`。多 Agent、Apps、插件、hooks、图片生成、Goals、Workspace Dependencies、Tool Suggestion 和原生浏览器/电脑控制保持关闭。app-server 使用隔离 `CODEX_HOME`,provider 凭据只由 AGC 客户端代理持有,不能进入模型上下文或 shell 环境。 -- `ui-prototype`(设计图片)与 UI 编辑器 `ui-design-doc` JSON 是不同资源。Agent 只通过三个工具驱动:`ui-design-doc.from-images` 由一至四张已登记设计图新建并登记文档(文档内设计图身份即图片 assetId),`ui-design-doc.run-workflow` 在 Rust 内跑 `recognize → separate → write-back` 并写回 State/revision,`ui-design-doc.into-js` 产出 `ui/generated-*.js`(不推进 revision);工具名与入参文案在 `prompts/runtime/texts/ui-design-doc.json`。Provider 缺失、请求失败、工具缺失或结果不匹配时保留真实 State 并返回错误,不得用 deterministic seed 伪造完成;旧 `ui.workflow.run`、多树合并、组件绑定与原型幂等桥接已退役,不保留兼容入口。 +- `ui-prototype`(设计图片)与 UI 编辑器 `ui-design-doc` JSON 是不同资源。Agent 只通过三个工具驱动:`ui-design-doc.from-images` 由一至四张已登记设计图新建并登记文档(文档内设计图身份即图片 assetId),`ui-design-doc.run-workflow` 在 Rust 内跑 `recognize → separate → write-back` 并写回 State/revision,`ui-design-doc.into-js` 产出 `ui/generated-*.js`(不推进 revision);三个工具都算项目变更观察(成功返回即算改过项目),其中只有前两个推进项目 revision;工具名与入参文案在 `prompts/runtime/texts/ui-design-doc.json`。Provider 缺失、请求失败、工具缺失或结果不匹配时保留真实 State 并返回错误,不得用 deterministic seed 伪造完成;旧 `ui.workflow.run`、多树合并、组件绑定与原型幂等桥接已退役,不保留兼容入口。 - UI workflow 的资源桥接与 Runtime 边界以 `docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md` 和 AGC 实施计划的 2026-08-24 覆盖段为准;只生成图片、登记空 JSON 或进入普通图片画布都不构成 workflow 完成。 ## 当前产品边界