diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index c40e8297c..981a7c932 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -47,79 +47,6 @@ 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/nginx/snippets/tsingnova-client-downloads.conf b/deploy/nginx/snippets/tsingnova-client-downloads.conf new file mode 100644 index 000000000..d786b7bc3 --- /dev/null +++ b/deploy/nginx/snippets/tsingnova-client-downloads.conf @@ -0,0 +1,30 @@ +# 清智创游官网下载接口:此 snippet 只在现役 tsingnova-games HTTPS server 中 include。 +# 依赖主 Nginx 配置已声明的 genarrative_api、限流区和维护状态变量。 +location = /api/client-downloads { + default_type application/json; + limit_conn genarrative_api_conn 64; + limit_req zone=genarrative_api_rps burst=64 nodelay; + limit_conn_status 429; + limit_req_status 429; + + if ($genarrative_maintenance) { + return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; + } + + proxy_pass http://genarrative_api; + proxy_http_version 1.1; + 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-Request-Id $request_id; + proxy_set_header Cookie ""; + proxy_set_header Authorization ""; + proxy_connect_timeout 3s; + proxy_read_timeout 15s; + proxy_send_timeout 15s; + proxy_buffering off; + proxy_cache off; + add_header X-Accel-Buffering no always; +} diff --git a/deploy/pingora/nginx-route-parity.matrix.json b/deploy/pingora/nginx-route-parity.matrix.json index c620792f0..1d3f8b993 100644 --- a/deploy/pingora/nginx-route-parity.matrix.json +++ b/deploy/pingora/nginx-route-parity.matrix.json @@ -112,19 +112,6 @@ }, "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/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 60ff72e24..50ceb846d 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -6,11 +6,14 @@ ### 清智创游官网独立域名路由 -清智创游官网 `tsingnovagames.com` / `www.tsingnovagames.com` 的首页静态资源位于同一 Web 根目录的 `/home/`,首页“游戏引擎”入口按同源请求 `/api/client-downloads`。生产 Nginx 必须让这两个域名进入包含 `/api` 反代的主站 `server block`,并把该域名根路径重写到 `/home/`;否则页面可以打开,但接口会被静态站点直接返回 HTML 404,前端显示“暂时无法获取下载信息,请稍后重试”。 +清智创游官网 `tsingnovagames.com` / `www.tsingnovagames.com` 使用独立的 Nginx 配置 `/etc/nginx/conf.d/tsingnova-games.conf`,静态根目录是 `/srv/genarrative/corporate-site-current`;主站模板 `deploy/nginx/genarrative.conf` 不应重复声明这两个域名。官网首页按同源请求 `/api/client-downloads`,因此应在现役官网 HTTPS `server` 中 include `deploy/nginx/snippets/tsingnova-client-downloads.conf`,只代理这个精确路径到 `genarrative_api`,不要把整个 `/api` 交给官网静态站点兜底。 -`Genarrative-Server-Provision` 在 `SERVER_NAME=genarrative.world`(或 `www.genarrative.world`)且使用 `production-https` 时会自动补入这两个官网别名,并渲染 `/home/` 根路径兼容规则。修改后需重新执行一次正式服务器配置发布并 reload Nginx,不要只重启 api-server: +线上修复或排障后必须确认的是**生效配置**而不是仓库模板: ```bash +sudo nginx -T | grep -n -A45 -B5 'server_name tsingnovagames.com' +sudo nginx -t +sudo systemctl reload nginx curl -fsS -D ~/data/tmp/tsingnova-client-download.headers \ https://tsingnovagames.com/api/client-downloads \ -o ~/data/tmp/tsingnova-client-download.json @@ -18,7 +21,7 @@ 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`。 +验收必须同时满足:接口返回 `200 application/json`,响应包含 `downloads` 与 `unavailablePlatforms`,并带 `Cache-Control: no-store`;`https://tsingnovagames.com/` 仍返回官网首页。若 API 直连 `http://127.0.0.1:8082/api/client-downloads` 是 `200`、官网域名仍是 `404`,说明官网 vhost 没有 include 下载 snippet,不能靠 reload 旧配置解决。 ## 构建回归的隔离与发布文件权限 diff --git a/scripts/tsingnova-home-route.test.ts b/scripts/tsingnova-home-route.test.ts index caa7934fc..0510c375d 100644 --- a/scripts/tsingnova-home-route.test.ts +++ b/scripts/tsingnova-home-route.test.ts @@ -3,26 +3,97 @@ 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', +const clientDownloadsSnippet = readFileSync( + 'deploy/nginx/snippets/tsingnova-client-downloads.conf', 'utf8', ); +function extractServerBlocks(source: string): string[] { + const blocks: string[] = []; + const serverStart = /\bserver\s*\{/gu; + let match: RegExpExecArray | null; + + while ((match = serverStart.exec(source)) !== null) { + const openBrace = source.indexOf('{', match.index); + let depth = 0; + let quote: '"' | "'" | null = null; + let escaped = false; + let inComment = false; + + for (let index = openBrace; index < source.length; index += 1) { + const character = source[index]; + + if (inComment) { + if (character === '\n') { + inComment = false; + } + continue; + } + if (quote !== null) { + if (escaped) { + escaped = false; + } else if (character === '\\') { + escaped = true; + } else if (character === quote) { + quote = null; + } + continue; + } + if (character === '#') { + inComment = true; + } else if (character === '"' || character === "'") { + quote = character; + } else if (character === '{') { + depth += 1; + } else if (character === '}') { + depth -= 1; + if (depth === 0) { + blocks.push(source.slice(match.index, index + 1)); + serverStart.lastIndex = index + 1; + break; + } + } + } + } + + return blocks; +} + describe('清智创游官网引擎下载路由', () => { - it('生产 Nginx 为官网域名提供独立 vhost 和 API 反代', () => { - expect(productionNginx).toContain( - 'server_name tsingnovagames.com www.tsingnovagames.com;', + it('主站模板不重复声明官网独立域名,下载路由由现役官网 vhost 单独维护', () => { + expect(productionNginx).not.toContain('tsingnovagames.com'); + expect(clientDownloadsSnippet).toContain( + 'location = /api/client-downloads {', ); - 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', + it('下载 snippet 只声明精确接口并复用主站保护与 upstream', () => { + expect(clientDownloadsSnippet).toContain( + 'location = /api/client-downloads {', + ); + expect(clientDownloadsSnippet).not.toMatch( + /location\s+(?:~|\^~)?\s*\/api(?:\/|\s|\{|\$)/u, + ); + for (const directive of [ + 'limit_conn genarrative_api_conn 64;', + 'limit_req zone=genarrative_api_rps burst=64 nodelay;', + 'limit_conn_status 429;', + 'limit_req_status 429;', + 'if ($genarrative_maintenance) {', + 'proxy_pass http://genarrative_api;', + 'proxy_set_header Cookie "";', + 'proxy_set_header Authorization "";', + 'proxy_connect_timeout 3s;', + 'proxy_read_timeout 15s;', + 'proxy_send_timeout 15s;', + 'proxy_buffering off;', + 'proxy_cache off;', + 'add_header X-Accel-Buffering no always;', + ]) { + expect(clientDownloadsSnippet).toContain(directive); + } + expect(clientDownloadsSnippet).not.toMatch( + /^\s*add_header\s+Cache-Control\b/mu, ); }); });