From b8d0912907be6219f1973ac100d5de350bfc98a9 Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Sun, 15 Mar 2026 19:33:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(dev-runtime):=20=E7=BB=9F=E4=B8=80=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=AB=AF=E5=8F=A3=E5=B9=B6=E4=BC=98=E5=8C=96=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 dev 启动脚本,自动分配前后端端口并等待 Nuxt 就绪后再启动 Electron - 开发模式忽略持久化 API 覆盖,统一 Nuxt 与桌面端端口配置 - API 健康检查改为挂载后执行,聊天页预取改为 lazy,并隐藏搜索区账号切换 --- desktop/package.json | 2 +- desktop/scripts/dev.cjs | 179 +++++++++++++++++++++++++++ desktop/src/main.cjs | 5 + frontend/composables/useApiBase.js | 7 +- frontend/nuxt.config.ts | 11 +- frontend/pages/chat/[[username]].vue | 6 +- frontend/plugins/api-check.client.js | 21 ++-- 7 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 desktop/scripts/dev.cjs diff --git a/desktop/package.json b/desktop/package.json index a88b255..2709dce 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -4,7 +4,7 @@ "version": "1.3.0", "main": "src/main.cjs", "scripts": { - "dev": "concurrently -k -s first \"cd ..\\\\frontend && npm run dev\" \"cross-env ELECTRON_START_URL=http://localhost:3000 electron .\"", + "dev": "node scripts/dev.cjs", "dev:static": "pushd ..\\\\frontend && npm run generate && popd && cross-env ELECTRON_START_URL=http://127.0.0.1:10392 electron .", "build:ui": "pushd ..\\\\frontend && npm run generate && popd && node scripts\\\\copy-ui.cjs", "build:backend": "uv sync --extra build && node scripts/build-backend.cjs", diff --git a/desktop/scripts/dev.cjs b/desktop/scripts/dev.cjs new file mode 100644 index 0000000..1e2f1ab --- /dev/null +++ b/desktop/scripts/dev.cjs @@ -0,0 +1,179 @@ +const http = require("http"); +const net = require("net"); +const path = require("path"); +const { spawn, spawnSync } = require("child_process"); + +const repoRoot = path.resolve(__dirname, "..", ".."); +const frontendDir = path.join(repoRoot, "frontend"); +const desktopDir = path.join(repoRoot, "desktop"); + +function parsePort(value) { + const n = Number.parseInt(String(value || "").trim(), 10); + return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null; +} + +function log(message) { + process.stdout.write(`[dev] ${message}\n`); +} + +function prefixPipe(stream, prefix) { + if (!stream) return; + let pending = ""; + stream.setEncoding("utf8"); + stream.on("data", (chunk) => { + pending += chunk; + const lines = pending.split(/\r?\n/); + pending = lines.pop() || ""; + for (const line of lines) { + process.stdout.write(`${prefix} ${line}\n`); + } + }); + stream.on("end", () => { + const tail = pending.trim(); + if (tail) process.stdout.write(`${prefix} ${tail}\n`); + }); +} + +function isPortAvailable(port, host) { + return new Promise((resolve) => { + const server = net.createServer(); + const done = (ok) => { + try { + server.close(); + } catch {} + resolve(ok); + }; + server.once("error", () => done(false)); + server.once("listening", () => done(true)); + server.listen(port, host); + }); +} + +async function choosePort({ label, envName, preferredPort, host, searchLimit = 20 }) { + if (preferredPort != null) { + const ok = await isPortAvailable(preferredPort, host); + if (!ok) throw new Error(`${label}端口 ${preferredPort} 已被占用,请修改环境变量 ${envName}`); + return preferredPort; + } + + const startPort = envName === "NUXT_PORT" ? 3000 : 10392; + for (let port = startPort; port <= startPort + searchLimit; port += 1) { + if (await isPortAvailable(port, host)) return port; + } + throw new Error(`未找到可用的${label}端口(起始 ${startPort})`); +} + +function httpReady(url) { + return new Promise((resolve) => { + const req = http.get(url, (res) => { + res.resume(); + resolve(true); + }); + req.on("error", () => resolve(false)); + req.setTimeout(1000, () => { + req.destroy(); + resolve(false); + }); + }); +} + +async function waitForUrl(url, child, timeoutMs) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + if (child.exitCode != null) { + throw new Error(`前端进程提前退出,exitCode=${child.exitCode}`); + } + if (await httpReady(url)) return; + await new Promise((resolve) => setTimeout(resolve, 300)); + } + throw new Error(`等待前端启动超时:${url}`); +} + +function killChild(child) { + if (!child || child.killed || child.exitCode != null) return; + if (process.platform === "win32") { + spawnSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" }); + return; + } + try { + child.kill("SIGTERM"); + } catch {} +} + +function spawnLogged(command, args, options, prefix) { + const child = spawn(command, args, { + ...options, + shell: process.platform === "win32", + stdio: ["inherit", "pipe", "pipe"], + }); + prefixPipe(child.stdout, `${prefix}`); + prefixPipe(child.stderr, `${prefix}`); + return child; +} + +async function main() { + const frontendHost = String(process.env.NUXT_HOST || "127.0.0.1").trim() || "127.0.0.1"; + const requestedFrontendPort = parsePort(process.env.NUXT_PORT); + const requestedBackendPort = parsePort(process.env.WECHAT_TOOL_PORT); + const frontendPort = await choosePort({ + label: "前端", + envName: "NUXT_PORT", + preferredPort: requestedFrontendPort, + host: frontendHost, + }); + const backendPort = await choosePort({ + label: "后端", + envName: "WECHAT_TOOL_PORT", + preferredPort: requestedBackendPort, + host: "127.0.0.1", + }); + const startUrl = `http://${frontendHost}:${frontendPort}`; + + log(`frontend=${startUrl}`); + log(`backend=http://127.0.0.1:${backendPort}/api`); + + const sharedEnv = { + ...process.env, + NUXT_HOST: frontendHost, + NUXT_PORT: String(frontendPort), + WECHAT_TOOL_PORT: String(backendPort), + ELECTRON_START_URL: startUrl, + }; + + const npmCommand = "npm"; + const electronCommand = "electron"; + const children = new Set(); + let shuttingDown = false; + + const shutdown = (exitCode) => { + if (shuttingDown) return; + shuttingDown = true; + for (const child of children) killChild(child); + process.exitCode = exitCode; + }; + + process.on("SIGINT", () => shutdown(130)); + process.on("SIGTERM", () => shutdown(143)); + + const frontend = spawnLogged(npmCommand, ["run", "dev"], { cwd: frontendDir, env: sharedEnv }, "[frontend]"); + children.add(frontend); + frontend.once("exit", (code, signal) => { + log(`frontend exited code=${code} signal=${signal}`); + shutdown(code == null ? 1 : code); + }); + + await waitForUrl(startUrl, frontend, 60_000); + log("frontend is ready, starting Electron"); + + const electron = spawnLogged(electronCommand, ["."], { cwd: desktopDir, env: sharedEnv }, "[electron]"); + children.add(electron); + electron.once("exit", (code, signal) => { + log(`electron exited code=${code} signal=${signal}`); + shutdown(code == null ? 0 : code); + }); +} + +main().catch((err) => { + process.stderr.write(`[dev] ${err?.stack || err}\n`); + process.exit(1); +}); diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index 3a0cb16..e3f4072 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -83,6 +83,11 @@ function getBackendAccessHost() { } function getBackendPort() { + const envPort = parsePort(process.env.WECHAT_TOOL_PORT); + if (envPort != null) return envPort; + // In dev we intentionally ignore persisted packaged-app settings so the + // launcher can keep Electron, Nuxt devProxy and the backend child aligned. + if (!app.isPackaged) return DEFAULT_BACKEND_PORT; const settingsPort = parsePort(loadDesktopSettings()?.backendPort); return settingsPort ?? DEFAULT_BACKEND_PORT; } diff --git a/frontend/composables/useApiBase.js b/frontend/composables/useApiBase.js index a76e869..f18dbc0 100644 --- a/frontend/composables/useApiBase.js +++ b/frontend/composables/useApiBase.js @@ -4,6 +4,11 @@ import { normalizeApiBase, readApiBaseOverride } from '~/lib/api-settings' // the Nuxt composable context (e.g. inside async callbacks / onMounted chains). let _clientCache = '' +const shouldIgnoreStoredOverride = () => { + if (!process.client || !import.meta.dev) return false + return typeof window !== 'undefined' && !!window.wechatDesktop?.__brand +} + export const useApiBase = () => { if (process.client && _clientCache) return _clientCache @@ -23,7 +28,7 @@ export const useApiBase = () => { // 1) Local UI setting (web + desktop) // 2) NUXT_PUBLIC_API_BASE env/runtime config // 3) `/api` - const override = process.client ? readApiBaseOverride() : '' + const override = process.client && !shouldIgnoreStoredOverride() ? readApiBaseOverride() : '' const runtime = String(config?.public?.apiBase || '').trim() const result = normalizeApiBase(override || runtime || '/api') diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 7a03351..7472053 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,10 +1,18 @@ // https://nuxt.com/docs/api/configuration/nuxt-config +const frontendHost = String(process.env.NUXT_HOST || '').trim() +const frontendPort = Number.parseInt(String(process.env.NUXT_PORT || process.env.PORT || '3000').trim(), 10) const backendPort = String(process.env.WECHAT_TOOL_PORT || '10392').trim() || '10392' const devProxyTarget = `http://127.0.0.1:${backendPort}/api` export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: false }, + experimental: { + // This app does not use Nuxt route rules on the client, so disabling + // the app manifest avoids an unnecessary `/_nuxt/builds/meta/dev.json` + // preload request and the related Chrome warning in dev mode. + appManifest: false, + }, runtimeConfig: { public: { @@ -16,7 +24,8 @@ export default defineNuxtConfig({ // 配置前端开发服务器端口 devServer: { - port: 3000 + ...(frontendHost ? { host: frontendHost } : {}), + port: Number.isInteger(frontendPort) && frontendPort >= 1 && frontendPort <= 65535 ? frontendPort : 3000 }, // 配置API代理,解决跨域问题 diff --git a/frontend/pages/chat/[[username]].vue b/frontend/pages/chat/[[username]].vue index 43f6e71..e2b74e7 100644 --- a/frontend/pages/chat/[[username]].vue +++ b/frontend/pages/chat/[[username]].vue @@ -206,6 +206,7 @@