Files
metona-ai-desktop/electron/main.ts
T
thzxx ceb8ee644d feat: 升级至 v0.3.7 — 前后端状态同步与错误处理全量修复
核心引擎修复:
- CE-1: 上下文压缩摘要 role 从 system 改为 user,避免被 adapter 过滤
- CE-2: 工具失败时优先使用 error 字段(engine/openai-format/ollama 三处)
- P0-1: DeadLoopError 终止时正确传 error 参数,前端可见 ERROR 事件
- MT-1: 新增 waitForAbort 方法,abortSession 等待 run 结束再返回
- MT-2: TERMINATED 状态到达时标记步骤完成,避免 Trace Viewer 转圈
- MT-3: 压缩边界检测孤立 tool 消息,避免 API 400 错误
- isRetryableError 与 catch 分支统一 toLowerCase

IPC 与主进程修复:
- P0-2: createAdapter 配置缺失返回 null,FALLBACK_ADAPTER 兜底
- P0-3: reloadAdapter 失败返回 success:false 通知前端
- P1-5: 校验失败发 ERROR+DONE 流事件,防止 isStreaming 卡死
- P1-6: configLoaded 标志,配置加载前禁用发送按钮
- P1-7: MCP initialize 移到 agentLoop 后,完成后同步工具
- P2-11: beforeLoad 在 loadURL 前注册 IPC handler
- P2-12: provider 切换竞态保护

前端状态同步修复:
- clearSessions 后同步清空前端会话与消息状态
- clearMemories 通过 memoryVersion 触发 MemoryViewer 重新加载
- ContextMenu 4 个 session 操作补全 IPC 调用与 try/catch
- useConfig 配置保存失败回滚 UI 并提示
- handleToggle 工具切换失败回滚单个工具状态

错误处理全量补全:
- 所有 await window.metona 调用补全 try/catch 与 toast 反馈
- MCP addServer/toggleServer/removeServer 检查返回值
- showItemInFolder 检查返回值(handleOpen/handleOpenInFolder)
- sse-stream/ollama NDJSON 解析失败改为 log.warn
- adapter throwHttpError 带 status 属性供 isRetryableError 判断
2026-07-16 22:40:32 +08:00

570 lines
23 KiB
TypeScript
Raw Permalink 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. 初始化日志系统
* 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 { MimoAdapter } from './harness/adapters/mimo.adapter';
import { OllamaAdapter } from './harness/adapters/ollama.adapter';
import type { IMetonaProviderAdapter } from './harness/types/metona-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<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();
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 工厂 =====
// P0-2 修复: 配置缺失时返回 null 而非抛异常,让应用能启动到 Onboarding
const createAdapter = (): IMetonaProviderAdapter | null => {
const provider = configService.get<string>('llm.provider') ?? '';
const model = configService.get<string>('llm.model') ?? '';
const apiKey = configService.get<string>('llm.apiKey') ?? '';
const baseURL = configService.get<string>('llm.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;
}
// v0.3.1: 读取 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);
default: return new DeepSeekAdapter(adapterConfig);
}
};
// 配置未就绪时的 fallback adapter — getContextWindow 返回安全值,send/sendStream 会报错但 P0-1 修复后前端可见
const FALLBACK_ADAPTER = new DeepSeekAdapter({
provider: '', baseURL: '', apiKey: '', defaultModel: '', contextWindow: 4096,
});
const adapter = createAdapter() ?? FALLBACK_ADAPTER;
// ===== 步骤 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);
// P1-7 修复: initialize() 移到 agentLoop 创建之后,完成后重新同步工具
// ===== 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<number>('ollama.numCtx');
const agentMaxIter = configService.get<number>('agent.maxIterations');
const agentTimeout = configService.get<number>('agent.totalTimeoutMs');
const agentThinkingEnabled = configService.get<boolean>('agent.enableThinking');
const agentThinkingEffort = configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null;
const toolExecTimeout = configService.get<number>('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);
// P1-7 修复: MCP 初始化在 agentLoop 创建后执行,完成后重新同步工具到 AgentLoop
mcpManager.initialize().then(() => {
agentLoop.setTools(toolRegistry.listTools());
log.info('[MCP] Tools registered and synced to AgentLoop');
}).catch((err) => {
log.warn('MCP Manager initialization error:', err);
});
// ===== 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<string>('llm.provider') ?? '';
// v0.3.1: configSig 加入 contextWindow 配置,使 contextWindow 变化也能触发热重载
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') ?? '',
// v0.3.1: 加入 contextWindow 配置(Ollama 用 numCtx,其他 Provider 用 ${provider}.contextWindow
contextWindow: provider === 'ollama'
? configService.get<number>('ollama.numCtx')
: configService.get<number>(`${provider}.contextWindow`),
});
};
let lastConfigSig = buildConfigSig();
const reloadAdapter = (): boolean => {
try {
// 检测配置是否真的变化(避免每次发消息都重建 adapter 和弹 toast
const currentConfig = {
provider: configService.get<string>('llm.provider') ?? '',
model: configService.get<string>('llm.model') ?? '',
apiKey: configService.get<string>('llm.apiKey') ?? '',
baseURL: configService.get<string>('llm.baseURL') ?? '',
};
const currentSig = buildConfigSig();
// 配置未变化 — 幂等返回成功,不重建 adapter,不发通知
if (currentSig === lastConfigSig) {
return true;
}
// 配置变化 — 重建 adapter
const newAdapter = createAdapter();
if (!newAdapter) {
log.warn('[CONFIG] Cannot create adapter: LLM config incomplete (provider/apiKey/baseURL/model)');
// 不更新 lastConfigSig,下次还会尝试
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
type: 'warning',
message: 'LLM 配置不完整,请在设置中补全 Provider、API Key、Base URL 和 Model',
});
}
return false;
}
agentLoop.setAdapter(newAdapter);
memoryConsolidator.setAdapter(newAdapter);
// Provider 切换时同步 contextLength 和 contextWindow
const provider = currentConfig.provider;
if (provider === 'ollama') {
const numCtx = configService.get<number>('ollama.numCtx');
agentLoop.updateConfig({
contextLength: numCtx ?? undefined,
// v0.3.1: 同步 contextWindowOllama 的 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',
// P2-11 修复: 在 loadURL 之前注册 IPC handler,消除渲染进程加载与 IPC 注册的时序窗口
beforeLoad: (win) => {
confirmationHook.setMainWindow(win);
registerAllIPCHandlers(
win, sessionService, configService, workspaceService,
contextBuilder, agentLoop, toolRegistry, auditService,
sessionRecorder, memoryManager, mcpManager, reloadAdapter,
promptDefender, outputValidator,
confirmationHook, memoryConsolidator, orchestrator,
);
},
});
// P2-11 修复: ConfirmationHook.setMainWindow 已在 beforeLoad 中调用,此处无需重复
// beforeLoad 在 loadURL 之前执行,确保 IPC handler 注册时 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<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 完成时发送系统通知 =====
agentLoop.on('complete', (data: { sessionId: string; durationMs: number }) => {
trayManager?.sendNotification(
'MetonaAI — 任务完成',
`Agent 已完成任务 (${(data.durationMs / 1000).toFixed(1)}s)`,
() => windowManager?.focusWindow(),
);
});
// 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<string, unknown>).isQuitting = true;
TrayManager.markQuitting();
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();
// 内部异步清理,加 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<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' };
});
});