把 UI 设计文档三个工具算进项目变更门禁

- 项目变更判定与项目版本号推进判定补上 UI 设计文档的三个工具:成功返回即算改过项目
- 只重写派生产物的代码生成工具不推进项目版本号,避免虚报推进量、把"项目被别处改动"的误报再造出来
- 新建文档与跑工作流这两个工具在观察明细里报出真实推进格数,失败路径也报,避免切图素材已登记却漏计
- 非法人写的读明细逻辑收敛成一个helper,三处调用共用
- 补六条门禁用例覆盖成功即算、只出代码不算推进、真实推进量优先、失败但已推进才算
- 同步共享记忆的项目概览与决策记录
This commit is contained in:
2026-09-24 18:58:12 +08:00
parent 565c2277d5
commit 74609e2a5e
4 changed files with 215 additions and 34 deletions
@@ -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::<serde_json::Value>(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<u64> {
observation
.detail
.as_deref()
.and_then(|detail| serde_json::from_str::<serde_json::Value>(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::<serde_json::Value>(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::<serde_json::Value>(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::<serde_json::Value>(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
));
}
}
@@ -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>) -> 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<T: serde::Serialize>(
result: &T,
revision_advance: Option<u64>,
) -> Option<String> {
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::*;
@@ -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 侧没有任何恢复点,任一步失败只能整轮重来。
@@ -67,7 +67,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
- 2026-09-09 起,AGC 已新增遵循 OpenAI Agent Plugins 组合模型的通用 Plugin Host/SDKPlugin、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 完成。
## 当前产品边界