/** * MetonaAI Desktop — Electron 主进程入口 * * 启动流程(按架构规范强制顺序): * 1. 初始化日志系统 * 2. 选择/创建工作空间 → 校验必需文件(缺失自动创建) * 3. 连接 SQLite → 执行 schema 迁移 → 加载配置 * 4. 初始化 Provider Adapter * 5. 加载 4 个磁盘文件 → 构建 System Prompt * 6. 注册内置工具 + 连接 MCP Servers * 7. 启动 React UI → Agent 就绪 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — 启动流程 * @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理 */ import { app, shell, Menu } from 'electron'; import { join } from 'path'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { electronApp, optimizer } from '@electron-toolkit/utils'; import log from 'electron-log'; import { DatabaseService } from './services/database.service'; import { SessionService } from './services/session.service'; import { ConfigService } from './services/config.service'; import { WorkspaceService } from './services/workspace.service'; import { AuditService } from './services/audit.service'; import { SessionRecorder } from './services/session-recorder.service'; import { TrayManager } from './services/tray-manager.service'; import { WindowManager } from './services/window-manager.service'; import { MCPManager } from './services/mcp-manager.service'; import { ContextBuilder } from './harness/prompts/context-builder'; import { MemoryManager } from './harness/memory/manager'; import { MemoryConsolidator } from './harness/memory/consolidator'; import { registerAllIPCHandlers } from './ipc/handlers'; import { AgentLoopEngine } from './harness/agent-loop'; import { ToolRegistry } from './harness/tools/registry'; import { DeepSeekAdapter } from './harness/adapters/deepseek.adapter'; import { AgnesAdapter } from './harness/adapters/agnes-ai.adapter'; import { OllamaAdapter } from './harness/adapters/ollama.adapter'; import { ReadFileTool, WriteFileTool, ListDirectoryTool, SearchFilesTool, WebSearchTool, WebFetchTool, MemoryStoreTool, MemorySearchTool, RunCommandTool, WebBrowserTool, cleanupBrowser, DelegateTaskTool, // v0.2.0 新增工具 FileEditorTool, CodeSearchTool, TaskManagerTool, DiffViewerTool, // v0.3.1 新增工具(11 个) GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool, LintCodeTool, RunTestsTool, ProjectInfoTool, HttpRequestTool, TodoWriteTool, ThinkTool, ViewImageTool, // v0.3.2 新增工具(1 个) DeleteFileTool, } from './harness/tools/built-in'; import { AuditLogHook, MemoryTriggerHook, PermissionCheckHook, RateLimitHook } from './harness/hooks'; import { ConfirmationHook } from './harness/hooks/confirmation-hook'; import { PolicyEngine } from './harness/sandbox/permissions'; import { SandboxManager } from './harness/sandbox/sandbox'; import { PromptInjectionDefender } from './harness/security/prompt-injection-defense'; import { OutputValidator } from './harness/verification/output-validator'; import { TaskOrchestrator } from './harness/orchestration/orchestrator'; import { UpdateService } from './services/update.service'; // ===== 步骤 1: 初始化日志系统(SYS 层)===== log.transports.file.level = 'info'; log.transports.console.level = 'debug'; // ===== 工作空间路径独立存储(解决 DB 在 workspace 内的鸡生蛋问题)===== const WORKSPACE_CONFIG_FILE = join(app.getPath('userData'), 'workspace-config.json'); function readWorkspacePathFromFile(): string | null { try { if (existsSync(WORKSPACE_CONFIG_FILE)) { const data = JSON.parse(readFileSync(WORKSPACE_CONFIG_FILE, 'utf-8')); return data.workspacePath ?? null; } } catch { // 忽略读取错误 } return null; } function writeWorkspacePathToFile(workspacePath: string): void { try { writeFileSync(WORKSPACE_CONFIG_FILE, JSON.stringify({ workspacePath }, null, 2), 'utf-8'); } catch (err) { log.error('Failed to write workspace config file:', err); } } export { readWorkspacePathFromFile, writeWorkspacePathToFile }; let databaseService: DatabaseService | null = null; let trayManager: TrayManager | null = null; let windowManager: WindowManager | null = null; async function initialize(): Promise { log.info('MetonaAI Desktop starting...'); electronApp.setAppUserModelId('com.metona.ai-desktop'); // 移除原生菜单栏 Menu.setApplicationMenu(null); app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window); }); // ===== 步骤 2: 工作空间(优先从独立配置文件读取路径)===== const savedWorkspacePath = readWorkspacePathFromFile(); const workspaceService = new WorkspaceService(savedWorkspacePath ?? undefined); const workspaceInfo = workspaceService.initialize(); // 持久化工作空间路径(供下次启动读取) if (savedWorkspacePath !== workspaceInfo.path) { writeWorkspacePathToFile(workspaceInfo.path); } log.info(`Workspace: ${workspaceInfo.path} (missing: ${workspaceInfo.missingFiles.join(', ') || 'none'})`); // ===== 步骤 3: SQLite ===== databaseService = new DatabaseService(workspaceInfo.path); databaseService.initialize(); const db = databaseService.getDB(); const sessionService = new SessionService(() => db); const configService = new ConfigService(() => db); // ===== 日志服务 ===== const auditService = new AuditService(() => db); const sessionRecorder = new SessionRecorder(workspaceInfo.path); // ===== 记忆系统 ===== const memoryManager = new MemoryManager(() => db); memoryManager.initialize(); // ===== 步骤 4: Provider Adapter 工厂 ===== const createAdapter = () => { const provider = configService.get('llm.provider') ?? ''; const model = configService.get('llm.model') ?? ''; const apiKey = configService.get('llm.apiKey') ?? ''; const baseURL = configService.get('llm.baseURL') ?? ''; if (!provider || !baseURL || !model) { log.warn('LLM not configured. Please set provider, baseURL, and model in Settings.'); } if (!apiKey && provider !== 'ollama') { throw new Error(`API key is required for provider "${provider || 'unknown'}". Please set it in Settings.`); } // v0.3.1: 读取 Provider 对应的 contextWindow 配置(Ollama 不使用此字段) const contextWindow = provider !== 'ollama' ? configService.get(`${provider}.contextWindow`) ?? undefined : undefined; const adapterConfig = { provider, baseURL, apiKey, defaultModel: model, contextWindow }; switch (provider) { case 'agnes': return new AgnesAdapter(adapterConfig); case 'ollama': return new OllamaAdapter(adapterConfig); default: return new DeepSeekAdapter(adapterConfig); } }; const adapter = createAdapter(); // ===== 步骤 5: 工作空间文件 + System Prompt ===== const contextBuilder = new ContextBuilder(); // ===== v0.2.0: 安全模块(必须在工具注册之前,便于 RunCommandTool 注入 SandboxManager)===== const policyEngine = new PolicyEngine(); const sandboxManager = new SandboxManager({ allowedPaths: [workspaceInfo.path], networkPolicy: 'allowlist', }); const promptDefender = new PromptInjectionDefender(); const outputValidator = new OutputValidator(); // v0.2.0: ConfirmationHook(提前创建,mainWindow 创建后再注入) // 注入 ConfigService 以支持持久化自动执行设置 const confirmationHook = new ConfirmationHook(null, configService); // ===== 步骤 6: 注册内置工具 ===== const toolRegistry = new ToolRegistry(); toolRegistry.registerBuiltin(new ReadFileTool()); toolRegistry.registerBuiltin(new WriteFileTool()); toolRegistry.registerBuiltin(new ListDirectoryTool()); toolRegistry.registerBuiltin(new SearchFilesTool()); // v0.3.2: 文件删除工具(破坏性操作,HIGH + requireConfirmation) toolRegistry.registerBuiltin(new DeleteFileTool()); // v0.2.0: 新增文件工具 toolRegistry.registerBuiltin(new FileEditorTool()); toolRegistry.registerBuiltin(new CodeSearchTool()); toolRegistry.registerBuiltin(new DiffViewerTool()); // WebFetchTool 先于 WebSearchTool 构造,注入为依赖 const webFetchTool = new WebFetchTool(); toolRegistry.registerBuiltin(webFetchTool); toolRegistry.registerBuiltin(new WebSearchTool(configService, webFetchTool)); toolRegistry.registerBuiltin(new MemoryStoreTool(memoryManager)); toolRegistry.registerBuiltin(new MemorySearchTool(memoryManager)); // v0.2.0: RunCommandTool 注入 SandboxManager const runCommandTool = new RunCommandTool(); runCommandTool.setSandboxManager(sandboxManager); toolRegistry.registerBuiltin(runCommandTool); // v0.2.0: 任务管理工具 toolRegistry.registerBuiltin(new TaskManagerTool(() => db)); // 注册 Web Browser 统一浏览器工具 toolRegistry.registerBuiltin(new WebBrowserTool()); // v0.3.1: Git 工具集(4 个) toolRegistry.registerBuiltin(new GitStatusTool()); toolRegistry.registerBuiltin(new GitDiffTool()); toolRegistry.registerBuiltin(new GitLogTool()); toolRegistry.registerBuiltin(new GitCommitTool()); // v0.3.1: 开发工具集(3 个) toolRegistry.registerBuiltin(new LintCodeTool()); toolRegistry.registerBuiltin(new RunTestsTool()); toolRegistry.registerBuiltin(new ProjectInfoTool()); // v0.3.1: 独立工具(4 个) toolRegistry.registerBuiltin(new HttpRequestTool()); toolRegistry.registerBuiltin(new TodoWriteTool()); toolRegistry.registerBuiltin(new ThinkTool()); toolRegistry.registerBuiltin(new ViewImageTool()); // v0.3.1 修复 WARN-5: DelegateTaskTool 注册后再输出总数(此时才是完整的 26 个) // log.info 移至 DelegateTaskTool 注册后 // ===== MCP Manager ===== const mcpManager = new MCPManager(() => db, toolRegistry); mcpManager.initialize().catch((err) => { log.warn('MCP Manager initialization error:', err); }); // ===== Hooks ===== // v0.2.0: ConfirmationHook 注入到 preToolHooks 管道 // 注意:setToolDefs 延迟到 DelegateTaskTool 注册后调用,确保包含所有工具的风险等级 const preToolHooks = [ new PermissionCheckHook(policyEngine), new RateLimitHook(20), confirmationHook, ]; const postToolHooks = [ new AuditLogHook(auditService), new MemoryTriggerHook(memoryManager), ]; // ===== Agent Loop ===== // Agent 配置初始化:仅此处一次性读取,配置变更时由 handlers.ts 的 config:set // 监听器调用 agentLoop.updateConfig() 和 orchestrator.updateDefaultConfig() 即时生效。 // @see electron/ipc/handlers.ts — 'config:set' handler const ollamaNumCtx = configService.get('ollama.numCtx'); const agentMaxIter = configService.get('agent.maxIterations'); const agentTimeout = configService.get('agent.totalTimeoutMs'); const agentThinkingEnabled = configService.get('agent.enableThinking'); const agentThinkingEffort = configService.get('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null; const toolExecTimeout = configService.get('agent.toolExecutionTimeoutMs'); const agentLoop = new AgentLoopEngine( { maxIterations: agentMaxIter ?? 20, totalTimeoutMs: agentTimeout ?? 600_000, contextLength: ollamaNumCtx ?? undefined, // v0.3.1: 从 adapter.getContextWindow() 读取,修复 Engine 128K vs Adapter 1M 不一致 bug contextWindow: adapter.getContextWindow(), thinkingEnabled: agentThinkingEnabled ?? true, thinkingEffort: agentThinkingEffort ?? 'high', toolExecutionTimeoutMs: toolExecTimeout ?? 120_000, }, adapter, toolRegistry, preToolHooks, postToolHooks, ); agentLoop.setTools(toolRegistry.listTools()); agentLoop.setWorkspacePath(workspaceInfo.path); // ===== Memory Consolidator(会话结束 AI 提取重要记忆到 MEMORY.md)===== const memoryConsolidator = new MemoryConsolidator(adapter, workspaceService); // ===== Task Orchestrator(子任务委派)===== const orchestrator = new TaskOrchestrator( agentLoop, toolRegistry, preToolHooks, postToolHooks, { thinkingEnabled: agentThinkingEnabled ?? true, thinkingEffort: agentThinkingEffort ?? 'high', contextLength: ollamaNumCtx ?? undefined, // v0.3.1: 同步 contextWindow 到 SubAgent 配置 contextWindow: adapter.getContextWindow(), }, ); toolRegistry.registerBuiltin(new DelegateTaskTool(orchestrator)); // 重新设置工具列表,包含新注册的 delegate_task agentLoop.setTools(toolRegistry.listTools()); // v0.2.0: 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存 confirmationHook.setToolDefs(toolRegistry.listAllTools()); // v0.3.1: 所有工具(含 DelegateTaskTool)注册完成后输出总数,确保日志显示 26 而非 25 log.info(`Registered ${toolRegistry.size} built-in tools`); // ===== 热重载 Adapter 回调(设置变更时触发)===== // 记录上次创建 adapter 时的配置快照,用于检测配置是否真的变化 let lastProvider = configService.get('llm.provider') ?? ''; // v0.3.1: configSig 加入 contextWindow 配置,使 contextWindow 变化也能触发热重载 const buildConfigSig = () => { const provider = configService.get('llm.provider') ?? ''; return JSON.stringify({ provider, model: configService.get('llm.model') ?? '', apiKey: configService.get('llm.apiKey') ?? '', baseURL: configService.get('llm.baseURL') ?? '', // v0.3.1: 加入 contextWindow 配置(Ollama 用 numCtx,其他 Provider 用 ${provider}.contextWindow) contextWindow: provider === 'ollama' ? configService.get('ollama.numCtx') : configService.get(`${provider}.contextWindow`), }); }; let lastConfigSig = buildConfigSig(); const reloadAdapter = (): boolean => { try { // 检测配置是否真的变化(避免每次发消息都重建 adapter 和弹 toast) const currentConfig = { provider: configService.get('llm.provider') ?? '', model: configService.get('llm.model') ?? '', apiKey: configService.get('llm.apiKey') ?? '', baseURL: configService.get('llm.baseURL') ?? '', }; const currentSig = buildConfigSig(); // 配置未变化 — 幂等返回成功,不重建 adapter,不发通知 if (currentSig === lastConfigSig) { return true; } // 配置变化 — 重建 adapter const newAdapter = createAdapter(); agentLoop.setAdapter(newAdapter); memoryConsolidator.setAdapter(newAdapter); // Provider 切换时同步 contextLength 和 contextWindow const provider = currentConfig.provider; if (provider === 'ollama') { const numCtx = configService.get('ollama.numCtx'); agentLoop.updateConfig({ contextLength: numCtx ?? undefined, // v0.3.1: 同步 contextWindow(Ollama 的 getContextWindow 返回固定 4096) contextWindow: newAdapter.getContextWindow(), }); } else { // v0.3.1: DeepSeek/Agnes 无 contextLength,但同步 contextWindow agentLoop.updateConfig({ contextLength: undefined, contextWindow: newAdapter.getContextWindow(), }); } log.info(`[CONFIG] Adapter reloaded: provider=${provider}`); // 仅在 Provider 真正变化时通知渲染进程 if (mainWindow && !mainWindow.isDestroyed()) { if (lastProvider && lastProvider !== provider) { mainWindow.webContents.send('agent:providerSwitched', { from: lastProvider, to: provider, reason: 'config_changed', sessionId: '', }); mainWindow.webContents.send('toast:show', { type: 'success', message: `Provider 已切换: ${lastProvider} → ${provider}`, }); } } lastProvider = provider; lastConfigSig = currentSig; return true; } catch (err) { log.error(`[CONFIG] Failed to reload adapter: ${(err as Error).message}`); // 通知渲染进程 Provider 切换失败(UI 显示错误 Toast) if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('toast:show', { type: 'error', message: `Provider 切换失败: ${(err as Error).message}`, }); } return false; } }; // ===== 窗口管理 ===== windowManager = new WindowManager(); const mainWindow = windowManager.createWindow({ id: 'main', workspacePath: workspaceInfo.path, title: 'MetonaAI Desktop', }); // v0.2.0: 为 ConfirmationHook 注入主窗口(用于向渲染进程发送确认请求) confirmationHook.setMainWindow(mainWindow); // ===== 系统托盘 ===== const resourcesPath = join(__dirname, '../../assets'); trayManager = new TrayManager(resourcesPath); trayManager.initialize(mainWindow); // ===== 全局快捷键 ===== windowManager.registerGlobalShortcuts(); // ===== Agent 状态同步到托盘 ===== agentLoop.on('stateChange', (data: { previous: string; current: string; state?: string }) => { const statusMap: Record = { INIT: 'idle', THINKING: 'thinking', PARSING: 'thinking', EXECUTING: 'executing', OBSERVING: 'thinking', REFLECTING: 'thinking', COMPRESSING: 'thinking', TERMINATED: 'idle', }; const stateValue = (data.current || data.state) ?? ''; trayManager?.setStatus(statusMap[stateValue] ?? 'idle'); }); // ===== Agent 完成时发送系统通知 ===== agentLoop.on('complete', (data: { sessionId: string; durationMs: number }) => { trayManager?.sendNotification( 'MetonaAI — 任务完成', `Agent 已完成任务 (${(data.durationMs / 1000).toFixed(1)}s)`, () => windowManager?.focusWindow(), ); }); // ===== 注册 IPC ===== registerAllIPCHandlers( mainWindow, sessionService, configService, workspaceService, contextBuilder, agentLoop, toolRegistry, auditService, sessionRecorder, memoryManager, mcpManager, reloadAdapter, promptDefender, outputValidator, confirmationHook, memoryConsolidator, orchestrator, ); // TODO: Initialize UpdateService for auto-update functionality // const updateService = new UpdateService(); // updateService.initialize(mainWindow); // ===== 应用生命周期 ===== app.on('window-all-closed', () => { // macOS: 保持应用运行(托盘模式) }); app.on('activate', () => { if (windowManager && windowManager.count === 0) { windowManager.createWindow({ id: 'main', workspacePath: workspaceInfo.path }); } else { windowManager?.focusWindow(); } }); // M-13 修复: before-quit 回调改为同步 + event.preventDefault() 确保异步清理完成 // 之前 async 回调 Electron 不会 await,导致 MCP shutdown 未完成时应用已退出 app.on('before-quit', (event) => { // 标记为正在退出,允许窗口关闭(两处 isQuitting 统一设置) (global as Record).isQuitting = true; TrayManager.markQuitting(); windowManager?.unregisterGlobalShortcuts(); trayManager?.destroy(); windowManager?.closeAll(); // 防止重复触发(macOS before-quit 可能触发多次) if ((global as Record).shutdownInProgress === true) { return; } (global as Record).shutdownInProgress = true; event.preventDefault(); // 内部异步清理,加 5 秒超时保护防止卡死 const shutdownTimeout = setTimeout(() => { log.warn('[Shutdown] Timeout reached, forcing exit'); if (databaseService) { databaseService.close(); databaseService = null; } app.exit(0); }, 5_000); (async () => { try { await mcpManager.shutdown(); } catch (err) { log.error('[Shutdown] MCP shutdown failed:', err); } cleanupBrowser(); if (databaseService) { databaseService.close(); databaseService = null; } clearTimeout(shutdownTimeout); app.exit(0); })().catch((err) => { log.error('[Shutdown] Async cleanup failed:', err); clearTimeout(shutdownTimeout); if (databaseService) { try { databaseService.close(); } catch { /* ignore */ } databaseService = null; } app.exit(1); }); }); // ===== 应用日志级别配置 ===== const logLevel = configService.get('logging.level') as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly' | null; if (logLevel) { log.transports.file.level = logLevel; log.transports.console.level = logLevel; log.info(`Log level applied: ${logLevel}`); } // ===== 恢复工具启用/禁用状态 ===== for (const toolDef of toolRegistry.listAllTools()) { const stored = configService.get(`tools.${toolDef.name}.enabled`); if (stored === false) { toolRegistry.setToolEnabled(toolDef.name, false); log.info(`Tool restored as disabled: ${toolDef.name}`); } } log.info(`MetonaAI Desktop initialized [Provider: ${configService.get('llm.provider') ?? 'none'}, Model: ${configService.get('llm.model') ?? 'none'}, Tools: ${toolRegistry.size}]`); } app.whenReady().then(initialize); app.on('web-contents-created', (_, contents) => { // C-8 修复: 全局 web-contents 监听器也校验 URL 协议 contents.setWindowOpenHandler(({ url }) => { try { const parsed = new URL(url); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { shell.openExternal(url); } else { log.warn(`[Main] Blocked window.open with unsafe protocol: ${parsed.protocol}`); } } catch { log.warn(`[Main] Blocked window.open with invalid URL: ${url.slice(0, 100)}`); } return { action: 'deny' }; }); });