From e2a50088c9650c8eb750f9a4d193220c38ac483c Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 8 Sep 2026 13:29:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=9C=AC=E5=9C=B0=E8=BA=AB=E4=BB=BD=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 资源依赖图同时索引远端资源身份和 local-asset 本地资源身份 本地 canonical 引用与远端候选去重后再建立依赖连线 补充本地身份解析与候选不歧义回归测试 更新资源自由画板技术方案的依赖身份合同 --- .../src/project/resource_dependency_graph.rs | 101 +++++++++++++++++- ...】GameAgent资源自由画板与快速编辑-2026-08-20.md | 1 + 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs index 82ec4561c..4fbfeecb0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs @@ -400,6 +400,12 @@ pub(crate) fn build_project_resource_graph( .push(resource_ids[0].clone()); } + let resources_by_manifest_asset_id = resource_ids_by_manifest_asset + .iter() + .filter(|(_, resource_ids)| resource_ids.len() == 1) + .map(|(asset_id, resource_ids)| (asset_id.clone(), resource_ids[0].clone())) + .collect::>(); + let mut unresolved_reference_resource_ids = BTreeSet::new(); let mut reference_edge_by_id = BTreeMap::::new(); for (asset_id, target_resource_ids) in &resource_ids_by_manifest_asset { @@ -418,16 +424,32 @@ pub(crate) fn build_project_resource_graph( .filter(|value| !value.is_empty()) .collect::>() { - let source_candidates = resources_by_external_id + let mut source_candidates = resources_by_external_id .get(external_reference_id) .map(Vec::as_slice) - .unwrap_or(&[]); + .unwrap_or(&[]) + .iter() + .collect::>(); + if let Some(referenced_asset_id) = external_reference_id + .strip_prefix("local-asset:") + .map(str::trim) + .filter(|value| !value.is_empty()) + { + if let Some(source_resource_id) = + resources_by_manifest_asset_id.get(referenced_asset_id) + { + source_candidates.insert(source_resource_id); + } + } if source_candidates.len() != 1 { unresolved_reference_resource_ids.insert(external_reference_id.to_string()); continue; } - let source_resource_id = &source_candidates[0]; - if !resource_by_id.contains_key(source_resource_id) + let source_resource_id = source_candidates + .iter() + .next() + .expect("a non-empty candidate set must have one resource"); + if !resource_by_id.contains_key(source_resource_id.as_str()) || !resource_by_id.contains_key(target_resource_id) { continue; @@ -438,7 +460,7 @@ pub(crate) fn build_project_resource_graph( ProjectResourceReferenceEdge { id, kind: "asset-reference".to_string(), - source_resource_id: source_resource_id.clone(), + source_resource_id: source_resource_id.to_string(), target_resource_id: target_resource_id.clone(), cyclic: false, }, @@ -835,6 +857,75 @@ mod tests { ); } + #[test] + fn graph_resolves_local_asset_reference_identities() { + let manifest = manifest( + Vec::new(), + vec![ + asset("source-1", None, &[], None), + asset( + "derivative-1", + Some("local-asset:derivative-1"), + &["local-asset:source-1"], + None, + ), + ], + ); + let graph = build_project_resource_graph( + &manifest, + vec![ + resource("asset:source-1", Some("source-1"), None), + resource("asset:derivative-1", Some("derivative-1"), None), + ], + &[], + false, + ); + + assert_eq!(graph.reference_edges.len(), 1); + assert_eq!( + graph.reference_edges[0].source_resource_id, + "asset:source-1" + ); + assert_eq!( + graph.reference_edges[0].target_resource_id, + "asset:derivative-1" + ); + assert!(graph.unresolved_reference_resource_ids.is_empty()); + assert!(graph + .connection_index + .iter() + .any(|index| index.resource_id == "asset:derivative-1" + && index.upstream_reference_resource_ids == vec!["asset:source-1"])); + } + + #[test] + fn graph_resolves_local_asset_identity_without_ambiguous_remote_duplicate() { + let manifest = manifest( + Vec::new(), + vec![ + asset("source-1", Some("external-source"), &[], None), + asset( + "derivative-1", + Some("local-asset:derivative-1"), + &["local-asset:source-1"], + None, + ), + ], + ); + let graph = build_project_resource_graph( + &manifest, + vec![ + resource("asset:source-1", Some("source-1"), None), + resource("asset:derivative-1", Some("derivative-1"), None), + ], + &[], + false, + ); + + assert_eq!(graph.reference_edges.len(), 1); + assert!(graph.unresolved_reference_resource_ids.is_empty()); + } + #[test] fn graph_aggregates_flows_filters_missing_resources_and_detects_cycles_iteratively() { let manifest = manifest( diff --git a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md index 6abf63e70..26fd5dd77 100644 --- a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md +++ b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md @@ -39,6 +39,7 @@ ### 栏目分页投影与设计文档缺失边界 - 栏目内容只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。 +- 资源依赖图的精确引用身份同时支持两种稳定索引:远端 `source.resourceId` 与本地 `local-asset:`。派生资源把 `referenceResourceIds` 写成本地 canonical 身份时,必须能解析到同一项目的唯一资源;候选去重后才允许建立一条 `asset-reference`,无法唯一解析的引用继续进入 `unresolvedReferenceResourceIds`,不得在前端猜测连线。 - 固定栏目页始终为 `设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本`。若某栏目没有对应投影,Dock 仍显示该栏目并允许打开空画布;空画布只表达“当前没有已登记资源”,不得由 UI 补假数据。 - Direct Codex 的游戏生成链路以 `game/index.html`、`game/style.css`、`game/game.js` 作为代码资产登记;是否额外产出设计文档由当前任务和 Agent 决定,资源页只消费已登记结果。