增加不可变源码检出、两阶段 finalize/activate、Runtime 同源对象读取与 iframe 安全边界,并用 fail-closed CI 门固定契约。Git 仅提交平台实现、契约、迁移和 SoT,不包含具体游戏源码、素材、bundle 或验收证据。
347 lines
22 KiB
YAML
347 lines
22 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(已发布);status=2(已失效/下架)继续返回未发布错误 1-102-001-002。
|
||
无对应就绪运行包返回 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),
|
||
任何包裹/重序列化都会破坏字节一致性导致校验失败。
|
||
存储介质由服务端 read-mode=db|shadow|oss 切换;浏览器始终访问本同源端点,不向对象域发送平台 Token。
|
||
oss 模式只返回 V34 committed locator 指向且通过原文/bundle 摘要校验的对象,任何失败不回退 DB。
|
||
取包门禁与 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/package/{versionId}/assets/{sha256}:
|
||
get:
|
||
tags: [app-runtime]
|
||
summary: 按内容摘要读取版本素材
|
||
description: >
|
||
scene/status/preview owner 门与取包端点同源。服务端只从 V34 committed artifact manifest 中按 sha256
|
||
映射精确对象 key,逐字节复算 bytes/hash 后返回;客户端不能提交 bucket、key 或 URL。
|
||
parameters:
|
||
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 ID }
|
||
- { name: sha256, in: path, required: true, schema: { type: string, pattern: '^[a-f0-9]{64}$' }, description: 'GamePackage.assets[].hash' }
|
||
- { name: scene, in: query, required: false, schema: { type: string, enum: [preview, play], default: play }, description: 取包场景 }
|
||
responses:
|
||
'200':
|
||
description: >
|
||
素材原始字节;Content-Type/Length/ETag 均来自已验清单和真实对象。
|
||
preview 返回 Cache-Control: private,no-store;play 的 version+sha256 内容寻址素材返回 public,max-age=31536000,immutable。
|
||
content:
|
||
application/octet-stream:
|
||
schema: { type: string, format: binary }
|
||
|
||
/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: >
|
||
非狗粮环境供管理端运营发布;dogfood=true 时该 HTTP 入口固定拒绝并返回 1-102-001-005,
|
||
必须由 project 审核通过后经 RuntimePackageApi 本地编排发布,防止绕过审核绑定。
|
||
作用:仅置 game_runtime_package.status=1(已发布,玩家可试玩);project.game_version.status=3 由 project 编排负责。
|
||
幂等:运行包已是 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)' }
|
||
- { name: expectedArtifactHash, in: query, required: true, schema: { type: string, pattern: '^[0-9a-f]{64}$' }, description: '审核绑定的运行产物 SHA-256;与运行包 checksum 不一致时拒发' }
|
||
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)。
|
||
【取包约束】① 宿主只从受信 API origin 的同源 manifestUrl 读取 manifest,按 checksum 做 sha256 完整性校验通过后再注入运行容器(T-RT-15);
|
||
② DB/shadow/oss 的对象读取由 Runtime 服务端完成,浏览器不直连对象域、不向对象域发送平台 Token;
|
||
③ 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: '历史兼容字段;浏览器不得据此直连对象域,现行取包统一使用 manifestUrl' }
|
||
manifestUrl: { type: string, description: '固定指向 GET /app-api/runtime/package/{versionId}/manifest 同源端点;Runtime 服务端按 db/shadow/oss 模式读取原文,宿主校验响应原始 sha256 后注入运行容器' }
|
||
assetUrlTemplate: { type: string, description: 'OSS 主读模式下返回的对象素材同源代理模板;宿主把 {sha256} 替换为 GamePackage.assets[].hash 后带平台鉴权读取。DB/shadow 不返回' }
|
||
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 属性与 postMessage origin 白名单(T-RT-14)。
|
||
manifest 与对象素材均由受信 Runtime API 同源代理,本字段不承担对象存储 CORS 配置。
|
||
properties:
|
||
sandboxAttr: { type: string, description: "iframe sandbox 属性值(现行固定为 'allow-scripts')" }
|
||
allowOrigins: { type: array, items: { type: string }, description: 'postMessage 允许的 origin 白名单(宿主侧来源窗口/origin/schema 三校验)' }
|
||
|
||
# ---- 试玩会话开/收(脊柱: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 }
|