From 15623a9eff770d732efc624c9003c45415b8da26 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 21 Jun 2026 21:57:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E7=AB=AF=E5=8F=A3=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面壳 dev 命令显式锁定 Vite 3000 端口 dev 端口解析支持严格端口模式并避免显式端口被 Linux 端口段改写 补充端口错配回归测试和宿主壳文档记忆 --- apps/desktop-shell/scripts/check-config.mjs | 6 ++-- apps/desktop-shell/src-tauri/tauri.conf.json | 2 +- .../shared-memory/decision-log.md | 7 +++++ docs/project-memory/shared-memory/pitfalls.md | 8 +++++ ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 ++ scripts/dev-stack-port-utils.mjs | 14 +++++++++ scripts/dev-stack-port-utils.test.ts | 17 +++++++++++ scripts/dev.mjs | 15 +++++++--- scripts/dev.test.ts | 30 +++++++++++++++++++ 9 files changed, 93 insertions(+), 8 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 86c51ecc7..b2b76828a 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -2341,8 +2341,8 @@ if (config.build?.beforeBuildCommand !== 'npm --prefix ../.. run build:raw && np throw new Error('desktop shell build command must run from apps/desktop-shell'); } -if (config.build?.beforeDevCommand !== 'npm --prefix ../.. run dev:web') { - throw new Error('desktop shell dev command must run the root H5 Vite dev server'); +if (config.build?.beforeDevCommand !== 'npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port') { + throw new Error('desktop shell dev command must run the root H5 Vite dev server on the Tauri devUrl port'); } if ( @@ -2350,7 +2350,7 @@ if ( config.build?.devUrl !== 'http://127.0.0.1:3000/' ) { throw new Error( - 'desktop shell dev script must pin WEB_PORT=3000 to match Tauri devUrl', + 'desktop shell dev script and beforeDevCommand must pin WEB_PORT=3000 to match Tauri devUrl', ); } diff --git a/apps/desktop-shell/src-tauri/tauri.conf.json b/apps/desktop-shell/src-tauri/tauri.conf.json index f7ad81180..6c30d818e 100644 --- a/apps/desktop-shell/src-tauri/tauri.conf.json +++ b/apps/desktop-shell/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ "version": "0.1.0", "identifier": "world.genarrative.desktop", "build": { - "beforeDevCommand": "npm --prefix ../.. run dev:web", + "beforeDevCommand": "npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port", "beforeBuildCommand": "npm --prefix ../.. run build:raw && npm run typecheck", "devUrl": "http://127.0.0.1:3000/", "frontendDist": "../../../dist" diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5cc85cc2a..8b8b8d39a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3264,3 +3264,10 @@ - 决策:`scripts/check-native-shells.mjs` 必须把 HostBridge 模块分成三端共同、Expo / Tauri 原生 App 共同、移动端专属、桌面端专属和微信端专属五类,并从现有文件清单反推实际分类。新增、拆分或迁移桥接模块时,必须先更新分类归属,再同步目录清单、文档和能力流证据。 - 影响范围:`miniprogram/host-bridge/`、`apps/mobile-shell/src/host-bridge/`、`apps/desktop-shell/src-tauri/src/host_bridge/`、`scripts/check-native-shells.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + +## 2026-06-21 Tauri devUrl 与 Vite 端口必须显式对齐 + +- 背景:`npm run desktop-shell:dev` 通过 Tauri `devUrl` 固定加载 `http://127.0.0.1:3000/`,但 Linux dev 端口段逻辑会把未显式指定的 `dev:web` 主站端口映射到用户端口段,例如 `10000+`。只在桌面壳 package script 里设置 `WEB_PORT=3000` 不会让 `scripts/dev.mjs` 把 Web 端口视为显式 CLI 参数,结果 Tauri 仍打开 3000,而 Vite 实际监听其它端口。 +- 决策:桌面壳 `beforeDevCommand` 必须执行 `npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port`,用 CLI 参数锁定主站 Vite 端口并禁止静默漂移;`devUrl` 继续固定 `http://127.0.0.1:3000/`。如果 3000 被占用,应该释放端口后再启动桌面壳,而不是让 Vite 漂移后继续由 Tauri 加载旧端口。 +- 影响范围:`apps/desktop-shell/src-tauri/tauri.conf.json`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/dev.test.ts`、Expo / Tauri HostBridge 方案文档。 +- 验证方式:`npm run test -- scripts/dev.test.ts -t "Linux 桌面壳显式指定 web-port"`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0b94909cf..cec5b6a7b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2198,6 +2198,14 @@ - 验证:生成前后检查 `request.json`,其中 `prompt` 字段应显示中文而不是问号;同一提示词在 UTF-8 文件脚本下应能得到符合主题的图。 - 关联:`.codex/skills/gpt-image-2-apimart/SKILL.md`、`server-rs/crates/api-server/src/jump_hop.rs`。 +## Tauri devUrl 不会自动跟随 dev:web 端口漂移 + +- 现象:运行 `npm run desktop-shell:dev` 时终端显示主站 Vite 实际启动在 `10000+` 端口,但 Tauri 窗口仍加载 `http://127.0.0.1:3000/`,桌面壳表现为白屏、连接失败或加载到旧页面。 +- 原因:Linux dev 端口段只把 CLI `--web-port` 视为显式端口;桌面壳 package script 里的 `WEB_PORT=3000` 会被端口段映射覆盖。Tauri `devUrl` 是静态配置,不会读取 `scripts/dev.mjs` 最终解析出的漂移端口。 +- 处理:桌面壳 `beforeDevCommand` 必须使用 `npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port`,让 Vite 实际监听端口和 Tauri `devUrl` 一致,并在 3000 被占用时直接失败。若 3000 被占用,先释放占用进程再启动桌面壳,不要依赖 Vite 漂移。 +- 验证:`npm run test -- scripts/dev.test.ts -t "Linux 桌面壳显式指定 web-port"`、`npm run desktop-shell:typecheck`、实际启动时终端应显示 `[dev] web: http://127.0.0.1:3000`。 +- 关联:`apps/desktop-shell/src-tauri/tauri.conf.json`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/dev.mjs`。 + ## 自动试玩退出不要回到生成页 - 现象:拼图草稿生成完成后自动进入试玩,用户从试玩退出或使用系统返回时落回生成进度页,页面还暴露“重新生成”按钮。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 03481493a..04ade60cd 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -560,6 +560,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳 bundle 图标集从现有真实品牌 PNG `apps/desktop-shell/src-tauri/icons/icon.png` 派生,补齐 `32x32.png`、`128x128.png`、`128x128@2x.png`、`icon.ico` 和 `icon.icns`,Tauri `bundle.icon` 同时声明这些平台图标。没有引入外部素材或占位图;`apps/desktop-shell/scripts/check-config.mjs` 会校验 PNG 尺寸、ICO 多尺寸头部、ICNS 容器长度和 bundle 图标列表,避免后续退回单图标或替换为非品牌素材。 +2026-06-21 追加:桌面壳本地开发入口必须让 Tauri `devUrl` 和主站 Vite 实际监听端口保持一致。`apps/desktop-shell/src-tauri/tauri.conf.json` 的 `devUrl` 固定为 `http://127.0.0.1:3000/`,`beforeDevCommand` 必须执行 `npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port`,不能只依赖 `WEB_PORT=3000` 环境变量;Linux 用户端口段分配会改写未显式指定的 Web 端口,裸 `dev:web` 可能漂移到 `10000+`,导致 Tauri 窗口加载 3000 时白屏或连接失败。3000 被占用时必须直接失败并提示释放端口,不能静默漂移。`apps/desktop-shell/scripts/check-config.mjs` 和 `scripts/dev.test.ts` 会反查该口径。 + 2026-06-18 追加:移动壳生产包网络安全元数据显式收紧。Android `usesCleartextTraffic=false`,iOS `NSAppTransportSecurity.NSAllowsArbitraryLoads=false`,并设置 `ITSAppUsesNonExemptEncryption=false` 作为当前未接入自定义加密能力的出口合规声明;开发联调本机 Vite 仍通过显式 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/` 进入 development build,不把任意明文流量开关带进默认包配置。`apps/mobile-shell/scripts/check-config.mjs` 会校验这些网络安全字段。 2026-06-19 追加:移动壳麦克风权限只服务同源 H5 实时玩法。汪汪声浪正式 `published` runtime 必须通过浏览器标准 `getUserMedia({ audio: true })` 采样真实音量;Expo 壳因此显式配置 iOS 麦克风用途文案、Android `RECORD_AUDIO` 权限和 `react-native-webview` 的 `mediaCapturePermissionGrantType="grantIfSameHostElsePrompt"`。该权限不新增 HostBridge method,不开放后台录音、远程语音 SDK、任意文件系统或 AI H5 sandbox 直连宿主能力;只有通过同源主站 WebView 留壳规则的 H5 页面能触发系统授权。`expo-camera` 与 `expo-image-picker` 的麦克风用途文案必须保持同一条真实玩法说明,`android.blockedPermissions` 不得再移除 `RECORD_AUDIO`,最终 public config 只允许 `CAMERA`、`POST_NOTIFICATIONS` 和 `RECORD_AUDIO` 三类显式权限。移动拍照、扫码、相册、音频导入和本地通知仍按各自受控能力边界执行,不把麦克风权限当作通用录音入口。`apps/mobile-shell/scripts/check-config.mjs` 与 `check-expo-config.mjs` 会拒绝权限、文案或 WebView 媒体捕获策略漂移。 diff --git a/scripts/dev-stack-port-utils.mjs b/scripts/dev-stack-port-utils.mjs index bbd8f6e54..c119ea46e 100644 --- a/scripts/dev-stack-port-utils.mjs +++ b/scripts/dev-stack-port-utils.mjs @@ -479,6 +479,7 @@ export async function findAvailablePort({ reservedPorts = new Set(), maxAttempts = null, portRange = null, + strict = false, }) { const range = normalizePortRange(portRange); const startPort = normalizePort(preferredPort, 0); @@ -501,6 +502,18 @@ export async function findAvailablePort({ throw new Error(`端口 ${startPort} 不在允许端口段 ${range.label} 内`); } + if (strict && startPort !== 0) { + if (reservedPorts.has(startPort)) { + throw new Error(`端口 ${host}:${startPort} 已被当前 dev 启动流程占用,无法严格使用该端口`); + } + + if (await isPortAvailable({host, port: startPort})) { + return startPort; + } + + throw new Error(`端口 ${host}:${startPort} 不可用,无法严格使用该端口`); + } + const boundedAttempts = range ? Number.isFinite(maxAttempts) ? Math.min(Math.max(0, maxAttempts), range.end - startPort) @@ -565,6 +578,7 @@ export async function resolveDevStackPorts(config) { preferredPort: portConfig.preferredPort, reservedPorts, portRange: portConfig.portRange, + strict: Boolean(portConfig.strict), }); reservedPorts.add(resolvedPort); result[name] = resolvedPort; diff --git a/scripts/dev-stack-port-utils.test.ts b/scripts/dev-stack-port-utils.test.ts index 5521538d6..353b7a100 100644 --- a/scripts/dev-stack-port-utils.test.ts +++ b/scripts/dev-stack-port-utils.test.ts @@ -58,6 +58,23 @@ describe('dev stack port utils', () => { } }); + it('严格端口模式在端口被占用时直接失败而不是漂移', async () => { + const server = await reservePort(0); + const port = server.address().port; + + try { + await expect( + findAvailablePort({ + host: '127.0.0.1', + preferredPort: port, + strict: true, + }), + ).rejects.toThrow('无法严格使用该端口'); + } finally { + await new Promise((resolve) => server.close(resolve)); + } + }); + it('端口查找不会越过 Linux 用户端口段', async () => { await expect( findAvailablePort({ diff --git a/scripts/dev.mjs b/scripts/dev.mjs index a76c20ee9..8e27887f8 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -132,6 +132,7 @@ function parseArgs(argv, baseEnv) { migrationBootstrapSecretMode: 'auto', watch: false, interactive: true, + strictWebPort: false, }; for (let index = 0; index < args.length; index += 1) { @@ -167,6 +168,9 @@ function parseArgs(argv, baseEnv) { options.webPort = normalizePort(readValue(), options.webPort); explicitOptions.add('webPort'); break; + case '--strict-web-port': + options.strictWebPort = true; + break; case '--admin-web-host': options.adminWebHost = readValue(); explicitOptions.add('adminWebHost'); @@ -956,13 +960,15 @@ class DevRunner { async resolvePorts(command) { const {options} = this; const portConfig = {}; + const portRangeFor = (optionName) => + this.explicitOptions.has(optionName) ? null : this.state.portRange; if (command === 'all' || command === 'spacetime') { if (!options.skipSpacetime && !this.state.spacetimeReused) { portConfig.spacetime = { host: options.spacetimeHost, preferredPort: options.spacetimePort, - portRange: this.state.portRange, + portRange: portRangeFor('spacetimePort'), }; } } @@ -971,7 +977,7 @@ class DevRunner { portConfig.api = { host: options.apiHost, preferredPort: options.apiPort, - portRange: this.state.portRange, + portRange: portRangeFor('apiPort'), }; } @@ -979,7 +985,8 @@ class DevRunner { portConfig.web = { host: options.webHost, preferredPort: options.webPort, - portRange: this.state.portRange, + portRange: portRangeFor('webPort'), + strict: options.strictWebPort, }; } @@ -987,7 +994,7 @@ class DevRunner { portConfig.adminWeb = { host: options.adminWebHost, preferredPort: options.adminWebPort, - portRange: this.state.portRange, + portRange: portRangeFor('adminWebPort'), }; } diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts index 3cd31d33b..c51fdfecf 100644 --- a/scripts/dev.test.ts +++ b/scripts/dev.test.ts @@ -139,6 +139,36 @@ describe('dev scheduler argument routing', () => { } }); + linuxTest('Linux 桌面壳显式指定 web-port 时不被系统级端口段改写', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-')); + try { + const {command, explicitOptions, options} = parseArgs( + ['web', '--web-port', '3000', '--strict-web-port'], + { + USER: 'alice', + LOGNAME: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, + }, + ); + const runner = new DevRunner(options, { + USER: 'alice', + LOGNAME: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, + }, explicitOptions); + + await runner.prepareLinuxPortRange(command); + expect(runner.state.portRange.label).toBe('22000-22099'); + expect(runner.options.webPort).toBe(3000); + expect(runner.options.apiPort).toBe(22001); + expect(runner.options.spacetimePort).toBe(22002); + expect(runner.options.adminWebPort).toBe(22003); + } finally { + rmSync(tempDir, {recursive: true, force: true}); + } + }); + test('Windows 仍沿用原有端口解析,不启用 Linux 端口段登记', async () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); Object.defineProperty(process, 'platform', {