- 范围:全仓除已完成的 game-cloud/game-admin(及排除区);91 文件 - cn.iocoder.yudao→com.wanxiang.huijing / cn.wanxiang.game→com.wanxiang.huijing.game / cn.iocoder.cloud→com.wanxiang / Yudao*→Huijing* / bare yudao→huijing - 含 contracts/api-schemas(apiInterface FQCN)、docs/architecture+agent-specs(含中文名,git ls-files -z 修复)、AGENTS.md/CLAUDE.md 等根文档 - 保留不动:上游归属 URL(gitee/github yudaocode·YunaiV,改则断链+违反署名)、game-cloud Flyway 迁移注释(保 checksum)、lockfile(装包重生) - 纯文档/字符串,零构建影响(game-cloud/game-admin 字节码未动) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
324 lines
21 KiB
YAML
324 lines
21 KiB
YAML
openapi: 3.0.3
|
||
# 契约 #1 API | 模块:runtime(编译 / 沙箱预览 / 试玩会话)| owner:WS3(runtime 工位主笔,全员 review)
|
||
# 端:/app-api(产品端 game-studio,用户 Token+DataPermission) /admin-api(管理端 game-admin,RBAC);前缀由 huijing 框架按 controller.app/admin 包名自动添加
|
||
# 错误码段:runtime = 1-102-***-***(本模块独占,禁止与他模块重叠)
|
||
# 响应统一 Huijing CommonResult 信封:{ code, data, msg };code=0 成功
|
||
#
|
||
# 脊柱对接(闭环贯通关键):
|
||
# - 上游输入:GamePackage(契约 #4 game-package.schema.json)—— 由 aigc/studio 生成、project 持久化版本元数据;runtime 编译产出版本化运行包。
|
||
# - 对外提供:版本运行包清单(manifest URL + 入口 + checksum + 预加载策略)给「试玩宿主」(game-studio iframe 宿主)。
|
||
# - 宿主↔游戏边界:postMessage 协议(契约 #3 sdk-interface.d.ts)由宿主与游戏侧落地;runtime 不实现 postMessage,只为宿主提供加载清单与会话开/收端点。
|
||
# - 试玩会话:game_runtime_session 记录 play_start/play_end/时长,供回灌 telemetry(契约 #5 events.schema.json,由 telemetry 模块消费)。
|
||
#
|
||
# MVP 范围纪律:只做核心闭环所需端点 —— 编译产出运行包 → 预览/试玩取包清单 → 试玩会话开/收 → 会话回灌。
|
||
# 渠道转换(微信/抖音/快手/TapTap 试玩包,T-RT-21~24/31/32)、云游戏容器(T-RT-30)属"扩"远期,不进 MVP,仅在 §对接点 留契约挂点。
|
||
info:
|
||
title: 绘境AI runtime 模块 API
|
||
version: 1.0.0
|
||
description: GameConfig→版本化可运行包编译、iframe 沙箱预览/试玩、运行包清单分发、试玩会话开收(回灌 telemetry)。前端据此 vite-plugin-mock 自动生成 mock。
|
||
|
||
servers:
|
||
- url: http://localhost:48080
|
||
description: 本地(Swagger/Knife4j http://localhost:48080/doc.html)
|
||
|
||
paths:
|
||
# ==========================================================================
|
||
# 产品端 /app-api —— 预览/试玩宿主取包清单 + 试玩会话开收(用户 Token)
|
||
# ==========================================================================
|
||
/app-api/runtime/package/{versionId}:
|
||
get:
|
||
tags: [app-runtime]
|
||
summary: 取版本运行包清单(预览/试玩宿主据此渲染 iframe、桥接 SDK ←#3)
|
||
description: >
|
||
宿主据返回的 manifest(入口/包URL/checksum/预加载策略)加载游戏并做完整性校验(门禁 Manifest 完整性 T-RT-15)。
|
||
取包门禁判定字段 = game_runtime_package.status(运行包状态,非 project.game_version.status):
|
||
scene=preview 放行 status∈{0 预览就绪, 1 已发布},并校验调用者为版本 owner(创作者本人预览未发布版本);
|
||
scene=play 仅放行 status=1(已发布)。无对应就绪运行包返回 1-102-001-001。
|
||
parameters:
|
||
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 ID(project.game_version.id) }
|
||
- { name: scene, in: query, required: false, schema: { type: string, enum: [preview, play], default: play }, description: 'preview=创作者预览未发布版本 / play=玩家试玩已发布版本' }
|
||
responses:
|
||
'200':
|
||
description: 成功
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultRuntimePackage' }
|
||
|
||
/app-api/runtime/package/{versionId}/manifest:
|
||
get:
|
||
tags: [app-runtime]
|
||
summary: 取版本运行包 manifest 原始 JSON(宿主 fetch 此端点取 manifest 并做 sha256 完整性校验)
|
||
description: >
|
||
【§3.4 C4 关键约束】返回 manifest 的原始 JSON 文本,**不包 CommonResult、不 parse/re-serialize、不加换行**——
|
||
宿主对该响应文本原始字节算 sha256,须与 RuntimePackageRespVO.checksum 严格一致(PackageFactory 写入时按相同字节计算并存 game_runtime_package.checksum),
|
||
任何包裹/重序列化都会破坏字节一致性导致校验失败。
|
||
MVP 无 OSS:manifest 整包存 game_runtime_package.package_json,由 PackageStore 的 DB impl 读出原样返回(M3 接 OSS 后换 OSS impl,调用方/契约不变,退场契约)。
|
||
取包门禁与 GET /app-api/runtime/package/{versionId} 同源:scene=preview 放行 status∈{0,1} 且校验版本 owner;scene=play 仅放行 status=1;无就绪运行包返回 1-102-001-001。
|
||
parameters:
|
||
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 ID(project.game_version.id) }
|
||
- { name: scene, in: query, required: false, schema: { type: string, enum: [preview, play], default: play }, description: 'preview=创作者预览未发布版本 / play=玩家试玩已发布版本' }
|
||
responses:
|
||
'200':
|
||
description: manifest 原始 JSON 文本(application/json,不包 CommonResult;sha256(响应文本)==checksum)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: string
|
||
description: 'GamePackage.manifest 的原始 JSON 字符串(原样字节,宿主据此算 sha256 校验后注入运行容器,对齐 #4 manifest 段)'
|
||
|
||
/app-api/runtime/session/start:
|
||
post:
|
||
tags: [app-runtime]
|
||
summary: 开始试玩会话(即点即玩 P-PLZ-03 / 预览试玩 P-CRT-08)
|
||
description: >
|
||
宿主在游戏 game_start(生命周期 ←#3)时调用,落 game_runtime_session 一行并返回 sessionId。
|
||
sessionId 作为本局唯一键,贯穿后续 end 与 telemetry 回灌的关联键。
|
||
clientPlayToken 幂等:同一 token 重复提交返回同一 sessionId(防抖/重连不重复计数)。
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/SessionStartReqVO' }
|
||
responses:
|
||
'200':
|
||
description: 返回会话 ID
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultSessionStart' }
|
||
|
||
/app-api/runtime/session/end:
|
||
post:
|
||
tags: [app-runtime]
|
||
summary: 结束试玩会话(仅记录时长/质量观测,不计 play_count)
|
||
description: >
|
||
宿主在游戏 game_end 或容器卸载(sendBeacon 兜底)时调用,写 play_end 与 duration_ms,置会话状态=1已结束。
|
||
服务端以 sessionId 为准计算时长(end-start),客户端上报 durationMs 仅作离线兜底。
|
||
幂等:会话已结束再次提交直接返回成功,不二次累加。
|
||
注:runtime session 仅作时长/质量观测,不计 play_count;play_count 权威源 = telemetry 的 game_play_start 事件聚合
|
||
(见 telemetry V5.0.0 / telemetry.yaml)。runtime sessionId(int64) 透传进 telemetry envelope.session_id(转 string)作对账键。
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/SessionEndReqVO' }
|
||
responses:
|
||
'200':
|
||
description: 成功
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
|
||
|
||
# ==========================================================================
|
||
# 管理端 /admin-api —— 编译编排 + 编译状态 + 会话/质量观测(管理 RBAC)
|
||
# 注:编译由 project 统一发布编排(T-PRJ-08)或运营触发;此处暴露管理侧入口与查询。
|
||
# ==========================================================================
|
||
/admin-api/runtime/compile:
|
||
post:
|
||
tags: [admin-runtime]
|
||
summary: 触发编译(GameConfig→版本化运行包,消费 aigc 产物 ←#4)
|
||
description: >
|
||
以 versionId 为入参,读取该版本的 GamePackage(#4),执行 GameConfig 静态校验(门禁 T-RT-16)、
|
||
体积门禁(≤10MB/首屏≤2MB,T-RT-08)、Manifest 生成与打包(T-RT-02)、资源版本化上传(T-RT-03)。
|
||
编译为异步幂等任务:返回 buildId;同一 versionId 重复触发命中进行中任务返回同一 buildId。
|
||
编译成功后回写 game_runtime_package(manifest/包URL/checksum),并置 game_runtime_package.status=0(预览就绪);
|
||
发布态(status=1 已发布 + 回写 project.game_version.status=3)不在此处写,统一由 publish 对接点
|
||
POST /admin-api/runtime/package/{versionId}/publish 在发布编排成功后回写。
|
||
同时按「决策5 写权属」回写 project.game_version 的 package_url/checksum/bundle_size(runtime 为该三字段权威写者,与 game_runtime_package 同源)。
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CompileReqVO' }
|
||
responses:
|
||
'200':
|
||
description: 返回编译任务 ID
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultCompile' }
|
||
|
||
/admin-api/runtime/build/{buildId}:
|
||
get:
|
||
tags: [admin-runtime]
|
||
summary: 查询编译任务状态(发布编排/运营轮询)
|
||
parameters:
|
||
- { name: buildId, in: path, required: true, schema: { type: integer, format: int64 }, description: 编译任务 ID }
|
||
responses:
|
||
'200':
|
||
description: 成功
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultBuild' }
|
||
|
||
/admin-api/runtime/package/{versionId}/publish:
|
||
post:
|
||
tags: [admin-runtime]
|
||
summary: 发布态回写对接点(编译就绪运行包 → 置为已发布,补全发布态写入链路)
|
||
description: >
|
||
由 project.publish 统一发布编排(T-PRJ-08)成功后调用(admin-api,RBAC / 内部编排调用,非用户直接触发)。
|
||
作用:置 game_runtime_package.status=1(已发布,玩家可试玩),并回写 project.game_version.status=3(已发布),
|
||
补全「编译就绪 → 统一发布编排 → 发布态写入」链路的最后一跳,消除原先发布态无写入对接点的断点。
|
||
幂等:运行包已是 status=1 时重复调用直接返回成功,不二次写入。
|
||
前置:该 versionId 必须已编译成功且存在就绪运行包(status=0 预览就绪);否则返回 1-102-001-001。
|
||
parameters:
|
||
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: '版本 ID(project.game_version.id);其余入参由路径参数即可承载,无需请求体' }
|
||
responses:
|
||
'200':
|
||
description: 成功(运行包已置已发布、版本状态已回写)
|
||
content:
|
||
application/json:
|
||
schema: { $ref: '#/components/schemas/CommonResultBoolean' }
|
||
|
||
/admin-api/runtime/session/page:
|
||
get:
|
||
tags: [admin-runtime]
|
||
summary: 试玩会话列表(运行时质量观测 / 回灌核对)
|
||
parameters:
|
||
- { name: gameId, in: query, required: false, schema: { type: integer, format: int64 }, description: 按游戏筛选 }
|
||
- { name: versionId, in: query, required: false, schema: { type: integer, format: int64 }, description: 按版本筛选 }
|
||
- { name: status, in: query, required: false, schema: { type: integer }, description: '会话状态:0进行中 1已结束 2异常' }
|
||
- { 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/CommonResultSessionPage' }
|
||
|
||
components:
|
||
schemas:
|
||
# ---- Huijing CommonResult 信封 ----
|
||
CommonResultBoolean:
|
||
type: object
|
||
properties:
|
||
code: { type: integer, description: '0=成功,非0=错误码 1-102-***-***' }
|
||
data: { type: boolean }
|
||
msg: { type: string }
|
||
CommonResultRuntimePackage:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/RuntimePackageRespVO' }
|
||
msg: { type: string }
|
||
CommonResultSessionStart:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/SessionStartRespVO' }
|
||
msg: { type: string }
|
||
CommonResultCompile:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/CompileRespVO' }
|
||
msg: { type: string }
|
||
CommonResultBuild:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data: { $ref: '#/components/schemas/BuildRespVO' }
|
||
msg: { type: string }
|
||
CommonResultSessionPage:
|
||
type: object
|
||
properties:
|
||
code: { type: integer }
|
||
data:
|
||
type: object
|
||
properties:
|
||
list: { type: array, items: { $ref: '#/components/schemas/SessionRespVO' } }
|
||
total: { type: integer, format: int64 }
|
||
msg: { type: string }
|
||
|
||
# ---- 运行包清单(脊柱对外产物:宿主据此加载 + 校验,对齐 #4 manifest 段)----
|
||
RuntimePackageRespVO:
|
||
type: object
|
||
description: >-
|
||
版本运行包清单。字段语义对齐 GamePackage(#4).manifest,宿主据此渲染 iframe 并桥接 SDK(#3)。
|
||
【GAP-2 取包约束】① 宿主先 fetch manifestUrl 取 manifest,按 checksum 做 sha256 完整性校验通过后再注入运行容器(T-RT-15),校验失败拒绝加载并落 error;
|
||
② OSS/CDN 须放行宿主 origin 的 CORS(AllowedOrigin=宿主 origin、AllowedMethod=GET/HEAD、ExposeHeader=ETag),否则宿主跨域 fetch 被拦;
|
||
③ scene=preview|play 鉴权与取包门禁须一致:preview 仅版本 owner 可取未发布包、play 仅放行已发布包(与 getPackageManifest 门禁同源,不在前端兜底)。
|
||
properties:
|
||
gameId: { type: integer, format: int64, description: 游戏 ID }
|
||
versionId: { type: integer, format: int64, description: 版本 ID }
|
||
templateId: { type: string, description: 玩法模板 ID(宿主据此选 Runtime 容器) }
|
||
packageUrl: { type: string, description: 'GamePackage(manifest)OSS/CDN URL,按 /games/{gameId}/versions/{versionId}/ 版本化(#4)' }
|
||
manifestUrl: { type: string, description: '§3.4 C4:指向 GET /app-api/runtime/package/{versionId}/manifest 端点(MVP 包存 DB,端点原样服务 manifest JSON)。宿主先 fetch 此端点取 manifest,对响应原始文本算 sha256 与 checksum 严格比对通过后再注入运行容器(相对 URL 须 resolve 到 API base,见 §2.5);M3 接 OSS 后改指向 OSS/CDN 版本化 URL(退场契约)' }
|
||
entry: { type: string, description: 入口文件相对路径(#4 manifest.entry) }
|
||
runtimeVersion: { type: string, description: '目标 WanxiangGameSDK / Canvas Runtime 版本(semver,#4 manifest.runtimeVersion)' }
|
||
preloadPolicy: { type: string, enum: [eager, lazy], description: '预加载策略(#4 manifest.preloadPolicy)' }
|
||
bundleSize: { type: integer, format: int64, description: 包总字节(#4 manifest.bundleSize) }
|
||
checksum: { type: string, description: '整包 sha256(hex),宿主做 Manifest 完整性校验 T-RT-15(#4 manifest.checksum)' }
|
||
sandbox: { $ref: '#/components/schemas/SandboxPolicyVO' }
|
||
traceId: { type: string, description: '贯穿 生成→编译→加载→运行 的 trace_id(#3 init / #4 provenance)' }
|
||
SandboxPolicyVO:
|
||
type: object
|
||
description: >-
|
||
iframe 沙箱隔离策略(T-RT-04),宿主据此设置 iframe sandbox 属性与 CSP、postMessage origin 白名单(T-RT-14)。
|
||
【GAP-2 CORS 约束】allowOrigins 同时是 OSS/CDN 取包(manifestUrl/packageUrl)须放行的跨域来源:
|
||
OSS 桶 CORS 规则 AllowedOrigin 应与本白名单一致(含宿主 origin),AllowedMethod=GET/HEAD、ExposeHeader=ETag,否则宿主跨域 fetch manifest 被拦。
|
||
properties:
|
||
sandboxAttr: { type: string, description: "iframe sandbox 属性值(如 'allow-scripts allow-same-origin')" }
|
||
allowOrigins: { type: array, items: { type: string }, description: 'postMessage 允许的 origin 白名单(宿主侧 #3 双校验);亦为 OSS/CDN 取包 CORS 须放行的宿主 origin' }
|
||
|
||
# ---- 试玩会话开/收(脊柱:play_start/end/时长 → 回灌 telemetry #5)----
|
||
SessionStartReqVO:
|
||
type: object
|
||
required: [gameId, versionId, clientPlayToken]
|
||
properties:
|
||
gameId: { type: integer, format: int64, description: 游戏 ID }
|
||
versionId: { type: integer, format: int64, description: 版本 ID }
|
||
clientPlayToken: { type: string, maxLength: 64, description: 客户端本局唯一 token(幂等键,防抖/重连不重复计数) }
|
||
scene: { type: string, enum: [preview, play], default: play, description: 'preview=创作者预览 / play=玩家试玩' }
|
||
traceId: { type: string, description: '贯穿链路 trace_id(透传至 telemetry 回灌)' }
|
||
SessionStartRespVO:
|
||
type: object
|
||
properties:
|
||
sessionId: { type: integer, format: int64, description: '试玩会话 ID(本局唯一键,贯穿 end 与 telemetry 回灌关联)。透传进 telemetry envelope.session_id(int64→string)作对账键;session 仅观测时长,play_count 权威源=telemetry game_play_start(见 telemetry V5.0.0 / telemetry.yaml)' }
|
||
startTime: { type: string, format: date-time, description: 服务端记录的开局时间 }
|
||
SessionEndReqVO:
|
||
type: object
|
||
required: [sessionId]
|
||
properties:
|
||
sessionId: { type: integer, format: int64, description: 试玩会话 ID(start 返回) }
|
||
durationMs: { type: integer, format: int64, description: 客户端上报时长毫秒(离线兜底;服务端以 end-start 为准) }
|
||
endReason: { type: string, enum: [game_end, quit, error, timeout], description: '结束原因(game_end 正常结束 / quit 主动退出 / error 运行错误 / timeout 加载或心跳超时)' }
|
||
|
||
# ---- 编译编排(消费 #4 产物,产出运行包;外部 OSS/打包为对接点不实现)----
|
||
CompileReqVO:
|
||
type: object
|
||
required: [versionId]
|
||
properties:
|
||
versionId: { type: integer, format: int64, description: '待编译版本 ID(读取该版本 GamePackage #4)' }
|
||
force: { type: boolean, default: false, description: 是否强制重编(忽略已就绪缓存) }
|
||
CompileRespVO:
|
||
type: object
|
||
properties:
|
||
buildId: { type: integer, format: int64, description: '编译任务 ID(异步幂等,轮询 /admin-api/runtime/build/{buildId})' }
|
||
reused: { type: boolean, description: 是否命中进行中/已就绪任务复用(幂等结果) }
|
||
BuildRespVO:
|
||
type: object
|
||
properties:
|
||
buildId: { type: integer, format: int64 }
|
||
versionId: { type: integer, format: int64 }
|
||
status: { type: integer, description: '编译状态机:0排队 1编译中 2成功 3失败' }
|
||
packageUrl: { type: string, description: 成功后产出包 URL(失败为空) }
|
||
checksum: { type: string, description: 成功后整包 sha256 }
|
||
bundleSize: { type: integer, format: int64, description: 包总字节(体积门禁 T-RT-08 校验值) }
|
||
failCode: { type: string, description: '失败错误码(如 1-102-002-002 体积超限 / 1-102-002-001 GameConfig 校验失败),成功为空' }
|
||
failReason: { type: string, description: 失败原因(回填运营,成功为空) }
|
||
finishTime: { type: string, format: date-time, description: 完成时间 }
|
||
|
||
# ---- 会话观测 VO ----
|
||
SessionRespVO:
|
||
type: object
|
||
properties:
|
||
id: { type: integer, format: int64, description: 会话 ID }
|
||
gameId: { type: integer, format: int64 }
|
||
versionId: { type: integer, format: int64 }
|
||
playerUserId: { type: integer, format: int64, description: 试玩玩家用户 ID }
|
||
scene: { type: string, enum: [preview, play] }
|
||
status: { type: integer, description: '会话状态:0进行中 1已结束 2异常' }
|
||
durationMs: { type: integer, format: int64, description: 时长毫秒(服务端 end-start) }
|
||
startTime: { type: string, format: date-time }
|
||
endTime: { type: string, format: date-time }
|