合并主分支最新改动

同步 master 的最新功能与修复
保留 AI 游戏创作客户端分支现有实现

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
#	scripts/dev.mjs
#	server-rs/crates/api-server/src/editor_screen_background_decision.rs
#	server-rs/crates/api-server/src/modules/admin.rs
#	src/components/rpg-entry/RpgEntryHomeView.tsx
This commit is contained in:
AIGameCreator App
2026-07-14 13:05:45 +08:00
384 changed files with 44028 additions and 5120 deletions
@@ -0,0 +1,52 @@
# 官网 SEO 地基实施约定
更新时间:`2026-07-10`
## 本轮边界
本轮只建立陶泥儿中文首页的 SEO 基础设施,不新增英文站、长尾落地页、SSR、动态 sitemap、前端访问埋点或分享图。
- `public/robots.txt` 是真实静态文件;非 SEO 产品路径本轮阻止抓取,但不把 `Disallow` 解释为保证不收录。
- `public/sitemap.xml` 本轮只包含 `https://www.genarrative.world/`,不写难以持续维护的 `lastmod`。
- `index.html` 提供首页 title、description、canonical、robots、基础 OG/Twitter 文本和 JSON-LD;没有正式分享图时不写 `og:image`、`twitter:image` 或 JSON-LD `logo`。
- 桌面首页与 `/creation` 创作主页渲染后只有一个稳定、可见的产品定位 H1,固定为 `陶泥儿 · 开启全民精品游戏创作`;小眉题、产品说明、权益提示、动态作品名、卡片标题、按钮内部标题、弹窗标题和隐藏 Tab 不作为 H1。
- H1 下方真实展示游戏美术 AI 工作台、美术 Agent、无限画布以及角色、场景、UI、宣发素材说明,不使用透明、极小、屏幕外或页面底部堆词文本;`游戏美术 AI 创作工具` 使用 H2,能力卡标题使用 H3。
- 本轮可见首页文案调整不得删除或改写已经验收的 title、description、canonical、robots、OG/Twitter 文本和 JSON-LD。
- Nginx 和 Pingora 只对当前已知完整 SPA 路径回退 `index.html`;未知路径以及 `/creation/not-exist` 等同前缀未知路径返回 HTTP 404。
- Nginx access log 继续使用现有 `$http_referer`,本轮不新增前端 pageview 或用户身份采集。
## 路由事实源
主站 SPA 路由以以下源码为事实源:
- `src/routing/appRoutes.tsx`
- `src/routing/appPageRoutes.ts`
`/creation/rpg/agent` 仍被现有刷新恢复链路使用,当前作为兼容深链保留。新增或删除前端路由时,必须同步三套 Nginx 配置、Pingora 路由、`deploy/pingora/nginx-route-parity.matrix.json` 和对应自动门禁。不得把 `/creation/*`、`/runtime/*` 等一级目录整体设为 SPA fallback。
后续新增 SEO 落地页时,还必须同时满足:返回 200、不被 robots.txt 阻止抓取、加入 sitemap,并提供独立 title、description、canonical、H1、正文和内链入口。纯 SPA 页面需要独立 head 时,应评估构建时静态 HTML、预渲染或 SSR。
## 验收口径
```bash
npm run build
npm run typecheck
npm run check:nginx-spa-routes
npm run check:pingora-route-parity
npm run check:pingora-gateway-smoke
npm run check:encoding
git diff --check
```
部署后至少验证:
```bash
curl -I https://www.genarrative.world/robots.txt
curl -I https://www.genarrative.world/sitemap.xml
curl -I https://www.genarrative.world/not-exist-test
curl -I https://www.genarrative.world/creation/not-exist-test
curl -I https://www.genarrative.world/runtime/not-exist-test
curl -I https://www.genarrative.world/puzzle/not-exist-test
```
真实 SPA 路径不得误 404;以上未知路径必须返回 404。首页浏览器 DOM 应只有一个 H1,并确认桌面和移动端布局、导航、推荐流和创作入口没有因 SEO 文案变形。
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”按当天真实日期自动填充对应自然日 / 自然周 / 自然月范围并刷新,手动修改起止日期后按当前日期范围查询。
- 前端默认日期和“本日 / 本周 / 本月”快捷入口都按 `Asia/Shanghai` 计算,不使用浏览器本地时区。
- 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。
- 图表横轴按后端返回的 bucket label 展示;本月和较长自定义时段使用固定最小日期列宽并允许横向滚动,避免日期刻度互相挤压。
## 指标口径
@@ -112,7 +112,9 @@ worker 配置:
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_ID`:实例 ID;未配置时用 hostname/pid 派生。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY`:单进程并发领取/执行数量。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS`:空队列轮询间隔。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS`:任务 lease 时长;worker 会按约三分之一 lease、最长 30 秒的间隔续租。该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS`:任务 lease 时长,默认 `600`;worker 会按约三分之一 lease、最长 30 秒的间隔续租。该值应覆盖一次心跳网络抖动窗口,不需要大于完整外部生成链路耗时。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS`:普通外部生成 job 的执行预算,默认 `900`。超过预算后当前 worker 停止当前尝试、写入失败 / 重试状态并释放 worker 槽位,避免任务长期保持 `running_active`;若业务 future 已在计费操作内被取消,计费层会按外部生成 job id 异步补偿退款。
- `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS`:视频、角色动作等长耗时 job 的执行预算,默认 `1800`。
controller 配置:
@@ -124,7 +126,7 @@ controller 配置:
- `GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_SERVICE_TEMPLATE`:systemd worker 模板,默认 `genarrative-external-generation-worker@{}.service`。
- `GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_DRY_RUN`:只记录决策不执行 systemctl,默认 `false`。
动态缩扩容方式:生产默认由 `deploy/systemd/genarrative-external-generation-controller.service` 启动 `GENARRATIVE_PROCESS_ROLE=external-generation-controller`,controller 读取 `get_external_generation_queue_stats_and_return` 后对 `genarrative-external-generation-worker@N.service` 执行精确 `systemctl start/stop`;无需改变 HTTP 进程数。controller 只操作 `@1..@MAX` 中的缺口或最高编号多余实例,保留 `@1` 作为保底 worker。缩容或发布重启 worker 时,进程收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务会在 lease 过期后被其它 worker 重新领取。容器链路已有独立 `external-generation-worker` compose service;扩 worker 必须扩这个 worker service,不能只扩 `api-server` HTTP service。
动态缩扩容方式:生产默认由 `deploy/systemd/genarrative-external-generation-controller.service` 启动 `GENARRATIVE_PROCESS_ROLE=external-generation-controller`,controller 读取 `get_external_generation_queue_stats_and_return` 后对 `genarrative-external-generation-worker@N.service` 执行精确 `systemctl start/stop`;无需改变 HTTP 进程数。controller 只操作 `@1..@MAX` 中的缺口或最高编号多余实例,保留 `@1` 作为保底 worker。缩容或发布重启 worker 时,进程收到 SIGINT/SIGTERM 后会停止 claim 新任务并等待当前任务完成;若进程被硬杀、机器断电或超过 systemd `TimeoutStopSec`,未完成任务会在 lease 过期后被其它 worker 重新领取。若 worker 内业务 future 长时间无返回,执行预算到期后会结束当前尝试并释放槽位;如果当时 SpacetimeDB 写回失败,任务也会按较短 lease 进入可重领窗口,避免无进展续租无限延长。容器链路已有独立 `external-generation-worker` compose service;扩 worker 必须扩这个 worker service,不能只扩 `api-server` HTTP service。
## 已接入的拼图纵切
@@ -42,6 +42,7 @@ cargo run -p pingora-gateway --manifest-path server-rs/Cargo.toml
```bash
npm run check:pingora-gateway-smoke
npm run check:nginx-spa-routes
npm run check:pingora-route-parity
npm run check:nginx-pingora-canary
npm run check:pingora-canary-docker
@@ -59,9 +60,11 @@ npm run check:pingora-cutover-evidence-audit
npm run check:pingora-release-readiness
```
`check:pingora-gateway-smoke` 会临时启动 mock `api-server`、mock SpacetimeDB、mock Gitea 和 `pingora-gateway`,覆盖 SPA fallback、后台静态路由、HTML / 普通静态资源 `no-cache`、Vite 指纹静态资源 immutable 缓存、静态 `ETag` / `Last-Modified` 与 `304` 协商缓存、静态 `HEAD` 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Real-IP` / `X-Forwarded-For`)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 `check-pingora-direct-live.mjs` 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / `Range: bytes=0-0` 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 `-- --verbose` 输出网关 stderr / stdout;已确认二进制无需重编时可追加 `-- --skip-build`。
`check:pingora-gateway-smoke` 会临时启动 mock `api-server`、mock SpacetimeDB、mock Gitea 和 `pingora-gateway`,覆盖精确主站 SPA fallback、大小写与尾部斜杠兼容、同前缀未知路径真实 404、后台静态路由、HTML / 普通静态资源 `no-cache`、Vite 指纹静态资源 immutable 缓存、静态 `ETag` / `Last-Modified` 与 `304` 协商缓存、静态 `HEAD` 响应、静态 Range、静态 access log method/path/status 对账、gzip 最小长度、小响应不压缩、图片资源不压缩、大响应压缩、ACME、TLS 直连、HTTP/2 ALPN、HTTP 到 HTTPS 重定向、内部路由拒绝、shadow probe、API 代理头(`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `X-Real-IP` / `X-Forwarded-For`)、Gitea Host 整站转发、请求体上限、429 接流保护、上游断连 / 超时 JSON 错误、维护模式、维护模式不拦截 Gitea Host 和 SpacetimeDB WebSocket Upgrade,并复用 `check-pingora-direct-live.mjs` 对临时 HTTPS / HTTP redirect / WSS subscribe 入口做 live smoke。该本地 fixture 会让首页同时引用普通静态资源和 Vite 指纹静态资源,direct live JSON 必须确认指纹资源 GET / HEAD / `Range: bytes=0-0` 以及 access log method/path/status 证据,避免正式直连前只证明普通静态读取。排查失败时可追加 `-- --verbose` 输出网关 stderr / stdout;已确认二进制无需重编时可追加 `-- --skip-build`。
`check:pingora-route-parity` 读取 `deploy/pingora/nginx-route-parity.matrix.json`,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由单测和本文档都覆盖同一组核心路由。`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix` 会读取同一份矩阵,逐条断言 `classify_path` 的路由结果、body limit 和接流保护分组。
`check:nginx-spa-routes` 从 `appPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` / `APP_RUNTIME_ROUTES`、`appRoutes.tsx` 的精确路由判断和兼容恢复路径 `/creation/rpg/agent` 提取当前主站 SPA allowlist,确认生产、开发和容器三套 Nginx 模板集合一致,并验证大小写、尾部斜杠和 `/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 等未知反例。
`check:pingora-route-parity` 会先执行同一 Nginx SPA 路由门禁,再读取 `deploy/pingora/nginx-route-parity.matrix.json`,静态确认生产 / 开发 Nginx 模板、Pingora Rust 路由 allowlist / 单测和本文档都覆盖同一组核心路由。`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml matches_nginx_route_parity_matrix` 会读取同一份矩阵,逐条断言 `classify_path` 的路由结果、body limit 和接流保护分组。
`check:nginx-pingora-canary` 会静态校验 `deploy/nginx/snippets/genarrative-pingora-canary.conf` 的本机来源限制、handoff 响应头、probe token 占位、前缀 rewrite、低缓冲和 WebSocket Upgrade 设置,也会校验 `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 只能作为独立 loopback `server` 片段使用、默认监听 `127.0.0.1:18083`、写独立 access log、没有 rewrite、覆盖真实 `/api` / `/v1` / `/assets` 代表路径。本机安装了 Nginx 时脚本会额外把两个 snippet 包进临时 `http {}` 执行 `nginx -t`;需要在 CI / 目标 agent 上强制要求真实 Nginx 语法检查时执行 `node scripts/check-nginx-pingora-canary.mjs --require-nginx`。
@@ -539,9 +542,10 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
| `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
| `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 |
| 其它路径 | 先读取静态文件或目录 index,失败回退 `/index.html`,HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| 主站 SPA allowlist | 只对当前前端完整路由及兼容恢复路径 `/creation/rpg/agent` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist`、`/runtime/not-exist`、`/puzzle/not-exist` 不进入 SPA fallback。 |
维护模式下,API-like 路由返回 JSON `503`,Web 静态路由优先返回 `maintenance.html`,不存在时返回纯文本 `503`。
维护模式下,公网 API-like 路由返回 JSON `503`;公网 Web 静态路由先读取 `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_PAGE_FILE` 指向的 release 外运行态公告,缺失时回退 `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT/maintenance.html`,两者都不存在时返回纯文本 `503`。版本化默认页不得包含日期或具体时段,临时公告由 `maintenance-on.sh --page-file` 安装并在 `maintenance-off.sh` 时清理。IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与后台 API、SpacetimeDB 路由均按非维护状态继续处理;应用层登录、管理员鉴权和其它业务鉴权保持不变。Pingora 直连按 TCP peer 判定来源;仅当 peer 是 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`,绝不使用客户端可伪造的 `X-Forwarded-For` 做维护放行。该放行只绕过网关维护响应;若 `pause-after-stdb` 已停止 api-server,内网普通 API 和后台 API 仍不可用。
代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。
静态 `Range` 只支持单段 bytes range;多段 range 暂按完整文件返回,避免在正式替换前引入 multipart 响应面。`If-None-Match` / `If-Modified-Since` 优先于 `Range` 判定,命中时仍返回 `304`;`If-Range` 日期匹配时继续返回 `206`,日期旧于文件或弱 ETag 校验器时回完整 `200`;`206` / `304` / `416` 不做 gzip 压缩,避免 `Content-Range` 语义被响应体改写破坏。Gateway smoke 会用固定 `X-Request-Id` 对账静态 `304`、`405`、`206`、`416` 的 Pingora access log 行,确认本地响应状态也进入正式切换证据链。
静态路由只允许 `GET` / `HEAD` 读取;其它方法在确认命中静态候选后返回 `405` 并写入 `Allow: GET, HEAD`,缺失文件仍返回 `404`,避免直连后错误客户端把静态入口当作可写接口。