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 }