建立AGC资源语义类型契约

共享契约新增 canonical 资源类型目录和历史类型读时映射

角色动画派生结果写入 character-animation 主类型

补充角色动画落盘与 legacy 类型映射回归测试

更新资源自由画板技术方案的类型边界说明
This commit is contained in:
2026-09-08 13:41:55 +08:00
parent f7374b2101
commit 57a5b642fc
6 changed files with 186 additions and 2 deletions
@@ -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");
@@ -13,6 +13,9 @@
### 栏目分页投影与设计文档缺失边界
- 栏目内容只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。
- 资源依赖图的精确引用身份同时支持两种稳定索引:远端 `source.resourceId` 与本地 `local-asset:<manifest assetId>`。派生资源把 `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 决定,资源页只消费已登记结果。
@@ -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');
});
});
@@ -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;
@@ -499,6 +499,43 @@ pub struct GameCreationAppAssetManifestEntry {
pub image_sequence_duration_ms: Option<u64>,
}
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;
+1 -1
View File
@@ -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',