games-development-ai/docs/plans/2026-07-29-游戏包与资产对象存储迁移-plan.md
lili 17727d08c3
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(storage): 接入游戏内容对象存储与运行时提交门
冻结 GameArtifactManifest/GameSourceArchive 契约和 V34 pending/committed 状态记录,增加专用 writer 的全量回读物化校验,并让 Runtime API 在消费和发布前对账 committed、GamePackage checksum 与实际 engineBundle hash。隔离 staging 已验证真实消费拒绝未提交版本;源码桶权限仍保持 fail-closed,正式金标不切换。
2026-07-29 12:02:22 -07:00

169 lines
19 KiB
Markdown
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.

---
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 packageLittleJS/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 hashRelease 继续绑定 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 金标绑定对象身份。
- [ ] **波次 6Git 清理。** 上传所有具体游戏、切换全部消费者、确认观察窗无回退后,独立提交删除 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 分别满足 c1c4`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 删除;任何一项缺失都继续保留,避免发布链和金标门被切断。