建立AGC资源语义类型契约
共享契约新增 canonical 资源类型目录和历史类型读时映射 角色动画派生结果写入 character-animation 主类型 补充角色动画落盘与 legacy 类型映射回归测试 更新资源自由画板技术方案的类型边界说明
This commit is contained in:
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user