Files
metona-ai-desktop/electron/main.ts
T
thzxx 2230bcec3f feat: v0.4.0 四阶段迭代 — 安全加固 + 工程基线 + 架构重构 + 双 Provider 扩展
P0 安全修复:
- API Key 加密存储(safeStorage 密钥链,版本化前缀,历史明文平滑兼容)
- 间接提示注入防护(SecurityScanHook 工具结果深扫描,网络工具脱敏/本地工具警示分级)
- error:report IPC 断链修复(渲染进程错误上报落 electron-log + 审计)
- abort 信号贯通工具层(run_command/dev-tools 子进程随会话中断终止)
- run_command 沙箱加固(cd 系统目录/敏感文件读取拦截 + chcp 前缀剥离防解析退化)
- .env 真实生效(dotenv 回退加载,应用内配置优先)

P1 工程基础:
- ESLint 9 flat config + 全部 34 条存量 warnings 清零(零容忍基线)
- 测试基线 118 用例 11 文件(token/文件防护/权限/沙箱/注入/命令/引擎/注册表/审计链/摘要分层)
- test:electron 双模式(ELECTRON_RUN_AS_NODE 跑 Electron ABI,SQLite 套件全执行)
- SessionRecorder 多会话隔离 + 9 种 TRACE 事件补全(含最终轮 iteration_end)
- Provider 故障转移(重试耗尽/不可重试一次性切换 fallback + 前端通知)
- MCP 真就绪(等待全部连接完成再广播 tools:ready)
- SLO/HealthChecker 真实接入(60s 巡检 + 托盘状态)
- CONFIG_DEFAULTS 单一来源(消除 SEED 双源漂移)

P2 架构升级:
- handlers.ts 1940 行拆分为 13 个 IPC 域模块(防重入注册 + 多窗口广播)
- AgentEngineManager 每会话独立引擎(LRU 30 + adapter 工厂隔离 abort 信号)
- TaskOrchestrator EngineProvider 改造 + abortByParent 联动中断 SubAgent
- 会话摘要分层上下文(session_summaries 滚动摘要 + 截断游标清理防因果污染)
- 消息编辑重发/重新生成(truncateAfter IPC + store 动作 + UI)
- Markdown 导出 / WebSearch 并行抓取(并发 3)/ 记忆 TF 缓存 / 版本构建期注入

P3 能力扩展:
- OpenAI Adapter(o 系列推理模型 reasoning_effort/max_completion_tokens)
- Anthropic Adapter(原生 Messages API:tool_use 块/角色合并/thinking budget/图片 base64/SSE 事件机)
- 设置页/Onboarding 六 Provider 全链路接入
2026-08-20 23:17:02 +08:00

688 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MetonaAI Desktop — Electron 主进程入口
*
* 启动流程(按架构规范强制顺序):
* 1. 初始化日志系统 + 加载 .env 环境变量(P0-4: 应用内配置优先,env 作为回退)
* 2. 选择/创建工作空间 → 校验必需文件(缺失自动创建)
* 3. 连接 SQLite → 执行 schema 迁移 → 加载配置
* 4. 初始化 Provider AdapterP2-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 } 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 { ContextBuilder } from './harness/prompts/context-builder';
import { MemoryManager } from './harness/memory/manager';
import { MemoryConsolidator } from './harness/memory/consolidator';
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 } 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';
// ===== 步骤 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;
/**
* P0-4: 环境变量回退映射(应用内配置优先,.env 值作为未配置时的默认)
* 与 .env.example 保持一致。
*/
const ENV_FALLBACK: Record<string, { apiKey?: string; baseURL?: string }> = {
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' },
};
async function initialize(): Promise<void> {
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.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<string, unknown> = {};
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<string>('llm.provider') ?? '';
const model = configService.get<string>('llm.model') ?? '';
const env = ENV_FALLBACK[provider] ?? {};
const apiKey = configService.get<string>('llm.apiKey') || (env.apiKey ? process.env[env.apiKey] : '') || '';
const baseURL = configService.get<string>('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;
}
if (!apiKey && provider !== 'ollama') {
log.warn(`API key is required for provider "${provider}". Please set it in Settings.`);
return null;
}
// 读取 Provider 对应的 contextWindow 配置(Ollama 不使用此字段)
const contextWindow = provider !== 'ollama'
? configService.get<number>(`${provider}.contextWindow`) ?? undefined
: 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 — getContextWindow 返回安全值,send/sendStream 会报错但前端可见
const FALLBACK_ADAPTER = new DeepSeekAdapter({
provider: '', baseURL: '', apiKey: '', defaultModel: '', contextWindow: 4096,
});
// ===== 步骤 5: 工作空间文件 + System Prompt =====
const contextBuilder = new ContextBuilder();
// ===== v0.2.0: 安全模块(必须在工具注册之前) =====
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 创建后再注入)
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());
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);
// P2v0.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);
// ===== HooksP0-2: SecurityScanHook 前置,对工具结果做间接注入防护) =====
const preToolHooks = [
new PermissionCheckHook(policyEngine),
new RateLimitHook(20),
confirmationHook,
];
const postToolHooks = [
new SecurityScanHook(promptDefender),
new AuditLogHook(auditService),
new MemoryTriggerHook(memoryManager),
];
// ===== P2-10: Agent Engine Manager(每会话独立引擎,替代全局单引擎) =====
const buildAdapter = (): IMetonaProviderAdapter => createAdapter() ?? FALLBACK_ADAPTER;
const ollamaNumCtx = configService.get<number>('ollama.numCtx');
const agentEngineManager = new AgentEngineManager({
buildAdapter,
baseConfig: {
maxIterations: configService.get<number>('agent.maxIterations') ?? 20,
totalTimeoutMs: configService.get<number>('agent.totalTimeoutMs') ?? 600_000,
contextLength: ollamaNumCtx ?? undefined,
contextWindow: buildAdapter().getContextWindow(),
thinkingEnabled: configService.get<boolean>('agent.enableThinking') ?? true,
thinkingEffort: (configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null) ?? 'high',
toolExecutionTimeoutMs: configService.get<number>('agent.toolExecutionTimeoutMs') ?? 120_000,
},
toolRegistry,
preToolHooks,
postToolHooks,
});
agentEngineManager.setWorkspacePath(workspaceInfo.path);
// ===== P1: 故障转移 Provider(主 Provider 不可用时切换) =====
const buildFallbackAdapter = (): IMetonaProviderAdapter | null => {
const provider = configService.get<string>('llm.fallbackProvider');
const model = configService.get<string>('llm.fallbackModel');
if (!provider || !model) return null;
const env = ENV_FALLBACK[provider] ?? {};
const apiKey = configService.get<string>('llm.fallbackApiKey') || (env.apiKey ? process.env[env.apiKey] : '') || '';
const baseURL = configService.get<string>('llm.fallbackBaseURL') || (env.baseURL ? process.env[env.baseURL] : '') || '';
if (!baseURL) return null;
if (!apiKey && provider !== 'ollama') return null;
const contextWindow = provider !== 'ollama'
? configService.get<number>(`${provider}.contextWindow`) ?? undefined
: 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());
// P1-11: MCP 初始化等待所有连接完成后再广播 tools:ready(修复工具未注册即广播的窗口)
// v0.3.18: toolsReadyRef 供 tools:isReady 查询(解决事件竞态)
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);
// ===== 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<boolean>('agent.enableThinking') ?? true,
thinkingEffort: (configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null) ?? 'high',
contextLength: ollamaNumCtx ?? undefined,
contextWindow: buildAdapter().getContextWindow(),
},
);
toolRegistry.registerBuiltin(new DelegateTaskTool(orchestrator));
// 重新设置工具列表,包含新注册的 delegate_task
agentEngineManager.setToolsAll(toolRegistry.listTools());
// 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存
confirmationHook.setToolDefs(toolRegistry.listAllTools());
log.info(`Registered ${toolRegistry.size} built-in tools`);
// ===== 热重载 Adapter 回调(设置变更时触发) =====
let lastProvider = configService.get<string>('llm.provider') ?? '';
// v0.3.1 + P1: configSig 覆盖主/备 Provider 全部 LLM 字段
const buildConfigSig = () => {
const provider = configService.get<string>('llm.provider') ?? '';
return JSON.stringify({
provider,
model: configService.get<string>('llm.model') ?? '',
apiKey: configService.get<string>('llm.apiKey') ?? '',
baseURL: configService.get<string>('llm.baseURL') ?? '',
fallbackProvider: configService.get<string>('llm.fallbackProvider') ?? '',
fallbackModel: configService.get<string>('llm.fallbackModel') ?? '',
fallbackApiKey: configService.get<string>('llm.fallbackApiKey') ?? '',
fallbackBaseURL: configService.get<string>('llm.fallbackBaseURL') ?? '',
contextWindow: provider === 'ollama'
? configService.get<number>('ollama.numCtx')
: configService.get<number>(`${provider}.contextWindow`),
});
};
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: 'LLM 配置不完整,请在设置中补全 Provider、API Key、Base URL 和 Model',
});
}
return false;
}
// P2-10: 重建所有引擎的 adapter(工厂闭包读取最新配置)
agentEngineManager.refreshAdapters();
agentEngineManager.setFallbackAdapter(buildFallbackAdapter());
memoryConsolidator.setAdapter(agentEngineManager.getAdapter());
// Provider 切换时同步 contextLength 和 contextWindow
const provider = configService.get<string>('llm.provider') ?? '';
if (provider === 'ollama') {
const numCtx = configService.get<number>('ollama.numCtx');
agentEngineManager.updateConfigAll({
contextLength: numCtx ?? undefined,
contextWindow: agentEngineManager.getAdapter().getContextWindow(),
});
} else {
agentEngineManager.updateConfigAll({
contextLength: undefined,
contextWindow: agentEngineManager.getAdapter().getContextWindow(),
});
}
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: `Provider 已切换: ${lastProvider}${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: `Provider 切换失败: ${(err as Error).message}`,
});
}
return false;
}
};
// ===== 窗口管理 =====
windowManager = new WindowManager();
const mainWindow = windowManager.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,
auditService,
sessionRecorder,
memoryManager,
mcpManager,
reloadAdapter,
promptInjectionDefender: promptDefender,
outputValidator,
confirmationHook,
memoryConsolidator,
orchestrator,
sessionSummaryService,
toolsReadyRef,
});
},
});
// ===== 系统托盘 =====
const resourcesPath = join(__dirname, '../../assets');
trayManager = new TrayManager(resourcesPath);
trayManager.initialize(mainWindow);
// ===== 全局快捷键 =====
windowManager.registerGlobalShortcuts();
// ===== Agent 状态同步到托盘(P2-10: 监听 manager 聚合事件) =====
agentEngineManager.on('stateChange', (data: { previous?: string; current?: string; state?: string }) => {
const statusMap: Record<string, 'idle' | 'thinking' | 'executing' | 'error'> = {
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 完成时发送系统通知 =====
agentEngineManager.on('complete', (data: { sessionId: string; durationMs: number }) => {
trayManager?.sendNotification(
'MetonaAI — 任务完成',
`Agent 已完成任务 (${(data.durationMs / 1000).toFixed(1)}s)`,
() => windowManager?.focusWindow(),
);
});
// ===== P1-12: SLO 健康监控接入(原为死代码,现真实运行) =====
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');
});
const healthTimer = setInterval(async () => {
try {
const report = await healthChecker.check();
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?.();
// ===== 应用生命周期 =====
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() 确保异步清理完成
app.on('before-quit', (event) => {
// 标记为正在退出,允许窗口关闭
(global as Record<string, unknown>).isQuitting = true;
TrayManager.markQuitting();
clearInterval(healthTimer);
windowManager?.unregisterGlobalShortcuts();
trayManager?.destroy();
windowManager?.closeAll();
// 防止重复触发(macOS before-quit 可能触发多次)
if ((global as Record<string, unknown>).shutdownInProgress === true) {
return;
}
(global as Record<string, unknown>).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();
// 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<string>('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<boolean>(`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<string>('llm.provider') ?? 'none'}, Model: ${configService.get<string>('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' };
});
});