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:
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Window Manager — 多窗口管理
|
||||
*
|
||||
* 职责:
|
||||
* 1. 创建和管理多个窗口(每个工作空间可独立开窗口)
|
||||
* 2. 窗口状态持久化(位置、大小)
|
||||
* 3. 全局快捷键注册(Cmd+Shift+M 切换到 Metona)
|
||||
* 4. 关闭到托盘行为
|
||||
*
|
||||
* @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理
|
||||
*/
|
||||
|
||||
import { BrowserWindow, globalShortcut, app, shell } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { is } from '@electron-toolkit/utils';
|
||||
import log from 'electron-log';
|
||||
|
||||
export interface WindowState {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isMaximized?: boolean;
|
||||
}
|
||||
|
||||
export class WindowManager {
|
||||
private windows = new Map<string, BrowserWindow>();
|
||||
private activeWindowId: string | null = null;
|
||||
|
||||
/**
|
||||
* 创建新窗口
|
||||
*/
|
||||
createWindow(options: {
|
||||
id?: string;
|
||||
workspacePath?: string;
|
||||
title?: string;
|
||||
state?: WindowState;
|
||||
}): BrowserWindow {
|
||||
const id = options.id ?? `window_${Date.now()}`;
|
||||
const state = options.state ?? { width: 1440, height: 900 };
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
icon: this.getIconPath(),
|
||||
show: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
title: options.title ?? 'MetonaAI Desktop',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/preload.mjs'),
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 加载页面
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
win.loadURL(process.env['ELECTRON_RENDERER_URL']);
|
||||
} else {
|
||||
win.loadFile(join(__dirname, '../../dist/index.html'));
|
||||
}
|
||||
|
||||
// 窗口事件
|
||||
win.on('ready-to-show', () => {
|
||||
if (state.isMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
win.show();
|
||||
});
|
||||
|
||||
win.on('closed', () => {
|
||||
this.windows.delete(id);
|
||||
if (this.activeWindowId === id) {
|
||||
this.activeWindowId = this.windows.size > 0 ? this.windows.keys().next().value ?? null : null;
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
this.windows.set(id, win);
|
||||
this.activeWindowId = id;
|
||||
|
||||
log.info(`Window created: ${id} (${options.title ?? 'MetonaAI Desktop'})`);
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗口
|
||||
*/
|
||||
getWindow(id: string): BrowserWindow | undefined {
|
||||
return this.windows.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活动窗口
|
||||
*/
|
||||
getActiveWindow(): BrowserWindow | null {
|
||||
if (this.activeWindowId) {
|
||||
return this.windows.get(this.activeWindowId) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有窗口
|
||||
*/
|
||||
getAllWindows(): BrowserWindow[] {
|
||||
return Array.from(this.windows.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚焦到窗口(从任意应用切换)
|
||||
*/
|
||||
focusWindow(): void {
|
||||
const win = this.getActiveWindow();
|
||||
if (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore();
|
||||
}
|
||||
win.show();
|
||||
win.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局快捷键
|
||||
*/
|
||||
registerGlobalShortcuts(): void {
|
||||
// Cmd/Ctrl+Shift+M — 从任意应用切换到 Metona
|
||||
const registered = globalShortcut.register('CommandOrControl+Shift+M', () => {
|
||||
this.focusWindow();
|
||||
log.info('Global shortcut: Switch to Metona');
|
||||
});
|
||||
|
||||
if (!registered) {
|
||||
log.warn('Failed to register global shortcut: Cmd+Shift+M');
|
||||
} else {
|
||||
log.info('Global shortcut registered: Cmd+Shift+M');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销全局快捷键
|
||||
*/
|
||||
unregisterGlobalShortcuts(): void {
|
||||
globalShortcut.unregisterAll();
|
||||
log.info('Global shortcuts unregistered');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗口状态(用于持久化)
|
||||
*/
|
||||
getWindowState(id: string): WindowState | null {
|
||||
const win = this.windows.get(id);
|
||||
if (!win) return null;
|
||||
|
||||
const bounds = win.getBounds();
|
||||
return {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
isMaximized: win.isMaximized(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有窗口
|
||||
*/
|
||||
closeAll(): void {
|
||||
for (const [id, win] of this.windows) {
|
||||
try {
|
||||
win.destroy();
|
||||
} catch {
|
||||
log.warn(`Failed to close window: ${id}`);
|
||||
}
|
||||
}
|
||||
this.windows.clear();
|
||||
this.activeWindowId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗口数量
|
||||
*/
|
||||
get count(): number {
|
||||
return this.windows.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用图标路径
|
||||
*/
|
||||
private getIconPath(): string | undefined {
|
||||
const candidates = [
|
||||
join(__dirname, '../../assets/logo.ico'),
|
||||
join(__dirname, '../../assets/logo.png'),
|
||||
join(process.resourcesPath || '', 'assets/logo.ico'),
|
||||
join(process.resourcesPath || '', 'assets/logo.png'),
|
||||
];
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user