Commit Graph

5876 Commits

Author SHA1 Message Date
suzmii 909f3d5fa2 网页端共创主题:共创 Tab + 主题页(逐根内嵌作品树)+ 详情页主题入口 + 网关清单
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
- 客户端:新增 listThemes(游标分页,读 nextCursor)与 getTheme(404 由页面渲染成降级态);getGame 的返回类型增量 themes(匿名也返回;无主题时是空数组而不是缺键)
- 共创 Tab:广场页新增「游戏 / 共创」页签,复用既有 game-category-tabs 结构与类名,切到 /games/themes
- 主题列表页 GameThemesPage:卡片展示 name / summary / badge / memberCount;只有 nextCursor 非空才渲染「加载更多」并在点击时追加;空态与失败重试照既有写法
- 主题页 GameThemePage:标题 / 简介 / 角标 + 成员根清单;**逐根内嵌渲染作品树**——直接复用族谱页(GameLineagePage 新增 embedded 变体,只隐藏页面外壳:返回按钮 / 页头 / 脚注),树查询、可见性过滤、稳定排序与截断全部走 M3 那一份实现,不新建第二套树查询或树渲染(每根一次 /lineage 的 N+1 是技术方案 §3.10.4 已接受的取舍);已发布但可见成员为空 → 明确空态而不是错误;主题不存在 / 未发布 → 错误态
- 详情页:共创信息卡渲染所属主题角标(点击进主题页),第 N 代作品同样可见(服务端按根反查);无主题时不渲染空容器
- 路由登记:新增 /games/themes 与 /games/theme(详情用 ?id=),四处同批登记(STAGE_ROUTE_ENTRIES + SelectionStage 类型 + 壳层 lazy/回调/渲染分支 + 页面标题),并同步三份 nginx SPA allowlist 与 Pingora MAIN_SPA_PATHS:check:nginx-spa-routes(20 SPA routes / 3 模板)与 check:pingora-route-parity 均绿
- 测试:主题列表三例(渲染与卡片跳转 / 有游标时的加载更多与追加 / 空态与失败重试)、主题页三例(逐根两棵树且复用 lineage 查询 + 全屏入口参数 / 空成员空态 / 404 错误态)、详情页主题入口两例(渲染并可跳转 / 无主题不渲染容器)、路由与标题登记断言、壳层三例(页签切换 / 列表进主题页 / 主题页按 ?id= 渲染并复用族谱查询)
2026-10-06 03:45:38 +08:00
suzmii 58da0e67db test(游戏共创): 新增共创主题端到端验收脚本
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
- 新增 scripts/check-game-distribution-theme-e2e.mjs(约 1160 行):真库验收公开读 + 后台写 +
  作品详情 themes 增量,复用 lineage 脚本的完整发布链路 helper(脚本头注释逐条标注出处),
  覆盖 9 组共 54 条断言:
  · 建主题→加根成员→公开读:公开列表只含 published(且不含 status 字段)、详情 roots 逐条与公开
    目录同一投影(键集合 23=23)、memberCount 与 roots 长度一致
  · 只允许根:第 1 代子作品当成员 → 409 THEME_MEMBER_NOT_ROOT;不存在的 gameId → 404 THEME_MEMBER_GAME_NOT_FOUND
  · 跨主题多归属:同一个根加入两个主题,两条主题都能读到它,作品详情 themes 长度=2
  · 作品详情 themes 口径:匿名与登录都发;**专门构造第 1 代衍生作品**(带 fork 声明 + 完整发布链路)
    并断言它也返回根所属的两个主题(先取根再反查);无归属作品返回空数组
  · 可见性:draft 主题公开 404(后台按 draft 过滤仍可见)、archived 公开 404 且作品详情 themes 同步消失、
    改回 published 恢复;已发布但成员全不可见 → 200 + 空 roots + memberCount=0;
    成员下架 → 从 roots 消失,重新公开(新版本审核通过)→ 同一行自动回到 roots 且仍只一行
  · 分页:默认 20、limit=999 截断到 50、limit=20 游标翻页 3 页 54 条不重不漏、末页 nextCursor=null、
    ?cursor=abc → **400**(状态码符合契约)
  · 后台权限:五条路由未登录 401;普通作者 token 403;成员 PUT/DELETE 免幂等键、重复 PUT 只留一行
    且 createdAt 不变、DELETE 幂等(data 载荷逐字段相同,信封 meta.requestId 每请求不同属既有协议)
  · 后台幂等:同键重放 replayed=true 且不产生第二个主题、同键换请求 409 THEME_IDEMPOTENCY_CONFLICT、
    缺 Idempotency-Key → 400
  · 三条公开路由匿名 200 且带 Cache-Control: no-store
- **实测 53 PASS / 1 FAIL**:唯一失败是已确认的产品缺口(仅报告、未改后端)——非法游标的状态码是
  400 ✓,但稳定码 THEME_INVALID_CURSOR 不可达:api-server/...:5343-5346 为该码注册了 400 映射,
  而模块侧只回纯文案「主题列表游标格式无效」(module-game-distribution/src/theme.rs:205-215),
  不含分支前置所需的 `THEME_` 前缀,客户端实际拿到 code=BAD_REQUEST。脚本刻意保留这条红断言
  (契约真值),并附 NOTE 说明成因与证据。
- package.json:新增 npm script check:game-distribution-theme-e2e
2026-10-06 03:44:31 +08:00
suzmii 09a1cf5497 docs(游戏共创): 共创主题文档回填(§3.4 接口表 / §3.10 实现状态 / 里程碑 Status 与验收)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m57s
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
服务端三块(`2fa201e0d` 模型与纯函数、`742723a58` 公开读、`033e3aa79` 后台写)已落地,本次把文档从「只落设计」补齐到「服务端已实现」,并写死一条已知限制。

- **技术方案 `§3.4`**:新增 5 条接口行(真实路由/鉴权/错误码/字段名均以 `git show` 核对代码为准,非猜测)——公开 `GET /api/game-distribution/themes`(匿名 + `no-store`;只含 `published`;limit 缺省 20 / 上限 50 / 超界截断;游标 `"{createdAtMicros}:{themeId}"`;非法游标 400;末页 `nextCursor: null`;`memberCount` = 公开可见成员数)、`GET /api/game-distribution/themes/{themeId}`(顶层扁平 `{themeId,name,summary,badge,memberCount,roots}`;`roots` 复用公开目录同一份投影;不存在/draft/archived → 404 不返回空壳;已发布空成员 → 200 + 空 `roots`),以及 `GET /games/{gameId}` 的 `themes` 增量(匿名与登录都发、空数组恒发;先取根再按根反查 ⇒ 第 N 代作品也能跳主题页);后台 5 条(`require_admin_auth` + `AuthenticatedAdmin`,未登录/失效 401 `UNAUTHORIZED`、非 admin 403;创建与编辑要求 `Idempotency-Key`,同键重放 `replayed: true`、同键换请求 409 `THEME_IDEMPOTENCY_CONFLICT`;成员增删**不要求**幂等键(确定性主键天然幂等,删不存在也 200);非根成员 409 `THEME_MEMBER_NOT_ROOT`;后台列表含 draft/archived、limit 缺省与上限 200、无游标;`theme_id` 服务端生成 `theme-{uuid}`)。
- **`§3.10`**:各小节「本轮实现 / 留白」标注更新为现状(服务端已实现;公开前端与后台 UI 仍留白);`§3.10.6`/`§3.10.9` 明确写下**已知限制**——主题详情 `roots` 沿用页上限 **50** 且 `memberCount == roots.len()`,可见成员 > 50 时会**静默截断且没有「还有更多」标志**,三个备选(去上限 / 给成员加游标 / `memberCount` 报真实可见数)待拍板,本轮不改契约。
- **`§7`**:第 3 条(共创主题)改为「设计已采纳 + 服务端已实现」,仍待拍板收敛为两条:① 主题级排序口径(公开列表当前按 `created_at` 倒序 + `themeId` 兜底;若要按运营 `sort_order` 展示需改游标格式并同步前端);② 上述 `roots` 50 上限行为。
- **里程碑文件**:`Status` 由 `proposed` → 「服务端已实现(附四个提交号);公开前端与后台管理 UI 待做」,Version 1.0 → 1.1,新增「已落地提交」行;实施清单 A–F 标注已落地提交、G 标未做(待前端)、H 分「已落地层 / 未做层」、I 补执行状态;**验收标准勾 17/19**(第 17 条公开前端、第 18 条 SPA 路由门禁留空并注「等前端」);文末写明「dev 栈端到端整链未跑(`scripts/check-game-distribution-theme-e2e.mjs` 未创建),依赖真实库存的运行时分支目前只到单元/结构/映射层」。
- **数据契约文档**:两张主题表小节的「写入路径属后续里程碑」改为现状(读/写 procedure 与事务名),表结构描述未动。
- **门禁**:`check:doc-index` 0(252 份 Markdown,current=121 / historical=15 / review=1);`check:encoding` 0(5414 files);`git diff --check` 0。
- **环境限制(标注,非绿)**:`check:generated-bindings` 本机 `EXIT=1`,原因是 `apps/ai-game-creator-shell/src-tauri/build.rs:167` 校验随包资源时缺 `bin/win-x64/agc_godot_editor.dll`(需先跑随包资源准备步骤)——纯环境前置,与本次只改 `docs/**` 无关;该检查的 AGC 段本地从来跑不了(CI 绿)。
2026-10-06 03:38:11 +08:00
suzmii 033e3aa79a feat(游戏共创): 共创主题(三)后台写路径(5 条 admin 路由 + 事务 + 幂等)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m59s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m43s
Project CI / Backend tests (pull_request) Successful in 9m27s
Project CI / Frontend tests (pull_request) Successful in 3m13s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m24s
Project CI / Repository checks (pull_request) Successful in 5m4s
Project CI / Native shell tests (pull_request) Successful in 6m50s
按 §3.10.8:后台 UI 不在本轮,本轮只落**接口与事务**。

- **鉴权(照仓库既有 admin 体系,不自造)**:5 条路由挂在既有 `/admin/api/game-distribution/*` 支,统一 `route_layer(require_admin_auth)`,handler 取 `Extension<AuthenticatedAdmin>`;未带 / 失效会话 → **401 `UNAUTHORIZED`**(与同组 games/reviews 路由同码同形),非 admin role → 403。模块侧第二层:写 procedure 与后台列表都要求受信服务身份(`require_editor_generation_runtime_service_identity`),只有 api-server 能调。
- **5 条路由**:`POST /themes`(创建;服务端生成 `theme-{uuid}`;要求 `Idempotency-Key`,摘要只覆盖客户端可见请求体,**不含重试时重新生成的 `theme_id`**)、`PUT /themes/{theme_id}`(整体覆盖 name/summary/badge/sortOrder/status,生效即刷新 `updated_at`,重放不写库不刷新)、`PUT /themes/{theme_id}/members/{root_game_id}`(**不要求幂等键**——确定性主键天然幂等;可带 `sortOrder`)、`DELETE …/members/{root_game_id}`(不要求幂等键;成员不存在也算 200,响应刻意不含「之前存不存在」→ 重复调用逐字节相同)、`GET /themes?limit=&status=`(**含 draft/archived**,status 白名单 `all|draft|published|archived`,limit 缺省与上限 200、超界截断、无游标)。
- **成员只允许根作品**:用 `game_distribution_theme_root_acceptable(has_lineage_row)`,非根 → **409 `THEME_MEMBER_NOT_ROOT`**;作品不存在 → 404 `THEME_MEMBER_GAME_NOT_FOUND`;主题不存在 → 404 `THEME_NOT_FOUND`。**不做「成员必须已公开」的前置校验**(运营可先挂草稿根,作品公开后自动出现在公开投影里);归档主题**不清理**成员行(与公开投影「跳过不删行」同一口径)。
- **领域错误码**:`module-game-distribution/src/errors.rs` 新增 5 个带码错误(`ThemeNotFound` / `ThemeBadRequest{reason}` / `ThemeIdempotencyConflict` / `ThemeMemberNotRoot{game_id}` / `ThemeMemberGameNotFound{game_id}`),Display 一律 `CODE: 文案`,并由测试钉住「码后必须跟冒号」;api-server 集中映射,不落 axum 默认 422 纯文本。
- **纯函数补充**:后台 status 过滤器白名单(`all` + 三态的单一实现)、文本上限常量(名称 40 / 简介 200 / 角标 16,按 UI 一行与卡片摘要的量级取,写进注释)与 `game_distribution_theme_text_violation`(trim 后判空名与超长,返回可拼进 `THEME_BAD_REQUEST:` 的原因)。
- **测试**:`theme.rs` +2 纯函数(status 过滤器白名单、文本违规判定)、`domain.rs` +1(错误码前缀)、spacetime-module +8 事务结构断言、client mapper +3、api-server +6(401 同码同形、创建/编辑/成员增删的形状与错误码、后台列表含 draft、重复 PUT 幂等、非根成员被拒)→ `cargo test -p api-server game_distribution` **85 passed**、`module-game-distribution` **100 passed**、`spacetime-module` **286 passed**、`spacetime-client` 37 passed。
- **门禁**:wasm build 0;`cargo check --all-targets` 0;`cargo test -p api-server game_distribution` 85 passed;`cargo test -p module-game-distribution` 100 passed;`cargo test -p spacetime-module` 286 passed / 1 ignored;`cargo test -p spacetime-client` 37 passed;DTO parity 58 组 / 10 构建器;`check:spacetime-schema` 0(96 tables);`check:encoding` 0(5414 files);`cargo fmt --all -- --check` 0;`git diff --check` 0。
- 生成绑定:新增 15 个 `module_bindings/*theme*/admin_theme*` 文件 + 入口声明(无重排);其余 202 个文件的 rustfmt 漂移按上一轮做法逐字节还原,未跑任何 git 写命令。
2026-10-06 02:45:30 +08:00
suzmii 742723a58c feat(游戏共创): 共创主题(二)公开读路径(事务 + client + 两个公开路由 + 详情 themes 增量)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m35s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m37s
Project CI / Backend tests (pull_request) Successful in 9m29s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m7s
Project CI / Repository checks (pull_request) Successful in 6m48s
Project CI / Native shell tests (pull_request) Successful in 8m39s
- **事务**(`spacetime-module`):公共列表 `list_game_distribution_themes_tx`(匿名可读;先按 `published` 过滤、再排序切页——`created_at` 倒序 + `theme_id` 兜底;只对当页现算 `member_count`;不写库);主题详情 `get_game_distribution_theme_detail_tx`(不存在 / 未发布**同一句**「主题不存在」→ 404;`roots` 逐条复用公开目录同一份 `public_game_distribution_snapshot`;成员可见性委托 `game_distribution_theme_member_visible`,被滤掉的行**不删除**;已发布但可见成员为空 = Ok + 空 roots = 200);作品→主题 `list_game_distribution_theme_refs_for_root_tx`(入参任意代作品,先取**根**再按根反查成员、只保留 `published` 主题;排序复用 `sort_public_themes`)。**根解析只有一份实现**:抽成 `game_distribution_theme_root_of` 并让既有 `game_distribution_lineage_tx` 复用(测试钉住)。
- **client**:3 个新方法 + mapper 记录类型(`spacetime-client`)。
- **公开路由**(`api-server`):`GET /api/game-distribution/themes?limit=&cursor=`(匿名可读 + `no-store`;`limit` 缺省 20 / 上限 50 / 超界截断;非法游标 **400** 透传;末页 `nextCursor: null`)、`GET /api/game-distribution/themes/{theme_id}`(顶层扁平 `{themeId,name,summary,badge,memberCount,roots}`;不存在 / draft / archived → **404**,不回空壳、不带 `THEME_` 码)、既有公开作品详情追加 `themes`(匿名与登录**都**发、空数组恒发;`collected` 仅登录发的老口径不变;因按根反查,**第 N 代作品也能跳到主题页**)。
- **错误码**:新增 6 个 `THEME_*` 稳定码集中在 `map_spacetime_error` 映射(不落 axum 默认 422 纯文本);未登记码兜底 **400 `THEME_ERROR`**。
- **契约/parity**:5 条手拼响应构建器登记(`public_themes_payload` / `public_theme_detail_payload` / `public_theme_summary_payload` / `theme_reference_payload`,并给 `public_game_detail_payload` 加 `mustEmit: [themes]`)→ 58 组类型 + 10 个构建器一致。
- **测试 +13**:spacetime-module 4 条源码结构断言(根解析单一实现、先过滤再切页、投影不删行、详情只有两种失败且同一 404 文案、refs 走具名根索引 + 只保留 published)、api-server 7 条(路由挂载与 `no-store`、列表/详情响应形状与末页 null、`roots` 逐字节等于 `public_game_payload`、limit 缺省与截断、非法游标 400、404 与 6 个 THEME_ 码映射、作品详情 `themes` 恒发而 `collected` 仅登录)、client mapper 2 条。
- **门禁**:wasm build 0;`cargo check --all-targets` 0;`cargo test -p api-server game_distribution` **79 passed**;`cargo test -p module-game-distribution` **97 passed**;`cargo test -p spacetime-module` **278 passed** / 1 ignored;`cargo test -p spacetime-client` 34 passed;DTO parity **58 组 / 10 构建器**;`check:spacetime-schema` 0(96 tables);`check:encoding` 0(5399 files);`cargo fmt --all -- --check` 0;`git diff --check` 0。

已知限制(**已上报,待拍板**):主题详情的 `roots` 沿用页上限 50,`memberCount == roots.len()`,因此可见成员 > 50 时**静默截断且无「还有更多」标志**——设计文档未定义该行为,三个备选(去上限 / 加成员游标 / memberCount 报真实数)待定;本轮不改契约。
2026-10-06 02:15:03 +08:00
suzmii 2fa201e0dc feat(游戏共创): 共创主题(一)数据模型与领域纯函数
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m30s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m12s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m44s
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
按 `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md §3.10` 与里程碑《共创主题与作品树-2026-10-06》的清单 A/B/F 落地(本块**不含** procedure / 路由)。

- **表**:`game_distribution_theme`(`theme_id` 主键、`name`/`summary`/`badge`/`sort_order`/`status ∈ draft|published|archived`/`created_by_user_id`/`created_at`/`updated_at`)+ `game_distribution_theme_member`(确定性主键 `member_id = "{theme_id}:{root_game_id}"`,`theme_id`、`root_game_id` 各具名 btree 索引,`sort_order`、`created_at`);归档 / 下架只改公开投影,不删成员行。`migration.rs` 白名单登记两张表(随迁移导出/导入)。
- **生成绑定**:4 个 `game_distribution_theme*` 生成文件 + `module_bindings.rs` 入口(纯追加)。生成时**不覆盖工作树**(本仓共享工作树禁止 git 写命令):先 `spacetime generate --lang rust --out-dir <仓外临时目录>`,rustfmt 后只拷回本次相关文件,避免上一轮踩过的「rustfmt 漂移把别人文件一起改」。
- **领域纯函数**(`module-game-distribution/src/theme.rs`,风格照同目录 `collection.rs`):状态合法/公开可见判定、`game_distribution_theme_member_id`、成员可见性(**委托** `collection.rs` 的判定,不复制第二套)、`game_distribution_theme_root_acceptable(has_lineage_row)`、页大小(缺省 20 / 上限 50 / 0 取默认 / 超界截断)、游标编解码(`"{createdAtMicros}:{themeId}"`,Err 文案「主题列表游标格式无效」,已核对不含 404/409 关键词)、`sort_public_themes`(`created_at` 倒序 + `theme_id` 兜底)、`page_public_themes`(排序 → retain 游标之后 → 多取一条判 has_more;**先过滤再切页**的顺序语义写进文档注释)、`sort_theme_members`(`sort_order` 升序 + `member_id` 兜底)与 `page_theme_members`。
- **契约**:`shared-contracts` 新增 `GameDistributionThemeStatus` / `GameDistributionThemeReference` / `GameDistributionThemeSummary` / `GameDistributionThemeListResponse` / `GameDistributionThemeDetail` / `GameDistributionCreateThemeRequest` / `GameDistributionUpdateThemeRequest` / `GameDistributionUpsertThemeMemberRequest`,`GameDistributionGameSummary` 追加可选 `themes`(按 `collected` 先例只在详情路径下发);TS 镜像同步;parity 登记 **58 组**(响应构建器待路由落地后再登记,parity 要求函数真实存在)。
- **数据契约表目录**:补两张表小节,94 → **96**。
- **测试**:`theme.rs` 16 个单测(成员 id 确定性、状态三态与公开可见、成员可见性逐格与收藏判定等价、根判定、页大小边界、游标往返与非法输入、排序稳定性与同 `sort_order` 兜底、分页不重不漏)。
- **门禁**:wasm build 0;`cargo check --all-targets` 0;`cargo test -p module-game-distribution` **97 passed**;`cargo test -p spacetime-module` 274 passed / 1 ignored;DTO parity **58 组**;`check:spacetime-schema` 0(**96** tables);`check:server-rs-ddd` 0;`check:encoding` 0(5388 files);`cargo fmt --all -- --check` 0;`git diff --check` 0(8 个改动文件 0 删除行)。
2026-10-06 01:46:23 +08:00
suzmii 063c04a1b3 docs(游戏共创): 共创主题设计定稿(§3.10 + 里程碑《共创主题与作品树-2026-10-06》)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
需求形态(飞书):共创 Tab 下有多个「共创主题」,点进去是一棵(或一组)作品树;**主题由平台/运营命名,不由根作品决定**;作品在游戏 Tab 里仍作为独立作品展示,详情页有 Fork 入口并能看到 / 跳到对应主题页。本轮只落设计(不写业务代码),实施清单见里程碑文件。

设计要点与理由(详见 §3.10):
- 实体:`game_distribution_theme`(`theme_id` 主键、三态 `draft|published|archived`、`sort_order`、`badge`、`created_by_user_id`)+ `game_distribution_theme_member`(确定性 `member_id = "{theme_id}:{root_game_id}"`,`theme_id` / `root_game_id` 各具名 btree 索引)。
- **成员只允许根作品**:树查询 `/games/{id}/lineage` 按根聚合,否则同一棵树会在主题页里出现多次(去重/高亮打架);代价=要挂就挂它所属的根。跳到某一代节点属 UI 层(族谱页已有 `from` 高亮)。
- **允许跨主题多归属**:主题是运营叙事,「平台精选」「双人合作」会同时收录同一个根;确定性主键只保证**同一主题内**不重复。代价=作品详情要返回多值、运营需知悉。
- **树直接复用 `/games/{id}/lineage`**,主题侧**不重建**任何树查询/树整形(没有第二套可见性/排序可漂移);主题详情只返回成员根清单(N+1 本轮接受,批量树端点留白)。
- 可见性:公开侧只见 `published` 主题;成员只出现公开未删且有当前公开版本的根(**投影跳过但不删行**,重新公开自动回来);主题不存在/未发布 → **404**;已发布但可见成员为空 → **200 + 空成员列表**。
- 公开三接口:`GET /themes`(游标分页沿用默认 20/上限 50/非法 400/末页 null)、`GET /themes/{theme_id}`(+ `roots`)、作品详情 `themes` 增量(**先取该作品的根、再按根反查**,因此第 N 代作品也能跳到主题页)。
- 后台五接口(创建/编辑/增删成员/后台列表,均幂等),鉴权**复用既有 `require_admin_auth` + `AuthenticatedAdmin`**,不自造;后台 UI 不在本轮。
- 留白 5 项(各写不做理由与将来接法):主题封面图、slug/URL 别名、埋点统计、跳到某一代的高亮、批量树端点。
- 同步收敛:§6 里程碑表 M4 行指向新里程碑;§7 第 3 条「共创主题」从**待拍板**改为**已拍板采纳**,并把「主题级排序口径」留作唯一待产品确认项。

门禁:`check:doc-index` 0(252 份 md;新里程碑在 `plans/` 下免分类);`check:encoding` 0(5383 files);`git diff --check` 0。

注:§3.10 的前约 200 行因共享工作树/索引,被上一提交 `f703bc49d`(注释修正)一并带上;本提交补齐其余章节 + 里程碑文件。
2026-10-06 01:35:56 +08:00
suzmii f703bc49d4 docs(游戏共创): 修正收藏幂等注释与实现不符之处(换用户不是 409)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
端到端实测(`scripts/check-game-distribution-collection-e2e.mjs`)发现注释与实现不一致,**行为本身无缺陷,改的是注释**:

- `api-server/src/modules/game_distribution.rs:1389-1396`(`collection_request_digest` 文档注释):原写「同一个 key 撞到不同作品 / 不同用户」都被判成同键不同请求(409)。实际只有**换作品**是 409;**换用户**不会命中彼此收据(收据键 `(user_id, action, idempotency_key)` 本身含 `user_id`),两个用户各走各自的新请求、正常 200。已改写为「换作品 → 409;换用户 → 200 各自成功」,并注明这条注释曾写错。
- `api-server/src/modules/game_distribution.rs:1404-1408`(`collect_game` handler 幂等说明):同上(原文「同键不同请求(换作品 / 换用户)是 409」)。
- `api-server/src/modules/game_distribution.rs:7896-7897`(测试 `collection_request_digest_binds_user_and_game` 的描述):补上「换作品 → 409 / 换用户不会(收据按用户隔离)」;**断言未改**(它只断言摘要对两维度敏感,本来就与实现一致)。
- `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md:339`(`§3.4` 接口表同一句话):同一口径修正。
- `module-game-distribution/src/collection.rs:173`:把易被误读为「客户端 Idempotency-Key」的注释改为明确的「主键派生必须对两个维度都敏感」,并说明与幂等收据的键无关。

`spacetime-module/src/game_distribution.rs:4717-4719` 的注释本来就与实测一致(「不同用户 / 不同作品即使共用同一个 Idempotency-Key 也不会互相命中」),未改。

门禁:`cargo check -p api-server --all-targets` 0(仅既有 1 warning);`cargo test -p api-server collection` 7 passed;`cargo fmt --all -- --check` 0;`check:encoding` 0(5382 files);`git diff --check` 0。

遗留(非本人文件,未改):`scripts/check-game-distribution-collection-e2e.mjs:32-34,537` 的说明文字仍在描述「api-server 注释与实测不一致」,现已一致,需该脚本作者同步(归另一会话)。
2026-10-06 01:34:35 +08:00
suzmii f09607d903 feat(游戏共创): 收录(收藏)列表补真分页(cursor + limit)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 纯函数(`module-game-distribution/src/collection.rs`,与收藏另两条规则同文件):`encode/parse_game_distribution_collection_cursor`(`"{created_at_micros}:{collection_id}"`,复用仓库既有 `"{micros}:{id}"` 惯例;非法 → Err「收藏列表游标格式无效」→ 400)、`game_distribution_collection_page_limit`(0/缺省 → 20,超界 → 50)、`game_distribution_collection_page`(稳定全序排序 → 跳过游标之前 → 取 limit → 多取一条判 has_more,只对页内最后一条编码下一游标)
- 排序与顺序语义:`created_at` 倒序 + `collection_id` 升序兜底(主键、同用户内唯一 ⇒ 全序,翻页不重不漏);**先按可见性过滤、再排序切页**——游标位置必须落在已过滤序列上,否则被过滤的行会让下一页漏项(推理写进 tx 与纯函数注释);snapshot 只对最终页成员计算
- 模块:list 的 input 增 `limit` / `cursor`,result 增 `next_cursor`;api-server `my-collections` 加 `Query<MyCollectionsQuery>`,默认 20 / 上限 50(网格一屏 vs 单响应体积;与后台列表的 200 口径不必相等,理由写进注释),超界**截断**不报错,非法游标 400 透传不吞,日志带 `has_more`
- 契约:Rust DTO 增 `next_cursor`、TS 镜像同步、parity 登记(50 组一致)
- 文档:§3.4 的 `my-collections` 行补分页语义(默认/上限/游标格式/非法 400/末页 null/排序口径)
- 测试 13 条:纯函数 8(不足一页 → None;恰好整页不产生空游标;5 条 limit=2 连翻 3 页不重不漏;同 `created_at` 用 `collection_id` 兜底;limit=0 与超界;游标往返 + 7 种非法输入)、模块事务结构 2、api-server 3(默认与截断、`nextCursor` 透传、非法游标 → 400)
- 门禁:wasm build 0;`cargo check --all-targets` 0;`cargo test -p module-game-distribution` **81 passed**;`cargo test -p api-server game_distribution` **73 passed**;`cargo test -p spacetime-module` 274 passed / 1 ignored;DTO parity **50 组**;`check:spacetime-schema` 0(94 tables);`check:encoding` 0(5382 files);`cargo fmt --all -- --check` 0;`git diff --check` 0
2026-10-06 01:31:09 +08:00
suzmii eb7349ef3d test(游戏共创): 新增收藏(收录)端到端验收脚本
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 新增 scripts/check-game-distribution-collection-e2e.mjs(约 750 行):真实 HTTP 验收收藏链路,
  复用 lineage 脚本的发布链路 helper(脚本头注释逐条标注出处),覆盖 9 组共 46 条断言:
  · 幂等与「只一行」:两次不同键都成功(replayed=false);同键重放 replayed=true;两种情况都断言
    my-collections 长度=1 且该作品只出现一次(无收藏计数接口,按工单允许的列表去重与长度证明)
  · 同键不同请求:换作品 → 409(「幂等键对应的请求摘要不一致」);**换用户 → 200 新收藏**,与
    api-server handler 注释「换用户也是 409」不一致(收据键含 user_id,模块侧注释与实测一致),
    脚本按实测断言并在 NOTE 与报告里标注
  · DELETE 幂等:已收藏 / 未收藏 / 从未收藏且未公开 三种情形都 200 {collected:false} 且不发 replayed
  · 鉴权与缓存:三条路由未登录都 401 且带 Cache-Control: no-store(证明挂载而非 404/405);
    已登录但缺 Idempotency-Key 的 PUT → 400
  · 状态门:草稿 PUT → 409「作品状态不允许收藏(未公开或已软删除)」(含「状态」不含「不存在」,
    不泄露未公开作品存在性);不存在的 gameId → 404「作品不存在」
  · 下架 → 重新公开:收藏后下架读侧过滤掉该作品,重新公开(新版本审核通过)后又回到列表且仍只一行
    (证明下架不删行、只在读侧过滤)
  · 公开详情 collected:匿名 23 键无 collected;已登录 24 键恰好多 collected=true;
    去掉该键后与匿名逐字段一致;公开目录同作品条目与匿名详情键集合与取值完全一致
  · 跨用户隔离:A 收藏后 B 的列表不含该作品、B 的 collected=false
- package.json:新增 npm script check:game-distribution-collection-e2e
- 实测:本地 dev 栈(SpacetimeDB 3110 / api-server 8188)46 项 46 PASS / 0 FAIL / 0 SKIP
2026-10-06 01:30:53 +08:00
suzmii 785152f70b 网页端收藏:作品详情页收藏切换 + 我的收藏页签
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m38s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m38s
Project CI / Frontend tests (pull_request) Failing after 1m10s
Project CI / Backend tests (pull_request) Successful in 9m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m3s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
- 客户端:新增 setGameCollection(PUT 带 Idempotency-Key;DELETE 不带)与 listMyCollections({games,nextCursor},条目形状与公开目录逐字一致,可复用同一套卡片);getGame 返回类型放宽为 GameDistributionGameWithCollection,collected 为可选
- 详情页:在动作区(「立即玩」旁,而不是共创信息卡——那张卡讲改编关系)新增收藏切换。三态严格区分:collected=true → 「已收藏」点击取消;collected=false → 「收藏」点击收藏;**缺字段(匿名 / 无效 token 被按匿名处理)绝不落成「未收藏」**,按钮 title 标注「登录后可收藏」,点击走既有登录门禁且不发请求
- 乐观更新 + 回滚:先切换本地态,失败回滚到服务端投影并用服务端 message 提示(role=status);成功以服务端返回的 collected 为准
- 广场卡片:卡片本身没有操作区,按「没有就别硬塞」不动
- 我的收藏:落在 /games/mine 的页签(「我的作品」/「我的收藏」)。理由:两者都是登录后的个人内容、该页已有登录门禁;且不需要新路由,避免牵动「路由表 + nginx×3 + Pingora allowlist」这组必须同批登记的清单。复用公开目录同一套卡片(shared 的 GameCard + GameCover),空态/失败重试/登录态照既有写法
- 分页按「将来会有 cursor」的形态写:读 nextCursor,非空才渲染「加载更多」并在点击时追加;服务端当前恒为 null,不伪造分页数据
- 文案统一「收藏 / 已收藏」(不用需求文档旧词「收录」)
- 测试:详情页收藏四例(已收藏→取消 / 未收藏→收藏 / 缺字段走门禁且零请求且不渲染已收藏 / 失败回滚+服务端文案);我的收藏两例(空态且默认页签不预取 / 列表渲染+卡片点击跳详情+无 cursor 不出现「加载更多」)
2026-10-06 01:18:45 +08:00
suzmii 4b1f24a8fc feat(游戏共创): 收录(收藏)契约与文档同步(块 3/3)
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
- `shared-contracts`:新增 `GameDistributionCollectionState { collected, replayed: Option<bool> }` 承载 PUT/DELETE 共用形状(PUT 才发 `replayed`);既有 `GameDistributionGameSummary` 增 `collected: Option<bool>`(公开详情登录时才带)——该类型只被 api-server 测试与 `GameDistributionListResponse` 引用,加字段不影响任何现有发送方
- `packages/shared` TS 镜像同步(`{ collected: boolean; replayed?: boolean }` 与 `GameDistributionGame.collected?: boolean`),并按既有约定登记进 `scripts/check-game-distribution-dto-parity.mjs`(PAIRS + 手拼响应构建器);注意 `json!` 字面量里不插注释行(parity 脚本抓顶层键时不剥注释,这个坑今天踩过一次)
- 技术方案:`§2.4` 入口矩阵补「收藏按钮 / 我的收藏(登录)与我的作品页同行风格」;`§3.4` 接口表补三条路由(含 404/409 语义、PUT 要求幂等键而 DELETE 不要求及理由、`my-collections` 只含公开作品且不删行)与公开详情 `collected` 的可见性口径;`§7` 把「收录/收藏」从范围外移除并标注已纳入落地
- 数据契约表目录:新增 `game_distribution_collection` 小节(主键确定性、两个索引、为什么不删行、为什么不建唯一索引),供 `check:spacetime-schema` 守卫通过(94 tables)
- 门禁:DTO parity 0(**49 组** Rust/TS 一致,较本轮前 +7);`check:spacetime-schema` 0(94 tables,对照新 merge-base `51cb05f4`);`check:encoding` 0;`git diff --check` 0
2026-10-06 01:14:05 +08:00
suzmii cc7d214136 feat(游戏共创): 收录(收藏)接口:三条路由 + 公开详情 collected(块 2/3)
- 新增 `collections` 路由组(整组 `require_bearer_auth` + `add_no_store_response_headers`,用户态读不会被任何缓存复用):
  · `PUT /api/game-distribution/games/{game_id}/collection` → `{ collected: true, replayed }`,要求 `Idempotency-Key`(沿用既有 `idempotency_key(&headers)?`);同键重放 `replayed=true`,同键不同请求(摘要绑定 `(userId, gameId)`)→ 409,重复收藏(不同键)只留一行且成功
  · `DELETE .../collection` → `{ collected: false }`,**不要求 Idempotency-Key**:按确定性主键删除本就幂等(不存在也算成功),没有「重放 vs 新意图」可区分;也**不要求作品仍公开**(下架后仍可清理)
  · `GET /api/game-distribution/my-collections` → `{ games: [<public_game_payload>…], nextCursor: null }`,逐条复用既有公开目录的组装方式,只含当前公开可读作品
- 公开详情 `GET /games/{game_id}` 增量:**已认证**返回 `collected: true|false`;**匿名不返回该键**(选这种口径的理由:`false` 会把「未登录」说成「没收藏」,客户端无法区分就会渲染出错误的按钮态);携带无效/过期 token 时按**匿名**处理(沿用 `record_game_play` 的取舍——公开详情匿名可读,不该因此掉 401),因此该响应与纯匿名逐字节相同(单测断言)。实现复用既有 `auth::optional_access_token_from_headers`(`auth.rs:223`),**没有自造 extractor**;值来自新 procedure(真实投影),**未改动既有公开快照契约**
- **共享缓存核实(硬性项)**:公开详情所在的 `public_games` 路由组带 `add_no_store_response_headers`;`app::build_router` 只做 merge/背压/错误归一化/request_id/tracing/`attach_request_context`,**没有任何响应体缓存或预渲染**;本模块唯一进程内缓存是 `RELEASE_PACKAGE_CACHE`(键 = OSS 对象键、值 = 发行包字节),与 per-user JSON 无关。结论:**该路径无共享缓存,不需要绕开**
- 错误码:作品不存在 → 404;未公开/已软删除/无当前公开版本 → 409(文案「作品状态不允许收藏(未公开或已软删除)」刻意含「状态」而不含「已被删除」,以命中既有映射的 409 分支而非 404 分支)
- 测试(本文件内,新增 5 条):三条路由无 Bearer → 401 且 `no-store`;404/409/摘要不一致 409 的映射;摘要绑定 `(user, game)` 的幂等隔离;PUT/DELETE 的载荷形状;公开详情「匿名无 collected 且与目录负载逐字节相等、登录时 Some(true)/Some(false) 各出现且只多这一个键」
- 门禁:`cargo test -p api-server game_distribution` **70 passed**(含新增 5 条);`cargo check --all-targets` 0
2026-10-06 01:13:37 +08:00
suzmii 77c30bb7b2 feat(游戏共创): 收录(收藏)数据模型与绑定(块 1/3)
- `spacetime-module`:新增表 `game_distribution_collection`(`collection_id` 主键、`user_id` / `game_id` 各具名 btree 索引、`created_at`)。**防重靠确定性主键** `collection_id = "{user_id}:{game_id}"`(与 `profile_save_archive.archive_id` 同一写法):同一 (用户, 作品) 在结构上不可能有第二行,不依赖「事务里先查后写」(那种写法只在单写者假设下成立,分片/并发时两个事务都可能先查到不存在再各写一行);正因主键即约束,**不需要**再建 `(user_id, game_id)` 唯一索引,两个索引只服务两个读取方向
- 下架(未公开/软删除)**不删除**收藏行:收藏是用户态事实,读侧投影时才按当前公开性过滤,作品重新公开后同一行自动回到列表(注释写明)
- 新增动作常量、4 个输入类型、3 个结果类型、3 个事务(collect / uncollect / list)+ 读态 helper + 4 个 procedure;`uncollect` 按确定性键删除且**不要求作品仍公开**(下架后拒绝取消会给用户留下清理不掉的脏行);`list` 走 `user_id` 索引 + 既有 `public_game_distribution_snapshot` 投影,只含当前公开可读作品
- 新增 `module-game-distribution/src/collection.rs`:两条纯函数(`game_distribution_collection_id`、`game_distribution_collection_visible`)+ 单测——后者用「同一行、四种可见性输入」直接钉住「已收藏 → 下架 → 列表不含 → 重新公开 → 列表又含」这条链路,不必起真库
- `migration.rs`:新表进白名单并补中文注释
- `spacetime-client`:record + 3 个 mapper + 4 个方法 + 2 个 RecordInput,并按既有做法只保留 `module_bindings/game_distribution*`(13 个新文件)与 `module_bindings.rs` 的生成改动、还原其余 rustfmt 漂移
- 门禁:wasm build 0;`cargo check --all-targets` 0;`cargo test -p module-game-distribution` 75 passed;`cargo test -p spacetime-module` 273 passed / 1 ignored(含 4 条新事务结构断言);`cargo fmt --all -- --check` 0
2026-10-06 01:13:08 +08:00
suzmii c3030eddba chore(游戏共创): 收尾清掉最后一条旧口径注释(成品包只作可玩参考 → 生成脚手架 + 参考副本)
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
2026-10-06 01:06:20 +08:00
suzmii f94946ef8e feat(游戏共创): 注册并处理 Fork 深链 genarrative://fork?gameId=…
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
冻结约定:`genarrative://fork?gameId=<gameId>`,同时接受 `genarrative://fork/<gameId>`。

方案(用官方两个插件,不手搓单实例与注册表):
- `tauri-plugin-single-instance`(开 `deep-link` feature)**第一个注册**:Windows/Linux 上深链是
  「带 URL 启动第二个进程」,该插件把第二个实例的 argv 转交给已有实例,并(feature 生效时)
  交给 deep-link 插件发事件——否则用户点第二次链接会再开一个窗口。macOS 上它用 Unix socket
  做同样的收口(系统本身也只会有一个 .app 实例)。
- `tauri-plugin-deep-link`:`tauri.conf.json > plugins.deep-link.desktop.schemes = ["genarrative"]`。
  · macOS:打包器按该配置往 `Info.plist` 注入 `CFBundleURLTypes`(插件 `config.rs` 里那句
    `// Used in tauri-bundler` 是代码级证据),运行时由系统把 URL 交给已运行的 .app。
  · Windows:运行时 `register_all()` 写 `HKCU\Software\Classes\genarrative`(`URL Protocol` 标记 +
    `DefaultIcon` + `shell\open\command`,不需要管理员);安装器侧同样按该配置注册。
  · Linux:运行时写用户级 `applications/<exe>-handler.desktop` 并调 xdg-mime(AGC 未以此平台分发,
  但仍走同一条运行时路径)。
- 冷启动:插件在插件初始化阶段就解析 argv 并暂存,而应用 `setup` 晚于插件,因此
  `initialize_fork_deep_link` **先取 `get_current()`**、再 `on_open_url` 订阅后续链接(漏了前者会
  丢冷启动那一次)。
- 收到 URL 后:聚焦主窗口(unminimize + show + set_focus)→ 发 `game-fork-deep-link` 事件
  `{ gameId | message }` → 渲染层把请求存进 store 并切回首页;**不自动下载**,用户按
  「Fork 到我的项目」才走与手工输入完全相同的提交路径。正停在项目工作区时不强行切走
  (避免把用户从进行中的项目里踢出来),请求留在 store 里等用户回到首页。
- 非法链接:解析出可读原因(空链接 / 非 genarrative 方案 / 非 fork 形式 / 缺 gameId / gameId 不合法)
  一并聚焦窗口并交给渲染层展示,不做静默失败。

测试:`fork_game_id_from_deep_link` 的合法(两种形式、大小写、多余参数、game_id/id 别名)与
非法(空、错方案、错形式、缺 gameId、`../escape`、URL 编码斜杠、非 URL)各一组;`gameId` 形状
判据与取件路径共用(`is_safe_fork_game_id`);TS 侧覆盖载荷解析、订阅映射、深链预填后**不自动
下载**、非法链接展示原因且不请求。
2026-10-06 01:05:04 +08:00
suzmii 96309c895c 新增「原版与二创对比」页与路由登记(含 nginx×3 + Pingora allowlist)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 新增路由 /games/compare(stage game-compare):STAGE_ROUTE_ENTRIES + SelectionStage 类型 + 壳层 lazy 引入/导航回调/渲染分支 + 页面标题,四处同批登记
- 三份 nginx SPA allowlist(deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、deploy/container/nginx.conf)与 Pingora MAIN_SPA_PATHS 同步放行 /games/compare(保持既有排序):check:nginx-spa-routes 18 SPA routes、check:pingora-route-parity 均绿
- 对比页左右两侧各自取数与降级:任一侧不可读(不存在/未公开/已软删除)或没有可玩版本时只降级那一侧,另一侧照常;显示标题、作者与代际;桌面左右并排、窄屏上下堆叠
- iframe 复用游玩页同一份嵌入:抽出 GamePlayFrame(sandbox=allow-scripts、allow=fullscreen、referrerPolicy=no-referrer、.game-player-frame),游玩页改为渲染它,不再存在第二套嵌入逻辑
- 入口:二创作品详情页在存在父作品时给出「与原版对比」;族谱节点仅在其父作品可见时给出同一入口
- 测试:对比页两侧渲染与 iframe 复用属性、单侧降级后另一侧照常;详情页入口(有/无父作品);族谱节点入口(父可见才出现);路由与标题登记;壳层「族谱节点→对比页」导航与对比 stage 渲染
- 顺手:按 eslint 自动修复两个文件的 import 排序
2026-10-06 00:56:31 +08:00
suzmii 4cf00ac07d refactor(游戏共创): Fork 口径收敛为「把整个工程复制到你的项目里继续改造」
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
产品裁定:不再对外解释「可玩参考 + 素材 / 不能一键复刻工程 / 需自建合规工程」那套口径。
统一一句话:**Fork = 把整个工程复制一份到你的项目里继续改造(与 git fork 同理)**,动作名统一
叫 Fork。必要告知只保留一句:开启共创并上传工程时,整个工程(含素材与文档)会对他人可见,
且不可撤销。

- 入口(首页):`从平台作品 Fork` + 角标 `Fork`,说明压成一句「把平台上的作品整个工程复制到
  你的项目里继续改造(像 git fork)。输入要 Fork 的作品 ID 或作品链接即可。」;按钮
  `Fork 到我的项目`、加载态「正在 Fork:下载、校验并创建项目…」;成功态只留
  「已创建项目《X》,Fork 自平台作品 <gameId>(来源已记录在 …fork-source.json)」,
  删掉按取件形态分岔的长句。
- 发布面板:授权区只留一句「授权非「禁止共创」即允许他人 Fork 你的作品;开启共创并上传工程时,
  整个工程(含素材与文档)会对他人可见,且不可撤销。」;确认项压成一句
  「我已确认工程里没有不愿公开的内容,同意随本次发布公开整个工程(含素材与文档);一经发布
  不可撤销。」,其后一行写「默认已勾选,可随时取消;不勾选则本次不上传工程源码。」
- 错误与进度文案:取件失败四态改为 Fork 措辞(`无法 Fork` / `该作品当前不能被 Fork(未公开、
  已下架或没有公开版本)`),读取/下载/响应类错误改为 `读取 Fork 信息失败` / `下载 Fork 内容失败`,
  超限提示改为「Fork 内容超过客户端下载上限,无法完成」;「未上传工程源码」提示改为
  「他人 Fork 时只能拿到平台已发布的成品」;默认项目名前缀 `改编项目` → `Fork 项目`;
  记录标签 `项目改编来源记录` → `项目 Fork 来源记录`。
- 实现事实(成品包形态的脚手架 + `reference/<gameId>/`)保留在 Rust 文档注释里,不再作为对外
  表述;`collect_project_bundle_files` 等实现注释不动。
- 测试:入口用例改为断言一句话口径与「Fork 自平台作品」并断言旧的解释性表述已消失;面板用例
  断言新的简短告知与「默认已勾选,可随时取消;不勾选则本次不上传工程源码」;错误文案断言同步。
2026-10-06 00:55:53 +08:00
suzmii 928fb0e616 Fork 入口主路径改为唤起客户端(genarrative://fork),未失焦才降级为手动路径
- 新增 src/components/game-distribution/gameForkProtocol.ts 收口唤起:协议 genarrative://fork?gameId=<id>,用顶层 location.href 赋值(协议未注册时桌面浏览器只是「什么都不发生」,不会把页面导航走也不会被弹窗拦截,失败可观察),并导出降级等待窗口 FORK_LAUNCH_FALLBACK_DELAY_MS=1200
- 详情页点击「Fork 这个作品」直接唤起;失焦(window blur)或页面转入后台(visibilitychange=hidden)都视为客户端接手 → 只给状态反馈,不出降级面板
- 未失焦 → 经过降级窗口后展示手动面板(作品 ID / 完整作品链接 / 复制按钮 / 客户端指引 / 一句话告知)
- 移动端与未知平台不尝试唤起(协议未注册时移动浏览器行为不可控),直接给手动路径
- 每条路径都有可观察反馈:正在唤起 / 已唤起 / 没有检测到客户端 / 当前设备无法直接唤起;定时器与失焦监听的清理覆盖卸载、重复点击、超时三条路径
- 顺手修掉同文件一个既有竞态:作者昵称补查的断言紧跟 findByRole(effect 尚未发出请求)导致偶发 0 次调用、让整份文件的门禁随机变红,改为 waitFor 等待
2026-10-06 00:50:28 +08:00
suzmii b6fe63b551 文案收敛:动作名统一 Fork,去掉「可玩参考 + 素材 / 不能重新发布」那套表述
- 动作名统一:「改造这个作品」→「Fork 这个作品」,「登录后可改造」→「登录后可 Fork」
- 面板指引改为「在陶泥儿客户端里打开「从平台作品开始创作」,粘贴下面的作品 ID 或作品链接。」
- 对外口径统一为「Fork = 把整个工程复制一份到你的项目里继续改造(与 git fork 同理)」,并保留一句话必要告知:整个工程(含素材与文档)复制后对他人可见,且不可撤销
- 删除「成品包只能可玩参考 + 素材」「不能直接再次发布」「参考产物在 reference/<作品ID>/」等展开说明(冻结口径 1)
- 测试同步:断言新文案与「不可撤销」告知,测试名与注释不再出现「改造」
2026-10-06 00:48:23 +08:00
suzmii bdcefe91d1 Merge remote-tracking branch 'origin/master' into feat/game-fork
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m31s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m3s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Backend tests (pull_request) Successful in 7m36s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m26s
Project CI / Repository checks (pull_request) Successful in 6m25s
Project CI / Native shell tests (pull_request) Successful in 9m34s
# Conflicts:
#	deploy/container/nginx.conf
#	deploy/nginx/genarrative-dev-http.conf
#	deploy/nginx/genarrative.conf
#	server-rs/crates/api-server/src/modules/game_distribution.rs
#	src/components/game-distribution/GameDetailPage.tsx
#	src/components/game-distribution/GameDistributionPages.test.tsx
#	src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
2026-10-06 00:19:40 +08:00
lhk229 51cb05f418 补充创作者主页与关注粉丝工程文档 (#638)
Project CI / AI game creator shell Rust crates (push) Successful in 5m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 7m37s
Project CI / Backend tests (push) Successful in 9m22s
Project CI / Frontend tests (push) Successful in 4m40s
Project CI / AI game creator shell web tests (push) Successful in 3m50s
Project CI / Repository checks (push) Successful in 6m30s
Project CI / Native shell tests (push) Successful in 8m32s
本 PR 补齐创作者主页与关注粉丝功能的产品合同和工程设计,供后续按里程碑实施;本次只修改文档。

桌面导航第四项进入自己的创作者主页,“我的”移到第五项。游戏详情可进入作者主页并关注/取消关注;自己或他人的关注、粉丝列表均公开可查看,点击用户头像或昵称可进入其主页。本人支持取消/重新关注、回关和移除粉丝,并明确两个关注方向独立。

工程设计包含关系表与 DTO、认证和事务边界、跨页状态同步、共享组件、深链及验收要求,拆分关系查询和页面交互两个里程碑。按用户要求,游戏列表沿用最多 48 项的现有限制,不增加游戏目录分页改造。

验证:
- 已合入最新 origin/master(21c79f54a);共享决策记录冲突保留双方新增内容。
- npm run check:encoding 通过。
- npm run check:doc-index 通过。
- git diff --check 与暂存差异检查通过。
- 相对 master 仅六份文档发生变化;没有业务代码或 schema 修改,未执行业务测试或部署。

Reviewed-on: #638
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-10-05 22:05:11 +08:00
suzmii 96859666ff Merge pull request 'AGC 运行画面点选:canvas 玩法上识别 Phaser 4 / three.js 的已登记素材(#612)' (#624) from feat/agc-runtime-picking-engines into master
Project CI / AI game creator shell Rust crates (push) Successful in 5m29s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 7m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m28s
Project CI / Backend tests (push) Successful in 9m14s
Project CI / Frontend tests (push) Successful in 4m4s
Project CI / AI game creator shell web tests (push) Successful in 3m36s
Project CI / Repository checks (push) Successful in 7m37s
Project CI / Native shell tests (push) Successful in 10m32s
Reviewed-on: http://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/624
2026-10-05 19:16:03 +08:00
suzmii fa42cdb139 Merge pull request '修复在线游玩加载体验:加载期黑屏/空白与发行资源传输开销(#636)' (#639) from fix/game-play-loading into master
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: http://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/639
2026-10-05 19:13:44 +08:00
suzmii df221f4eb9 收口评审 P2:点选条款限定引擎档、对象名上限入边界、素材 id 格式写入 skill
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m29s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m6s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m50s
Project CI / Frontend tests (pull_request) Successful in 3m26s
Project CI / Backend tests (pull_request) Successful in 7m48s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m3s
Project CI / Repository checks (pull_request) Successful in 7m15s
Project CI / Native shell tests (pull_request) Successful in 9m30s
- 功能说明第 8 条「命中区域裁剪在画布内」限定为引擎档,并说明 DOM 档仍是元素矩形;证据表对应行同步对齐
- 功能说明「边界与非目标」新增一条:引擎档对象名超过 40 字符按取不到处理(有意保留),宿主侧标签上限另为 120
- SKILL.md「运行画面点选契约」第 3 条补一句:素材 id 不含空白与路径分隔符(localAssetId 由客户端生成)
- skill-pack 指纹同步(agc-web-game-development version 2026-08-26.42 与 sha256)
2026-10-05 18:31:59 +08:00
suzmii 364b1aa342 Merge remote-tracking branch 'origin/master' into feat/agc-runtime-picking-engines
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 冲突解决(3 处):
  - agc-game-production-workflow/SKILL.md:双方都保留——第 4 条用本分支版本(含「运行画面点选契约」指针),第 5 条用 master 的新构建验证口径
  - agc-skills/manifest.json:先取 master 侧,再用 `agc:skill-pack:sync` 按合并后内容重新生成 version 与指纹(2026-08-26.41),`skill-pack:check` 通过
  - pitfalls.md:两侧新增条目全部保留(按日期倒序:本分支 10-05 / 10-04 条目在前,master 的 10-04 / 10-03 条目紧随,未改动任一方内容)
- 无冲突但人工核对:`agc-web-game-development/SKILL.md`(本分支「运行画面点选契约」小节 + master 内容都在)、
  `index.tsx`(我们的 `handleRuntimeInspectSelection` 回调体逐字不变,master 新增内容都在)
- 桥脚本 sha256 未变(9513020cfffa79128fb6355d44cdaabe666eefecbd96b7e807b15d2c4601bf76),真机证据对应字节不变
2026-10-05 18:25:49 +08:00
suzmii d7c4f82d7d 修复自查发现的门禁与体验问题:格式、超时态语义、压缩 CPU 边界
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m48s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m33s
Project CI / Frontend tests (pull_request) Successful in 3m24s
Project CI / Backend tests (pull_request) Successful in 7m29s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m55s
Project CI / Repository checks (pull_request) Successful in 6m33s
Project CI / Native shell tests (pull_request) Successful in 9m0s
- 新增 Rust 测试补齐 cargo fmt;ESLint 按仓库约定收口:常量与解析函数拆到 platformGameLoadingSurfaceModel.ts,删除 GAME_PLAY_STARTUP_TIMEOUT_MS 常量别名(测试直接用共享常量)
- 共享加载面新增 stalled:宿主判定等待不再推进时停掉进度动画并把 aria-busy 置回 false(去掉与 role=status 重复的 aria-live)
- 后台试玩超时时传入 stalled,避免「一边转圈一边说超时」
- Accept-Encoding 权重参数名改为大小写不敏感(RFC 9110 §12.5.3),GZIP;Q=0 视为拒绝并补用例
- 发行资源压缩分级:≥2 MiB 改用 level 1(zlib 端实测约 1/3 耗时、压缩比差约 3%),小文件仍用 level 6
- 文档:主规范补压缩与条件请求契约(并写明 60 秒窗口与撤销窗口不放宽)、nginx README 修正 Pingora 压缩关闭时的口径、pitfalls 补 CPU 边界与措辞修正
2026-10-05 18:02:48 +08:00
suzmii 126fb7bc06 test(游戏共创): 两个浏览器 e2e 的登录等待抗冷启动(Vite 首次编译)
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m3s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m53s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m31s
Project CI / Frontend tests (pull_request) Successful in 2m58s
Project CI / Backend tests (pull_request) Successful in 7m44s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m6s
Project CI / Repository checks (pull_request) Successful in 6m45s
Project CI / Native shell tests (pull_request) Successful in 9m32s
- rebase 到 origin/master 后的第一次实跑里,lineage 与(同源的)fork 脚本在浏览器段都卡在
  `page.waitForSelector('.platform-account-entry', { timeout: 120_000 })` 并超时:
  Vite dev server 重启后首次导航要现编译整张模块图,rebase 引入大量新代码时首屏会超过 120s。
  证据:两次运行都在该行超时退出;随后用 Playwright 直接探针(同一 URL)页面正常渲染、
  `.platform-account-entry` count=1、无 pageerror(仅两条未登录态 401 的资源错误),
  紧接着再跑同一脚本即 99/99 全绿。
- 修法:登录等待改成 240s + state:attached,并在超时后重载一次再等(避免把「开发服务器还没编译完」
  误判成页面坏了)。两处改动逐字相同,lineage 与 fork 脚本各一处。
- 实测(HEAD 3ae0d129e,固定端口栈 3110/8188):lineage 99/99、fork 48/48、project-bundle 72/72;
  三条只读门禁 policy-parity / dto-parity / spacetime-schema 均 exit 0。
2026-10-05 17:52:16 +08:00
suzmii 05cf5b9547 docs(游戏共创): 修掉 §3.5.2 里那处嵌套加粗
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- `docs/【技术方案】游戏共创与作品Fork-2026-10-03.md:362`:`- **服务端复核(已落地,**不信任客户端**)**:` → `- **服务端复核(已落地、不信任客户端)**:`(去掉内层多出的一对 `**`)
- 之前改不掉的原因搞清楚了:edit 工具的模糊匹配按**整行**比对,我先前传的是该行前缀(该行还有 300+ 字符),相似度自然只有六七成;这次把**整行**(429 字符)作为 old_string 即一次命中。渲染上本来就是连续加粗,本次只是源码观感与「别让人以为漏了个闭合标记」
- 门禁:check:doc-index 248 份 OK;check:encoding 5305 files OK;git diff --check 0
2026-10-05 17:44:56 +08:00
suzmii 3ae0d129ef test(游戏共创): 一致性门禁把内容嗅探与 magic bytes 纳入比对(补最后一处规则空白)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- `scripts/check-project-bundle-policy-parity.mjs` 新增第 4 维:**凭据内容嗅探与嵌套包 magic bytes**。复核指出这两套「换名字也拦得住」的规则只靠常量表表达,过去门禁对 `sniff|PEM|AKIA|magic|secret` 零命中(只有看得见的规则才有守卫),本次补齐:
  · `SECRET_CONTENT_TEXT_EXTENSIONS` / `SECRET_CONTENT_SIGNATURES`(字符串数组集合相等)、`SECRET_CONTENT_MAX_SNIFF_BYTES` / `NESTED_ARCHIVE_SNIFF_BYTES`(整数相等)、`SECRET_AWS_ACCESS_KEY_PREFIX` / `PEM_PRIVATE_KEY_LINE_PREFIX` / `PEM_PRIVATE_KEY_MARKER`(字符串逐字相等)——新增 `rustStringConst` helper 抓 `&str` 常量
  · `nested_archive_format` 里的 `b"..."` magic 字面量按「服务端 ⊆ 客户端」比对(客户端可以先拦、不能漏拦),并用新的 `byteStringLiterals` helper 抽取;两侧空集合一律 throw
- **「故意改一侧会红」已验证(两处,均已原样恢复)**:① 从客户端 `BUNDLE_FORBIDDEN_PATH_CHARS` 去掉 `'?'`/`'*'` → 报两条「路径段禁止字符…客户端没有」;② 从客户端 `SECRET_CONTENT_SIGNATURES` 去掉 `ghp_` → 报「嗅探规则不一致:服务端 …=ghp_|github_pat_|…,客户端 …=github_pat_|…」
- 文档 §3.5.2 与 §5.2 同步门禁的四个覆盖维度(目录/前缀/后缀/全名 + 上限、路径形状、内容嗅探特征表、magic bytes),并把 §5.2 的门禁行从「P1-a」扩为「P1-a + 嗅探维度」
- 门禁:`check-project-bundle-policy-parity` 0(服务端 51 条规则全被客户端覆盖;嗅探:`ghp_/github_pat_/xoxa-/xoxb-/xoxp-` + `AKIA` + PEM 标记;magic:7z / PK / Rar! / gzip / ustar;窗口 512 字节两侧一致);`check:doc-index` 248 份 OK;`check:encoding` 5305 files OK;`git diff --check` 0
2026-10-05 17:42:46 +08:00
suzmii 15024dab0a docs(游戏共创): M2b 证据表与里程碑改为冻结点的实测数字(含补强证据与已知留白)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 技术方案 §5.2 三处过期数字按**自己去代码里数一遍**的结果改:校验器单测 16 → **19**(`project_bundle::tests`)、模块总测 69 → **73**、e2e 由「46 项 46 PASS」改为「**51 个 `check(` 调用点**」(复核另按 72 条断言计,差值来自同一 `check` 内的多条子断言);「`package.rs` 零改动」改为「已同批补强(读取层按声明封顶 + 压缩比分母改为条目压缩字节,+70/−4)」;客户端行补「打包器 **24 条单测**、上传前按 `max_archive_bytes` 预检、定向 `cargo test -- project_bundle` **24 passed**」
- 技术方案 §5.2 新增四行:**校验器对抗性补强(P0-a/b/c)**(凭据目录/前缀/后缀/全名 + 小文本内容嗅探、嵌套包扩展名并集 + magic 嗅探、读取层封顶、新错误变体 `CredentialDirectoryNotAllowed` / `SecretContentDetected`)、**执行级交叉证据(P1-b)**(dev-dependency 交叉测试)、**一致性门禁路径形状维度(P1-a)**(含「故意破坏会红」实测)、**已知留白**
- 里程碑:`前置依赖 ②` 里「客户端预检属待办」改为**已完成**并给出代码位置(`write_project_bundle_zip` 内 `bytes.len() > limits.max_archive_bytes` 失败关闭);证据行与验收项重数、并补齐补强后的新增规则;新增「已知留白」条目(`.bz2`/`.xz`/`.zst`、`*.db`/`*.sql`、`.gcloud`/`.azure`/`.password-store`、CP437 条目名与 `external_attributes == 0`、峰值内存未量测、内容嗅探误报率无实测)
- 本块在 rebase 之后完成,基线为新的 merge-base `21c79f54a`
- 门禁:`check:doc-index` 248 份 OK;`check:encoding` 5305 files OK;`git diff --check` 0
2026-10-05 17:41:11 +08:00
suzmii df43ffd05a test(游戏共创): 工程源包 e2e 补对抗用例(路径/凭据/嵌套包/符号链接/声明说谎)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- scripts/check-game-distribution-project-bundle-e2e.mjs 新增 A7 对抗段(脚本内自带裸 ZIP 写手,
  可逐字节控制条目名、unix mode 与 central directory 声明值),逐条断言 422 + details.reason:
  · A7-1 路径穿越:foo/../bar、/etc/passwd、C:/evil.txt、a\..\b、./x、a//b → InvalidPath
  · A7-2 路径形状:a/secret.、a/secret(尾随空格)、src/a?.ts、a*b.ts → InvalidPath
  · A7-3 大小写变体:Node_Modules/… → DependencyDirectoryNotAllowed;.GIT/HEAD → VersionControlDirectoryNotAllowed;.AGENT/x → LocalStateDirectoryNotAllowed
  · A7-4 符号链接条目(external attrs=0o120777)→ SymlinkNotAllowed
  · A7-5 嵌套包改名 deps.dat(zip magic)→ NestedArchiveNotAllowed
  · A7-6 凭据:.aws/credentials、.ssh/id_ecdsa → CredentialDirectoryNotAllowed;.htpasswd、service-account-prod.json、terraform.tfstate、app.p8 → SensitiveFileNotAllowed
  · A7-7 内容嗅探:无扩展名文件里的 PEM 私钥块 → SecretContentDetected
  · A7-8 声明说谎:STORE 条目声明 1 字节、实际 4096 字节 → ReadFailed(失败关闭;非内存量测)
  · A7 收尾:全部拒绝后该版本仍 bytes=0/sha256 空;对照包(.dat/文本/无扩展名/PNG)200 不被过度拦截
- 修正一处我此前的错误判断:`./x`/`a//b` 并非「被 zip crate 归一化后放行」,而是被校验器自身的
  路径形状检查(空段/`.`)拒为 InvalidPath(zip-2.4.2 types.rs:537-555 只拒 NUL/根/`..` 逃逸,
  且返回未归一化原串);脚本 NOTE 已按实测与源码改正,两条也改为严格 422 断言
- 实测:本地 dev 栈(SpacetimeDB 3110 / api-server 8188)72 项 72 PASS / 0 FAIL / 0 SKIP;
  同栈回归 lineage-e2e 99/99(真实发行包上传,覆盖 package.rs 读取封顶改动)与
  fork-authorization-e2e 48/48
2026-10-05 17:30:50 +08:00
suzmii 88368d0418 test(游戏共创): 一致性门禁补「路径形状」维度,并同步校验器文档
- `scripts/check-project-bundle-policy-parity.mjs` 新增第 3 维:服务端 `normalize_archive_path` 的拒绝形状必须被客户端打包器的三个常量逐 token 覆盖——**哨兵段**(`.` / `..`)与**路径段结尾字符**(空格 / 点)分别用 `rustStringArray` / `rustCharArray` 抽取,**路径段禁止字符**同时抓 `.contains('x')` 与 `matches!(expr, 'a' | 'b')` 两种写法;再单独钉「两侧都必须显式拒 `/` 开头与 `\`」两条独立分支(它们靠常量表覆盖不到)。**空集合一律 throw**,沿用既有「抽不到不许当绿灯」的写法
- 修掉我自己在第一版里写错的两处(由补强过程中的实测发现,并已用临时副本验证修法):① `BUNDLE_FORBIDDEN_PATH_SEGMENTS` 是 `&[&str]` 而非 `&[char]`,须用 `rustStringArray`(`".."` 也无法用 char 字面量表达);② 反斜杠分支的正则写成了 4 个字面反斜杠,而源码是 `contains('\\')`(2 个),已改为 `/contains\('\\\\'\)/u`
- **「故意破坏会红」已验证**:临时从客户端 `BUNDLE_FORBIDDEN_PATH_CHARS` 去掉 `'?'` 与 `'*'` → 门禁立即报两条「路径段禁止字符:服务端有「?」/「*」,客户端没有」,随后已原样恢复
- 文档 `§3.5.2/§3.2.3`:校验器清单同步到新规则(新增目录/前缀/后缀/全名 + 内容嗅探及其取舍理由、嵌套包扩展名并集与 magic 嗅探、读取层封顶与压缩比口径更正、路径形状维度),并新增**产品口径「知情同意」**一条——清单是黑名单,`docs/`、`*.pdf`、`notes.txt`、截图等不在任何拒绝集内会原样外发,发布面板必须写明「整个项目目录(除少数排除项)会原样公开」
- 门禁:policy parity 0(服务端 51 条全被客户端覆盖 + 路径形状维度 OK);`check:doc-index` 248 份 OK;`check:encoding` OK;`git diff --check` 0
2026-10-05 17:30:50 +08:00
suzmii 10d1fcbc5f fix(游戏共创): 工程源包校验器对抗性补强(P0-a/b/c + 执行级交叉证据)
对抗性复核(`local://verify-m2b.md`)确认三个真实缺口,本次按 P0 → P1-b 补齐;两端规则逐条对齐,parity 门禁仍绿。

- **P0-a 凭据/依赖识别**:任意层级目录新增 `.aws` / `.ssh` / `.kube` / `.docker` / `.gnupg` / `.terraform` / `.secrets`(服务端新增独立变体 `CredentialDirectoryNotAllowed`);文件名前缀新增 `credentials` / `id_ecdsa` / `id_dsa` / `terraform.tfstate` / `service-account`;后缀新增 `.jks` / `.keystore` / `.ppk` / `.p8` / `.kdbx` / `.der`;全名新增 `.htpasswd` / `.pgpass`。另外对**小体积文本条目**(≤256 KiB、扩展名在白名单或无扩展名)做内容特征扫描:PEM 私钥块、`AKIA`+16 位、`ghp_`/`github_pat_`/`xox*-`(服务端新增 `SecretContentDetected`);更宽的前缀启发(如 `sk-`)**故意不做**并在注释里写明理由(误报会直接阻断作者发布)
- **P0-b 规模检查不再被声明值骗**:两侧校验器改为**读取层封顶**——`by_index(index)?.take(declared_size + 1)` + 预分配收紧到单文件硬上限 + 多读 1 字节即判溢出、短读同样失败;压缩比分母从整包字节改为**该条目自身的压缩字节**;**同一修法同步到发行包校验器**(`package.rs` 原有同款洞),常量注释改成与实现一致(明确它不是整包炸弹防线)
- **P0-c 嵌套包识别**:扩展名并集 `.zip/.tar/.gz/.tgz/.7z/.rar/.jar/.whl/.nupkg` + **magic bytes 嗅探**(zip/7z/rar/gzip/tar-ustar),改名成 `.dat` 也拦。**一处有证据的偏离**:`by_index_raw` 拿到的是**压缩流**(zip 2.4.2 文档与实测一致),按它嗅探会漏拦,因此改为对**已按声明大小封顶读出的解压内容**判 magic;客户端命中即报错并点名路径与格式
- **P1-b 执行级交叉证据(走 dev-dependency 路,未退化到 fixture)**:客户端 crate 加 `[dev-dependencies] module-game-distribution`,新测试用真实打包器产出字节喂给服务端 `validate_project_bundle_zip` 断言接受(并逐项比对摘要/字节数/条目顺序),再手造含 `.env` 与 `node_modules` 的包断言服务端独立拒绝
- 客户端同时补上**路径形状检查**(`BUNDLE_FORBIDDEN_PATH_CHARS` / `_SUFFIX_CHARS` / `_SEGMENTS` + `bundle_entry_path_shape_error`),修掉「macOS/Linux 上 `src/a?.ts`、`x.` 客户端能打、服务端必 422」的不一致;客户端压缩比预检也改按条目压缩字节
- 测试:`cargo test -p module-game-distribution` **73 passed**(+4);AGC `cargo test -- project_bundle` **24 passed**(含交叉证据那条);**发行包回归**:`cargo test -p api-server game_distribution` **60 passed / 0 failed**(`package.rs` 改动无行为回归,新增「声明说谎 → ReadFailed」用例)
- 门禁:`cargo check --all-targets` 0;两端 `cargo fmt --check` 0;policy parity 0(服务端 51 条全被客户端覆盖);DTO parity 0;schema 0;encoding 0;`git diff --check` 0
2026-10-05 17:30:50 +08:00
suzmii 18d19d5d54 fix(游戏共创): 源码公开确认改回默认勾选(与「允许共创即开放源码」口径一致)
按产品裁定回落默认值:保留主动取消能力与全部知情告知,但确认项**默认已勾选**——
「允许共创本身就意味着源码开放,不设独立开关」,默认不勾选等于第二个开关,与本面板
自己写着的那句「没有单独的源码可见性开关」自相矛盾。

- `projectBundleConfirmed` 初值 true;每次重新选择授权档位都重置为「非禁止档位即已勾选」,
  避免把上一次的取消状态带到新的档位选择上(禁止共创时不显示该确认项,值本身无意义)。
- 告知文案一字不减(整个项目目录原样公开、含文档/截图/设计稿/笔记、只自动排除依赖/版本库/
  构建产物/常见凭据、一经发布不可撤销、无法收回已下载内容)。
- 新增一句提示说明默认值来源:「上面这项默认已勾选(平台按「允许共创即开放源码」的默认帮你
  勾上,不是替你做决定——你可以随时取消)。」——避免作者以为平台偷偷替他决定。
- 未勾选时的后果提示保留,仍写清「不勾选则本次不上传工程源码……」。

测试(面板 23 条):默认「禁止共创」无确认项;切到非禁止档位后确认项出现且**已勾选**,
并逐条断言告知文案 + 「默认已勾选/可随时取消」;默认状态提交不携带 `includeProjectBundle`
(交给原生按授权执行上传);作者主动取消后提交携带 `includeProjectBundle: false`(这是
「作者能退出」的证据);重新选择档位回到已勾选;切回「禁止共创」确认项消失。
2026-10-05 17:30:50 +08:00
suzmii 7972089a5f fix(游戏共创): 工程源码公开改成作者的主动知情确认,文案如实告知外发范围
背景(对抗性复核 D.7):工程源包的拒绝规则是**黑名单**,作者的 `docs/`、`*.pdf`、`notes.txt`、
截图、设计稿、会议记录等不在任何拒绝集内,会被原样外发。也就是说「授权非禁止 + 上传工程源码」
= **整个项目目录(除少数排除项)对任何登录用户公开**。原来那句「同时公开工程源码……取消勾选则
只能被试玩」是免责声明式措辞,不足以构成知情同意。

文案(最终措辞):
- 授权三态说明统一口径:「授权非「禁止共创」即允许他人改编(**没有单独的源码可见性开关**);
  是否随本次发布把工程源码一起交出去,由下面的确认决定。上架后授权只能单向提升,不能撤销。」
- 确认项(仅授权非「禁止共创」且首次发布时出现):
  「我已确认项目里没有不愿公开的内容,同意随本次发布公开**整个项目目录**:源码、素材与其中
  其它文件(含文档、截图、设计稿、笔记等)都会对任何登录用户公开;只会自动排除依赖目录、
  版本库、构建产物与常见凭据文件。一经发布**不可撤销**:即使之后把授权降级,也无法收回别人
  已经下载的内容。」
- 紧随其后写清不勾选的后果:「不勾选则本次不上传工程源码:作品仍可正常发布,但他人只能拿到
  已构建成品试玩与参考,不能直接重新发布。」

交互选择:**默认不勾选 + 不勾选就不上传**(不是禁用发布按钮)。
- 选它的理由:① 它把第 3 点做成了真正的「主动确认动作」;② 发布本身不被阻断——工程包缺失本来
  就不影响发布(服务端与原生链路都按「可选资产」设计),禁用按钮反而会把「禁止共创 / 只想先发
  出去」的作者一起挡住;③ 勾选状态只作用于本次版本上传,不引入平台侧的独立开关,授权语义不变。
- 注意:这一条**取代**了早先「授权非禁止时默认帮作者上传」的默认值——知情同意优先于默认帮忙;
  若要把默认值改回勾选,只需把 `projectPackageConfirmed` 初值改成 true 并翻转对应断言。

测试:改写两条面板用例——授权开放后确认项出现且**默认未勾选**、九处告知文案逐条断言(整个项目
目录 / 含文档截图设计稿笔记 / 只自动排除四类 / 不可撤销 / 无法收回已下载内容 / 不勾选则不上传 /
没有单独的源码可见性开关)、未确认时显式传 `includeProjectBundle: false`(不上传但照常发布)、
确认后不带该字段(由原生按授权执行上传)。
2026-10-05 17:30:49 +08:00
suzmii 843ecbf4a2 chore(游戏共创): 删掉打包器的 dead_code 豁免,并只保留真正被用到的范围分支
上传链路已在 `5dd9d1c5d` 接好,`#![allow(dead_code)]` 的豁免期结束:

- 删掉模块级 `#![allow(dead_code)]`,注释改成写明调用方(发布链路的
  `upload_project_bundle_for_version`,取件侧摘要校验共用 `sha256_hex`)。
- 移除豁免后只剩一条 dead code:`ProjectBundleScope::GameDirectory` 从未被生产代码构造
  (发布链路一律上传整个项目——收到方要能直接打开并重新构建,只带 `game/` 会丢掉根 `assets/`)。
  该变体承载的是独立能力(不同的遍历根 + 剪枝 + 「没有 game/ 目录」失败关闭),且已被两条用例
  覆盖,因此按仓库做法标 `#[cfg(test)]`(连带的 match 分支同步 cfg),并在注释里写清「真有生产
  调用方时去掉 cfg」;没有连带删除其它代码——模块里其余函数与常量都已被生产路径引用。
2026-10-05 17:30:49 +08:00
suzmii 779bbd1157 docs(游戏共创): M2b 文档回填两处实现偏差 + 状态改已实现(含 M2b 证据表)
- 技术方案 §3.4:上行路由行按端到端实测补两条按实现为准的细节——① **非作者上传返回 404 而不是 403**(`load_owner_version_or_404` 把 owner 不匹配按「版本不存在」处理,与发行包上行族同口径,不泄露版本存在性、响应不含对象键);② **阶段门先判「已存在」再判档位**(`ensure_project_bundle_uploadable`:`project_bundle_bytes > 0` → 409 `PROJECT_BUNDLE_ALREADY_EXISTS`,再看 `status` → 409 `PROJECT_BUNDLE_UPLOAD_NOT_ALLOWED`),因此「已公开且已有工程包」返回 `ALREADY_EXISTS`,「已公开但无工程包」才返回 `UPLOAD_NOT_ALLOWED`。证据:`check-game-distribution-project-bundle-e2e` 的 A5 / A5b / A6 及其 NOTE
- 技术方案 §3.5.2:AGC 侧「上传」「下载与建项」由「接线待做」改为**已落地**(发布时上传且失败不阻断、按 `source` 分支建项、来源记录 v2 兼容 v1、取件提示按形态措辞),并写明两端规则由 `npm run check-project-bundle-policy-parity` 机器门禁钉住一致
- 技术方案 §3.5.4:路线 B 由「服务端已实现、客户端接线待做」改为**两端均已落地**
- 技术方案 §3.7:补 M2b 口径——共创写入仍复用 `game-distribution:publish`(未新增灰度键);内容下发与工程包上传当前只有 `tracing` 日志,三个结构化埋点事件**仍未接**(待产品定事件口径)
- 技术方案新增 §5.2「M2b(工程源包)实施证据」表:三列与契约、校验器(16 条单测 / 上限镜像)、上行路由族(api-server 60 passed)、下行优先与缓存不串味、端到端 `check:game-distribution-project-bundle-e2e` **46/46**、两端规则 parity 门禁、客户端 vitest 1961 passed 与定向 cargo test 17+16;并显式登记「backfill 明确不做」「真实极端边界与结构化埋点待补」
- 里程碑「作品工程源包与一键改造」:Status → `implemented-local`;新增「实现进展(2026-10-05)」列出两端提交 SHA 与证据命令、以及四条未做项及原因;验收标准改为 ✅/⏳/⏭ 三段并有证据指向(阶段门顺序、非作者 404、半包清理等按实测更新);证据要求补上已落地的命令清单与「真机建项链路仍未跑」的括注
- 门禁:`check:doc-index` 248 份 OK;`check:encoding` 5304 files OK;`git diff --check` 0
2026-10-05 17:30:49 +08:00
suzmii 09a11ac215 fix(游戏共创): 取件成功提示按两种取件形态如实措辞
M2b 之后取件有两种形态:工程源包解压即可编辑工程(没有 reference/ 参考副本),成品包才放在
reference/<gameId>/ 作可玩参考。原来的成功提示写死「参考产物在 reference/<gameId>/ 下」,
对工程源包形态是错的。改为两种情形都说明,并同步测试断言(仍保留「改编自 X」这条来源事实)。
2026-10-05 17:30:49 +08:00
suzmii 1a4af39e56 feat(游戏共创): 取件按 source 分支建项,来源记录升到 v2(兼容 v1)
- 来源记录 v2:新增 `source`(`package` / `project`),当前写入 `agc-fork-source.v2`;
  解析同时接受 v1——v1 没有该字段,按「成品包」解释(当时只有这一条取件通道),
  因此老项目仍能正确读出「改编自哪个作品」,发布时照常带上声明。未知版本与非法取值仍按
  「没有来源记录」失败关闭。
- 建项按取件形态分支(`create_project_from_platform_fork_at` 增加 `source` 入参,远程
  `source` 取值在命令层映射):
  · `project`(工程源包):先把包解到**项目根**再走标准初始化。源包自带
    `package.json` / `vite.config.*`,初始化会跳过 npm 脚手架判据,只补 `.agent` 身份、
    目录与 agent.db,得到一份能继续改、能重新构建的工程;不再生成 `reference/` 参考副本。
  · `package`(成品包):维持原形态(合规脚手架 + `reference/<gameId>/` 可玩参考)。
- 解压仍走既有门禁(`extract_template_archive`:条目数/单文件上限、拒符号链接、条目路径
  只允许项目内相对路径;ZIP 根 == 项目根与模板包同一合同)。
- 命令文档同步说明两种形态的差别(工程源包=解压即工程;成品包=可玩参考,不能直接再发布)。

测试:新增「工程源包建项得到可编辑工程」用例(源码逐字节不被脚手架覆盖、`.agent` 身份已补、
无 `reference/`、记录 source=project)与「v1 记录仍可读且按成品包解释」用例;
既有成品包建项用例补断言 source=package。
2026-10-05 17:30:49 +08:00
suzmii 1fc78faff2 feat(游戏共创): 授权非禁止时随版本上传工程源包(M2b 上传链路,失败不阻断发布)
上传链路(复用既有分片续传,不另写一套):
- `PackageUploadAsset` 收拢两条资产的差异(URL 资源段 `package` / `project-bundle`、文案、
  偏移错误码 `PACKAGE_UPLOAD_OFFSET_MISMATCH` / `PROJECT_BUNDLE_UPLOAD_OFFSET_MISMATCH`、
  是否有创建版本时预登记大小),`upload_staged_game_package` 改名
  `upload_staged_version_asset`,发行包与工程源包共用同一段主循环与重试/续传语义。
- 发布流程:建游戏 → 建版本 → 上传发行包 → 〔授权非「禁止共创」且作者没取消时〕打包工程源包
  并上传 → 送审。插入点必须在送审**之前**:服务端只允许 `awaiting_upload` / `upload_failed`
  档位补传工程包,送审后即 409。
- 失败关闭与不阻断:工程源包打包/上传任何失败都只写进结果的 `projectBundleWarning`
  (「未上传工程源码,本作品只能被产物级改编:…」),发布本身照常成功;作者的作品仍可玩可发布。
- 进度复用既有 `game-package-upload-progress` 事件(phase 仍用 upload/verify,文案区分资产),
  渲染层的阶段联合是闭集,不新增阶段名。

面板与渲染层:
- 新增「同时公开工程源码」勾选:仅当授权非「禁止共创」且为首次发布时出现,默认勾选,
  文案写清「允许他人下载并使用你的工程源文件(含素材),且不可撤销」;
  取消勾选才显式传 `includeProjectBundle: false`,其余情况交给原生按「授权非禁止即上传」判断,
  渲染层不重复实现同一套默认值。
- 成功页如实展示 `projectBundleWarning`,避免作者误以为源码已随版本公开。

测试:Rust 新增「两条资产的路由段/偏移错误码/预登记大小差异」用例,并更新既有 `upload_url` 用例;
TS 新增勾选默认值、取消勾选显式 false、未上传提示三条,服务层新增「true/缺省不带字段、
false 才带」用例。
2026-10-05 17:30:49 +08:00
suzmii 360cb9830a test(游戏共创): 新增工程源包(M2b)端到端验收脚本
- 新增 scripts/check-game-distribution-project-bundle-e2e.mjs:真实 HTTP 验收 M2b 上行族与下行优先
  · A1 合法工程源包整包 PUT → 200;版本私有 payload 的 projectBundleBytes/projectBundleSha256 与上传一致且不含对象键
  · A2 >8 MiB 不可压缩负载两片续传:chunkBytes=8388608,偏移 8388608→9438176,upload-state 权威偏移逐片一致,complete 后 bytes/sha256 一致
  · A3 含 .env → 422 PROJECT_BUNDLE_VALIDATION_FAILED,且该版本仍 bytes=0 / sha256 空
  · A4 二次上传 → 409 PROJECT_BUNDLE_ALREADY_EXISTS
  · A5 已公开且已有工程包再传 → 409 ALREADY_EXISTS(阶段门先判已存在);A5b 已公开但无工程包补传 → 409 PROJECT_BUNDLE_UPLOAD_NOT_ALLOWED
  · A6 未带 Bearer → 401;另一作者 token → 404(实现按不存在处理,脚本按实现断言并标注与工单 403 的差异)
  · B7/B8 发布后 fork-source source=project、sha256/bytes 一致、downloadPath 指向 project,下载校验 zip/length/sha256/解压条目
  · B9 对照作品(无工程包)source 回落 package,/fork-source/project → 409
- package.json:新增 npm script check:game-distribution-project-bundle-e2e
- 复用 lineage 脚本的发布链路与 helper(脚本头注释标注出处),未抽公共模块以免改动其它脚本
- 实测:本地 dev 栈 46 项 46 PASS / 0 FAIL / 0 SKIP
2026-10-05 17:30:48 +08:00
suzmii 524ca890c6 feat(游戏共创): 工程源包规则与服务端对齐,并加机器 parity 门禁
规则对齐(以服务端为权威,`module-game-distribution/src/project_bundle.rs`):
- 凭据/隐私两侧补齐:客户端新增 `.git-credentials`、`id_rsa*`、`id_ed25519*`、`*.map`,
  并把 `.env` 放宽成服务端同形的 `starts_with(".env")`(`.envrc` 之类一并排掉)。
  反向无需补:客户端没有服务端缺的凭据类规则,比对一次后两侧凭据集合完全一致。
- 新增被漏掉的嵌套压缩包规则:服务端对任意层级 `*.zip` 一律拒收(解包阶段不递归校验,
  等于绕过整份清单),客户端原来会把它打进包里 → 现在同样排除。
- `.agent` 由「项目根首层」对齐到**任意层级**(服务端口径;平台保留名两边一致比更宽松重要)。
- 客户端额外项保留并逐项写明理由:`game/dist`、`game/build`(AGC 脚手架构建输出)、
  `.godot`(编辑器缓存,模板包指南同规则)、`exports`/`memory`(AGC 生成物与 Agent 记忆)。
- 规模上限统一到服务端数值:压缩包 ≤ 200 MiB(新增)、展开 ≤ 500 MiB、单文件 ≤ 64 MiB、
  条目 ≤ 10 000、单文件 ≤ 包体 × 100(新增)。客户端先拦,不再出现「传到一半被 422 拒掉」。
- 顺手补上服务端已有的「大小写折叠后重名条目」防线:客户端提前失败关闭。

可机器比对的常量:规则集拆成 `BUNDLE_EXCLUDED_*` 常量、上限拆成 `PROJECT_BUNDLE_MAX_*`,
新增 `scripts/check-project-bundle-policy-parity.mjs` 逐 token/逐数值比对两侧(服务端每条规则
必须在客户端存在,客户端额外项只打印不算失败;抽不到 token 直接报错,避免假绿),并照既有
DTO parity 的形态接到 `package.json` 的 `check:*` 与 `lint` 链。

测试:客户端 17 条(新增 `.git-credentials`/`id_rsa`/`*.map`/`.envrc`/`.zip`/任意层级 `.agent`、
压缩包上限、压缩比上限各自一条;合同常量锚点改为与服务端逐项相等)。
2026-10-05 17:30:48 +08:00
suzmii 861dc0a676 feat(游戏共创): M2b 上行路由族 + 下行优先与缓存资产维度(块 C+D)
- 上行(作者、Bearer + 发布灰度,与发行包族逐条对齐):`PUT /versions/{id}/project-bundle`(整包一次上传,≤ 200 MiB)、`GET …/project-bundle/upload-state`、`PUT …/project-bundle/chunk`(`x-genarrative-upload-offset`,分片上限同发行包)、`POST …/project-bundle/complete`、`POST …/project-bundle/reset`;载体一律 `application/octet-stream`,分片边界与偏移语义只有一套
- 对象键:新增 `game_distribution_project_bundle_object_key(game_id, version_id)` = `…/{version_id}.project.zip`;发行包 helper **未改**(仍是 `.zip`)→ 同一(作品, 版本)的两份资产互不覆盖、也不会串用读取缓存
- 阶段门:新增**唯一**共享函数 `ensure_project_bundle_uploadable`,5 条上行路由全部调用、无重复实现。判定顺序:① 已确认(`project_bundle_bytes > 0`)→ 409 `PROJECT_BUNDLE_ALREADY_EXISTS`(必须先判:确认工程包不驱动版本状态机,已确认的版本可能仍停在 `awaiting_upload`);② `status` ∉ {`awaiting_upload`, `upload_failed`} → 409 `PROJECT_BUNDLE_UPLOAD_NOT_ALLOWED`(与模块事务同口径)
- `complete`:回读整包 → 与 HEAD 权威长度比对 → `validate_project_bundle_zip`(失败删半包对象 + 记上传失败 + 422 `PROJECT_BUNDLE_VALIDATION_FAILED`)→ `confirm_game_distribution_project_bundle`;幂等键与摘要口径照抄发行包 complete
- 下行优先:`fork_source_target` 选定资产 = 有工程包时 `Project`(优先)否则 `Package`;「字节数 > 0 **且**摘要非空」才算有工程包,半写行回落 `package`(失败关闭)。元数据 `source` / `sha256` / `bytes` / `downloadPath` 随所选资产;新增 `GET /games/{id}/fork-source/project`(同两层中间件、无 Query 提取器;没有工程包时 409 `FORK_SOURCE_NOT_AVAILABLE`,**绝不静默回落成品包**),响应头与成品包同形(文件名 `{gameId}-{versionId}-project.zip`)
- 缓存键加资产维度:`release_package_bytes` 泛化为 `release_asset_bytes(state, object_key, max_bytes)`(缓存键即对象键),发行包侧退化为薄封装——键字符串、上限、`Bytes` 值类型与 4 条 / 256 MiB 预算逐字节不变,既有缓存测试继续通过
- 顺着同一「不复制第二套」原则抽出/参数化的共享件:`fork_source_bundle_response`(两份资产共用响应构造)、`package_upload_offset(headers, asset)`、`require_octet_stream_content_type(headers, asset)`(按资产给错误文案,发行包文案不变)
- 补上块 A 遗漏的两处客户端登记(否则下行字段读不到):`spacetime_client` 对 `GameDistributionConfirmProjectBundleRecordInput` 的 re-export,以及 `GameDistributionForkSourceRecord` 新增 `project_bundle_sha256` / `project_bundle_bytes` 与对应映射
- 测试 6 条:5 条上行 + 1 条下行共 6 条路由未带 Bearer → 401(逐条);`ensure_project_bundle_uploadable` 全状态(含「已确认优先于阶段」)+ 7 档拒绝;`fork_source_target` 6 种资产组合(工程包优先 / 回落成品包 / 两种半写行 / 只有工程包 / 两份都缺 → 409);两个对象键互不相等 + `ReleasePackageCache` 双资产不串味;上限镜像(`MAX_PROJECT_BUNDLE_BYTES == MAX_PACKAGE_BYTES == shared_contracts::GAME_DISTRIBUTION_MAX_PACKAGE_BYTES`、分片放行量 > 分片大小);校验失败 422 映射
- 门禁:`cargo check --all-targets` 0;`cargo test -p api-server game_distribution` **60 passed**(基线 54;既有发行包族用例全绿 → 抽取共享件未改发行包行为);DTO parity 42 组 OK;`check:encoding` 5301 files OK;`git diff --check` 0;`cargo fmt --all -- --check` 0
2026-10-05 17:30:48 +08:00
suzmii d39257b608 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
2026-10-05 17:30:48 +08:00
suzmii 8ce4649081 feat(游戏共创): M2b 工程源包专属 zip 校验器(块 B)
- 新增 `server-rs/crates/module-game-distribution/src/project_bundle.rs`:`validate_project_bundle_zip(bytes) -> Result<ProjectBundleManifest, ProjectBundleError>`
  · 复用 `crate::package::normalize_archive_path`(不复制路径安全逻辑),路径不安全统一归 `InvalidPath`
  · 拒绝清单:空包 / 非 zip / 条目数 > 10 000 / 符号链接 / 加密条目 / 嵌套 `.zip` / 完全重复与大小写折叠重复路径 / `node_modules`(任意层级)/ `.git`、`.svn`(任意层级)/ `.agent`(任意层级:避免把上一个作品的改编来源记录带给下一个人)/ 根级 `dist`、`build`、`library`、`temp`、`local`(子目录同名允许,与模板包规则一致)/ 根级 `.idea`、`.vscode` / 凭据与隐私:`.env`、`.env*`、`*.pem`、`*.key`、`*.p12`、`*.pfx`、`.npmrc`、`.netrc`、`.git-credentials`、`id_rsa*`、`id_ed25519*`、`*.map` / 单文件 > 64 MiB / 累计 > 500 MiB / 单文件 > 包体 × 100
  · 规模上限与发行包**逐项相等**(200 MiB / 500 MiB / 64 MiB / 10 000 / 100):两者共用同一条上传链路,反代与 Pingora 的放行量就是按 200 MiB 校准的,放宽就得同步放开多处部署配置;单测用 `assert_eq!` 钉住这条等价关系
  · 不要求根 `index.html`(源码包没有入口约定),非空即可
- `lib.rs` 只追加 `mod project_bundle;` 与 `pub use project_bundle::{...}`(+6 行);`package.rs` **零改动**,发行包行为与测试不受影响
- 测试 16 条:合法源码包通过(故意不含 `index.html`)、上述每条拒绝规则各一条、「子目录里的 `dist/` 允许」反例、常量与发行包逐项相等、manifest 的 sha256/字节数与输入一致
- 门禁:`cargo test -p module-game-distribution` 69 passed(含本校验器 16 条);wasm build 0;`cargo fmt --all -- --check` 0
2026-10-05 17:30:48 +08:00
suzmii 604ce7f3ec feat(游戏共创): M2b 数据模型——版本行三列工程源包 + 契约与绑定(块 A)
- `server-rs/crates/spacetime-module/src/game_distribution.rs`:`game_distribution_version` 表尾追加三列(明确默认值,遵循既有追加规则)——`project_bundle_object_key: Option<String>`(`#[default(None)]`)、`project_bundle_bytes: u64`(`#[default(0)]` = 未上传)、`project_bundle_sha256: Option<String>`(`#[default(None)]`);三列同步进 `GameDistributionVersionSnapshot`、版本快照构造与建版本时的初始化
- 同文件:`GameDistributionForkSourceSnapshot` 与扁平结果类型追加 `project_bundle_sha256` / `project_bundle_bytes`,下行据此判断走 `project` 还是回落 `package`(字节数与摘要成对,缺一视为没有工程包)
- 同文件:新增 `GAME_DISTRIBUTION_ACTION_UPLOAD_PROJECT_BUNDLE` 动作常量、输入 `GameDistributionConfirmProjectBundleInput`、事务 `confirm_game_distribution_project_bundle_tx` 与 procedure `confirm_game_distribution_project_bundle_and_return`。语义:可选资产、**不驱动版本状态机**;owner 校验 → 阶段门(`awaiting_upload` / `upload_failed`,与发行包确认同口径,已公开/已取消/已撤回一律拒绝)→ 同内容幂等重放 / 换内容冲突 → 写三列 + 幂等收据
- `spacetime-client`:`GameDistributionVersionRecord` 与 `map_version` 同步三列(生成绑定把 `project_bundle_sha256` 折成 `project_bundle_sha_256`,与 `package_sha_256` 同约定);新增 `GameDistributionConfirmProjectBundleRecordInput` 与 `confirm_game_distribution_project_bundle`(沿用 `map_game_distribution_game_result` 再取 version 的既有写法)
- 契约:`shared-contracts` 的 `GameDistributionPrivateVersion` 追加 `project_bundle_bytes`(必需,`0` = 未上传)与 `project_bundle_sha256`(可选);`packages/shared` TS 镜像同步;api-server `private_version_payload` 追加同名键——**对象键不下发**,只回摘要与字节数
- 生成物:重跑 `npm run spacetime:generate -- --rust-only`,只保留 `module_bindings/game_distribution*`(含 2 个新文件)与 `module_bindings.rs`,其余 `module_bindings/**` 的 rustfmt 漂移已 `git checkout --` 还原
- 文档:数据契约表目录 `game_distribution_version` 小节补三列语义与对象键不出服务端的口径
- 顺手记下一条门禁陷阱:`scripts/check-game-distribution-dto-parity.mjs` 抓 `json!` 顶层键时要求「键前一个非空字符是 `{` 或 `,`」且**不剥注释**,在字面量里插注释行会让紧随其后的键被判成缺失(已写进 `private_version_payload` 的文档注释)
- 门禁:wasm build 0;`cargo check --all-targets` 0;`cargo test -p module-game-distribution` 69 passed;`cargo test -p api-server game_distribution` 54 passed;DTO parity 42 组 OK;`check:spacetime-schema` 92 tables OK;`check:encoding` 5301 files OK;`git diff --check` 0;`cargo fmt --all -- --check` 0
2026-10-05 17:30:48 +08:00
suzmii 63e6c6345e feat(游戏共创): 新增工程源包确定性打包器(M2b 客户端半,暂不接上传)
- 新模块 `src-tauri/src/project_bundle.rs`:`build_project_bundle(root, scope)` 产出
  `(bytes, sha256, file_count, entry_paths)`,口径见 `docs/【技术方案】游戏共创与作品Fork-…` §3.5.2。
- 确定性:条目按路径排序、固定时间戳(ZIP 下限 1980-01-01)、固定权限位 0o644、固定 deflate,
  只写文件条目不写目录条目;同内容必然得到同一 sha256(有测试钉住,含「改一字节即变」)。
- 路径安全:条目名一律项目根相对并过解压侧同一道门禁 `template_library::safe_archive_relative_path`;
  符号链接 / Windows 重解析点、非普通条目一律失败关闭。ZIP 根 == 项目根(与模板包同一合同)。
- 排除规则:依赖与 VCS 任意层级(`node_modules`/`.git`/`.svn`)、项目根首层构建产物与 IDE
  (`dist`/`build`/`library`/`temp`/`local`/`.idea`/`.vscode`/`.godot`)、任意层级凭据
  (`.env`/`.env.*`/`*.pem`/`*.key`/`*.p12`/`*.pfx`/`.npmrc`/`.netrc`)、AGC 自有目录
  (`.agent`/`exports`/`memory`,理由逐条写在模块注释里)。
- 带上限常量:条目 ≤ 4096、单文件 ≤ 256 MiB、展开总量 ≤ 512 MiB;超限失败关闭并点名违规条目,
  不静默截断(测试用小上限跑同一段逻辑,不另写一条检查路径)。
- 复用而非另起一套:`safe_archive_relative_path`(解压侧)、`project::relative_project_path`、
  `image_inspect::metadata_is_windows_reparse_point`、`export.rs` 的排序 + 逐文件写 ZIP 写法;
  摘要实现抽出 `project_bundle::sha256_hex`,`game_fork` 的取件校验改为复用它(少一份副本)。
- 打包范围:`WholeProject`(默认,M2b 用)与 `GameDirectory`(只收集 `game/`,条目命名不变)。
- 未接上传:上行路由族未定,模块整体带 `#![allow(dead_code)]` 并在注释里写明接线时删除。
- 测试 14 条:每类排除规则、确定性(两次/乱序/改字节)、条目路径不变量 + 解压器往返、
  两个打包范围、空包与三类超限失败关闭、合同常量锚点;符号链接用例按平台 `#[cfg(unix)]`。
2026-10-05 17:30:47 +08:00
suzmii d2698cbb1d fix(门禁): 补 /games/lineage 的 Pingora 登记,并修掉被掩盖的 eslint 失败
- `server-rs/crates/pingora-gateway/src/main.rs`(MAIN_SPA_PATHS,:64-79):插入 `"/games/lineage"`(放在 `"/games/detail"` 之后,保持既有字典序)。这是主站 SPA 深链在 Pingora 网关下的登记,漏一条就把该深链打成 404;与上一提交补的三份 nginx 模板属同一条「新 SPA 路由三处同批更新」规则
- 矩阵与文档无需改:`deploy/pingora/nginx-route-parity.matrix.json` 的既有 `games_spa_fallback` 用例仍前缀匹配同一行 `location`(本次只改交替项、未新增 `location`),Pingora 试点文档的取值表同理未受影响
- `src/components/game-distribution/GameLineagePage.tsx:146`:把 `lineage?.nodes ?? []` 用 `useMemo(..., [lineage])` 稳定下来,消掉两条 `react-hooks/exhaustive-deps`。取值仍是同一表达式的结果,identity 只在 `lineage` 变化时改变,因此两个下游 useMemo(代际分组、可见 ID 集合)取值不变、只少做无谓重算;未用 disable 注释压掉告警
- 6 处 `simple-import-sort/imports` 用 `eslint --fix` 机械修(`apps/.../view/home/PlatformGameForkSection.tsx`、`apps/.../view/home/index.tsx`、`src/components/game-distribution/GameDetailPage.tsx`、`MyGamesPage.test.tsx`、`MyGamesPage.tsx`、`src/services/gameDistributionClient.ts`):逐文件比对「剥掉 import 行后的正文」——5 个文件完全一致,`GameDetailPage.tsx` 仅多一个 import 分组空行
- 这批修的是「修 nginx 时被前一道红掩盖的第二、第三个失败」:CI run 3387 卡在 `check:nginx-spa-routes`,`check:pingora-route-parity` 与 `lint:eslint` 从未跑到
- 本地验证:`check:repository-ci` 的 15 个 lint 环节 + `appSurface` vitest + `npm run build` + `git diff --check 6c9566058...HEAD` 全绿,仅两个**本机不可执行**项(`check:git-hooks` 的 Windows EBUSY、`check:generated-bindings` 的 AGC `export_bindings` 本地构建);另跑 `cargo test -p pingora-gateway` 40 passed、`npx vitest run apps/ai-game-creator-shell/tests` 1957 passed / 17 skipped
2026-10-05 17:30:47 +08:00
suzmii 5795a07a7d fix(部署): nginx SPA allowlist 补 /games/lineage(三份模板同批)
- `deploy/nginx/genarrative.conf:237`:allowlist 交替项插入 `games/lineage`(放在 `games/detail` 与 `games/mine` 之间,保持既有字典序)
- `deploy/nginx/genarrative-dev-http.conf:209`:同上
- `deploy/container/nginx.conf:160`:同上(8 空格缩进,`~*` + `/?$` 形态不变)
- 原因:M3 新增网页端 SPA 路由 `/games/lineage`(`src/routing/activeAppPageRoutes.ts:18`)后没同步三份 nginx 模板,`npm run check:nginx-spa-routes` 以 9 条断言失败(每份模板 3 条:缺少路由 / 未匹配完整路径 / 未允许大小写差异与尾部斜杠);CI run 3387 的 `Repository checks`(3m36s)即因此红,属本分支引入(master 尖端 `6c9566058` 8/8 绿)
- 只补路由项,未改门禁脚本、未放宽任何断言、未动 `apps/**`、`server-rs/**`、`src/**`
- 验证:`npm run check:nginx-spa-routes` → OK(15 SPA routes / 1 prefix routes / 3 Nginx templates)
2026-10-05 17:30:47 +08:00