diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
index 2b25d8d4c..76c357582 100644
--- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
+++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx
@@ -53,10 +53,10 @@ 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,
+ isGameCreationAppUiDesignDocAsset,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { resolveTauriInvoke } from '../../app/tauri';
import {
@@ -1311,7 +1311,7 @@ function ResourcePickerThumbnail({
if (mediaType.startsWith('image/')) {
return ;
}
- if (mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE) {
+ if (asset && isGameCreationAppUiDesignDocAsset(asset)) {
return ;
}
return ;
diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignResourceBridge.ts b/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignResourceBridge.ts
index 9d341d98f..39168801f 100644
--- a/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignResourceBridge.ts
+++ b/apps/ai-game-creator-shell/src/features/ui-editor/uiDesignResourceBridge.ts
@@ -4,8 +4,7 @@ import type {
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
GAME_CREATION_APP_UI_DESIGN_ASSET_KIND,
- GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
- GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
+ isGameCreationAppUiDesignDocAsset,
parseGameCreationAppAssetKind,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
@@ -54,11 +53,7 @@ export function findLinkedUiDesignResource(
manifest.assets
.filter(
(asset) =>
- parseGameCreationAppAssetKind(
- asset.kind,
- 'ui-design-resource-bridge.linked-document',
- ) === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
- asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
+ isGameCreationAppUiDesignDocAsset(asset) &&
asset.source.referenceResourceIds?.some((reference) =>
referenceIds.has(reference),
),
diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
index f8e6a334a..c89245da8 100644
--- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx
+++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx
@@ -83,10 +83,10 @@ import type {
import {
GAME_CREATION_APP_UI_DESIGN_ASSET_KIND,
GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
- GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
isGameCreationAppAssetKind,
+ isGameCreationAppUiDesignDocAsset,
parseGameCreationAppAssetKind,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { ImageCanvasCharacterAnimationPanelView } from '../../../../../src/components/image-editor/ImageCanvasCharacterAnimationPanelView';
@@ -5262,8 +5262,7 @@ export default function ProjectDevelopmentView({
if (canvasOpenEpochRef.current !== openEpoch) return;
if (
result.manifest.projectId !== manifest.projectId ||
- result.asset.kind !== GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND ||
- result.asset.mediaType !== GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
+ !isGameCreationAppUiDesignDocAsset(result.asset)
) {
throw new Error('UI 编辑资源结果与当前项目不一致');
}
@@ -5400,8 +5399,7 @@ export default function ProjectDevelopmentView({
if (uiEditorRoute) return;
const completed = manifest.assets.find(
(asset) =>
- asset.kind === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
- asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE &&
+ isGameCreationAppUiDesignDocAsset(asset) &&
asset.source.generationKind === 'ui-workflow.completed',
);
if (
diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts
index 7323aace9..d214cb871 100644
--- a/packages/shared/src/contracts/gameCreationApp.test.ts
+++ b/packages/shared/src/contracts/gameCreationApp.test.ts
@@ -16,6 +16,8 @@ import {
GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION,
+ GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
+ GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
type GameCreationAgentRunTrace,
gameCreationAppAssetCategory,
gameCreationAppAssetCategoryForKind,
@@ -23,6 +25,7 @@ import {
gameCreationAppAssetPersistedCategory,
gameCreationAppAssetTags,
type GameCreationAppManifest,
+ isGameCreationAppUiDesignDocAsset,
normalizeGameCreationAppAssetCategory,
normalizeGameCreationAppAssetTags,
parseGameCreationAppAssetKind,
@@ -795,6 +798,39 @@ describe('AI 游戏创作 App 共享契约', () => {
}
});
+ it('识别 UI 编辑器文档资产只认 kind + mediaType 的唯一定义', () => {
+ expect(
+ isGameCreationAppUiDesignDocAsset({
+ kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
+ mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
+ }),
+ ).toBe(true);
+
+ // 只满足一半不算文档:kind 对但 mediaType 不符、mediaType 对但 kind 不是文档。
+ expect(
+ isGameCreationAppUiDesignDocAsset({
+ kind: GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND,
+ mediaType: 'text/plain',
+ }),
+ ).toBe(false);
+ expect(
+ isGameCreationAppUiDesignDocAsset({
+ kind: 'ui-design',
+ mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
+ }),
+ ).toBe(false);
+
+ // legacy / 非 canonical kind 一律严格收口,不被当作文档(判据只做等值匹配)。
+ for (const raw of ['UI', 'ui', 'ui-design-doc ', 'UI-DESIGN-DOC']) {
+ expect(
+ isGameCreationAppUiDesignDocAsset({
+ kind: raw,
+ mediaType: GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE,
+ }),
+ ).toBe(false);
+ }
+ });
+
it('maps every asset kind to a functional category', () => {
expect(GAME_CREATION_APP_ASSET_CATEGORIES).toHaveLength(6);
expect(Object.keys(GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND)).toHaveLength(
diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts
index c6c8c21f6..3981089de 100644
--- a/packages/shared/src/contracts/gameCreationApp.ts
+++ b/packages/shared/src/contracts/gameCreationApp.ts
@@ -499,6 +499,23 @@ export const GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND: GameCreationAppAssetKin
export const GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE =
'application/json' as const;
+/**
+ * 该 manifest 资产是否是 UI 编辑器文档:`kind` + `mediaType` 双条件是唯一定义。
+ *
+ * 判据原本散在三处(资源画布入口、UI 编辑器桥接、资源引用入口),任一处改了 kind /
+ * mediaType 口径就会出现「能引用但打不开」这类分叉,所以只保留这一份实现。
+ * `kind` 走严格解析:认不出的值收口成 `unknown`,不等于文档 kind。
+ */
+export function isGameCreationAppUiDesignDocAsset(
+ asset: Pick,
+): boolean {
+ return (
+ parseGameCreationAppAssetKind(asset.kind, 'ui-design-doc.asset') ===
+ GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND &&
+ asset.mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE
+ );
+}
+
/**
* 运行期 kind 列表:穷举 Record 保证它不可能与生成 union 分叉(漏一个/多一个都编译报错)。
* 键顺序即生成声明顺序。kind 字符串表**只有这一份运行期副本**,别名表已删除。