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 7dcc9cf14..4b1476b40 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 @@ -81,6 +81,7 @@ import type { ProjectResourceCanvasLayoutMode, } 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, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; @@ -5300,7 +5301,7 @@ export default function ProjectDevelopmentView({ const openResourceUiEditor = useCallback( (resource: ProjectResource) => { - if (resource.subtype === 'ui-prototype') { + if (resource.subtype === GAME_CREATION_APP_UI_DESIGN_ASSET_KIND) { void openUiDesignEditor(resource); return; } @@ -7471,7 +7472,7 @@ export default function ProjectDevelopmentView({ ); const selectedResourceOpensUiEditor = selectedResource?.subtype === GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND || - selectedResource?.subtype === 'ui-prototype'; + selectedResource?.subtype === GAME_CREATION_APP_UI_DESIGN_ASSET_KIND; const selectedToolbarStyle = selectedResourceLayer ? resolveSelectedToolbarStyle({ diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts index 998c5b9f5..23f1cfb72 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts @@ -1,4 +1,5 @@ import { + GAME_CREATION_APP_UI_DESIGN_ASSET_KIND, type GameCreationAppAssetCategory, gameCreationAppAssetCategory, gameCreationAppAssetTags, @@ -323,7 +324,7 @@ export function projectResourcesFromReadModels( } const assetCategory = gameCreationAppAssetCategory(asset); const isPendingUiPrototype = - asset.kind === 'ui-prototype' && + asset.kind === GAME_CREATION_APP_UI_DESIGN_ASSET_KIND && taskById.get('design-foundation')?.status !== 'completed'; resources.push({ id: `asset:${asset.id}`, diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 1ceec12f2..d5c99fbcd 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -6,6 +6,7 @@ import type { ProjectResourceCanvasPosition, } 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, } from '../../../../packages/shared/src/contracts/gameCreationApp'; @@ -6463,7 +6464,7 @@ export function registerProjectWorkbenchFoundationTests() { ); manifest.assets.push({ id: 'ui-prototype-candidate', - kind: 'ui-prototype', + kind: GAME_CREATION_APP_UI_DESIGN_ASSET_KIND, mediaType: 'image/png', localPath: 'assets/ui-prototype.png', source: { @@ -6513,6 +6514,85 @@ export function registerProjectWorkbenchFoundationTests() { ).not.toBeNull(); }); + it('opens the UI editor bridge from a canonical ui-design prototype image', async () => { + installResizeObserverStub(); + const manifest = createGameCreationAppManifest( + 'workbench-ui-prototype-entry', + 'UI 原型入口测试', + ); + manifest.assets = [ + { + id: 'ui-prototype-asset', + kind: GAME_CREATION_APP_UI_DESIGN_ASSET_KIND, + mediaType: 'image/png', + localPath: 'assets/ui-prototype.png', + source: { kind: 'canvas', taskId: 'design-foundation' }, + }, + ]; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return resourceGraphForInputs(args); + } + if (command === 'read_local_project_resource_canvas_layout') { + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: manifest.projectId, + mode: args?.mode, + revision: 0, + positions: [], + updatedAt: 0, + }; + } + if (command === 'update_local_project_resource_canvas_layout') { + return { + status: 'updated', + layout: { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: manifest.projectId, + mode: args?.mode, + revision: 1, + positions: args?.positions, + updatedAt: 1, + }, + }; + } + if (command === 'ensure_ui_design_resource_for_prototype') { + return null; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-ui-prototype-entry', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + await openResourceBookCategory('UI 交互'); + fireEvent.click( + await findResourceSelectButton('ui-prototype.png(待视觉验收)'), + ); + fireEvent.click(await screen.findByRole('button', { name: 'UI 编辑器' })); + expect(invoke).toHaveBeenCalledWith( + 'ensure_ui_design_resource_for_prototype', + expect.objectContaining({ + input: expect.objectContaining({ + prototypeAssetId: 'ui-prototype-asset', + }), + }), + ); + }); + it('refuses to embed a non-loopback game preview in the client workbench', () => { const manifest = createGameCreationAppManifest( 'workbench-remote-preview', diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index 65370e4be..735940906 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -480,6 +480,9 @@ export interface GameCreationAppAssetManifestEntry { tags?: string[]; } +/** UI 设计图(原型图与页面设计图)资产的唯一 kind。 */ +export const GAME_CREATION_APP_UI_DESIGN_ASSET_KIND = 'ui-design' as const; + /** UI Editor JSON 文档资产的唯一 kind 与媒体类型。 */ export const GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND = 'ui-design-doc' as const; @@ -494,7 +497,7 @@ export const GAME_CREATION_APP_CANONICAL_ASSET_KINDS = [ 'icon', 'icon-spritesheet', 'icon-spec', - 'ui-design', + GAME_CREATION_APP_UI_DESIGN_ASSET_KIND, GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND, 'publication-material', 'spec', @@ -595,7 +598,7 @@ export const GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND: Record< icon: 'ui-interaction', 'icon-spritesheet': 'ui-interaction', 'icon-spec': 'ui-interaction', - 'ui-design': 'ui-interaction', + [GAME_CREATION_APP_UI_DESIGN_ASSET_KIND]: 'ui-interaction', [GAME_CREATION_APP_UI_DESIGN_DOC_ASSET_KIND]: 'ui-interaction', 'publication-material': 'unclassified', spec: 'document',