From 01394ed238f953e0f52fe52d9d76f679b54c38f1 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:15:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=97=A8=E7=A6=81=E5=8A=A0=E5=9B=BA=E4=B8=8E?= =?UTF-8?q?=20Windows=20=E5=8F=AF=E8=B7=91=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scripts/check-generated-bindings.mjs 并接入 lint 与 ai-game-creator-shell:check:rust:crates:重跑 export_bindings 前后逐字节比对共享契约绑定。 - check:native-shells 的 AGC 边界改为负向门禁:渲染源码不得出现 fetch(/XHR/EventSource/sendBeacon/WebSocket/plugin-http,客户端 capability 不得授予 http:default。 - check-pingora-gateway-smoke 在 Windows 上找 pingora-gateway.exe,并修掉「API 并发保护」先关客户端连接再读响应的竞态。 - mobile-shell 三个 smoke 改成固定命令串启动 npm(绕开 npm.cmd EINVAL 与 DEP0190),desktop-shell 的 stage-release-binary 改用 fileURLToPath 修掉 F:\F:\… 路径拼接。 - vitest.config.ts 白名单补回现役根用例;三份 nginx 模板的 SPA allowlist 补回 /components 与 /design-system。 --- .../scripts/stage-release-binary.mjs | 7 +- .../scripts/check-eas-build-config.mjs | 15 ++-- .../scripts/check-expo-config.mjs | 7 +- .../scripts/check-expo-export.mjs | 16 ++-- deploy/container/nginx.conf | 2 +- deploy/nginx/genarrative-dev-http.conf | 2 +- deploy/nginx/genarrative.conf | 2 +- package.json | 5 +- scripts/check-generated-bindings.mjs | 81 +++++++++++++++++++ scripts/check-native-shells.mjs | 69 ++++++++++------ scripts/check-pingora-gateway-smoke.mjs | 11 ++- vitest.config.ts | 8 ++ 12 files changed, 169 insertions(+), 56 deletions(-) create mode 100644 scripts/check-generated-bindings.mjs diff --git a/apps/desktop-shell/scripts/stage-release-binary.mjs b/apps/desktop-shell/scripts/stage-release-binary.mjs index 1bd9a98e8..1c95e6005 100644 --- a/apps/desktop-shell/scripts/stage-release-binary.mjs +++ b/apps/desktop-shell/scripts/stage-release-binary.mjs @@ -1,7 +1,12 @@ import fs from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const repoRoot = path.resolve(new URL('../../../', import.meta.url).pathname); +// `new URL(...).pathname` 在 Windows 上会给出 `/F:/…` 这种带前导斜杠的形式, +// 交给 path.resolve 会拼成 `F:\F:\…`;必须用 fileURLToPath 做跨平台转换。 +const repoRoot = path.resolve( + fileURLToPath(new URL('../../../', import.meta.url)), +); const releaseDir = path.join( repoRoot, 'apps', diff --git a/apps/mobile-shell/scripts/check-eas-build-config.mjs b/apps/mobile-shell/scripts/check-eas-build-config.mjs index f629b10ae..b79b9f78b 100644 --- a/apps/mobile-shell/scripts/check-eas-build-config.mjs +++ b/apps/mobile-shell/scripts/check-eas-build-config.mjs @@ -42,14 +42,13 @@ if (packageConfig.devDependencies?.['eas-cli'] !== '^20.3.0') { ); } -const easVersionResult = spawnSync( - npmCommand, - ['exec', 'eas', '--', '--version'], - { - cwd: shellRoot, - encoding: 'utf8', - }, -); +// 同 check-expo-config.mjs:Windows 上启动 npm.cmd 必须有 shell,这里改用固定命令串, +// 既避开 EINVAL,也避开 Node 对「shell + args 数组」的 DEP0190 警告。 +const easVersionResult = spawnSync(`${npmCommand} exec eas -- --version`, { + cwd: shellRoot, + encoding: 'utf8', + shell: true, +}); if (easVersionResult.error) { throw new Error( diff --git a/apps/mobile-shell/scripts/check-expo-config.mjs b/apps/mobile-shell/scripts/check-expo-config.mjs index e3ee16064..d17a3dd1e 100644 --- a/apps/mobile-shell/scripts/check-expo-config.mjs +++ b/apps/mobile-shell/scripts/check-expo-config.mjs @@ -31,12 +31,15 @@ const expoPrivacyInfoPluginSource = fs.readFileSync( const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8'); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +// Node 18.20+/20+/24 在 Windows 上不允许不带 shell 直接执行 .cmd(npm.cmd → EINVAL), +// 因此这里用一条固定命令串交给 shell;参数都是字面量、不含空格或中文,不存在重新解析歧义。 +// 不用「shell: true + args 数组」是为了避开 Node 的 DEP0190(args 在 shell 模式下不会被转义)。 const result = spawnSync( - npmCommand, - ['exec', 'expo', 'config', '--', '--type', 'public', '--json'], + `${npmCommand} exec expo config -- --type public --json`, { cwd: shellRoot, encoding: 'utf8', + shell: true, }, ); diff --git a/apps/mobile-shell/scripts/check-expo-export.mjs b/apps/mobile-shell/scripts/check-expo-export.mjs index cb58b2d8d..ed96ebfa4 100644 --- a/apps/mobile-shell/scripts/check-expo-export.mjs +++ b/apps/mobile-shell/scripts/check-expo-export.mjs @@ -50,22 +50,16 @@ const requiredNativeHostContextTokens = [ function runExpoExport(platform) { const outputDir = `.expo-export-smoke/${platform}`; + // 同 check-expo-config.mjs:Windows 上启动 npm.cmd 必须有 shell,这里用固定命令串, + // 既避开 EINVAL 也避开 Node 对「shell + args 数组」的 DEP0190;串里的平台名来自固定 + // 列表、输出目录是相对路径,都不含空格或中文。 const result = spawnSync( - npmCommand, - [ - 'exec', - 'expo', - 'export', - '--', - '--platform', - platform, - '--output-dir', - outputDir, - ], + `${npmCommand} exec expo export -- --platform ${platform} --output-dir ${outputDir}`, { cwd: shellRoot, encoding: 'utf8', stdio: 'pipe', + shell: true, }, ); diff --git a/deploy/container/nginx.conf b/deploy/container/nginx.conf index 6d3284ce0..ac2043c76 100644 --- a/deploy/container/nginx.conf +++ b/deploy/container/nginx.conf @@ -141,7 +141,7 @@ http { try_files /index.html =404; } - location ~* "^/(?:creation|editor/canvas|profile|project)/?$" { + location ~* "^/(?:components|creation|design-system|editor/canvas|profile|project)/?$" { try_files $uri /index.html =404; } # END GENARRATIVE MAIN SPA ROUTES diff --git a/deploy/nginx/genarrative-dev-http.conf b/deploy/nginx/genarrative-dev-http.conf index 640ef088c..d95dba4a8 100644 --- a/deploy/nginx/genarrative-dev-http.conf +++ b/deploy/nginx/genarrative-dev-http.conf @@ -190,7 +190,7 @@ server { try_files /index.html =404; } - location ~* "^/(?:creation|editor/canvas|profile|project)/?$" { + location ~* "^/(?:components|creation|design-system|editor/canvas|profile|project)/?$" { error_page 503 /maintenance.html; if ($genarrative_maintenance) { diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index b7d1c433a..c3dce597f 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -210,7 +210,7 @@ server { try_files /index.html =404; } - location ~* "^/(?:creation|editor/canvas|profile|project)/?$" { + location ~* "^/(?:components|creation|design-system|editor/canvas|profile|project)/?$" { error_page 503 /maintenance.html; if ($genarrative_maintenance) { diff --git a/package.json b/package.json index 0098a0e6b..9d23f82af 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "check:repository-ci": "bash scripts/check-repository-ci.sh", "check:rustfmt": "cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check && cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check && cargo fmt --all --manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml -- --check && cargo fmt --all --manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml -- --check", "check:spacetime-schema": "node scripts/check-spacetime-schema-guard.mjs", + "check:generated-bindings": "node scripts/check-generated-bindings.mjs", "check:game-distribution-media-e2e": "node scripts/check-game-distribution-media-e2e.mjs", "check:production-ops": "node scripts/check-production-ops-guardrails.mjs", "check:preview-deployer": "node scripts/check-preview-deployer.mjs", @@ -120,7 +121,7 @@ "check:server-rs-ddd": "npm run check:spacetime-schema && npm run check:spacetime-runtime-access && npm run check:module-runtime-artifact && node scripts/check-server-rs-ddd-boundaries.mjs", "lint:eslint": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --max-warnings 0", "typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit", - "lint": "npm run check:encoding && npm run check:doc-index && npm run check:npm-workspaces && npm run check:git-hooks && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:production-ops && npm run check:preview-deployer && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck", + "lint": "npm run check:encoding && npm run check:doc-index && npm run check:npm-workspaces && npm run check:git-hooks && npm run check:rustfmt && npm run check:spacetime-schema && npm run check:generated-bindings && npm run check:production-ops && npm run check:preview-deployer && npm run check:maintenance-page && npm run lint:eslint && npm run typecheck", "lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .", "format:rust": "cargo fmt --all --manifest-path server-rs/Cargo.toml && cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml && cargo fmt --all --manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml && cargo fmt --all --manifest-path plugins/agc-godot-editor/native/godot-editor-bridge/Cargo.toml", "format": "prettier --write . && npm run format:rust", @@ -193,7 +194,7 @@ "agent-runtime-orchestration:check": "cargo test --manifest-path server-rs/crates/agent-runtime-orchestration/Cargo.toml", "ai-game-creator-shell:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck", "ai-game-creator-shell:check:web": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests", - "ai-game-creator-shell:check:rust:crates": "npm run agent-runtime-core:check && npm run agent-runtime-orchestration:check && cargo test --locked -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test --locked -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app", + "ai-game-creator-shell:check:rust:crates": "npm run agent-runtime-core:check && npm run agent-runtime-orchestration:check && cargo test --locked -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test --locked -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app && npm run check:generated-bindings", "ai-game-creator-shell:check:rust:shell": "node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4", "ai-game-creator-shell:check:rust": "npm run ai-game-creator-shell:check:rust:crates && npm run ai-game-creator-shell:check:rust:shell", "ai-game-creator-shell:check": "npm run ai-game-creator-shell:check:web && npm run ai-game-creator-shell:check:rust && npm run ai-game-creator-shell:agent-run:smoke", diff --git a/scripts/check-generated-bindings.mjs b/scripts/check-generated-bindings.mjs new file mode 100644 index 000000000..f2a108c09 --- /dev/null +++ b/scripts/check-generated-bindings.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * 校验 ts-rs 生成的共享契约绑定与 Rust 声明一致。 + * + * 做法是「重新生成一遍并与工作区现有内容逐字节比较」:只比 git diff 会漏掉「Rust 改了但 + * 生成文件没重新跑」以外的情形,也很容易被本地未提交改动掩盖;比较生成前后快照更直接—— + * 只要重新生成后的内容与现有内容不同,就说明仓库里的绑定是陈旧的。 + */ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const generatedDir = path.join( + repoRoot, + 'packages/shared/src/contracts/generated', +); + +function snapshot() { + if (!fs.existsSync(generatedDir)) return new Map(); + return new Map( + fs + .readdirSync(generatedDir, { withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => [ + entry.name, + fs.readFileSync(path.join(generatedDir, entry.name)), + ]), + ); +} + +const before = snapshot(); +const result = spawnSync( + 'cargo', + [ + 'test', + '--locked', + '-p', + 'shared-contracts', + '--features', + 'ts-bindings', + '--manifest-path', + 'server-rs/Cargo.toml', + 'export_bindings', + ], + { cwd: repoRoot, encoding: 'utf8' }, +); +if (result.status !== 0) { + console.error('生成绑定校验无法运行:export_bindings 未通过'); + if (result.stdout) console.error(result.stdout.trim()); + if (result.stderr) console.error(result.stderr.trim()); + process.exit(result.status ?? 1); +} +const after = snapshot(); + +const problems = []; +for (const [name, content] of after) { + const previous = before.get(name); + if (!previous) problems.push(`新增 ${name}`); + else if (!previous.equals(content)) problems.push(`内容变化 ${name}`); +} +for (const name of before.keys()) { + if (!after.has(name)) problems.push(`删除 ${name}`); +} + +if (problems.length > 0) { + console.error('生成绑定与 Rust 声明不一致:'); + for (const problem of problems) console.error(` - ${problem}`); + console.error( + '请运行 `cargo test -p shared-contracts --features ts-bindings export_bindings`,并把重新生成的结果与 Rust 改动一并提交。', + ); + process.exit(1); +} + +console.log( + `生成绑定校验通过:${after.size} 个文件与 Rust 声明一致(${path.relative(repoRoot, generatedDir)})。`, +); diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 5c0f8af18..03a4d6d69 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -2564,21 +2564,47 @@ function assertAiGameCreatorShellUserDevBoundary() { !aiGameCreatorClientHttpSource.includes( "'https://dev.genarrative.world'", ) || - !aiGameCreatorClientHttpSource.includes("transport: 'tauri-http'") || - !aiGameCreatorClientHttpSource.includes('target.origin !==') + !aiGameCreatorClientHttpSource.includes( + "'https://www.genarrative.world'", + ) || + /\bfetch\s*\(/u.test(aiGameCreatorClientHttpSource) || + aiGameCreatorClientHttpSource.includes("transport: 'tauri-http'") ) { throw new Error( - 'AI game creator release dev API transport boundary drifted', + 'AI game creator clientHttp must only resolve the server selection, not carry a transport', ); } + // 渲染层是离线前端:平台接口、OSS 直传、Provider 与更新清单的网络 IO 全部在 Rust 侧 + // (`src-tauri/src/account_api.rs` / `auth_session.rs` / `platform_asset_upload.rs` / + // `game_distribution_publish.rs` 等 reqwest facade)。这里对整棵渲染源码加网络原语与 + // Tauri HTTP guest 的负向门禁,避免以后再长出第二条传输路径。 + const rendererNetworkPrimitivePatterns = [ + ['fetch(', /\bfetch\s*\(/u], + ['XMLHttpRequest', /\bXMLHttpRequest\b/u], + ['EventSource', /\bEventSource\b/u], + ['sendBeacon(', /\bsendBeacon\s*\(/u], + ['new WebSocket(', /\bnew\s+WebSocket\s*\(/u], + ['@tauri-apps/plugin-http', /@tauri-apps\/plugin-http/u], + ]; + for (const rendererFilePath of collectFiles( + 'apps/ai-game-creator-shell/src', + (entryPath) => /\.(ts|tsx)$/u.test(entryPath), + )) { + const rendererSource = fs.readFileSync(rendererFilePath, 'utf8'); + for (const [label, pattern] of rendererNetworkPrimitivePatterns) { + if (pattern.test(rendererSource)) { + throw new Error( + `AI game creator renderer must stay offline, but ${normalizeScannedFilePath(rendererFilePath)} contains ${label}`, + ); + } + } + } if ( - !aiGameCreatorCargoManifestSource.includes( - 'tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }', - ) || - !aiGameCreatorShellTauriSource.includes('tauri_plugin_http::init()') + aiGameCreatorCargoManifestSource.includes('tauri-plugin-http') || + aiGameCreatorShellTauriSource.includes('tauri_plugin_http::init()') ) { throw new Error( - 'AI game creator release must register the native HTTP plugin without the OS automatic system-proxy feature', + 'AI game creator shell must not register a renderer HTTP plugin; native IO goes through the Rust reqwest facades', ); } if ( @@ -2588,27 +2614,18 @@ function assertAiGameCreatorShellUserDevBoundary() { 'AI game creator monorepo build must dedupe React runtime packages', ); } - const httpPermission = (aiGameCreatorMainCapability.permissions ?? []).find( - (permission) => - typeof permission === 'object' && - permission?.identifier === 'http:default', - ); if ( - !httpPermission || - JSON.stringify(httpPermission.allow ?? []) !== - JSON.stringify([ - { url: 'https://dev.genarrative.world/api/*' }, - { url: 'https://www.genarrative.world/api/*' }, - { url: 'https://*/api/*' }, - { url: 'http://localhost:*/*' }, - { url: 'http://127.0.0.1:*/*' }, - { url: 'https://*.aliyuncs.com/*' }, - ]) + (aiGameCreatorMainCapability.permissions ?? []).some( + (permission) => + permission === 'http:default' || + (typeof permission === 'object' && + permission?.identifier === 'http:default'), + ) ) { throw new Error( - // 更新清单与安装包下载由 tauri-plugin-updater 在原生侧完成; - // 封面与截图仍通过 webview 的 http 插件向凭证指定的 OSS 地址直传。 - 'AI game creator native HTTP scope must match the release, dev, custom HTTPS, loopback API, and OSS media upload boundary', + // 客户端窗口只保留剪贴板、图片、资源关闭、opener、updater 与原生对话框权限; + // 更新清单下载由 tauri-plugin-updater 在原生侧完成,素材直传也不再经渲染层。 + 'AI game creator client capability must not grant the renderer HTTP permission', ); } diff --git a/scripts/check-pingora-gateway-smoke.mjs b/scripts/check-pingora-gateway-smoke.mjs index 27e35d45b..44978b284 100644 --- a/scripts/check-pingora-gateway-smoke.mjs +++ b/scripts/check-pingora-gateway-smoke.mjs @@ -1720,8 +1720,10 @@ async function expectConcurrencyLimit(baseUrl, api) { ); } finally { api.state.releaseHold?.(); - hold.socket.end(); + // 先读完被放行的响应再关客户端连接:原先「释放 hold 后立刻 FIN」会让客户端半关闭 + // 与上游响应抢跑,Windows 上稳定表现为 `HTTP 响应提前关闭`(Linux 上只是偶尔更宽松)。 const holdResponse = await hold.done.catch((error) => ({ error })); + hold.socket.end(); if (holdResponse?.error) { failures.push( `API 并发保护: hold 请求失败:${holdResponse.error.message}`, @@ -2519,9 +2521,12 @@ function resolveGatewayBinary() { return explicit; } + // Windows 上 cargo 产出的是 `pingora-gateway.exe`;Linux 侧名字保持不变。 + const binaryName = + process.platform === 'win32' ? 'pingora-gateway.exe' : 'pingora-gateway'; const candidates = [ - path.join(repoRoot, 'server-rs', 'target', 'debug', 'pingora-gateway'), - path.join(repoRoot, 'target', 'debug', 'pingora-gateway'), + path.join(repoRoot, 'server-rs', 'target', 'debug', binaryName), + path.join(repoRoot, 'target', 'debug', binaryName), ]; const found = candidates.find((candidate) => existsSync(candidate)); if (!found) { diff --git a/vitest.config.ts b/vitest.config.ts index 76a7753c4..8f3abe2bb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -109,12 +109,17 @@ export default defineConfig({ 'src/config/**/*.test.ts', 'src/routing/activeAppPageRoutes.test.ts', 'src/routing/activeAppRoutes.test.ts', + 'src/routing/RouteImageReadyGate.test.ts', + 'src/persistence/**/*.test.ts', + 'src/editor/shared/jsonClient.test.ts', 'src/services/activeAppTitle.test.ts', 'src/services/gameDistributionClient.test.ts', 'src/services/assetReadUrlService.test.ts', 'src/services/authService.test.ts', 'src/services/apiClient.test.ts', 'src/services/clipboard.test.ts', + 'src/services/frontendRuntimeConfigService.test.ts', + 'src/services/sseStream.test.ts', 'src/services/host-bridge/**/*.test.ts', 'src/services/image-editor/**/*.test.ts', 'src/services/external-generation/**/*.test.ts', @@ -149,15 +154,18 @@ export default defineConfig({ 'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.test.tsx', 'src/components/platform-entry/platformProfile*.test.ts', 'src/components/platform-entry/usePlatformProfileCenterController*.test.tsx', + 'src/components/platform-entry/PlatformProfilePrimitives.test.tsx', 'src/hooks/useHostNavigationCanGoBack.test.tsx', 'src/hooks/useResolvedAssetReadUrl.test.tsx', 'apps/admin-web/src/**/*.test.ts', 'apps/admin-web/src/**/*.test.tsx', 'apps/ai-game-creator-shell/tests/**/*.test.ts', 'apps/ai-game-creator-shell/tests/**/*.test.tsx', + 'apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.test.tsx', 'miniprogram/**/*.test.js', 'scripts/**/*.test.ts', 'packages/shared/src/contracts/*.test.ts', + 'packages/shared/src/theme.test.ts', 'packages/shared/src/components/**/*.test.ts', 'packages/shared/src/components/**/*.test.tsx', 'packages/shared/src/stores/**/*.test.ts',