docs(游戏共创): M2b 文档按实现收敛(块 E)

- 技术方案 §3.2.3:三列语义改为与实现一致;删掉原「保留一次补齐机会」的规则(**已公开版本的补传未实现**,本期不做),改为「只在 `awaiting_upload` / `upload_failed` 可写一次、写入后换内容 409、不做版本回溯」;补产品口径(不做独立「源码可见性」开关、作者不传即产物级改编、`nonCommercial` 约束力属平台规则层面);对象键改为实现值 `…/{version_id}.project.zip`
- 技术方案 §2.3:把「公开后可补传一次工程源包」的表述改为如实口径(公开后不接受补传,只能走试玩 + 素材)
- 技术方案 §3.3:version 行的 `project_bundle_*` 迁移规则改为「只在未公开时可写一次」
- 技术方案 §3.4:`PUT /versions/{versionId}/project-bundle` 从「规划」改为**已实现**(整包 + 4 条分片族路由、作者限定、阶段门、无既有工程包、服务端派生对象键);「登录用户」小节把取件接口从两个改为三个并说明 `source` 可能是 `project`(优先)或 `package`,新增 `/fork-source/project` 本体行(失败关闭、缓存含资产维度)
- 技术方案 §3.5.2:打包/服务端复核/上传/下载建项/优先级五条按实现重写——写明服务端 `validate_project_bundle_zip` 的完整拒绝清单与与发行包逐项相等的上限(200 MiB / 500 MiB / 64 MiB / 10 000 / 100,理由:共用同一上传链路且反代按 200 MiB 校准),以及「打包器已落地、上传与建项接线待做」
- 技术方案 §3.5.4:记录产品已采纳推荐口径(A 默认继续服务未上传工程包的作品、B 服务端已实现、C 不做)
- 里程碑「作品工程源包与一键改造」:Status → in-progress(服务端已落地 / 客户端接线待做)、Version 1.2;范围去掉「是否公开工程」选项(不存在独立开关);不在范围内新增「已公开版本补传」;两条前置依赖标注已落地并写清 512 MiB 与 200 MiB 的对齐口径(客户端须按服务端上限预检);验收标准改为带 ✅/⏳ 标注、删掉两条 backfill 验收、补阶段门与半写行回落等已实现项
- 门禁:`check:doc-index` 248 份 OK;`check:encoding` 5301 files OK;`git diff --check` 0
This commit is contained in:
2026-10-05 15:28:07 +08:00
parent 8ce4649081
commit d39257b608
2 changed files with 45 additions and 35 deletions
@@ -2,12 +2,14 @@
| 字段 | 值 |
| ----------- | ----------------------------------------------------- |
| Version | 1.1 |
| Status | proposed(**正解路径**;未拍板前不得实现,见依赖与前置条件) |
| Date | 2026-10-03(2026-10-04 标注为正解路径并补前置依赖) |
| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md`(口径见 §2.5、§3.5.1、§3.5.2、§3.5.4) |
| Version | 1.2 |
| Status | in-progress(**服务端已落地**:三列 + 工程包校验器 + 上行/下行路由族;AGC 侧确定性打包器已落地,**上传与建项接线待做**) |
| Date | 2026-10-03(2026-10-04 标注为正解路径;2026-10-05 按实现更新范围与验收) |
| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md`(口径见 §2.5、§3.2.3、§3.4、§3.5.1、§3.5.2、§3.5.4) |
> **定位(2026-10-04)**:只读侦察证明成品包路径只能试玩、不能重新发布(见「成品包改造闭环」里程碑的「事实依据」),因此**本里程碑是「一键复刻完整工程」的唯一正解路径**:只有拿到作者随版本上传的工程源包,用户才可能在不重写工程的前提下改核心逻辑并重新发布。技术方案 §3.5.4 的推荐是「路线 A(参考式改编)先行验证需求,路线 B(本里程碑)作为正解排后续」。
>
> **产品口径(2026-10-05 已拍板,§3.2.3/§3.5.4)**:**不做**独立的「源码可见性」开关——作品授权非 `forbidden` 时,工程源包与成品包走同一道取件鉴权;作者不想给源码就是**不上传**,「上传与否」本身就是开关。`nonCommercial` 的约束力是**平台规则层面**(源码被取走后无法从技术上阻止商用)。上传与发布/审核解耦:未上传不影响发布。
## 目标
@@ -15,15 +17,16 @@
## 范围
- 工程源包作为发行版本的可选伴随资产:上传、确认、只读回读与对象存储生命周期。
- 工程源包的内容门禁(与模板包同一套排除规则与体积上限),服务端独立复核。
- 工程源包作为发行版本的可选伴随资产:上传(整包或分片续传)、确认、只读回读与对象存储生命周期。
- 工程源包的内容门禁:服务端**独立恢复**(不信任客户端)——与发行包同级的体积/条目上限 + 源码场景项(凭据与隐私文件、依赖与 VCS 目录、根级构建产物与 IDE 目录、嵌套压缩包、符号链接、加密条目)。
- 改造内容下发入口的来源优先级:同一版本同时存在工程源包与成品包时优先下发工程源包,并如实标注来源类型。
- 客户端:源码工程打包 → 上传(含中断续传);下载 → 校验摘要 → 解压 → 以源码工程形态建项。
- 发布面板中「是否公开工程」的选择与后果说明(默认不公开)。
- 客户端:源码工程确定性打包(条目排序 + 固定时间戳)→ 上传(含中断续传);下载 → 校验摘要 → 解压 → 以源码工程形态建项。
- 发布面板的工程包上传入口与后果说明:**上传与否是作者唯一的开关**(不做独立的「源码可见性」授权位);未上传时如实提示「只能被他人试玩与参考」。
## 不在范围内
- 成品包路径的「试玩 + 素材参考」闭环(见「成品包改造闭环」里程碑;本里程碑复用其内容下发入口、血缘声明与建项基座)。
- **已公开版本的补传(backfill)**:原方案曾设想「公开后可给当前公开版本补传一次工程包」,本期**不做**——上传阶段门与发行包确认一致(只允许 `awaiting_upload` / `upload_failed`),公开后不接受任何内容写入。若产品要这条,需要单独设计(它会是「版本不可变」的例外)。
- 工程源包的版本回溯(历史版本没有工程包时不为它补做)。
- 网页端上传工程源码包(首期只支持客户端)。
- 相似度比对与低改动度判定、收益分成。
@@ -32,23 +35,25 @@
- 「游戏共创授权与血缘」里程碑已通过验收。
- 「成品包改造闭环」里程碑已通过验收(内容下发入口、客户端建项基座、发布携带来源、授权面板均在那一里程碑落地)。
- **新增前置依赖 ①(当前不存在,必须在本里程碑内新建):「工程包下载/授权」接口。** 平台今天**没有**任何「按版本取包」的下载通道——作者侧路由只有 create/upload/chunk/complete/reset/submit/get_owner_version/cancel,作者侧 payload 不回对象键或下载地址(`server-rs/crates/api-server/src/modules/game_distribution.rs:3029-3041`);对外只有公开发行网关**逐文件**读取且只服务当前已公开版本(`:395-421`、`:800-863`),扩展名受限(`server-rs/crates/module-game-distribution/src/release.rs:36-64`)。工程源包含 `.ts`/`.vue`/`package.json` 等不在白名单内的文件,**无法**靠发行网关拼出来,必须新做受鉴权下载(并设计 Range/续传或分片,现状两侧都是整包进内存:服务端 `:926-958`、客户端 `src-tauri/src/template_library.rs:564-591`)。
- **新增前置依赖 ②:包内禁项必须先定义清楚。** 服务端已拒绝 `node_modules`、`.git`、`.agent`、`.env*`、`*.map`、`*.pem`、`*.key` 与嵌套 `.zip`(`server-rs/crates/module-game-distribution/src/package.rs:135-142`、`:170-176`),且 `node_modules` 不能进包意味着用户拿到源包后仍需本机 `npm install`;本地导出上限 512 MiB 与平台 200 MiB 的上限并不对齐,需要在本里程碑内定口径。
- **前置依赖 ①「工程包下载/授权」接口——已落地(2026-10-05)**:`GET /games/{gameId}/fork-source` 在有工程包时返回 `source: "project"`,本体的独立路径 `GET /games/{gameId}/fork-source/project` 与成品包共用同一套鉴权与校验函数(不重复实现),**不下发对象键**、只服务当前公开版本、`no-store`。读取复用现役 OSS 读路径,**缓存键含资产维度**(对象键 `…/{version_id}.project.zip` 与 `…/{version_id}.zip` 不同),两份资产不串味。
- **前置依赖 ②包内禁项与规模上限——已定稿(2026-10-05)**:服务端 `module-game-distribution/src/project_bundle.rs` 的 `validate_project_bundle_zip` 独立复核,拒绝清单与上限见技术方案 §3.5.2。**512 MiB(客户端打包上限)与 200 MiB(服务端/反代放行量)的不对齐按下述口径收敛**:服务端上限与发行包逐项相等(200 MiB / 展开 500 MiB / 单文件 64 MiB / 条目 10 000 / 压缩比 100),因此**客户端在上传前必须按服务端上限预检**(客户端打包器的 512 MiB 只是本机护栏),超限包应在本地就被拦下而不是上传到一半被拒——这一条属客户端待办。
- 客户端已有的「从包安装并建项」基座与包内容门禁可直接复用;本里程碑不新建第二套解压或建项路径。
- 决策:本里程碑成立的前提是产品同意引入工程源包;若否决,本里程碑取消,改编能力止于「试玩 + 素材」(见「成品包改造闭环」里程碑)。
- 决策:产品已同意引入工程源包,且**不设**独立的「源码可见性」授权位(口径见文首)。
## 验收标准
- [ ] 授权为「禁止共创」时不产生任何工程包上传,也不出现可被他人下载的工程地址。
- [ ] 含被排除目录(版本控制、依赖目录、编辑器与构建产物)或含绝对路径、上级路径、盘符、反斜杠、符号链接、超出条目数或体积上限的工程包在上传确认阶段被拒绝,且不产生任何对象或残留。
- [ ] 同一版本重复确认同一工程包不产生第二份对象;换内容重传按冲突拒绝。
- [ ] 上传中断后可按权威偏移续传,不重放整包、不跳段。
- [ ] 同一版本同时存在两种来源时,下发入口优先返回工程源包并如实标注来源类型;未上传工程包时仍能拿到成品包。
- [ ] 客户端在真实环境中完成源码工程形态建项:新项目可打开、可编辑源码、可试玩、可重跑构建并发布。
- [ ] 通过源码路径发布的作品,其来源、代际与根与成品包路径完全一致,不产生第二套血缘语义。
- [ ] 工程包未上传或上传失败不阻断发布,但界面必须明确告知改造能力被降到「试玩 + 素材参考」(成品包路径,不能直接重新发布)。
- [ ] 作品公开后提升授权,可对**当前公开版本**补传工程包一次;补齐后改造路径升级为源码级,无需作者发新版本。
- [ ] 再次补传、换内容重传被拒绝;目标版本不是当前公开版本、或作品授权仍为禁止时,补传同样被拒绝;历史版本不会被补齐。
> 标注:✅ = 服务端已完成并有单测/门禁证据(本地);⏳ = 待客户端接线或真实栈(dev 栈端到端)验证。
- [x] ✅ 工程包校验器拒绝清单齐备:绝对路径、`..`、盘符、反斜杠、通配符、符号链接、加密条目、嵌套 `.zip`、完全重复/大小写折叠重复路径、任意层级 `node_modules` / `.git` / `.svn` / `.agent`、根级 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode`、凭据与隐私文件(`.env*`、`*.pem`、`*.key`、`*.p12`、`*.pfx`、`.npmrc`、`.netrc`、`.git-credentials`、`id_rsa*`、`id_ed25519*`、`*.map`)、单文件/累计/压缩比超限;合法源码包(**不含** `index.html`)通过。证据:`module-game-distribution` 16 条校验器单测。
- [x] ✅ 同一版本重复确认同一工程包不产生第二份对象(同内容幂等重放);换内容重传按冲突拒绝(409 `同一版本已存在不同的工程源包`)。
- [x] ✅ 阶段门:只有 `awaiting_upload` / `upload_failed` 可写;已上传、验证中、待审核、已拒绝、已公开、已撤回、已取消一律拒绝;已公开版本的**补传不做**(见「不在范围内」)。
- [x] ✅ 下行优先级:同一版本同时有两种资产时 `fork-source` 返回 `source: "project"` 且 `downloadPath` 指向 `/fork-source/project`;只有成品包时回落 `package`;「字节数 > 0 但摘要为空」的半写行回落 `package`(失败关闭);两条下载路径共用同一个校验函数,请求 project 而实际没有工程包时 409 而非静默回落。
- [x] ✅ 两种资产的读取缓存不串味(缓存键即对象键,键名分别以 `.zip` / `.project.zip` 结尾)。
- [x] ✅ 未上传工程包不阻断发布与审核(上传是独立可选资产,不参与版本状态机);无工程包的作品仍能拿到成品包。
- [ ] ⏳ 上传中断后可按权威偏移续传(分片路由已实现,客户端接线与真实栈验证待做)。
- [ ] ⏳ 客户端在真实环境中完成源码工程形态建项:新项目可打开、可编辑源码、可试玩、可重跑构建并发布。
- [ ] ⏳ 通过源码路径发布的作品,其来源、代际与根与成品包路径完全一致(服务端共用同一套血缘,真机验证待做)。
- [ ] ⏳ 界面如实告知:未上传时改造能力止于「试玩 + 素材参考(不能直接重新发布)」。
## 证据要求
@@ -109,7 +109,7 @@ stateDiagram-v2
- 只有 `owner_user_id` 可以变更;请求必须带 `expectedForkAuthorization`(CAS),不匹配返回 `409`。
- 任何降级请求一律 `409 FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED`,不写库。
- 已按旧授权完成的 Fork **不受后续提升影响**(授权在建立血缘时已兑现)。
- **提升与源码的时序**:作品公开后提升授权时,已发布的版本里没有工程源包,因此允许对**当前公开版本补传一次**工程源包(规则见 §3.2.3)。提升界面对此给出提示,但不作为提升的前置条件。
- **提升与源码的时序**:工程源包只能在版本**尚未公开**时随版本上传(阶段门与发行包确认一致);作品公开后**不接受**补传(本期不做 backfill),因此「先禁止共创、后提升授权」的作品在其当前公开版本上不会被源码级改造,只能走「试玩 + 素材参考」。提升界面对此如实提示,不作为提升的前置条件。
### 2.4 用户视角入口矩阵
@@ -269,14 +269,15 @@ pub(crate) project_bundle_bytes: u64,
pub(crate) project_bundle_sha256: Option<String>,
```
工程包跟随版本,但保留**一次补齐机会**:
工程包跟随版本,**只在版本尚未公开时可上传一次**(已实现口径,2026-10-05):
- **发布时**:仅当该版本所属作品的 `fork_authorization != forbidden` 才上传,与版本创建在同一次确认里完成。
- **补齐**:作品公开后,作者可以把工程源包补传给它**当前公开版本**,每个版本至多一次,内容摘要写入后不可再改。这是「版本不可变」的唯一例外——理由是工程源包不是发行内容:它不参与试玩、不参与审核、不改包摘要,补传只增加「能否被源码级改造」这一个能力。
- **上传时机**:只允许版本处于 `awaiting_upload` / `upload_failed`(与发行包确认同一道阶段门);`uploaded`、验证中、待审核、已拒绝、已公开、已撤回、已取消一律拒绝。工程包是**可选**资产:不上传不影响发布与审核。
- **不可变**:同一版本一旦确认过工程包,**换内容按冲突拒绝**(`409 同一版本已存在不同的工程源包`),同内容按幂等重放(`replayed = true`);要改内容只能新建版本。
- **不做版本回溯**:作者 v1 传了、v2 没传,那么 v2 只能走「试玩 + 素材参考」的成品包路径,历史版本不给补。
- **补齐前置**:目标版本必须是该作品**当前公开版本**,且该作品 `fork_authorization` 已不是 `forbidden`。
- **已公开版本的补传(backfill)未实现**:原方案曾设想「作品公开后可给当前公开版本补传一次」。本期**不做**——上传阶段门与发行包一致,公开后不接受任何内容写入;若产品要这条,需要单独设计(它会是「版本不可变」的例外)。
- **产品口径(已拍板)**:平台**不做**独立的「源码可见性」开关。作品授权非 `forbidden` 时,工程包与成品包走同一道取件鉴权与校验被他人取走;作者不想给源码,就是**不上传**。「上传与否」本身就是作者的开关,不传即退化为产物级改编。三态授权保留不变,但 `nonCommercial` 的约束力是**平台规则层面**(源码一旦被取走,无法从技术上阻止商用),文档与界面必须如实标注这一点。
对象键:`agc/project-snapshots/v1/game-fork/{game_id}/{version_id}.zip`(与发行包同前缀族,便于生命周期统一)。
对象键:`{GAME_DISTRIBUTION_OBJECT_PREFIX}{game_id}/{version_id}.project.zip`(即 `agc/project-snapshots/v1/game-distribution/{game_id}/{version_id}.project.zip`)——与发行包同前缀族便于生命周期统一,靠 `.project.zip` 后缀区分资产,避免同一(作品, 版本)的两份资产互相覆盖或串用读取缓存。对象键只在服务端使用,投影里只回 `projectBundleBytes` / `projectBundleSha256`。
### 3.3 状态与流转
@@ -284,7 +285,7 @@ pub(crate) project_bundle_sha256: Option<String>,
| --- | --- | --- | --- | --- | --- |
| game | `fork_authorization` | `forbidden` / `nonCommercial` / `full` | 只升不降(可跳级) | owner | `expectedForkAuthorization` 值 CAS |
| lineage | 全字段 | 创建即不可变 | 无 | 创建 game 时 | 主键冲突 → 409 |
| version | `project_bundle_*` | 有 / 无 | ① 随版本创建时确认;② 仍为空时,可对**当前公开版本**补传一次。写入后内容不可再改 | owner | 与 `confirm_package` 同一幂等键族 |
| version | `project_bundle_*` | 有 / 无 | 只在 `awaiting_upload` / `upload_failed` 时可写一次(未公开前);写入后不可再改,换内容 → 409 | owner | 与 `confirm_package` 同一幂等键族 |
**血缘建立的服务端校验(全部失败关闭)**:
@@ -312,16 +313,17 @@ pub(crate) project_bundle_sha256: Option<String>,
| 方法 / 路径 | 说明 |
| --- | --- |
| `PUT /games/{gameId}/fork-authorization`(新) | body `{ expectedForkAuthorization, forkAuthorization }` + `Idempotency-Key`;只允许提升;返回最新 `forkAuthorization` 与 `replayed` |
| `PUT /versions/{versionId}/project-bundle`(新) | `application/octet-stream`,整包或复用现役分片族(`upload-state` / `chunk` / `complete`);服务端校验 zip 门禁后写 OSS 并确认。前置:调用者是该版本作者,且该版本尚**没有**工程包;**发布阶段**上传只要求版本归属,**补齐**场景额外要求该版本是作品当前公开版本且 `fork_authorization != forbidden` |
| `PUT /versions/{versionId}/project-bundle`(**M2b 已实现**) | `application/octet-stream` 整包一次上传(≤ 200 MiB);另有分片族 `GET …/project-bundle/upload-state`、`PUT …/project-bundle/chunk`(偏移头 `x-genarrative-upload-offset`)、`POST …/project-bundle/complete`(服务端独立跑工程包 zip 门禁 + 算摘要 + 确认)、`POST …/project-bundle/reset`(丢弃未确认的暂存对象)。前置:调用者是该版本作者;版本处于 `awaiting_upload` / `upload_failed`;该版本**尚无**已确认的工程包(换内容 → 409)。对象键由服务端派生(`…/{version_id}.project.zip`),不接受客户端指定 |
| `POST /games`(**既有,请求增量**) | 追加可选 `forkedFromGameId` / `forkedFromVersionId`;再追加可选 `forkAuthorization`(`forbidden` / `nonCommercial` / `full`,**缺省禁止共创**,非法取值整请求 400 + 平台信封),使作者**上架时**即可选择授权档位,不必事后提升 |
#### 登录用户(Bearer,**不叠加**发布灰度)
灰度只针对「发布」,改编不应当被发布开关挡住:任何已登录用户都能改编已授权公开的作品。两个接口共用同一条校验(同一个函数,规则不分叉),错误码沿用 `FORK_*`:
灰度只针对「发布」,改编不应当被发布开关挡住:任何已登录用户都能改编已授权公开的作品。三个接口共用同一条校验(同一个函数,规则不分叉),错误码沿用 `FORK_*`:
| 方法 / 路径 | 说明 |
| --- | --- |
| `GET /games/{gameId}/fork-source`(**M2a 已实现**) | 受鉴权元数据。404 `FORK_SOURCE_NOT_FOUND`(作品不存在)/ 409 `FORK_SOURCE_NOT_AVAILABLE`(已软删除、未公开、或没有当前公开版本)/ 403 `FORK_NOT_AUTHORIZED`(授权为 `forbidden` 或未知档位,未知按禁止解释);通过时返回 `{ forkSource: { gameId, versionId, source, sha256, bytes, downloadPath } }`。`sha256` / `bytes` 取自版本行已存的发行包摘要(不重算),`source` 当前恒为 `package`(M2b 有工程源包时优先 `project`),`downloadPath` 是同源相对路径,**绝不下发 OSS 对象键** |
| `GET /games/{gameId}/fork-source`(**M2a 已实现**,M2b 增补 `source`) | 受鉴权元数据。404 `FORK_SOURCE_NOT_FOUND`(作品不存在)/ 409 `FORK_SOURCE_NOT_AVAILABLE`(已软删除、未公开、或没有当前公开版本)/ 403 `FORK_NOT_AUTHORIZED`(授权为 `forbidden` 或未知档位,未知按禁止解释);通过时返回 `{ forkSource: { gameId, versionId, source, sha256, bytes, downloadPath } }`。**`source` 现在可能是 `project`(该版本有工程源包时优先)或 `package`**;`sha256` / `bytes` 取自**所选资产**在版本行上的摘要(不重算),`downloadPath` 是同源相对路径并指向对应资产(`…/fork-source/project` 或 `…/fork-source/package`),**绝不下发 OSS 对象键** |
| `GET /games/{gameId}/fork-source/project`(**M2b 已实现**) | 取件本体(源码级):与 `/fork-source/package` 同一套鉴权与校验(同一个函数),仅当该作品当前公开版本**确有工程包**时才服务,否则 `409 FORK_SOURCE_NOT_AVAILABLE`(**失败关闭**,绝不悄悄回落成品包)。响应头与成品包同形,文件名为 `{gameId}-{versionId}-project.zip`;不做引用归一化 / `RELEASE_STORAGE_BOOTSTRAP` 注入(下发原始源码包),读取走同一条 OSS 读路径但**缓存键含资产维度**(对象键不同,两份资产不串味) |
| `GET /games/{gameId}/fork-source/package`(**M2a 已实现**) | 取件本体:直接回该版本发行包 ZIP 的字节,头为 `Content-Type: application/zip`、`Content-Length`、`Content-Disposition: attachment; filename="{gameId}-{versionId}.zip"`、`Cache-Control: no-store`。数据复用现役发行网关的读包路径(整包进内存 + 进程内缓存,上限 4 条 / 256 MiB),**不做**网关那套引用归一化与 `RELEASE_STORAGE_BOOTSTRAP` 注入——下发的是原始构建产物,客户端要按摘要校验后离线解压,任何改写都会让摘要对不上 |
#### 后台(admin)
@@ -354,10 +356,11 @@ pub(crate) project_bundle_sha256: Option<String>,
#### 3.5.2 工程源包路径(源码级复刻,现为正解路径)
- **打包(AGC)**:新增 Rust 侧 `project_bundle` 模块,复用模板包门禁:拒绝 `.agent` / `.git` / `.svn` / `node_modules` 段(任意层级)与根 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode`;条目数 ≤4096;单文件 ≤256 MiB;解压总量 ≤512 MiB;条目排序 + 固定时间戳保证同内容同摘要。
- **上传(AGC)**:仅当作者选择的授权不是 `forbidden` 时上传(默认不上传,省流量且避免无谓的源码外发)。上传失败不影响发布,但要在发布面板明确提示「未上传工程包,你的作品只能被他人试玩与参考(不能直接重新发布)」。
- **下载与建项(AGC,复用现役基座)**:走同一条 `GET /games/{gameId}/fork-source` → 校验 `bundleSha256` 与 `bundleBytes` → 解压到 `<app_data>/forks/<gameId>/<versionId>/`(与模板安装同一套路径门禁、同一套私有 DACL 写入)→ `create_project_from_installed_template_at` 建到用户工作区 → `manifest` 写入 `forkedFrom`。
- **优先级**:同一版本同时存在工程源包与成品包时,`fork-source` 优先返回工程源包,并在响应里标明 `source: 'project' | 'package'`,由客户端决定建项形态。
- **打包(AGC,已落地)**:客户端 `project_bundle` 模块做**确定性打包**(条目排序 + 固定时间戳,同内容同摘要),排除 `.agent` / `.git` / `.svn` / `node_modules` 段(任意层级)与根 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode`;客户端自身另有总量上限(512 MiB)。
- **服务端复核(已落地,**不信任客户端**)**:`module-game-distribution/src/project_bundle.rs` 的 `validate_project_bundle_zip` 独立校验上传内容——上限与发行包**逐项相等**(压缩包 200 MiB / 展开 500 MiB / 单文件 64 MiB / 条目 10 000 / 压缩比 100,理由是两者共用同一条上传链路,反代与 Pingora 的放行量就是按 200 MiB 校准的);额外拦凭据与隐私文件(`.env`、`.env*`、`*.pem`、`*.key`、`*.p12`、`*.pfx`、`.npmrc`、`.netrc`、`.git-credentials`、`id_rsa*`、`id_ed25519*`、`*.map`)、嵌套 `.zip`、符号链接、加密条目与不安全路径;**不要求**根 `index.html`(源码包没有入口约定)。
- **上传(AGC,接线待做)**:走 §3.4 的 `PUT /versions/{versionId}/project-bundle`(或分片族)。只在版本尚未公开时可写一次;**可选**——上传失败不影响发布与审核,但要在发布面板明确提示「未上传工程包,你的作品只能被他人试玩与参考(不能直接重新发布)」。是否有工程包**不改变**授权是否开放(见 §3.2.3 的产品口径)。
- **下载与建项(AGC,接线待做)**:`fork-source` 返回 `source: 'project'` 时走 `GET /games/{gameId}/fork-source/project` → 校验 `sha256` 与 `bytes` → 解压到 `<app_data>/forks/<gameId>/<versionId>/`(与模板安装同一套路径门禁与私有 DACL 写入)→ 建到用户工作区 → 写 `manifest.forkedFrom`。已落地的客户端半段只有确定性打包器。
- **优先级(已落地)**:同一版本同时存在工程源包与成品包时,`fork-source` 优先返回工程源包并标 `source: 'project'`;只有工程包缺失(或行上「字节数 > 0 但摘要为空」这种半写状态)才回落 `package`,客户端据此决定建项形态。
#### 3.5.3 发布时声明(两条路径共用)
@@ -367,6 +370,8 @@ AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom`
> 本节是**待拍板材料**,不是定稿合同;定稿前不得据此实现。结论来自 2026-10-04 的只读侦察(材料:`local://m2a-design.md`),它否掉了原方案「成品包铺进项目即可试玩与发布」的假设(事实依据见 §2.5 与 §3.5.1)。
**进展(2026-10-05)**:产品已采纳下面的推荐口径(**不做**独立的「源码可见性」开关;作者不传工程包即退化产物级改编;`nonCommercial` 的约束力是平台规则层面)。路线 **B 的服务端已实现**(§3.2.3 三列 + `module-game-distribution` 的工程包校验器 + 上行/下行路由族,见 §3.4),AGC 侧已落地确定性打包器,**上传与建项接线待做**;A 作为默认路径继续服务未上传工程包的作品;C 不做。
三条候选路线:
| 路线 | 用户拿到什么 | 能不能发布 | 对「一键复刻完整工程」的兑现度 | 工作量 |