/** * IPC Session Handlers — 会话管理域(P2-9 从 handlers.ts 拆分) * * 会话 CRUD、消息加载/删除/清空、Trace 持久化。 * P2-11: 新增 sessions:truncateAfter(编辑重发/重新生成的消息截断)。 */ import { ipcMain } from 'electron'; import type { IPCContext } from './context'; /** M-34 修复: 统一 sessionId 校验辅助函数 */ const isValidSessionId = (id: unknown): id is string => typeof id === 'string' && id.length > 0 && id.length <= 200; export function registerSessionHandlers(ctx: IPCContext): void { const { sessionService, confirmationHook, agentEngineManager } = ctx; 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: 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' }; // v0.7.4 P3-5: 删除会话前先中断其进行中的 run(若在运行) // —— 防止删除后引擎仍在后台执行工具副作用 if (agentEngineManager.isRunning(sessionId)) { agentEngineManager.abort(sessionId); await agentEngineManager.waitForAbort(sessionId); } const result = { success: sessionService.delete(sessionId) }; // v0.7.3 P2-3: 会话删除 = 会话终态 —— 决策记忆/pending 确认一并清理 // (防 rememberedDecisions 随会话数累积泄漏) if (result.success) { confirmationHook.forgetSession(sessionId); // v0.7.4 P3-5: 显式销毁引擎与 adapter 实例(内存收口) // —— 旧实现只靠 LRU 上限 30 淘汰,会话删除后引擎仍驻留 agentEngineManager.disposeEngine(sessionId); } return result; }); ipcMain.handle('sessions:getMessages', async (_event, sessionId: unknown) => { // M-34 修复: 校验 sessionId if (!isValidSessionId(sessionId)) return []; return sessionService.getMessages(sessionId); }); // v0.7.4 P3-10: 更新单条用户消息内容(编辑"仅保存"落库)。 // 此前"仅保存"只改前端 store,会话重载即丢失(与"保存并重发"的截断语义割裂)。 ipcMain.handle( 'sessions:updateMessageContent', async (_event, sessionId: unknown, messageId: unknown, content: unknown) => { if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; if (typeof messageId !== 'string' || !messageId) return { success: false, error: 'Invalid messageId' }; if (typeof content !== 'string') return { success: false, error: 'Invalid content' }; try { const updated = sessionService.updateMessageContent(sessionId, messageId, content); return { success: updated }; } catch (error) { return { success: false, error: (error as Error).message }; } }, ); ipcMain.handle('sessions:pin', async (_event, sessionId: unknown, pinned: unknown) => { 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) => { 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) }; }); // v0.7.3 P1-3: sessions:deleteMessage 已删除 —— 渲染层零调用方(死通道), // 且原实现不回减 sessions.message_count(计数漂移面)。消息删除语义由 // sessions:truncateAfter(编辑重发/重新生成)与会话删除级联完整覆盖。 // v0.7.2 A1 根治: 语义修正 —— "清空会话"的成功判定是"操作完成"而非"有行被删除"。 // 原实现透传 DELETE 影响行数(changes > 0),空会话清空会返回 success:false, // 导致前端误报失败而不清空本地状态(/clear 断链的 IPC 侧根源之一)。 ipcMain.handle('sessions:clearMessages', async (_event, sessionId: unknown) => { if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; try { sessionService.clearMessages(sessionId); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // P2-11: 消息截断(编辑重发 / 重新生成) ipcMain.handle( 'sessions:truncateAfter', async (_event, sessionId: unknown, messageId: unknown, inclusive: unknown) => { if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' }; if (typeof messageId !== 'string' || !messageId) return { success: false, error: 'Invalid messageId' }; const isInclusive = typeof inclusive === 'boolean' ? inclusive : true; try { const truncated = sessionService.truncateMessagesAfter(sessionId, messageId, isInclusive); return { success: true, truncated }; } catch (error) { return { success: false, error: (error as Error).message }; } }, ); // v0.5.0: 全文搜索会话内容(FTS5,跨会话) ipcMain.handle('sessions:searchContent', async (_event, query: unknown) => { if (typeof query !== 'string' || !query.trim()) return { success: false, error: 'Invalid query' }; try { const results = sessionService.searchMessages(query); return { success: true, data: results }; } catch (error) { return { success: false, error: (error as Error).message }; } }); 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) => { if (!isValidSessionId(sessionId)) return null; return sessionService.getTraceData(sessionId); }); }