/** * MetonaAI Desktop — Electron 主进程入口 * * 启动流程(按架构规范强制顺序): * 1. 初始化日志系统 + 加载 .env 环境变量(P0-4: 应用内配置优先,env 作为回退) * 2. 选择/创建工作空间 → 校验必需文件(缺失自动创建) * 3. 连接 SQLite → 执行 schema 迁移 → 加载配置 * 4. 初始化 Provider Adapter(P2-10: AgentEngineManager 管理每会话独立引擎) * 5. 加载磁盘文件 → 构建 System Prompt * 6. 注册内置工具 + 连接 MCP Servers(P1-11: 等待连接完成再广播就绪) * 7. 启动 React UI → Agent 就绪 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — 启动流程 * @see docs/MetonaAI-Desktop UI UX 设计集成方案.html — 窗口管理 */ import 'dotenv/config'; import { app, shell, Menu, BrowserWindow, dialog, session } 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 { GlobalConfigService } from './services/global-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 { SessionSummaryService } from './services/session-summary.service'; import { AgentEngineManager } from './services/agent-engine-manager.service'; import { TitleGenerator } from './services/title-generator.service'; import { ContextBuilder } from './harness/prompts/context-builder'; import { MemoryManager } from './harness/memory/manager'; import { MemoryConsolidator } from './harness/memory/consolidator'; import { MemoryMaintainer } from './harness/memory/maintainer'; import { registerAllIPCHandlers } from './ipc'; import type { ToolsReadyRef } from './ipc'; import { ToolRegistry } from './harness/tools/registry'; import { DeepSeekAdapter } from './harness/adapters/deepseek.adapter'; import { AgnesAdapter } from './harness/adapters/agnes-ai.adapter'; import { MimoAdapter } from './harness/adapters/mimo.adapter'; import { OllamaAdapter } from './harness/adapters/ollama.adapter'; import { OpenAIAdapter } from './harness/adapters/openai.adapter'; import { AnthropicAdapter } from './harness/adapters/anthropic.adapter'; import type { IMetonaProviderAdapter } from './harness/types/metona-adapter'; import { ReadFileTool, WriteFileTool, ListDirectoryTool, SearchFilesTool, WebSearchTool, WebFetchTool, MemoryStoreTool, MemorySearchTool, RunCommandTool, WebBrowserTool, cleanupBrowser, DelegateTaskTool, FileEditorTool, CodeSearchTool, TaskManagerTool, DiffViewerTool, GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool, LintCodeTool, RunTestsTool, ProjectInfoTool, HttpRequestTool, ThinkTool, ViewImageTool, DeleteFileTool, FileMoveTool, FileInfoTool, } from './harness/tools/built-in'; import { AuditLogHook, MemoryTriggerHook, PermissionCheckHook, RateLimitHook, SecurityScanHook, } from './harness/hooks'; import { ConfirmationHook } from './harness/hooks/confirmation-hook'; import { PolicyEngine, parseToolPolicy } 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 { HealthChecker, SLOMonitor } from './utils/slo'; // v0.8.1 P0-4: 主进程 toast/通知文案双语(ui.locale 驱动) import { setMainLocale, mt } from './utils/main-locale'; // v0.6.4 P4-5: session 级网络代理应用工具(default + agent-browser 分区) // ===== 步骤 1: 初始化日志系统(SYS 层)===== log.transports.file.level = 'info'; log.transports.console.level = 'debug'; // ===== 崩溃可观测性(此前崩溃无迹可查 — main.log 只会无声中断) ===== // 三个历史崩溃点(07-25 ×2 / 08-22 ×1)的日志均无声截断,无任何错误线索。 // 以下钩子确保任何致命异常/进程级崩溃都先落盘再终止,供事后归因。 process.on('uncaughtException', (err) => { log.error('[FATAL] uncaughtException:', err?.stack ?? err); }); process.on('unhandledRejection', (reason) => { log.error('[FATAL] unhandledRejection:', reason instanceof Error ? reason.stack : reason); }); app.on('render-process-gone', (_event, webContents, details) => { log.error( `[FATAL] render-process-gone: wcId=${webContents.id} reason=${details.reason} exitCode=${details.exitCode}`, ); }); app.on('child-process-gone', (_event, details) => { log.error( `[FATAL] child-process-gone: type=${details.type} reason=${details.reason} exitCode=${details.exitCode ?? 'n/a'}`, ); }); // ===== E2E 测试引导(v0.8.1 P2-5)===== // METONA_USER_DATA_DIR:重定向 userData(workspace-config.json / 全局配置层), // 使 Playwright 冒烟测试与真实用户数据完全隔离; // METONA_E2E_SEED_CONFIG:跳过引导向导并写入一组确定性的合法 LLM 配置 //(Provider/Model/输出上限/上下文长度 —— 全部为设置面板合法配置项), // 指向 e2e/mock-llm.ts 启动的本地 OpenAI 兼容服务。仅在显式设置时生效。 if (process.env['METONA_USER_DATA_DIR']) { app.setPath('userData', process.env['METONA_USER_DATA_DIR']); } // ===== 工作空间路径独立存储(解决 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; /** * P0-4: 环境变量回退映射(应用内配置优先,.env 值作为未配置时的默认) * 与 .env.example 保持一致。 */ const ENV_FALLBACK: Record = { deepseek: { apiKey: 'DEEPSEEK_API_KEY', baseURL: 'DEEPSEEK_BASE_URL' }, agnes: { apiKey: 'AGNES_API_KEY', baseURL: 'AGNES_BASE_URL' }, mimo: { apiKey: 'MIMO_API_KEY', baseURL: 'MIMO_BASE_URL' }, ollama: { baseURL: 'OLLAMA_BASE_URL' }, openai: { apiKey: 'OPENAI_API_KEY', baseURL: 'OPENAI_BASE_URL' }, anthropic: { apiKey: 'ANTHROPIC_API_KEY', baseURL: 'ANTHROPIC_BASE_URL' }, }; /** * F-7 修复: 已知 Provider 白名单 * 此前 createAdapter 的 switch default 静默落入 DeepSeekAdapter —— provider 配置 * 拼写错误(如 "deepsek")不报错,而是以空配置静默失败。显式校验后未知值 * 走"配置不完整"路径,用户可在设置中看到明确提示。 */ const KNOWN_PROVIDERS = new Set(['deepseek', 'agnes', 'mimo', 'ollama', 'openai', 'anthropic']); 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(); // v0.3.17: 初始化全局配置层(跨工作空间共享 LLM/Agent/onboarding 等机器级配置) const globalConfigService = new GlobalConfigService(); globalConfigService.initialize(); const sessionService = new SessionService(() => db); const configService = new ConfigService(() => db); // 注入全局配置层:读取时回退到全局 JSON,写入时双写 configService.setGlobalConfig(globalConfigService); // v0.8.1 P0-4: 主进程语言随 ui.locale 注入(变更经 applyConfigSideEffects 热切换) setMainLocale(configService.get('ui.locale')); // v0.8.1 P2-5: E2E 引导种子(见文件顶部说明;仅 METONA_E2E_SEED_CONFIG 时生效) if (process.env['METONA_E2E_SEED_CONFIG'] === '1') { configService.set('onboarding.completed', true); configService.set('llm.provider', 'deepseek'); configService.set('llm.model', 'deepseek-v4-flash'); configService.set('llm.baseURL', process.env['METONA_E2E_LLM_URL'] ?? 'http://127.0.0.1:0'); configService.set('llm.apiKey', 'e2e-test-key'); configService.set('llm.maxTokens', 2048); configService.set('llm.contextWindow', 32768); log.info('[E2E] Seeded deterministic LLM config for smoke test'); } // v0.3.17 迁移: 首次启用全局配置层时,把工作空间 DB 中的全局配置同步到全局 JSON // 幂等设计:migrateFromWorkspaceDB 仅写入全局层不存在的 key const configRows = db.prepare('SELECT key, value FROM app_config').all() as Array<{ key: string; value: string; }>; const workspaceConfig: Record = {}; for (const row of configRows) { try { workspaceConfig[row.key] = JSON.parse(row.value); } catch { workspaceConfig[row.key] = row.value; } } const migratedCount = globalConfigService.migrateFromWorkspaceDB(workspaceConfig); if (migratedCount > 0) { log.info( `[MIGRATION] Migrated ${migratedCount} global config keys from workspace DB to global layer`, ); } // ===== 日志服务 ===== const auditService = new AuditService(() => db); const sessionRecorder = new SessionRecorder(workspaceInfo.path); // ===== 记忆系统 ===== const memoryManager = new MemoryManager(() => db); memoryManager.initialize(); // ===== 步骤 4: Provider Adapter 工厂(P2-10: 每引擎独立实例) ===== // P0-4: 配置缺失时回退读取 .env 环境变量(应用内配置优先) const createAdapter = (): IMetonaProviderAdapter | null => { const provider = configService.get('llm.provider') ?? ''; const model = configService.get('llm.model') ?? ''; const env = ENV_FALLBACK[provider] ?? {}; const apiKey = configService.get('llm.apiKey') || (env.apiKey ? process.env[env.apiKey] : '') || ''; const baseURL = configService.get('llm.baseURL') || (env.baseURL ? process.env[env.baseURL] : '') || ''; // 配置不完整时返回 null(不抛异常,让应用能启动到 Onboarding) if (!provider || !baseURL || !model) { log.warn('LLM not fully configured. Please set provider, baseURL, and model in Settings.'); return null; } // F-7 修复: 未知 Provider 显式拒绝(此前 switch default 静默落入 DeepSeekAdapter) if (!KNOWN_PROVIDERS.has(provider)) { log.warn( `Unknown LLM provider "${provider}". Supported: ${[...KNOWN_PROVIDERS].join(', ')}.`, ); return null; } if (!apiKey && provider !== 'ollama') { log.warn(`API key is required for provider "${provider}". Please set it in Settings.`); return null; } // v0.8.1 硬性契约: 上下文窗口唯一合法来源是设置面板「上下文长度」 // (llm.contextWindow,全局单一配置,适用于一切 Provider/模型)。 // 删除了旧的 deepseek/agnes/mimo/openai/anthropic.contextWindow 分 Provider 键。 const contextWindow = configService.get('llm.contextWindow') ?? undefined; const adapterConfig = { provider, baseURL, apiKey, defaultModel: model, contextWindow }; switch (provider) { case 'agnes': return new AgnesAdapter(adapterConfig); case 'mimo': return new MimoAdapter(adapterConfig); case 'ollama': return new OllamaAdapter(adapterConfig); case 'openai': return new OpenAIAdapter(adapterConfig); case 'anthropic': return new AnthropicAdapter(adapterConfig); default: return new DeepSeekAdapter(adapterConfig); } }; // 配置未就绪时的 fallback adapter —— send/sendStream 会报错但前端可见。 // v0.8.1: 不再携带写死的 contextWindow(4096)—— 未配置时 getContextWindow // 返回 0,压缩判定跳过;该 adapter 仅在 LLM 配置缺失时兜底存在。 const FALLBACK_ADAPTER = new DeepSeekAdapter({ provider: '', baseURL: '', apiKey: '', defaultModel: '', }); // ===== 步骤 5: 工作空间文件 + System Prompt ===== const contextBuilder = new ContextBuilder(); // ===== F-8 接通: 此前为死配置的安全/日志开关真实消费 ===== // fail-secure: 仅显式 false 才关闭 —— 配置值异常(空串/null/类型错误)时保持默认开启 const promptInjectionEnabled = configService.get('security.promptInjectionDefense') !== false; // v0.6.4 P4-5: 应用启动即按配置/环境变量设置 session 级代理(变更在 shared.ts 侧联动) void (async () => { try { const { applySessionProxy } = await import('./utils/network-proxy'); await applySessionProxy(configService.get('network.proxyUrl') ?? null); } catch (err) { log.warn('[Network] Initial proxy application failed:', err); } })(); const auditEnabled = configService.get('logging.auditEnabled') !== false; const traceEnabled = configService.get('logging.traceEnabled') !== false; // SessionRecorder 录制总开关(false 时不写 TRACE JSONL 文件) sessionRecorder.setEnabled(traceEnabled); // v0.7.3 P3-3: 启动期清理旧 JSONL 录制文件(按 mtime 保留最近 200 个) void sessionRecorder.pruneOldRecordings(); // v0.8.0 P2-1: 回收站 30 天自动清理(启动 + 每 24h;软删除会话超期硬删除) const TRASH_RETENTION_DAYS = 30; const purgeTrash = (): void => { try { const purged = sessionService.purgeExpiredTrash(TRASH_RETENTION_DAYS); if (purged > 0) { log.info( `[Trash] Purged ${purged} session(s) older than ${TRASH_RETENTION_DAYS} days from trash`, ); } } catch (err) { log.warn('[Trash] Purge failed:', err); } }; purgeTrash(); const trashTimer = setInterval(purgeTrash, 24 * 60 * 60 * 1000); trashTimer.unref?.(); // ===== v0.2.0: 安全模块(必须在工具注册之前) ===== const policyEngine = new PolicyEngine(); const sandboxManager = new SandboxManager({ allowedPaths: [workspaceInfo.path], }); const promptDefender = new PromptInjectionDefender(); const outputValidator = new OutputValidator(); // v0.2.0: ConfirmationHook(提前创建,mainWindow 创建后再注入) // v0.6.4 P2-1: 注入 PolicyEngine —— 消费策略级 requireConfirmation(mcp_* 通配 // 策略),修复"外部 MCP 工具全部免确认执行"的跨层防线不一致 const confirmationHook = new ConfirmationHook(null, configService); confirmationHook.setPolicyEngine(policyEngine); // ===== 步骤 6: 注册内置工具 ===== const toolRegistry = new ToolRegistry(); toolRegistry.registerBuiltin(new ReadFileTool()); toolRegistry.registerBuiltin(new WriteFileTool()); toolRegistry.registerBuiltin(new ListDirectoryTool()); toolRegistry.registerBuiltin(new SearchFilesTool()); toolRegistry.registerBuiltin(new DeleteFileTool()); toolRegistry.registerBuiltin(new FileMoveTool()); toolRegistry.registerBuiltin(new FileInfoTool()); 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); // P2(v0.3.13): 注入 onTaskChanged 回调,工具写入后广播 IPC 事件给所有窗口 toolRegistry.registerBuiltin( new TaskManagerTool( () => db, (sessionId) => { for (const win of BrowserWindow.getAllWindows()) { win.webContents.send('task:changed', sessionId); } }, ), ); toolRegistry.registerBuiltin(new WebBrowserTool()); // v0.3.1: Git 工具集(4 个)+ 开发工具集(3 个)+ 独立工具(3 个) toolRegistry.registerBuiltin(new GitStatusTool()); toolRegistry.registerBuiltin(new GitDiffTool()); toolRegistry.registerBuiltin(new GitLogTool()); toolRegistry.registerBuiltin(new GitCommitTool()); toolRegistry.registerBuiltin(new LintCodeTool()); toolRegistry.registerBuiltin(new RunTestsTool()); toolRegistry.registerBuiltin(new ProjectInfoTool()); toolRegistry.registerBuiltin(new HttpRequestTool()); toolRegistry.registerBuiltin(new ThinkTool()); toolRegistry.registerBuiltin(new ViewImageTool()); // ===== MCP Manager ===== const mcpManager = new MCPManager(() => db, toolRegistry); // ===== Hooks(P0-2: SecurityScanHook 前置,对工具结果做间接注入防护) ===== // F-8 接通: SecurityScanHook / AuditLogHook 按 security.promptInjectionDefense // 与 logging.auditEnabled 配置构建(此前两配置为死配置,hook 无条件挂载) const preToolHooks = [ new PermissionCheckHook(policyEngine), new RateLimitHook(20), confirmationHook, ]; const postToolHooks = [ ...(promptInjectionEnabled ? [new SecurityScanHook(promptDefender)] : []), ...(auditEnabled ? [new AuditLogHook(auditService)] : []), new MemoryTriggerHook(memoryManager), ]; if (!promptInjectionEnabled) { log.warn('[Security] promptInjectionDefense disabled by config — injection scanning off'); } if (!auditEnabled) { log.warn('[Logging] auditEnabled disabled by config — tool audit logging off'); } // ===== P2-10: Agent Engine Manager(每会话独立引擎,替代全局单引擎) ===== // v0.8.1: contextWindow / maxTokens 唯一来源是设置面板 llm.contextWindow / llm.maxTokens; // Ollama 的 num_ctx(contextLength)与「上下文长度」同源 —— 同一设置同时驱动 // 压缩预算与 num_ctx 下发,不再存在独立的 ollama.numCtx 配置键。 const buildAdapter = (): IMetonaProviderAdapter => createAdapter() ?? FALLBACK_ADAPTER; const getContextWindowSetting = (): number | undefined => configService.get('llm.contextWindow') ?? undefined; const getOllamaContextLength = (): number | undefined => (configService.get('llm.provider') ?? '') === 'ollama' ? getContextWindowSetting() : undefined; const agentEngineManager = new AgentEngineManager({ buildAdapter, baseConfig: { maxIterations: configService.get('agent.maxIterations') ?? 20, totalTimeoutMs: configService.get('agent.totalTimeoutMs') ?? 600_000, contextLength: getOllamaContextLength(), contextWindow: getContextWindowSetting(), thinkingEnabled: configService.get('agent.enableThinking') ?? true, thinkingEffort: (configService.get('agent.thinkingEffort') as | 'low' | 'medium' | 'high' | 'max' | null) ?? 'high', toolExecutionTimeoutMs: configService.get('agent.toolExecutionTimeoutMs') ?? 120_000, // v0.7.3 P3-1 接线: agent.enableReflection 此前为死配置(引擎读取但全链路 // 无置 true 路径)。现注入引擎基线配置,REFLECTING 状态由此开关真实驱动。 enableReflection: configService.get('agent.enableReflection') === true, // F-8 接通: llm.temperature / llm.maxTokens 此前为死配置(引擎硬编码 0.0/63488) temperature: configService.get('llm.temperature') ?? 0.0, // v0.8.1 硬性契约: 直接读设置面板值(种子默认由 CONFIG_DEFAULTS 保证), // 不再在代码侧携带 63488 写死兜底 maxTokens: configService.get('llm.maxTokens') ?? undefined, }, toolRegistry, preToolHooks, postToolHooks, }); agentEngineManager.setWorkspacePath(workspaceInfo.path); // ===== P1: 故障转移 Provider(主 Provider 不可用时切换) ===== const buildFallbackAdapter = (): IMetonaProviderAdapter | null => { const provider = configService.get('llm.fallbackProvider'); const model = configService.get('llm.fallbackModel'); if (!provider || !model) return null; const env = ENV_FALLBACK[provider] ?? {}; const apiKey = configService.get('llm.fallbackApiKey') || (env.apiKey ? process.env[env.apiKey] : '') || ''; const baseURL = configService.get('llm.fallbackBaseURL') || (env.baseURL ? process.env[env.baseURL] : '') || ''; if (!baseURL) return null; if (!apiKey && provider !== 'ollama') return null; // F-7 修复: 故障转移 Provider 同样做白名单校验(未知值禁用故障转移而非静默降级) if (!KNOWN_PROVIDERS.has(provider)) { log.warn( `[Fallback] Unknown provider "${provider}". Supported: ${[...KNOWN_PROVIDERS].join(', ')}. Failover disabled.`, ); return null; } // v0.8.1: 上下文窗口全局单一配置(llm.contextWindow),不再按 Provider 读取 const contextWindow = configService.get('llm.contextWindow') ?? undefined; const cfg = { provider, baseURL, apiKey, defaultModel: model, contextWindow }; switch (provider) { case 'agnes': return new AgnesAdapter(cfg); case 'mimo': return new MimoAdapter(cfg); case 'ollama': return new OllamaAdapter(cfg); case 'openai': return new OpenAIAdapter(cfg); case 'anthropic': return new AnthropicAdapter(cfg); default: return new DeepSeekAdapter(cfg); } }; agentEngineManager.setFallbackAdapter(buildFallbackAdapter()); // ===== v0.8.1 P1-1: 本地向量记忆嵌入器装配 ===== // 仅在 Provider=Ollama(本地推理、数据不出设备、零 token 成本)且用户在设置面板 // 配置了 memory.embeddingModel(如 nomic-embed-text)时启用;否则 embedder 返回 // null,MemoryManager 全量回退纯 TF-IDF 检索(历史行为兼容)。 // adapter 经闭包动态读取 —— 故障转移/热重载后无需重新装配。 // v0.8.2 P3-1: 注入 modelName 指纹 —— 用户更换 embedding 模型后旧向量按 // 模型不匹配处理(检索时惰性重算自愈),不再产生跨模型噪声分数。 memoryManager.setEmbedder({ modelName: (configService.get('memory.embeddingModel') ?? '').trim() || undefined, embed: async (text) => { const adapter = agentEngineManager.getAdapter(); if (!(adapter instanceof OllamaAdapter)) return null; const model = configService.get('memory.embeddingModel') ?? ''; if (!model.trim()) return null; try { const r = await adapter.embed({ model: model.trim(), input: text }); return r.embeddings?.[0] ?? null; } catch (err) { log.warn('[Memory] Embedding failed (falling back to TF-IDF):', (err as Error).message); return null; } }, }); // ===== P1-11: MCP 初始化等待所有连接完成后再广播 tools:ready(修复工具未注册即广播的窗口) ===== // v0.3.18: toolsReadyRef 供 tools:isReady 查询(解决事件竞态) // v0.5.3: MCP 工具集合运行中变更(添加/断开/启停 server)→ 同步所有已存在引擎。 // 此前仅启动期 setToolsAll;运行中增删后已打开会话的引擎拿不到新工具 // (或持有失效定义,调用报 Unknown tool)。懒创建引擎由 createEngine // 实时拉取(v0.5.2),此回调覆盖既有引擎。 mcpManager.setOnToolsChanged(() => { agentEngineManager.setToolsAll(toolRegistry.listTools()); }); // v0.7.3 P4-2: MCP 自动重连开关(默认开启;配置变更经 shared.ts applyConfigSideEffects 联动) mcpManager.setAutoReconnect(configService.get('mcp.autoReconnect') !== false); const toolsReadyRef: ToolsReadyRef = { ready: false, toolCount: 0 }; mcpManager .initialize() .then(() => { agentEngineManager.setToolsAll(toolRegistry.listTools()); log.info('[MCP] Tools registered and synced to all engines'); toolsReadyRef.ready = true; toolsReadyRef.toolCount = toolRegistry.size; // 广播工具就绪事件给所有窗口 for (const win of BrowserWindow.getAllWindows()) { win.webContents.send('tools:ready', { toolCount: toolRegistry.size }); } }) .catch((err) => { log.warn('MCP Manager initialization error:', err); // 即使 MCP 失败,内置工具已就绪,仍广播 ready 让前端启用发送 toolsReadyRef.ready = true; toolsReadyRef.toolCount = toolRegistry.size; for (const win of BrowserWindow.getAllWindows()) { win.webContents.send('tools:ready', { toolCount: toolRegistry.size }); } }); // ===== Memory Consolidator(会话结束 AI 提取重要记忆到 MEMORY.md) ===== const memoryConsolidator = new MemoryConsolidator(buildAdapter(), workspaceService); // v0.3.18: 注入 MemoryManager,实现 DB 记忆与 MEMORY.md 双轨交叉 memoryConsolidator.setMemoryManager(memoryManager); // ===== v0.8.1 P1-2: MEMORY.md 维护闭环(分析/应用两阶段,应用前需用户确认) ===== const memoryMaintainer = new MemoryMaintainer( () => agentEngineManager.getAdapter(), workspaceService, () => db, ); // ===== P2-11: 会话摘要分层上下文服务 ===== const sessionSummaryService = new SessionSummaryService( () => db, sessionService, () => buildAdapter(), ); // ===== Task Orchestrator(子任务委派,P2-10: 适配 EngineProvider) ===== const orchestrator = new TaskOrchestrator( agentEngineManager, toolRegistry, preToolHooks, postToolHooks, { thinkingEnabled: configService.get('agent.enableThinking') ?? true, thinkingEffort: (configService.get('agent.thinkingEffort') as | 'low' | 'medium' | 'high' | 'max' | null) ?? 'high', contextLength: getOllamaContextLength(), contextWindow: getContextWindowSetting(), // v0.7.3 P3-1: SubAgent 与主引擎同源消费 enableReflection enableReflection: configService.get('agent.enableReflection') === true, }, ); toolRegistry.registerBuiltin(new DelegateTaskTool(orchestrator)); // 重新设置工具列表,包含新注册的 delegate_task agentEngineManager.setToolsAll(toolRegistry.listTools()); // 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存 confirmationHook.setToolDefs(toolRegistry.listAllTools()); // ===== v0.8.1 P2-1: 加载用户自定义工具策略(tools.{name}.policy)===== // 设置面板保存经 shared.ts 热加载;此处覆盖应用启动的冷加载。 for (const toolDef of toolRegistry.listAllTools()) { const raw = configService.get(`tools.${toolDef.name}.policy`); if (raw != null && raw !== '') { const parsed = parseToolPolicy(raw); policyEngine.setPolicyOverride(toolDef.name, parsed); if (!parsed) log.warn(`[Policy] Ignored invalid policy config for tool: ${toolDef.name}`); } } log.info(`Registered ${toolRegistry.size} built-in tools`); // ===== 热重载 Adapter 回调(设置变更时触发) ===== let lastProvider = configService.get('llm.provider') ?? ''; // v0.3.1 + P1: configSig 覆盖主/备 Provider 全部 LLM 字段 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') ?? '', fallbackProvider: configService.get('llm.fallbackProvider') ?? '', fallbackModel: configService.get('llm.fallbackModel') ?? '', fallbackApiKey: configService.get('llm.fallbackApiKey') ?? '', fallbackBaseURL: configService.get('llm.fallbackBaseURL') ?? '', // v0.8.1: 全局单一「上下文长度」配置进入签名(替代旧的分 Provider 键) contextWindow: configService.get('llm.contextWindow') ?? null, }); }; let lastConfigSig = buildConfigSig(); const reloadAdapter = (): boolean => { try { const currentSig = buildConfigSig(); // 配置未变化 — 幂等返回成功,不重建 adapter,不发通知 if (currentSig === lastConfigSig) { return true; } // 配置变化 — 校验新配置可构建 adapter const probe = createAdapter(); if (!probe) { log.warn( '[CONFIG] Cannot create adapter: LLM config incomplete (provider/apiKey/baseURL/model)', ); const wins = BrowserWindow.getAllWindows(); for (const win of wins) { win.webContents.send('toast:show', { type: 'warning', message: mt('config.toast.configIncomplete'), }); } return false; } // P2-10: 重建所有引擎的 adapter(工厂闭包读取最新配置) agentEngineManager.refreshAdapters(); agentEngineManager.setFallbackAdapter(buildFallbackAdapter()); memoryConsolidator.setAdapter(agentEngineManager.getAdapter()); // v0.8.1: 同步「上下文长度」到所有引擎(Ollama 同时作为 num_ctx/contextLength) const provider = configService.get('llm.provider') ?? ''; const ctxSetting = getContextWindowSetting(); agentEngineManager.updateConfigAll({ contextLength: provider === 'ollama' ? ctxSetting : undefined, contextWindow: ctxSetting, }); orchestrator.updateDefaultConfig({ contextLength: provider === 'ollama' ? ctxSetting : undefined, contextWindow: ctxSetting, }); log.info(`[CONFIG] Adapter reloaded: provider=${provider}`); // 仅在 Provider 真正变化时通知渲染进程 if (lastProvider && lastProvider !== provider) { for (const win of BrowserWindow.getAllWindows()) { win.webContents.send('agent:providerSwitched', { from: lastProvider, to: provider, reason: 'config_changed', sessionId: '', }); win.webContents.send('toast:show', { type: 'success', message: mt('config.toast.providerSwitched', { from: lastProvider, to: provider }), }); } } lastProvider = provider; lastConfigSig = currentSig; return true; } catch (err) { log.error(`[CONFIG] Failed to reload adapter: ${(err as Error).message}`); for (const win of BrowserWindow.getAllWindows()) { win.webContents.send('toast:show', { type: 'error', message: mt('config.toast.providerSwitchFailed', { message: (err as Error).message }), }); } return false; } }; // ===== P1-12 + v0.7.3 P3-4: SLO 健康监控接入(提前至窗口创建前, // 使 IPC 侧 getHealthSnapshot 闭包可安全引用 sloMonitor/healthChecker) ===== const healthChecker = new HealthChecker( () => db, join(workspaceInfo.path, '.metona', 'agent.db'), ); const sloMonitor = new SLOMonitor(); agentEngineManager.on('complete', (data: { durationMs: number; terminationReason?: string }) => { sloMonitor.recordRequest(data.durationMs, data.terminationReason === 'completed'); }); /** 最近一次健康检查报告(供 IPC app:healthSnapshot 展示;初始 null = 尚未检查) */ let lastHealthReport: Awaited> | null = null; const healthTimer = setInterval(async () => { try { // v0.7.3 P3-1 接线: cleanupExpired 此前零调用方(且 expires_at 无写入方)双重死路。 // 现挂入健康检查周期,让过期记忆(episodic_memories.expires_at)真正可回收, // 为后续记忆 TTL 语义预留真实管道;无过期条目时为零开销(一条带索引的 DELETE)。 const expired = memoryManager.cleanupExpired(); if (expired > 0) { log.info(`[Health] Expired episodic memories purged: ${expired}`); } const report = await healthChecker.check(); lastHealthReport = report; if (!report.healthy) { const failed = report.checks .filter((c) => !c.healthy) .map((c) => c.name) .join(', '); log.warn(`[Health] Unhealthy checks: ${failed}`); trayManager?.setStatus('error'); } else { // 恢复 idle(运行中状态会被后续 stateChange 覆盖) trayManager?.setStatus('idle'); } const slo = sloMonitor.getStatus(); if (slo.violated) { log.warn( `[SLO] Burn rate ${slo.burnRate.toFixed(2)} exceeds budget (errorRate=${(slo.errorRate * 100).toFixed(1)}%, ` + `P95=${slo.percentiles['P95'] ?? 0}ms, ${slo.totalRequests} requests)`, ); } } catch (err) { log.warn('[Health] Check failed:', err); } }, 60_000); healthTimer.unref?.(); // ===== v0.7.3 P4-1: 会话标题生成器(首轮 run 完成后 LLM 生成精炼标题) ===== const titleGenerator = new TitleGenerator(() => agentEngineManager.getAdapter(), sessionService); // ===== 窗口管理 ===== windowManager = new WindowManager(); // 局部 const 捕获:闭包内 TS 无法对模块级可空变量做流收窄 const wm = windowManager; // F-4 修复: 窗口创建收敛为单一入口 —— macOS activate / 托盘重建窗口此前不走 // beforeLoad,导致 confirmationHook 仍指向已销毁的旧窗口,所有需确认工具被 // "no main window available" 永久阻断。此闭包保证任何重建路径都完成 // setMainWindow + IPC 注册(registerAllIPCHandlers 有防重入标志,重复调用安全)。 const createMainWindow = (): BrowserWindow => wm.createWindow({ id: 'main', workspacePath: workspaceInfo.path, title: 'MetonaAI Desktop', // P2-11 修复: 在 loadURL 之前注册 IPC handler,消除渲染进程加载与 IPC 注册的时序窗口 beforeLoad: (win) => { confirmationHook.setMainWindow(win); registerAllIPCHandlers({ mainWindow: win, sessionService, configService, workspaceService, contextBuilder, agentEngineManager, toolRegistry, policyEngine, auditService, sessionRecorder, memoryManager, mcpManager, reloadAdapter, promptInjectionDefender: promptDefender, outputValidator, confirmationHook, memoryConsolidator, memoryMaintainer, orchestrator, sessionSummaryService, titleGenerator, getHealthSnapshot: () => ({ slo: sloMonitor.getStatus(), health: lastHealthReport, generatedAt: Date.now(), }), toolsReadyRef, }); }, }); const mainWindow = createMainWindow(); // ===== 系统托盘 ===== const resourcesPath = join(__dirname, '../../assets'); trayManager = new TrayManager(resourcesPath); trayManager.initialize(mainWindow); // ===== v0.8.0 P2-3: electron-updater 自动更新(仅生产环境 + 已配置 feed) ===== // dev 模式 / 未配置 app.updateFeedUrl 时静默禁用;状态事件广播 update:status if (!process.env['ELECTRON_RENDERER_URL']) { const feedUrl = configService.get('app.updateFeedUrl') ?? ''; if (feedUrl) { const { startAutoUpdater } = await import('./services/update.service'); await startAutoUpdater(feedUrl, (event) => { for (const win of BrowserWindow.getAllWindows()) { if (!win.isDestroyed()) win.webContents.send('update:status', event); } if (event.status === 'available') { trayManager?.sendNotification( mt('notify.updateAvailable.title'), mt('notify.updateAvailable.body', { version: event.latestVersion }), ); } }); } } // ===== 全局快捷键 ===== windowManager.registerGlobalShortcuts(); // ===== Agent 状态同步到托盘(P2-10: 监听 manager 聚合事件) ===== agentEngineManager.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 完成时发送系统通知 ===== // v0.6.4 修复(B-6): 仅在任务真正完成时弹出系统通知 —— 引擎对 user_interrupt / // timeout / dead_loop / error 等也走统一 finishRun 发出 complete 事件, // 原实现让"用户主动停止"也会收到「Agent 已完成任务」的误导性通知。 agentEngineManager.on( 'complete', (data: { sessionId: string; durationMs: number; terminationReason?: string }) => { if (data.terminationReason !== undefined && data.terminationReason !== 'completed') { return; } trayManager?.sendNotification( mt('notify.taskCompleted.title'), mt('notify.taskCompleted.body', { seconds: (data.durationMs / 1000).toFixed(1) }), () => windowManager?.focusWindow(), ); }, ); // ===== 应用生命周期 ===== app.on('window-all-closed', () => { // macOS: 保持应用运行(托盘模式) }); app.on('activate', () => { if (windowManager && windowManager.count === 0) { // F-4 修复: 重建窗口走统一入口(beforeLoad 补 confirmationHook/IPC), // 并把托盘引用重新绑定到新窗口(否则托盘点击指向已销毁窗口) const win = createMainWindow(); trayManager?.rebindWindow(win); } else { windowManager?.focusWindow(); } }); // M-13 修复: before-quit 回调改为同步 + event.preventDefault() 确保异步清理完成 app.on('before-quit', (event) => { // 标记为正在退出,允许窗口关闭 (global as Record).isQuitting = true; TrayManager.markQuitting(); clearInterval(healthTimer); windowManager?.unregisterGlobalShortcuts(); trayManager?.destroy(); windowManager?.closeAll(); // 防止重复触发(macOS before-quit 可能触发多次) if ((global as Record).shutdownInProgress === true) { return; } (global as Record).shutdownInProgress = true; event.preventDefault(); // v0.3.18 修复: 超时从 5 秒延长到 40 秒,以容纳 consolidate 的 35 秒等待 const shutdownTimeout = setTimeout(() => { log.warn('[Shutdown] Timeout reached, forcing exit'); if (databaseService) { databaseService.close(); databaseService = null; } app.exit(0); }, 40_000); (async () => { try { await mcpManager.shutdown(); } catch (err) { log.error('[Shutdown] MCP shutdown failed:', err); } // 崩溃修复: cleanupBrowser 现为异步(close 含 session 存储终态清理) try { await cleanupBrowser(); } catch (err) { log.error('[Shutdown] Browser cleanup failed:', err); } // v0.3.18 修复: 等待进行中的记忆固化任务完成,避免应用退出导致记忆丢失 if (memoryConsolidator.isRunning()) { log.info('[Shutdown] Waiting for memory consolidation to complete...'); const completed = await memoryConsolidator.waitForCompletion(35_000); if (!completed) { log.warn('[Shutdown] Memory consolidation timed out, proceeding with exit'); } else { log.info('[Shutdown] Memory consolidation completed'); } } 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}]`, ); } // ===== v0.7.2 A6: 单实例锁(根治双开) ===== // 此前无任何单实例约束 —— 双开应用会创建两个实例共用同一 agent.db(WAL 多进程 // 写存在 busy 冲突面),托盘/全局快捷键重复注册。现契约: // - 抢锁失败的第二实例立即退出; // - 抢锁成功的实例监听 second-instance,任何后续启动请求都聚焦已有主窗口 // (含从托盘隐藏状态唤起 —— focusWindow 内部处理 restore + show + focus)。 const gotSingleInstanceLock = app.requestSingleInstanceLock(); if (!gotSingleInstanceLock) { log.info('[SYS] Another MetonaAI Desktop instance is running — quitting this one'); app.quit(); } else { app.on('second-instance', () => { log.info('[SYS] Second instance launch requested — focusing existing window'); windowManager?.focusWindow(); }); // v0.5.0: 启动链路异常兜底 — DB 损坏/工作空间不可写等初始化失败时, // 原实现会静默挂起(渲染进程白屏且无任何用户可见错误)。 // 兜底策略:记录日志 + 弹出系统错误对话框 + 退出(exit 1) app .whenReady() .then(initialize) .catch((err) => { log.error('[Startup] Initialization failed:', err); try { dialog.showErrorBox( // v0.8.2 P2-5: 启动失败对话框文案出层(ui.locale 已随配置初始化) mt('app.dialog.startupFailed.title'), mt('app.dialog.startupFailed.body', { message: (err as Error)?.message ?? String(err), }), ); } catch { // showErrorBox 失败(极端环境)时仅保留日志 } app.exit(1); }); } // ===== v0.6.4 安全加固:进程级权限防线(P2-3)===== // 此前主窗口无 CSP、无 permission handler —— notifications/geo/media/clipboard // 等请求全部走 Chromium 默认放行,且渲染层一旦被注入可静默触达敏感能力。 // 两条防线均为 deny-by-default 白名单制,任何一条失败都不放大攻击面。 { // 防线一:权限请求白名单(deny-by-default) // v0.8.1 根治: session.defaultSession 必须在 app ready 后才能访问 —— 此前在 // 模块加载期(ready 前)直接调用,每次启动都抛 "Session can only be received // when app is ready",权限白名单静默失效(FATAL 日志为证)。现延迟到 ready 后 // 注册,防线真正生效。 const ALLOWED_PERMISSIONS = new Set(['clipboard-sanitized-write', 'fullscreen']); void app .whenReady() .then(() => { session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => { callback(ALLOWED_PERMISSIONS.has(permission)); }); session.defaultSession.setPermissionCheckHandler((_wc, permission) => ALLOWED_PERMISSIONS.has(permission), ); log.info('[Security] Permission whitelist registered (app ready)'); }) .catch((err) => { log.error('[Security] Failed to register permission whitelist:', err); }); // 防线二:生产环境 CSP 注入(仅 mainFrame,不触碰 dev server 的 HMR)。 // MUI/emotion 需要 style-src 'unsafe-inline'(运行时注入