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

99 lines
4.4 KiB
TypeScript
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.

/**
* runtime 模块 mock handler(严格贴 contracts/api-schemas/runtime.yaml)。
* 覆盖端点:
* GET /app-api/runtime/package/:versionId → RuntimePackageRespVO
* POST /app-api/runtime/session/start → SessionStartRespVO
* POST /app-api/runtime/session/end → boolean
*
* 游戏包清单:字段语义对齐 GamePackage(#4).manifest;entry 约定为 'demo'(与 A2 Runtime demo 对齐),
* sandbox.allowOrigins 给本地白名单(宿主 bridge 三校验时放行同源)。
* 会话:clientPlayToken 幂等(同 token 返回同 sessionId)。
*/
import type { MockRoute, MockRequest } from './shared'
import { MockError, fakeSha256, nowISO } from './shared'
import type { RuntimePackage, SessionStartResp, SessionStartReq } from '../api/types'
/** demo 版本 ID(与 feed/aigc 对齐:全链路指向同一 demo 包) */
const DEMO_VERSION_ID = 9001
/** clientPlayToken → sessionId 幂等表 */
const tokenToSession = new Map<string, number>()
/** 已结束会话集合(幂等:重复 end 直接成功) */
const endedSessions = new Set<number>()
let sessionSeq = 50000
/** GET /app-api/runtime/package/:versionId?scene */
function packageHandler(req: MockRequest): RuntimePackage {
const versionId = Number(req.params.versionId)
if (!versionId) throw new MockError(1102001001, '无对应就绪运行包')
// gameId 在真实后端由 version 关联得到;mock 给一个稳定派生值
const gameId = versionId === DEMO_VERSION_ID ? 1 : versionId
// 整包字节:贴 §8 AC7 Runtime <15KB 语境,这里给一个合理 demo 包体积
const bundleSize = 14_336
return {
gameId,
versionId,
// 模板:clicker(与 demo 点击计数玩法一致)
templateId: 'clicker',
packageUrl: `/games/${gameId}/versions/${versionId}/manifest.json`,
// GAP-2:mock 刻意「不」回填 manifestUrl。
// 宿主 GamePlayer.resolvePackage 对「无 manifestUrl」走 demo 兜底(assets 为空的 clicker),
// 保证本地 mock 链路无需真实 OSS 即可跑通;而 manifestUrl 的 fetch+sha256 校验路径在
// 隔离 staging 联调(Runtime 回填同源 manifestUrl 并代理 OSS)时验证。
// 若在此填一个 mock 中间件(仅拦 /app-api、/admin-api)无法服务的 /games/... 路径,
// 只会触发取包失败兜底,污染 error 事件,故不填。详见返回 openIssues。
// manifestUrl: undefined,
// 入口约定为 'demo':A2 Runtime 据此加载内置 demo 游戏(非真实文件路径,链路约定值)
entry: 'demo',
runtimeVersion: '1.0.0',
preloadPolicy: 'eager',
bundleSize,
checksum: fakeSha256(`pkg-${gameId}-${versionId}`),
sandbox: {
// iframe 沙箱属性:只允许脚本;内置 demo 不需要同源权限。
sandboxAttr: 'allow-scripts',
// postMessage origin 白名单:本地开发/预览同源(宿主 bridge 三校验放行)
allowOrigins: [
'http://localhost:5173',
'http://localhost:4173',
'http://127.0.0.1:5173',
'http://127.0.0.1:4173',
],
},
traceId: `tr-pkg-${versionId}`,
}
}
/** POST /app-api/runtime/session/start */
function sessionStartHandler(req: MockRequest): SessionStartResp {
const body = (req.body ?? {}) as Partial<SessionStartReq>
const token = body.clientPlayToken
if (!body.gameId || !body.versionId || !token) {
throw new MockError(1102003001, 'gameId/versionId/clientPlayToken 必填')
}
// 幂等:同 token 复用同 sessionId(防抖/重连不重复计数)
let sessionId = tokenToSession.get(token)
if (sessionId === undefined) {
sessionId = ++sessionSeq
tokenToSession.set(token, sessionId)
}
return { sessionId, startTime: nowISO() }
}
/** POST /app-api/runtime/session/end */
function sessionEndHandler(req: MockRequest): boolean {
const body = (req.body ?? {}) as { sessionId?: number }
const sessionId = Number(body.sessionId)
if (!sessionId) throw new MockError(1102003002, 'sessionId 必填')
// 幂等:已结束直接成功,不二次累加
endedSessions.add(sessionId)
return true
}
/** 导出本模块路由表 */
export const runtimeRoutes: MockRoute[] = [
{ method: 'GET', pattern: '/app-api/runtime/package/:versionId', handler: packageHandler },
{ method: 'POST', pattern: '/app-api/runtime/session/start', handler: sessionStartHandler },
{ method: 'POST', pattern: '/app-api/runtime/session/end', handler: sessionEndHandler },
]