feat: MetonaAI Desktop 初始项目

- Electron + React + TypeScript 架构
- 三栏布局: Sidebar | ChatPanel | DetailPanel
- 9 个内置工具 (文件系统/网络/记忆/命令)
- SQLite 持久化 (better-sqlite3)
- MUI 暗色/亮色主题系统
- Agent Loop ReAct 状态机引擎
- DeepSeek / Agnes AI / Ollama Provider 适配器
- MCP 协议集成
- 系统托盘 + 全局快捷键
- Tailwind CSS v4 + Tailwind Merge
- 修复: Sidebar 缺失 TextField 导入导致黑屏
This commit is contained in:
thzxx
2026-06-27 21:33:27 +08:00
commit 1d185db6b3
109 changed files with 39155 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
/**
* 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<TrayStatus, string> = {
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 {
// 优先使用 icoWindows),其次 pngmacOS/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<TrayStatus, string> = {
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);
}
}