修复桌面壳开发端口对齐
桌面壳 dev 命令显式锁定 Vite 3000 端口 dev 端口解析支持严格端口模式并避免显式端口被 Linux 端口段改写 补充端口错配回归测试和宿主壳文档记忆
This commit is contained in:
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
## 自动试玩退出不要回到生成页
|
||||
|
||||
- 现象:拼图草稿生成完成后自动进入试玩,用户从试玩退出或使用系统返回时落回生成进度页,页面还暴露“重新生成”按钮。
|
||||
|
||||
@@ -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 媒体捕获策略漂移。
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
+11
-4
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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', {
|
||||
|
||||
Reference in New Issue
Block a user