/** * Tray Manager — 系统托盘管理 * * 职责: * 1. 创建系统托盘图标 * 2. 托盘右键菜单(新建会话、显示窗口、退出) * 3. 托盘图标状态指示(空闲/思考中/执行中) * 4. 点击托盘图标显示/隐藏窗口 * * @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理 */ import { Tray, Menu, BrowserWindow, app, nativeImage, Notification } from 'electron'; import { join } from 'path'; import { existsSync } from 'fs'; import log from 'electron-log'; export type TrayStatus = 'idle' | 'thinking' | 'executing' | 'error'; export class TrayManager { private tray: Tray | null = null; private mainWindow: BrowserWindow | null = null; private currentStatus: TrayStatus = 'idle'; private static isQuitting = false; constructor(private resourcesPath: string) {} /** * 初始化系统托盘 */ initialize(mainWindow: BrowserWindow): void { this.mainWindow = mainWindow; // 创建托盘图标 const iconPath = this.getTrayIconPath(); if (!iconPath) { log.warn('Tray icon not found, skipping tray initialization'); return; } this.tray = new Tray(iconPath); this.tray.setToolTip('MetonaAI Desktop'); // 构建右键菜单 this.updateContextMenu(); // 点击托盘图标:显示/隐藏窗口 this.tray.on('click', () => { if (this.mainWindow) { if (this.mainWindow.isVisible()) { this.mainWindow.hide(); } else { this.mainWindow.show(); this.mainWindow.focus(); } } }); // 窗口关闭时隐藏到托盘(不退出) mainWindow.on('close', (event) => { if (!TrayManager.isQuitting) { event.preventDefault(); mainWindow.hide(); } }); log.info('System tray initialized'); } /** * 更新托盘状态 */ setStatus(status: TrayStatus): void { if (!this.tray) return; this.currentStatus = status; const statusLabels: Record = { idle: 'MetonaAI — 空闲', thinking: 'MetonaAI — 思考中...', executing: 'MetonaAI — 执行中...', error: 'MetonaAI — 错误', }; this.tray.setToolTip(statusLabels[status]); this.updateContextMenu(); // 更新图标(如果有多状态图标) this.updateTrayIcon(status); } /** * 发送系统通知 */ sendNotification(title: string, body: string, onClick?: () => void): void { if (!Notification.isSupported()) return; const notification = new Notification({ title, body, icon: this.getTrayIconPath() ?? undefined, }); if (onClick) { notification.on('click', onClick); } notification.show(); } /** * 销毁托盘 */ destroy(): void { if (this.tray) { this.tray.destroy(); this.tray = null; } } // ===== 私有方法 ===== /** * 获取托盘图标路径 */ private getTrayIconPath(): string | null { // 优先使用 ico(Windows),其次 png(macOS/Linux) const candidates = [ join(this.resourcesPath, 'logo.ico'), join(this.resourcesPath, 'logo.png'), join(this.resourcesPath, 'icon.ico'), join(this.resourcesPath, 'icon.png'), ]; for (const candidate of candidates) { if (existsSync(candidate)) return candidate; } return null; } /** * 更新托盘图标(根据状态) */ private updateTrayIcon(status: TrayStatus): void { if (!this.tray) return; // 基础图标 const iconPath = this.getTrayIconPath(); if (!iconPath) return; // 对于 thinking/executing 状态,可以使用 overlay 图标 // 当前实现使用基础图标,后续可扩展 const image = nativeImage.createFromPath(iconPath); this.tray.setImage(image.resize({ width: 16, height: 16 })); } /** * 更新右键菜单 */ private updateContextMenu(): void { if (!this.tray) return; const statusIcons: Record = { idle: '⚪', thinking: '🔵', executing: '🟢', error: '🔴', }; const contextMenu = Menu.buildFromTemplate([ { label: `MetonaAI Desktop`, enabled: false, }, { label: `状态: ${statusIcons[this.currentStatus]} ${this.currentStatus}`, enabled: false, }, { type: 'separator' }, { label: '显示窗口', click: () => { if (this.mainWindow) { this.mainWindow.show(); this.mainWindow.focus(); } }, }, { label: '新建会话', click: () => { if (this.mainWindow) { this.mainWindow.show(); this.mainWindow.focus(); this.mainWindow.webContents.send('tray:newSession'); } }, }, { type: 'separator' }, { label: '退出', click: () => { TrayManager.isQuitting = true; app.quit(); }, }, ]); this.tray.setContextMenu(contextMenu); } }