diff --git a/docs/README.md b/docs/README.md index 07185677f..1c2993a6f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,6 +24,7 @@ - [后台游戏评价管理合同](./【玩法创作】平台入口与玩法链路-2026-05-15.md#后台游戏评价管理合同):查找、分页、隐藏/恢复/删除、必填原因、统计与个人状态联动;已实现并通过本地验证,待用户验收,未部署。 - [后台游戏评价管理里程碑](./project-memory/plans/【里程碑】后台游戏评价管理-2026-10-01.md)与[实施计划](./project-memory/plans/【实施计划】后台游戏评价管理-2026-10-01.md):单里程碑范围、接口/schema 边界及验收要求;本地证据已回写主规范。 - [游戏广场评分展示合同](./【玩法创作】平台入口与玩法链路-2026-05-15.md#游戏广场评分展示合同)、[里程碑](./project-memory/plans/【里程碑】游戏广场评分展示-2026-10-01.md)与[实施计划](./project-memory/plans/【实施计划】游戏广场评分展示-2026-10-01.md):已实现并通过本地定向验证,待用户验收,未部署;公开列表/详情携带真实摘要,卡片显示一位小数均分与人数,复用有效评价统计。 +- [游戏共创与作品 Fork](./【技术方案】游戏共创与作品Fork-2026-10-03.md):共创授权三态(只升不降)、作品级血缘与代际、成品包与工程源包两条改造路径、族谱树与溯源署名的产品设计与技术方案;同时作为该功能主规范,已评审通过,按 M1 逐里程碑实现。 - [外部 OpenAPI 与 API Key 接入方案](./【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md) - [外部 MCP 语义工具说明与参数设计](./technical/【技术方案】外部MCP语义工具说明与参数设计-2026-09-23.md):15 个新增语义工具与全部原工具并存,复用现有 External API;包含工具说明、action、参数、幂等和兼容合同。 - [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json):公开 HTTP 契约唯一机器可读来源。 diff --git a/docs/project-memory/plans/【实施计划】游戏共创授权与血缘-2026-10-04.md b/docs/project-memory/plans/【实施计划】游戏共创授权与血缘-2026-10-04.md new file mode 100644 index 000000000..aaa2fbb84 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】游戏共创授权与血缘-2026-10-04.md @@ -0,0 +1,98 @@ +# 【实施计划】游戏共创授权与血缘 + +| 字段 | 值 | +| ------------- | ------------------------------------------------------------------------ | +| Version | 1.0 | +| Status | implemented-local(代码与静态门禁已过;运行时端到端与浏览器验证待补) | +| Date | 2026-10-04 | +| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` | +| Milestone | `docs/project-memory/plans/【里程碑】游戏共创授权与血缘-2026-10-03.md` | +| 授权默认值 | `forbidden`(保守取值,与需求文档一致;如需改默认允许,改一个常量即可) | +| 分支 / 工作树 | `feat/game-fork` / `.worktrees/feat/game-fork` | + +## 修改边界(逐文件) + +### 1. `server-rs/crates/spacetime-module/src/game_distribution.rs` + +- `GameDistributionGame` 表尾追加 `fork_authorization: String`,`#[default("forbidden".to_string())]`。 +- 新增表 `GameDistributionLineage`(accessor `game_distribution_lineage`): + - `game_id` PK;`owner_user_id` / `parent_game_id` / `root_game_id` 三个 btree 索引。 + - 字段:`game_id, owner_user_id, parent_game_id, parent_version_id, root_game_id, generation, created_at`。 +- 新常量:`GAME_DISTRIBUTION_FORK_AUTHORIZATION_{FORBIDDEN,NON_COMMERCIAL,FULL}`、`GAME_DISTRIBUTION_ACTION_SET_FORK_AUTHORIZATION`。 +- `GameDistributionCreateGameInput` 追加 `forked_from_game_id: Option`、`forked_from_version_id: Option`。 +- 新增 `GameDistributionSetForkAuthorizationInput { game_id, owner_user_id, fork_authorization, expected_fork_authorization, idempotency_key, request_digest, now_micros }`。 +- `create_game_distribution_game_tx`:在「新建分支」写 game 之后、写收据之前校验并写血缘行(幂等回放分支与 `local_project_id` 复用分支都不写血缘;复用分支带血缘直接报错)。 +- 新增 `set_game_distribution_fork_authorization_tx` + procedure `set_game_distribution_fork_authorization_and_return`。 +- 快照:`GameDistributionGameSnapshot` 加 `fork_authorization`;公开快照加 `fork_authorization` / `fork_count` / `lineage`(父作品摘要 + 代际 + 根);后台快照加 `fork_authorization` / `generation` / `forked_from_game_id` / `derived_count`。 +- 血缘校验函数 `validate_game_distribution_fork_declaration_tx(ctx, owner, parent_game_id, parent_version_id) -> Result<(root, generation, parent_owner), String>`。 + +### 2. `server-rs/crates/spacetime-module/src/migration.rs` + +- 在 `game_distribution_game` 之后追加 `game_distribution_lineage`,并补一行注释说明它是业务事实、随迁移导出。 + +### 3. `server-rs/crates/module-game-distribution/src/{domain,errors,lib}.rs` + +- 授权阶梯规则(只升不降)与错误:`FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED`、`FORK_AUTHORIZATION_UNKNOWN`、`FORK_NOT_AUTHORIZED`、`FORK_SOURCE_NOT_AVAILABLE`、`FORK_SOURCE_VERSION_MISMATCH`、`FORK_DECLARATION_ON_EXISTING_GAME`(展示文案中文,错误码进 details)。 +- 代际计算:`next_generation(parent_generation) = parent_generation + 1`;根判定。 +- 配套单测。 + +### 4. `server-rs/crates/spacetime-client/src/**` + +- `game_distribution.rs`:创建输入 record 加血缘字段;新增 `set_game_distribution_fork_authorization`。 +- `active/mapper/game_distribution.rs`:`GameDistributionGameRecord` 加 `fork_authorization`;公开 record 加 `fork_count`/`lineage`;admin record 加 `generation`/`forked_from_game_id`/`derived_count`;新增血缘 record 与映射。 +- `module_bindings/**` 由 `npm run spacetime:generate` 生成,禁止手改。 + +### 5. `server-rs/crates/shared-contracts/src/game_distribution.rs` + +- `GameDistributionCreateGameRequest` 追加 `fork: Option`(`parentGameId` / `parentVersionId`)。 +- 新增 `GameDistributionSetForkAuthorizationRequest { expectedForkAuthorization, forkAuthorization }`。 +- 游戏响应加 `forkAuthorization`,公开响应加 `forkCount` / `lineage` / `forkSourceAvailable`。 + +### 6. `server-rs/crates/api-server/src/modules/game_distribution.rs` + +- `protected` 路由新增 `PUT /api/game-distribution/games/{game_id}/fork-authorization`。 +- `create_game` 校验并传递血缘声明。 +- `public_game_payload` / `game_payload` / `admin_game_payload` 增量字段。 +- `map_spacetime_error` 增加血缘/授权关键字的映射分支(放在 `不匹配` 分支之前,避免子串碰撞)。 +- 定向测试(沿用 `app.rs` 里的分发测试 harness)。 + +### 7. 网页端 + +- `packages/shared/src/contracts/gameDistribution.ts`:`GameDistributionGame` / 创建请求 / 新授权请求类型。 +- `src/services/gameDistributionClient.ts`:`updateGameForkAuthorization`(照 `unpublishGame` 写)。 +- `src/components/game-distribution/GameDetailPage.tsx`:授权徽章、溯源卡、代际。 +- `src/components/game-distribution/MyGamesPage.tsx`:卡片内授权设置(行内两段式确认,复用现有 `PlatformActionButton` 视觉),只升不降。 +- 定向测试:`GameDistributionPages.test.tsx` / `MyGamesPage.test.tsx` 增量用例。 + +### 8. 文档 + +- 主规范 `§5 验收证据` 回填、里程碑验收勾选;`docs/README.md` 索引已登记。 + +## 顺序 + +1. schema(表 + 列 + 迁移登记)→ `npm run spacetime:generate` → `npm run check:spacetime-schema`。 +2. 领域规则与单测(`module-game-distribution`)。 +3. module tx/procedure + client facade + mapper。 +4. shared-contracts DTO + api-server handler/payload/错误映射 + 后端定向测试。 +5. 网页端类型、服务层、详情页、我的作品页 + 定向测试。 +6. 运行时 smoke:真实栈走「设置授权 → 提升 → 拒绝降级 → 声明血缘 → 详情页展示 → 父作品下架后新声明被拒」。 + +## 验证命令 + +```bash +npm run spacetime:generate +npm run check:spacetime-schema +cargo test -p module-game-distribution +cargo test -p api-server +npx tsc --noEmit +npx vitest run src/components/game-distribution +npm run check:encoding +git diff --check +``` + +## 风险与回滚点 + +- **回滚点 1(schema)**:新列与新表只做追加,旧行默认 `forbidden`;回滚只需还原表定义并重新发布(新表直接删表定义)。 +- **风险**:`map_spacetime_error` 靠子串匹配,新错误文案若含「不匹配」「已存在」「状态」会被打成 409 —— 必须在映射表前面显式加分支。 +- **风险**:新增血缘校验若放在 `local_project_id` 复用分支之后,会放过「既有作品改判成衍生作品」;必须放在复用分支内判定。 +- **风险**:公开 payload 不得回传对象键;血缘只回传作品摘要与代际。 diff --git a/docs/project-memory/plans/【里程碑】作品工程源包与一键改造-2026-10-03.md b/docs/project-memory/plans/【里程碑】作品工程源包与一键改造-2026-10-03.md new file mode 100644 index 000000000..6c3a11b04 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】作品工程源包与一键改造-2026-10-03.md @@ -0,0 +1,53 @@ +# 【里程碑】作品工程源包与一键改造 + +| 字段 | 值 | +| ----------- | ----------------------------------------------------- | +| Version | 1.0 | +| Status | proposed | +| Date | 2026-10-03 | +| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` | + +## 目标 + +作者在授权允许时,把工程源码作为该发行版本的可选伴随资产一起发布;其他用户拿到的是**源码级工程**,可以改核心逻辑、重跑构建并发布,改造质量与原作者体验一致。 + +## 范围 + +- 工程源包作为发行版本的可选伴随资产:上传、确认、只读回读与对象存储生命周期。 +- 工程源包的内容门禁(与模板包同一套排除规则与体积上限),服务端独立复核。 +- 改造内容下发入口的来源优先级:同一版本同时存在工程源包与成品包时优先下发工程源包,并如实标注来源类型。 +- 客户端:源码工程打包 → 上传(含中断续传);下载 → 校验摘要 → 解压 → 以源码工程形态建项。 +- 发布面板中「是否公开工程」的选择与后果说明(默认不公开)。 + +## 不在范围内 + +- 成品包路径的改造闭环(见「成品包改造闭环」里程碑;本里程碑复用其入口、血缘声明与建项基座)。 +- 工程源包的版本回溯(历史版本没有工程包时不为它补做)。 +- 网页端上传工程源码包(首期只支持客户端)。 +- 相似度比对与低改动度判定、收益分成。 + +## 依赖与前置条件 + +- 「游戏共创授权与血缘」里程碑已通过验收。 +- 「成品包改造闭环」里程碑已通过验收(内容下发入口、客户端建项基座、发布携带来源、授权面板均在那一里程碑落地)。 +- 客户端已有的「从包安装并建项」基座与包内容门禁可直接复用;本里程碑不新建第二套解压或建项路径。 +- 决策:本里程碑成立的前提是产品同意引入工程源包;若否决,本里程碑取消,改造能力止于产物级。 + +## 验收标准 + +- [ ] 授权为「禁止共创」时不产生任何工程包上传,也不出现可被他人下载的工程地址。 +- [ ] 含被排除目录(版本控制、依赖目录、编辑器与构建产物)或含绝对路径、上级路径、盘符、反斜杠、符号链接、超出条目数或体积上限的工程包在上传确认阶段被拒绝,且不产生任何对象或残留。 +- [ ] 同一版本重复确认同一工程包不产生第二份对象;换内容重传按冲突拒绝。 +- [ ] 上传中断后可按权威偏移续传,不重放整包、不跳段。 +- [ ] 同一版本同时存在两种来源时,下发入口优先返回工程源包并如实标注来源类型;未上传工程包时仍能拿到成品包。 +- [ ] 客户端在真实环境中完成源码工程形态建项:新项目可打开、可编辑源码、可试玩、可重跑构建并发布。 +- [ ] 通过源码路径发布的作品,其来源、代际与根与成品包路径完全一致,不产生第二套血缘语义。 +- [ ] 工程包未上传或上传失败不阻断发布,但界面必须明确告知改造能力被降到产物级。 +- [ ] 作品公开后提升授权,可对**当前公开版本**补传工程包一次;补齐后改造路径升级为源码级,无需作者发新版本。 +- [ ] 再次补传、换内容重传被拒绝;目标版本不是当前公开版本、或作品授权仍为禁止时,补传同样被拒绝;历史版本不会被补齐。 + +## 证据要求 + +- 自动化:打包排除规则与体积上限的客户端定向测试、上传确认与冲突用例、服务端门禁与鉴权拒绝用例、来源优先级与标注用例、DTO 一致性与 schema 检查、编码与文档索引检查。 +- 运行时:真实 api-server + 真实对象存储 + 真实客户端跑通「拿到源码 → 改核心逻辑 → 试玩 → 发布 → 溯源与代际正确」,附对比截图或录屏。 +- 边界:越权下载、无授权下载、来源已下架、摘要不符、超限包、解压失败、上传中断续传、账号切换后的迟到响应、同版本双来源优先级。 diff --git a/docs/project-memory/plans/【里程碑】创作族谱与衍生列表-2026-10-03.md b/docs/project-memory/plans/【里程碑】创作族谱与衍生列表-2026-10-03.md new file mode 100644 index 000000000..a96bf9aa2 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】创作族谱与衍生列表-2026-10-03.md @@ -0,0 +1,48 @@ +# 【里程碑】创作族谱与衍生列表 + +| 字段 | 值 | +| ----------- | ----------------------------------------------------- | +| Version | 1.0 | +| Status | proposed | +| Date | 2026-10-03 | +| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` | + +## 目标 + +任意作品的读者都能看到以母版为顶的创作族谱树,并从任一节点进入对应作品;作者能在自己的作品页看到「被改编」的直接衍生作品列表。 + +## 范围 + +- 族谱读取:以某个作品的根为顶,返回各代节点(标题、作者、代际、父作品、公开状态、游玩数),按代际与创建时间稳定排序,超出上限截断并如实标注。 +- 网页新增族谱页面与路由,含根节点、分支、当前作品高亮、节点跳转、空态与失败态。 +- 作品详情页的族谱入口。 +- 我的作品页的「被改编」入口与直接衍生作品列表弹层。 +- 来源作品已下架或封禁时节点的降级展示。 + +## 不在范围内 + +- 共创主题(平台命名的归组实体)与广场共创分区。 +- 族谱的排序算法、热度权重、推荐位。 +- 树的可视化交互增强(缩放、拖拽、导出图片)。 +- 收益或流量回馈的任何计算。 + +## 依赖与前置条件 + +- 「游戏共创授权与血缘」里程碑已通过验收。 +- 「成品包改造闭环」里程碑产生的真实数据用于端到端验证(仅展示层不依赖它,但真实链路验证需要)。 + +## 验收标准 + +- [ ] 三层链路作品打开族谱页,根节点为母版,各节点代际与父作品关系正确,点击任意节点进入对应详情页。 +- [ ] 未登录用户可以浏览族谱页,不触发登录门禁;已下架但仍有血缘的节点保留在树上并标注原作品不可用,不出现空白或断链。 +- [ ] 超出节点上限时如实标注已截断,不静默丢弃;空族谱(无任何衍生)给出明确空态而不是报错。 +- [ ] 我的作品页「被改编」列表只包含直接衍生作品,数量与族谱树中该作品的子节点一致。 +- [ ] 族谱与衍生列表的公开数据不泄露未公开作品、被隐藏内容或对象键。 +- [ ] 灰度未命中时族谱入口不渲染,直接访问路由给出与现役未知路径一致的降级行为。 +- [ ] 桌面与移动端布局可用,长标题与较大数字不撑破容器。 + +## 证据要求 + +- 自动化:族谱与衍生列表的排序、截断、可见性过滤定向测试;路由与前端组件定向测试;DTO 一致性与编码、文档索引检查。 +- 运行时:真实数据上打开族谱页与「被改编」列表,包含一条来源已下架的链路;桌面与窄屏浏览器验证跳转与布局。 +- 边界:无衍生作品的空族谱、超上限截断、来源下架、未公开子作品不出现在树中、未登录访问。 diff --git a/docs/project-memory/plans/【里程碑】成品包改造闭环-2026-10-03.md b/docs/project-memory/plans/【里程碑】成品包改造闭环-2026-10-03.md new file mode 100644 index 000000000..adcb08940 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】成品包改造闭环-2026-10-03.md @@ -0,0 +1,50 @@ +# 【里程碑】成品包改造闭环 + +| 字段 | 值 | +| ----------- | ----------------------------------------------------- | +| Version | 1.0 | +| Status | proposed | +| Date | 2026-10-03 | +| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` | + +## 目标 + +用户能在作品详情页一键把别人的已公开作品拿到本地、改完再发布,且溯源自动正确。整条链路**复用平台已存在的成品包**,作者侧不需要任何新增上传动作。 + +## 范围 + +- 受鉴权的改造内容下发入口(先只服务成品包):只对满足授权与公开性条件的请求开放,不下发可直接匿名访问的对象地址。 +- 客户端:下载 → 摘要校验 → 解压 → 以「静态可玩入口」形态在本机建成新项目 → 项目内记录来源并在界面展示。 +- 从平台作品唤起客户端的入口。 +- 发布链路自动携带来源声明,不要求用户手填。 +- 客户端发布面板的共创授权选择与相应提示。 +- 能力边界的如实告知:基于已构建成品时,界面必须说明可改范围,不能让用户误以为拿到源码。 + +## 不在范围内 + +- 工程源包的上传、下载与源码形态建项(见「作品工程源包与一键改造」里程碑)。 +- 网页端手填来源声明。 +- 创作族谱页面(见「创作族谱与衍生列表」里程碑)。 +- 相似度比对、收益分成、游玩次数上报。 + +## 依赖与前置条件 + +- 「游戏共创授权与血缘」里程碑已通过验收(授权与血缘的数据合同、状态机、灰度入口)。 +- 客户端现有的「从包安装并建项」基座、解压路径门禁与「已有可玩入口直接打包」能力可直接复用,不新建第二套解压或建项路径。 + +## 验收标准 + +- [ ] 授权为「禁止共创」的作品不出现改造入口;绕过界面直接请求内容入口同样被拒绝。 +- [ ] 未登录、无授权、来源作品未公开或已下架时,内容入口全部拒绝,且不泄露摘要与地址。 +- [ ] 客户端在真实环境中完成「详情页 → 唤起客户端 → 下载 → 建项」:摘要校验失败时不留半成品目录,成功时新项目可直接试玩。 +- [ ] 由该路径建成并发布的作品,详情页显示正确的来源、代际与衍生关系;代际与根由服务端计算,客户端无法伪造或覆盖。 +- [ ] 未携带来源声明的发布路径(旧客户端、网页端)不产生血缘,也不因此报错。 +- [ ] 账号切换或退出后,迟到的下载与建项响应不得写入任何本地项目或项目来源信息。 +- [ ] 移动端不出现需要桌面端才能完成的改造动作,或明确给出桌面端提示。 +- [ ] 灰度未命中时改造入口不渲染,写接口返回服务不可用。 + +## 证据要求 + +- 自动化:内容入口鉴权与可用性拒绝用例、客户端下载/摘要校验/解压门禁/建项的定向测试、发布链路携带来源的用例、DTO 一致性与编码、文档索引检查。 +- 运行时:真实 api-server + 真实对象存储 + 真实客户端跑通「改别人的已公开作品 → 试玩 → 发布 → 溯源与代际正确」,附对比截图或录屏。 +- 边界:越权下载、无授权下载、来源已下架、摘要不符、解压失败、账号切换后的迟到响应、旧客户端发布不产生血缘。 diff --git a/docs/project-memory/plans/【里程碑】游戏共创授权与血缘-2026-10-03.md b/docs/project-memory/plans/【里程碑】游戏共创授权与血缘-2026-10-03.md new file mode 100644 index 000000000..ddaedb17a --- /dev/null +++ b/docs/project-memory/plans/【里程碑】游戏共创授权与血缘-2026-10-03.md @@ -0,0 +1,57 @@ +# 【里程碑】游戏共创授权与血缘 + +| 字段 | 值 | +| ----------- | ----------------------------------------------------- | +| Version | 1.0 | +| Status | implemented-local(本地实现完成;运行时端到端与浏览器验证待补) | +| Date | 2026-10-03 | +| Parent Spec | `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md` | + +## 目标 + +作者可以对自己的作品设置「共创授权」,之后只能单向提升;所有浏览者能在作品详情页看到授权状态、代际与溯源信息;公开页展示真实作者署名。 + +## 范围 + +- 作品级共创授权三态(禁止共创 / 允许非商用共创 / 允许全开放共创)的持久化、默认值与单向提升规则。 +- 作品级血缘关系(父作品、来源版本快照、根作品、代际)的持久化与查询,创建作品时对来源的完整校验。 +- 详情页:授权徽章、溯源卡(改编自《X》· 由 Y 制作)、代际与衍生数量。 +- 我的作品页:授权三态设置(仅允许提升,终态只读)。 +- 后台游戏管理:授权与代际的只读展示。 +- 公开页作者署名取真实账号资料,不再落到兜底文案。 +- 端到端灰度开关与前端入口联动。 + +## 不在范围内 + +- 改造内容的下发与客户端建项(成品包路径见「成品包改造闭环」里程碑,源码路径见「作品工程源包与一键改造」里程碑)。 +- 创作族谱页面与衍生作品列表(见「创作族谱与衍生列表」里程碑)。 +- 共创主题(平台命名的归组实体)。 +- 收益分成、流量回馈、相似度反洗稿校验。 +- 游玩次数上报。 + +## 依赖与前置条件 + +- 主规范 §3 的数据模型与状态机已评审通过。 +- 决策:授权默认值取「禁止共创」还是「允许」需产品拍板;本里程碑的实现按拍板结果设定默认值,其余合同不变。 +- 决策:是否引入共创主题实体需产品拍板;本里程碑不依赖该实体。 + +## 验收标准 + +- [ ] 新建作品默认授权为拍板结果;迁移前已存在的作品在升级后同样按该默认解释,无需人工回填。 +- [ ] 同一次授权变更里,合法提升(含跳级)成功、任何降级被拒绝且不写库。 +- [ ] 非作品作者不能变更授权;基于旧值的并发请求按期望值冲突拒绝。 +- [ ] 重复提交同一授权变更请求不产生第二次副作用,如实回报为幂等重放。 +- [ ] 声明血缘时:来源作品不存在、未公开、已被封禁、授权为禁止、来源版本不等于来源作品当前公开版本,五类情形全部失败关闭且给出可区分的原因。 +- [ ] 代际与根作品由服务端计算:三层链路(A→B→C)得到 B 为第 1 代、C 为第 2 代,且 B、C 的根作品都是 A。 +- [ ] 血缘不可变:同一作品第二次声明血缘被拒绝;既有作品复用身份的场景不接受血缘声明。 +- [ ] 来源作品下架或封禁后,既有衍生作品保持公开,新的血缘声明被拒绝。 +- [ ] 详情页展示的授权、代际、溯源与衍生数量与后端一致;来源作品不可读时该节点降级展示而非空白。 +- [ ] 公开详情与广场展示真实作者名与头像(读时联账号表),兜底文案不再出现在可正常读取账号的场景;现役公开 DTO 未暴露陶泥号,如需展示须另加字段。 +- [ ] 公开响应不泄露对象键、未公开作品或未公开来源信息。 +- [ ] 发布开关收紧时,共创授权写入返回 `503 GAME_DISTRIBUTION_PUBLISH_DISABLED`;开关状态读取失败按关闭处理。(M1 复用现役 `game-distribution:publish` 开关,不新增独立灰度键。) + +## 证据要求 + +- 自动化:领域规则单测(授权状态机、代际与根计算、血缘校验)、发行模块与 BFF 定向测试、共享 DTO 一致性检查、生成绑定与 schema 检查、编码与文档索引检查。 +- 运行时:真实 api-server + 本地数据库上完成「设置授权 → 提升 → 声明血缘 → 详情页展示 → 父作品下架后新声明被拒」整链 smoke;浏览器在桌面与窄屏检查详情页与我的作品页。 +- 边界:未登录、非作者、期望值不匹配、重复请求、来源作品五种不可用情形、父作品下架前后对比。 diff --git a/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md new file mode 100644 index 000000000..b6bb49195 --- /dev/null +++ b/docs/【技术方案】游戏共创与作品Fork-2026-10-03.md @@ -0,0 +1,464 @@ +# 【技术方案】游戏共创与作品 Fork + +更新时间:`2026-10-03` + +> 状态:`draft`(待评审)。评审通过前不写业务代码。 +> 本文件同时作为「游戏共创」的主规范与技术方案;行为合同部分不得绑定类名、文件名和实现算法。 + +## 0. 结论摘要 + +1. **Fork 关系挂在 `game`,不挂在 `version`。** `game` 是稳定作品身份,`version` 是不可变内容快照;血缘是「作品 ↔ 作品」关系。唯一留在版本级的是**被复刻的内容本身**(已存在的成品包与新增的工程源包都属于版本资产),因为内容随版本演进。 +2. **现状不存在任何 fork/remix/来源/父作品字段或表。** 旧的「作品改造 / Remix」在网页端已整体退役(`vite.config.ts:16-18,20-59,126-140,170-173` 把退役固化成构建门禁),后端只剩 `#[cfg(any())]` 死码(`module-runtime/src/domain.rs:137`、`api-server/src/state.rs:1317`、`shared-contracts/src/runtime.rs:879-880`)与 CSS 死类名。本功能是**从零建**,没有历史包袱,也不复活 Remix 命名。 +3. **必须区分两种 Fork,它们是两层能力而不是一个开关**: + - **成品包 Fork(零新增资产,可立即做)**:每个已发布版本本来就存着构建产物 ZIP(`game_distribution_version.package_object_key`,`:797`),直接拿它建项目就能跑通「下载 → 建项 → 改造 → 发布带溯源」。 + - **工程源包 Fork(真·复刻工程)**:成品包是 Vite 构建产物——模板工程 `vite.config.js` 只配了 `build: { outDir: 'dist' }`,**未关压缩也未开 sourcemap**,且平台发行校验明确拒收 `*.map`(`server-rs/crates/module-game-distribution/src/package.rs:171-175`)。所以 AI 在成品包上改核心逻辑不可靠,要让文档里「用户改造完成后可发布」成立,仍需版本级可选资产**工程源包**。 + - 两者共用同一套血缘模型;成品包路径同时是工程源包缺失时(网页端发布、作者不愿公开源码)的天然降级路径。 +4. **工程源包有现成规范与现成客户端链路可复用**:排除规则见 `docs/【模板规范】AGC模板包组织指南-2026-09-21.md`;下载→校验 SHA-256→解压→建项全链路已存在于 `apps/ai-game-creator-shell/src-tauri/src/template_library.rs`(`create_project_from_installed_template_at` `:816`)。 +5. **本期不做**:创作者收益分成(仓库无收益/分成/结算表,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)、相似度反洗稿校验、上链。 +6. **顺带必修的相关缺口(已在 M1 修复)**:公开 `author.name` 长期落到兜底文案(创建游戏时 `author_name` 写死 `None`,`api-server/src/modules/game_distribution.rs:1001-1003`;公开 payload 兜底「创作者」)。修法:公开快照改为读时联 `user_account`,账号改名/换头像立即跟随,与后台游戏管理页同口径。 + +--- + +## 1. 现状事实(设计依据) + +### 1.1 现役「作品」只有一套:游戏分发 + +| 形态 | 表 | 状态 | +| --- | --- | --- | +| 游戏分发(游戏广场) | `game_distribution_game` / `_version` / `_review` / `_review_moderation_log` / `_idempotency_receipt` | 现役 | +| 编辑器精选素材 | `editor_showcase_asset` / `_like` | 现役,与作品 Fork 无关 | +| 公开玩法作品(custom-world / puzzle / 大鱼 …) | `public_work_like` / `public_work_play_daily_stat` / `profile_played_world` + 各玩法源表 | **写入路径全部 `#[cfg(any())]` 死码**,前端路由与组件已删除 | + +现役路由唯一真相 `src/routing/activeAppPageRoutes.ts:7-18`,作品相关只有: + +``` +/games 游戏广场 src/components/game-distribution/GameGalleryPage.tsx +/games/detail 作品详情 GameDetailPage.tsx +/games/play 游玩(iframe) GamePlayPage.tsx +/games/mine 我的作品 MyGamesPage.tsx +/games/publish 发布 / 发布新版本 GamePublishPage.tsx +``` + +导航:桌面 rail 创作 / 项目 / 游戏 / 我的(`PlatformEntryActiveFlowShell.tsx:653-682`),移动 dock 游戏 / 我的(`:198-207`)。 + +### 1.2 游戏 - 版本模型 + +- `game_distribution_game`(`server-rs/crates/spacetime-module/src/game_distribution.rs:718-764`):`game_id` PK、`owner_user_id`、资料(标题/简介/分类/标签/封面/截图/设备/输入/朝向)、`publication_revision`(公开修订号 CAS)、`active_version_id`(当前公开版本指针)、`visibility`(`unpublished` / `published` / `suspended`)、`play_count`、`local_project_id`(同作者同本地项目复用身份,**不构成所有权证明**)、`cover_object_key`、`screenshots_json`。 +- `game_distribution_version`(`:766-821`):`version_id` PK、`game_id` 索引、`owner_user_id` 索引、`version_number`、包摘要(`package_sha256` / `package_bytes` / `package_file_count` / `package_entry_path`)、`status`、`package_object_key`、`package_manifest_json`、`entry_url`、审核人/阶段时间、`metadata_json`(该版本冻结的作者资料快照,审核通过时整体生效到 game)。 +- 版本状态:`awaiting_upload → uploaded → pending_review → published`,失败终态 `upload_failed / validation_failed / rejected / cancelled / revoked`(常量 `:868-877`,枚举 `module-game-distribution/src/domain.rs:42-53`)。 +- 公开性唯一口径:`visibility == published` 且 `active_version_id` 指向 `status == published` 的版本(`:3335-3346`)。 +- 公开读只有 3 条无鉴权路径:列表、详情、发行网关 `/api/game-distribution/releases/{gameId}[/{*asset}]`(`api-server/src/modules/game_distribution.rs:344-363`)。 +- 写操作全部经受信服务身份 procedure;幂等收据 30 天;所有公开切换走 `publication_revision` CAS。 + +### 1.3 与 Fork 相关的既有能力与缺口 + +| 项 | 现状 | 证据 | +| --- | --- | --- | +| 「能否被 fork」 | **不存在**(旧的 `remixEnabled` 是玩法级死配置) | `module-runtime/src/domain.rs:137`(`#[cfg(any())]`) | +| 「fork 自谁」 | **不存在**。最近似的只有 `local_project_id` 身份复用,语义是同一作者自己的本地项目 | `game_distribution.rs:1700-1727` | +| fork 计数 | **不存在**(`remixCount` 是 `#[cfg(any())]` 退役契约里的死字段,零消费方) | `shared-contracts/src/runtime.rs:922,950` | +| 作者署名 | 字段存在但创建时写死 `None`,公开页兜底「创作者」 | `api-server/.../game_distribution.rs:1001-1003`、`:2390` | +| 作者主页 / 关注 / 粉丝 | **不存在**(无任何社交关系表) | `migration.rs:149-200` 无相关表 | +| 收藏 / 收录 | **不存在**,且文档明确禁止虚构收藏状态 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md:74` | +| 播放次数 | 本分支上 `play_count` 仍**从无递增路径**却被前端展示;PR #565 已新增游玩计数上报 procedure,合并后以 master 结果为准 | `game_distribution.rs:1759`(仅初值);`GameDetailPage.tsx:292` | +| 收益 / 分成 | **不存在**;`PuzzleAuthorIncentiveClaim` 有枚举无写入方 | `module-runtime/src/domain.rs:1147` | +| 从包建项(客户端) | **存在且完整**:下载→SHA-256 校验→解压(拒绝绝对路径/`..`/盘符/反斜杠/符号链接,条目 ≤4096、单文件 ≤256 MiB)→建项→失败删半成品 | `template_library.rs`、`:815-850`、`:240-342` | +| 工程包排除规则 | **存在**:禁止 `.agent` / `.git` / `.svn` / `node_modules` 段与根 `dist` / `build` / `library` / `temp` / `local` / `.idea` / `.vscode` | `docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md`(后台模板上传章节)、`docs/【模板规范】AGC模板包组织指南-2026-09-21.md` | + +--- + +## 2. 产品设计 + +### 2.1 目标 + +让平台上的游戏可以被他人**合法地继续改造**,并让改造形成的世代链路可展示、可追溯、原作者可署名。 + +### 2.2 非目标 + +- 不做收益分成、版税结算、算力成本核算(无账本与口径,属独立议题)。 +- 不做相似度反洗稿校验(无判定标准;本方案只保证**来源声明不可伪造**)。 +- 不做上链;「永久溯源」实现为**不可变的父子链 + 平台持久化事实**。 +- 不做跨作品类型 Fork(现役只有一种作品:游戏分发)。 +- 不做收藏 / 关注 / 粉丝。 + +### 2.3 授权模式(作者侧,作品级) + +作者在上架时可选择,之后**只能单向提升开放度**: + +| 值 | 用户可见文案 | 含义 | +| --- | --- | --- | +| `forbidden`(默认) | 禁止共创 | 仅可游玩,不可复刻改编 | +| `nonCommercial` | 允许非商用共创 | 可二次开发,禁止盈利,仅可公开分享 | +| `full` | 允许全开放共创 | 可改编、可商用、可引流、可发布新版本 | + +状态机(唯一合法迁移): + +```mermaid +stateDiagram-v2 + [*] --> forbidden : 创建作品(默认) + forbidden --> nonCommercial : 作者提升 + forbidden --> full : 作者跳级提升 + nonCommercial --> full : 作者提升 + full --> [*] : 终态(不可降级) +``` + +约束: +- 只有 `owner_user_id` 可以变更;请求必须带 `expectedForkAuthorization`(CAS),不匹配返回 `409`。 +- 任何降级请求一律 `409 FORK_AUTHORIZATION_DOWNGRADE_NOT_ALLOWED`,不写库。 +- 已按旧授权完成的 Fork **不受后续提升影响**(授权在建立血缘时已兑现)。 +- **提升与源码的时序**:作品公开后提升授权时,已发布的版本里没有工程源包,因此允许对**当前公开版本补传一次**工程源包(规则见 §3.2.3)。提升界面对此给出提示,但不作为提升的前置条件。 + +### 2.4 用户视角入口矩阵 + +**玩家 / 未登录** + +| 位置 | 内容 | +| --- | --- | +| `/games` 广场卡片 | `forkedFrom` 存在时显示「改编」角标;不做父子聚合,卡片仍独立展示 | +| `/games/detail` 详情页 | ① 授权徽章(禁止共创 / 允许非商用共创 / 允许全开放共创);② 有父作品时显示溯源卡「改编自《X》· 由 Y 制作」+ 可点进父作品;③ 显示「第 N 代作品」与「N 个衍生作品」;④ `forkAuthorization != forbidden` 时显示主行动作「改造这个作品」(未登录点击走既有登录门禁) | +| `/games/detail` 族谱入口 | 「查看创作族谱」→ `/games/lineage?id=` | +| `/games/lineage`(新页面) | 以根作品为顶的树:根节点、各代分支、每代作品卡(封面 / 标题 / 作者 / 第 N 代 / 游玩数),点击进入详情;父作品已下架时该节点显示「原作品已下架」但仍可点;空态与加载失败按现有平台错误组件 | + +**作者 / 已登录** + +| 位置 | 内容 | +| --- | --- | +| `/games/mine` 每张作品卡 | 新增「共创授权」三态设置(仅允许提升,终态 `full` 时只读);新增「被改编 N」入口,弹层列出直接子代(标题 / 作者 / 代际 / 状态);提升授权后若当前公开版本没有工程源包,行内提示「上传工程源码以支持源码级改造」(上传在桌面端客户端完成,网页端只做引导) | +| `/games/publish`、AGC 发布面板 | 新增「授权共创」三态单选(默认「禁止共创」,页面提示:开启后可被他人复刻改编,开启后不可撤销);从父作品 Fork 而来时显示只读的「改编自《X》」 | +| AGC 客户端 | ① 详情页「改造这个作品」唤起 AGC(deep link `genarrative://fork?gameId=`);② AGC 首页/项目入口提供「从平台作品开始创作」(输入 gameId 或从平台跳转) | + +**后台** + +| 位置 | 内容 | +| --- | --- | +| `#game-management` 游戏管理 | 列表新增「授权」「代际」列;版本历史弹层不变;不做强制改授权(避免平台替作者背授权责任) | +| `#game-distribution` 审核队列 | 允许共创的作品审核通过时,若作者未提供工程源包,标注「仅产物级改造」(不影响发布与审核) | + +### 2.5 两个层级的改造能力(必须让作者和用户都看懂) + +已公开的作品**总是**有成品包,所以「改造这个作品」按钮对所有开放授权的作品都可用;区别只在用户拿到的东西能改到什么程度。 + +| 情形 | 用户拿到什么 | 用户可见文案 | +| --- | --- | --- | +| 父作品有**工程源包**(AGC 发布且作者选择公开源码) | **源码级复刻**:AGC 下载工程 → 解压为新项目 → 可改源码、可重跑构建、可发布 | 「改造这个作品」 | +| 父作品只有**成品包**(网页端发布,或作者未上传工程源包) | **产物级改造**:AGC 以该发行版本的 ZIP 作为项目内容(静态可玩入口、无构建步骤),可改素材、数值、HUD、样式与局部逻辑 | 「改造这个作品」,并在面板注明「基于已构建成品,改核心逻辑建议作者开放工程」 | + +两条路径在发布时写入**完全相同**的血缘与溯源,差别只在可编辑程度;不设「作者必须开源才能被改造」的前置条件。 + +### 2.6 关键流程 + +**A. 作者发起共创** + +```mermaid +sequenceDiagram + participant A as 作者(AGC/网页) + participant API as api-server + A->>API: 发布游戏/版本,携带 forkAuthorization + API->>API: 校验三态合法;写入 game.fork_authorization + A->>API: (AGC 且授权非 forbidden)上传工程源包 + API->>API: 校验 zip(复用模板包门禁)→ OSS → version.project_bundle_* + Note over A,API: 审核通过后作品公开,且可被改造 + A->>API: 后续提升授权(POST fork-authorization,仅升) +``` + +**B. 用户一键改造** + +```mermaid +sequenceDiagram + participant U as 用户(浏览器) + participant AGC as AGC 客户端 + participant API as api-server + U->>U: 详情页点「改造这个作品」 + U->>AGC: deep link genarrative://fork?gameId=X + AGC->>API: GET /api/game-distribution/games/X/fork-source (Bearer) + API-->>AGC: {versionId, bundleSha256, bundleBytes, downloadPath} + AGC->>AGC: 下载 → 校验 SHA-256 → 解压(同模板安装门禁)→ 新建项目 + AGC->>AGC: manifest 写入 forkedFrom{gameId, versionId, rootGameId, generation} + Note over AGC: 用户改造 → 发布 + AGC->>API: POST /games(携带 forkedFromGameId/VersionId) + API->>API: 校验授权/来源版本/计算 generation 与 root → 写 lineage +``` + +**C. 溯源与命名** + +- 溯源卡出现在详情页与 AGC 项目内(项目信息区显示「改编自《X》」)。 +- 代际:根作品 = 第 0 代;直接改编根作品 = 第 1 代;`generation = parent.generation + 1`。 +- 「衍生作品数」= lineage 表按 `parent_game_id` 的计数(实时算,与现有评分实时聚合同口径;不新增物化计数)。 + +--- + +## 3. 技术方案 + +### 3.1 分层与边界 + +```text +src/(网页) 详情页/我的作品/族谱页 ←→ /api/game-distribution/** +apps/ai-game-creator-shell(AGC) + React 展示层 发布面板、改造入口 + Rust facade 工程包打包/上传/下载/解压/建项(复用 template_library 基座) +api-server 鉴权、灰度、OSS 编排、ZIP 校验、payload 组装 +spacetime-client typed facade / mapper +spacetime-module 表、procedure、事务 +module-game-distribution 领域规则(授权状态机、代际计算、血缘校验) +shared-contracts Rust DTO ←→ packages/shared TS DTO +``` + +不新增数据库访问通道;不新增第二套作品系统。 + +### 3.2 数据模型 + +#### 3.2.1 `game_distribution_game` 追加 1 列(表尾,带默认) + +```rust +/// 共创授权等级:forbidden(默认)/ nonCommercial / full。只允许单向提升;不改变不写库。 +#[default("forbidden".to_string())] +pub(crate) fork_authorization: String, +``` + +旧行反序列化自动补 `"forbidden"`,与「默认禁止共创」语义一致。 + +#### 3.2.2 新增 `game_distribution_lineage`(父子血缘,1:1) + +```rust +#[spacetimedb::table( + accessor = game_distribution_lineage, + index(accessor = by_game_distribution_lineage_parent, btree(columns = [parent_game_id])), + index(accessor = by_game_distribution_lineage_root, btree(columns = [root_game_id])), + index(accessor = by_game_distribution_lineage_owner, btree(columns = [owner_user_id])), +)] +pub struct GameDistributionLineage { + /// 子作品;主键即 1:1 约束,一个作品只能有一个父。 + #[primary_key] + pub(crate) game_id: String, + /// 子作品作者,冗余用于「我改编过的作品」列表,不参与授权判定。 + pub(crate) owner_user_id: String, + pub(crate) parent_game_id: String, + /// 建立血缘时父作品的当前公开版本(不可变溯源事实)。 + pub(crate) parent_version_id: String, + /// 0 代母版;根作品自身不写行,读侧以「无行」判定为根。 + pub(crate) root_game_id: String, + /// 代际;直接改编根作品为 1。 + pub(crate) generation: u32, + pub(crate) created_at: Timestamp, +} +``` + +为什么用独立表而不是在 game 上再加 3 列: +- 血缘查询必须按 `parent` / `root` 建索引,而这两列在 game 上只能是 `Option`;本仓从未验证过对 `Option` 列建 btree 索引的行为,不拿 schema 赌。 +- 血缘是 1:1 关系实体,主键承载「一个作品只有一个父」这条不变量。 +- 不污染 game 主行读路径(详情页绝大多数请求不需要血缘行)。 +- 事务一致性:创建 game 与插入 lineage 必须同事务,插入失败整笔回滚。 + +#### 3.2.3 `game_distribution_version` 追加 3 列(表尾,带默认) + +```rust +/// 该版本随包上传的工程源包对象键(不含 node_modules/.git/dist 等,规则见模板包组织指南)。 +#[default(None::)] +pub(crate) project_bundle_object_key: Option, +#[default(0u64)] +pub(crate) project_bundle_bytes: u64, +#[default(None::)] +pub(crate) project_bundle_sha256: Option, +``` + +工程包跟随版本,但保留**一次补齐机会**: + +- **发布时**:仅当该版本所属作品的 `fork_authorization != forbidden` 才上传,与版本创建在同一次确认里完成。 +- **补齐**:作品公开后,作者可以把工程源包补传给它**当前公开版本**,每个版本至多一次,内容摘要写入后不可再改。这是「版本不可变」的唯一例外——理由是工程源包不是发行内容:它不参与试玩、不参与审核、不改包摘要,补传只增加「能否被源码级改造」这一个能力。 +- **不做版本回溯**:作者 v1 传了、v2 没传,那么 v2 只能走产物级改造,历史版本不给补。 +- **补齐前置**:目标版本必须是该作品**当前公开版本**,且该作品 `fork_authorization` 已不是 `forbidden`。 + +对象键:`agc/project-snapshots/v1/game-fork/{game_id}/{version_id}.zip`(与发行包同前缀族,便于生命周期统一)。 + +### 3.3 状态与流转 + +| 对象 | 字段 | 取值 | 合法迁移 | 触发方 | 并发控制 | +| --- | --- | --- | --- | --- | --- | +| game | `fork_authorization` | `forbidden` / `nonCommercial` / `full` | 只升不降(可跳级) | owner | `expectedForkAuthorization` 值 CAS | +| lineage | 全字段 | 创建即不可变 | 无 | 创建 game 时 | 主键冲突 → 409 | +| version | `project_bundle_*` | 有 / 无 | ① 随版本创建时确认;② 仍为空时,可对**当前公开版本**补传一次。写入后内容不可再改 | owner | 与 `confirm_package` 同一幂等键族 | + +**血缘建立的服务端校验(全部失败关闭)**: + +1. `forkedFromGameId` 必须存在 → 否则 `409 FORK_SOURCE_NOT_FOUND`。 +2. 父游戏 `visibility == published`、`deleted_at` 为空,且存在 `status == published` 的当前版本 → 否则 `409 FORK_SOURCE_NOT_AVAILABLE`(下架 / 封禁 / 已软删除作品不可被新 fork,但不影响既有子作品)。 +3. 父游戏 `fork_authorization != forbidden` → 否则 `403 FORK_NOT_AUTHORIZED`。 +4. `forkedFromVersionId` 必须等于父游戏当前公开版本 id → 否则 `409 FORK_SOURCE_VERSION_MISMATCH`(禁止指向历史版本或伪造)。 +5. 同一 `localProjectId` 复用既有 game 的场景**不允许**携带血缘(避免把既有作品改判成衍生作品)→ `409 FORK_DECLARATION_ON_EXISTING_GAME`。 +6. `generation = parent.generation + 1`;`root_game_id = parent 有血缘行 ? parent.root_game_id : parent.game_id`。服务端计算,不接受客户端传入。 + +### 3.4 接口契约 + +所有新增路径沿用 `/api/game-distribution` 命名空间与平台 envelope;作者写路由叠加 Bearer + 发布灰度;读路径 `Cache-Control: no-store`。 + +#### 公开(匿名可读) + +| 方法 / 路径 | 说明 | +| --- | --- | +| `GET /games/{gameId}`(**既有,响应增量**) | 追加 `forkAuthorization`、`forkCount`、`lineage`(可选)、`forkSourceAvailable`(布尔,仅表达"能否真复刻",不泄露对象键) | +| `GET /games/{gameId}/lineage`(新) | 以该 game 的根为顶返回树:`{ root: GameSummary, nodes: [{ gameId, title, author, generation, parentGameId, status, playCount }] }`,按代际与创建时间稳定排序;节点上限 200,超出返回 `truncated: true` | +| `GET /games/{gameId}/derived`(新,可选分页) | 直接子代列表 | + +#### 作者(Bearer + 发布灰度) + +| 方法 / 路径 | 说明 | +| --- | --- | +| `POST /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` | +| `GET /games/{gameId}/fork-source`(新) | 校验授权与来源可用性,返回 `{ gameId, versionId, bundleSha256, bundleBytes, downloadPath }`;`downloadPath` 为受鉴权网关路径,不是可匿名访问的对象键 | +| `POST /games`(**既有,请求增量**) | 追加可选 `forkedFromGameId` / `forkedFromVersionId` | + +#### 后台(admin) + +| 方法 / 路径 | 说明 | +| --- | --- | +| `GET /admin/api/game-distribution/games`(**既有,响应增量**) | 追加 `forkAuthorization` / `generation` / `forkedFromGameId` / `derivedCount`(只读展示) | + +#### 契约同步(强制) + +- Rust DTO:`server-rs/crates/shared-contracts/src/game_distribution.rs` +- TS DTO:`packages/shared/src/contracts/gameDistribution.ts` +- 版本状态枚举如需新增取值,同步 `GameDistributionVersionStatus` 与 `scripts/check-game-distribution-dto-parity.mjs` 的公开构建器约束。 +- 本期不动 `/api/external/v1`,因此不改 External OpenAPI;若后续对外开放 Fork 查询,再按现有门禁同步 OpenAPI。 + +### 3.5 两条改造路径:成品包(先行)与工程源包(补强) + +#### 3.5.1 成品包路径(零新增上传资产) + +- **内容来源**:父作品当前公开版本的 `package_object_key`(已存在,无需作者做任何额外动作)。服务端按现有发行网关同一套对象读取与校验复用该 ZIP,不新造存储。 +- **下载授权**:与工程源包同一条受鉴权入口(`GET /games/{gameId}/fork-source`),返回的是网关路径而非裸对象键;未登录、无授权、来源不可用时一律拒绝。 +- **AGC 建项**:把 ZIP 内容铺进新项目的可玩入口目录(`game/`),项目无需构建步骤即可试玩与发布——这条形态 AGC 本来就支持(`export_local_project_package_for_publish_at` 在已有可玩入口时直接打包,`src-tauri/src/project/export.rs:259-262`)。 +- **能力边界**:可改素材、数值、样式、HUD 与局部逻辑;**不可**重建压缩后的核心逻辑。面板必须如实说明这一点,不能让用户以为拿到了源码。 +- **价值**:零新增资产即可跑通完整闭环(血缘、溯源、代际、族谱),也是工程源包缺失时的降级路径。 + +#### 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` → 解压到 `/forks///`(与模板安装同一套路径门禁、同一套私有 DACL 写入)→ `create_project_from_installed_template_at` 建到用户工作区 → `manifest` 写入 `forkedFrom`。 +- **优先级**:同一版本同时存在工程源包与成品包时,`fork-source` 优先返回工程源包,并在响应里标明 `source: 'project' | 'package'`,由客户端决定建项形态。 + +#### 3.5.3 发布时声明(两条路径共用) + +AGC 发布链路(`game_distribution_publish.rs`)读取 `manifest.forkedFrom` 并写入创建 game 的请求体。网页端**不提供**手填来源,声明只能来自真实下载过的内容,避免伪造血缘;网页端发布的作品因此只能作为父作品,不能作为子作品。 + +### 3.6 前端链路 + +| 文件 | 改动 | +| --- | --- | +| `src/routing/activeAppPageRoutes.ts` | 新增 `['game-lineage', '/games/lineage']`;同步 `SelectionStage` 类型与 `PlatformEntryActiveFlowShell` 的 stage 分支 | +| `src/components/game-distribution/GameDetailPage.tsx` | 授权徽章、溯源卡、「改造这个作品」主行动作、族谱入口 | +| `src/components/game-distribution/GameLineagePage.tsx`(新) | 族谱树页面 | +| `src/components/game-distribution/MyGamesPage.tsx` | 授权三态设置(只升)、「被改编 N」弹层 | +| `src/components/game-distribution/GamePublishPage.tsx` | 「授权共创」三态单选 + 不可撤销提示 | +| `src/services/gameDistributionClient.ts` | 新增 `setForkAuthorization` / `getGameLineage`;`getGame` 类型增量 | +| `apps/ai-game-creator-shell` | 发布面板授权选择;`genarrative://fork` deep link 注册;改造入口与进度/失败提示 | +| `apps/admin-web/src/pages/AdminGameManagementPage.tsx` | 列表新增授权 / 代际列 | + +移动端:移动 dock 只有游戏 / 我的,族谱页与详情页可读;「改造这个作品」在移动端提示「请在桌面端改造」,与现有创作/项目入口的桌面端提示同口径。 + +### 3.7 灰度与可观测 + +- **M1 实现口径**:共创写入复用现役 `game-distribution:publish` 开关(`ensure_publish_enabled`),不新增独立灰度键——本功能与发布能力同批上线,先少一个开关减少漂移面。独立键 `game-distribution:fork`(含 `/api/runtime/frontend-config` 的 `gameDistributionForkEnabled` 与前端入口门禁)作为后续增强保留,接入前需要同时改 `module-runtime`、`api-server` 与前端配置读取。 +- 事件:`game_fork_source_downloaded`、`game_fork_declared`、`game_fork_authorization_updated`,含成功/失败分类;不发对象键与用户隐私字段。 +- **M1 未接埋点**:上述事件在 M1 未实现(当前只有既有 `tracing` 日志),随 M2 的内容下发一起补,避免在无下载能力时先埋无意义事件。 + +### 3.8 兼容与迁移 + +1. `game_distribution_game` / `game_distribution_version` 均为**表尾追加 + 明确默认值**,符合 SpacetimeDB 兼容追加规则;旧行语义 = 禁止共创 / 无工程包。 +2. `game_distribution_lineage` 为新表,初始为空;根作品的「0 代」由「无血缘行」表达,不需要回填。 +3. `migration.rs` 的 `migration_tables!` 白名单登记新表,并在注释中说明「血缘是业务事实,随迁移导出/导入;工程包正文在对象存储,不进入表」。 +4. 必须运行 `npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:game-distribution-dto-parity`、`npm run check:generated-bindings`。 +5. 不改删除 / 改名 / 重排 / 类型;若实施中发现必须如此,先停下来确认迁移计划。 + +### 3.9 与「作品管理」改动的关系(PR #565) + +`feat/game-works-management`(PR #565,作品管理与 Phaser4 客户端发布)与本功能改同一批文件,且引入了三处**语义**影响,落地时必须按其口径收敛: + +| PR #565 带来的变化 | 对本功能的影响 | +| --- | --- | +| `game_distribution_game` 追加 `deleted_at`,软删除后对该作品一律按「不存在」处理(作者视图、公开目录/详情、发行网关、后台默认列表全部下线,版本行与冻结资料保留) | ① 血缘来源校验增加「父作品未软删除」;②「衍生作品数」只统计**未删除**的子作品;③ 已被软删除的父作品是既有子作品的可见来源,但溯源只展示「原作品已不可用」,**不展示被删作品标题**,避免软删除语义被血缘绕过 | +| 游戏行同时被两条分支在表尾追加列(`deleted_at` 与 `fork_authorization`) | 合并后两列并存即可;SpacetimeDB 自动迁移按列补齐,旧行各自取默认值 | +| `play_count` 由新增的游玩计数上报变成**活字段** | 本功能不依赖它;主规范 §1.3 里「死字段」的结论在 #565 合并后失效,以合并结果为准 | + +同一批重叠文件(24 个)里多数是**机械冲突**(同一函数/同一 `json!` 块/同一枚举块各加一段),唯一需要重新生成的是 `spacetime-client/src/module_bindings/**`:合并后必须重跑 `npm run spacetime:generate`,不得手工合并生成物。 + +合并顺序:**先合 #565 到 master,再把本分支 rebase 到新 master**,然后重跑 §5.1 的全部门禁与本地发布 smoke。 + +--- + +## 4. 为什么是 `game` 而不是 `version` + +| 判断 | 结论 | 理由 | +| --- | --- | --- | +| 血缘(forkedFrom / root / generation) | **game** | 血缘是作品间关系;父作品后续发 v4 不应改写子作品的父指针。放 version 会导致同一作品的每个版本都要重复声明来源,且任一新版本可能丢掉来源 | +| 授权(能否被 fork) | **game** | 是作者对「这部作品」的策略,不是对某个构建产物;且授权可后续提升,必须有稳定载体 | +| 衍生计数 | **game**(由 lineage 表按 parent 计) | 与 game 维度一致 | +| 被复刻的内容(工程源包) | **version** | 内容随版本演进;复刻必须取到确定的内容快照,才能让溯源精确到「改编自 v3」 | +| 复刻时锁定的父版本 | **game 侧血缘行里的 `parent_version_id`** | 是建立血缘那一刻的不可变事实,之后父作品发新版不影响它 | + +一句话:**关系与策略在 game,内容在 version,血缘行把两者钉在一起。** + +--- + +## 5. 验收标准与证据 + +| 条款 | 验收方式 | 证据 | +| --- | --- | --- | +| 授权默认 `forbidden`,旧作品迁移后一律禁止共创 | 迁移后读旧 game 的公开详情 | 待补 | +| 授权只能提升:`forbidden→nonCommercial→full` 合法,任何降级 `409` | 定向 API/领域测试 + 真实栈 smoke | 待补 | +| 非 owner 改授权 `403`;CAS 不匹配 `409`;同 key 重放 `replayed=true` | 定向测试 + 真实栈 | 待补 | +| 未授权 / 已下架 / 非当前公开版本的 fork 声明被拒(`403` / `409`) | 定向测试 | 待补 | +| 血缘不可变:同一作品第二次声明血缘 `409`;`local_project_id` 复用场景拒绝携带血缘 | 定向测试 | 待补 | +| 代际与根计算正确:A→B→C 得到 generation 1/2、root 均为 A | 领域单测 + 真实栈三层链路 smoke | 待补 | +| 父作品下架后:既有子作品保持公开,新 fork 被拒 | 真实栈 smoke | 待补 | +| 工程包门禁:含 `node_modules` / 符号链接 / 绝对路径 / 超限的包被拒且不产生任何对象 | 客户端 Rust 定向测试 + 服务端校验测试 | 待补 | +| 补齐:提升授权后可对当前公开版本补传工程包一次,补齐后改造升级为源码级;二次补传或换内容重传被拒;历史版本不被补齐 | 定向测试 + 真实栈 | 待补 | +| 一键改造闭环:详情页 → AGC → 下载 → 解压 → 建项 → 改造 → 发布 → 详情页出现溯源 | 真实 AGC + 真实 api-server smoke(含截图/录屏) | 待补 | +| 无工程源包时仍有可用的产物级改造路径,且面板如实说明能力边界;不伪造血缘、不出现空白或死链 | 真实栈 + 浏览器验证 | 待补 | +| 公开 DTO 不泄露对象键、不泄露未公开作品信息 | DTO parity + 逐键核对 | 待补 | +| 署名:公开详情/广场展示真实作者名与陶泥号,不再落到「创作者」兜底 | 真实栈 + 浏览器验证 | 待补 | +| 移动端与桌面端布局可用;移动端改造入口给出桌面端提示 | 320px / 1280px 浏览器验证 | 待补 | + +### 5.1 M1(授权与血缘)实施证据 — 2026-10-04 + +分支 `/` 工作树:`feat/game-fork` / `.worktrees/feat/game-fork`。 + +| 项 | 证据 | 状态 | +| --- | --- | --- | +| schema 迁移(持久表加列 + 新表 + 新索引) | 本地 SpacetimeDB 2.8.3 上发布模块后,线上 schema 含 `fork_authorization` 列、`game_distribution_lineage` 表(`_game_id_key` + parent / root / owner 三个 btree 索引)与 `set_game_distribution_fork_authorization_and_return` procedure | ✅ 已验证 | +| 领域规则(授权阶梯、代际、错误码前缀) | `cargo test -p module-game-distribution` 全部通过 | ✅ 已验证 | +| BFF / 路由 / payload / 错误映射 | `cargo test -p api-server game_distribution` 全部通过(含新增的「共创授权路由未带 Bearer 必须 401」用例) | ✅ 已验证 | +| 前端类型与页面 | `npx tsc`(仓库 `npm run typecheck`)通过;`npx vitest run src/components/game-distribution src/services/gameDistributionClient.test.ts` 全部通过 | ✅ 已验证 | +| 契约一致性 | `npm run check:game-distribution-dto-parity`、`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过 | ✅ 已验证 | +| 生成绑定 | `npm run spacetime:generate` 后 `check:generated-bindings` 的 shared-contracts 段通过;AGC 段因工作树缺少随包 Codex CLI 资源(`resources/codex/win-x64/manifest.json`,非本次改动)无法运行 | ⚠️ 环境限制 | +| HTTP 端到端(BFF 路由与真实数据库连接) | `npm run dev` 整套拉起后:`/healthz` 200;`GET /api/game-distribution/games` 200(空列表,走通真实数据库查询与 envelope);`PUT …/fork-authorization` 未带 Bearer 返回 401 `UNAUTHORIZED` | ✅ 已验证 | +| HTTP 端到端(真实登录态下的提升 / 声明血缘 / 详情页字段) | 未跑:需要真实登录态与一个已发布作品;本地开发库当前为空 | ⏳ 待补 | +| 浏览器视觉与交互验证(详情页共创卡、我的作品授权入口、移动端布局) | 未跑:本地库无作品数据,登录态不可用,页面无法进入有效状态 | ⏳ 待补 | +| 权限与边界(非作者 403、降级 409、父作品下架后新声明被拒) | 领域层与 procedure 校验已实现并覆盖单测;真实栈边界用例待补 | ⏳ 待补 | + +--- + +## 6. 里程碑拆分(评审后逐个开实施计划) + +| 里程碑 | 范围 | 交付判据 | +| --- | --- | --- | +| [M1 授权与血缘骨架](./project-memory/plans/【里程碑】游戏共创授权与血缘-2026-10-03.md) | game 追加授权列、新血缘表、创建 game 携带血缘的校验与写入、授权提升接口、公开 DTO 增量、详情页徽章+溯源卡、`/games/mine` 授权设置、后台展示、署名修复 | 可在平台内看到并管理「能否被改编 / 改编自谁 / 第几代」;改造动作本身在 M2a 接线 | +| [M2a 成品包改造闭环](./project-memory/plans/【里程碑】成品包改造闭环-2026-10-03.md) | 复用已存在的成品包做受鉴权 `fork-source`、AGC 下载/建项/改造入口、发布自动声明血缘、发布面板授权选择 | 真实 AGC 上完成「改别人的作品 → 发布 → 溯源正确」,零新增上传资产 | +| [M2b 工程源包与源码级复刻](./project-memory/plans/【里程碑】作品工程源包与一键改造-2026-10-03.md) | version 追加工程包字段、AGC 打包/上传、`fork-source` 优先返回工程包、源码形态建项 | 真实 AGC 上完成「拿到源码 → 改核心逻辑 → 发布 → 溯源正确」 | +| [M3 族谱与衍生列表](./project-memory/plans/【里程碑】创作族谱与衍生列表-2026-10-03.md) | `/games/lineage` 树页、`/games/{id}/lineage` 与 `/derived` 接口、`/games/mine` 被改编列表 | 族谱树可浏览、可跳转、父作品下架有降级展示 | +| M4(可选,需产品决策) | 共创主题(平台命名的归组实体)与广场共创 Tab | 仅当采纳「平台命名主题」方案时才做 | + +依赖:M2 依赖 M1 的数据模型;M3 依赖 M1;M4 依赖 M2 的实际使用数据。 + +--- + +## 7. 未决问题(需要拍板) + +1. **成品包路径默认要做**(零新增上传资产,且是工程源包缺失时的降级路径);需拍板的是**是否追加工程源包(M2b)**:不做,则用户只能产物级改造,AI 改核心逻辑不可靠,需求文档里「一键复刻完整工程」这半句无法兑现。建议:做。 +2. **授权默认值**:本文档按需求描述取「默认禁止」;飞书文档评论中包仲航建议改为「默认允许 + 发布前合同勾选」。两者会改变默认曝光面与合规口径,需产品拍板(默认允许对生态更友好,但要处理存量作品的合法性回溯)。 +3. **是否引入「共创主题」实体**:包仲航建议「平台命名主题 → 作品树」,并明确作品之间不存在曝光挂靠。本文档的 M1–M3 按「作品间独立展示 + 详情页溯源 + 族谱按根归组」实现,不引入新实体;若采纳主题方案,需要额外的运营命名流程与表。 +4. **收益分成**:需求文档要求「每一代均享有权益(署名 / 流量回馈 / 版权分成)」。署名本期做,流量回馈与分成本期不做(无账本、无算力成本口径,`docs/【技术方案】外部产品支付服务接入-2026-10-03.md:200` 明确人工结算)。 +5. **相似度反洗稿校验**:需求文档要求「低改动度复刻判定」。本期不做;本方案只保证来源声明真实、不可伪造。若要做,只能基于工程源包做结构化比对,属于独立议题。 +6. **「永久链上溯源」表述**:实现为平台持久化的不可变父子链,不上链。需确认该措辞是否可以调整。 +7. **`play_count` 死字段**:它被前端展示为「N 次游玩」但永不自增。本方案不依赖它;是否顺带修复(新增游玩上报)需单独立项。