pi-ele Phase 1: Electron 骨架(主进程+托盘+preload,可复用 web/src)
- 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 无报错
This commit is contained in:
2
web/.gitignore
vendored
Normal file
2
web/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Electron 构建产物
|
||||||
|
/release/
|
||||||
124
web/electron/main.cjs
Normal file
124
web/electron/main.cjs
Normal file
@ -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';
|
||||||
|
});
|
||||||
25
web/electron/preload.cjs
Normal file
25
web/electron/preload.cjs
Normal file
@ -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,
|
||||||
|
});
|
||||||
@ -3,9 +3,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "electron/main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:electron": "ELECTRON_START_URL=http://localhost:5173 electron electron/main.cjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:win": "vite build && electron-builder --win",
|
||||||
|
"build:linux": "vite build && electron-builder --linux",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "node test/markdown-xss.test.mjs && node test/narrow-layout.test.mjs && node test/theme.test.mjs && vitest run",
|
"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",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"electron": "^44.2.0",
|
||||||
|
"electron-builder": "^26.15.3",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^3.4.6",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.4",
|
||||||
"vitest": "3.2.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user