统一引用 UI 文档资产类型常量

前端入口、资源桥接与引用预览统一使用共享常量

Rust 持久化与测试复用共享契约常量

补齐 UI 编辑器入口 fixture 的新资产类型
This commit is contained in:
2026-09-14 21:36:43 +08:00
parent 2e4a1996c8
commit b7e3dac661
10 changed files with 81 additions and 44 deletions
@@ -2031,8 +2031,8 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
let registered = register_local_asset_at(
&root,
"ui/UI 设计 1.json",
"UI",
"application/json",
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
"ui-workflow",
source(),
)
@@ -2053,8 +2053,8 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
register_local_asset_at(
&root,
"ui/UI 设计 1.json",
"UI",
"application/json",
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
"ui-workflow",
source(),
)
@@ -2063,7 +2063,10 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
let manifest: Value =
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
.expect("manifest json");
assert_eq!(manifest["assets"][0]["kind"], "UI");
assert_eq!(
manifest["assets"][0]["kind"],
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND
);
assert_eq!(manifest["assets"][0]["category"], "audio");
assert_eq!(manifest["assets"][0]["tags"], serde_json::json!(["界面"]));
@@ -2074,7 +2077,7 @@ fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
///
/// 这里的字面量与写入侧逐字一致:UI 设计资产是
/// `ui_editor/resource_bridge.rs` / `workflow.rs` / `persistence.rs` 的
/// `register_local_asset_at(root, path, "UI", "application/json", ...)`
/// `register_local_asset_at` 使用 UI 文档 kind/media 常量
/// 字体是 `commands.rs` 字体上传的 `register_local_asset_entry(root, path, "font", ...)`。
/// 只要别名表漏掉它们,真机资产就会永远停在「待归类」且读时自愈也救不回来。
#[test]
@@ -2099,8 +2102,8 @@ fn register_local_asset_derives_category_from_real_write_side_kinds() {
register_local_asset_at(
&root,
"ui/UI 设计 1.json",
"UI",
"application/json",
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
"ui-workflow",
source(),
)
@@ -2133,7 +2136,11 @@ fn register_local_asset_derives_category_from_real_write_side_kinds() {
assert_eq!(
categories,
vec![
("UI".to_string(), "ui-interaction".to_string()),
(
shared_contracts::game_creation_app::GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND
.to_string(),
"ui-interaction".to_string(),
),
("font".to_string(), "document".to_string()),
]
);
@@ -19,8 +19,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
use typed_floats::tf32::StrictlyPositiveFinite;
const UI_DESIGN_STATE_SCHEMA_VERSION: &str = "game-creator-ui-design-state.v1";
pub(crate) const UI_DESIGN_DOC_ASSET_KIND: &str = "ui-design-doc";
pub(crate) const UI_DESIGN_DOC_MEDIA_TYPE: &str = "application/json";
pub(crate) use shared_contracts::game_creation_app::{
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND as UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE as UI_DESIGN_DOC_MEDIA_TYPE,
};
const UI_DESIGN_STATE_MAX_BYTES: usize = 2 * 1024 * 1024;
const UI_DESIGN_CODE_MAX_BYTES: usize = UI_DESIGN_STATE_MAX_BYTES * 8;
const UI_DESIGN_STATE_MAX_IMAGES: usize = 4;
@@ -53,6 +53,7 @@ import { createPortal, flushSync } from 'react-dom';
import { PlatformResourceFilterBar } from '../../../../../packages/shared/src/components/PlatformResourceFilterBar';
import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
type GameCreationAppAssetManifestEntry,
gameCreationAppAssetTags,
type GameIterationVersion,
@@ -1253,7 +1254,10 @@ function ResourcePickerThumbnail({
if (mediaType.startsWith('image/')) {
return <Loader2 size={18} className="animate-spin" aria-hidden="true" />;
}
if (kind === 'ui' || mediaType === 'application/json') {
if (
kind === 'ui' ||
mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
) {
return <FileText size={18} aria-hidden="true" />;
}
return <ImageIcon size={18} aria-hidden="true" />;
@@ -2,6 +2,10 @@ import type {
GameCreationAppAssetManifestEntry,
GameCreationAppManifest,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
export type UiDesignResourceBridgeResult = {
asset: GameCreationAppAssetManifestEntry;
@@ -48,8 +52,8 @@ export function findLinkedUiDesignResource(
manifest.assets
.filter(
(asset) =>
asset.kind === 'ui-design-doc' &&
asset.mediaType === 'application/json' &&
asset.kind === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
asset.source.referenceResourceIds?.some((reference) =>
referenceIds.has(reference),
),
@@ -73,6 +73,10 @@ import type {
GameIterationVersion,
ProjectResourceCanvasLayoutMode,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { ImageCanvasCharacterAnimationPanelView } from '../../../../../src/components/image-editor/ImageCanvasCharacterAnimationPanelView';
import type {
CanvasLayer,
@@ -5040,8 +5044,8 @@ export default function ProjectDevelopmentView({
if (canvasOpenEpochRef.current !== openEpoch) return;
if (
result.manifest.projectId !== manifest.projectId ||
result.asset.kind !== 'ui-design-doc' ||
result.asset.mediaType !== 'application/json'
result.asset.kind !== GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND ||
result.asset.mediaType !== GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
) {
throw new Error('UI 编辑资源结果与当前项目不一致');
}
@@ -5112,8 +5116,8 @@ export default function ProjectDevelopmentView({
if (uiEditorRoute) return;
const completed = manifest.assets.find(
(asset) =>
asset.kind === 'ui-design-doc' &&
asset.mediaType === 'application/json' &&
asset.kind === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
asset.source.generationKind === 'ui-workflow.completed',
);
if (
@@ -6812,7 +6816,7 @@ export default function ProjectDevelopmentView({
[manifest, selectedResource],
);
const selectedResourceOpensUiEditor =
selectedResource?.subtype === 'ui-design-doc' ||
selectedResource?.subtype === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND ||
selectedResource?.subtype === 'ui-prototype';
const selectedToolbarStyle = selectedResourceLayer
@@ -4,6 +4,10 @@ import type {
ProjectResourceCanvasLayout,
ProjectResourceCanvasPosition,
} from '../../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../../packages/shared/src/contracts/gameCreationApp';
import { RESOURCE_REFERENCE_INSERT_EVENT } from '../../src/features/project-workspace/resourceReferences';
import { RESOURCE_BOOK_OVERVIEW_STACK_LIMIT } from '../../src/view/project-development/resourceBookLayout';
import {
@@ -5908,8 +5912,8 @@ export function registerProjectWorkbenchFoundationTests() {
manifest.assets = [
{
id: 'ui-design-resource',
kind: 'ui-design-doc',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'assets/ui-design.json',
source: { kind: 'generated' },
},
@@ -10,6 +10,7 @@ import {
canonicalGameCreationAppAssetKind,
GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND,
GAME_CREATION_APP_CANONICAL_ASSET_KINDS,
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
type GameCreationAppAssetCategory,
gameCreationAppAssetCategory,
gameCreationAppAssetCategoryForKind,
@@ -222,8 +223,8 @@ describe('真机出现的 kind 归类口径', () => {
// UI 文档与字体 kind,两者都必须落进明确栏目。
{ kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' },
{
kind: 'ui-design-doc',
canonical: 'ui-design-doc',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
canonical: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
category: 'ui-interaction',
},
{ kind: 'font', canonical: 'document', category: 'document' },
@@ -286,9 +287,11 @@ describe('写侧 kind 字面量 → 分类的端到端口径', () => {
for (const file of UI_EDITOR_WRITE_SITES) {
expect(readFileSync(file, 'utf8')).toContain('UI_DESIGN_DOC_ASSET_KIND');
}
expect(gameCreationAppAssetCategoryForKind('ui-design-doc')).toBe(
'ui-interaction',
);
expect(
gameCreationAppAssetCategoryForKind(
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
),
).toBe('ui-interaction');
});
test('UI 编辑器写点写出的每个 kind 都落明确栏目', () => {
@@ -4,6 +4,10 @@ import type {
GameCreationAppAssetManifestEntry,
GameCreationAppManifest,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import {
ensureUiDesignResourceForPrototype,
findLinkedUiDesignResource,
@@ -37,8 +41,8 @@ describe('ui design resource bridge', () => {
const manifest = manifestWithPrototype();
manifest.assets.push({
id: 'unrelated-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/unrelated.json',
source: { kind: 'generated', referenceResourceIds: ['other'] },
imageSequenceFrames: null,
@@ -47,8 +51,8 @@ describe('ui design resource bridge', () => {
expect(findLinkedUiDesignResource(manifest, 'prototype-asset')).toBeNull();
manifest.assets.push({
id: 'linked-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/linked.json',
source: { kind: 'generated', referenceResourceIds: ['prototype-asset'] },
imageSequenceFrames: null,
@@ -64,8 +68,8 @@ describe('ui design resource bridge', () => {
manifest.assets[0]!.source.resourceId = 'canvas-resource-1';
manifest.assets.push({
id: 'workflow-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/workflow.json',
source: {
kind: 'generated',
@@ -85,8 +89,8 @@ describe('ui design resource bridge', () => {
manifest.assets.push(
{
id: 'bridge-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/UI 设计 1.json',
source: {
kind: 'generated',
@@ -97,8 +101,8 @@ describe('ui design resource bridge', () => {
},
{
id: 'completed-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/ui-workflow-page.json',
source: {
kind: 'generated',
@@ -118,8 +122,8 @@ describe('ui design resource bridge', () => {
const manifest = manifestWithPrototype();
const linked = {
id: 'linked-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/linked.json',
source: {
kind: 'generated' as const,
@@ -146,8 +150,8 @@ describe('ui design resource bridge', () => {
const result = {
asset: {
id: 'new-ui',
kind: 'UI',
mediaType: 'application/json',
kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
localPath: 'ui/UI 设计 1.json',
source: {
kind: 'generated' as const,
@@ -549,7 +549,7 @@ const GAME_CREATION_APP_LEGACY_ASSET_KINDS: Record<
export function canonicalGameCreationAppAssetKind(
value: string,
): GameCreationAppCanonicalAssetKind {
/** kind 词汇大小写不敏感;UI Editor 文档写入侧统一使用 `ui-design-doc`。 */
/** kind 词汇大小写不敏感;UI Editor 文档写入侧使用 UI 文档常量。 */
const normalized = value.trim().toLowerCase();
/**
* `kind` 是外部输入,可能正好是 `Object.prototype` 上的键(`constructor` / `__proto__` /
@@ -596,7 +596,7 @@ export const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: Record<
'icon-spritesheet': 'ui-interaction',
'icon-spec': 'ui-interaction',
'ui-design': 'ui-interaction',
'ui-design-doc': 'ui-interaction',
[GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND]: 'ui-interaction',
'publication-material': 'unclassified',
spec: 'document',
video: 'unclassified',
@@ -5,6 +5,8 @@ pub const GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION: &str = "game-creation-app.m
pub const GAME_CREATION_AGENT_RUN_SCHEMA_VERSION: &str = "game-creator-agent-run.v1";
pub const GAME_CREATION_AGENT_RUN_MAX_PASSES: u8 = 3;
pub const GAME_CREATION_AGENT_TOOL_CALL_MAX: u16 = 128;
pub const GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND: &str = "ui-design-doc";
pub const GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE: &str = "application/json";
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
@@ -574,7 +576,7 @@ pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 17] = [
"icon-spritesheet",
"icon-spec",
"ui-design",
"ui-design-doc",
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
"publication-material",
"spec",
"video",
@@ -652,7 +654,10 @@ pub const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: [(&str, GameCreationAppAsset
),
("icon-spec", GameCreationAppAssetCategory::UiInteraction),
("ui-design", GameCreationAppAssetCategory::UiInteraction),
("ui-design-doc", GameCreationAppAssetCategory::UiInteraction),
(
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
GameCreationAppAssetCategory::UiInteraction,
),
(
"publication-material",
GameCreationAppAssetCategory::Unclassified,