UI 编辑器文档资产的 kind + mediaType 判据收敛到共享契约唯一实现

- packages/shared/src/contracts/gameCreationApp.ts:新增 `isGameCreationAppUiDesignDocAsset(asset)`,`kind` 严格解析后再比文档 kind、`mediaType` 比 `application/json`,这是该判据的唯一定义。
- packages/shared/src/contracts/gameCreationApp.test.ts:补用例钉住判据——只满足一半(kind 对 / mediaType 不符、mediaType 对 / kind 不是文档)与 legacy kind(`UI`、`ui`、带空格、大写)一律返回 false。
- apps/ai-game-creator-shell/src/view/project-development/index.tsx:桥接结果校验与「自动打开 ui-workflow 完成资源」两处改用共享判据,删掉本地 kind/mediaType 双条件。
- apps/ai-game-creator-shell/src/features/ui-editor/uiDesignResourceBridge.ts:`findLinkedUiDesignResource` 的过滤条件改用共享判据。
- apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx:缩略图文档分支改用共享判据(原来只看 mediaType,kind 不同也能命中)。
This commit is contained in:
2026-09-17 16:19:26 +08:00
parent 0510cd8d9c
commit 04e5512777
5 changed files with 60 additions and 14 deletions
@@ -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 <Loader2 size={18} className="animate-spin" aria-hidden="true" />;
}
if (mediaType === GAME_CREATION_APP_UI_DESIGN_DOC_MEDIA_TYPE) {
if (asset && isGameCreationAppUiDesignDocAsset(asset)) {
return <FileText size={18} aria-hidden="true" />;
}
return <ImageIcon size={18} aria-hidden="true" />;
@@ -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),
),
@@ -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 (
@@ -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(
@@ -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<GameCreationAppAssetManifestEntry, 'kind' | 'mediaType'>,
): 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 字符串表**只有这一份运行期副本**,别名表已删除。