/** * 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 { join } from 'path'; 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, AuditEventType } from '../services/audit.service'; import type { SessionRecorder } from '../services/session-recorder.service'; import type { MemoryManager, MemoryType } from '../harness/memory/manager'; import type { MCPManager } from '../services/mcp-manager.service'; import type { PromptInjectionDefender } from '../harness/security/prompt-injection-defense'; import type { OutputValidator } from '../harness/verification/output-validator'; import type { ConfirmationHook } from '../harness/hooks/confirmation-hook'; import type { MemoryConsolidator } from '../harness/memory/consolidator'; import type { TaskOrchestrator } from '../harness/orchestration/orchestrator'; import type { MetonaMessage, MetonaStreamEvent } from '../harness/types'; import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types'; import type { MetonaError } from '../harness/types'; import { estimateMessagesTokens } from '../harness/utils/token-estimator'; 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, reloadAdapter: () => boolean, promptInjectionDefender: PromptInjectionDefender, outputValidator: OutputValidator, confirmationHook: ConfirmationHook, memoryConsolidator: MemoryConsolidator, orchestrator: TaskOrchestrator, ): void { // ===== Agent 交互 ===== ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => { // M-33 修复: 参数校验,防止 undefined/非字符串导致下游异常 if (!sessionId || typeof sessionId !== 'string') { log.warn('[AGENT] sendMessage rejected: invalid sessionId'); return { success: false, error: 'Invalid sessionId' }; } if (!userMessage || typeof userMessage !== 'object' || typeof userMessage.content !== 'string') { log.warn('[AGENT] sendMessage rejected: invalid userMessage'); return { success: false, error: 'Invalid message format' }; } log.info('[AGENT] sendMessage:', sessionId, (userMessage.content ?? '').slice(0, 80)); // 发送消息前确保 Adapter 使用最新配置(失败则中止,防止用旧 Provider 的 adapter 发送) if (!reloadAdapter()) { const errorMsg = 'Adapter 加载失败,请检查 LLM 配置(Provider、API Key、Base URL、Model 是否完整)'; log.error('[AGENT]', errorMsg); if (!mainWindow.isDestroyed()) { const errorEvent: MetonaStreamEvent = { type: MetonaStreamEventType.ERROR, requestId: '', sessionId, iteration: 0, seq: 0, timestamp: Date.now(), error: { code: MetonaErrorCode.UNKNOWN, message: errorMsg, retryable: false }, }; mainWindow.webContents.send('agent:streamEvent', errorEvent); mainWindow.webContents.send('agent:streamEvent', { ...errorEvent, type: MetonaStreamEventType.DONE, }); } sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error' }); return { success: false, error: errorMsg }; } // TRACE 层:开始录制 sessionRecorder.startRecording(sessionId); // TOOL 层:记录会话开始 auditService.logSessionStart(sessionId); // 保存用户消息到数据库 // IPC boundary: frontend sends attachments not defined in the MetonaMessage type sessionService.saveMessage({ sessionId, role: 'user', content: userMessage.content, attachments: (userMessage as MetonaMessage & { attachments?: unknown[] }).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, toolCalls: m.toolCalls as MetonaMessage['toolCalls'], toolResult: m.toolResult as MetonaMessage['toolResult'], timestamp: m.timestamp, })); // 从工作空间文件构建 System Prompt(注入当前工作空间路径,便于 LLM 在需要绝对路径时使用) const workspaceFiles = workspaceService.getFiles(); const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles, workspaceService.getPath()); // 检索与用户消息相关的记忆,注入到 System Prompt 动态区 try { const memories = memoryManager.search(userMessage.content, { topK: 5, minImportance: 0.3 }); if (memories.length > 0) { const memorySection = memories.map((m, i) => `[${i + 1}] (${m.type}, 重要度: ${m.importance.toFixed(1)}) ${m.content.slice(0, 200)}`, ).join('\n'); const memoryBlock = `## Relevant Memories (Retrieved)\n${memorySection}`; systemPrompt.dynamicReminders = systemPrompt.dynamicReminders ? `${systemPrompt.dynamicReminders}\n\n---\n\n${memoryBlock}` : memoryBlock; log.debug(`[AGENT] Injected ${memories.length} memories into system prompt`); } } catch (err) { log.warn('[AGENT] Memory retrieval failed, proceeding without memories:', err); } // 监听 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; runId?: string }) => { // AGENT 层:记录状态转换 if (data.previous) log.info(`[AGENT] State: ${data.previous} → ${data.current}`); // 转发到渲染进程(携带迭代号) if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('agent:stateChange', data); } }; // M-6: 转发上下文压缩事件为 toast 通知 const onCompressed = (data: { iteration?: number; originalTokens?: number; compressedTokens?: number }) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('toast:show', { type: 'info', message: `上下文压缩: ${data.originalTokens ?? '?'} → ${data.compressedTokens ?? '?'} tokens`, }); } }; // v0.3.0: 转发死循环检测事件为 toast 警告 const onDeadLoop = (data: { iteration?: number; runId?: string; sessionId?: string }) => { log.warn(`[AGENT] Dead loop detected at iteration ${data.iteration ?? '?'}`); if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('toast:show', { type: 'warning', message: `检测到死循环(第 ${data.iteration ?? '?'} 轮):连续3轮重复相同工具调用,已自动终止`, }); } }; agentLoop.on('streamEvent', onStreamEvent); agentLoop.on('stateChange', onStateChange); agentLoop.on('compressed', onCompressed); agentLoop.on('deadLoop', onDeadLoop); try { // 提示注入检测(安全模块) const injectionResult = promptInjectionDefender.detect(userMessage.content); if (injectionResult.riskScore >= 7) { log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings); if (!mainWindow.isDestroyed()) { const metonaError: MetonaError = { code: MetonaErrorCode.UNKNOWN, message: `Message blocked by prompt injection defense: ${injectionResult.recommendation}`, 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); } // TRACE 层:停止录制 sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error', }); return { success: false, error: 'Message blocked by prompt injection defense' }; } if (injectionResult.riskScore >= 4) { log.warn('[PromptInjectionDefender] Suspicious patterns detected:', injectionResult.findings); } // TRACE 层:记录上下文构建 sessionRecorder.recordContextBuilt({ tokenCount: estimateMessagesTokens(history), usageRatio: 0, }); // 启动 Agent Loop const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt); // 输出验证(不阻塞响应,仅记录警告) // v0.3.0 修复: 传入 toolResults 和 context,启用事实一致性检查和幻觉检测 try { const toolResults = output.iterations .flatMap((step) => step.toolResults ?? []) .map((r) => (typeof r.result === 'string' ? r.result : JSON.stringify(r.result))); const context = [...history, { role: 'user', content: userMessage.content }] .map((m) => `${m.role}: ${m.content}`).join('\n'); const validation = await outputValidator.validate(output.finalAnswer, { toolResults: toolResults.length > 0 ? toolResults : undefined, context, }); if (!validation.valid || validation.issues.length > 0) { log.warn('[OutputValidator] Validation issues:', validation.issues); } log.debug(`[OutputValidator] Score: ${validation.score}, Valid: ${validation.valid}`); } catch (err) { log.error('[OutputValidator] Validation failed:', err); } // 保存每轮迭代的 assistant 消息到数据库(含思考内容和工具调用) for (const step of output.iterations) { if (!step.thought) continue; // 将 MetonaToolCall + MetonaToolResult 转换为前端 ToolCallInfo 格式 const toolCallsWithResults = step.toolCalls?.map((tc) => { const result = step.toolResults?.find((r) => r.toolCallId === tc.id); return { id: tc.id, name: tc.name, args: tc.args, status: result?.success ? 'success' as const : 'error' as const, result: result?.result, durationMs: result?.durationMs, error: result?.error, }; }); // 只有当有内容、思考内容或工具调用时才保存 if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) { // C-6 修复: assistant 消息仅有 tool_calls 时 content 必须为 null(而非空字符串) // @see project_memory.md — Assistant messages with tool_calls must set content to null // 空字符串会导致 LLM API 返回 400 错误,必须使用 null const assistantContent = (toolCallsWithResults?.length && !step.thought.content) ? null : step.thought.content; sessionService.saveMessage({ sessionId, role: 'assistant', content: assistantContent, reasoningContent: step.thought.reasoningContent || undefined, toolCalls: toolCallsWithResults, iteration: step.iteration, }); } // v0.3.0 修复: 保存 tool 结果消息到数据库 // DeepSeek/OpenAI API 要求 assistant 消息有 tool_calls 时,后续必须有对应的 tool 结果消息 // 缺失会导致第二次发消息时 API 返回 400 Bad Request if (step.toolResults) { for (const result of step.toolResults) { const resultContent = typeof result.result === 'string' ? result.result : JSON.stringify(result.result); sessionService.saveMessage({ sessionId, role: 'tool', content: result.error ?? resultContent, toolResult: result, iteration: step.iteration, }); } } } // 更新 Token 统计 if (output.totalTokenUsage.totalTokens > 0) { sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens); } // 更新 MEMORY.md 时间戳 workspaceService.updateMemoryTimestamp(); // 会话结束:AI 判断本次对话有哪些重要内容需要持久化到 MEMORY.md // 异步执行,不阻塞主流程返回;失败仅记录日志 memoryConsolidator .consolidate(userMessage.content, output.finalAnswer, output.iterations) .then((result) => { if (result.appended > 0) { log.info(`[AGENT] Memory consolidated: ${result.appended} entries appended to MEMORY.md`); // 通知渲染进程记忆已更新 if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('toast:show', { type: 'info', message: `AI 已将 ${result.appended} 条重要记忆写入 MEMORY.md`, }); } } }) .catch((err) => { log.warn('[AGENT] Memory consolidation failed:', err); }); // 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); agentLoop.off('compressed', onCompressed); agentLoop.off('deadLoop', onDeadLoop); } }); ipcMain.handle('agent:abortSession', async (_event, sessionId) => { log.info('[AGENT] Abort:', sessionId); agentLoop.abort(); // v0.3.0 修复: 清理所有等待中的工具确认,避免定时器泄漏和超时 toast 在新会话中弹出 confirmationHook.clearPending(); // 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); }); // M-34 修复: 统一 sessionId 校验辅助函数 const isValidSessionId = (id: unknown): id is string => typeof id === 'string' && id.length > 0 && id.length <= 200; ipcMain.handle('sessions:rename', async (_event, sessionId: unknown, title: unknown) => { // M-34 修复: 校验 sessionId 和 title if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; if (typeof title !== 'string' || !title.trim()) return { success: false, error: 'Invalid title' }; return { success: sessionService.rename(sessionId, title) }; }); ipcMain.handle('sessions:delete', async (_event, sessionId: unknown) => { // M-34 修复: 校验 sessionId if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; return { success: sessionService.delete(sessionId) }; }); ipcMain.handle('sessions:getMessages', async (_event, sessionId: unknown) => { // M-34 修复: 校验 sessionId if (!isValidSessionId(sessionId)) return []; return sessionService.getMessages(sessionId); }); ipcMain.handle('sessions:pin', async (_event, sessionId: unknown, pinned: unknown) => { // M-34 修复: 校验 sessionId 和 pinned if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; if (typeof pinned !== 'boolean') return { success: false, error: 'Invalid pinned flag' }; return { success: sessionService.pin(sessionId, pinned) }; }); ipcMain.handle('sessions:archive', async (_event, sessionId: unknown, archived: unknown) => { // M-34 修复: 校验 sessionId 和 archived if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; if (typeof archived !== 'boolean') return { success: false, error: 'Invalid archived flag' }; return { success: sessionService.archive(sessionId, archived) }; }); ipcMain.handle('sessions:deleteMessage', async (_event, messageId: unknown) => { // M-34 修复: 校验 messageId if (typeof messageId !== 'string' || !messageId) return { success: false, error: 'Invalid messageId' }; return { success: sessionService.deleteMessage(messageId) }; }); ipcMain.handle('sessions:clearMessages', async (_event, sessionId: unknown) => { // M-34 修复: 校验 sessionId if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; return { success: sessionService.clearMessages(sessionId) }; }); ipcMain.handle('sessions:saveTrace', async (_event, sessionId: unknown, data: unknown) => { // M-34 修复: 校验 sessionId 和 data if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; // 严格校验 data 结构:traceSteps 必须为数组,tokenUsage 必须存在 if (!data || typeof data !== 'object') return { success: false, error: 'Invalid trace data' }; const traceData = data as { traceSteps?: unknown; tokenUsage?: unknown }; if (!Array.isArray(traceData.traceSteps)) { return { success: false, error: 'Invalid trace data: traceSteps must be an array' }; } if (traceData.tokenUsage === undefined) { return { success: false, error: 'Invalid trace data: tokenUsage is required' }; } try { sessionService.saveTraceData(sessionId, { traceSteps: traceData.traceSteps, tokenUsage: traceData.tokenUsage, }); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('sessions:getTrace', async (_event, sessionId: unknown) => { // M-34 修复: 校验 sessionId if (!isValidSessionId(sessionId)) return null; 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 }) => { // M-38 修复: 完整参数校验,防止字段缺失或类型不符导致异常行为 if (!config || typeof config !== 'object') { return { success: false, error: 'Invalid config' }; } if (typeof config.name !== 'string' || !config.name.trim()) { return { success: false, error: 'Server name is required' }; } // M-5 修复: transport 运行时校验(替代 as 'stdio' | 'sse' 断言) if (config.transport !== 'stdio' && config.transport !== 'sse') { return { success: false, error: `Invalid transport: ${config.transport}. Must be 'stdio' or 'sse'` }; } // stdio 类型必须有 command if (config.transport === 'stdio' && (typeof config.command !== 'string' || !config.command.trim())) { return { success: false, error: 'command is required for stdio transport' }; } // sse 类型必须有合法 url if (config.transport === 'sse') { if (typeof config.url !== 'string' || !config.url.trim()) { return { success: false, error: 'url is required for sse transport' }; } try { new URL(config.url); } catch { return { success: false, error: 'Invalid url format' }; } } try { await mcpManager.addServer({ name: config.name, transport: config.transport, // 已校验,无需断言 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) => { // M-38 修复: name 校验 if (typeof name !== 'string' || !name.trim()) { return { success: false, error: 'Invalid server name' }; } try { await mcpManager.removeServer(name); return { success: true }; } catch (error) { return { success: false, error: (error instanceof Error ? error.message : String(error)) }; } }); ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => { // M-38 修复: name 和 enabled 校验 if (typeof name !== 'string' || !name.trim()) { return { success: false, error: 'Invalid server name' }; } if (typeof enabled !== 'boolean') { return { success: false, error: 'Invalid enabled flag' }; } try { await mcpManager.toggleServer(name, enabled); return { success: true }; } catch (error) { return { success: false, error: (error instanceof Error ? error.message : String(error)) }; } }); // ===== 记忆 ===== ipcMain.handle('db:searchMemories', async (_event, query: unknown, options: unknown) => { // M-37 修复: query 和 options 校验 if (typeof query !== 'string' || !query.trim()) { return []; } // 构造合法的搜索选项(仅保留已知字段,强制类型安全) const VALID_MEMORY_TYPES: readonly MemoryType[] = ['episodic', 'semantic', 'working']; const searchOptions: { topK?: number; sessionId?: string; type?: MemoryType; minImportance?: number } = {}; if (options && typeof options === 'object') { const opts = options as Record; // topK 限制范围 1-100(防止过大查询) if (opts.topK !== undefined) { const topK = Number(opts.topK); if (Number.isFinite(topK) && topK >= 1 && topK <= 100) { searchOptions.topK = topK; } else { searchOptions.topK = 10; // 默认值 } } if (typeof opts.sessionId === 'string') searchOptions.sessionId = opts.sessionId; // type 必须是合法的 MemoryType 枚举值 if (typeof opts.type === 'string' && (VALID_MEMORY_TYPES as readonly string[]).includes(opts.type)) { searchOptions.type = opts.type as MemoryType; } if (typeof opts.minImportance === 'number' && Number.isFinite(opts.minImportance)) { searchOptions.minImportance = opts.minImportance; } } return memoryManager.search(query, searchOptions); }); // ===== 配置 ===== ipcMain.handle('config:get', async (_event, key) => { return configService.get(key); }); ipcMain.handle('config:set', async (_event, key: unknown, value: unknown) => { // M-42 修复: 参数校验 + 敏感数据脱敏 if (typeof key !== 'string' || !key.trim()) { return { success: false, error: 'Invalid config key' }; } // value 必须是可序列化的基本类型(string|number|boolean|null) if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { return { success: false, error: 'Invalid config value: must be string, number, boolean, or null' }; } // C-1 修复: Provider 切换时清空 API key,防止使用不兼容的 key 导致 401 错误 // @see project_memory.md — When switching LLM providers, the API key must be cleared if (key === 'llm.provider') { const oldProvider = configService.get('llm.provider') ?? ''; const newProvider = (value as string) ?? ''; if (oldProvider && newProvider && oldProvider !== newProvider) { configService.set('llm.apiKey', ''); log.info(`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`); } } configService.set(key, value); // M-42 修复: 敏感配置项脱敏后再写入审计日志,防止 API Key 明文泄露 // 敏感 key 包括 llm.apiKey、llm.fallbackApiKey、agnes.apiKey 等 // 审计补充修复: 短值(length <= 4)时 slice(-4) 会返回整个 value 导致脱敏失效, // 改为:长值保留后 4 位核对,短值完全掩码为 '***' const SENSITIVE_KEY_PATTERNS = ['apikey', 'api_key', 'apitoken', 'token', 'secret', 'password']; const isSensitiveKey = SENSITIVE_KEY_PATTERNS.some(p => key.toLowerCase().includes(p)); const auditValue = isSensitiveKey && typeof value === 'string' && value.length > 0 ? (value.length > 4 ? '***' + value.slice(-4) : '***') : value; // TOOL 层:记录配置变更 auditService.log({ sessionId: '', eventType: 'config_change', actor: 'user', target: key, details: { value: auditValue }, outcome: 'success', }); // LLM 相关配置变更时热重载 Adapter // v0.3.1: 加入 deepseek.contextWindow / agnes.contextWindow,使上下文窗口配置变化也触发热重载 if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx', 'deepseek.contextWindow', 'agnes.contextWindow'].includes(key)) { // C-1 修复: reloadAdapter 返回 false 时表示加载失败,需要通知前端 const reloadSuccess = reloadAdapter(); if (!reloadSuccess) { log.warn(`[CONFIG] Adapter reload failed after ${key} change`); } else { log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`); } } // Agent 配置变更时热更新 Engine if (key === 'agent.maxIterations') { agentLoop.updateConfig({ maxIterations: value as number }); log.info(`[CONFIG] Agent maxIterations updated to ${value}`); } else if (key === 'agent.totalTimeoutMs') { agentLoop.updateConfig({ totalTimeoutMs: value as number }); log.info(`[CONFIG] Agent totalTimeoutMs updated to ${value}`); } else if (key === 'agent.enableThinking') { agentLoop.updateConfig({ thinkingEnabled: value as boolean }); // L-18 修复: 同步更新 orchestrator.defaultConfig,确保新建 SubAgent 使用最新配置 orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean }); log.info(`[CONFIG] Agent thinkingEnabled updated to ${value}`); } else if (key === 'agent.thinkingEffort') { agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' }); orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' }); log.info(`[CONFIG] Agent thinkingEffort updated to ${value}`); } else if (key === 'ollama.numCtx') { // numCtx 同时更新 Engine 的 contextLength agentLoop.updateConfig({ contextLength: (value as number) || undefined }); orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined }); log.info(`[CONFIG] Agent contextLength updated to ${value}`); } else if (key === 'deepseek.contextWindow' || key === 'agnes.contextWindow') { // v0.3.1: DeepSeek/Agnes contextWindow 变更,同步到 Engine 和 Orchestrator // reloadAdapter 已重建 adapter,此处确保 Engine contextWindow 同步(兜底) const ctxWindow = (value as number) || undefined; agentLoop.updateConfig({ contextWindow: ctxWindow }); orchestrator.updateDefaultConfig({ contextWindow: ctxWindow }); log.info(`[CONFIG] ${key} updated to ${value}`); } else if (key === 'agent.confirmationTimeoutMs') { // 工具确认超时变更,即时应用到 ConfirmationHook confirmationHook.setConfirmationTimeout(value as number); log.info(`[CONFIG] Confirmation timeout updated to ${value}ms`); } else if (key === 'agent.toolExecutionTimeoutMs') { // 工具执行兜底超时变更,即时应用到 Engine agentLoop.updateConfig({ toolExecutionTimeoutMs: value as number }); log.info(`[CONFIG] Tool execution timeout updated to ${value}ms`); } // 日志级别变更时即时应用 if (key === 'logging.level') { const level = value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly'; log.transports.file.level = level; log.transports.console.level = level; log.info(`[CONFIG] Log level updated to ${level}`); } // 工作空间路径变更时写入独立文件(下次启动生效) if (key === 'workspace.path') { try { const { writeWorkspacePathToFile } = await import('../main'); if (value) writeWorkspacePathToFile(value as string); log.info(`[CONFIG] Workspace path saved (restart required): ${value}`); } catch (err) { log.error('[CONFIG] Failed to save workspace path:', err); } } 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: unknown) => { // M-12 修复: URL 协议白名单校验,防止打开 file:///smb:// 等危险协议 // @see main.ts setWindowOpenHandler — 两处安全策略保持一致 if (typeof url !== 'string' || !url) { return { success: false, error: 'Invalid URL' }; } try { const parsed = new URL(url); const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:']; if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) { log.warn(`[IPC] openExternal blocked: protocol "${parsed.protocol}" not in whitelist`); return { success: false, error: `Protocol not allowed: ${parsed.protocol}` }; } await shell.openExternal(url); return { success: true }; } catch (error) { log.warn('[IPC] openExternal failed:', (error as Error).message); return { success: false, error: (error as Error).message }; } }); ipcMain.handle('app:showItemInFolder', async (_event, path: unknown) => { // M-36 修复: path 类型校验,防止非字符串参数 if (typeof path !== 'string' || !path) { return { success: false, error: 'Invalid path' }; } try { shell.showItemInFolder(path); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); 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('app:restart', async () => { log.info('[APP] Restart requested'); // 延迟 200ms 让 IPC 响应先返回 setTimeout(() => { app.relaunch(); app.exit(0); }, 200); return { success: true }; }); // ===== 工作空间校验与继承 ===== // 校验工作空间路径:检测路径有效性 + 4 个必需文件状态 ipcMain.handle('workspace:check', async (_event, targetPath: string) => { if (!targetPath || typeof targetPath !== 'string') { return { valid: false, reason: '路径不能为空' }; } const { existsSync, statSync } = await import('fs'); const { resolve } = await import('path'); const resolvedPath = resolve(targetPath); // 校验 1: 路径是否存在 if (!existsSync(resolvedPath)) { return { valid: true, path: resolvedPath, exists: false, missingFiles: ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md'], isNewWorkspace: true, reason: '目录不存在,将在切换后自动创建', }; } // 校验 2: 是否为目录 try { const stat = statSync(resolvedPath); if (!stat.isDirectory()) { return { valid: false, reason: '路径不是目录' }; } } catch { return { valid: false, reason: '无法访问路径' }; } // 校验 3: 检测 4 个必需文件状态 const requiredFiles = ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md']; const missingFiles: string[] = []; for (const f of requiredFiles) { if (!existsSync(join(resolvedPath, f))) missingFiles.push(f); } return { valid: true, path: resolvedPath, exists: true, missingFiles, isNewWorkspace: missingFiles.length === requiredFiles.length, }; }); // 从旧工作空间继承文件到新工作空间 ipcMain.handle('workspace:inheritFiles', async (_event, params: { targetPath: string; sourcePath: string; files: string[]; }) => { const { targetPath, sourcePath, files } = params; if (!targetPath || !sourcePath || !Array.isArray(files)) { return { success: false, error: '参数无效' }; } const { existsSync, mkdirSync, copyFileSync } = await import('fs'); const { resolve } = await import('path'); const resolvedTarget = resolve(targetPath); const resolvedSource = resolve(sourcePath); // 确保目标目录存在 if (!existsSync(resolvedTarget)) { mkdirSync(resolvedTarget, { recursive: true }); } const inherited: string[] = []; const failed: Array<{ file: string; error: string }> = []; for (const fileName of files) { // 仅允许继承白名单文件(防止路径遍历) if (!['SOUL.md', 'AGENTS.md', 'USERS.md'].includes(fileName)) { failed.push({ file: fileName, error: '不在继承白名单中' }); continue; } const srcFile = join(resolvedSource, fileName); const dstFile = join(resolvedTarget, fileName); try { if (existsSync(srcFile)) { copyFileSync(srcFile, dstFile); inherited.push(fileName); log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource} → ${resolvedTarget}`); } else { failed.push({ file: fileName, error: '源文件不存在' }); } } catch (err) { failed.push({ file: fileName, error: (err as Error).message }); } } return { success: true, inherited, failed }; }); // 获取当前工作空间详情:路径 + 4 个核心文件状态 + 自动目录状态 ipcMain.handle('workspace:getInfo', async () => { const { existsSync, statSync, readFileSync, readdirSync } = await import('fs'); const workspacePath = workspaceService.getPath(); const files = workspaceService.reload(); // 同步外部可能的手动修改 const REQUIRED = ['SOUL.md', 'AGENTS.md', 'MEMORY.md', 'USERS.md'] as const; const AUTO_DIRS = ['logs', 'traces', '.metona'] as const; const fileInfos = REQUIRED.map((name) => { const filePath = join(workspacePath, name); let exists = false; let size = 0; let mtime = 0; let preview = ''; try { if (existsSync(filePath)) { const stat = statSync(filePath); exists = true; size = stat.size; mtime = stat.mtimeMs; // 截取前 500 字符作为预览 const content = readFileSync(filePath, 'utf-8'); preview = content.length > 500 ? content.slice(0, 500) + '\n...(已截断)' : content; } } catch { // ignore } // 文件内容映射:SOUL.md → files.soul, AGENTS.md → files.agents 等 const contentKey = name.toLowerCase().replace('.md', '') as 'soul' | 'agents' | 'memory' | 'users'; return { name, path: filePath, exists, size, mtime, preview: exists ? preview : (files[contentKey] ?? ''), }; }); const dirInfos = AUTO_DIRS.map((name) => { const dirPath = join(workspacePath, name); let exists = false; let fileCount = 0; try { if (existsSync(dirPath)) { exists = true; const stat = statSync(dirPath); if (stat.isDirectory()) { fileCount = readdirSync(dirPath).length; } } } catch { // ignore } return { name, path: dirPath, exists, fileCount }; }); return { path: workspacePath, files: fileInfos, dirs: dirInfos, }; }); // ===== 工具管理 ===== ipcMain.handle('tools:list', async () => { return toolRegistry.listAllTools().map((t) => ({ name: t.name, description: t.description, category: t.category, riskLevel: t.riskLevel, requiresPermission: t.requiresPermission, enabled: t.enabled, })); }); ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => { // M-35 修复: 校验 toolName 合法性,防止配置 key 污染 if (typeof toolName !== 'string' || !toolName || typeof enabled !== 'boolean') { return { success: false, error: 'Invalid parameters' }; } // 校验 toolName 在已注册工具列表中(防止写入任意配置 key) const allTools = toolRegistry.listAllTools(); if (!allTools.some((t) => t.name === toolName)) { log.warn(`[IPC] tools:toggle rejected: unknown tool "${toolName}"`); return { success: false, error: `Unknown tool: ${toolName}` }; } // 工具开关通过配置持久化 configService.set(`tools.${toolName}.enabled`, enabled); // 同步到 ToolRegistry(立即生效) toolRegistry.setToolEnabled(toolName, enabled); // 同步到 AgentLoop 的工具列表 agentLoop.setTools(toolRegistry.listTools()); log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`); return { success: true }; }); // ===== 数据管理 ===== ipcMain.handle('data:export', async (_event, sessionId?: string) => { try { 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 () => { // M-40 修复: 危险操作审计日志,便于追踪异常调用 log.warn('[DATA] DANGER: clearSessions invoked — all sessions and messages will be deleted'); const db = sessionService.getDB(); try { db.exec('BEGIN'); const msgCount = db.prepare('SELECT COUNT(*) as c FROM messages').get() as { c: number }; const sessCount = db.prepare('SELECT COUNT(*) as c FROM sessions').get() as { c: number }; db.exec('DELETE FROM messages'); db.exec('DELETE FROM sessions'); db.exec('COMMIT'); log.info(`[DATA] All sessions cleared: ${sessCount.c} sessions, ${msgCount.c} messages deleted`); return { success: true, deletedSessions: sessCount.c, deletedMessages: msgCount.c }; } catch (error) { try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ } log.error('[DATA] clearSessions failed:', (error as Error).message); return { success: false, error: (error as Error).message }; } }); ipcMain.handle('data:clearMemories', async () => { // M-40 修复: 危险操作审计日志,便于追踪异常调用 log.warn('[DATA] DANGER: clearMemories invoked — all memories will be deleted'); const db = sessionService.getDB(); try { // M-41 修复: 三个 DELETE 操作用事务包裹,防止部分失败导致三类记忆数据不一致 const epiCount = db.prepare('SELECT COUNT(*) as c FROM episodic_memories').get() as { c: number }; const semCount = db.prepare('SELECT COUNT(*) as c FROM semantic_memories').get() as { c: number }; const workCount = db.prepare('SELECT COUNT(*) as c FROM working_memories').get() as { c: number }; db.exec('BEGIN'); db.exec('DELETE FROM episodic_memories'); db.exec('DELETE FROM semantic_memories'); db.exec('DELETE FROM working_memories'); db.exec('COMMIT'); log.info(`[DATA] All memories cleared: ${epiCount.c} episodic, ${semCount.c} semantic, ${workCount.c} working memories deleted`); return { success: true, deletedEpisodic: epiCount.c, deletedSemantic: semCount.c, deletedWorking: workCount.c }; } catch (error) { // M-41 修复: 失败时回滚事务,确保数据一致性 try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ } log.error('[DATA] clearMemories failed:', (error as Error).message); return { success: false, error: (error as Error).message }; } }); ipcMain.handle('data:clearAuditLogs', async () => { // 审计日志是 INSERT-ONLY,需要先禁用触发器 const db = sessionService.getDB(); try { db.exec('BEGIN'); 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 `); db.exec('COMMIT'); log.info('[DATA] Audit logs cleared'); return { success: true }; } catch (error) { try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ } return { success: false, error: (error as Error).message }; } }); // ===== SearXNG ===== ipcMain.handle('searxng:testConnection', async (_event, url: string, authKey: string, authType: string) => { try { if (!url || !/^https?:\/\//.test(url)) { return { success: false, error: 'URL 需以 http:// 或 https:// 开头' }; } const startTime = Date.now(); const headers: Record = {}; if (authKey) { if (authType === 'bearer') { headers['Authorization'] = `Bearer ${authKey}`; } else if (authType === 'basic') { headers['Authorization'] = `Basic ${Buffer.from(authKey).toString('base64')}`; } } const testUrl = `${url.replace(/\/$/, '')}/search?q=test&format=json&pageno=1`; const response = await fetch(testUrl, { headers, signal: AbortSignal.timeout(10_000), }); const latencyMs = Date.now() - startTime; if (response.ok) { return { success: true, statusCode: response.status, latencyMs }; } return { success: false, statusCode: response.status, latencyMs, error: `HTTP ${response.status} ${response.statusText}` }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // ===== v0.2.0: 工具确认响应(ConfirmationDialog → 主进程)===== // 使用 ipcMain.on(渲染进程通过 ipcRenderer.send 发送) ipcMain.on('tool:confirmationResponse', (_event, data: unknown) => { // M-43 修复: 校验 data 结构,防止 undefined/null 导致 TypeError if (!data || typeof data !== 'object') { log.warn('[IPC] tool:confirmationResponse rejected: invalid data'); return; } const req = data as { toolCallId?: unknown; approved?: unknown; remember?: unknown; autoExecute?: unknown }; if (typeof req.toolCallId !== 'string' || !req.toolCallId) { log.warn('[IPC] tool:confirmationResponse rejected: invalid toolCallId'); return; } if (typeof req.approved !== 'boolean') { log.warn('[IPC] tool:confirmationResponse rejected: invalid approved'); return; } const remember = typeof req.remember === 'boolean' ? req.remember : false; const autoExecute = typeof req.autoExecute === 'boolean' ? req.autoExecute : false; confirmationHook.resolveConfirmation(req.toolCallId, req.approved, remember, autoExecute); log.info(`[CONFIRM] Tool ${req.toolCallId} ${req.approved ? 'approved' : 'denied'}${remember ? ' (remembered)' : ''}${autoExecute ? ' (autoExecute)' : ''}`); }); // ===== v0.2.0: 持久化自动执行设置 ===== ipcMain.handle('tool:setAutoExecute', async (_event, toolName: unknown, enabled: unknown) => { // M-44 修复: 校验 toolName 合法性和 enabled 类型,防止配置 key 污染 if (typeof toolName !== 'string' || !toolName) { return { success: false, error: 'Invalid toolName' }; } if (typeof enabled !== 'boolean') { return { success: false, error: 'Invalid enabled (must be boolean)' }; } // 校验 toolName 在已注册工具列表中(与 tools:toggle 保持一致) const allTools = toolRegistry.listAllTools(); if (!allTools.some((t) => t.name === toolName)) { log.warn(`[IPC] tool:setAutoExecute rejected: unknown tool "${toolName}"`); return { success: false, error: `Unknown tool: ${toolName}` }; } try { confirmationHook.setAutoExecute(toolName, enabled); log.info(`[CONFIRM] Tool ${toolName} autoExecute set to ${enabled}`); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('tool:getAutoExecuteList', async () => { return { success: true, data: confirmationHook.getAutoExecuteList() }; }); // ===== v0.2.0: 审计日志链式哈希验证 ===== ipcMain.handle('audit:verifyChain', async () => { try { const result = auditService.verifyChain(); log.info(`[AUDIT] Chain verification: ${result.valid ? 'valid' : 'TAMPERED'} (${result.verifiedRecords}/${result.totalRecords})`); return { success: true, ...result }; } catch (error) { log.error('[AUDIT] Chain verification failed:', error); return { success: false, error: (error as Error).message }; } }); ipcMain.handle('audit:query', async (_event, filters?: unknown) => { // M-45 修复: 校验 filters 参数类型和范围 const VALID_AUDIT_EVENT_TYPES: readonly AuditEventType[] = [ 'tool_call', 'permission_check', 'error', 'llm_request', 'llm_response', 'session_start', 'session_end', 'config_change', ]; let safeFilters: { sessionId?: string; eventType?: AuditEventType; limit?: number } = {}; if (filters && typeof filters === 'object') { const f = filters as Record; if (typeof f.sessionId === 'string' && f.sessionId) safeFilters.sessionId = f.sessionId; if (typeof f.eventType === 'string' && f.eventType) { // 校验 eventType 是否在合法枚举内 if (!(VALID_AUDIT_EVENT_TYPES as readonly string[]).includes(f.eventType)) { return { success: false, error: `Invalid eventType (must be one of: ${VALID_AUDIT_EVENT_TYPES.join(', ')})` }; } safeFilters.eventType = f.eventType as AuditEventType; } if (f.limit !== undefined) { const limit = Number(f.limit); // 审计补充修复: limit 越界时返回 error(与 memory:listAll 策略一致,原为静默降级为 100) // 限制 1-1000 范围,防止过大查询拖慢性能 if (!Number.isFinite(limit) || limit < 1 || limit > 1000) { return { success: false, error: 'Invalid limit (must be 1-1000)' }; } safeFilters.limit = Math.floor(limit); } } try { return { success: true, data: auditService.query(safeFilters) }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // ===== v0.2.0: 任务管理 IPC(TaskList UI)===== const VALID_TASK_PRIORITIES: readonly string[] = ['low', 'medium', 'high', 'critical']; const VALID_TASK_STATUSES: readonly string[] = ['pending', 'in_progress', 'completed', 'blocked', 'cancelled']; ipcMain.handle('tasks:list', async (_event, sessionId?: unknown) => { // M-46 修复: 校验 sessionId 类型(可选参数) if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId)) { return { success: false, error: 'Invalid sessionId' }; } const db = sessionService.getDB(); let sql = 'SELECT * FROM tasks'; const params: unknown[] = []; if (sessionId) { sql += ' WHERE session_id = ?'; params.push(sessionId); } sql += ' ORDER BY order_idx ASC, created_at ASC'; return { success: true, data: db.prepare(sql).all(...params) }; }); ipcMain.handle('tasks:create', async (_event, data: unknown) => { // M-46 修复: 校验 data 结构和字段类型/枚举 if (!data || typeof data !== 'object') { return { success: false, error: 'Invalid task data' }; } const req = data as Record; if (typeof req.sessionId !== 'string' || !req.sessionId.trim()) { return { success: false, error: 'Invalid sessionId' }; } if (typeof req.title !== 'string' || !req.title.trim()) { return { success: false, error: 'Invalid title' }; } if (req.priority !== undefined && !VALID_TASK_PRIORITIES.includes(req.priority as string)) { return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` }; } if (req.parentId !== undefined && req.parentId !== null && typeof req.parentId !== 'string') { return { success: false, error: 'Invalid parentId' }; } const db = sessionService.getDB(); const id = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; try { db.prepare(` INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?) `).run( id, req.sessionId, req.title, typeof req.description === 'string' ? req.description : '', (req.priority as string) ?? 'medium', (req.parentId as string | null) ?? null, Date.now(), Date.now(), ); return { success: true, id }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown) => { // M-47 修复: 校验 id 和 updates 结构/枚举 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } if (!updates || typeof updates !== 'object') { return { success: false, error: 'Invalid updates' }; } const u = updates as Record; if (u.status !== undefined && !VALID_TASK_STATUSES.includes(u.status as string)) { return { success: false, error: `Invalid status (must be one of: ${VALID_TASK_STATUSES.join(', ')})` }; } if (u.priority !== undefined && !VALID_TASK_PRIORITIES.includes(u.priority as string)) { return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` }; } if (u.assignedTo !== undefined && u.assignedTo !== null && typeof u.assignedTo !== 'string') { return { success: false, error: 'Invalid assignedTo' }; } // 审计补充修复: 补全 title/description 类型校验(与 tasks:create 的严格校验一致) if (u.title !== undefined && typeof u.title !== 'string') { return { success: false, error: 'Invalid title (must be string)' }; } if (u.description !== undefined && typeof u.description !== 'string') { return { success: false, error: 'Invalid description (must be string)' }; } const db = sessionService.getDB(); try { const fields: string[] = []; const values: unknown[] = []; if (u.title !== undefined) { fields.push('title = ?'); values.push(u.title); } if (u.description !== undefined) { fields.push('description = ?'); values.push(u.description); } if (u.status !== undefined) { fields.push('status = ?'); values.push(u.status); } if (u.priority !== undefined) { fields.push('priority = ?'); values.push(u.priority); } if (u.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(u.assignedTo); } if (fields.length === 0) return { success: true }; fields.push('updated_at = ?'); values.push(Date.now()); if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); } values.push(id); db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('tasks:delete', async (_event, id: unknown) => { // M-48 修复: 校验 id 类型 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } const db = sessionService.getDB(); try { db.prepare('DELETE FROM tasks WHERE id = ?').run(id); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // ===== v0.2.0: 记忆系统增强查询(Memory Viewer UI)===== const VALID_MEMORY_TYPES_LIST: readonly string[] = ['episodic', 'semantic', 'working']; ipcMain.handle('memory:listAll', async (_event, options?: unknown) => { // M-49 修复: 校验 options.type 枚举和 limit 范围 let limit = 100; let type: string | undefined; if (options && typeof options === 'object') { const opts = options as Record; if (opts.type !== undefined) { if (typeof opts.type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(opts.type)) { return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` }; } type = opts.type; } if (opts.limit !== undefined) { const num = Number(opts.limit); // 限制 1-1000 范围,SQLite 中 LIMIT -1 表示无限制,需阻止 if (!Number.isFinite(num) || num < 1 || num > 1000) { return { success: false, error: 'Invalid limit (must be 1-1000)' }; } limit = Math.floor(num); } } const db = sessionService.getDB(); const results: Record = {}; try { if (!type || type === 'episodic') { const rows = db.prepare('SELECT * FROM episodic_memories ORDER BY created_at DESC LIMIT ?').all(limit) as Array>; results.episodic = rows.map((r) => ({ ...r, type: 'episodic', content: r.content ?? '' })); } if (!type || type === 'semantic') { const rows = db.prepare('SELECT * FROM semantic_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array>; results.semantic = rows.map((r) => ({ ...r, type: 'semantic', content: r.value ?? r.key ?? '', importance: r.confidence ?? 0, created_at: r.created_at ?? r.updated_at })); } if (!type || type === 'working') { const rows = db.prepare('SELECT * FROM working_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array>; results.working = rows.map((r) => ({ ...r, type: 'working', content: r.value ?? r.key ?? '', importance: 0.5, created_at: r.updated_at ?? Date.now() })); } return { success: true, data: results }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('memory:delete', async (_event, type: unknown, id: unknown) => { // M-50 修复: 校验 type 枚举(防止三元表达式默认映射到 working_memories)和 id 类型 if (typeof type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(type)) { return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` }; } if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid memory id' }; } const db = sessionService.getDB(); try { const table = type === 'episodic' ? 'episodic_memories' : type === 'semantic' ? 'semantic_memories' : 'working_memories'; db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); log.info('[SYS] All IPC handlers registered'); }