Initial commit
This commit is contained in:
229
electron/main.cjs
Normal file
229
electron/main.cjs
Normal file
@@ -0,0 +1,229 @@
|
||||
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 */
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user