From f216381619851297e861e1f19ad7f9df0da83f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 16 Sep 2026 23:10:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20UI=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=85=A5=E5=8F=A3=E5=AF=B9=20canonical=20ui-design=20?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B=E7=9A=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/shared:新增 `GAME_CREATION_APP_UI_DESIGN_ASSET_KIND`(`ui-design`)并让 canonical kind 列表与分类表引用它,避免散落裸字符串。 - project-development/index.tsx:`openResourceUiEditor` 与 `selectedResourceOpensUiEditor` 的原型判断由旧值 `ui-prototype` 改为该常量,恢复原型图的「UI 编辑器」入口。 - resourceProjectionModel.ts:`isPendingUiPrototype` 同步改用该常量,恢复待视觉验收标注。 - project-development.suite.ts:候选界面图 fixture 改用 canonical kind,并补一条原型图打开 UI 编辑器桥接的回归用例(去掉修正即转红)。 --- .../src/view/project-development/index.tsx | 5 +- .../resourceProjectionModel.ts | 3 +- .../appSurface/project-development.suite.ts | 82 ++++++++++++++++++- .../shared/src/contracts/gameCreationApp.ts | 7 +- 4 files changed, 91 insertions(+), 6 deletions(-) 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',