230 lines
5.0 KiB
JavaScript
230 lines
5.0 KiB
JavaScript
const { app, BrowserWindow, shell } = require("electron");
|
|
const { spawn, execSync } = require("child_process");
|
|
const http = require("http");
|
|
const path = require("path");
|
|
|
|
const PORT = Number(process.env.PORT || 23337);
|
|
const PROJECT_ROOT = path.join(__dirname, "..");
|
|
const APP_URL = `http://127.0.0.1:${PORT}/`;
|
|
const isDev = process.env.WRITING_AGENT_ELECTRON_DEV === "1";
|
|
|
|
/** @type {import('child_process').ChildProcess | null} */
|
|
let serverProcess = null;
|
|
/** @type {import('electron').BrowserWindow | null} */
|
|
let mainWindow = null;
|
|
let cleaningUp = false;
|
|
|
|
function getPidsOnPort(port) {
|
|
const pids = new Set();
|
|
|
|
try {
|
|
if (process.platform === "win32") {
|
|
const out = execSync(`netstat -ano | findstr ":${port} " | findstr "LISTENING"`, {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
for (const line of out.split(/\r?\n/)) {
|
|
const pid = line.trim().split(/\s+/).pop();
|
|
if (pid && /^\d+$/.test(pid) && pid !== "0") {
|
|
pids.add(Number(pid));
|
|
}
|
|
}
|
|
} else {
|
|
const out = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
for (const line of out.split(/\r?\n/)) {
|
|
const pid = Number(line.trim());
|
|
if (pid > 0) pids.add(pid);
|
|
}
|
|
}
|
|
} catch {
|
|
/* no listeners */
|
|
}
|
|
|
|
return [...pids];
|
|
}
|
|
|
|
function killPidTree(pid) {
|
|
if (!pid || pid <= 0) return;
|
|
|
|
try {
|
|
if (process.platform === "win32") {
|
|
execSync(`taskkill /pid ${pid} /f /t`, { stdio: "ignore" });
|
|
} else {
|
|
process.kill(pid, "SIGTERM");
|
|
}
|
|
} catch {
|
|
/* already exited */
|
|
}
|
|
}
|
|
|
|
function killPortListeners(port) {
|
|
for (const pid of getPidsOnPort(port)) {
|
|
killPidTree(pid);
|
|
}
|
|
}
|
|
|
|
function waitForServer(maxAttempts = 60) {
|
|
return new Promise((resolve, reject) => {
|
|
let attempts = 0;
|
|
|
|
const tryOnce = () => {
|
|
const req = http.get(`${APP_URL}api/health`, (res) => {
|
|
res.resume();
|
|
if (res.statusCode === 200) {
|
|
resolve();
|
|
} else {
|
|
scheduleRetry();
|
|
}
|
|
});
|
|
req.on("error", scheduleRetry);
|
|
req.setTimeout(2000, () => {
|
|
req.destroy();
|
|
scheduleRetry();
|
|
});
|
|
};
|
|
|
|
const scheduleRetry = () => {
|
|
attempts += 1;
|
|
if (attempts >= maxAttempts) {
|
|
reject(new Error(`后端未在 ${PORT} 端口就绪`));
|
|
return;
|
|
}
|
|
setTimeout(tryOnce, 500);
|
|
};
|
|
|
|
tryOnce();
|
|
});
|
|
}
|
|
|
|
function startServer() {
|
|
const isWin = process.platform === "win32";
|
|
serverProcess = spawn(isWin ? "npm.cmd" : "npm", ["run", "web"], {
|
|
cwd: PROJECT_ROOT,
|
|
env: { ...process.env, PORT: String(PORT) },
|
|
stdio: "inherit",
|
|
shell: isWin,
|
|
});
|
|
|
|
serverProcess.on("error", (err) => {
|
|
console.error("[electron] 启动后端失败:", err);
|
|
});
|
|
}
|
|
|
|
function killServer() {
|
|
if (cleaningUp) return;
|
|
cleaningUp = true;
|
|
|
|
const pid = serverProcess?.pid;
|
|
serverProcess = null;
|
|
|
|
if (pid) {
|
|
killPidTree(pid);
|
|
}
|
|
|
|
killPortListeners(PORT);
|
|
}
|
|
|
|
function registerCleanupHandlers() {
|
|
const cleanup = () => {
|
|
killServer();
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
};
|
|
|
|
process.on("SIGINT", cleanup);
|
|
process.on("SIGTERM", cleanup);
|
|
}
|
|
|
|
function registerDevToolsShortcuts(win) {
|
|
win.webContents.on("before-input-event", (_event, input) => {
|
|
const f12 = input.key === "F12";
|
|
const ctrlShiftI =
|
|
input.control && input.shift && input.key.toLowerCase() === "i";
|
|
if (f12 || ctrlShiftI) {
|
|
win.webContents.toggleDevTools();
|
|
}
|
|
});
|
|
}
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 860,
|
|
minWidth: 800,
|
|
minHeight: 600,
|
|
title: "Writing Agent",
|
|
show: false,
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
});
|
|
|
|
mainWindow.once("ready-to-show", () => {
|
|
mainWindow?.show();
|
|
});
|
|
|
|
mainWindow.loadURL(APP_URL);
|
|
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
shell.openExternal(url);
|
|
}
|
|
return { action: "deny" };
|
|
});
|
|
|
|
registerDevToolsShortcuts(mainWindow);
|
|
|
|
if (isDev) {
|
|
mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
}
|
|
|
|
mainWindow.on("closed", () => {
|
|
mainWindow = null;
|
|
killServer();
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
registerCleanupHandlers();
|
|
startServer();
|
|
try {
|
|
await waitForServer();
|
|
createWindow();
|
|
} catch (err) {
|
|
console.error("[electron]", err instanceof Error ? err.message : err);
|
|
killServer();
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
killServer();
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("before-quit", () => {
|
|
killServer();
|
|
});
|
|
|
|
app.on("will-quit", () => {
|
|
killServer();
|
|
});
|
|
|
|
app.on("activate", async () => {
|
|
if (mainWindow === null && serverProcess) {
|
|
try {
|
|
await waitForServer(10);
|
|
createWindow();
|
|
} catch {
|
|
/* server gone */
|
|
}
|
|
}
|
|
});
|