From dc5b9e440fa9dc812a989d575634b7321b5932e7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 7 Sep 2026 07:46:07 +0800 Subject: [PATCH] =?UTF-8?q?pi-ele=20Phase=201:=20Electron=20=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=EF=BC=88=E4=B8=BB=E8=BF=9B=E7=A8=8B+=E6=89=98?= =?UTF-8?q?=E7=9B=98+preload=EF=BC=8C=E5=8F=AF=E5=A4=8D=E7=94=A8=20web/src?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web/electron/main.cjs: BrowserWindow 加载 dist/index.html(prod)或 vite dev server 托盘(进托盘/退出/单击恢复),关闭按钮隐藏到托盘而非退出(用户要求) - web/electron/preload.cjs: contextBridge 暴露 gatewayUrl/versions/platform - web/package.json: 加 main 指向 main.cjs、dev:electron / build:win / build:linux 脚本、 electron 44.2.0 + electron-builder 26.15.3 依赖、build 配置(NSIS/AppImage/deb) - .gitignore: /release/ 验证(headless + xvfb): - Electron 加载 Gateway WebUI 成功(title=AgentMail, body 渲染) - Tray API 可用,BrowserWindow 正常 - prod 模式真实 main.cjs 无报错 --- web/.gitignore | 2 + web/electron/main.cjs | 124 +++++++++++++++++++++++++++++++++++++++ web/electron/preload.cjs | 25 ++++++++ web/package.json | 36 +++++++++++- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 web/.gitignore create mode 100644 web/electron/main.cjs create mode 100644 web/electron/preload.cjs diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..092fc77 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,2 @@ +# Electron 构建产物 +/release/ diff --git a/web/electron/main.cjs b/web/electron/main.cjs new file mode 100644 index 0000000..64a1021 --- /dev/null +++ b/web/electron/main.cjs @@ -0,0 +1,124 @@ +/** + * AgentMail Electron 主进程。 + * + * 职责: + * - 创建 BrowserWindow 加载前端(dev=http://localhost:5173,prod=../dist/index.html) + * - 系统托盘(Phase 4 完整实现,这里先预留) + * - SSE 长连接由主进程维持(Phase 2),这里先只做窗口生命周期 + * + * 设计约束: + * - 渲染进程复用 web/src 的 React 代码,零改动 + * - 只通过 contextBridge 暴露必要 API,不放开 nodeIntegration + */ + +const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain } = require('electron'); +const path = require('node:path'); + +// 开发模式:ELECTRON_START_URL 环境变量指向 vite dev server +const DEV_URL = process.env.ELECTRON_START_URL || 'http://localhost:5173'; +const isDev = !!process.env.ELECTRON_START_URL || !app.isPackaged; + +let mainWindow = null; +let tray = null; + +/** 创建主窗口 */ +function createMainWindow() { + mainWindow = new BrowserWindow({ + width: 1280, + height: 800, + minWidth: 375, + minHeight: 640, + title: 'AgentMail', + icon: path.join(__dirname, '..', 'src', 'icons', 'tray-icon.png'), + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + if (isDev) { + mainWindow.loadURL(DEV_URL); + } else { + mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); + } + + // 点关闭按钮时隐藏到托盘而不是退出(Phase 4 行为,但用户要求进托盘,直接做) + mainWindow.on('close', (e) => { + if (!app.isQuitting) { + e.preventDefault(); + mainWindow.hide(); + } + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +/** 系统托盘(Phase 4,这里是骨架:进托盘 + 重新显示) */ +function createTray() { + // 用 16x16 像素的图标;缺文件时降级为 nativeImage.createEmpty() + let iconPath = path.join(__dirname, '..', 'src', 'icons', 'tray-icon.png'); + let icon = nativeImage.createFromPath(iconPath); + if (icon.isEmpty()) { + icon = nativeImage.createEmpty(); + } + + tray = new Tray(icon); + tray.setToolTip('AgentMail'); + + const contextMenu = Menu.buildFromTemplate([ + { + label: '打开 AgentMail', + click: () => { + if (!mainWindow) createMainWindow(); + mainWindow.show(); + mainWindow.focus(); + }, + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + app.isQuitting = true; + app.quit(); + }, + }, + ]); + tray.setContextMenu(contextMenu); + + // 单击托盘图标显示窗口(Windows 常用行为) + tray.on('click', () => { + if (!mainWindow) createMainWindow(); + mainWindow.show(); + mainWindow.focus(); + }); +} + +// Electron 生命周期 +app.whenReady().then(() => { + createMainWindow(); + createTray(); + + app.on('activate', () => { + // macOS 点击 Dock 图标重新建窗口 + if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); + else if (mainWindow) mainWindow.show(); + }); +}); + +app.on('window-all-closed', () => { + // 托盘应用:关窗口不退出(macOS 惯例 + 用户要求的"进托盘"行为) + // 除非显式退出 +}); + +app.on('before-quit', () => { + app.isQuitting = true; +}); + +// IPC:渲染进程问主进程要 Gateway 地址(Phase 2 用) +ipcMain.handle('get-gateway-url', () => { + return process.env.AGENTMAIL_GATEWAY_URL || 'http://127.0.0.1:8180'; +}); \ No newline at end of file diff --git a/web/electron/preload.cjs b/web/electron/preload.cjs new file mode 100644 index 0000000..d245a20 --- /dev/null +++ b/web/electron/preload.cjs @@ -0,0 +1,25 @@ +/** + * AgentMail Electron preload —— 通过 contextBridge 暴露安全 API 给渲染进程。 + * + * 渲染进程是 web/src 的 React,跑在 sandbox: true 下(无 nodeIntegration)。 + * 需要桌面能力时(文件选择、通知、托盘角标、SSE 转发)走这里暴露的桥。 + * + * Phase 1 只暴露最小集;后续 Phase 逐步加。 + */ + +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('agentmail', { + /** Gateway 基础地址(主进程 env 或默认 127.0.0.1:8180) */ + gatewayUrl: () => ipcRenderer.invoke('get-gateway-url'), + + /** 版本信息(渲染进程可显示在 About 页) */ + versions: { + electron: process.versions.electron, + chrome: process.versions.chrome, + node: process.versions.node, + }, + + /** 平台标识:win32 / linux / darwin */ + platform: process.platform, +}); \ No newline at end of file diff --git a/web/package.json b/web/package.json index a6f2f7a..95c8a61 100644 --- a/web/package.json +++ b/web/package.json @@ -3,9 +3,13 @@ "version": "0.1.0", "private": true, "type": "module", + "main": "electron/main.cjs", "scripts": { "dev": "vite", + "dev:electron": "ELECTRON_START_URL=http://localhost:5173 electron electron/main.cjs", "build": "vite build", + "build:win": "vite build && electron-builder --win", + "build:linux": "vite build && electron-builder --linux", "preview": "vite preview", "typecheck": "tsc --noEmit", "test": "node test/markdown-xss.test.mjs && node test/narrow-layout.test.mjs && node test/theme.test.mjs && vitest run", @@ -31,11 +35,41 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", + "electron": "^44.2.0", + "electron-builder": "^26.15.3", "jsdom": "26.1.0", "postcss": "^8.4.39", "tailwindcss": "^3.4.6", "typescript": "^5.5.3", "vite": "^5.3.4", "vitest": "3.2.4" + }, + "build": { + "appId": "xyz.jianfgit.agentmail", + "productName": "AgentMail", + "files": [ + "dist/**/*", + "electron/**/*" + ], + "directories": { + "buildResources": "src/icons", + "output": "release" + }, + "win": { + "target": [ + { "target": "nsis", "arch": ["x64"] } + ] + }, + "linux": { + "target": [ + { "target": "AppImage", "arch": ["x64"] }, + { "target": "deb", "arch": ["x64"] } + ], + "category": "Network" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } } -} +} \ No newline at end of file