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 全链路接入
This commit is contained in:
2026-08-20 23:17:02 +08:00
parent b9f7ec5118
commit 2230bcec3f
90 changed files with 6581 additions and 2771 deletions
+195 -150
View File
@@ -2,19 +2,20 @@
* MetonaAI Desktop — Electron 主进程入口
*
* 启动流程(按架构规范强制顺序):
* 1. 初始化日志系统
* 1. 初始化日志系统 + 加载 .env 环境变量(P0-4: 应用内配置优先,env 作为回退)
* 2. 选择/创建工作空间 → 校验必需文件(缺失自动创建)
* 3. 连接 SQLite → 执行 schema 迁移 → 加载配置
* 4. 初始化 Provider Adapter
* 5. 加载 4 个磁盘文件 → 构建 System Prompt
* 6. 注册内置工具 + 连接 MCP Servers
* 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 { app, shell, Menu, BrowserWindow, ipcMain } from 'electron';
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';
@@ -29,16 +30,20 @@ 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/handlers';
import { AgentLoopEngine } from './harness/agent-loop';
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,
@@ -47,31 +52,27 @@ import {
RunCommandTool,
WebBrowserTool, cleanupBrowser,
DelegateTaskTool,
// v0.2.0 新增工具
FileEditorTool,
CodeSearchTool,
TaskManagerTool,
DiffViewerTool,
// v0.3.1 新增工具(11 个)
GitStatusTool, GitDiffTool, GitLogTool, GitCommitTool,
LintCodeTool, RunTestsTool, ProjectInfoTool,
HttpRequestTool,
ThinkTool,
ViewImageTool,
// v0.3.2 新增工具(1 个)
DeleteFileTool,
// v0.3.3 新增工具(2 个)
FileMoveTool,
FileInfoTool,
} from './harness/tools/built-in';
import { AuditLogHook, MemoryTriggerHook, PermissionCheckHook, RateLimitHook } from './harness/hooks';
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 { UpdateService } from './services/update.service';
import { HealthChecker, SLOMonitor } from './utils/slo';
// ===== 步骤 1: 初始化日志系统(SYS 层)=====
log.transports.file.level = 'info';
@@ -106,6 +107,19 @@ 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');
@@ -133,7 +147,6 @@ async function initialize(): Promise<void> {
const db = databaseService.getDB();
// v0.3.17: 初始化全局配置层(跨工作空间共享 LLM/Agent/onboarding 等机器级配置)
// 必须在 ConfigService 创建前初始化,以便注入
const globalConfigService = new GlobalConfigService();
globalConfigService.initialize();
@@ -143,9 +156,7 @@ async function initialize(): Promise<void> {
configService.setGlobalConfig(globalConfigService);
// v0.3.17 迁移: 首次启用全局配置层时,把工作空间 DB 中的全局配置同步到全局 JSON
// 幂等设计:migrateFromWorkspaceDB 仅写入全局层不存在的 key,重复执行无副作用
// 解决场景:老用户从 v0.3.16 升级,LLM 配置在工作空间 DB 里,需要迁移到全局层
// 注意: 此处直接查 DB 而不通过 configService.getAll(),因为后者会合并全局层,导致迁移逻辑循环
// 幂等设计: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) {
@@ -168,13 +179,14 @@ async function initialize(): Promise<void> {
const memoryManager = new MemoryManager(() => db);
memoryManager.initialize();
// ===== 步骤 4: Provider Adapter 工厂 =====
// P0-2 修复: 配置缺失时返回 null 而非抛异常,让应用能启动到 Onboarding
// ===== 步骤 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 apiKey = configService.get<string>('llm.apiKey') ?? '';
const baseURL = configService.get<string>('llm.baseURL') ?? '';
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) {
@@ -187,7 +199,7 @@ async function initialize(): Promise<void> {
return null;
}
// v0.3.1: 读取 Provider 对应的 contextWindow 配置(Ollama 不使用此字段)
// 读取 Provider 对应的 contextWindow 配置(Ollama 不使用此字段)
const contextWindow = provider !== 'ollama'
? configService.get<number>(`${provider}.contextWindow`) ?? undefined
: undefined;
@@ -197,21 +209,21 @@ async function initialize(): Promise<void> {
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 会报错但 P0-1 修复后前端可见
// 配置未就绪时的 fallback adapter — getContextWindow 返回安全值,send/sendStream 会报错但前端可见
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=====
// ===== v0.2.0: 安全模块(必须在工具注册之前) =====
const policyEngine = new PolicyEngine();
const sandboxManager = new SandboxManager({
allowedPaths: [workspaceInfo.path],
@@ -221,7 +233,6 @@ async function initialize(): Promise<void> {
const outputValidator = new OutputValidator();
// v0.2.0: ConfirmationHook(提前创建,mainWindow 创建后再注入)
// 注入 ConfigService 以支持持久化自动执行设置
const confirmationHook = new ConfirmationHook(null, configService);
// ===== 步骤 6: 注册内置工具 =====
@@ -230,15 +241,9 @@ async function initialize(): Promise<void> {
toolRegistry.registerBuiltin(new WriteFileTool());
toolRegistry.registerBuiltin(new ListDirectoryTool());
toolRegistry.registerBuiltin(new SearchFilesTool());
// v0.3.2: 文件删除工具(破坏性操作,HIGH + requireConfirmation
toolRegistry.registerBuiltin(new DeleteFileTool());
// v0.3.3: 文件移动/重命名工具 + 文件信息查询工具
toolRegistry.registerBuiltin(new FileMoveTool());
toolRegistry.registerBuiltin(new FileInfoTool());
// v0.2.0: 新增文件工具
toolRegistry.registerBuiltin(new FileEditorTool());
toolRegistry.registerBuiltin(new CodeSearchTool());
toolRegistry.registerBuiltin(new DiffViewerTool());
@@ -256,91 +261,95 @@ async function initialize(): Promise<void> {
runCommandTool.setSandboxManager(sandboxManager);
toolRegistry.registerBuiltin(runCommandTool);
// v0.2.0: 任务管理工具
// P2v0.3.13: 注入 onTaskChanged 回调,工具写入后广播 IPC 事件给所有窗口
toolRegistry.registerBuiltin(new TaskManagerTool(() => db, (sessionId) => {
// 广播任务变更事件给所有窗口(UI 订阅后自动刷新)
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send('task:changed', sessionId);
}
}));
// 注册 Web Browser 统一浏览器工具
toolRegistry.registerBuiltin(new WebBrowserTool());
// v0.3.1: Git 工具集(4 个)
// v0.3.1: Git 工具集(4 个)+ 开发工具集(3 个)+ 独立工具(3 个)
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: 独立工具(3 个,v0.3.13 删除 todo_write 后)
toolRegistry.registerBuiltin(new HttpRequestTool());
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 注册后调用,确保包含所有工具的风险等级
// ===== HooksP0-2: SecurityScanHook 前置,对工具结果做间接注入防护) =====
const preToolHooks = [
new PermissionCheckHook(policyEngine),
new RateLimitHook(20),
confirmationHook,
];
const postToolHooks = [
new SecurityScanHook(promptDefender),
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
// ===== P2-10: Agent Engine Manager(每会话独立引擎,替代全局单引擎) =====
const buildAdapter = (): IMetonaProviderAdapter => createAdapter() ?? FALLBACK_ADAPTER;
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,
const agentEngineManager = new AgentEngineManager({
buildAdapter,
baseConfig: {
maxIterations: configService.get<number>('agent.maxIterations') ?? 20,
totalTimeoutMs: configService.get<number>('agent.totalTimeoutMs') ?? 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,
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,
},
adapter, toolRegistry, preToolHooks, postToolHooks,
);
agentLoop.setTools(toolRegistry.listTools());
agentLoop.setWorkspacePath(workspaceInfo.path);
toolRegistry,
preToolHooks,
postToolHooks,
});
agentEngineManager.setWorkspacePath(workspaceInfo.path);
// P1-7 修复: MCP 初始化在 agentLoop 创建后执行,完成后重新同步工具到 AgentLoop
// v0.3.18 修复: MCP 完成后广播 tools:ready 事件,前端据以启用发送按钮
// 之前 MCP 初始化是异步的,用户在 MCP 完成前发送消息会用到不全的工具集
// v0.3.18 修复: 维护 toolsReady 标志 + 暴露 tools:isReady 查询,解决竞态条件
// MCP initialize 几乎立即 resolveconnectServer 不 await),tools:ready 事件
// 可能在前端监听器注册前就发出,导致前端永久错过事件、输入框永久禁用
let toolsReady = false;
// ===== 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(() => {
agentLoop.setTools(toolRegistry.listTools());
log.info('[MCP] Tools registered and synced to AgentLoop');
toolsReady = true;
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 });
@@ -348,41 +357,45 @@ async function initialize(): Promise<void> {
}).catch((err) => {
log.warn('MCP Manager initialization error:', err);
// 即使 MCP 失败,内置工具已就绪,仍广播 ready 让前端启用发送
toolsReady = true;
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(adapter, workspaceService);
// v0.3.18 修复: 注入 MemoryManager使 consolidate 同步写入 semantic_memories 表
// 实现 DB 记忆与 MEMORY.md 双轨交叉,避免"AI 记得一半"的不一致
// ===== Memory Consolidator(会话结束 AI 提取重要记忆到 MEMORY.md =====
const memoryConsolidator = new MemoryConsolidator(buildAdapter(), workspaceService);
// v0.3.18: 注入 MemoryManager实现 DB 记忆与 MEMORY.md 双轨交叉
memoryConsolidator.setMemoryManager(memoryManager);
// ===== Task Orchestrator(子任务委派)=====
// ===== P2-11: 会话摘要分层上下文服务 =====
const sessionSummaryService = new SessionSummaryService(
() => db,
sessionService,
() => buildAdapter(),
);
// ===== Task Orchestrator(子任务委派,P2-10: 适配 EngineProvider =====
const orchestrator = new TaskOrchestrator(
agentLoop, toolRegistry, preToolHooks, postToolHooks,
agentEngineManager, toolRegistry, preToolHooks, postToolHooks,
{
thinkingEnabled: agentThinkingEnabled ?? true,
thinkingEffort: agentThinkingEffort ?? 'high',
thinkingEnabled: configService.get<boolean>('agent.enableThinking') ?? true,
thinkingEffort: (configService.get<string>('agent.thinkingEffort') as 'low' | 'medium' | 'high' | 'max' | null) ?? 'high',
contextLength: ollamaNumCtx ?? undefined,
// v0.3.1: 同步 contextWindow 到 SubAgent 配置
contextWindow: adapter.getContextWindow(),
contextWindow: buildAdapter().getContextWindow(),
},
);
toolRegistry.registerBuiltin(new DelegateTaskTool(orchestrator));
// 重新设置工具列表,包含新注册的 delegate_task
agentLoop.setTools(toolRegistry.listTools());
// v0.2.0: 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存
agentEngineManager.setToolsAll(toolRegistry.listTools());
// 在所有工具(包括 DelegateTaskTool)注册完成后,刷新 ConfirmationHook 的工具定义缓存
confirmationHook.setToolDefs(toolRegistry.listAllTools());
// v0.3.1: 所有工具(含 DelegateTaskTool)注册完成后输出总数,确保日志显示 26 而非 25
log.info(`Registered ${toolRegistry.size} built-in tools`);
// ===== 热重载 Adapter 回调(设置变更时触发)=====
// 记录上次创建 adapter 时的配置快照,用于检测配置是否真的变化
// ===== 热重载 Adapter 回调(设置变更时触发) =====
let lastProvider = configService.get<string>('llm.provider') ?? '';
// v0.3.1: configSig 加入 contextWindow 配置,使 contextWindow 变化也能触发热重载
// v0.3.1 + P1: configSig 覆盖主/备 Provider 全部 LLM 字段
const buildConfigSig = () => {
const provider = configService.get<string>('llm.provider') ?? '';
return JSON.stringify({
@@ -390,7 +403,10 @@ async function initialize(): Promise<void> {
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
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`),
@@ -399,13 +415,6 @@ async function initialize(): Promise<void> {
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,不发通知
@@ -413,48 +422,50 @@ async function initialize(): Promise<void> {
return true;
}
// 配置变化 — 建 adapter
const newAdapter = createAdapter();
if (!newAdapter) {
// 配置变化 — 校验新配置可构建 adapter
const probe = createAdapter();
if (!probe) {
log.warn('[CONFIG] Cannot create adapter: LLM config incomplete (provider/apiKey/baseURL/model)');
// 不更新 lastConfigSig,下次还会尝试
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
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;
}
agentLoop.setAdapter(newAdapter);
memoryConsolidator.setAdapter(newAdapter);
// P2-10: 重建所有引擎的 adapter(工厂闭包读取最新配置)
agentEngineManager.refreshAdapters();
agentEngineManager.setFallbackAdapter(buildFallbackAdapter());
memoryConsolidator.setAdapter(agentEngineManager.getAdapter());
// Provider 切换时同步 contextLength 和 contextWindow
const provider = currentConfig.provider;
const provider = configService.get<string>('llm.provider') ?? '';
if (provider === 'ollama') {
const numCtx = configService.get<number>('ollama.numCtx');
agentLoop.updateConfig({
agentEngineManager.updateConfigAll({
contextLength: numCtx ?? undefined,
// v0.3.1: 同步 contextWindowOllama 的 getContextWindow 返回固定 4096
contextWindow: newAdapter.getContextWindow(),
contextWindow: agentEngineManager.getAdapter().getContextWindow(),
});
} else {
// v0.3.1: DeepSeek/Agnes 无 contextLength,但同步 contextWindow
agentLoop.updateConfig({
agentEngineManager.updateConfigAll({
contextLength: undefined,
contextWindow: newAdapter.getContextWindow(),
contextWindow: agentEngineManager.getAdapter().getContextWindow(),
});
}
log.info(`[CONFIG] Adapter reloaded: provider=${provider}`);
// 仅在 Provider 真正变化时通知渲染进程
if (mainWindow && !mainWindow.isDestroyed()) {
if (lastProvider && lastProvider !== provider) {
mainWindow.webContents.send('agent:providerSwitched', {
if (lastProvider && lastProvider !== provider) {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send('agent:providerSwitched', {
from: lastProvider,
to: provider,
reason: 'config_changed',
sessionId: '',
});
mainWindow.webContents.send('toast:show', {
win.webContents.send('toast:show', {
type: 'success',
message: `Provider 已切换: ${lastProvider}${provider}`,
});
@@ -465,9 +476,8 @@ async function initialize(): Promise<void> {
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', {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send('toast:show', {
type: 'error',
message: `Provider 切换失败: ${(err as Error).message}`,
});
@@ -485,22 +495,30 @@ async function initialize(): Promise<void> {
// 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,
);
// v0.3.18 修复: 注册 tools:isReady 查询,前端注册 onReady 监听器后立即查询,
// 解决 tools:ready 事件在监听器注册前发出的竞态条件
ipcMain.handle('tools:isReady', () => ({ ready: toolsReady, toolCount: toolRegistry.size }));
registerAllIPCHandlers({
mainWindow: win,
sessionService,
configService,
workspaceService,
contextBuilder,
agentEngineManager,
toolRegistry,
auditService,
sessionRecorder,
memoryManager,
mcpManager,
reloadAdapter,
promptInjectionDefender: promptDefender,
outputValidator,
confirmationHook,
memoryConsolidator,
orchestrator,
sessionSummaryService,
toolsReadyRef,
});
},
});
// P2-11 修复: ConfirmationHook.setMainWindow 已在 beforeLoad 中调用,此处无需重复
// beforeLoad 在 loadURL 之前执行,确保 IPC handler 注册时 mainWindow 已注入)
// ===== 系统托盘 =====
const resourcesPath = join(__dirname, '../../assets');
trayManager = new TrayManager(resourcesPath);
@@ -509,8 +527,8 @@ async function initialize(): Promise<void> {
// ===== 全局快捷键 =====
windowManager.registerGlobalShortcuts();
// ===== Agent 状态同步到托盘 =====
agentLoop.on('stateChange', (data: { previous: string; current: string; state?: string }) => {
// ===== 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',
@@ -521,7 +539,7 @@ async function initialize(): Promise<void> {
});
// ===== Agent 完成时发送系统通知 =====
agentLoop.on('complete', (data: { sessionId: string; durationMs: number }) => {
agentEngineManager.on('complete', (data: { sessionId: string; durationMs: number }) => {
trayManager?.sendNotification(
'MetonaAI — 任务完成',
`Agent 已完成任务 (${(data.durationMs / 1000).toFixed(1)}s)`,
@@ -529,9 +547,38 @@ async function initialize(): Promise<void> {
);
});
// TODO: Initialize UpdateService for auto-update functionality
// const updateService = new UpdateService();
// updateService.initialize(mainWindow);
// ===== 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', () => {
@@ -547,11 +594,11 @@ async function initialize(): Promise<void> {
});
// 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();
clearInterval(healthTimer);
windowManager?.unregisterGlobalShortcuts();
trayManager?.destroy();
windowManager?.closeAll();
@@ -564,7 +611,6 @@ async function initialize(): Promise<void> {
event.preventDefault();
// v0.3.18 修复: 超时从 5 秒延长到 40 秒,以容纳 consolidate 的 35 秒等待
// 之前 5 秒超时会导致进行中的 consolidate 被 kill,记忆丢失
const shutdownTimeout = setTimeout(() => {
log.warn('[Shutdown] Timeout reached, forcing exit');
if (databaseService) { databaseService.close(); databaseService = null; }
@@ -580,7 +626,6 @@ async function initialize(): Promise<void> {
cleanupBrowser();
// v0.3.18 修复: 等待进行中的记忆固化任务完成,避免应用退出导致记忆丢失
// consolidate 内部有 30 秒 LLM 超时保护,此处等待 35 秒足够覆盖
if (memoryConsolidator.isRunning()) {
log.info('[Shutdown] Waiting for memory consolidation to complete...');
const completed = await memoryConsolidator.waitForCompletion(35_000);