From 57a5b642fcd58d751645fccf300f724c559960d8 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Tue, 8 Sep 2026 13:41:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BB=BA=E7=AB=8BAGC=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E7=B1=BB=E5=9E=8B=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 共享契约新增 canonical 资源类型目录和历史类型读时映射 角色动画派生结果写入 character-animation 主类型 补充角色动画落盘与 legacy 类型映射回归测试 更新资源自由画板技术方案的类型边界说明 --- .../src-tauri/src/project/resource_editor.rs | 73 ++++++++++++++++++- ...】GameAgent资源自由画板与快速编辑-2026-08-20.md | 3 + .../src/contracts/gameCreationApp.test.ts | 22 ++++++ .../shared/src/contracts/gameCreationApp.ts | 51 +++++++++++++ .../shared-contracts/src/game_creation_app.rs | 37 ++++++++++ vitest.config.ts | 2 +- 6 files changed, 186 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs index 5ca59b742..ddc026873 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs @@ -3950,9 +3950,14 @@ fn commit_resource_edit_asset_internal( }) .transpose()?; let image_sequence_duration_ms = ledger.remote_sequence_duration_ms; + let asset_kind = if input.edit_kind == LocalProjectResourceEditKind::CharacterAnimation { + "character-animation".to_string() + } else { + source.asset_kind.clone() + }; let asset = GameCreationAppAssetManifestEntry { id: asset_id.clone(), - kind: source.asset_kind.clone(), + kind: asset_kind, media_type: staged_media_type.to_string(), local_path: relative_path.clone(), image_sequence_frames, @@ -8967,6 +8972,72 @@ mod tests { ); } + #[test] + fn character_animation_commit_uses_animation_asset_kind() { + let directory = tempfile::tempdir().expect("create animation commit fixture"); + let root = directory.path(); + init_local_game_project_at(root, PROJECT_ID, "角色动画提交类型测试") + .expect("initialize project"); + let uploaded = upload_local_asset_at( + root, + "character-source.png", + "image/png", + &resource_editor_test_png(), + ) + .expect("upload source image"); + let manifest = read_existing_manifest_for_project(root).expect("read source manifest"); + let source_asset = manifest + .assets + .iter() + .find(|asset| asset.id == uploaded.id) + .cloned() + .expect("find source image"); + + let mut request = input( + root, + Uuid::new_v4().to_string(), + LocalProjectResourceEditKind::CharacterAnimation, + format!("asset:{}", source_asset.id), + ); + request.source_asset_id = Some(source_asset.id.clone()); + request.source_path = Some(source_asset.local_path.clone()); + request.source_media_type = Some(source_asset.media_type.clone()); + request.source_subtype = Some(source_asset.kind.clone()); + let source = resolve_resource_edit_source( + root, + &read_existing_manifest_for_project(root).expect("reread manifest"), + &request, + ) + .expect("resolve animation source"); + let mut ledger = ledger_for(&request, &source, ResourceEditLedgerPhase::MediaDownloaded); + ledger.staged_media_type = Some("video/mp4".to_string()); + ledger.staged_extension = Some("mp4".to_string()); + ledger.remote_sequence_duration_ms = Some(4_000); + write_resource_edit_staging( + root, + &request.operation_id, + b"\0\0\0\x18ftypisom\0\0\0\0isomiso2", + ) + .expect("stage animation preview"); + + let result = commit_resource_edit_asset( + root, + &request, + &source, + &request.prompt, + &request.asset_name, + &mut ledger, + ) + .expect("commit animation derivative"); + let derivative = result.asset.expect("animation derivative"); + assert_eq!(derivative.kind, "character-animation"); + assert_eq!( + derivative.source.generation_kind.as_deref(), + Some("character-animation") + ); + assert_eq!(derivative.image_sequence_duration_ms, Some(4_000)); + } + #[test] fn platform_session_switch_waits_until_local_asset_commit_finishes() { let directory = tempfile::tempdir().expect("create commit account switch fixture"); diff --git a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md index 4b543bef8..c21017212 100644 --- a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md +++ b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md @@ -13,6 +13,9 @@ ### 栏目分页投影与设计文档缺失边界 - 栏目内容只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。 +- 资源依赖图的精确引用身份同时支持两种稳定索引:远端 `source.resourceId` 与本地 `local-asset:`。派生资源把 `referenceResourceIds` 写成本地 canonical 身份时,必须能解析到同一项目的唯一资源;候选去重后才允许建立一条 `asset-reference`,无法唯一解析的引用继续进入 `unresolvedReferenceResourceIds`,不得在前端猜测连线。 +- AGC 侧共享契约维护 canonical 资产类型目录和历史读时映射:`game-background -> scene`、`character-art -> character`、`ui-prototype -> ui-design`、`art-spritesheet -> icon-spritesheet`、`art-spritesheet-slice -> icon`、`illustration/game-art -> image`。该目录只约束 AGC 新写入与投影,不要求现役 Web 美术画布同步改造;历史 manifest 保持原值,读取时可归一到 canonical 展示。 +- 角色动画派生结果的 manifest `kind` 固定写 `character-animation`,`source.generationKind` 同步保留 `character-animation`,媒体仍以预览视频加正式序列帧登记。图片编辑等普通派生继续继承源资源语义 kind;后续如需改写为 canonical kind,只能在读取投影或显式迁移中完成,不重写历史 manifest。 - 固定栏目页始终为 `设计文档 -> 美术资源 -> 音乐音效 -> 游戏代码 -> 项目版本`。若某栏目没有对应投影,Dock 仍显示该栏目并允许打开空画布;空画布只表达“当前没有已登记资源”,不得由 UI 补假数据。 - Direct Codex 的游戏生成链路以 `game/index.html`、`game/style.css`、`game/game.js` 作为代码资产登记;是否额外产出设计文档由当前任务和 Agent 决定,资源页只消费已登记结果。 diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index 1742f5956..96be13fb6 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from 'vitest'; import { + canonicalGameCreationAppAssetKind, createGameCreationAppManifest, createGameCreationAppSeedTasks, GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_MAX_PASSES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, + GAME_CREATION_APP_CANONICAL_ASSET_KINDS, GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION, @@ -718,4 +720,24 @@ describe('AI 游戏创作 App 共享契约', () => { ], }); }); + + it('maps legacy canvas asset kinds to canonical kinds', () => { + expect(GAME_CREATION_APP_CANONICAL_ASSET_KINDS).toContain( + 'character-animation', + ); + expect(canonicalGameCreationAppAssetKind('game-background')).toBe('scene'); + expect(canonicalGameCreationAppAssetKind('character-art')).toBe( + 'character', + ); + expect(canonicalGameCreationAppAssetKind('ui-prototype')).toBe('ui-design'); + expect(canonicalGameCreationAppAssetKind('art-spritesheet')).toBe( + 'icon-spritesheet', + ); + expect(canonicalGameCreationAppAssetKind('art-spritesheet-slice')).toBe( + 'icon', + ); + expect(canonicalGameCreationAppAssetKind('illustration')).toBe('image'); + expect(canonicalGameCreationAppAssetKind('character')).toBe('character'); + expect(canonicalGameCreationAppAssetKind('unknown-kind')).toBe('image'); + }); }); diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index 1f39ad546..3f3061f57 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -476,6 +476,57 @@ export interface GameCreationAppAssetManifestEntry { imageSequenceDurationMs?: number | null; } +export const GAME_CREATION_APP_CANONICAL_ASSET_KINDS = [ + 'image', + 'scene', + 'character', + 'character-animation', + 'icon', + 'icon-spritesheet', + 'icon-spec', + 'ui-design', + 'publication-material', + 'spec', + 'video', + 'sound-effect', + 'background-music', + 'audio', + 'document', + 'code', +] as const; + +export type GameCreationAppCanonicalAssetKind = + (typeof GAME_CREATION_APP_CANONICAL_ASSET_KINDS)[number]; + +const GAME_CREATION_APP_LEGACY_ASSET_KINDS: Record< + string, + GameCreationAppCanonicalAssetKind +> = { + 'game-background': 'scene', + 'character-art': 'character', + 'ui-prototype': 'ui-design', + 'art-spritesheet': 'icon-spritesheet', + 'art-spritesheet-slice': 'icon', + illustration: 'image', + 'game-art': 'image', +}; + +export function canonicalGameCreationAppAssetKind( + value: string, +): GameCreationAppCanonicalAssetKind { + const normalized = value.trim(); + const legacyKind = GAME_CREATION_APP_LEGACY_ASSET_KINDS[normalized]; + if (legacyKind) return legacyKind; + if ( + (GAME_CREATION_APP_CANONICAL_ASSET_KINDS as readonly string[]).includes( + normalized, + ) + ) { + return normalized as GameCreationAppCanonicalAssetKind; + } + return 'image'; +} + export const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION = 'game-creator-resource-layout.v1' as const; export const GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION = 9_007_199_254_740_991; diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 0737447c7..0cd799981 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -499,6 +499,43 @@ pub struct GameCreationAppAssetManifestEntry { pub image_sequence_duration_ms: Option, } +pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [ + "image", + "scene", + "character", + "character-animation", + "icon", + "icon-spritesheet", + "icon-spec", + "ui-design", + "publication-material", + "spec", + "video", + "sound-effect", + "background-music", + "audio", + "document", + "code", +]; + +pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str { + match value.trim() { + "game-background" => "scene", + "character-art" => "character", + "ui-prototype" => "ui-design", + "art-spritesheet" => "icon-spritesheet", + "art-spritesheet-slice" => "icon", + "illustration" | "game-art" => "image", + canonical => match GAME_CREATION_APP_CANONICAL_ASSET_KINDS + .iter() + .find(|candidate| **candidate == canonical) + { + Some(value) => value, + None => "image", + }, + } +} + pub const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION: &str = "game-creator-resource-layout.v1"; pub const GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION: u64 = 9_007_199_254_740_991; diff --git a/vitest.config.ts b/vitest.config.ts index 4a7f87731..12acb6a43 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -125,7 +125,7 @@ export default defineConfig({ 'apps/ai-game-creator-shell/tests/**/*.test.tsx', 'miniprogram/**/*.test.js', 'scripts/**/*.test.ts', - 'packages/shared/src/contracts/hostBridge.test.ts', + 'packages/shared/src/contracts/*.test.ts', 'packages/shared/src/components/**/*.test.ts', 'packages/shared/src/components/**/*.test.tsx', 'packages/shared/src/stores/**/*.test.ts',