refactor(ai): remove orphaned AI conversation/message IPC handlers

Migrate conversation/message CRUD and debug queries from IPC to HTTP
by having ElectronAIAdapter extend FetchAIAdapter. Remove 18 dead
handlers (14 CRUD + 3 debug + 1 compressContext) from ipc/ai.ts and
slim down preload AiApi to IPC-only methods (filtering, export, tool
testing, desensitize rules, native shell).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
diguaandCursor committed 2026-06-02 15:41:21 +08:00
1 parent 1310b04bc0
commit fcca22a8dc
4 files changed
+74 -833

No files matched your search

+5 -307
View File
@@ -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<string, AbortController>()
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<string, AbortController>()
@@ -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
}
})
}
+18 -273
View File
@@ -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<string, unknown>
}
}
| { 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<string, unknown>
}
/** 工具执行结果 */
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<AIConversation> => {
return ipcRenderer.invoke('ai:createConversation', sessionId, title, assistantId)
},
// ===== 日志(native shell) =====
/**
* 获取会话的所有 AI 对话列表
*/
getConversations: (sessionId: string): Promise<AIConversation[]> => {
return ipcRenderer.invoke('ai:getConversations', sessionId)
},
/**
* 获取单个 AI 对话
*/
getConversation: (conversationId: string): Promise<AIConversation | null> => {
return ipcRenderer.invoke('ai:getConversation', conversationId)
},
/**
* 更新 AI 对话标题
*/
updateConversationTitle: (conversationId: string, title: string): Promise<boolean> => {
return ipcRenderer.invoke('ai:updateConversationTitle', conversationId, title)
},
/**
* 删除 AI 对话
*/
deleteConversation: (conversationId: string): Promise<boolean> => {
return ipcRenderer.invoke('ai:deleteConversation', conversationId)
},
/**
* 添加 AI 消息
*/
addMessage: (
conversationId: string,
role: AIMessageRole,
content: string,
dataKeywords?: string[],
dataMessageCount?: number,
contentBlocks?: ContentBlock[],
tokenUsage?: TokenUsageData
): Promise<AIMessage> => {
return ipcRenderer.invoke(
'ai:addMessage',
conversationId,
role,
content,
dataKeywords,
dataMessageCount,
contentBlocks,
tokenUsage
)
},
deleteMessagesFrom: (conversationId: string, messageId: string): Promise<void> => {
return ipcRenderer.invoke('ai:deleteMessagesFrom', conversationId, messageId)
},
forkConversation: (sourceConversationId: string, upToMessageId: string, title?: string): Promise<AIConversation> => {
return ipcRenderer.invoke('ai:forkConversation', sourceConversationId, upToMessageId, title)
},
updateMessageContent: (messageId: string, newContent: string): Promise<void> => {
return ipcRenderer.invoke('ai:updateMessageContent', messageId, newContent)
},
deleteAndRelinkMessage: (conversationId: string, messageId: string): Promise<void> => {
return ipcRenderer.invoke('ai:deleteAndRelinkMessage', conversationId, messageId)
},
insertMessageAfter: (
conversationId: string,
afterMessageId: string,
role: AIMessageRole,
content: string,
contentBlocks?: ContentBlock[],
tokenUsage?: TokenUsageData
): Promise<AIMessage> => {
return ipcRenderer.invoke(
'ai:insertMessageAfter',
conversationId,
afterMessageId,
role,
content,
contentBlocks,
tokenUsage
)
},
/**
* 获取 AI 对话的所有消息
*/
getMessages: (conversationId: string): Promise<AIMessage[]> => {
return ipcRenderer.invoke('ai:getMessages', conversationId)
},
/**
* 获取对话的累计 token 使用量
*/
getConversationTokenUsage: (conversationId: string): Promise<TokenUsageData> => {
return ipcRenderer.invoke('ai:getConversationTokenUsage', conversationId)
},
/**
* 删除 AI 消息
*/
deleteMessage: (messageId: string): Promise<boolean> => {
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<DesensitizeRule[]> => {
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<ToolCatalogEntry[]> => {
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)
+26 -132
View File
@@ -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<string, unknown>
}
}
| { 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<AIConversation>
getConversations: (sessionId: string) => Promise<AIConversation[]>
getConversation: (conversationId: string) => Promise<AIConversation | null>
updateConversationTitle: (conversationId: string, title: string) => Promise<boolean>
deleteConversation: (conversationId: string) => Promise<boolean>
addMessage: (
conversationId: string,
role: AIMessageRole,
content: string,
dataKeywords?: string[],
dataMessageCount?: number,
contentBlocks?: AIContentBlock[],
tokenUsage?: AITokenUsageData
) => Promise<AIMessage>
deleteMessagesFrom: (conversationId: string, messageId: string) => Promise<void>
forkConversation: (sourceConversationId: string, upToMessageId: string, title?: string) => Promise<AIConversation>
updateMessageContent: (messageId: string, newContent: string) => Promise<void>
deleteAndRelinkMessage: (conversationId: string, messageId: string) => Promise<void>
insertMessageAfter: (
conversationId: string,
afterMessageId: string,
role: AIMessageRole,
content: string,
contentBlocks?: AIContentBlock[],
tokenUsage?: AITokenUsageData
) => Promise<AIMessage>
getMessages: (conversationId: string) => Promise<AIMessage[]>
getConversationTokenUsage: (conversationId: string) => Promise<AITokenUsageData>
deleteMessage: (messageId: string) => Promise<boolean>
showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }>
clearDebugContext: () => Promise<{ success: boolean; cleared: number }>
getDefaultDesensitizeRules: (locale: string) => Promise<DesensitizeRule[]>
mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise<DesensitizeRule[]>
getToolCatalog: () => Promise<ToolCatalogEntry[]>
executeTool: (
testId: string,
toolName: string,
params: Record<string, unknown>,
sessionId: string
) => Promise<ToolExecuteResult>
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<FilterResultWithPagination>
// 导出筛选结果到文件
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<DesensitizeRule[]>
mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise<DesensitizeRule[]>
// Tool testing (tool registry on main process)
getToolCatalog: () => Promise<ToolCatalogEntry[]>
executeTool: (
testId: string,
toolName: string,
params: Record<string, unknown>,
sessionId: string
) => Promise<ToolExecuteResult>
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,
+25 -121
View File
@@ -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<AIConversation | null> {
return window.aiApi.getConversation(conversationId)
}
async getConversations(sessionId: string): Promise<AIConversation[]> {
return window.aiApi.getConversations(sessionId)
}
async createConversation(sessionId: string, title: string | undefined, assistantId: string): Promise<AIConversation> {
return window.aiApi.createConversation(sessionId, title, assistantId)
}
async updateConversationTitle(conversationId: string, title: string): Promise<boolean> {
return window.aiApi.updateConversationTitle(conversationId, title)
}
async deleteConversation(conversationId: string): Promise<boolean> {
return window.aiApi.deleteConversation(conversationId)
}
// ===== 消息 =====
async getMessages(conversationId: string): Promise<AIMessage[]> {
return window.aiApi.getMessages(conversationId) as Promise<AIMessage[]>
}
async addMessage(
conversationId: string,
role: AIMessageRole,
content: string,
dataKeywords?: string[],
dataMessageCount?: number,
contentBlocks?: ContentBlock[],
tokenUsage?: TokenUsageData
): Promise<AIMessage> {
return window.aiApi.addMessage(
conversationId,
role as any,
content,
dataKeywords,
dataMessageCount,
contentBlocks as any,
tokenUsage
) as Promise<AIMessage>
}
async deleteMessagesFrom(conversationId: string, messageId: string): Promise<void> {
return window.aiApi.deleteMessagesFrom(conversationId, messageId)
}
async forkConversation(sourceConversationId: string, upToMessageId: string, title?: string): Promise<AIConversation> {
return window.aiApi.forkConversation(sourceConversationId, upToMessageId, title) as Promise<AIConversation>
}
async updateMessageContent(messageId: string, newContent: string): Promise<void> {
return window.aiApi.updateMessageContent(messageId, newContent)
}
async deleteAndRelinkMessage(conversationId: string, messageId: string): Promise<void> {
return window.aiApi.deleteAndRelinkMessage(conversationId, messageId)
}
async insertMessageAfter(
conversationId: string,
afterMessageId: string,
role: AIMessageRole,
content: string,
contentBlocks?: ContentBlock[],
tokenUsage?: TokenUsageData
): Promise<AIMessage> {
return window.aiApi.insertMessageAfter(
conversationId,
afterMessageId,
role,
content,
contentBlocks as any,
tokenUsage
) as Promise<AIMessage>
}
async getConversationTokenUsage(conversationId: string): Promise<TokenUsageData> {
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<AiSQLResult> {
return window.aiApi.executeAiSQL(sql)
}
async getAiSchema(): Promise<AiSchemaTable[]> {
return window.aiApi.getAiSchema()
}
async clearDebugContext(): Promise<{ success: boolean; cleared: number }> {
return window.aiApi.clearDebugContext()
}
// ===== 工具 =====
async getToolCatalog(): Promise<ToolCatalogEntry[]> {
// ===== Tool testing (localized catalog + worker execution on main process) =====
override async getToolCatalog(): Promise<ToolCatalogEntry[]> {
return window.aiApi.getToolCatalog()
}
async executeTool(
override async executeTool(
testId: string,
toolName: string,
params: Record<string, unknown>,
@@ -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<DesensitizeRule[]> {
// ===== Desensitize rules (needs node-runtime) =====
override async getDefaultDesensitizeRules(locale: string): Promise<DesensitizeRule[]> {
return window.aiApi.getDefaultDesensitizeRules(locale)
}
async mergeDesensitizeRules(existingRules: DesensitizeRule[], locale: string): Promise<DesensitizeRule[]> {
override async mergeDesensitizeRules(existingRules: DesensitizeRule[], locale: string): Promise<DesensitizeRule[]> {
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()
}
}