冻结 GameArtifactManifest/GameSourceArchive 契约和 V34 pending/committed 状态记录,增加专用 writer 的全量回读物化校验,并让 Runtime API 在消费和发布前对账 committed、GamePackage checksum 与实际 engineBundle hash。隔离 staging 已验证真实消费拒绝未提交版本;源码桶权限仍保持 fail-closed,正式金标不切换。
169 lines
19 KiB
Markdown
169 lines
19 KiB
Markdown
---
|
||
status: 修订待复核
|
||
sot-impact: 修订 GamePackage、运行时分发、参照资产消费和游戏内容存储边界;本档只记录迁移方案,不复制架构 SoT
|
||
canonical: false
|
||
上级: docs/architecture/架构/契约总览.md
|
||
---
|
||
|
||
# 游戏包与资产对象存储迁移
|
||
|
||
> **执行约束**:按波次实施,每一波独立验证。任何具体游戏从 Git 删除前,必须先完成对象存储上传、下载逐文件对账、真实消费者切换和回滚观察。
|
||
|
||
**目标**:Git 只保存平台代码、契约和最小合成测试夹具;每款具体游戏的源工程、素材、运行包和验收证据进入对象存储,并能被授权下载、预览、发布和可信消费。
|
||
|
||
**架构**:对象存储保存不可变内容,数据库保存租户归属、发布状态、对象 key 和可信 hash。运行时清单继续使用现有 `GamePackage/1`;`GameArtifactManifest/1` 只索引运行包、素材和证据;引擎无关 `GameSourceArchive/1` 单独索引源码。三者不共用 schema、不共用 URL。对象 marker 只代表回读校验完成,V34 `game_artifact_storage` 的数据库 CAS 才代表 committed。
|
||
|
||
**现有技术**:MinIO/S3、MySQL、Spring Runtime API、Python 生成 worker、Vue 游戏宿主、ReferenceAsset 消费门。
|
||
|
||
## 边界
|
||
|
||
| 类别 | Git | 对象存储 | 生产消费方式 |
|
||
|---|---|---|---|
|
||
| 平台代码 | 保留 | 可选发布镜像 | 正常构建、部署 |
|
||
| 合成测试夹具 | 保留最小量 | 可选 | 只验证平台算法,不呈现具体游戏内容 |
|
||
| 游戏源工程 | 不保留 | 引擎无关源码进入私有 `game-sources`;Tier2 历史源工程仍在 `tier2-src` | 生成、修复服务按 source manifest 或 Tier2 manifest 物化 |
|
||
| 游戏素材 | 不保留 | `game-artifacts` | 运行时授权后代理或签发精确短期 URL |
|
||
| 游戏运行包 | 不保留 | `game-artifacts` | 宿主读取现有 `GamePackage/1` 和版本 bundle |
|
||
| 验收证据 | 不保留 | 私有 `game-evidence` | 审核服务按 record/key/hash 读取 |
|
||
| 临时批跑 | 不保留 | 默认不保存 | 需要审计时按保留期进入证据桶 |
|
||
| 设计和签认结论 | 保留摘要 | 原始媒体可外置 | Git 保存决策、签认身份和对象 hash,不保存游戏内容 |
|
||
|
||
平台共享引擎、SDK、生成器、构建器、上传下载器、契约和门脚本仍属于项目代码。绑定某一款游戏的玩法代码、配置、素材、bundle、截图、轨迹和测试不属于平台代码。
|
||
|
||
## 三份清单
|
||
|
||
运行版本前缀下保留运行清单和制品清单;源码修订使用独立清单和桶:
|
||
|
||
```text
|
||
tenants/<tenantId>/games/<gameId>/versions/<versionId>/
|
||
├── manifest.json # 现有 GamePackage/1,游戏宿主读取
|
||
├── artifact-manifest.json # GameArtifactManifest/1,运行包/素材/证据索引
|
||
├── assets/<相对路径>
|
||
└── runtime/<bundle 或渠道包>
|
||
|
||
game-sources 桶(GameSourceArchive/1;权限和桶预建完成前禁止联网写):
|
||
└── tenants/<tenantId>/games/<gameId>/source-revisions/<revisionId>/
|
||
├── source-manifest.json
|
||
└── source/<源工程相对路径>
|
||
|
||
tier2-src 桶(沿用现有 Tier2 SourceProjectStore,仅 Tier2):
|
||
└── <sourceGameId>/<sourceRevisionId>/<源工程相对路径>
|
||
|
||
game-evidence 桶:
|
||
└── tenants/<tenantId>/games/<gameId>/versions/<versionId>/evidence/<验收证据路径>
|
||
```
|
||
|
||
`manifest.json` 的原始字节 hash 继续写入 `game_runtime_package.checksum`,宿主按现有 `GamePackage/1` 解析。`artifact-manifest.json` 记录素材、运行包和证据对象的 `category/store/path/sourcePath/key/bytes/sha256/mime`、分类 tree hash、bundle hash 和自身 hash。引擎无关源工程由 `GameSourceArchive/1` 记录源码路径、字节 hash、sourceHash 和 `source-manifest.json` 自身 hash;`game_source_archive` provider 绑定这份清单,Tier2 继续使用 `tier2_source_project` 和现有 `tier2-src`。Tier2 provider 只允许 Phaser package,LittleJS/Canvas 必须走引擎无关 provider。清单不包含发布状态、时间或执行者,也不能作为 `manifestUrl` 返回给宿主。创建时间、操作者和 trace 属于数据库审计记录,不能让相同内容重复构建出不同 manifest hash。
|
||
|
||
对象 key 永久稳定,签名 URL 只在授权请求时生成,不写入清单。路径先做 NFC 规范化并按 POSIX 相对路径排序;tree hash 和 manifest hash 使用不同域标签,防止不同类型摘要被混用。
|
||
|
||
## 数据权威
|
||
|
||
| 数据 | 权威位置 | 说明 |
|
||
|---|---|---|
|
||
| 游戏版本身份 | `project.game_version.id` | 运行包对象前缀的 `versionId`,契约只接受正整数十进制字符串;Tier2 字符串版本只能写入 `sourceRevision.revisionId` |
|
||
| 发布生命周期 | `game_runtime_package.status` | 保持 preview/published/retired 的 CAS 门,不写入不可变对象清单 |
|
||
| GamePackage 原始 hash | `game_runtime_package.checksum` | 对 `manifest.json` 原始字节计算 |
|
||
| 对象索引身份 | 新的版本存储记录 | 保存 bucket、artifact manifest key/hash、状态 pending/committed、字节数和创建者 |
|
||
| 引擎无关源工程 | `GameSourceArchive/1` + `game-sources` | 保存 source manifest key/hash、source hash、引擎元数据;V34 对象记录保存其与运行版本的绑定 |
|
||
| Tier0/1 结构化源项目 | 迁移后由对象存储持有正文 | `game_source_project` 只保存版本、对象 key、source hash 和元数据 |
|
||
| Tier2 源工程 | 现有 MySQL manifest + `tier2-src` | 保留已上线的 manifest-first 路径;生产制品通过 `tier2-source-project:<gameId>:<revisionId>` 和 sourceHash 绑定,不复制源文件 |
|
||
| 金标消费身份 | 新版 ReferenceAsset Registry/Release | 保存可信对象 key/hash;消费前物化到临时可信根并逐文件校验 |
|
||
|
||
对象存储版本记录将在波次 2 使用状态 `pending → committed`。所有对象上传并 GET 全量复算后,还必须由数据库条件创建/CAS 把 pending 变为 committed;发布 CAS 只接受 committed 版本。同一 `tenantId/gameId/versionId` 已存在不同 manifest hash 时拒绝覆盖,相同 hash 视为幂等命中。失败的 pending 记录由定时回收任务按保留期清理,published/retired 版本都不能被覆盖。
|
||
|
||
波次 1 使用的 MinIO Python SDK 没有公开的条件 Put 接口。离线上传器因此只允许调用方先持有 `tenantId/gameId/versionId` 级外部单写锁,并返回 `verified_pending`,明确 `committed=false`。`artifact-manifest.json` 和 `source-manifest.json` 已存在时,不同 hash 会在任何 Put 前拒绝,相同 hash 会逐对象回读校验;marker 尚不存在时,残留 partial 对象可在单写锁内被当前内容覆盖。源码桶必须是显式独立桶,不能与 artifact/evidence 桶复用。这个行为不是并发不可覆盖保证:两个无锁写者仍可能在“读取 marker”和“写 marker”之间相互覆盖,波次 1 的 marker 也不能作为发布提交凭据。
|
||
|
||
对象存储提交状态与运行包发布状态是两台独立状态机,只通过“发布只接受 committed 对象”这道门连接。
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
state "对象存储版本" as storage {
|
||
[*] --> pending: 创建存储记录
|
||
pending --> committed: 对象全量 GET 对账 + 数据库 CAS
|
||
pending --> abandoned: 超时或上传失败
|
||
committed --> abandoned: 未发布且明确废弃
|
||
}
|
||
state "运行包发布" as release {
|
||
[*] --> preview
|
||
preview --> published: 存储记录必须 committed
|
||
published --> retired: 下架
|
||
published --> published: 禁止覆盖
|
||
retired --> retired: 只读或法务冻结
|
||
}
|
||
```
|
||
|
||
## 读写接口
|
||
|
||
平台只接受数据库中已登记的对象 key,不接受调用方传入任意 URL。
|
||
|
||
| 接口 | 输入 | 输出 | 失败方式 |
|
||
|---|---|---|---|
|
||
| 构建对象索引 | 本地临时游戏目录、数据库 tenant/game/version、完整 GamePackage、已保存的 Tier2 sourceRevision | `GameArtifactManifest/1` | GamePackage schema/身份不一致、源修订 hash 漂移、路径越界、符号链接、凭据文件或重复路径即拒绝 |
|
||
| 构建源归档 | 本地临时游戏目录、数据库 tenant/game、source revision、引擎描述 | `GameSourceArchive/1` | 空源树、二进制/路径 hash 漂移、运行 manifest 混入、路径越界、符号链接或重复路径即拒绝 |
|
||
| 波次 1 上传验证 | manifest、可信本地根、服务端身份、外部单写锁 | `verified_pending`、`committed=false` 回执 | 未声明外部单写、同版本 marker 不同 hash、上传/回读不一致即拒绝 |
|
||
| 生产上传提交(波次 2) | pending 数据库记录、manifest、可信本地根、服务端身份 | 数据库 CAS 后的 committed 回执 | 并发条件创建失败、同版本不同 hash、配额超限、上传/回读不一致即拒绝 |
|
||
| 物化版本 | 数据库登记的 tenant/game/version、manifest key/hash、用途、目标临时根 | 已逐文件校验并原子交付的本地快照 | 期望身份/hash 缺失、生产消费未 committed、越权或逐文件 hash 不一致即拒绝且清理临时目录;波次 1 只允许诊断回验,不接生产消费 |
|
||
| 运行时取包 | versionId、scene、当前用户 | 同源 API 响应或精确短期 URL | 保持 owner/status/tenant 门;生产失败显式报错,不回退 demo |
|
||
| 金标取参照 | releaseId、policyId、recordId | 验证回执与只读物化根 | registry/release/manifest/hash 任一不一致即拒绝 |
|
||
|
||
前端只向同源 Runtime API 发送平台 Token。访问对象存储时使用后端签发的精确短期 URL,不能附带平台 Authorization 或 tenant-id;重定向到非允许域时拒绝。source 和 evidence 永不直接暴露给浏览器,运行包域需要独立 CORS、CSP、MIME 和缓存策略。写入只允许专用 artifact-writer 身份,源码桶必须单独授权且不能和 artifact/evidence 同桶;对象 key 由数据库 tenant/game/version 关系派生,调用方不能提交桶或前缀。读者无 List/Delete,写者无建桶、删桶、策略管理或已提交版本 Delete 权限。现有 `ragflow` MinIO root 凭据不能复用;专用账号、源桶和权限未就位时上传器必须拒绝联网。生产启用 TLS、静态加密、版本化和 90 天凭据轮换。
|
||
|
||
## 四条迁移线
|
||
|
||
### 运行时与素材
|
||
|
||
Runtime 编译完成后同时写现有 GamePackage 和对象存储版本记录。影子读阶段仍由 DB 返回 GamePackage,但后台从 OSS 下载并对账;切换后 Runtime API 在完成 tenant/owner/status 门禁后代理返回 GamePackage 或签发对象 URL。宿主的生产模式遇到网络、schema 或 hash 错误必须显示加载失败,demo 兜底只留在显式 mock/development 模式。
|
||
|
||
当前真实消费链已核对为:`GET /app-api/runtime/package/{versionId}` 与原始 manifest 端点先进入 `RuntimePackageServiceImpl.getPackageManifest`,再由 `PackageStore` 读取 `game_runtime_package.package_json`;`publishPackage` 是同一运行包发布入口。新增的 `ArtifactStorageGate` 挂在取包和发布之前,配置 `game.artifact-storage.enforce-committed=false` 时保持旧 DB 路径,打开后只放行 V34 `game_artifact_storage.status=committed` 的版本,并对账 `runtime_manifest_hash == game_runtime_package.checksum`。当 GamePackage 有 `engineBundle` 时,门禁还对实际 UTF-8 bundle 文本计算 SHA-256 并与 `bundle_hash` 比较;没有可选 bundle 时两边都必须为空。manifest 缺失、解析失败、hash 漂移或对象记录未提交均拒绝,不回退 demo。这个门禁目前是生产切换前的影子实现,尚未代表 OSS `PackageStore` 已切换;V34 迁移、对象上传和观察窗完成前不得打开开关。
|
||
|
||
参照资产旧版 v2 测试仍有一条 worktree 断言假定具体游戏文件未被 Git 忽略;当前仓库已按本计划把具体游戏列入忽略并准备迁移对象存储,因此该红灯是测试口径陈旧,不是生产 `clean-archive/frozen_preflight` 消费失败。修该测试应单独纳入参照资产测试治理,不通过恢复 Git 游戏内容来消红。
|
||
|
||
### 引擎无关源工程
|
||
|
||
对 LittleJS、Canvas 和其它非 Tier2 工程,先用 `GameSourceArchive/1` 固定 NFC 路径、二进制安全 sourceHash 和 source manifest hash,再由 V34 对象记录绑定 game/version。`game-sources` 未完成专用权限和预建桶前只允许离线构建与 fake/合成测试;完成上传、逐文件回读和 CAS 后才允许生产物化。
|
||
|
||
### Tier0/1 结构化源工程
|
||
|
||
先确定 Tier0/1 SourceProjectStore 的 canonical hash 和不可变修订身份,再修订 artifact schema 增加对应 provider。随后给 `game_source_project` 增加对象 key、source hash、manifest hash 和迁移状态,写入时双写 DB JSON 与对象存储;完成历史 backfill 和全量对账后,读取改为对象存储,最后停止写正文 JSON。对外 fetch DTO 返回稳定对象身份,不返回永久公开 URL。
|
||
|
||
### Tier2 源工程
|
||
|
||
保留现有 `BackendStore` 的 MySQL manifest-first 与 `tier2-src` MinIO 实现。发布运行包时,artifact manifest 的 `versionId` 只写 `project.game_version.id`,Tier2 返回的 `{id, versionId, sourceHash}` 原样写入 `sourceRevision`;构建器必须复算本地 SourceProjectStore 口径的 `path\0content\0` hash 并与 sourceHash 相等。两者的映射随 artifact manifest hash 一起进入对象存储版本记录,不新建第二套 Tier2 源权威。
|
||
|
||
### 金标和参照资产
|
||
|
||
冻结现有 ReferenceAsset `/1`、`/2`,新增对象存储消费版本,不就地放宽相对路径。新 Registry 保存对象身份与 artifact manifest hash;Release 继续绑定 Registry 和 policy 原始字节 hash。`frozen_preflight` 先把对象下载到临时可信根,再沿现有 fd/逐文件 hash 逻辑校验。只有新消费路径和回归测试全绿后,才把《山海行纪》及其它参照内容从 Git 删除。
|
||
|
||
## 实施波次
|
||
|
||
- [x] **波次 1:契约与离线工具。** 已冻结 `GamePackage/1`、`GameArtifactManifest/1`、`GameSourceArchive/1` 三份清单,收紧 Tier2/Phaser provider 边界、引擎无关源 hash、外部单写前提下的 marker 幂等、`verified_pending` 回执、下载逐文件验证和安全负测;未产出 committed,未改生产消费,未删除 Git 内容。
|
||
- [ ] **波次 2:对象存储状态记录。** 增加 pending/committed 记录、数据库条件创建/CAS、并发不可覆盖门、孤儿回收、审计日志和配额;完成至少一款候选的上传、committed 和下载回验。
|
||
- [ ] **波次 3:运行时影子读。** GamePackage 双写 OSS,后端影子下载对账;修正绝对 URL Token 泄露和生产 demo 回退。
|
||
- [ ] **波次 4:源工程迁移。** 分别迁移 Tier0/1 和 Tier2,完成 backfill、影子读、全量对账和回滚观察。
|
||
- [ ] **波次 5:金标消费迁移。** 新 Registry/Release/物化 preflight 通过后,将 active 金标绑定对象身份。
|
||
- [ ] **波次 6:Git 清理。** 上传所有具体游戏、切换全部消费者、确认观察窗无回退后,独立提交删除 tracked 游戏内容并扩大 ignore;平台合成 fixture 留在 Git。
|
||
|
||
## 验收
|
||
|
||
### 当前执行证据(2026-07-29)
|
||
|
||
离线工具、V34 状态记录、运行时提交门和正式《山海行纪》验收均已在本地完成验证:Python worker `181 passed`,Java runtime 定向测试 `28 passed`,Shanhai Node 测试 `54/54`,三场景 headless smoke 全绿,quick/formal simulation gate 分别满足 c1–c4,`test_reference_asset_and_acceptance_v3.py` 为 `43/43`,docs-gate 与 `git diff --check` 全绿。运行时门的默认开关仍为 `false`;它会同时校验三元业务身份、V34 `committed`、runtime manifest hash 和实际 UTF-8 bundle hash。
|
||
|
||
波次 2 仍未宣称完成,但隔离 staging 已完成一轮真实身份和对象链路验证:通过正式 App API 创建项目 `gameId=80034`、生成版本 `versionId=93156`,并在 `game-staging-mysql/ruoyi-vue-pro` 应用 V34。原始 GamePackage 由实际 Runtime API 取回,`runtimeManifestHash=1191b84776703ce009d361c920da0c3dbb73f328f1383e25580915b10937e4ee`;为满足 generic 宿主的 `__GameBundle` 入口,staging 适配包的 `bundleHash=ef2fff2764227b773e4f4dc092941ddbef7f7062fae2dae80160992de637066d`,不改正式签认包的 `17b9073c767faf7990e0bf4563a86d55ffa81121f11e7c8ad37c8b8ce72e8cbd`。artifact manifest 已上传 `game-artifacts` 并由专用 writer 全量 GET 回读,得到 `manifestHash=815f8918c0355ebda4ca7feb9e9e012e9f76fa597ef4dab05df06b5a302f347e`;V34 存储记录为 `pending/committed=false`。打开 `game.artifact-storage.enforce-committed=true` 的隔离 Runtime 后,真实 `GET /app-api/runtime/package/93156?scene=preview` 返回 `1102001001`,日志明确记录 `[artifactStorageGate] 对象记录未 committed,拒绝运行时消费`;同一 artifact manifest 又从对象存储物化并逐文件校验 263 个对象,下载后的 bundle 与上述 hash 一致。
|
||
|
||
源码归档清单已离线构建(57 个对象,`sourceHash=26dcb489bd147c393dfae2b567090f787c2be1219525321e40d9d619af8eddbc`,`manifestHash=761dd48aef911bbfff9a5a03d9710ccec6cce1b8360382b397a233291f7d5358`),但联网上传仍按设计 fail-closed:`game-sources` 尚未预建并授权给专用写者,CLI 返回 `GAME-SOURCE-ARCHIVE-ERROR`。因此尚不能把这轮 staging 证据记为 committed、不能打开生产运行时开关,也不能据此改写正式金标 Registry;生产切换和波次 2 完成仍以源码桶权限、pending→committed CAS 及观察窗为前置条件。
|
||
|
||
- 干净 clone 不含任何具体游戏源、素材、bundle、dist 或 evidence;只保留平台代码和合成 fixture。
|
||
- 每款迁移游戏都有 committed 回执,能从对象存储下载并逐文件复算 hash。
|
||
- 浏览器真实加载对象存储提供的 GamePackage、bundle 和素材;控制台无阻断错误,平台 Token 不发往对象域。
|
||
- 运行包加素材总量不超过 10MB、首屏不超过 2MB;单文件不超过 100MB、单版本不超过 10000 个对象、相对路径不超过 1024 字节,租户总配额由服务端记录强制。
|
||
- preview、play、retired、跨租户、过期签名、重定向 SSRF、并发覆写、错误 MIME/CORS/CSP、配额和孤儿回收负测均被拦截。
|
||
- ReferenceAsset 的 Registry、Release、消费清单和物化快照形成完整 hash 链,干净 clone 下仍能完成 `frozen_preflight`。
|
||
- 同一版本重复上传相同 hash 为幂等命中,不同 hash 拒绝;published 后不可覆盖,retired 后不可再签发新 URL。
|
||
- `.agents/tools/docs-gate.sh`、相关契约/后端/前端测试、对象存储 smoke、浏览器真玩和 `git diff --check` 全绿。
|
||
|
||
## 删除门
|
||
|
||
现有 `game-runtime/games/*`、`tier2/games/*` 和 `_wg1-gen` 中仍有 tracked 具体游戏。增加 `.gitignore` 不能移除已跟踪文件,也不能替代消费迁移。只有对应游戏同时满足“已上传、已下载对账、消费者已切换、观察窗结束、回滚包可取”五项条件,才允许从 Git 删除;任何一项缺失都继续保留,避免发布链和金标门被切断。
|