Files
ChatLab/electron/preload/apis/ai.ts

807 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AI 相关 API - AI 对话、LLM 服务、Agent、Embedding
*/
import { ipcRenderer } from 'electron'
import type { ExportProgress } from '../../../src/types/base'
// ==================== 类型定义 ====================
// AI API 类型
export interface SearchMessageResult {
id: number
senderName: string
senderPlatformId: string
senderAliases: string[]
senderAvatar: string | null
content: string
timestamp: number
type: number
}
export interface AIConversation {
id: string
sessionId: string
title: string | null
createdAt: number
updatedAt: number
}
// 内容块类型(用于 AI 消息的混合渲染)
export type ContentBlock =
| { type: 'text'; text: string }
| {
type: 'tool'
tool: {
name: string
displayName: string
status: 'running' | 'done' | 'error'
params?: Record<string, unknown>
}
}
export interface AIMessage {
id: string
conversationId: string
role: 'user' | 'assistant'
content: string
timestamp: number
dataKeywords?: string[]
dataMessageCount?: number
contentBlocks?: ContentBlock[]
}
// LLM API 类型
export interface LLMProvider {
id: string
name: string
description: string
defaultBaseUrl: string
models: Array<{ id: string; name: string; description?: string }>
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export interface ChatOptions {
temperature?: number
maxTokens?: number
}
export interface ChatStreamChunk {
content: string
isFinished: boolean
finishReason?: 'stop' | 'length' | 'error'
}
// Agent API 类型
export interface AgentStreamChunk {
type: 'content' | 'think' | 'tool_start' | 'tool_result' | 'done' | 'error'
content?: string
thinkTag?: string
thinkDurationMs?: number
toolName?: string
toolParams?: Record<string, unknown>
toolResult?: unknown
error?: string
isFinished?: boolean
}
export interface AgentResult {
content: string
toolsUsed: string[]
toolRounds: number
}
export interface ToolContext {
sessionId: string
timeFilter?: { startTs: number; endTs: number }
}
// AI 服务配置类型(前端用)
export interface AIServiceConfigDisplay {
id: string
name: string
provider: string
apiKey: string // 脱敏后的 API Key
apiKeySet: boolean
model?: string
baseUrl?: string
maxTokens?: number
disableThinking?: boolean
isReasoningModel?: boolean
createdAt: number
updatedAt: number
}
// Embedding 服务配置(多配置模式)
export interface EmbeddingServiceConfig {
id: string
name: string
apiSource: 'reuse_llm' | 'custom'
model: string
baseUrl?: string
apiKey?: string
createdAt: number
updatedAt: number
}
// Embedding 配置展示用(隐藏 apiKey
export interface EmbeddingServiceConfigDisplay {
id: string
name: string
apiSource: 'reuse_llm' | 'custom'
model: string
baseUrl?: string
apiKeySet: boolean
createdAt: number
updatedAt: number
}
// 用户自定义提示词配置
export interface PromptConfig {
roleDefinition: string
responseRules: string
}
// ==================== AI API ====================
export const aiApi = {
/**
* 搜索消息(关键词搜索)
* @param senderId 可选的发送者成员 ID用于筛选特定成员的消息
*/
searchMessages: (
sessionId: string,
keywords: string[],
filter?: { startTs?: number; endTs?: number },
limit?: number,
offset?: number,
senderId?: number
): Promise<{ messages: SearchMessageResult[]; total: number }> => {
return ipcRenderer.invoke('ai:searchMessages', sessionId, keywords, filter, limit, offset, senderId)
},
/**
* 获取消息上下文
* @param messageIds 支持单个或批量消息 ID
*/
getMessageContext: (
sessionId: string,
messageIds: number | number[],
contextSize?: number
): Promise<SearchMessageResult[]> => {
return ipcRenderer.invoke('ai:getMessageContext', sessionId, messageIds, contextSize)
},
/**
* 获取最近消息AI Agent 专用)
*/
getRecentMessages: (
sessionId: string,
filter?: { startTs?: number; endTs?: number },
limit?: number
): Promise<{ messages: SearchMessageResult[]; total: number }> => {
return ipcRenderer.invoke('ai:getRecentMessages', sessionId, filter, limit)
},
/**
* 获取所有最近消息(消息查看器专用)
*/
getAllRecentMessages: (
sessionId: string,
filter?: { startTs?: number; endTs?: number },
limit?: number
): Promise<{ messages: SearchMessageResult[]; total: number }> => {
return ipcRenderer.invoke('ai:getAllRecentMessages', sessionId, filter, limit)
},
/**
* 获取两人之间的对话
*/
getConversationBetween: (
sessionId: string,
memberId1: number,
memberId2: number,
filter?: { startTs?: number; endTs?: number },
limit?: number
): Promise<{ messages: SearchMessageResult[]; total: number; member1Name: string; member2Name: string }> => {
return ipcRenderer.invoke('ai:getConversationBetween', sessionId, memberId1, memberId2, filter, limit)
},
/**
* 获取指定消息之前的 N 条(用于向上无限滚动)
*/
getMessagesBefore: (
sessionId: string,
beforeId: number,
limit?: number,
filter?: { startTs?: number; endTs?: number },
senderId?: number,
keywords?: string[]
): Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> => {
return ipcRenderer.invoke('ai:getMessagesBefore', sessionId, beforeId, limit, filter, senderId, keywords)
},
/**
* 获取指定消息之后的 N 条(用于向下无限滚动)
*/
getMessagesAfter: (
sessionId: string,
afterId: number,
limit?: number,
filter?: { startTs?: number; endTs?: number },
senderId?: number,
keywords?: string[]
): Promise<{ messages: SearchMessageResult[]; hasMore: boolean }> => {
return ipcRenderer.invoke('ai:getMessagesAfter', sessionId, afterId, limit, filter, senderId, keywords)
},
// ==================== 自定义筛选(支持分页) ====================
/**
* 筛选结果消息类型
*/
// FilterMessage 和 FilterResult 类型定义在下方
/**
* 按条件筛选消息并扩充上下文(支持分页)
* @param page 页码(从 1 开始,默认 1
* @param pageSize 每页块数(默认 50
*/
filterMessagesWithContext: (
sessionId: string,
keywords?: string[],
timeFilter?: { startTs: number; endTs: number },
senderIds?: number[],
contextSize?: number,
page?: number,
pageSize?: number
): Promise<{
blocks: Array<{
startTs: number
endTs: number
messages: Array<{
id: number
senderName: string
senderPlatformId: string
senderAliases: string[]
senderAvatar: string | null
content: string
timestamp: number
type: number
replyToMessageId: string | null
replyToContent: string | null
replyToSenderName: string | null
isHit: boolean
}>
hitCount: number
}>
stats: {
totalMessages: number
hitMessages: number
totalChars: number
}
pagination: {
page: number
pageSize: number
totalBlocks: number
totalHits: number
hasMore: boolean
}
}> => {
return ipcRenderer.invoke(
'ai:filterMessagesWithContext',
sessionId,
keywords,
timeFilter,
senderIds,
contextSize,
page,
pageSize
)
},
/**
* 获取多个会话的完整消息(支持分页)
* @param page 页码(从 1 开始,默认 1
* @param pageSize 每页块数(默认 50
*/
getMultipleSessionsMessages: (
sessionId: string,
chatSessionIds: number[],
page?: number,
pageSize?: number
): Promise<{
blocks: Array<{
startTs: number
endTs: number
messages: Array<{
id: number
senderName: string
senderPlatformId: string
senderAliases: string[]
senderAvatar: string | null
content: string
timestamp: number
type: number
replyToMessageId: string | null
replyToContent: string | null
replyToSenderName: string | null
isHit: boolean
}>
hitCount: number
}>
stats: {
totalMessages: number
hitMessages: number
totalChars: number
}
pagination: {
page: number
pageSize: number
totalBlocks: number
totalHits: number
hasMore: boolean
}
}> => {
return ipcRenderer.invoke('ai:getMultipleSessionsMessages', sessionId, chatSessionIds, page, pageSize)
},
/**
* 导出筛选结果到文件(后端生成,支持大数据量)
*/
exportFilterResultToFile: (params: {
sessionId: string
sessionName: string
outputDir: string
filterMode: 'condition' | 'session'
keywords?: string[]
timeFilter?: { startTs: number; endTs: number }
senderIds?: number[]
contextSize?: number
chatSessionIds?: number[]
}): Promise<{ success: boolean; filePath?: string; error?: string }> => {
return ipcRenderer.invoke('ai:exportFilterResultToFile', params)
},
/**
* 监听导出进度
*/
onExportProgress: (callback: (progress: ExportProgress) => void) => {
const handler = (_event: Electron.IpcRendererEvent, progress: ExportProgress) => {
callback(progress)
}
ipcRenderer.on('ai:exportProgress', handler)
return () => {
ipcRenderer.removeListener('ai:exportProgress', handler)
}
},
/**
* 创建 AI 对话
*/
createConversation: (sessionId: string, title?: string): Promise<AIConversation> => {
return ipcRenderer.invoke('ai:createConversation', sessionId, title)
},
/**
* 获取会话的所有 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: 'user' | 'assistant',
content: string,
dataKeywords?: string[],
dataMessageCount?: number,
contentBlocks?: ContentBlock[]
): Promise<AIMessage> => {
return ipcRenderer.invoke(
'ai:addMessage',
conversationId,
role,
content,
dataKeywords,
dataMessageCount,
contentBlocks
)
},
/**
* 获取 AI 对话的所有消息
*/
getMessages: (conversationId: string): Promise<AIMessage[]> => {
return ipcRenderer.invoke('ai:getMessages', 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')
},
}
// ==================== LLM API ====================
export const llmApi = {
/**
* 获取所有支持的 LLM 提供商
*/
getProviders: (): Promise<LLMProvider[]> => {
return ipcRenderer.invoke('llm:getProviders')
},
// ==================== 多配置管理 API ====================
/**
* 获取所有配置列表
*/
getAllConfigs: (): Promise<AIServiceConfigDisplay[]> => {
return ipcRenderer.invoke('llm:getAllConfigs')
},
/**
* 获取当前激活的配置 ID
*/
getActiveConfigId: (): Promise<string | null> => {
return ipcRenderer.invoke('llm:getActiveConfigId')
},
/**
* 添加新配置
*/
addConfig: (config: {
name: string
provider: string
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
disableThinking?: boolean
isReasoningModel?: boolean
}): Promise<{ success: boolean; config?: AIServiceConfigDisplay; error?: string }> => {
return ipcRenderer.invoke('llm:addConfig', config)
},
/**
* 更新配置
*/
updateConfig: (
id: string,
updates: {
name?: string
provider?: string
apiKey?: string
model?: string
baseUrl?: string
maxTokens?: number
disableThinking?: boolean
isReasoningModel?: boolean
}
): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('llm:updateConfig', id, updates)
},
/**
* 删除配置
*/
deleteConfig: (id?: string): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('llm:deleteConfig', id)
},
/**
* 设置激活的配置
*/
setActiveConfig: (id: string): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('llm:setActiveConfig', id)
},
/**
* 验证 API Key支持自定义 baseUrl 和 model
*/
validateApiKey: (provider: string, apiKey: string, baseUrl?: string, model?: string): Promise<boolean> => {
return ipcRenderer.invoke('llm:validateApiKey', provider, apiKey, baseUrl, model)
},
/**
* 检查是否已配置 LLM是否有激活的配置
*/
hasConfig: (): Promise<boolean> => {
return ipcRenderer.invoke('llm:hasConfig')
},
/**
* 发送 LLM 聊天请求(非流式)
*/
chat: (
messages: ChatMessage[],
options?: ChatOptions
): Promise<{ success: boolean; content?: string; error?: string }> => {
return ipcRenderer.invoke('llm:chat', messages, options)
},
/**
* 发送 LLM 聊天请求(流式)
* 返回一个 Promise该 Promise 在流完成后才 resolve
*/
chatStream: (
messages: ChatMessage[],
options?: ChatOptions,
onChunk?: (chunk: ChatStreamChunk) => void
): Promise<{ success: boolean; error?: string }> => {
return new Promise((resolve) => {
const requestId = `llm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
console.log('[preload] chatStream 开始requestId:', requestId)
const handler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; chunk: ChatStreamChunk; error?: string }
) => {
if (data.requestId === requestId) {
if (data.error) {
console.log('[preload] chatStream 收到错误:', data.error)
if (onChunk) {
onChunk({ content: '', isFinished: true, finishReason: 'error' })
}
ipcRenderer.removeListener('llm:streamChunk', handler)
resolve({ success: false, error: data.error })
} else {
if (onChunk) {
onChunk(data.chunk)
}
// 如果已完成,移除监听器并 resolve
if (data.chunk.isFinished) {
console.log('[preload] chatStream 完成requestId:', requestId)
ipcRenderer.removeListener('llm:streamChunk', handler)
resolve({ success: true })
}
}
}
}
ipcRenderer.on('llm:streamChunk', handler)
// 发起请求
ipcRenderer
.invoke('llm:chatStream', requestId, messages, options)
.then((result) => {
console.log('[preload] chatStream invoke 返回:', result)
if (!result.success) {
ipcRenderer.removeListener('llm:streamChunk', handler)
resolve(result)
}
// 如果 success等待流完成由 handler 处理 resolve
})
.catch((error) => {
console.error('[preload] chatStream invoke 错误:', error)
ipcRenderer.removeListener('llm:streamChunk', handler)
resolve({ success: false, error: String(error) })
})
})
},
}
// ==================== Agent API ====================
export const agentApi = {
/**
* 执行 Agent 对话(流式)
* Agent 会自动调用工具获取数据并生成回答
* @param historyMessages 对话历史(可选,用于上下文关联)
* @param chatType 聊天类型('group' | 'private'
* @param promptConfig 用户自定义提示词配置(可选)
* @param locale 语言设置(可选,默认 'zh-CN'
* @returns 返回 { requestId, promise }requestId 可用于中止请求
*/
runStream: (
userMessage: string,
context: ToolContext,
onChunk?: (chunk: AgentStreamChunk) => void,
historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>,
chatType?: 'group' | 'private',
promptConfig?: PromptConfig,
locale?: string
): { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> } => {
const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
console.log(
'[preload] Agent runStream 开始requestId:',
requestId,
'historyLength:',
historyMessages?.length ?? 0,
'chatType:',
chatType ?? 'group',
'hasPromptConfig:',
!!promptConfig
)
const promise = new Promise<{ success: boolean; result?: AgentResult; error?: string }>((resolve) => {
// 监听流式 chunks
const chunkHandler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; chunk: AgentStreamChunk }
) => {
if (data.requestId === requestId) {
if (onChunk) {
onChunk(data.chunk)
}
}
}
// 监听完成事件
const completeHandler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; result: AgentResult & { error?: string } }
) => {
if (data.requestId === requestId) {
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 {
resolve({ success: true, result: data.result })
}
}
}
ipcRenderer.on('agent:streamChunk', chunkHandler)
ipcRenderer.on('agent:complete', completeHandler)
// 发起请求(传递历史消息、聊天类型、提示词配置和语言设置)
ipcRenderer
.invoke('agent:runStream', requestId, userMessage, context, historyMessages, chatType, promptConfig, locale)
.then((result) => {
console.log('[preload] Agent invoke 返回:', result)
if (!result.success) {
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
ipcRenderer.removeListener('agent:complete', completeHandler)
resolve(result)
}
// 如果 success等待完成由 completeHandler 处理 resolve
})
.catch((error) => {
console.error('[preload] Agent invoke 错误:', error)
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
ipcRenderer.removeListener('agent:complete', completeHandler)
resolve({ success: false, error: String(error) })
})
})
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)
},
}
// ==================== Embedding API ====================
export const embeddingApi = {
/**
* 获取所有 Embedding 配置(展示用)
*/
getAllConfigs: (): Promise<EmbeddingServiceConfigDisplay[]> => {
return ipcRenderer.invoke('embedding:getAllConfigs')
},
/**
* 获取单个 Embedding 配置(用于编辑)
*/
getConfig: (id: string): Promise<EmbeddingServiceConfig | null> => {
return ipcRenderer.invoke('embedding:getConfig', id)
},
/**
* 获取激活的配置 ID
*/
getActiveConfigId: (): Promise<string | null> => {
return ipcRenderer.invoke('embedding:getActiveConfigId')
},
/**
* 检查语义搜索是否启用
*/
isEnabled: (): Promise<boolean> => {
return ipcRenderer.invoke('embedding:isEnabled')
},
/**
* 添加 Embedding 配置
*/
addConfig: (
config: Omit<EmbeddingServiceConfig, 'id' | 'createdAt' | 'updatedAt'>
): Promise<{ success: boolean; config?: EmbeddingServiceConfig; error?: string }> => {
return ipcRenderer.invoke('embedding:addConfig', config)
},
/**
* 更新 Embedding 配置
*/
updateConfig: (
id: string,
updates: Partial<Omit<EmbeddingServiceConfig, 'id' | 'createdAt' | 'updatedAt'>>
): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('embedding:updateConfig', id, updates)
},
/**
* 删除 Embedding 配置
*/
deleteConfig: (id: string): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('embedding:deleteConfig', id)
},
/**
* 设置激活的配置
*/
setActiveConfig: (id: string): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('embedding:setActiveConfig', id)
},
/**
* 验证 Embedding 配置
*/
validateConfig: (config: EmbeddingServiceConfig): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('embedding:validateConfig', config)
},
/**
* 获取向量存储统计信息
*/
getVectorStoreStats: (): Promise<{ enabled: boolean; count?: number; sizeBytes?: number }> => {
return ipcRenderer.invoke('rag:getVectorStoreStats')
},
/**
* 清空向量存储
*/
clearVectorStore: (): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('rag:clearVectorStore')
},
}