- main.cjs: 从 env 读 AGENTMAIL_GATEWAY_URL / AGENTMAIL_TOKEN(USER_KEY), 通过 additionalArguments 传给 preload;API_BASE = <gateway>/api/v1 - preload.cjs: 从 process.argv 读注入参数,contextBridge 暴露 window.__AGENTMAIL_API_BASE__ / __AGENTMAIL_TOKEN__ → web/src 的 config.ts 自动读取,client.ts/sse.ts 零改动 - 效果:Electron 启动后 api.me() 带 Bearer 返回用户,直接进入主界面 (无登录页),收件箱等功能立即可用 验证(xvfb headless): - 注入链路:渲染进程拿到 apiBase=192.168.2.60:8180 + token ✅ - 端到端:gui-lab key 注入 → 界面呈现 收件/授权/发件/日历/联系 导航 + 空收件箱, 无登录表单(inputCount=0, hasLogin=false)✅
136 lines
4.1 KiB
JavaScript
136 lines
4.1 KiB
JavaScript
/**
|
||
* AgentMail Electron 主进程。
|
||
*
|
||
* 职责:
|
||
* - 创建 BrowserWindow 加载前端(dev=<ELECTRON_START_URL>,prod=../dist/index.html)
|
||
* - 系统托盘(Phase 4 完整实现)
|
||
* - 离线缓存(Phase 4)
|
||
* - 把 Gateway 地址与用户密钥注入渲染进程(Phase 2:让 web/src 以独立客户端身份连任意 Gateway)
|
||
*
|
||
* 设计约束:
|
||
* - 渲染进程复用 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;
|
||
|
||
// Gateway 地址与用户密钥:环境变量注入(桌面客户端用 user_key 认证)
|
||
const GATEWAY_URL = (process.env.AGENTMAIL_GATEWAY_URL || 'http://127.0.0.1:8180').replace(/\/+$/, '');
|
||
const API_BASE = `${GATEWAY_URL}/api/v1`;
|
||
const TOKEN = process.env.AGENTMAIL_TOKEN || process.env.AGENTMAIL_USER_KEY || '';
|
||
|
||
let mainWindow = null;
|
||
let tray = null;
|
||
|
||
/** 创建主窗口 */
|
||
function createMainWindow() {
|
||
// 通过 additionalArguments 把 API 基地址与 token 传给 preload(preload 从 renderer 的 process.argv 末尾读)
|
||
// 传入的是透传 arg,用户页面不能直接改,preload 能读到
|
||
const injectArgs = [`--agentmail-api-base=${API_BASE}`];
|
||
if (TOKEN) injectArgs.push(`--agentmail-token=${TOKEN}`);
|
||
|
||
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.cjs'),
|
||
contextIsolation: true,
|
||
nodeIntegration: false,
|
||
sandbox: true,
|
||
additionalArguments: injectArgs,
|
||
},
|
||
});
|
||
|
||
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';
|
||
}); |