chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,136 @@
/**
* AgentMail Electron 主进程。
*
* 职责:
* - 创建 BrowserWindow 加载前端dev=<ELECTRON_START_URL>prod=../dist/index.html
* - 系统托盘Phase 4 完整实现)
* - 离线缓存Phase 4
* - 把 Gateway 地址与用户密钥注入渲染进程Phase 2让 client/electron/src 以独立客户端身份连任意 Gateway
*
* 设计约束:
* - 渲染进程复用 client/electron/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 传给 preloadpreload 从 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';
});

View File

@ -0,0 +1,37 @@
/**
* AgentMail Electron preload —— 通过 contextBridge 暴露安全 API 给渲染进程。
*
* 渲染进程是 client/electron/src 的 React跑在 sandbox: true 下(无 nodeIntegration
* 需要桌面能力时文件选择、通知、托盘角标、SSE 转发)走这里暴露的桥。
*
* Phase 1 只暴露最小集;后续 Phase 逐步加。
*/
const { contextBridge, ipcRenderer } = require('electron');
// 注入 API 基地址与 token渲染进程的 config.ts 在读 window.__AGENTMAIL_API_BASE__
// 与 __AGENTMAIL_TOKEN__ 时拿到它们,于是现有 client.ts/sse.ts **零改动**复用。
// 这两个值由主进程在创建窗口时通过附加参数传进来(见 main.cjs 的 additionalArguments
function getInjected(name) {
const prefix = `--agentmail-${name}=`;
const arg = process.argv.find(a => a.startsWith(prefix));
return arg ? arg.slice(prefix.length) : undefined;
}
contextBridge.exposeInMainWorld('__AGENTMAIL_API_BASE__', getInjected('api-base') || 'http://127.0.0.1:8180/api/v1');
contextBridge.exposeInMainWorld('__AGENTMAIL_TOKEN__', getInjected('token') || undefined);
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,
});