From 05bf69f2c2af0c9066fe10daf43a327f54e03092 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 30 Sep 2026 14:12:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=85=E6=99=BA=E5=88=9B?= =?UTF-8?q?=E6=B8=B8=E5=AE=98=E7=BD=91=E5=BC=95=E6=93=8E=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B7=AF=E7=94=B1=20=E6=96=B0=E5=A2=9E=20tsi?= =?UTF-8?q?ngnovagames.com=20=E7=8B=AC=E7=AB=8B=20Nginx=20vhost=20?= =?UTF-8?q?=E5=B9=B6=E4=BB=A3=E7=90=86=20/api=20=E5=88=B0=20api-server=20?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=AE=98=E7=BD=91=20/home/=20=E9=9D=99?= =?UTF-8?q?=E6=80=81=E9=A6=96=E9=A1=B5=E4=B8=8E=E4=B8=8B=E8=BD=BD=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E7=94=9F=E4=BA=A7=E8=BF=90=E7=BB=B4=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E4=B8=8E=E6=8E=92=E9=9A=9C=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/nginx/genarrative.conf | 73 +++++++++++++++++++ deploy/pingora/nginx-route-parity.matrix.json | 13 ++++ ...开发运维】Pingora独立网关试点-2026-06-11.md | 1 + ...发运维】本地开发验证与生产运维-2026-05-15.md | 16 ++++ scripts/tsingnova-home-route.test.ts | 28 +++++++ 5 files changed, 131 insertions(+) create mode 100644 scripts/tsingnova-home-route.test.ts diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index 981a7c932..c40e8297c 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -47,6 +47,79 @@ server { } } +# 清智创游官网独立静态站点:根页面位于 /home/index.html,API 仍由当前 api-server 提供。 +# 该 vhost 不依赖主站 SERVER_ALIASES,避免域名落入静态站点后对 /api 返回 Nginx 404。 +server { + listen 80; + server_name tsingnovagames.com www.tsingnovagames.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name tsingnovagames.com www.tsingnovagames.com; + access_log /var/log/nginx/genarrative.access.log genarrative_upstream; + error_log /var/log/nginx/genarrative.error.log warn; + + ssl_certificate /etc/letsencrypt/live/genarrative.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/genarrative.example.com/privkey.pem; + + root /srv/genarrative/web; + index index.html; + + include /etc/nginx/snippets/genarrative-maintenance.conf; + + location ~ ^/api(?:/|$) { + default_type application/json; + client_max_body_size 210m; + limit_conn genarrative_api_conn 64; + limit_req zone=genarrative_api_rps burst=64 nodelay; + + if ($genarrative_maintenance) { + return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; + } + + proxy_pass http://genarrative_api; + proxy_http_version 1.1; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + add_header X-Accel-Buffering no always; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Request-Id $request_id; + } + + location = / { + error_page 503 /maintenance.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files /home/index.html =404; + } + + location ^~ /home/ { + try_files $uri $uri/ =404; + } + + location / { + error_page 503 /maintenance.html; + error_page 404 /404.html; + + if ($genarrative_maintenance) { + return 503; + } + + try_files $uri $uri/ =404; + } +} + server { listen 443 ssl http2; server_name genarrative.example.com; diff --git a/deploy/pingora/nginx-route-parity.matrix.json b/deploy/pingora/nginx-route-parity.matrix.json index 1d3f8b993..c620792f0 100644 --- a/deploy/pingora/nginx-route-parity.matrix.json +++ b/deploy/pingora/nginx-route-parity.matrix.json @@ -112,6 +112,19 @@ }, "docs": ["`/assets/*`"] }, + { + "id": "official_home_assets", + "samplePath": "/home/assets/index.js", + "expect": { + "kind": "static", + "root": "web", + "mode": "exact" + }, + "nginx": { + "production": ["location ^~ /home/", "try_files $uri $uri/ =404;"] + }, + "docs": ["/home/"] + }, { "id": "generic_api_proxy", "samplePath": "/api/assets/history", diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md index 9ce42bc50..a29e55722 100644 --- a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -10,6 +10,7 @@ - 默认转发 `api-server` 到 `127.0.0.1:8082`。 - 默认转发最小 SpacetimeDB 公网路由到 `127.0.0.1:3101`。 - 默认静态目录为 `/srv/genarrative/web`,维护开关文件为 `/var/lib/genarrative/maintenance/enabled`。 +- 清智创游官网独立域名的 `/home/` 静态资源仍由 Nginx 独立 vhost 提供;`/api/*` 必须进入 api-server,不能回退为静态 404。 - 静态响应会显式写入 `Cache-Control`:HTML / SPA fallback 默认 `no-cache`,Vite 指纹静态资源默认 `public, max-age=31536000, immutable`,其它静态资源默认 `no-cache`。三档分别由 `GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL`、`GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL` 和 `GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL` 覆盖,配置值禁止换行或 NUL,避免响应头注入。静态文件还会按 metadata 写入弱 `ETag`、`Last-Modified` 与 `Accept-Ranges: bytes`,并对 `GET` / `HEAD` 的 `If-None-Match`、`If-Modified-Since` 返回 `304`;单段 `Range: bytes=` 返回 `206 + Content-Range`,越界范围返回 `416 + Content-Range: bytes */`,保持 HTML `no-cache` 下的浏览器协商缓存和 Nginx 直连体验一致。 - `/api` 通用路由同时按 `Content-Length` 与实际流式请求体累计字节数执行大小上限,避免客户端省略长度头绕过网关保护。 - `GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN` 非空时,`/__genarrative_pingora/healthz` 可用同名探针 header 做本机 shadow 健康检查;未带 token 或 token 不匹配时仍返回 404。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index c0f0a9078..60ff72e24 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -4,6 +4,22 @@ 主站 Vite 将 `/api/client-downloads` 转发到当前 `runtimeServerTarget`。通过 `npm run dev:api-server` 与 `npm run dev:web` 联调时,先确认运行日志与 `.app/dev-stack.json` 的实际 API 地址,检查 `/healthz`,再从 Vite 同源访问 `/api/client-downloads`;正常应返回 `downloads` 平台列表、`unavailablePlatforms` 和 `Cache-Control: no-store`,不能落入 SPA HTML fallback。渠道 404 表示未发布,单端失败只影响对应平台;公网 OSS 清单没有官网 CORS,浏览器不直接读取该清单。 +### 清智创游官网独立域名路由 + +清智创游官网 `tsingnovagames.com` / `www.tsingnovagames.com` 的首页静态资源位于同一 Web 根目录的 `/home/`,首页“游戏引擎”入口按同源请求 `/api/client-downloads`。生产 Nginx 必须让这两个域名进入包含 `/api` 反代的主站 `server block`,并把该域名根路径重写到 `/home/`;否则页面可以打开,但接口会被静态站点直接返回 HTML 404,前端显示“暂时无法获取下载信息,请稍后重试”。 + +`Genarrative-Server-Provision` 在 `SERVER_NAME=genarrative.world`(或 `www.genarrative.world`)且使用 `production-https` 时会自动补入这两个官网别名,并渲染 `/home/` 根路径兼容规则。修改后需重新执行一次正式服务器配置发布并 reload Nginx,不要只重启 api-server: + +```bash +curl -fsS -D ~/data/tmp/tsingnova-client-download.headers \ + https://tsingnovagames.com/api/client-downloads \ + -o ~/data/tmp/tsingnova-client-download.json +jq . ~/data/tmp/tsingnova-client-download.json +grep -i '^cache-control: no-store' ~/data/tmp/tsingnova-client-download.headers +``` + +验收必须同时满足:接口返回 `200 application/json`,响应包含 `downloads` 与 `unavailablePlatforms`,并带 `Cache-Control: no-store`;`https://tsingnovagames.com/` 仍返回官网首页。若接口仍返回 Nginx 404,优先检查 `nginx -T` 中 `server_name` 是否包含上述两个域名以及 `/api` location 是否指向 `genarrative_api`。 + ## 构建回归的隔离与发布文件权限 Git hook 的临时仓库测试必须清除子进程继承的仓库定位环境(例如 `GIT_DIR`、`GIT_WORK_TREE`、`GIT_INDEX_FILE`);仅设置 `cwd` 不能隔离 Git。回归应从带这些变量的外层仓库运行,验证外层引用、索引与配置不变。临时测试文件必须留在独立目录并清理,不得通过测试生成主仓库提交或覆盖 ESLint、Prettier 配置。修复 lint 配置时保留原有规则、忽略范围与零警告门禁,不以关闭规则代替排障。 diff --git a/scripts/tsingnova-home-route.test.ts b/scripts/tsingnova-home-route.test.ts new file mode 100644 index 000000000..caa7934fc --- /dev/null +++ b/scripts/tsingnova-home-route.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +const productionNginx = readFileSync('deploy/nginx/genarrative.conf', 'utf8'); +const provisionScript = readFileSync( + 'scripts/jenkins-server-provision.sh', + 'utf8', +); + +describe('清智创游官网引擎下载路由', () => { + it('生产 Nginx 为官网域名提供独立 vhost 和 API 反代', () => { + expect(productionNginx).toContain( + 'server_name tsingnovagames.com www.tsingnovagames.com;', + ); + expect(productionNginx).toMatch( + /server_name tsingnovagames\.com www\.tsingnovagames\.com;[\s\S]*?location ~ \^\/api\(\?:\/\|\$\)[\s\S]*?proxy_pass http:\/\/genarrative_api;/u, + ); + expect(productionNginx).toContain('try_files /home/index.html =404;'); + expect(productionNginx).toContain('location ^~ /home/'); + }); + + it('Server-Provision 会把正式证书路径渲染进官网 vhost', () => { + expect(provisionScript).toContain( + 's|/etc/letsencrypt/live/genarrative.example.com/|/etc/letsencrypt/live/${SERVER_NAME}/|g', + ); + }); +});