mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
b8d0912907
- 新增 dev 启动脚本,自动分配前后端端口并等待 Nuxt 就绪后再启动 Electron - 开发模式忽略持久化 API 覆盖,统一 Nuxt 与桌面端端口配置 - API 健康检查改为挂载后执行,聊天页预取改为 lazy,并隐藏搜索区账号切换
180 lines
5.2 KiB
JavaScript
180 lines
5.2 KiB
JavaScript
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);
|
||
});
|