fix(dev-runtime): 统一开发端口并优化页面初始化

- 新增 dev 启动脚本,自动分配前后端端口并等待 Nuxt 就绪后再启动 Electron
- 开发模式忽略持久化 API 覆盖,统一 Nuxt 与桌面端端口配置
- API 健康检查改为挂载后执行,聊天页预取改为 lazy,并隐藏搜索区账号切换
This commit is contained in:
2977094657
2026-03-15 19:35:36 +08:00
parent d2db3b1bab
commit b8d0912907
7 changed files with 218 additions and 13 deletions
+179
View File
@@ -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);
});