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/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 a97c5db52..5ce4c00eb 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -4,6 +4,25 @@ 主站 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` 使用独立的 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` 交给官网静态站点兜底。 + +线上修复或排障后必须确认的是**生效配置**而不是仓库模板: + +```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 +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/` 仍返回官网首页。若 API 直连 `http://127.0.0.1:8082/api/client-downloads` 是 `200`、官网域名仍是 `404`,说明官网 vhost 没有 include 下载 snippet,不能靠 reload 旧配置解决。 + ## 构建回归的隔离与发布文件权限 Git hook 的临时仓库测试必须清除子进程继承的仓库定位环境(例如 `GIT_DIR`、`GIT_WORK_TREE`、`GIT_INDEX_FILE`);仅设置 `cwd` 不能隔离 Git。回归应从带这些变量的外层仓库运行,验证外层引用、索引与配置不变。临时测试文件必须留在独立目录并清理,不得通过测试生成主仓库提交或覆盖 ESLint、Prettier 配置。修复 lint 配置时保留原有规则、忽略范围与零警告门禁,不以关闭规则代替排障。 diff --git a/public/creation-home/asset-manifest.json b/public/creation-home/asset-manifest.json index 88ae553d4..90cd05bc2 100644 --- a/public/creation-home/asset-manifest.json +++ b/public/creation-home/asset-manifest.json @@ -79,6 +79,11 @@ "subject": "左侧导航发现", "bytes": 109929 }, + { + "file": "/creation-home/nav-games.png", + "subject": "左侧导航游戏", + "bytes": 127964 + }, { "file": "/creation-home/nav-profile.png", "subject": "左侧导航我的", diff --git a/public/creation-home/nav-games.png b/public/creation-home/nav-games.png new file mode 100644 index 000000000..9c2f1091c Binary files /dev/null and b/public/creation-home/nav-games.png differ diff --git a/scripts/tsingnova-home-route.test.ts b/scripts/tsingnova-home-route.test.ts new file mode 100644 index 000000000..0510c375d --- /dev/null +++ b/scripts/tsingnova-home-route.test.ts @@ -0,0 +1,99 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +const productionNginx = readFileSync('deploy/nginx/genarrative.conf', 'utf8'); +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('主站模板不重复声明官网独立域名,下载路由由现役官网 vhost 单独维护', () => { + expect(productionNginx).not.toContain('tsingnovagames.com'); + expect(clientDownloadsSnippet).toContain( + 'location = /api/client-downloads {', + ); + }); + + 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, + ); + }); +}); diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index 7f8033a41..6c7a21e46 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -636,7 +636,7 @@ export function PlatformEntryFlowShellImpl({ active={isGamesStage} emphasized={isGamesStage} icon={Gamepad2} - iconSrc="/creation-home/nav-projects.png" + iconSrc="/creation-home/nav-games.png" label="游戏" onClick={openGames} />