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

19 KiB
Raw Blame History

status, sot-impact, canonical, 上级
status sot-impact canonical 上级
修订待复核 修订 GamePackage、运行时分发、参照资产消费和游戏内容存储边界本档只记录迁移方案不复制架构 SoT false docs/architecture/架构/契约总览.md

游戏包与资产对象存储迁移

执行约束:按波次实施,每一波独立验证。任何具体游戏从 Git 删除前,必须先完成对象存储上传、下载逐文件对账、真实消费者切换和回滚观察。

目标Git 只保存平台代码、契约和最小合成测试夹具;每款具体游戏的源工程、素材、运行包和验收证据进入对象存储,并能被授权下载、预览、发布和可信消费。

架构:对象存储保存不可变内容,数据库保存租户归属、发布状态、对象 key 和可信 hash。运行时清单继续使用现有 GamePackage/1GameArtifactManifest/1 只索引运行包、素材和证据;引擎无关 GameSourceArchive/1 单独索引源码。三者不共用 schema、不共用 URL。对象 marker 只代表回读校验完成V34 game_artifact_storage 的数据库 CAS 才代表 committed。

现有技术MinIO/S3、MySQL、Spring Runtime API、Python 生成 worker、Vue 游戏宿主、ReferenceAsset 消费门。

边界

类别 Git 对象存储 生产消费方式
平台代码 保留 可选发布镜像 正常构建、部署
合成测试夹具 保留最小量 可选 只验证平台算法,不呈现具体游戏内容
游戏源工程 不保留 引擎无关源码进入私有 game-sourcesTier2 历史源工程仍在 tier2-src 生成、修复服务按 source manifest 或 Tier2 manifest 物化
游戏素材 不保留 game-artifacts 运行时授权后代理或签发精确短期 URL
游戏运行包 不保留 game-artifacts 宿主读取现有 GamePackage/1 和版本 bundle
验收证据 不保留 私有 game-evidence 审核服务按 record/key/hash 读取
临时批跑 不保留 默认不保存 需要审计时按保留期进入证据桶
设计和签认结论 保留摘要 原始媒体可外置 Git 保存决策、签认身份和对象 hash不保存游戏内容

平台共享引擎、SDK、生成器、构建器、上传下载器、契约和门脚本仍属于项目代码。绑定某一款游戏的玩法代码、配置、素材、bundle、截图、轨迹和测试不属于平台代码。

三份清单

运行版本前缀下保留运行清单和制品清单;源码修订使用独立清单和桶:

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 自身 hashgame_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=falseartifact-manifest.jsonsource-manifest.json 已存在时,不同 hash 会在任何 Put 前拒绝,相同 hash 会逐对象回读校验marker 尚不存在时,残留 partial 对象可在单写锁内被当前内容覆盖。源码桶必须是显式独立桶,不能与 artifact/evidence 桶复用。这个行为不是并发不可覆盖保证:两个无锁写者仍可能在“读取 marker”和“写 marker”之间相互覆盖波次 1 的 marker 也不能作为发布提交凭据。

对象存储提交状态与运行包发布状态是两台独立状态机,只通过“发布只接受 committed 对象”这道门连接。

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_pendingcommitted=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_jsonpublishPackage 是同一运行包发布入口。新增的 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.idTier2 返回的 {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 删除。

实施波次

  • 波次 1契约与离线工具。 已冻结 GamePackage/1GameArtifactManifest/1GameSourceArchive/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 passedJava runtime 定向测试 28 passedShanhai Node 测试 54/54,三场景 headless smoke 全绿quick/formal simulation gate 分别满足 c1c4test_reference_asset_and_acceptance_v3.py43/43docs-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=815f8918c0355ebda4ca7feb9e9e012e9f76fa597ef4dab05df06b5a302f347eV34 存储记录为 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=26dcb489bd147c393dfae2b567090f787c2be1219525321e40d9d619af8eddbcmanifestHash=761dd48aef911bbfff9a5a03d9710ccec6cce1b8360382b397a233291f7d5358),但联网上传仍按设计 fail-closedgame-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 删除;任何一项缺失都继续保留,避免发布链和金标门被切断。