/** * IPC Tool Handlers — 工具与确认域(P2-9 从 handlers.ts 拆分) * * 工具列表/开关、工具就绪查询(P2-9: 从 main.ts beforeLoad 迁入)、 * 工具确认(单条/批量/拉取 pending)、自动执行设置。 */ import { ipcMain } from 'electron'; import type { IPCContext } from './context'; import log from 'electron-log'; export function registerToolHandlers(ctx: IPCContext): void { const { toolRegistry, confirmationHook, agentEngineManager } = ctx; 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}` }; } // 工具开关通过配置持久化 ctx.configService.set(`tools.${toolName}.enabled`, enabled); // 同步到 ToolRegistry(立即生效) toolRegistry.setToolEnabled(toolName, enabled); // 同步到所有会话引擎的工具列表 agentEngineManager.setToolsAll(toolRegistry.listTools()); log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`); return { success: true }; }); // ===== 工具就绪查询(P2-9: 从 main.ts beforeLoad 迁入,解决事件竞态) ===== ipcMain.handle('tools:isReady', () => ({ ready: ctx.toolsReadyRef.ready, toolCount: ctx.toolsReadyRef.toolCount, })); // ===== v0.2.0: 工具确认响应(ConfirmationDialog → 主进程) ===== 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.3.2: 批量工具确认响应(并行工具调用一次性审批) ===== ipcMain.on('tool:confirmationResponseBatch', (_event, data: unknown) => { if (!data || typeof data !== 'object') { log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid data'); return; } const req = data as { toolCallIds?: unknown; approved?: unknown; remember?: unknown; autoExecute?: unknown; }; // 严格校验 toolCallIds 数组 if (!Array.isArray(req.toolCallIds) || req.toolCallIds.length === 0) { log.warn( '[IPC] tool:confirmationResponseBatch rejected: toolCallIds must be non-empty array', ); return; } // 每个元素必须是字符串 for (const id of req.toolCallIds) { if (typeof id !== 'string' || !id) { log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid toolCallId in array'); return; } } if (typeof req.approved !== 'boolean') { log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid approved'); return; } const remember = typeof req.remember === 'boolean' ? req.remember : false; const autoExecute = typeof req.autoExecute === 'boolean' ? req.autoExecute : false; const resolved = confirmationHook.resolveConfirmationsBatch( req.toolCallIds as string[], req.approved, remember, autoExecute, ); log.info( `[CONFIRM] Batch ${req.approved ? 'approved' : 'denied'}: ${resolved.length}/${req.toolCallIds.length} resolved${remember ? ' (remembered)' : ''}${autoExecute ? ' (autoExecute)' : ''}`, ); }); // ===== v0.3.2: 拉取当前所有 pending 确认 ===== ipcMain.handle('tool:getPendingConfirmations', async () => { return { success: true, data: confirmationHook.getPendingConfirmations() }; }); // ===== v0.4.1: 会话内拒绝记忆管理(拒绝记忆带 TTL,支持手动恢复询问) ===== // v0.5.0: sessionId 可选参数 — 拒绝记忆按会话隔离,前端应传当前会话 ID ipcMain.handle('tool:getRememberedDenials', async (_event, sessionId?: unknown) => { const sid = typeof sessionId === 'string' && sessionId ? sessionId : undefined; return { success: true, data: confirmationHook.getRememberedDenials(sid) }; }); ipcMain.handle( 'tool:resetRememberedDenial', async (_event, toolName: unknown, sessionId?: unknown) => { if (typeof toolName !== 'string' || !toolName) { return { success: false, error: 'Invalid toolName' }; } const sid = typeof sessionId === 'string' && sessionId ? sessionId : undefined; const reset = confirmationHook.resetRememberedDenial(toolName, sid); if (reset) { log.info(`[CONFIRM] Reset remembered denial for tool "${toolName}" — will ask again`); } return { success: reset }; }, ); // ===== 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() }; }); }