From 52a58f7514f7f66be0632a75a487e8a273f7ff50 Mon Sep 17 00:00:00 2001 From: suzmii Date: Sat, 22 Aug 2026 17:31:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A7=E7=B2=BE=E4=BF=AE?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E7=A8=B3=E5=AE=9A=E5=85=A5=E5=8F=A3=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从已校验旧事务快照推导原稳定入口路径 允许稳定入口缺失时在下一次提交重建 补充旧事务连续精修回归测试与文档 --- .../src-tauri/src/project/asset_canvas.rs | 24 +++- .../src/project/asset_canvas_tests.rs | 112 ++++++++++++++++++ .../shared-memory/decision-log.md | 1 + ...客户端素材创作无限画布阶段一合同-2026-08-05.md | 2 +- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs index 97de94b9f..c3b0bb6bd 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas.rs @@ -3017,7 +3017,24 @@ fn asset_canvas_runtime_entry_path( if journal.asset_id != asset_id { continue; } - let Some(path) = journal.runtime_entry_relative_path else { + let candidate_path = match journal.runtime_entry_relative_path.clone() { + Some(path) => Some(path), + None => { + let journal_manifest_before: GameCreationAppManifest = read_asset_canvas_snapshot( + root, + &journal.commit_id, + "manifest.before.json", + &journal.manifest_before_sha256, + )?; + journal_manifest_before + .assets + .iter() + .find(|asset| asset.id == asset_id) + .map(|asset| asset.local_path.clone()) + .filter(|path| !path.starts_with("assets/canvas/")) + } + }; + let Some(path) = candidate_path else { continue; }; let is_newer = historical @@ -3043,10 +3060,7 @@ fn asset_canvas_runtime_entry_path( if source.local_path.starts_with("assets/canvas/") { return Ok(None); } - let path = resolve_local_project_path(root, &source.local_path)?; - if !path.is_file() { - return Ok(None); - } + resolve_local_project_path(root, &source.local_path)?; Ok(Some(source.local_path.clone())) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs index cd7b2f529..4073066f4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas_tests.rs @@ -584,6 +584,118 @@ fn refine_preserves_source_and_records_non_destructive_lineage() { ); } +#[test] +fn legacy_refine_transaction_preserves_runtime_entry_for_next_commit() { + let directory = tempfile::tempdir().expect("create legacy runtime entry fixture"); + init_local_game_project_at(directory.path(), PROJECT_ID, "旧事务精修项目") + .expect("initialize project"); + fs::write( + directory.path().join("assets/source.png"), + png_bytes([190, 40, 90, 255]), + ) + .expect("write source asset"); + let manifest_path = directory.path().join(".agent/manifest.json"); + let mut manifest = read_manifest(&manifest_path).expect("read manifest"); + manifest.assets.push(GameCreationAppAssetManifestEntry { + id: "source-asset".to_string(), + kind: "illustration".to_string(), + media_type: "image/png".to_string(), + local_path: "assets/source.png".to_string(), + source: GameCreationAppAssetSource { + kind: GameCreationAppAssetSourceKind::Uploaded, + canvas_project_id: None, + resource_id: None, + asset_object_id: None, + task_id: None, + prompt: None, + model: None, + generation_route: None, + generation_kind: None, + reference_resource_ids: Vec::new(), + }, + }); + write_manifest(&manifest_path, &manifest).expect("register source asset"); + let draft = create_asset_canvas_draft_at( + directory.path(), + &create_input( + directory.path(), + &Uuid::new_v4().to_string(), + AssetCanvasIntent::Refine, + Some("source-asset".to_string()), + ), + ) + .expect("create refine draft") + .draft; + let fixture = Fixture { + directory, + draft: draft.clone(), + png: png_bytes([30, 90, 210, 255]), + }; + let staged = stage_image(&fixture, &draft); + let first_input = commit_input( + &fixture, + &draft, + &staged, + Uuid::new_v4().to_string(), + Uuid::new_v4().to_string(), + ); + commit_asset_canvas_at(fixture.root(), &first_input) + .expect("commit legacy runtime entry image"); + + let mut legacy_journal = read_asset_canvas_journal(fixture.root(), &first_input.commit_id) + .expect("read legacy journal") + .expect("legacy journal retained"); + legacy_journal.runtime_entry_relative_path = None; + legacy_journal.runtime_entry_refreshed = false; + write_asset_canvas_journal(fixture.root(), &legacy_journal) + .expect("persist legacy journal shape"); + fs::write( + fixture.root().join("assets/source.png"), + png_bytes([1, 2, 3, 255]), + ) + .expect("corrupt legacy runtime entry"); + + let draft_after_first = + read_asset_canvas_draft_locked(fixture.root(), PROJECT_ID, &draft.draft_id) + .expect("read draft after legacy commit") + .expect("draft retained after legacy commit"); + let second_png = png_bytes([210, 180, 40, 255]); + let staged_second = stage_asset_canvas_image_at( + fixture.root(), + &StageAssetCanvasImageInput { + project_path: project_path(fixture.root()), + expected_project_id: PROJECT_ID.to_string(), + draft_id: draft.draft_id.clone(), + expected_draft_revision: draft_after_first.revision, + media_type: "image/png".to_string(), + bytes: second_png.clone(), + }, + ) + .expect("stage second legacy image"); + let second_input = commit_input( + &fixture, + &draft_after_first, + &staged_second, + Uuid::new_v4().to_string(), + Uuid::new_v4().to_string(), + ); + commit_asset_canvas_at(fixture.root(), &second_input) + .expect("commit after legacy runtime entry migration"); + + assert_eq!( + fs::read(fixture.root().join("assets/source.png")).expect("read migrated runtime entry"), + second_png + ); + let second_journal = read_asset_canvas_journal(fixture.root(), &second_input.commit_id) + .expect("read migrated journal") + .expect("migrated journal retained"); + assert_eq!( + second_journal.runtime_entry_relative_path.as_deref(), + Some("assets/source.png") + ); + assert!(second_journal.runtime_entry_refreshed); +} + #[test] fn candidate_commit_freezes_layer_and_media_identity_and_keeps_draft_editable() { let fixture = initialize_refine_fixture(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 53af05bb9..805437e46 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14353,6 +14353,7 @@ - 图片精修的 manifest 继续指向不可变正式版本 `assets/canvas/--.png`;游戏源码已引用的原路径(例如 `assets/direct-game-background.png`)是稳定运行入口,不要求代码改写。 - “设为最终图”提交、幂等重放和事务恢复都会校验不可变版本的字节、SHA-256、尺寸后刷新稳定入口。稳定入口缺失或损坏时,以不可变版本为恢复来源。 +- 旧事务 journal 尚无稳定入口字段时,后续精修必须从该事务已校验的 `manifest.before.json` 迁移原入口路径,不能因为当前 manifest 已指向 `assets/canvas/**` 而丢失游戏运行入口。 ## 2026-08-21 JavaScript 工程统一为 npm workspaces diff --git a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md index d517c091d..8d3f8d8c0 100644 --- a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md +++ b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md @@ -911,7 +911,7 @@ cancelling | A35 | 精修文件名包含历史提交后缀 | 后续精修重新打开当前 `localPath`,或再次生成 / 设为最终图 | 统一剥离文件名末尾一个或多个 `--` 后缀并规范化为合法 1..=80 字符显示名;生成与最终提交使用同一结果 | | A36 | 确定性提交参数无效 | 候选提交名称或用途在校验阶段失败 | 在读取候选、staging、transaction 或 ledger 写入前零副作用失败;UI 作为输入校验错误允许继续编辑,不触发安全恢复 | | A37 | 候选首次确认 | 生成完成后与旧 autosave 并发 | 前端先同步 authoritative layers 并保存确认;确认前后端把未确认候选层合回旧保存,重启恢复可从私有 ledger 重建候选层,确认后的显式删除仍允许 | -| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 | +| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片,或继续精修旧版本事务创建的资源 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;新事务可从旧事务 `manifest.before.json` 迁移稳定入口身份,幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 | 阶段一至五最终审计只有在矩阵对应的纯模型、共享 React、Web adapter、Tauri adapter、Rust 持久化与 AppSurface 测试全部通过后,才可宣称图片素材创作正式闭环完成。