diff --git a/apps/desktop/main/ipc/ai.ts b/apps/desktop/main/ipc/ai.ts index d4876b5e..08b63c01 100644 --- a/apps/desktop/main/ipc/ai.ts +++ b/apps/desktop/main/ipc/ai.ts @@ -2,7 +2,6 @@ import { ipcMain, shell } from 'electron' import * as fs from 'fs' import * as path from 'path' -import * as aiConversations from '../ai/conversations' import * as llm from '../ai/llm' import { aiLogger, setDebugMode } from '../ai/logger' import { serializeError } from '../ai/serialize-error' @@ -19,7 +18,6 @@ import type { AssistantConfig } from '../ai/assistant/types' import * as skillManager from '../ai/skills' import { checkAndCompress, - manualCompress, createCompressionLlmAdapter, type CompressionConfig, type CompressionLlmAdapter, @@ -56,7 +54,6 @@ function buildCompressionAdapter(activeAIConfig: AIServiceConfig, onCompressing? } function toPiSimpleMessages(messages: Array<{ role: string; content: string }>, timestamp: number): PiMessage[] { - // pi-ai 的 simple API 在类型上要求完整 Message 联合,这里沿用现有轻量消息格式并集中做兼容转换。 return messages.map((message) => ({ role: message.role as 'user' | 'assistant', content: message.content, @@ -65,7 +62,6 @@ function toPiSimpleMessages(messages: Array<{ role: string; content: string }>, } // ==================== AI Agent 请求追踪 ==================== -// 用于跟踪活跃的 Agent 请求,支持中止操作 const activeAgentRequests = new Map() function resolveProviderName(provider?: llm.LLMProvider): string { @@ -76,7 +72,6 @@ function resolveProviderName(provider?: llm.LLMProvider): string { export function registerAIHandlers({ win }: IpcContext): void { console.log('[IPC] Registering AI handlers...') - // 初始化助手管理器(同步内置助手、加载用户助手) try { assistantManager.initAssistantManager() console.log('[IPC] Assistant manager initialized') @@ -84,7 +79,6 @@ export function registerAIHandlers({ win }: IpcContext): void { console.error('[IPC] Failed to initialize assistant manager:', error) } - // 初始化技能管理器(扫描用户技能文件) try { skillManager.initSkillManager() console.log('[IPC] Skill manager initialized') @@ -99,53 +93,10 @@ export function registerAIHandlers({ win }: IpcContext): void { aiLogger.info('Config', `Debug mode ${enabled ? 'enabled' : 'disabled'}`) }) - ipcMain.handle('ai:clearDebugContext', async () => { - try { - const cleared = aiConversations.clearAllDebugContext() - aiLogger.info('Debug', `Cleared debug_context for ${cleared} messages`) - return { success: true, cleared } - } catch (error) { - console.error('Failed to clear debug context:', error) - throw error - } - }) + // ==================== AI 日志 ==================== - // ==================== AI 对话管理 ==================== - - /** - * 创建新的 AI 对话 - * 参数契约与 preload / 数据层保持一致:(sessionId, title?) - */ - ipcMain.handle( - 'ai:createConversation', - async (_, sessionId: string, title: string | undefined, assistantId: string) => { - try { - return aiConversations.createConversation(sessionId, title, assistantId) - } catch (error) { - console.error('Failed to create AI conversation:', error) - throw error - } - } - ) - - /** - * 获取所有 AI 对话列表 - */ - ipcMain.handle('ai:getConversations', async (_, sessionId: string) => { - try { - return aiConversations.getConversations(sessionId) - } catch (error) { - console.error('Failed to get AI conversations:', error) - return [] - } - }) - - /** - * 打开当前 AI 日志文件并定位到文件 - */ ipcMain.handle('ai:showLogFile', async () => { try { - // 优先使用当前已存在的日志文件,避免创建新的空日志 const existingLogPath = aiLogger.getExistingLogPath() if (existingLogPath) { shell.showItemInFolder(existingLogPath) @@ -163,7 +114,6 @@ export function registerAIHandlers({ win }: IpcContext): void { return { success: false, error: 'No AI log files found' } } - // 选择最近修改的日志文件 const latestLog = logFiles .map((name) => { const filePath = path.join(logDir, name) @@ -180,176 +130,6 @@ export function registerAIHandlers({ win }: IpcContext): void { } }) - /** - * 获取单个对话详情 - */ - ipcMain.handle('ai:getConversation', async (_, conversationId: string) => { - try { - return aiConversations.getConversation(conversationId) - } catch (error) { - console.error('Failed to get AI conversation details:', error) - return null - } - }) - - /** - * 更新 AI 对话标题 - */ - ipcMain.handle('ai:updateConversationTitle', async (_, conversationId: string, title: string) => { - try { - return aiConversations.updateConversationTitle(conversationId, title) - } catch (error) { - console.error('Failed to update AI conversation title:', error) - return false - } - }) - - /** - * 删除 AI 对话 - */ - ipcMain.handle('ai:deleteConversation', async (_, conversationId: string) => { - try { - return aiConversations.deleteConversation(conversationId) - } catch (error) { - console.error('Failed to delete AI conversation:', error) - return false - } - }) - - /** - * 添加 AI 消息 - */ - ipcMain.handle( - 'ai:addMessage', - async ( - _, - conversationId: string, - role: aiConversations.AIMessageRole, - content: string, - dataKeywords?: string[], - dataMessageCount?: number, - contentBlocks?: aiConversations.ContentBlock[], - tokenUsage?: aiConversations.TokenUsageData - ) => { - try { - return aiConversations.addMessage( - conversationId, - role, - content, - dataKeywords, - dataMessageCount, - contentBlocks, - tokenUsage - ) - } catch (error) { - console.error('Failed to add AI message:', error) - throw error - } - } - ) - - /** - * 获取 AI 对话的所有消息 - */ - ipcMain.handle('ai:getMessages', async (_, conversationId: string) => { - try { - return aiConversations.getMessages(conversationId) - } catch (error) { - console.error('Failed to get AI messages:', error) - return [] - } - }) - - /** - * 获取对话的累计 token 使用量 - */ - ipcMain.handle('ai:getConversationTokenUsage', async (_, conversationId: string) => { - try { - return aiConversations.getConversationTokenUsage(conversationId) - } catch (error) { - console.error('Failed to get conversation token usage:', error) - return { promptTokens: 0, completionTokens: 0, totalTokens: 0 } - } - }) - - /** - * 删除 AI 消息 - */ - ipcMain.handle('ai:deleteMessage', async (_, messageId: string) => { - try { - return aiConversations.deleteMessage(messageId) - } catch (error) { - console.error('Failed to delete AI message:', error) - return false - } - }) - - ipcMain.handle('ai:deleteMessagesFrom', async (_, conversationId: string, messageId: string) => { - try { - return aiConversations.deleteMessagesFrom(conversationId, messageId) - } catch (error) { - console.error('Failed to delete messages:', error) - throw error - } - }) - - ipcMain.handle( - 'ai:forkConversation', - async (_, sourceConversationId: string, upToMessageId: string, title?: string) => { - try { - return aiConversations.forkConversation(sourceConversationId, upToMessageId, title) - } catch (error) { - console.error('Failed to fork conversation:', error) - throw error - } - } - ) - - ipcMain.handle('ai:updateMessageContent', async (_, messageId: string, newContent: string) => { - try { - return aiConversations.updateMessageContent(messageId, newContent) - } catch (error) { - console.error('Failed to update message content:', error) - throw error - } - }) - - ipcMain.handle('ai:deleteAndRelinkMessage', async (_, conversationId: string, messageId: string) => { - try { - return aiConversations.deleteAndRelinkMessage(conversationId, messageId) - } catch (error) { - console.error('Failed to delete and relink message:', error) - throw error - } - }) - - ipcMain.handle( - 'ai:insertMessageAfter', - async ( - _, - conversationId: string, - afterMessageId: string, - role: aiConversations.AIMessageRole, - content: string, - contentBlocks?: aiConversations.ContentBlock[], - tokenUsage?: aiConversations.TokenUsageData - ) => { - try { - return aiConversations.insertMessageAfter( - conversationId, - afterMessageId, - role, - content, - contentBlocks, - tokenUsage - ) - } catch (error) { - console.error('Failed to insert message after:', error) - throw error - } - } - ) - // ==================== 脱敏规则 ==================== ipcMain.handle('ai:getDefaultDesensitizeRules', (_, locale: string) => { @@ -360,14 +140,8 @@ export function registerAIHandlers({ win }: IpcContext): void { return mergeRulesForLocale(existingRules as any[], locale) }) - // LLM CRUD (config, provider, model, validate, etc.) has been migrated to - // HTTP service layer via shared routes. Only chat APIs remain on IPC. + // ==================== LLM 直接调用 API(SQLLab 等非 Agent 场景) ==================== - // ==================== LLM 直接调用 API(SQLLab 等非 Agent 场景使用) ==================== - - /** - * 非流式 LLM 调用 - */ ipcMain.handle( 'llm:chat', async ( @@ -411,9 +185,6 @@ export function registerAIHandlers({ win }: IpcContext): void { } ) - /** - * 流式 LLM 调用(SQLLab AI 生成 / 结果总结等场景使用) - */ ipcMain.handle( 'llm:chatStream', async ( @@ -454,9 +225,7 @@ export function registerAIHandlers({ win }: IpcContext): void { } ) - // Assistant and Skill CRUD has been migrated to HTTP service layer via shared routes. - - // ==================== 工具测试 API(实验室 - 基础工具) ==================== + // ==================== 工具测试 API ==================== const activeToolTests = new Map() @@ -546,14 +315,6 @@ export function registerAIHandlers({ win }: IpcContext): void { // ==================== AI Agent API ==================== - /** - * 执行 Agent 对话(流式) - * Agent 会自动调用工具并返回最终结果 - * Agent 通过 context.conversationId 从 SQLite 读取对话历史(数据流倒置) - * @param chatType 聊天类型('group' | 'private') - * @param locale 语言设置(可选,默认 'zh-CN') - * @param assistantId 助手 ID(可选,传入时从 AssistantManager 获取配置) - */ ipcMain.handle( 'agent:runStream', async ( @@ -589,10 +350,8 @@ export function registerAIHandlers({ win }: IpcContext): void { } const piModel = buildPiModel(activeAIConfig) - // 上下文压缩前置步骤(在 Agent 创建之前执行) if (compressionConfig?.enabled && context.conversationId && context.historyLeafMessageId === undefined) { try { - // 获取助手 systemPrompt 用于 token 计算 const tempAssistantConfig = assistantId ? (assistantManager.getAssistantConfig(assistantId) ?? undefined) : undefined @@ -669,7 +428,6 @@ export function registerAIHandlers({ win }: IpcContext): void { : '(disabled)', }) - // 提示词系统已退场,主流程统一从助手配置获取 systemPrompt。 const defaultAssistantId = getDefaultGeneralAssistantId(locale) let resolvedAssistantId = assistantId || defaultAssistantId let assistantConfig: AssistantConfig | undefined = @@ -683,7 +441,6 @@ export function registerAIHandlers({ win }: IpcContext): void { assistantConfig = assistantManager.getAssistantConfig(defaultAssistantId) ?? undefined } - // 构建技能上下文 let skillCtx: SkillContext | undefined if (skillId) { const skillDef = skillManager.getSkillConfig(skillId) ?? undefined @@ -701,7 +458,6 @@ export function registerAIHandlers({ win }: IpcContext): void { } } - // 工具结果 token 预算注入:基于 context window 百分比计算 const maxToolResultPercent = compressionConfig?.maxToolResultPercent ?? 50 const modelDef = llm.findModelDefinition(activeAIConfig.provider, activeAIConfig.model || '') const resolvedContextWindow = modelDef?.contextWindow || 128000 @@ -740,11 +496,9 @@ export function registerAIHandlers({ win }: IpcContext): void { skillCtx ) - // 异步执行,通过事件发送流式数据 ;(async () => { try { const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => { - // 如果已中止,不再发送 if (abortController.signal.aborted) { return } @@ -769,7 +523,6 @@ export function registerAIHandlers({ win }: IpcContext): void { return } - // 发送完成信息 win.webContents.send('agent:complete', { requestId, result: { @@ -804,12 +557,10 @@ export function registerAIHandlers({ win }: IpcContext): void { serializedError.url = activeAIConfig.baseUrl } aiLogger.error('IPC', `Agent execution error: ${requestId}`, serializedError) - // 发送错误 chunk win.webContents.send('agent:streamChunk', { requestId, chunk: { type: 'error', error: serializedError, isFinished: true }, }) - // 发送完成事件(带错误信息),确保前端 promise 能 resolve win.webContents.send('agent:complete', { requestId, result: { @@ -821,7 +572,6 @@ export function registerAIHandlers({ win }: IpcContext): void { }, }) } finally { - // 清理请求追踪 activeAgentRequests.delete(requestId) } })() @@ -834,9 +584,6 @@ export function registerAIHandlers({ win }: IpcContext): void { } ) - /** - * 中止 Agent 请求 - */ ipcMain.handle('agent:abort', async (_, requestId: string) => { aiLogger.info('IPC', `Abort request received: ${requestId}`) @@ -852,64 +599,15 @@ export function registerAIHandlers({ win }: IpcContext): void { } }) - // ==================== 上下文压缩 ==================== - - ipcMain.handle( - 'ai:compressContext', - async (_, conversationId: string, compressionConfig: CompressionConfig, systemPrompt: string) => { - try { - const activeAIConfig = getDefaultAssistantConfig() - if (!activeAIConfig) { - return { success: false, error: t('llm.notConfigured') } - } - - const result = await manualCompress( - conversationId, - compressionConfig, - systemPrompt, - buildCompressionAdapter(activeAIConfig), - getConversationManager(), - compressionLogger - ) - return { success: true, result } - } catch (error) { - aiLogger.error('IPC', 'Manual compression failed', { error: String(error) }) - return { success: false, error: String(error) } - } - } - ) + // ==================== 上下文 token 估算 ==================== ipcMain.handle('ai:estimateContextTokens', async (_, conversationId: string) => { try { - const history = aiConversations.getHistoryForAgent(conversationId) + const history = getConversationManager().getHistoryForAgent(conversationId) const tokens = countMessagesTokens(history.map((m) => ({ role: m.role, content: m.content }))) return { success: true, tokens, messageCount: history.length } } catch (error) { return { success: false, tokens: 0, error: String(error) } } }) - - /** - * [Debug] 获取 AI 数据库 Schema - */ - ipcMain.handle('ai:getAiSchema', async () => { - try { - return aiConversations.getAiSchema() - } catch (error) { - console.error('Failed to get AI schema:', error) - return [] - } - }) - - /** - * [Debug] 在 AI 数据库上执行原始 SQL - */ - ipcMain.handle('ai:executeAiSQL', async (_, sql: string) => { - try { - return aiConversations.executeAiSQL(sql) - } catch (error: any) { - console.error('Failed to execute AI SQL:', error) - throw error - } - }) } diff --git a/apps/desktop/preload/apis/ai.ts b/apps/desktop/preload/apis/ai.ts index b01089ce..5b8c2d61 100644 --- a/apps/desktop/preload/apis/ai.ts +++ b/apps/desktop/preload/apis/ai.ts @@ -1,59 +1,20 @@ /** - * AI 相关 API - AI 对话、LLM 服务、Agent、Embedding + * AI 相关 API — 仅保留 IPC 必须的能力 + * + * Conversation/message CRUD 和 debug 已迁移到 HTTP 共享路由(FetchAIAdapter), + * 此处只保留需要 worker、native shell、工具注册表等 IPC 才能提供的功能。 */ import { ipcRenderer } from 'electron' import type { ExportProgress } from '../../../../src/types/base' +// Agent API 类型 — 从 shared/types 统一导入 +export type { TokenUsage, AgentRuntimeStatus } from '../../shared/types' +import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../shared/types' + +export type { SerializedErrorInfo } from '../../shared/types' + // ==================== 类型定义 ==================== -export interface AIConversation { - id: string - sessionId: string - title: string | null - assistantId: string - activeMessageId?: string | null - createdAt: number - updatedAt: number -} - -// 内容块类型(用于 AI 消息的混合渲染) -export type ContentBlock = - | { type: 'text'; text: string } - | { type: 'think'; tag: string; text: string; durationMs?: number } - | { - type: 'tool' - tool: { - name: string - displayName: string - status: 'running' | 'done' | 'error' - params?: Record - } - } - | { type: 'skill'; skillId: string; skillName: string } - | { type: 'error'; error: SerializedErrorInfo } - | { type: 'summary_meta'; bufferBoundaryTimestamp: number; compressedMessageCount: number } - -export type AIMessageRole = 'user' | 'assistant' | 'summary' - -export interface TokenUsageData { - promptTokens: number - completionTokens: number - totalTokens: number -} - -export interface AIMessage { - id: string - conversationId: string - role: AIMessageRole - content: string - timestamp: number - parentId?: string | null - dataKeywords?: string[] - dataMessageCount?: number - contentBlocks?: ContentBlock[] - tokenUsage?: TokenUsageData -} - export interface ChatMessage { role: 'system' | 'user' | 'assistant' content: string @@ -73,12 +34,6 @@ export interface ChatStreamChunk { thinkingDone?: boolean } -// Agent API 类型 — 从 shared/types 统一导入 -export type { TokenUsage, AgentRuntimeStatus } from '../../shared/types' -import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../../shared/types' - -export type { SerializedErrorInfo } from '../../shared/types' - export interface AgentStreamChunk { type: 'content' | 'think' | 'tool_start' | 'tool_result' | 'status' | 'compression_done' | 'done' | 'error' content?: string @@ -96,7 +51,6 @@ export interface AgentStreamChunk { tokensAfter: number timestamp: number } - /** Token 使用量(type=done 时返回累计值) */ usage?: TokenUsage } @@ -107,7 +61,6 @@ export interface AgentResult { error?: SerializedErrorInfo } -/** 单条脱敏规则 */ export interface DesensitizeRule { id: string label: string @@ -118,7 +71,6 @@ export interface DesensitizeRule { locales: string[] } -/** 工具目录条目(实验室 - 基础工具) */ export interface ToolCatalogEntry { name: string category: 'core' | 'analysis' @@ -126,7 +78,6 @@ export interface ToolCatalogEntry { parameters: Record } -/** 工具执行结果 */ export interface ToolExecuteResult { success: boolean elapsed?: number @@ -136,7 +87,6 @@ export interface ToolExecuteResult { truncated?: boolean } -/** 聊天记录预处理配置 */ export interface PreprocessConfig { dataCleaning: boolean mergeConsecutive: boolean @@ -166,10 +116,10 @@ export interface ToolContext { preprocessConfig?: PreprocessConfig } -// ==================== AI API ==================== +// ==================== AI API (IPC-only subset) ==================== export const aiApi = { - // ==================== 消息筛选/导出(IPC handler 保留) ==================== + // ===== 消息筛选/导出(worker-dependent) ===== filterMessagesWithContext: ( sessionId: string, @@ -224,11 +174,6 @@ export const aiApi = { ) }, - /** - * 获取多个会话的完整消息(支持分页) - * @param page 页码(从 1 开始,默认 1) - * @param pageSize 每页块数(默认 50) - */ getMultipleSessionsMessages: ( sessionId: string, chatSessionIds: number[], @@ -270,9 +215,6 @@ export const aiApi = { return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds, page, pageSize) }, - /** - * 导出筛选结果到文件(后端生成,支持大数据量) - */ exportFilterResultToFile: (params: { sessionId: string sessionName: string @@ -287,9 +229,6 @@ export const aiApi = { return ipcRenderer.invoke('ai:exportFilterResultToFile', params) }, - /** - * 监听导出进度 - */ onExportProgress: (callback: (progress: ExportProgress) => void) => { const handler = (_event: Electron.IpcRendererEvent, progress: ExportProgress) => { callback(progress) @@ -300,134 +239,13 @@ export const aiApi = { } }, - /** - * 创建 AI 对话 - */ - createConversation: (sessionId: string, title: string | undefined, assistantId: string): Promise => { - return ipcRenderer.invoke('ai:createConversation', sessionId, title, assistantId) - }, + // ===== 日志(native shell) ===== - /** - * 获取会话的所有 AI 对话列表 - */ - getConversations: (sessionId: string): Promise => { - return ipcRenderer.invoke('ai:getConversations', sessionId) - }, - - /** - * 获取单个 AI 对话 - */ - getConversation: (conversationId: string): Promise => { - return ipcRenderer.invoke('ai:getConversation', conversationId) - }, - - /** - * 更新 AI 对话标题 - */ - updateConversationTitle: (conversationId: string, title: string): Promise => { - return ipcRenderer.invoke('ai:updateConversationTitle', conversationId, title) - }, - - /** - * 删除 AI 对话 - */ - deleteConversation: (conversationId: string): Promise => { - return ipcRenderer.invoke('ai:deleteConversation', conversationId) - }, - - /** - * 添加 AI 消息 - */ - addMessage: ( - conversationId: string, - role: AIMessageRole, - content: string, - dataKeywords?: string[], - dataMessageCount?: number, - contentBlocks?: ContentBlock[], - tokenUsage?: TokenUsageData - ): Promise => { - return ipcRenderer.invoke( - 'ai:addMessage', - conversationId, - role, - content, - dataKeywords, - dataMessageCount, - contentBlocks, - tokenUsage - ) - }, - - deleteMessagesFrom: (conversationId: string, messageId: string): Promise => { - return ipcRenderer.invoke('ai:deleteMessagesFrom', conversationId, messageId) - }, - - forkConversation: (sourceConversationId: string, upToMessageId: string, title?: string): Promise => { - return ipcRenderer.invoke('ai:forkConversation', sourceConversationId, upToMessageId, title) - }, - - updateMessageContent: (messageId: string, newContent: string): Promise => { - return ipcRenderer.invoke('ai:updateMessageContent', messageId, newContent) - }, - - deleteAndRelinkMessage: (conversationId: string, messageId: string): Promise => { - return ipcRenderer.invoke('ai:deleteAndRelinkMessage', conversationId, messageId) - }, - - insertMessageAfter: ( - conversationId: string, - afterMessageId: string, - role: AIMessageRole, - content: string, - contentBlocks?: ContentBlock[], - tokenUsage?: TokenUsageData - ): Promise => { - return ipcRenderer.invoke( - 'ai:insertMessageAfter', - conversationId, - afterMessageId, - role, - content, - contentBlocks, - tokenUsage - ) - }, - - /** - * 获取 AI 对话的所有消息 - */ - getMessages: (conversationId: string): Promise => { - return ipcRenderer.invoke('ai:getMessages', conversationId) - }, - - /** - * 获取对话的累计 token 使用量 - */ - getConversationTokenUsage: (conversationId: string): Promise => { - return ipcRenderer.invoke('ai:getConversationTokenUsage', conversationId) - }, - - /** - * 删除 AI 消息 - */ - deleteMessage: (messageId: string): Promise => { - return ipcRenderer.invoke('ai:deleteMessage', messageId) - }, - - /** - * 打开当前 AI 日志文件并定位到文件 - */ showAiLogFile: (): Promise<{ success: boolean; path?: string; error?: string }> => { return ipcRenderer.invoke('ai:showLogFile') }, - /** - * 一键清除所有消息的 debug_context 数据 - */ - clearDebugContext: (): Promise<{ success: boolean; cleared: number }> => { - return ipcRenderer.invoke('ai:clearDebugContext') - }, + // ===== 脱敏规则(node-runtime) ===== getDefaultDesensitizeRules: (locale: string): Promise => { return ipcRenderer.invoke('ai:getDefaultDesensitizeRules', locale) @@ -437,6 +255,8 @@ export const aiApi = { return ipcRenderer.invoke('ai:mergeDesensitizeRules', existingRules, locale) }, + // ===== 工具测试(tool registry on main process) ===== + getToolCatalog: (): Promise => { return ipcRenderer.invoke('ai:getToolCatalog') }, @@ -454,63 +274,18 @@ export const aiApi = { return ipcRenderer.invoke('ai:cancelToolTest', testId) }, + // ===== 上下文估算(needs conversationManager) ===== + estimateContextTokens: ( conversationId: string ): Promise<{ success: boolean; tokens: number; messageCount?: number; error?: string }> => { return ipcRenderer.invoke('ai:estimateContextTokens', conversationId) }, - - compressContext: ( - conversationId: string, - compressionConfig: { - enabled: boolean - tokenThresholdPercent: number - bufferSizePercent: number - maxToolResultPercent?: number - }, - systemPrompt: string - ): Promise<{ - success: boolean - result?: { - compressed: boolean - reason: string - tokensBefore?: number - tokensAfter?: number - error?: string - } - error?: string - }> => { - return ipcRenderer.invoke('ai:compressContext', conversationId, compressionConfig, systemPrompt) - }, - - /** - * [Debug] 获取 AI 数据库 Schema - */ - getAiSchema: (): Promise< - Array<{ name: string; columns: Array<{ name: string; type: string; notnull: boolean; pk: boolean }> }> - > => { - return ipcRenderer.invoke('ai:getAiSchema') - }, - - /** - * [Debug] 在 AI 数据库上执行原始 SQL - */ - executeAiSQL: ( - sql: string - ): Promise<{ columns: string[]; rows: any[][]; rowCount: number; duration: number; limited: boolean }> => { - return ipcRenderer.invoke('ai:executeAiSQL', sql) - }, } // ==================== LLM API ==================== export const llmApi = { - /** - * LLM CRUD (config, provider, model, validate, etc.) has been migrated to - * HTTP service layer via FetchLLMAdapter. Only streaming / non-streaming - * chat APIs remain on IPC because they require real-time event forwarding. - */ - chat: ( messages: ChatMessage[], options?: ChatOptions @@ -544,7 +319,6 @@ export const llmApi = { onChunk(data.chunk) } - // 如果已完成,移除监听器并 resolve if (data.chunk.isFinished) { console.log('[preload] chatStream 完成,requestId:', requestId) ipcRenderer.removeListener('llm:streamChunk', handler) @@ -556,7 +330,6 @@ export const llmApi = { ipcRenderer.on('llm:streamChunk', handler) - // 发起请求 ipcRenderer .invoke('llm:chatStream', requestId, messages, options) .then((result) => { @@ -565,7 +338,6 @@ export const llmApi = { ipcRenderer.removeListener('llm:streamChunk', handler) resolve(result) } - // 如果 success,等待流完成(由 handler 处理 resolve) }) .catch((error) => { console.error('[preload] chatStream invoke 错误:', error) @@ -576,26 +348,9 @@ export const llmApi = { }, } -/** - * Assistant CRUD has been migrated to HTTP service layer via FetchAssistantAdapter. - * The preload bridge is no longer needed. - */ - -/** - * Skill CRUD has been migrated to HTTP service layer via FetchSkillAdapter. - * The preload bridge is no longer needed. - */ - // ==================== Agent API ==================== export const agentApi = { - /** - * 执行 Agent 对话(流式) - * Agent 通过 context.conversationId 从后端 SQLite 读取对话历史 - * @param chatType 聊天类型('group' | 'private') - * @param locale 语言设置(可选,默认 'zh-CN') - * @returns 返回 { requestId, promise },requestId 可用于中止请求 - */ runStream: ( userMessage: string, context: ToolContext, @@ -616,8 +371,6 @@ export const agentApi = { requestId: string promise: Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }> } => { - // 防御性处理:确保传给 IPC 的 context 是“可结构化克隆”的纯对象 - // 避免调用方误传入响应式 Proxy(例如 Pinia/Vue state)导致 invoke 失败 const sanitizedContext: ToolContext = { sessionId: context.sessionId, conversationId: context.conversationId, @@ -659,7 +412,6 @@ export const agentApi = { ) const promise = new Promise<{ success: boolean; result?: AgentResult; error?: SerializedErrorInfo }>((resolve) => { - // 监听流式 chunks const chunkHandler = ( _event: Electron.IpcRendererEvent, data: { requestId: string; chunk: AgentStreamChunk } @@ -671,7 +423,6 @@ export const agentApi = { } } - // 监听完成事件 const completeHandler = ( _event: Electron.IpcRendererEvent, data: { requestId: string; result: AgentResult & { error?: SerializedErrorInfo } } @@ -680,7 +431,6 @@ export const agentApi = { console.log('[preload] Agent 完成,requestId:', requestId, 'hasError:', !!data.result?.error) ipcRenderer.removeListener('agent:streamChunk', chunkHandler) ipcRenderer.removeListener('agent:complete', completeHandler) - // 如果 result 中包含 error,返回失败状态 if (data.result?.error) { resolve({ success: false, error: data.result.error }) } else { @@ -713,7 +463,6 @@ export const agentApi = { ipcRenderer.removeListener('agent:complete', completeHandler) resolve(result) } - // 如果 success,等待完成(由 completeHandler 处理 resolve) }) .catch((error) => { console.error('[preload] Agent invoke 错误:', error) @@ -733,10 +482,6 @@ export const agentApi = { return { requestId, promise } }, - /** - * 中止 Agent 请求 - * @param requestId 请求 ID - */ abort: (requestId: string): Promise<{ success: boolean; error?: string }> => { console.log('[preload] Agent abort 请求,requestId:', requestId) return ipcRenderer.invoke('agent:abort', requestId) diff --git a/apps/desktop/preload/index.d.ts b/apps/desktop/preload/index.d.ts index 9d292bd0..1b3a5d3a 100644 --- a/apps/desktop/preload/index.d.ts +++ b/apps/desktop/preload/index.d.ts @@ -153,124 +153,15 @@ interface FilterResultWithPagination extends FilterResult { pagination: PaginationInfo } -interface AIConversation { - id: string - sessionId: string - title: string | null - assistantId: string - activeMessageId?: string | null - createdAt: number - updatedAt: number -} - -// 内容块类型(用于 AI 消息的混合渲染) -type AIContentBlock = - | { type: 'text'; text: string } - | { type: 'think'; tag: string; text: string; durationMs?: number } - | { - type: 'tool' - tool: { - name: string - displayName: string - status: 'running' | 'done' | 'error' - params?: Record - } - } - | { type: 'skill'; skillId: string; skillName: string } - | { type: 'error'; error: SerializedErrorInfo } - | { - type: 'summary_meta' - bufferBoundaryTimestamp: number - compressedMessageCount: number - } - -type AIMessageRole = 'user' | 'assistant' | 'summary' - -interface AITokenUsageData { - promptTokens: number - completionTokens: number - totalTokens: number -} - -interface AIMessage { - id: string - conversationId: string - role: AIMessageRole - content: string - timestamp: number - parentId?: string | null - dataKeywords?: string[] - dataMessageCount?: number - contentBlocks?: AIContentBlock[] - tokenUsage?: AITokenUsageData -} - +/** + * AiApi — IPC-only subset + * + * Conversation/message CRUD and debug queries have been migrated to HTTP + * shared routes (FetchAIAdapter via Internal Server). Only features that + * require worker, native shell, or tool registry remain on IPC. + */ interface AiApi { - createConversation: (sessionId: string, title: string | undefined, assistantId: string) => Promise - getConversations: (sessionId: string) => Promise - getConversation: (conversationId: string) => Promise - updateConversationTitle: (conversationId: string, title: string) => Promise - deleteConversation: (conversationId: string) => Promise - addMessage: ( - conversationId: string, - role: AIMessageRole, - content: string, - dataKeywords?: string[], - dataMessageCount?: number, - contentBlocks?: AIContentBlock[], - tokenUsage?: AITokenUsageData - ) => Promise - deleteMessagesFrom: (conversationId: string, messageId: string) => Promise - forkConversation: (sourceConversationId: string, upToMessageId: string, title?: string) => Promise - updateMessageContent: (messageId: string, newContent: string) => Promise - deleteAndRelinkMessage: (conversationId: string, messageId: string) => Promise - insertMessageAfter: ( - conversationId: string, - afterMessageId: string, - role: AIMessageRole, - content: string, - contentBlocks?: AIContentBlock[], - tokenUsage?: AITokenUsageData - ) => Promise - getMessages: (conversationId: string) => Promise - getConversationTokenUsage: (conversationId: string) => Promise - deleteMessage: (messageId: string) => Promise - showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }> - clearDebugContext: () => Promise<{ success: boolean; cleared: number }> - getDefaultDesensitizeRules: (locale: string) => Promise - mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise - getToolCatalog: () => Promise - executeTool: ( - testId: string, - toolName: string, - params: Record, - sessionId: string - ) => Promise - cancelToolTest: (testId: string) => Promise<{ success: boolean }> - estimateContextTokens: ( - conversationId: string - ) => Promise<{ success: boolean; tokens: number; messageCount?: number; error?: string }> - compressContext: ( - conversationId: string, - compressionConfig: { - enabled: boolean - tokenThresholdPercent: number - bufferSizePercent: number - maxToolResultPercent?: number - }, - systemPrompt: string - ) => Promise<{ - success: boolean - result?: { - compressed: boolean - reason: string - tokensBefore?: number - tokensAfter?: number - error?: string - } - error?: string - }> - // 自定义筛选(支持分页) + // Message filtering / export (worker-dependent) filterMessagesWithContext: ( sessionId: string, keywords?: string[], @@ -286,7 +177,6 @@ interface AiApi { page?: number, pageSize?: number ) => Promise - // 导出筛选结果到文件 exportFilterResultToFile: (params: { sessionId: string sessionName: string @@ -298,19 +188,25 @@ interface AiApi { contextSize?: number chatSessionIds?: number[] }) => Promise<{ success: boolean; filePath?: string; error?: string }> - // 监听导出进度 onExportProgress: (callback: (progress: ExportProgress) => void) => () => void - // [Debug] AI 数据库直接访问 - getAiSchema: () => Promise< - Array<{ name: string; columns: Array<{ name: string; type: string; notnull: boolean; pk: boolean }> }> - > - executeAiSQL: (sql: string) => Promise<{ - columns: string[] - rows: any[][] - rowCount: number - duration: number - limited: boolean - }> + // Native shell + showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }> + // Desensitize rules (node-runtime) + getDefaultDesensitizeRules: (locale: string) => Promise + mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise + // Tool testing (tool registry on main process) + getToolCatalog: () => Promise + executeTool: ( + testId: string, + toolName: string, + params: Record, + sessionId: string + ) => Promise + cancelToolTest: (testId: string) => Promise<{ success: boolean }> + // Context estimation (needs conversationManager) + estimateContextTokens: ( + conversationId: string + ) => Promise<{ success: boolean; tokens: number; messageCount?: number; error?: string }> } // ==================== 新模型系统类型 ==================== @@ -780,8 +676,6 @@ export { CacheApi, NetworkApi, ProxyConfig, - AIConversation, - AIMessage, LLMChatMessage, LLMChatOptions, LLMChatStreamChunk, diff --git a/src/services/ai/electron.ts b/src/services/ai/electron.ts index 8668c0eb..6dcfc05a 100644 --- a/src/services/ai/electron.ts +++ b/src/services/ai/electron.ts @@ -1,114 +1,31 @@ import type { - AIAdapter, - AIConversation, - AIMessage, - AIMessageRole, - ContentBlock, - TokenUsageData, FilterResultWithPagination, ExportFilterParams, ExportProgress, - AiSQLResult, - AiSchemaTable, ToolCatalogEntry, ToolExecuteResult, DesensitizeRule, } from './types' import type { TimeFilter } from '@/types/base' +import { FetchAIAdapter } from './fetch' -export class ElectronAIAdapter implements AIAdapter { - // ===== 对话管理 ===== - async getConversation(conversationId: string): Promise { - return window.aiApi.getConversation(conversationId) - } - - async getConversations(sessionId: string): Promise { - return window.aiApi.getConversations(sessionId) - } - - async createConversation(sessionId: string, title: string | undefined, assistantId: string): Promise { - return window.aiApi.createConversation(sessionId, title, assistantId) - } - - async updateConversationTitle(conversationId: string, title: string): Promise { - return window.aiApi.updateConversationTitle(conversationId, title) - } - - async deleteConversation(conversationId: string): Promise { - return window.aiApi.deleteConversation(conversationId) - } - - // ===== 消息 ===== - async getMessages(conversationId: string): Promise { - return window.aiApi.getMessages(conversationId) as Promise - } - - async addMessage( - conversationId: string, - role: AIMessageRole, - content: string, - dataKeywords?: string[], - dataMessageCount?: number, - contentBlocks?: ContentBlock[], - tokenUsage?: TokenUsageData - ): Promise { - return window.aiApi.addMessage( - conversationId, - role as any, - content, - dataKeywords, - dataMessageCount, - contentBlocks as any, - tokenUsage - ) as Promise - } - - async deleteMessagesFrom(conversationId: string, messageId: string): Promise { - return window.aiApi.deleteMessagesFrom(conversationId, messageId) - } - - async forkConversation(sourceConversationId: string, upToMessageId: string, title?: string): Promise { - return window.aiApi.forkConversation(sourceConversationId, upToMessageId, title) as Promise - } - - async updateMessageContent(messageId: string, newContent: string): Promise { - return window.aiApi.updateMessageContent(messageId, newContent) - } - - async deleteAndRelinkMessage(conversationId: string, messageId: string): Promise { - return window.aiApi.deleteAndRelinkMessage(conversationId, messageId) - } - - async insertMessageAfter( - conversationId: string, - afterMessageId: string, - role: AIMessageRole, - content: string, - contentBlocks?: ContentBlock[], - tokenUsage?: TokenUsageData - ): Promise { - return window.aiApi.insertMessageAfter( - conversationId, - afterMessageId, - role, - content, - contentBlocks as any, - tokenUsage - ) as Promise - } - - async getConversationTokenUsage(conversationId: string): Promise { - return window.aiApi.getConversationTokenUsage(conversationId) - } - - async estimateContextTokens( +/** + * Electron AI Adapter + * + * Extends FetchAIAdapter so conversation/message CRUD and debug queries + * go through the Internal HTTP Server (shared routes). Only features + * that require IPC (worker, native shell, tool registry) are overridden. + */ +export class ElectronAIAdapter extends FetchAIAdapter { + // ===== Context estimation (needs conversationManager on main process) ===== + override async estimateContextTokens( conversationId: string ): Promise<{ success: boolean; tokens: number; messageCount?: number; error?: string }> { return window.aiApi.estimateContextTokens(conversationId) } - // ===== 消息筛选/导出 ===== - async filterMessagesWithContext( + // ===== Message filtering / export (worker-dependent) ===== + override async filterMessagesWithContext( sessionId: string, keywords?: string[], timeFilter?: TimeFilter, @@ -128,7 +45,7 @@ export class ElectronAIAdapter implements AIAdapter { ) } - async getMultipleSessionsMessages( + override async getMultipleSessionsMessages( sessionId: string, chatSessionIds: number[], page?: number, @@ -137,35 +54,22 @@ export class ElectronAIAdapter implements AIAdapter { return window.aiApi.getMultipleSessionsMessages(sessionId, chatSessionIds, page, pageSize) } - async exportFilterResultToFile( + override async exportFilterResultToFile( params: ExportFilterParams ): Promise<{ success: boolean; filePath?: string; error?: string }> { return window.aiApi.exportFilterResultToFile(params) } - onExportProgress(callback: (progress: ExportProgress) => void): () => void { + override onExportProgress(callback: (progress: ExportProgress) => void): () => void { return window.aiApi.onExportProgress(callback) } - // ===== 调试 ===== - async executeAiSQL(sql: string): Promise { - return window.aiApi.executeAiSQL(sql) - } - - async getAiSchema(): Promise { - return window.aiApi.getAiSchema() - } - - async clearDebugContext(): Promise<{ success: boolean; cleared: number }> { - return window.aiApi.clearDebugContext() - } - - // ===== 工具 ===== - async getToolCatalog(): Promise { + // ===== Tool testing (localized catalog + worker execution on main process) ===== + override async getToolCatalog(): Promise { return window.aiApi.getToolCatalog() } - async executeTool( + override async executeTool( testId: string, toolName: string, params: Record, @@ -174,21 +78,21 @@ export class ElectronAIAdapter implements AIAdapter { return window.aiApi.executeTool(testId, toolName, params, sessionId) } - async cancelToolTest(testId: string): Promise<{ success: boolean }> { + override async cancelToolTest(testId: string): Promise<{ success: boolean }> { return window.aiApi.cancelToolTest(testId) } - // ===== 脱敏 ===== - async getDefaultDesensitizeRules(locale: string): Promise { + // ===== Desensitize rules (needs node-runtime) ===== + override async getDefaultDesensitizeRules(locale: string): Promise { return window.aiApi.getDefaultDesensitizeRules(locale) } - async mergeDesensitizeRules(existingRules: DesensitizeRule[], locale: string): Promise { + override async mergeDesensitizeRules(existingRules: DesensitizeRule[], locale: string): Promise { return window.aiApi.mergeDesensitizeRules(existingRules, locale) } - // ===== 日志 ===== - async showAiLogFile(): Promise<{ success: boolean; path?: string; error?: string }> { + // ===== Native shell ===== + override async showAiLogFile(): Promise<{ success: boolean; path?: string; error?: string }> { return window.aiApi.showAiLogFile() } }