/** * IPC Config Handlers — 配置管理域(P2-9 从 handlers.ts 拆分) * * config:get / config:set / config:setBatch。 * P2-9: set 与 setBatch 共享 applyConfigSideEffects(消除原 300 行重复逻辑)。 */ import { ipcMain } from 'electron'; import type { IPCContext } from './context'; import { LLM_CONFIG_KEYS, applyConfigSideEffects, clearApiKeyOnProviderChange, maskSensitive, assertSafeConfigUrls, } from './shared'; import log from 'electron-log'; export function registerConfigHandlers(ctx: IPCContext): void { const { configService, auditService } = ctx; 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', }; } // v0.7.4 P2-9-C: URL 类配置键高危目标校验(llm.baseURL/searxng.url/updateFeedUrl) // v0.8.0 P1-5: 升级为 async 深校验(域名 DNS 解析拦云元数据绕过) try { await assertSafeConfigUrls([{ key, value }]); } catch (err) { log.warn(`[CONFIG] config:set rejected URL for ${key}: ${(err as Error).message}`); return { success: false, error: (err as Error).message }; } // C-1 修复: Provider 切换时清空 API key(P2-9: 统一走共享函数) clearApiKeyOnProviderChange(ctx, [{ key, value }]); configService.set(key, value); // M-42 修复: 敏感配置项脱敏后再写入审计日志 auditService.log({ sessionId: '', eventType: 'config_change', actor: 'user', target: key, details: { value: maskSensitive(key, value) }, outcome: 'success', }); // P2-9: 统一副作用(reloadAdapter + Engine 同步 + 广播 + 工作空间路径) const sideEffectError = await applyConfigSideEffects(ctx, [{ key, value }]); if (sideEffectError) { return { success: false, error: sideEffectError }; } if (LLM_CONFIG_KEYS.includes(key)) { log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`); } return { success: true }; }); // ===== v0.3.9: 批量保存配置(P2-9: 复用共享副作用逻辑) ===== // 设计原因:前端设置页一次保存多个字段,串行 config:set 会在中间态(如 provider // 已改但 apiKey 还没保存)触发 reloadAdapter 失败。批量保存:先写入所有字段, // 最后统一触发一次 reloadAdapter 和 Engine/Orchestrator 同步。 ipcMain.handle('config:setBatch', async (_event, entries: unknown) => { // 参数校验:必须是 {key, value}[] 非空数组 if (!Array.isArray(entries) || entries.length === 0) { return { success: false, error: 'Invalid entries: must be non-empty array of {key, value}' }; } // 逐条校验每个元素的结构和类型 for (const entry of entries) { if (!entry || typeof entry !== 'object') { return { success: false, error: 'Invalid entry: must be {key, value} object' }; } const e = entry as { key?: unknown; value?: unknown }; if (typeof e.key !== 'string' || !e.key.trim()) { return { success: false, error: 'Invalid config key in batch' }; } const v = e.value; if (v !== null && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') { return { success: false, error: `Invalid config value for key "${e.key}": must be string, number, boolean, or null`, }; } } // v0.7.4 P2-9-C: 批量写入同样校验 URL 类配置键 // v0.8.0 P1-5: 升级为 async 深校验 try { await assertSafeConfigUrls(entries as Array<{ key: string; value: unknown }>); } catch (err) { log.warn(`[CONFIG] config:setBatch rejected URL entry: ${(err as Error).message}`); return { success: false, error: (err as Error).message }; } // v0.3.10 防御性修复: Provider 切换清空 apiKey 必须在循环前执行(共享函数内含说明) clearApiKeyOnProviderChange(ctx, entries as Array<{ key: string; value: unknown }>); // 第一步:批量写入 configService(单事务 + 全局层一次落盘,v0.7.4 P3-6)+ 审计日志(脱敏) for (const entry of entries) { const { key, value } = entry as { key: string; value: unknown }; auditService.log({ sessionId: '', eventType: 'config_change', actor: 'user', target: key, details: { value: maskSensitive(key, value) }, outcome: 'success', }); } configService.setBatch(entries as Array<{ key: string; value: unknown }>); // 第二步:统一副作用(reloadAdapter + Engine 同步 + 日志级别 + 广播 + 工作空间路径) const sideEffectError = await applyConfigSideEffects( ctx, entries as Array<{ key: string; value: unknown }>, ); if (sideEffectError) { log.warn('[CONFIG] Config batch save side effects failed'); return { success: false, error: sideEffectError }; } log.info(`[CONFIG] Config batch saved (${entries.length} keys)`); return { success: true }; }); }