/** * IPC Handlers — 所有 IPC 通道处理逻辑 * * 集成三层日志: * - TOOL 层:AuditService 记录工具调用到 audit_logs 表 * - TRACE 层:SessionRecorder 写入 session_*.jsonl * - AGENT 层:electron-log 记录状态转换 * * @see docs/MetonaAI-Desktop 架构与交互设计.html — IPC 架构 + 日志分层 */ import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron'; import type { SessionService } from '../services/session.service'; import type { ConfigService } from '../services/config.service'; import type { WorkspaceService } from '../services/workspace.service'; import type { ContextBuilder } from '../harness/prompts/context-builder'; import type { AgentLoopEngine } from '../harness/agent-loop'; import type { ToolRegistry } from '../harness/tools/registry'; import type { AuditService } from '../services/audit.service'; import type { SessionRecorder } from '../services/session-recorder.service'; import type { MemoryManager } from '../harness/memory/manager'; import type { MCPManager } from '../services/mcp-manager.service'; import type { MetonaMessage, MetonaStreamEvent } from '../harness/types'; import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types'; import type { MetonaError } from '../harness/types'; import log from 'electron-log'; /** * 注册所有 IPC 处理器 */ export function registerAllIPCHandlers( mainWindow: BrowserWindow, sessionService: SessionService, configService: ConfigService, workspaceService: WorkspaceService, contextBuilder: ContextBuilder, agentLoop: AgentLoopEngine, _toolRegistry: ToolRegistry, auditService: AuditService, sessionRecorder: SessionRecorder, memoryManager: MemoryManager, mcpManager: MCPManager, ): void { // ===== Agent 交互 ===== ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => { log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80)); // TRACE 层:开始录制 sessionRecorder.startRecording(sessionId); // TOOL 层:记录会话开始 auditService.logSessionStart(sessionId); // 保存用户消息到数据库 sessionService.saveMessage({ sessionId, role: 'user', content: userMessage.content, attachments: (userMessage as any).attachments, }); // 加载历史消息 const historyRows = sessionService.getMessages(sessionId); const history: MetonaMessage[] = historyRows .filter((m) => m.role !== 'system') .slice(0, -1) .map((m) => ({ role: m.role as MetonaMessage['role'], content: m.content, reasoningContent: m.reasoningContent, timestamp: m.timestamp, })); // 从工作空间文件构建 System Prompt const workspaceFiles = workspaceService.getFiles(); const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles); // 监听 Agent Loop 事件 const onStreamEvent = (event: MetonaStreamEvent) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('agent:streamEvent', event); } }; const onStateChange = (data: { previous?: string; current?: string; sessionId?: string; iteration?: number; state?: string }) => { // AGENT 层:记录状态转换 if (data.previous) log.info(`[AGENT] State: ${data.previous} → ${data.current}`); // 转发到渲染进程(携带迭代号) if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('agent:stateChange', data); } }; agentLoop.on('streamEvent', onStreamEvent); agentLoop.on('stateChange', onStateChange); try { // TRACE 层:记录上下文构建 sessionRecorder.recordContextBuilt({ tokenCount: history.reduce((sum, m) => sum + Math.ceil(m.content.length / 2), 0), usageRatio: 0, }); // 启动 Agent Loop const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt); // 保存 assistant 回复到数据库 sessionService.saveMessage({ sessionId, role: 'assistant', content: output.finalAnswer, }); // 更新 Token 统计 if (output.totalTokenUsage.totalTokens > 0) { sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens); } // 更新 MEMORY.md 时间戳 workspaceService.updateMemoryTimestamp(); // TOOL 层:记录会话结束 auditService.logSessionEnd({ sessionId, totalIterations: output.iterations.length, totalTokens: output.totalTokenUsage.totalTokens, durationMs: output.durationMs, terminationReason: output.terminationReason, }); // TRACE 层:停止录制 sessionRecorder.stopRecording({ totalIterations: output.iterations.length, totalTokens: output.totalTokenUsage.totalTokens, durationMs: output.durationMs, terminationReason: output.terminationReason, }); log.info(`[AGENT] Completed: ${output.terminationReason}, ${output.iterations.length} iterations, ${output.durationMs}ms`); return { success: true }; } catch (error) { log.error('[AGENT] Error:', error); // TOOL 层:记录错误 auditService.log({ sessionId, eventType: 'error', actor: 'agent', target: 'agent_loop', details: { error: (error as Error).message }, outcome: 'error', }); // TRACE 层:停止录制 sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error', }); // 发送错误事件到 UI if (!mainWindow.isDestroyed()) { const metonaError: MetonaError = { code: MetonaErrorCode.UNKNOWN, message: (error as Error).message, retryable: false, }; const errorEvent: MetonaStreamEvent = { type: MetonaStreamEventType.ERROR, requestId: '', sessionId, iteration: 0, seq: 0, timestamp: Date.now(), error: metonaError, }; mainWindow.webContents.send('agent:streamEvent', errorEvent); } return { success: false, error: (error as Error).message }; } finally { agentLoop.off('streamEvent', onStreamEvent); agentLoop.off('stateChange', onStateChange); } }); ipcMain.handle('agent:abortSession', async (_event, sessionId) => { log.info('[AGENT] Abort:', sessionId); agentLoop.abort(); // TOOL 层:记录中断 auditService.log({ sessionId, eventType: 'session_end', actor: 'user', target: 'session', details: { reason: 'user_abort' }, outcome: 'denied', }); return { success: true }; }); // ===== 会话管理 ===== ipcMain.handle('sessions:list', async () => { return sessionService.list(); }); ipcMain.handle('sessions:create', async (_event, title?: string) => { return sessionService.create(title); }); ipcMain.handle('sessions:rename', async (_event, sessionId, title) => { return { success: sessionService.rename(sessionId, title) }; }); ipcMain.handle('sessions:delete', async (_event, sessionId) => { return { success: sessionService.delete(sessionId) }; }); ipcMain.handle('sessions:getMessages', async (_event, sessionId) => { return sessionService.getMessages(sessionId); }); ipcMain.handle('sessions:pin', async (_event, sessionId, pinned) => { return { success: sessionService.pin(sessionId, pinned) }; }); ipcMain.handle('sessions:deleteMessage', async (_event, messageId) => { return { success: sessionService.deleteMessage(messageId) }; }); ipcMain.handle('sessions:clearMessages', async (_event, sessionId) => { return { success: sessionService.clearMessages(sessionId) }; }); ipcMain.handle('sessions:saveTrace', async (_event, sessionId, data) => { try { sessionService.saveTraceData(sessionId, data); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('sessions:getTrace', async (_event, sessionId) => { return sessionService.getTraceData(sessionId); }); // ===== MCP 管理 ===== ipcMain.handle('mcp:listServers', async () => { return mcpManager.getServerStates(); }); ipcMain.handle('mcp:addServer', async (_event, config: { name: string; transport: string; command?: string; args?: string[]; url?: string }) => { try { await mcpManager.addServer({ name: config.name, transport: config.transport as 'stdio' | 'sse', command: config.command, args: config.args, url: config.url, enabled: true, }); log.info(`MCP server added: ${config.name}`); return { success: true }; } catch (error) { return { success: false, error: (error instanceof Error ? error.message : String(error)) }; } }); ipcMain.handle('mcp:removeServer', async (_event, name: string) => { try { await mcpManager.removeServer(name); return { success: true }; } catch { return { success: false }; } }); ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => { try { await mcpManager.toggleServer(name, enabled); return { success: true }; } catch { return { success: false }; } }); // ===== 记忆 ===== ipcMain.handle('db:searchMemories', async (_event, query, options) => { return memoryManager.search(query, options); }); // ===== 配置 ===== ipcMain.handle('config:get', async (_event, key) => { return configService.get(key); }); ipcMain.handle('config:set', async (_event, key, value) => { configService.set(key, value); // TOOL 层:记录配置变更 auditService.log({ sessionId: '', eventType: 'config_change', actor: 'user', target: key, details: { value }, outcome: 'success', }); return { success: true }; }); // ===== 应用工具 ===== ipcMain.handle('app:getVersion', async () => { return app.getVersion(); }); ipcMain.handle('app:getAppDataPath', async () => { return app.getPath('userData'); }); ipcMain.handle('app:openExternal', async (_event, url) => { await shell.openExternal(url); }); ipcMain.handle('app:showItemInFolder', async (_event, path) => { shell.showItemInFolder(path); }); ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory', 'createDirectory'], defaultPath: defaultPath ?? app.getPath('home'), title: '选择工作空间目录', }); if (result.canceled || result.filePaths.length === 0) return { canceled: true, path: '' }; return { canceled: false, path: result.filePaths[0] }; }); // ===== 工具管理 ===== ipcMain.handle('tools:list', async () => { return _toolRegistry.listTools().map((t) => ({ name: t.name, description: t.description, category: t.category, riskLevel: t.riskLevel, requiresPermission: t.requiresPermission, })); }); ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => { // 工具开关通过配置持久化 configService.set(`tools.${toolName}.enabled`, enabled); log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`); return { success: true }; }); // ===== 数据管理 ===== ipcMain.handle('data:export', async (_event, sessionId?: string) => { try { const db = sessionService.getDB(); if (sessionId) { // 导出单个会话 const messages = sessionService.getMessages(sessionId); return { success: true, data: messages }; } // 导出所有会话 const sessions = sessionService.list(); const allData: Record = { sessions: [], config: configService.getAll() }; for (const session of sessions) { (allData.sessions as Array>).push({ ...session, messages: sessionService.getMessages(session.id), }); } return { success: true, data: allData }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('data:clearSessions', async () => { try { const db = sessionService.getDB(); db.exec('DELETE FROM messages'); db.exec('DELETE FROM sessions'); log.info('[DATA] All sessions cleared'); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('data:clearMemories', async () => { try { const db = sessionService.getDB(); db.exec('DELETE FROM episodic_memories'); db.exec('DELETE FROM semantic_memories'); db.exec('DELETE FROM working_memories'); log.info('[DATA] All memories cleared'); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('data:clearAuditLogs', async () => { try { // 审计日志是 INSERT-ONLY,需要先禁用触发器 const db = sessionService.getDB(); db.exec('DROP TRIGGER IF EXISTS audit_no_delete'); db.exec('DELETE FROM audit_logs'); db.exec(` CREATE TRIGGER audit_no_delete BEFORE DELETE ON audit_logs BEGIN SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY. Deletion is not allowed.'); END `); log.info('[DATA] Audit logs cleared'); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); log.info('[SYS] All IPC handlers registered'); }