Files
Genarrative/server-rs/crates/api-server
kdletters f502829fde
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m2s
Project CI / Backend tests (push) Successful in 5m19s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m59s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m19s
Project CI / Native shell tests (push) Successful in 6m48s
Project CI / Frontend tests (push) Successful in 2m54s
Project CI / Repository checks (push) Successful in 2m58s
Project CI / AI game creator shell web tests (push) Successful in 2m34s
发布灰度默认关闭:修掉客户端“看得到点不动”与后台看不到 key (#482)
## 解决什么

你反馈的两个现象都是真问题,这个 PR 一次修掉:

1. **没开灰度也能看到发布按钮**:发布开关此前是「无 gate 行 = 默认开放」,所以没配灰度时后端把 `gameDistributionPublishEnabled` 判成 true,客户端就渲染了发布入口。
2. **按钮点了没反应**:发布动作的提示原先只写 `workspaceStatus`,而普通项目走 `DirectProjectChatView` 时并不渲染工作台状态行,于是「先打开一个项目再发布」「构建失败…」这类信息全都看不见。
3. **后台看不到 key**:预设项此前在未合并的另一个 PR 里,而且后台只列「已创建」的 gate 行。

## 改成什么

- `is_game_distribution_publish_enabled_for_user` 要求 gate 行存在且 `enabled=true`:**灰度默认关闭**,未登录/无行/`enabled=false` 一律不开放;只有白名单 / 灰度比例 / 用户标签命中才开放。作者写入与新版本激活都按这条判据。
- 新增与作者无关的总开关 `is_game_distribution_publish_open`:管理员审核激活新版本只看「灰度是否开启」,不会因为审核员不在作者白名单里而被挡住(这是合入后立刻发现的回归,已修 + 有用例)。
- AGC 新增 `announcePublishMessage`:发布相关提示同时写工作台状态与 DirectProject 对话,点「发布到游戏广场」不会再像没反应。
- 后台「灰度发布配置」新增「可配置开关」列表:`game-distribution:publish` 等预设即使还没创建行也会显示,点「配置」按默认关闭填表,运营开启后填白名单/比例即可放量。

## 验证

- `npm run check:game-distribution-media-e2e`(真实 Phaser 构建产物)现在覆盖:灰度关闭 → 作者拿不到入口 + 写入口 503 → 开启灰度 → 真实封面/截图直传 → 创建游戏/版本/上传/送审 → 管理员审核通过 → 网关 index.html 与 bundle 200 → nosniff;全流程通过。
- 后端:`cargo test -p api-server game_distribution` 19 passed(含新增 `game_distribution_publish_open_ignores_author_allowlist_for_activation`)、`frontend_runtime_config` 6 passed。
- 前端:admin 灰度页 10 用例(含「未创建的预设开关可见并可一键配置」)、AGC 聊天头 3 用例、网页发布页 15 用例;三端 typecheck、`check:encoding`、`check:doc-index`、`check:rustfmt`、全量 `npm test`(400 文件 / 4441 用例)与 `check:repository-ci -- origin/master HEAD` 全绿。

## 上线注意

灰度默认关闭是**刻意**的:合入后到运营在后台把 `game-distribution:publish` 配置并开启之前,作者看不到发布入口、写入口 503;已公开游戏、目录、详情、发行网关、安全下架不受影响。

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/482
Co-authored-by: kdletters <kdletters@qq.com>
Co-committed-by: kdletters <kdletters@qq.com>
2026-09-22 21:38:03 +08:00
..

api-server 主工程 crate 占位说明

日期:2026-04-20

1. crate 职责

api-server 是新后端的 Axum 主工程 crate,后续负责:

  1. main.rs 启动入口
  2. Router 装配
  3. with_state 共享状态注入
  4. 中间件挂载
  5. /healthz、/api/*、SSE 与静态资源兼容层装配
  6. 由 ../../scripts/dev.ps1 与 ../../scripts/dev.sh 驱动的本地开发启动链路
  7. 由 ../../scripts/test.ps1 与 ../../scripts/test.sh 驱动的本地测试链路
  8. 由 ../../scripts/check.ps1 与 ../../scripts/check.sh 驱动的本地统一检查链路
  9. 由 ../../scripts/smoke.ps1 与 ../../scripts/smoke.sh 驱动的本地启动与协议冒烟链路

2. 当前阶段说明

当前目录已经完成以下基础骨架:

  1. 目录占位
  2. Cargo.toml
  3. src/main.rs
  4. src/app.rs
  5. src/state.rs
  6. src/config.rs
  7. 基础 TraceLayer 挂载
  8. 接入 shared-logging 完成 tracing subscriber 初始化
  9. 接入 POST /api/auth/entry 首版密码登录链路
  10. 接入 POST /api/auth/password/change 登录后修改密码链路
  11. 接入 POST /api/auth/password/reset 手机验证码重置密码链路
  12. 接入 POST /api/assets/direct-upload-tickets 直传票据接口
  13. 接入 GET /api/auth/me 当前用户查询链路
  14. 接入 POST /api/auth/refresh refresh token 轮换链路
  15. 接入 POST /api/auth/logout 当前设备退出链路
  16. 接入 POST /api/assets/objects/confirm 上传完成确认链路
  17. 接入 GET /api/auth/login-options 登录方式探测链路
  18. 接入 POST /api/auth/phone/send-code 手机验证码发送链路
  19. 接入 POST /api/auth/phone/login 手机验证码登录链路
  20. 接入 GET /api/auth/wechat/start 微信授权起跳链路
  21. 接入 GET /api/auth/wechat/callback 微信回调换取系统登录态链路
  22. 接入 POST /api/auth/wechat/bind-phone 微信待绑定账号补绑手机号链路
  23. 接入 POST /api/assets/objects/bind 已确认对象绑定业务实体槽位链路
  24. 接入 POST /api/assets/sts-upload-credentials 禁用式 STS 写权限 contract
  25. 接入 custom-world-library、custom-world-gallery 与 agent publish_world 首批 Axum facade
  26. 接入 custom world agent session create / session snapshot Axum facade
  27. 旧 runtime story 兼容接口已下线并保持未挂载;运行时故事写链路改为:
    • POST /api/story/sessions/runtime
    • GET /api/story/sessions/{story_session_id}/runtime-projection
    • POST /api/story/sessions/{story_session_id}/actions/resolve
  28. 接入 POST /api/assets/character-visual/generate
  29. 接入 GET /api/assets/character-visual/jobs/{task_id}
  30. 接入 POST /api/assets/character-visual/publish
  31. 接入 GET /api/assets/character-animation/templates
  32. 接入 POST /api/assets/character-animation/import-video
  33. 接入 GET /api/assets/character-workflow-cache/{character_id}
  34. 接入 POST /api/assets/character-workflow-cache
  35. 接入 POST /api/assets/character-animation/generate
  36. 接入 GET /api/assets/character-animation/jobs/{task_id}
  37. 接入 POST /api/assets/character-animation/publish
  38. 下线旧 /generated-* 资产直读代理;生成资产读取统一走 /api/assets/read-url 或 asset object projection
  39. 下线旧 /api/custom-world/* 非 runtime 前缀和 /api/runtime/puzzle/runs/local-next-level,并用路由回归测试确认这些旧入口保持 404

后续与本 crate 直接相关的任务包括:

  1. 接入统一日志与 tracing
  2. 接入 request_id
  3. 接入统一错误处理中间件
  4. 接入 response envelope
  5. 接入 /healthz
  6. 接入 /api/auth/entry
  7. 接入 /api/assets/direct-upload-tickets
  8. 接入 /api/auth/me
  9. 接入 /api/auth/refresh
  10. 接入 /api/auth/logout
  11. 接入 /api/assets/objects/confirm
  12. 接入 /api/auth/login-options
  13. 接入 /api/auth/phone/send-code
  14. 接入 /api/auth/phone/login
  15. 接入 /api/auth/wechat/start
  16. 接入 /api/auth/wechat/callback
  17. 接入 /api/auth/wechat/bind-phone
  18. 接入 /api/assets/objects/bind
  19. 接入 /api/assets/sts-upload-credentials
  20. 接入 custom world library / gallery / publish_world 首批 facade
  21. 接入 custom world agent session create / snapshot facade
  22. 下线旧 runtime story compat facade,并接入 story session scoped runtime story 写读入口
  23. 接入 character-visual generate / jobs / publish 第一批 OSS 主链兼容 facade
  24. 接入 character-animation templates / import-video 第一批 OSS 草稿兼容 facade
  25. 接入 character-workflow-cache get / save 第一批 OSS JSON 草稿兼容 facade
  26. 接入 character-animation generate / jobs / publish 第一批 OSS 主链兼容 facade
  27. 下线旧 /generated-* 路径 OSS 私有读同源代理
  28. 下线旧 /api/custom-world/* 非 runtime 前缀和 Puzzle local-next-level 兼容入口

当前 tracing 约定:

  1. 进程启动时通过 shared-logging 统一初始化 tracing subscriber。
  2. 默认日志过滤器来自 GENARRATIVE_API_LOG,未提供时回落到 info,tower_http=info。
  3. HTTP 访问日志统一通过 Axum 路由层的 TraceLayer 输出,后续 request_id、响应头与错误中间件继续在同一层扩展。
  4. 本地启动器 npm run dev:api-server 和完整联调入口 npm run dev 会在保留终端实时输出的同时,把同一份 cargo / api-server 输出持久化到 logs/api-server/。如需固定文件或目录,可设置 GENARRATIVE_API_SERVER_LOG_FILE 或 GENARRATIVE_API_SERVER_LOG_DIR。

当前 request context 约定:

  1. 中间件优先读取来访 x-request-id,未提供时生成新的 UUID。
  2. request_id 会统一写入请求 extensions 与请求头,供 tracing、错误处理中间件和响应头层复用。
  3. 最终响应会回写同一个 x-request-id,保证调用方、日志链路和后续 envelope meta.requestId 可对齐。

当前错误处理中间件约定:

  1. 对 Axum 默认产生的空 4xx / 5xx 响应,统一归一化为 legacy 兼容 JSON 错误体:{ error: { code, message, details? } }。
  2. 已经带 content-type 的业务错误响应不会被覆盖,避免抢走后续 response envelope 的职责。
  3. 统一错误日志会复用当前请求的 request_id,便于后续和响应头、envelope 元信息串联。

当前 response envelope 约定:

  1. RequestContext 已记录 request_id、请求开始时间、默认 operation 与 envelope 协商结果。
  2. json_success_body(...) / json_error_body(...) 会根据 x-genarrative-response-envelope 自动在“裸数据 / 标准 envelope / legacy error + meta”之间切换。
  3. meta.apiVersion、meta.requestId、meta.routeVersion、meta.operation、meta.latencyMs、meta.timestamp 已按当前前端契约生成,响应头回写仍留给后续独立任务。

当前基础响应头约定:

  1. 所有响应都会回写 x-request-id。
  2. 所有响应都会回写固定的 x-api-version,值来自 shared_contracts::api::API_VERSION,当前为 2026-06-16,并与 body meta.apiVersion 保持一致。
  3. 所有响应都会回写 x-route-version,当前阶段默认与 x-api-version 保持一致,后续再按路由粒度细分。
  4. 所有响应都会回写 x-response-time-ms,值来源于 RequestContext 内记录的请求开始时间。

当前 /healthz 约定:

  1. 路径固定为 /healthz。
  2. 裸响应继续返回 { ok: true, service: "genarrative-node-server" },保持与当前 Node 工程兼容。
  3. 当请求携带 x-genarrative-response-envelope 时,/healthz 会返回标准 success envelope。
  4. x-request-id、x-api-version、x-route-version、x-response-time-ms 会在 /healthz 响应中一并回写。

当前本地检查链路约定:

  1. ../../scripts/check.ps1 与 ../../scripts/check.sh 统一串联 cargo fmt --all --check、cargo clippy、cargo check、cargo test。
  2. 默认检查整个 server-rs workspace,确保后续多 crate 扩容时仍然保持统一口径。
  3. 当只需聚焦单个 crate 时,可通过 -Package 或 SERVER_RS_CHECK_PACKAGE 收窄 clippy / check / test 目标。
  4. cargo fmt --all --check 仍固定覆盖整个 workspace,避免多 crate 下格式基线漂移。

当前本地 smoke 链路约定:

  1. ../../scripts/smoke.ps1 与 ../../scripts/smoke.sh 会先构建 api-server,再拉起临时本地进程完成冒烟验证。
  2. smoke 当前固定校验 /healthz 的 raw 响应、envelope 响应以及 x-request-id、x-api-version、x-route-version、x-response-time-ms 头。
  3. smoke 通过后,可作为“Axum 服务可独立启动且基础 contract 可联通”的本地自动化证据。

3. 边界约束

  1. api-server 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
  2. 业务逻辑优先通过独立模块 crate 暴露能力,再由主工程组合。
  3. 外部副作用通过 platform-auth、platform-oss、platform-llm 与各模块 crate 的应用层完成。
  4. 不把领域规则直接堆在 handler 中。
  5. 当前密码登录由 module-auth 负责用例编排,api-server 只负责请求解析、JWT 签发与 refresh cookie 写回。
  6. 当前 /api/auth/me 复用现有 Bearer JWT 中间件与 module-auth 用户快照查询,不直接绕过模块边界读取内部状态。
  7. 当前 /api/auth/refresh 复用 module-auth 的 refresh session 轮换能力,api-server 负责 refresh cookie 读取、失败清理与 access token 重签。
  8. 当前 /api/auth/logout 复用 module-auth 的当前会话吊销与用户版本递增能力,api-server 负责 Bearer JWT、refresh cookie 读取与清理 cookie 回写。
  9. 当前 /api/assets/objects/confirm 先由 platform-oss 完成私有 HEAD Object 校验,再通过 spacetime-client 调用 spacetime-module 的对象确认持久化入口。
  10. 当前 /api/assets/objects/bind 只绑定已确认对象到业务实体槽位,不访问 OSS,不创建悬空 asset_object_id。
  11. 当前手机号登录与微信登录都复用 module-auth 的进程内认证仓储,api-server 负责请求解析、场景判定、系统 JWT 签发与 refresh cookie 写回。
  12. 当前微信回调不会把第三方 token 直接透传给前端或 SpacetimeDB,而是统一换成系统签发的 JWT。
  13. 当前 /api/assets/sts-upload-credentials 按“服务器上传、Web 只下载”口径固定返回 403,不向浏览器下发 OSS 写权限。
  14. 当前 /api/runtime/custom-world/agent/sessions 与 /api/runtime/custom-world/agent/sessions/{session_id} 只提供 deterministic session 骨架与 snapshot 读取,不承诺 message submit、operation query、card detail 的完整能力。
  15. 当前 /api/runtime/story/* 不再挂载;runtime story 开局、读取和动作结算通过 /api/story/sessions/runtime、/api/story/sessions/{story_session_id}/runtime-projection 与 /api/story/sessions/{story_session_id}/actions/resolve 进入 BFF,并由 module-runtime-story、story session 和 runtime snapshot 投影共同闭合。
  16. 当前 /api/assets/character-visual/* 第一批只保证旧接口 contract、OSS 草稿/正式对象、asset_object 与 asset_entity_binding 主链可用;真实图片模型、workflow cache 与本地角色覆盖写回仍在后续阶段。
  17. 当前 /api/assets/character-animation/import-video 第一批只接受 data:video/*;base64,... 并写入 OSS 草稿区,不读取旧本地 public/ 路径,也不创建正式 asset_object。
  18. 当前 /api/assets/character-workflow-cache/* 第一批只把工作流 JSON 草稿写入 OSS,不迁移历史本地缓存,也不创建正式 asset_object。
  19. 当前 /api/assets/character-animation/generate 第一批只用 Rust 占位产物打通 AiTaskService + OSS 草稿链;image-sequence 写 SVG 帧,视频类策略优先复用参考视频或仓库内可播放占位视频,不代表真实上游视频模型已完成迁移。
  20. 当前 /api/assets/character-animation/publish 会把前端提交帧、动作级 manifest 与总 manifest 写入 OSS,并只把总 manifest 确认为 asset_object 后绑定到 character / animation_set。
  21. 当前旧 /generated-* 直读兼容层已下线;/generated-* 只作为 legacyPublicPath / OSS object key 标识,读取必须通过 /api/assets/read-url 或业务投影中的签名读 URL。
  22. 当前旧 /api/custom-world/entity、/api/custom-world/scene-npc、/api/custom-world/scene-image、/api/custom-world/cover-image、/api/custom-world/cover-upload 不再挂载,RPG 创作资产入口统一使用 /api/runtime/custom-world/*。
  23. 当前旧 /api/runtime/puzzle/runs/local-next-level 不再挂载,正式 Puzzle 运行态只通过 /api/runtime/puzzle/runs/{run_id}/next-level 进入后端真相源。