From 9300ae2d6d4416e65f53d05938a9bb49b1a3e1ca Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 17 Jun 2026 15:31:32 -0700 Subject: [PATCH] =?UTF-8?q?feat(studio):=20E6/U6=20=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E6=94=B9=E4=B8=80=E6=AD=A5=E5=BC=8F=E2=80=94=E2=80=94?= =?UTF-8?q?Create/Task/store=20=E6=8E=A5=20/studio/*=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把创作流从旧两步(project.create + aigc.generate)改为一步式 studio.create(plan 002 U6): - store/create.ts:重写为 studio 任务链编排——loadTemplates/create/modify/extend/pollTask(getTaskChain)/regenerate;终态集 {2完成,3失败}(studio 链无 4/5);currentTask→currentChain - views/create/Create.vue:onSubmit 一次调 createStore.create({title,prompt,templateId}) 取任务链(含 gameId),去 projectStore 两步;模板类型 AigcTemplate→StudioTemplate - views/create/Task.vue:轮询 studio 任务链(status 0初始/1生成中/2完成/3失败);去取消(studio 契约无 cancel 端点)、去 failureReason 逐项映射(链无该字段,通用兜底);retry→regenerate;成功→预览(versionId+gameId) - 预览侧 GamePlayer 零改(已支持 versionId/gameId) 验证:`npm run build`(vue-tsc -b + vite build)绿;消费方仅 Create/Task(已同步)。运行态 create→poll→preview 全链 CDP 真机走查待 modify 入口落地后一并做。 Co-Authored-By: Claude Opus 4.8 --- game-studio/src/store/create.ts | 142 ++++++++++++++---------- game-studio/src/views/create/Create.vue | 53 ++++----- game-studio/src/views/create/Task.vue | 141 +++++++---------------- 3 files changed, 144 insertions(+), 192 deletions(-) diff --git a/game-studio/src/store/create.ts b/game-studio/src/store/create.ts index 48eb97be..9f873e38 100644 --- a/game-studio/src/store/create.ts +++ b/game-studio/src/store/create.ts @@ -1,25 +1,39 @@ /** - * useCreateStore —— 一句话生成(模板 + 异步任务 + 进度轮询)。 - * 对应 spec §3.2:state templates/currentTask/progress;actions loadTemplates/generate/pollTask/cancel/retry。 - * 异步生成走「提交→轮询 getTask→终态」,轮询计时器在终态/取消/卸载时务必清理(防泄漏)。 + * useCreateStore —— 一步式创作(studio.yaml /studio/*:模板 + 异步任务链 + 进度轮询)。 + * 对应 plan 002 U6 / execution §5.6:state templates/currentChain/progress; + * actions loadTemplates/create/modify/extend/pollTask/regenerate/reset。 + * + * 创作链路(改源不改产物,产新预览版): + * create → 一步式(draft+generate 封装),返回任务链; + * modify → 改源(deterministic 覆写 / regenerate-module 重生成单 behavior),产新预览版; + * extend → additive 内容扩展; + * 三者均返 StudioTaskChain,前端轮询 task/{id} 至终态(status=2 完成取 versionId / 3 失败)。 + * 轮询计时器在终态/卸载时务必清理(防泄漏)。studio 契约无「取消」端点,失败经 regenerate 重试。 */ import { defineStore } from 'pinia' import { ref } from 'vue' -import * as aigcApi from '../api/aigc' -import type { AigcTemplate, AigcTask, AigcTaskStatus } from '../api/types' +import * as studioApi from '../api/studio' +import type { + StudioTemplate, + StudioTaskChain, + StudioTaskChainStatus, + StudioCreateReq, + StudioModifyReq, + StudioExtendReq, +} from '../api/types' -/** 终态集合:succeeded(2)/failed(3)/timed_out(4)/canceled(5) → 停止轮询 */ -const TERMINAL: ReadonlySet = new Set([2, 3, 4, 5]) +/** 终态集合:完成(2)/失败(3) → 停止轮询(studio 任务链无 4/5) */ +const TERMINAL: ReadonlySet = new Set([2, 3]) -/** 轮询间隔(毫秒)。mock 进度递增节奏与之匹配(见 mock/aigc)。 */ +/** 轮询间隔(毫秒)。mock 进度递增节奏与之匹配(见 mock/studio)。 */ const POLL_INTERVAL = 1200 export const useCreateStore = defineStore('create', () => { - /** 玩法模板列表 */ - const templates = ref([]) - /** 当前生成任务(轮询对象) */ - const currentTask = ref(null) - /** 进度百分比(0-100,进度条用,冗余自 currentTask.progress 便于直接绑定) */ + /** 玩法模板列表(透传 aigc 模板;一句话直生可不选) */ + const templates = ref([]) + /** 当前创作任务链(轮询对象) */ + const currentChain = ref(null) + /** 进度百分比(0-100,进度条用,冗余自 currentChain.progress 便于直接绑定) */ const progress = ref(0) /** 是否轮询中 */ const polling = ref(false) @@ -38,18 +52,18 @@ export const useCreateStore = defineStore('create', () => { /** 加载玩法模板列表 */ async function loadTemplates() { - templates.value = await aigcApi.getTemplates() + templates.value = await studioApi.getTemplates() } /** - * 拉取一次任务状态并更新 state;命中终态自动停轮询。 - * @param taskId 任务 ID + * 拉取一次任务链状态并更新 state;命中终态自动停轮询。 + * @param id 任务链 ID */ - async function fetchOnce(taskId: number) { - const task = await aigcApi.getTask(taskId) - currentTask.value = task - progress.value = task.progress - if (TERMINAL.has(task.status)) { + async function fetchOnce(id: number) { + const chain = await studioApi.getTaskChain(id) + currentChain.value = chain + progress.value = chain.progress + if (TERMINAL.has(chain.status)) { stopPolling() } } @@ -57,81 +71,87 @@ export const useCreateStore = defineStore('create', () => { /** * 启动轮询(提交后或进入进度页时调用)。 * 先立刻拉一次(避免首屏空白),再定时轮询直至终态。 - * @param taskId 任务 ID + * @param id 任务链 ID */ - async function pollTask(taskId: number) { + async function pollTask(id: number) { // 切换任务前先清理旧轮询 stopPolling() polling.value = true - await fetchOnce(taskId) + await fetchOnce(id) // 已是终态则无需起定时器 if (!polling.value) return timer = setInterval(() => { // 轮询内异常已被 axios 拦截器 Toast;这里吞掉 reject 防止未捕获 - void fetchOnce(taskId).catch(() => {}) + void fetchOnce(id).catch(() => {}) }, POLL_INTERVAL) } /** - * 提交生成任务并自动开始轮询,返回 taskId(供路由跳转 /create/task/:taskId)。 - * @param prompt 一句话 Prompt - * @param templateId 玩法模板 ID(可选,缺省 generic:W-CLEAN 废玩法模板层后由后端归一) - * @param extra 关联草稿信息(gameId/versionId) + * 一步式创作(draft+generate 封装),自动开始轮询,返回任务链(含 id/gameId)。 + * @param req StudioCreateReq(title/prompt 必填,templateId 缺省 generic,assetContext 六类素材) */ - async function generate( - prompt: string, - templateId?: string, - extra?: { gameId?: number; versionId?: number }, - ): Promise { - const resp = await aigcApi.generate(prompt, templateId, extra) - progress.value = 0 - // 不阻塞返回 taskId:轮询在后台进行 - void pollTask(resp.taskId) - return resp.taskId + async function create(req: StudioCreateReq): Promise { + const chain = await studioApi.createGame(req) + currentChain.value = chain + progress.value = chain.progress + void pollTask(chain.id) + return chain } /** - * 取消当前/指定任务(仅 queued/running 可取消)。成功后停轮询。 - * @param taskId 任务 ID + * 改源(产新预览版,不动 currentVersion),自动轮询,返回任务链。 + * @param req StudioModifyReq(baseVersionId + mode + target + payload) */ - async function cancel(taskId: number): Promise { - const ok = await aigcApi.cancelTask(taskId) - if (ok) { - stopPolling() - // 立即刷新一次拿到 canceled 终态 - await fetchOnce(taskId).catch(() => {}) - } - return ok + async function modify(req: StudioModifyReq): Promise { + const chain = await studioApi.modifyGame(req) + currentChain.value = chain + progress.value = chain.progress + void pollTask(chain.id) + return chain } /** - * 重试任务(仅 failed/timed_out 可重试,新建任务),返回新 taskId 并自动轮询。 - * @param taskId 原任务 ID + * 内容扩展(additive),自动轮询,返回任务链。 + * @param req StudioExtendReq(baseVersionId + intent) */ - async function retry(taskId: number): Promise { - const resp = await aigcApi.retryTask(taskId) - progress.value = 0 - void pollTask(resp.taskId) - return resp.taskId + async function extend(req: StudioExtendReq): Promise { + const chain = await studioApi.extendGame(req) + currentChain.value = chain + progress.value = chain.progress + void pollTask(chain.id) + return chain + } + + /** + * 重生成(失败/不满意时重试,新建任务链,retryOf 指原链),返回新任务链并自动轮询。 + * @param id 原任务链 ID + */ + async function regenerate(id: number): Promise { + const chain = await studioApi.regenerate(id) + currentChain.value = chain + progress.value = chain.progress + void pollTask(chain.id) + return chain } /** 重置(离开创作流时调用,清理状态与计时器) */ function reset() { stopPolling() - currentTask.value = null + currentChain.value = null progress.value = 0 } return { templates, - currentTask, + currentChain, progress, polling, loadTemplates, - generate, + create, + modify, + extend, pollTask, - cancel, - retry, + regenerate, reset, stopPolling, } diff --git a/game-studio/src/views/create/Create.vue b/game-studio/src/views/create/Create.vue index 9955d3e7..d79676cf 100644 --- a/game-studio/src/views/create/Create.vue +++ b/game-studio/src/views/create/Create.vue @@ -3,23 +3,20 @@ * 创作入口页(路由 /create,spec §3.1 / §4.2 创作者闭环第 1 步) * ---------------------------------------------------------------------------- * 职责: - * 1. 拉取玩法模板列表(aigc.getTemplates,契约 GET /app-api/aigc/template/list,MVP 3-5 个)。 + * 1. 拉取玩法模板列表(studio.getTemplates,契约 GET /app-api/studio/template/list,MVP 3-5 个)。 * 2. 选模板 + 一句话自然语言描述输入 + 示例 Prompt 提示(点击示例可一键填入)。 - * 3. 提交生成:先建草稿项目(project.create,契约 POST /app-api/project)拿 projectId, - * 再投递生成任务(create.generate → 契约 POST /app-api/aigc/generate)拿 taskId, - * 最后跳进度页 /create/task/:taskId(携带 gameId 便于终态后续链路)。 + * 3. 一步式创作:create.create → 契约 POST /app-api/studio/create(draft+generate 封装,一次调用拿任务链), + * 最后跳进度页 /create/task/:taskChainId(携带任务链返回的 gameId 便于终态后续链路)。 * * 契约纪律: - * - 仅消费 useCreateStore / useProjectStore(地基 store),不直连后端、不发明端点。 - * - 生成请求体严格贴 AigcGenerateReqVO:prompt + templateId(+ gameId 关联草稿)。 - * - 创建草稿请求体严格贴 ProjectCreateReqVO:title + templateId(均必填)。 + * - 仅消费 useCreateStore(地基 store),不直连后端、不发明端点。 + * - 创作请求体严格贴 StudioCreateReqVO:title + prompt 必填(templateId 缺省 generic,assetContext 六类素材)。 */ import { ref, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' import { useCreateStore } from '../../store/create' -import { useProjectStore } from '../../store/project' -import type { AigcTemplate } from '../../api/types' +import type { StudioTemplate } from '../../api/types' import { LoadingBar, EmptyState, AppButton } from '../../components' import Icon from '../../components/Icon.vue' @@ -28,7 +25,6 @@ const { t } = useI18n() const router = useRouter() const createStore = useCreateStore() -const projectStore = useProjectStore() /** 当前选中的模板 ID(默认空,需用户主动选) */ const selectedTemplateId = ref('') @@ -38,14 +34,14 @@ const prompt = ref('') const loadingTpl = ref(false) /** 模板加载失败标记(用于空态重试) */ const tplError = ref(false) -/** 提交中(建草稿 + 投递生成期间禁用按钮,防重复提交) */ +/** 提交中(一步式创作期间禁用按钮,防重复提交) */ const submitting = ref(false) -/** Prompt 最大长度(贴契约 AigcGenerateReqVO.prompt maxLength=1000) */ +/** Prompt 最大长度(贴契约 StudioCreateReqVO.prompt maxLength=1000) */ const PROMPT_MAX = 1000 /** 当前选中的模板对象(用于取示例 Prompt / 作为草稿标题兜底) */ -const selectedTemplate = computed(() => +const selectedTemplate = computed(() => createStore.templates.find((t) => t.templateId === selectedTemplateId.value), ) @@ -97,34 +93,29 @@ function goFeed() { } /** - * 提交生成(创作者闭环核心动作)。 - * 步骤:建草稿项目 → 拿 projectId 作为 gameId → 投递生成任务 → 跳进度页。 - * 失败路径:任一步异常 → 错误码 Toast(拦截器)+ 解除提交锁,停留本页可重试。 + * 一步式创作(创作者闭环核心动作)。 + * 步骤:studio.create(draft+generate 封装)→ 拿任务链(id/gameId) → 跳进度页。 + * 失败路径:异常 → 错误码 Toast(拦截器)+ 解除提交锁,停留本页可重试。 */ async function onSubmit() { if (!canSubmit.value) return submitting.value = true try { - // generic 桥接(W-CLEAN 废玩法模板层后无需选模板):缺省 templateId 传 'generic'(后端亦会归一), + // generic 桥接(一句话直生无需选模板):缺省 templateId 传 'generic'(后端亦归一), // 兼容用户未选模板(template/list 返空、空态)与显式选中两种情况。 const templateId = selectedTemplateId.value || 'generic' - - // 1) 建草稿项目(title 取 Prompt 前 30 字兜底) - // 契约 ProjectCreateReqVO required:[title](templateId 可选,缺省 generic) + // title 取 Prompt 前 30 字兜底(契约 StudioCreateReqVO.title 必填 ≤128)。 const title = prompt.value.trim().slice(0, 30) || selectedTemplate.value?.name || '未命名作品' - const projectId = await projectStore.create({ - title, - templateId, - }) - // 2) 投递生成任务,关联草稿(gameId=projectId);store 内部已自动开始轮询。 - // 契约 AigcGenerateReqVO:prompt(+ gameId 关联草稿;templateId 可选,缺省 generic)。 - const taskId = await createStore.generate(prompt.value.trim(), templateId, { - gameId: projectId, - }) + // 一步式创作:POST /app-api/studio/create → 任务链(含 id/gameId);store 内部已自动开始轮询。 + const chain = await createStore.create({ title, prompt: prompt.value.trim(), templateId }) - // 3) 跳进度页(携带 gameId,供终态后预览/发布链路使用) - router.push({ name: 'create-task', params: { taskId: String(taskId) }, query: { gameId: String(projectId) } }) + // 跳进度页(携带任务链返回的 gameId,供终态后预览/发布链路使用)。 + router.push({ + name: 'create-task', + params: { taskId: String(chain.id) }, + query: chain.gameId != null ? { gameId: String(chain.gameId) } : {}, + }) } catch { // 错误已由拦截器 Toast;解除锁,停留本页 submitting.value = false diff --git a/game-studio/src/views/create/Task.vue b/game-studio/src/views/create/Task.vue index a9312e8d..23e55470 100644 --- a/game-studio/src/views/create/Task.vue +++ b/game-studio/src/views/create/Task.vue @@ -1,25 +1,23 @@