lili 1981a6be01
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(storage): 闭合 Agent 到 Runtime 的 OSS 游戏链路
增加不可变源码检出、两阶段 finalize/activate、Runtime 同源对象读取与 iframe 安全边界,并用 fail-closed CI 门固定契约。Git 仅提交平台实现、契约、迁移和 SoT,不包含具体游戏源码、素材、bundle 或验收证据。
2026-07-30 05:21:44 -07:00

347 lines
22 KiB
YAML
Raw Permalink 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 | 模块runtime编译 / 沙箱预览 / 试玩会话)| ownerWS3runtime 工位主笔,全员 review
# 端:/app-api产品端 game-studio用户 Token+DataPermission /admin-api管理端 game-adminRBAC前缀由 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: 版本 IDproject.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} 且校验版本 ownerscene=play 仅放行 status=1无就绪运行包返回 1-102-001-001。
parameters:
- { name: versionId, in: path, required: true, schema: { type: integer, format: int64 }, description: 版本 IDproject.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不包 CommonResultsha256(响应文本)==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-storeplay 的 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_countplay_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/首屏≤2MBT-RT-08、Manifest 生成与打包T-RT-02、资源版本化上传T-RT-03
编译为异步幂等任务:返回 buildId同一 versionId 重复触发命中进行中任务返回同一 buildId。
编译成功后回写 game_runtime_packagemanifest/包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_sizeruntime 为该三字段权威写者,与 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: '版本 IDproject.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: '整包 sha256hex宿主做 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_idint64→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: 试玩会话 IDstart 返回) }
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 }