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 92b472c9a..6ccbb4fa4 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 @@ -95,6 +95,15 @@ import { resolveResourceCanvasPanelEntries, resolveSelectedPanelEntries, } from '../../features/resource-canvas/resourceCanvasAssetTransferModel'; +import { + isResourceCanvasGenerationAvailable, + resourceCanvasGenerationOption, + resourceCanvasGenerationSourceId, +} from '../../features/resource-canvas/resourceCanvasGenerationModel'; +import { + ResourceCanvasGenerationPanelView, + type ResourceCanvasGenerationSubmitInput, +} from '../../features/resource-canvas/ResourceCanvasGenerationPanelView'; import { canRedoResourceCanvasHistory, canUndoResourceCanvasHistory, @@ -1247,6 +1256,7 @@ export default function ProjectDevelopmentView({ createResourceCanvasHistory, ); const [resourcePanelOpen, setResourcePanelOpen] = useState(false); + const [resourceGenerationOpen, setResourceGenerationOpen] = useState(false); const [resourcePanelNotice, setResourcePanelNotice] = useState(''); const [resourcePanelUploading, setResourcePanelUploading] = useState(false); const [uiEditorRoute, setUiEditorRoute] = useState( @@ -5065,6 +5075,86 @@ export default function ProjectDevelopmentView({ quickEditSourceLayer, ]); + /** + * 生成入口:从资源画布无源新建视频 / 音效 / 背景音乐。 + * + * 产出物是一张全新的资源卡(`generationMode: 'create'`),源素材、画布布局与 + * 已有 manifest 条目都不参与派生;成功后用 `pendingResourceFocusRef` 定位新卡。 + */ + const submitResourceCanvasGeneration = useCallback( + async (input: ResourceCanvasGenerationSubmitInput) => { + const invoke = window.__TAURI__?.core?.invoke; + if (!invoke) { + throw new Error('生成资源需要在客户端内执行'); + } + const option = resourceCanvasGenerationOption(input.kind); + const actionProject = { projectPath, projectId: manifest.projectId }; + const flowId = crypto.randomUUID(); + const status = await invoke<{ revision: number }>( + 'get_local_game_project_revision', + { projectPath }, + ); + if (!Number.isSafeInteger(status.revision) || status.revision < 0) { + throw new Error('项目 revision 无效'); + } + const result = await withPlatformSessionRefresh(() => + invoke( + 'derive_local_project_resource', + { + input: { + projectPath, + expectedProjectId: actionProject.projectId, + expectedProjectRevision: status.revision, + operationId: input.operationId, + idempotencyKey: input.idempotencyKey, + editKind: option.editKind, + generationMode: 'create', + sourceResourceId: resourceCanvasGenerationSourceId( + input.operationId, + ), + sourceAssetId: null, + sourcePath: null, + sourceMediaType: option.sourceMediaType, + sourceSubtype: null, + producerTaskId: null, + sourceVersionId: null, + prompt: input.prompt, + assetName: input.assetName, + }, + }, + ), + ); + if ( + !result.asset || + result.manifest.projectId !== actionProject.projectId + ) { + throw new Error('生成资源结果与当前项目不一致'); + } + onManifestChange?.(projectPath, result.manifest, { + projectId: result.manifest.projectId, + revision: result.committedProjectRevision, + source: 'asset-command', + commitId: result.operationId, + }); + activeFocusFlowIdRef.current = flowId; + pendingResourceFocusRef.current = { + flowId, + saveAttemptId: result.operationId, + sessionId: result.operationId, + draftId: result.operationId, + commitId: result.operationId, + projectPath, + projectId: result.manifest.projectId, + focusGeneration: focusGenerationRef.current, + resourceId: `asset:${result.asset.id}`, + completed: false, + }; + setResourceWorkbenchNotice('生成资源已保存,正在同步资源与布局…'); + setResourceGenerationOpen(false); + }, + [manifest.projectId, onManifestChange, projectPath], + ); + const selectedResourceCardPreview = selectedResourcePreviewIdentity ? (resourceCardPreviews.previews.get(selectedResourcePreviewIdentity) ?? null) @@ -5127,6 +5217,12 @@ export default function ProjectDevelopmentView({ canvasSize: resourceBookSceneSize, }) : null; + // 没有客户端 invoke 桥或项目还没就绪时不渲染生成入口,避免留下点了没反应的按钮。 + const resourceGenerationAvailable = isResourceCanvasGenerationAvailable({ + hasRuntimeInvoke: Boolean(window.__TAURI__?.core?.invoke), + projectPath, + projectId: manifest.projectId, + }); return (