import { app, BrowserWindow, ipcMain, shell } from "electron"; import { join } from "path"; import { existsSync, mkdirSync } from "fs"; import { spawn } from "child_process"; import WebSocket from "ws"; import __cjs_mod__ from "node:module"; const __filename = import.meta.filename; const __dirname = import.meta.dirname; const require2 = __cjs_mod__.createRequire(import.meta.url); const USER_DATA = join(__dirname, "../../.electron-userdata"); app.setPath("userData", USER_DATA); const PATHS = { userData: USER_DATA, data: join(USER_DATA, "data"), // SQLite 数据库 workspaces: join(USER_DATA, "workspaces"), // worktree 根目录 backups: join(USER_DATA, "backups"), // 数据库备份 logs: join(USER_DATA, "logs") // 日志 }; const AGENT_CORE_PORT = 8787; const AGENT_CORE_PATH = join(__dirname, "../../../agent-core"); const AGENT_CORE_ENTRY = "src/index.ts"; let agentCoreProcess = null; let agentCoreWs = null; let mainWindow = null; function initUserDataDirs() { for (const dir of Object.values(PATHS)) { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } console.log("[main] userData:", USER_DATA); } function startAgentCore() { return new Promise((resolve, reject) => { const isDev = !!process.env.ELECTRON_RENDERER_URL; const cmd = isDev ? "npx" : "node"; const args = isDev ? ["tsx", AGENT_CORE_ENTRY] : ["dist/index.js"]; agentCoreProcess = spawn(cmd, args, { cwd: AGENT_CORE_PATH, env: { ...process.env, AGENT_CORE_PORT: String(AGENT_CORE_PORT), AGENT_CORE_DB_DIR: PATHS.data }, stdio: ["pipe", "pipe", "pipe"], shell: process.platform === "win32" // Windows 上需要 shell 才能找到 npx }); agentCoreProcess.stdout?.on("data", (data) => { console.log("[agent-core]", data.toString().trim()); }); agentCoreProcess.stderr?.on("data", (data) => { console.error("[agent-core]", data.toString().trim()); }); agentCoreProcess.on("exit", (code) => { console.log(`[agent-core] 进程退出,code=${code}`); agentCoreProcess = null; }); let retries = 0; const tryConnect = () => { agentCoreWs = new WebSocket(`ws://localhost:${AGENT_CORE_PORT}`); agentCoreWs.on("open", () => { console.log("[main] 已连接 agent-core WebSocket"); resolve(); }); agentCoreWs.on("error", () => { retries++; if (retries > 20) { reject(new Error("无法连接 agent-core WebSocket")); return; } setTimeout(tryConnect, 500); }); }; setTimeout(tryConnect, 1e3); }); } function callAgentCore(type, data) { return new Promise((resolve, reject) => { if (!agentCoreWs || agentCoreWs.readyState !== WebSocket.OPEN) { reject(new Error("agent-core 未连接")); return; } const id = Date.now().toString(); const onMessage = (raw) => { const msg = JSON.parse(raw.toString()); if (msg.id === id) { agentCoreWs?.off("message", onMessage); if (msg.error) reject(new Error(msg.error)); else resolve(msg.data); } }; agentCoreWs.on("message", onMessage); agentCoreWs.send(JSON.stringify({ type, data, id })); setTimeout(() => { agentCoreWs?.off("message", onMessage); reject(new Error("agent-core 响应超时")); }, 1e4); }); } function createWindow() { mainWindow = new BrowserWindow({ width: 1440, height: 900, minWidth: 1024, minHeight: 640, titleBarStyle: "hiddenInset", backgroundColor: "#1f1f1f", webPreferences: { preload: join(__dirname, "../preload/index.mjs"), contextIsolation: true, nodeIntegration: false, sandbox: false } }); mainWindow.on("ready-to-show", () => { mainWindow?.show(); }); mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url); return { action: "deny" }; }); const rendererUrl = process.env.ELECTRON_RENDERER_URL; if (rendererUrl) { mainWindow.loadURL(rendererUrl); } else { mainWindow.loadFile(join(__dirname, "../renderer/index.html")); } } app.whenReady().then(async () => { initUserDataDirs(); try { await startAgentCore(); } catch (err) { console.error("[main] agent-core 启动失败:", err); } createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (agentCoreWs) agentCoreWs.close(); if (agentCoreProcess) agentCoreProcess.kill(); if (process.platform !== "darwin") app.quit(); }); ipcMain.handle("ping", () => "pong"); ipcMain.handle("get-paths", () => PATHS); ipcMain.handle("get-db-path", () => join(PATHS.data, "agent-console.db")); ipcMain.handle("agent:call", async (_event, { type, data }) => { return callAgentCore(type, data); }); ipcMain.handle("agent:status", () => ({ connected: agentCoreWs?.readyState === WebSocket.OPEN, port: AGENT_CORE_PORT }));