lili 847089b541 feat(m3a): 便宜档 Node 退役机制 U1-U4 本机交付(Python §6.1 worker + 后端路由契约/A2A/幂等)
M3a 退役机制 plan 经 Codex+Opus 双评审(needs-rework)→ 按创始人 A(全局 dispatcher
flag 整体翻转、不做 per-request 灰度)重写 → Opus 复审 minor-fixes → 发现项全数修复。

U1 Python §6.1 HTTP worker(cheap-worker/worker_service.py + result_out.py):
  202 投递握手 + 有界串行队列(治串行 worker 忙 409 误判)+ 三字段 result-out
  (gameConfig 占位 / engineBundle 承重含 __GameBundle / sourceProject best-effort 2.0)
  + HMAC 回调。27 单测 + 真生成 e2e 过九门 + 真网络回调。

U2 跨语言契约 slice test(CheapWorkerResultOutContractTest):反射调真
  validateSucceededPayload 过薄校验 + 真 CallbackSignatureVerifier HMAC。

U3 A2A 状态(additive):AigcTaskStatusEnum.toA2aStatus + app VO/RPC DTO a2aStatus
  字段(保留数字 status 不破坏调用方)+ Convert/ApiImpl backfill + 契约 aigc.yaml。

U4 创作请求级幂等(行业标准 idempotency):DO idempotencyKey + Flyway V26
  (列+唯一索引,NULL 多值=不去重)+ Mapper selectByIdempotency + submitGenerate dedup。

真验证抓到并修 2 个真 bug:① worker 回调走系统代理被 fake-ip 拦 502 → 禁代理 opener;
② result-out 顶层 costRmb 不在 DifyCallbackReqVO 字段内、后端严格拒未知 → 落 trace.cost。

验证:Python 27 测 + Java 21 测全绿(mvn BUILD SUCCESS)。
剩余 U2 真派发 / U4 真 DB / U5 待 mini-desktop 真部署。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:18:28 -07:00

297 lines
18 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

openapi: 3.0.3
# 契约 #1 API | 模块:aigc(game-module-aigc,"做得出"生成入口)| owner:WS2 主笔,全员 review
# 职责:生成入口——异步提交生成任务(prompt+templateId)+ 轮询状态;产物=GamePackage(契约#4),落到 project 的 game_version(经 game_id 关联);对接 Dify(契约#6) 仅留对接点。
# 端:/app-api(产品端 game-studio,用户 Token+DataPermission,创作者只见自己任务) /admin-api(管理端 game-admin,RBAC);前缀由 huijing 框架按 controller.app/admin 包名自动添加。
# 错误码段:aigc = 1-101-***-***(本模块独占,禁止与他模块重叠)
# 响应统一 Huijing CommonResult 信封:{ code, data, msg };code=0 成功
# 范围纪律:只做 MVP 核心闭环(异步提交+轮询+取消+重试+模板列表);LLM 实名充值为人工闸门,Dify/MQ/LLM 只定义对接点不实现。
info:
title: 绘境AI aigc 模块 API
version: 1.0.0
description: >-
AI 生成("做得出"入口)。一句话 Prompt + 模板 → 异步生成任务 → 轮询状态 → 产物 GamePackage(契约#4)。
任务状态机对齐 Dify(契约#6) 输出 + RocketMQ 队列态(queued/running/succeeded/failed/timed_out/canceled)。
成功产物经 game_id 关联落到 project.game_version;前端据此 vite-plugin-mock 自动生成 mock。
servers:
- url: http://localhost:48080
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
paths:
# ===================== 产品端 /app-api(创作者)=====================
/app-api/aigc/template/list:
get:
tags: [app-aigc]
summary: 玩法模板列表(P-TPL-01;提交生成前选模板,MVP 3-5 个 P0 模板)
description: 返回 aigc 模板注册表中启用的模板(T-AGC-03)。templateId 取值如 dodge/runner/clicker/puzzle/shooter(最终由产品拍板)。
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultTemplateList' }
/app-api/aigc/generate:
post:
tags: [app-aigc]
summary: 提交生成任务(P-CRT-01 自然语言生成;异步入队,立即返回 taskId)
description: >-
校验 prompt 非空 + templateId 存在;落库 game_aigc_task(status=0 queued),生成 traceId,投递 RocketMQ 异步队列(T-AGC-07,仅对接点)。
不同步等待生成结果;客户端据 taskId 轮询 GET /app-api/aigc/task/{id}。
关联的 gameId/versionId 由调用方(studio 草稿)传入。
写权属(决策5):aigc 成功后只回填 game_version.id 关联 + 生成元数据(gen_task_id);不写 game_version 的 package_url/checksum/bundle_size——这些的权威写者=runtime 编译成功后回写(见 runtime V3.0.0 / runtime.yaml)。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AigcGenerateReqVO' }
responses:
'200':
description: 返回新建生成任务(含 taskId + 初始状态 queued + traceId)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultGenerateResp' }
/app-api/aigc/task/{id}:
get:
tags: [app-aigc]
summary: 轮询生成任务状态(P-CRT-12/13;前端轮询直至终态)
description: >-
返回任务当前状态机 + 进度。终态 succeeded 携带产物引用(gameId/versionId + qualityScore;packageUrl 由 runtime 编译成功后回写,aigc 仅透传只读);
终态 failed/timed_out 携带 failureReason(结构化错误分类,映射用户可读提示)。DataPermission:创作者只见自己任务。
注:qualityScore=生成质量分(0-1),对齐契约#6 Dify output;≠ telemetry/feed 的运营质量分(0-100),两者口径不同、不可互相回灌。
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: 生成任务 ID(taskId) }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultTask' }
/app-api/aigc/task/{id}/cancel:
post:
tags: [app-aigc]
summary: 取消生成任务(仅 queued/running 可取消 → canceled)
description: 服务端校验状态机,仅非终态可取消;终态拒绝(错误码 1-101-002-001)。
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: 生成任务 ID }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
/app-api/aigc/task/{id}/retry:
post:
tags: [app-aigc]
summary: 重试生成任务(P-CRT-09 重生成;仅 failed/timed_out 可重试,新建任务)
description: >-
基于原任务 prompt+templateId 新建一条 game_aigc_task 重新入队(retry_of 指向原任务),返回新 taskId。
仅终态失败可重试;成功/进行中拒绝(错误码 1-101-002-002)。
parameters:
- { name: id, in: path, required: true, schema: { type: integer, format: int64 }, description: 原生成任务 ID }
responses:
'200':
description: 返回新建重试任务
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultGenerateResp' }
/app-api/aigc/task/my:
get:
tags: [app-aigc]
summary: 我的生成任务列表(创作者只见自己数据)
parameters:
- { name: status, in: query, required: false, schema: { type: integer }, description: 按状态机筛选(见 status 枚举) }
- { name: gameId, in: query, required: false, schema: { type: integer, format: int64 }, description: 按所属游戏筛选 }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultTaskPage' }
# ===================== 管理端 /admin-api(运营/排障)=====================
/admin-api/aigc/task/page:
get:
tags: [admin-aigc]
summary: 生成任务全量分页(管理端 RBAC,排障/健康监控 T-TEL-14)
description: 跨创作者查看生成任务,支持按状态/模板/traceId 检索,用于生成成功率统计与失败排障。
parameters:
- { name: status, in: query, required: false, schema: { type: integer }, description: 按状态机筛选 }
- { name: templateId, in: query, required: false, schema: { type: string }, description: 按模板筛选 }
- { name: traceId, in: query, required: false, schema: { type: string }, description: 按全链路 traceId 精确检索 }
- { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } }
- { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } }
responses:
'200':
description: 成功
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultTaskPage' }
# ===================== Dify 回调对接点(仅契约,不实现)=====================
/admin-api/aigc/dify/callback:
post:
tags: [admin-aigc]
summary: '[对接点] Dify workflow 完成回调(契约#6 output → 驱动任务状态机;不在 MVP 实现)'
description: >-
【外部依赖对接点,仅定义契约,不实现、不 mock】Dify 工作流(6 节点)完成后回调本端点,
以契约#6 output 形态(status/traceId/gameConfig/assets/qualityScore/failureReason)驱动 game_aigc_task 状态机:
succeeded → 只回填 game_version.id 关联 + 生成元数据(gen_task_id)并置 status=2,不写 game_version 的 package_url/checksum/bundle_size(权威写者=runtime 编译成功后回写,见 runtime V3.0.0 / runtime.yaml);failed → 落 failure_reason 置 status=3。
端鉴权:内网/服务间调用,MVP 阶段经 admin-api 受 RBAC 保护;正式实现需加签名校验。LLM 实名充值为人工闸门,本端点不触发计费。
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/DifyCallbackReqVO' }
responses:
'200':
description: 回调受理(幂等:同一 traceId 重复回调只生效一次)
content:
application/json:
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
components:
schemas:
# ---- Huijing CommonResult 信封 ----
CommonResultBoolean:
type: object
properties:
code: { type: integer, description: '0=成功,非0=错误码 1-101-***-***' }
data: { type: boolean }
msg: { type: string }
CommonResultGenerateResp:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/GenerateRespVO' }
msg: { type: string }
CommonResultTask:
type: object
properties:
code: { type: integer }
data: { $ref: '#/components/schemas/AigcTaskRespVO' }
msg: { type: string }
CommonResultTaskPage:
type: object
properties:
code: { type: integer }
data:
type: object
properties:
list: { type: array, items: { $ref: '#/components/schemas/AigcTaskRespVO' } }
total: { type: integer, format: int64 }
msg: { type: string }
CommonResultTemplateList:
type: object
properties:
code: { type: integer }
data: { type: array, items: { $ref: '#/components/schemas/TemplateRespVO' } }
msg: { type: string }
# ---- 请求 VO ----
AigcGenerateReqVO:
type: object
required: [prompt]
properties:
prompt: { type: string, minLength: 1, maxLength: 1000, description: 创作者一句话自然语言 Prompt(→ 契约#6 input.prompt) }
templateId: { type: string, description: '玩法模板 ID(→ 契约#6 input.templateId;可选,缺省=generic:W-CLEAN 废玩法模板层后,一句话创作无需选模板,入口将缺省 templateId 归一为 generic 单一通用口径,品类区分留 prompt 层;前端亦可显式从模板列表选)' }
gameId: { type: integer, format: int64, description: '关联游戏 ID(project.game_project.id)。产物 GamePackage 经此落 game_version;studio 草稿场景传入' }
versionId: { type: integer, format: int64, description: '关联版本 ID(project.game_version.id)。指定则产物写入该版本;为空时由生成流程新建版本' }
attachments:
type: array
description: 附件驱动创作上下文(图片/参考,可空;→ 契约#6 input.attachments)
items:
type: object
properties:
type: { type: string, description: 附件类型 }
url: { type: string, format: uri, description: 附件 URL }
DifyCallbackReqVO:
type: object
description: '[对接点] 形态对齐契约#6 dify-workflow-io.json 的 output。仅契约,不实现。'
required: [traceId, status]
properties:
traceId: { type: string, description: 贯穿全链路 traceId(与 game_aigc_task.trace_id 匹配定位任务) }
status: { type: string, enum: [succeeded, failed], description: 'Dify 生成结果状态(→ 驱动任务状态机 2/3)' }
templateId: { type: string, description: 最终匹配/使用的模板 ID }
gameConfig: { type: object, description: '生成的可玩配置(注入 GamePackage.gameConfig)' }
assets:
type: array
description: 生成资源引用(合入 GamePackage.assets)
items:
type: object
properties:
type: { type: string }
url: { type: string, format: uri }
qualityScore: { type: number, minimum: 0, maximum: 1, description: '生成质量分(0-1),对齐契约#6 Dify output 0-1,质量门禁参考;≠ telemetry/feed 的运营质量分(0-100),两者不可互相回灌' }
failureReason: { $ref: '#/components/schemas/FailureReason' }
# ↓↓ P3 生成主线 additive 字段(Java DifyCallbackReqVO 已加,此处补 yaml 漂移对齐;契约先行收口)↓↓
engineBundle:
type: string
nullable: true
description: >-
引擎包路 bundle 文本(P3 additive;worker 产 __GameBundle iife bundle 全文,
→ DifyCallbackTxService.resolveEngineBundleText 取值写入 GamePackage.engineBundle 落包)。
与 GamePackage.engineBundle(契约#4 落库侧产物)分清:本字段是回调入参侧载体。
存量 M-b 回调不带本字段时为 null,组包链省略该键、字节零变化。
# ↓↓ W-G1 组B 新增(additive 可选):9d 富生成轨迹账本 ↓↓
trace:
type: object
nullable: true
description: >-
9d 生成轨迹账本(W-G1 组B,additive 可选):worker 已产出的富生成数据
(guards 九门逐门/cost/models/attempts/gatespec/verdict/repairs/wall/similarity)。
字段全可选(worker 路径未定,按必填子集统计完整率);后端 best-effort 落
game_aigc_task.trace_json,落库失败不阻断主回调链。存量回调不带本字段时为 null,trace_json 列 NULL,字节零变化。
# ---- 响应 VO ----
GenerateRespVO:
type: object
description: 提交/重试生成任务的返回(异步,立即返回任务句柄)
properties:
taskId: { type: integer, format: int64, description: 生成任务 ID(轮询用) }
status: { type: integer, description: '初始状态:0 queued', example: 0 }
traceId: { type: string, description: 贯穿全链路 traceId(透传给前端/日志关联) }
AigcTaskRespVO:
type: object
description: 生成任务详情(轮询返回)。终态 succeeded 带产物引用;失败带 failureReason。
properties:
id: { type: integer, format: int64, description: 生成任务 ID(taskId) }
prompt: { type: string, description: 创作者 Prompt }
templateId: { type: string, description: 模板 ID }
gameId: { type: integer, format: int64, description: '关联游戏 ID(project.game_project.id)' }
versionId: { type: integer, format: int64, description: '产物落地的版本 ID(project.game_version.id);succeeded 后回填' }
status: { type: integer, description: '状态机:0 queued 排队 / 1 running 生成中 / 2 succeeded 成功 / 3 failed 失败 / 4 timed_out 超时 / 5 canceled 已取消' }
a2aStatus: { type: string, enum: [submitted, working, input-required, completed, failed, canceled], description: 'A2A 语义状态(M3a U3 additive):由 status 映射(queued→submitted / running→working / succeeded→completed / failed,timed_out→failed / canceled→canceled);前端可选消费、底层换实现不感知;input-required 为协议占位(内部六态无对应、M3a 不可触发,交互待切片三 A11)。保留上方 integer status 不破坏既有调用方。' }
progress: { type: integer, minimum: 0, maximum: 100, description: 进度百分比(0-100,前端进度条 P-CRT-12) }
qualityScore: { type: number, minimum: 0, maximum: 1, description: '生成质量分(0-1),对齐契约#6 Dify output 0-1(succeeded 后回填);≠ telemetry/feed 的运营质量分(0-100),两者不可互相回灌' }
packageUrl: { type: string, description: '产物 GamePackage(manifest) OSS URL(= game_version.package_url,只读透传)。aigc 不写此字段,权威写者=runtime 编译成功后回写(见 runtime V3.0.0 / runtime.yaml)' }
failureReason: { $ref: '#/components/schemas/FailureReason' }
retryOf: { type: integer, format: int64, description: 若本任务为重试,指向原任务 ID }
traceId: { type: string, description: 全链路 traceId }
createTime: { type: string, format: date-time, description: 创建时间 }
finishTime: { type: string, format: date-time, description: 终态完成时间 }
TemplateRespVO:
type: object
description: 玩法模板(aigc 模板注册表 T-AGC-03)
properties:
templateId: { type: string, description: '模板 ID(如 dodge/runner/clicker/puzzle/shooter)' }
name: { type: string, description: 模板名称 }
description: { type: string, description: 模板说明 }
coverUrl: { type: string, description: 模板封面/示意图 URL }
examplePrompt: { type: string, description: 示例 Prompt(P-CRT-11 示例 Prompt 库) }
# ---- 共享枚举:失败原因分类(T-AGC-10;取值对齐契约#6 output.failureReason)----
FailureReason:
type: string
description: >-
生成失败原因分类(结构化错误码,映射用户可读提示)。取值与契约#6 dify-workflow-io.json output.failureReason 一致。
unsafe_prompt=Prompt 不安全 / intent_unclear=意图不清 / no_template_match=无匹配模板 /
config_invalid=配置校验失败 / llm_error=LLM 异常 / timeout=超时 / asset_gen_failed=素材生成失败。
enum: [unsafe_prompt, intent_unclear, no_template_match, config_invalid, llm_error, timeout, asset_gen_failed]