增加不可变源码检出、两阶段 finalize/activate、Runtime 同源对象读取与 iframe 安全边界,并用 fail-closed CI 门固定契约。Git 仅提交平台实现、契约、迁移和 SoT,不包含具体游戏源码、素材、bundle 或验收证据。
99 lines
4.4 KiB
TypeScript
99 lines
4.4 KiB
TypeScript
/**
|
||
* 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 },
|
||
]
|