feat: 优化AI配置

This commit is contained in:
digua
2025-12-03 23:05:10 +08:00
parent 63ed7765aa
commit 6b2f4e6db6
20 changed files with 2460 additions and 478 deletions
+180 -22
View File
@@ -8,6 +8,64 @@ import { chatStream, chat } from './llm'
import { getAllToolDefinitions, executeToolCalls } from './tools'
import type { ToolContext } from './tools/types'
import { aiLogger } from './logger'
import { randomUUID } from 'crypto'
// ==================== Fallback 解析器 ====================
/**
* 从文本内容中提取 <think> 标签内容
*/
function extractThinkingContent(content: string): { thinking: string; cleanContent: string } {
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi
let thinking = ''
let cleanContent = content
const matches = content.matchAll(thinkRegex)
for (const match of matches) {
thinking += match[1].trim() + '\n'
cleanContent = cleanContent.replace(match[0], '')
}
return { thinking: thinking.trim(), cleanContent: cleanContent.trim() }
}
/**
* 从文本内容中解析 <tool_call> 标签并转换为标准 ToolCall 格式
*/
function parseToolCallTags(content: string): ToolCall[] | null {
const toolCallRegex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi
const toolCalls: ToolCall[] = []
const matches = content.matchAll(toolCallRegex)
for (const match of matches) {
try {
const jsonStr = match[1].trim()
const parsed = JSON.parse(jsonStr)
if (parsed.name && parsed.arguments) {
toolCalls.push({
id: `fallback-${randomUUID()}`,
type: 'function',
function: {
name: parsed.name,
arguments: typeof parsed.arguments === 'string' ? parsed.arguments : JSON.stringify(parsed.arguments),
},
})
}
} catch (e) {
aiLogger.warn('Agent', 'Failed to parse tool_call tag', { content: match[1], error: String(e) })
}
}
return toolCalls.length > 0 ? toolCalls : null
}
/**
* 检测内容是否包含工具调用标签(用于判断是否需要 fallback 解析)
*/
function hasToolCallTags(content: string): boolean {
return /<tool_call>/i.test(content)
}
/**
* Agent 配置
@@ -138,21 +196,56 @@ export class Agent {
contentLength: response.content?.length,
})
// 如果是普通文本响应,完成
let toolCallsToProcess = response.tool_calls
// 如果没有标准 tool_calls,尝试 fallback 解析
if (response.finishReason !== 'tool_calls' || !response.tool_calls) {
aiLogger.info('Agent', '执行完成', {
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
})
return {
content: response.content,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
// Fallback: 检查内容中是否有 <tool_call> 标签
if (hasToolCallTags(response.content)) {
aiLogger.info('Agent', '检测到 <tool_call> 标签,执行 fallback 解析')
// 提取 thinking 内容
const { thinking, cleanContent } = extractThinkingContent(response.content)
if (thinking) {
aiLogger.info('Agent', '提取到 thinking 内容', { length: thinking.length })
}
// 解析 tool_call 标签
const fallbackToolCalls = parseToolCallTags(response.content)
if (fallbackToolCalls && fallbackToolCalls.length > 0) {
aiLogger.info('Agent', 'Fallback 解析成功', {
toolCount: fallbackToolCalls.length,
tools: fallbackToolCalls.map((tc) => tc.function.name),
})
toolCallsToProcess = fallbackToolCalls
} else {
// 解析失败,返回清理后的内容
aiLogger.info('Agent', '执行完成', {
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
})
return {
content: cleanContent,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
}
}
} else {
// 没有 tool_call 标签,正常完成
aiLogger.info('Agent', '执行完成', {
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
})
return {
content: response.content,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
}
}
}
// 处理工具调用
await this.handleToolCalls(response.tool_calls)
await this.handleToolCalls(toolCallsToProcess!)
this.toolRounds++
}
@@ -196,7 +289,9 @@ export class Agent {
// 执行循环
while (this.toolRounds < this.config.maxToolRounds!) {
let accumulatedContent = ''
let displayedContent = '' // 已发送给前端的内容
let toolCalls: ToolCall[] | undefined
let isBufferingToolCall = false // 是否正在缓冲 tool_call 内容
// 流式调用 LLM
for await (const chunk of chatStream(this.messages, {
@@ -205,7 +300,32 @@ export class Agent {
})) {
if (chunk.content) {
accumulatedContent += chunk.content
onChunk({ type: 'content', content: chunk.content })
// 检测是否开始出现 <tool_call> 或 <think> 标签
// 一旦检测到,停止向前端发送后续内容
if (!isBufferingToolCall) {
// 检查累积内容中是否有开始标签
if (/<tool_call>/i.test(accumulatedContent) || /<think>/i.test(accumulatedContent)) {
isBufferingToolCall = true
// 发送标签之前的内容(如果有)
const tagStart = Math.min(
accumulatedContent.indexOf('<tool_call>') >= 0 ? accumulatedContent.indexOf('<tool_call>') : Infinity,
accumulatedContent.indexOf('<think>') >= 0 ? accumulatedContent.indexOf('<think>') : Infinity
)
if (tagStart > displayedContent.length) {
const newContent = accumulatedContent.slice(displayedContent.length, tagStart)
if (newContent) {
onChunk({ type: 'content', content: newContent })
displayedContent = accumulatedContent.slice(0, tagStart)
}
}
} else {
// 正常发送内容
onChunk({ type: 'content', content: chunk.content })
displayedContent = accumulatedContent
}
}
// 如果已经在缓冲模式,不发送内容
}
if (chunk.tool_calls) {
@@ -213,20 +333,58 @@ export class Agent {
}
if (chunk.isFinished) {
// 如果是普通文本响应,完成
// 如果没有标准 tool_calls,尝试 fallback 解析
if (chunk.finishReason !== 'tool_calls' || !toolCalls) {
finalContent = accumulatedContent
onChunk({ type: 'done', isFinished: true })
// Fallback: 检查内容中是否有 <tool_call> 标签
if (hasToolCallTags(accumulatedContent)) {
aiLogger.info('Agent', '检测到 <tool_call> 标签,执行 fallback 解析')
aiLogger.info('Agent', '流式执行完成', {
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
})
// 提取 thinking 内容
const { thinking, cleanContent } = extractThinkingContent(accumulatedContent)
if (thinking) {
aiLogger.info('Agent', '提取到 thinking 内容', { length: thinking.length })
}
return {
content: finalContent,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
// 解析 tool_call 标签
const fallbackToolCalls = parseToolCallTags(accumulatedContent)
if (fallbackToolCalls && fallbackToolCalls.length > 0) {
aiLogger.info('Agent', 'Fallback 解析成功', {
toolCount: fallbackToolCalls.length,
tools: fallbackToolCalls.map((tc) => tc.function.name),
})
toolCalls = fallbackToolCalls
// 更新累积内容为清理后的内容(移除 think 和 tool_call 标签)
accumulatedContent = cleanContent.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '').trim()
// 不返回,继续执行工具调用
} else {
// 解析失败,作为普通响应处理(发送清理后的内容)
const remainingContent = cleanContent.slice(displayedContent.length)
if (remainingContent) {
onChunk({ type: 'content', content: remainingContent })
}
finalContent = cleanContent
onChunk({ type: 'done', isFinished: true })
return {
content: finalContent,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
}
}
} else {
// 没有 tool_call 标签,正常完成
finalContent = accumulatedContent
onChunk({ type: 'done', isFinished: true })
aiLogger.info('Agent', '流式执行完成', {
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
})
return {
content: finalContent,
toolsUsed: this.toolsUsed,
toolRounds: this.toolRounds,
}
}
}
}
+259 -36
View File
@@ -1,21 +1,34 @@
/**
* LLM 服务模块入口
* 提供统一的 LLM 服务管理
* 提供统一的 LLM 服务管理(支持多配置)
*/
import * as fs from 'fs'
import * as path from 'path'
import { app } from 'electron'
import type { LLMConfig, LLMProvider, ILLMService, ProviderInfo, ChatMessage, ChatOptions, ChatStreamChunk } from './types'
import { randomUUID } from 'crypto'
import type {
LLMConfig,
LLMProvider,
ILLMService,
ProviderInfo,
ChatMessage,
ChatOptions,
ChatStreamChunk,
AIServiceConfig,
AIConfigStore,
} from './types'
import { MAX_CONFIG_COUNT } from './types'
import { DeepSeekService, DEEPSEEK_INFO } from './deepseek'
import { QwenService, QWEN_INFO } from './qwen'
import { OpenAICompatibleService, OPENAI_COMPATIBLE_INFO } from './openai-compatible'
import { aiLogger } from '../logger'
// 导出类型
export * from './types'
// 所有支持的提供商信息
export const PROVIDERS: ProviderInfo[] = [DEEPSEEK_INFO, QWEN_INFO]
export const PROVIDERS: ProviderInfo[] = [DEEPSEEK_INFO, QWEN_INFO, OPENAI_COMPATIBLE_INFO]
// 配置文件路径
let CONFIG_PATH: string | null = null
@@ -33,10 +46,9 @@ function getConfigPath(): string {
return CONFIG_PATH
}
/**
* LLM 配置管理
*/
export interface StoredConfig {
// ==================== 旧配置格式(用于迁移)====================
interface LegacyStoredConfig {
provider: LLMProvider
apiKey: string
model?: string
@@ -44,9 +56,70 @@ export interface StoredConfig {
}
/**
* 保存 LLM 配置
* 检测是否为旧格式配置
*/
export function saveLLMConfig(config: StoredConfig): void {
function isLegacyConfig(data: unknown): data is LegacyStoredConfig {
if (!data || typeof data !== 'object') return false
const obj = data as Record<string, unknown>
return 'provider' in obj && 'apiKey' in obj && !('configs' in obj)
}
/**
* 迁移旧配置到新格式
*/
function migrateLegacyConfig(legacy: LegacyStoredConfig): AIConfigStore {
const now = Date.now()
const newConfig: AIServiceConfig = {
id: randomUUID(),
name: getProviderInfo(legacy.provider)?.name || legacy.provider,
provider: legacy.provider,
apiKey: legacy.apiKey,
model: legacy.model,
maxTokens: legacy.maxTokens,
createdAt: now,
updatedAt: now,
}
return {
configs: [newConfig],
activeConfigId: newConfig.id,
}
}
// ==================== 多配置管理 ====================
/**
* 加载配置存储(自动处理迁移)
*/
export function loadConfigStore(): AIConfigStore {
const configPath = getConfigPath()
if (!fs.existsSync(configPath)) {
return { configs: [], activeConfigId: null }
}
try {
const content = fs.readFileSync(configPath, 'utf-8')
const data = JSON.parse(content)
// 检查是否需要迁移
if (isLegacyConfig(data)) {
aiLogger.info('LLM', '检测到旧配置格式,执行迁移')
const migrated = migrateLegacyConfig(data)
saveConfigStore(migrated)
return migrated
}
return data as AIConfigStore
} catch {
return { configs: [], activeConfigId: null }
}
}
/**
* 保存配置存储
*/
export function saveConfigStore(store: AIConfigStore): void {
const configPath = getConfigPath()
const dir = path.dirname(configPath)
@@ -54,55 +127,204 @@ export function saveLLMConfig(config: StoredConfig): void {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
fs.writeFileSync(configPath, JSON.stringify(store, null, 2), 'utf-8')
}
/**
* 加载 LLM 配置
* 获取所有配置列表
*/
export function loadLLMConfig(): StoredConfig | null {
const configPath = getConfigPath()
export function getAllConfigs(): AIServiceConfig[] {
return loadConfigStore().configs
}
if (!fs.existsSync(configPath)) {
return null
/**
* 获取当前激活的配置
*/
export function getActiveConfig(): AIServiceConfig | null {
const store = loadConfigStore()
if (!store.activeConfigId) return null
return store.configs.find((c) => c.id === store.activeConfigId) || null
}
/**
* 获取单个配置
*/
export function getConfigById(id: string): AIServiceConfig | null {
const store = loadConfigStore()
return store.configs.find((c) => c.id === id) || null
}
/**
* 添加新配置
*/
export function addConfig(config: Omit<AIServiceConfig, 'id' | 'createdAt' | 'updatedAt'>): {
success: boolean
config?: AIServiceConfig
error?: string
} {
const store = loadConfigStore()
if (store.configs.length >= MAX_CONFIG_COUNT) {
return { success: false, error: `最多只能添加 ${MAX_CONFIG_COUNT} 个配置` }
}
try {
const content = fs.readFileSync(configPath, 'utf-8')
return JSON.parse(content) as StoredConfig
} catch {
return null
const now = Date.now()
const newConfig: AIServiceConfig = {
...config,
id: randomUUID(),
createdAt: now,
updatedAt: now,
}
store.configs.push(newConfig)
// 如果是第一个配置,自动设为激活
if (store.configs.length === 1) {
store.activeConfigId = newConfig.id
}
saveConfigStore(store)
return { success: true, config: newConfig }
}
/**
* 更新配置
*/
export function updateConfig(
id: string,
updates: Partial<Omit<AIServiceConfig, 'id' | 'createdAt' | 'updatedAt'>>
): { success: boolean; error?: string } {
const store = loadConfigStore()
const index = store.configs.findIndex((c) => c.id === id)
if (index === -1) {
return { success: false, error: '配置不存在' }
}
store.configs[index] = {
...store.configs[index],
...updates,
updatedAt: Date.now(),
}
saveConfigStore(store)
return { success: true }
}
/**
* 删除配置
*/
export function deleteConfig(id: string): { success: boolean; error?: string } {
const store = loadConfigStore()
const index = store.configs.findIndex((c) => c.id === id)
if (index === -1) {
return { success: false, error: '配置不存在' }
}
store.configs.splice(index, 1)
// 如果删除的是当前激活的配置,选择第一个作为新的激活配置
if (store.activeConfigId === id) {
store.activeConfigId = store.configs.length > 0 ? store.configs[0].id : null
}
saveConfigStore(store)
return { success: true }
}
/**
* 设置激活的配置
*/
export function setActiveConfig(id: string): { success: boolean; error?: string } {
const store = loadConfigStore()
const config = store.configs.find((c) => c.id === id)
if (!config) {
return { success: false, error: '配置不存在' }
}
store.activeConfigId = id
saveConfigStore(store)
return { success: true }
}
/**
* 检查是否有激活的配置
*/
export function hasActiveConfig(): boolean {
const config = getActiveConfig()
return config !== null
}
// ==================== 兼容旧 APIdeprecated====================
/**
* @deprecated 使用 loadConfigStore 代替
*/
export function loadLLMConfig(): LegacyStoredConfig | null {
const activeConfig = getActiveConfig()
if (!activeConfig) return null
return {
provider: activeConfig.provider,
apiKey: activeConfig.apiKey,
model: activeConfig.model,
maxTokens: activeConfig.maxTokens,
}
}
/**
* 删除 LLM 配置
* @deprecated 使用 addConfig 或 updateConfig 代替
*/
export function saveLLMConfig(config: LegacyStoredConfig): void {
const store = loadConfigStore()
// 如果有激活配置,更新它;否则创建新的
if (store.activeConfigId) {
updateConfig(store.activeConfigId, config)
} else {
addConfig({
name: getProviderInfo(config.provider)?.name || config.provider,
...config,
})
}
}
/**
* @deprecated 使用 deleteConfig 代替
*/
export function deleteLLMConfig(): void {
const configPath = getConfigPath()
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath)
const store = loadConfigStore()
if (store.activeConfigId) {
deleteConfig(store.activeConfigId)
}
}
/**
* 检查是否已配置 LLM
* @deprecated 使用 hasActiveConfig 代替
*/
export function hasLLMConfig(): boolean {
const config = loadLLMConfig()
return config !== null && !!config.apiKey
return hasActiveConfig()
}
/**
* 扩展的 LLM 配置(包含本地服务特有选项)
*/
interface ExtendedLLMConfig extends LLMConfig {
disableThinking?: boolean
}
/**
* 创建 LLM 服务实例
*/
export function createLLMService(config: LLMConfig): ILLMService {
export function createLLMService(config: ExtendedLLMConfig): ILLMService {
switch (config.provider) {
case 'deepseek':
return new DeepSeekService(config.apiKey, config.model, config.baseUrl)
case 'qwen':
return new QwenService(config.apiKey, config.model, config.baseUrl)
case 'openai-compatible':
return new OpenAICompatibleService(config.apiKey, config.model, config.baseUrl, config.disableThinking)
default:
throw new Error(`Unknown LLM provider: ${config.provider}`)
}
@@ -112,16 +334,18 @@ export function createLLMService(config: LLMConfig): ILLMService {
* 获取当前配置的 LLM 服务实例
*/
export function getCurrentLLMService(): ILLMService | null {
const config = loadLLMConfig()
if (!config || !config.apiKey) {
const activeConfig = getActiveConfig()
if (!activeConfig) {
return null
}
return createLLMService({
provider: config.provider,
apiKey: config.apiKey,
model: config.model,
maxTokens: config.maxTokens,
provider: activeConfig.provider,
apiKey: activeConfig.apiKey,
model: activeConfig.model,
baseUrl: activeConfig.baseUrl,
maxTokens: activeConfig.maxTokens,
disableThinking: activeConfig.disableThinking,
})
}
@@ -217,4 +441,3 @@ export async function* chatStream(messages: ChatMessage[], options?: ChatOptions
throw error
}
}
+313
View File
@@ -0,0 +1,313 @@
/**
* OpenAI Compatible LLM Provider
* 支持任何兼容 OpenAI API 格式的服务(如 Ollama、LocalAI、vLLM 等)
*/
import type {
ILLMService,
LLMProvider,
ChatMessage,
ChatOptions,
ChatResponse,
ChatStreamChunk,
ProviderInfo,
ToolCall,
} from './types'
const DEFAULT_BASE_URL = 'http://localhost:11434/v1'
export const OPENAI_COMPATIBLE_INFO: ProviderInfo = {
id: 'openai-compatible',
name: 'OpenAI 兼容',
description: '支持任何兼容 OpenAI API 的服务(如 Ollama、LocalAI、vLLM 等)',
defaultBaseUrl: DEFAULT_BASE_URL,
models: [
{ id: 'llama3.2', name: 'Llama 3.2', description: 'Meta Llama 3.2 模型' },
{ id: 'qwen2.5', name: 'Qwen 2.5', description: '通义千问 2.5 模型' },
{ id: 'deepseek-r1', name: 'DeepSeek R1', description: 'DeepSeek R1 推理模型' },
],
}
export class OpenAICompatibleService implements ILLMService {
private apiKey: string
private baseUrl: string
private model: string
private disableThinking: boolean
constructor(apiKey: string, model?: string, baseUrl?: string, disableThinking?: boolean) {
this.apiKey = apiKey || 'sk-no-key-required' // 本地服务可能不需要 API Key
this.baseUrl = baseUrl || DEFAULT_BASE_URL
this.model = model || 'llama3.2'
this.disableThinking = disableThinking ?? true // 默认禁用思考模式
}
getProvider(): LLMProvider {
return 'openai-compatible'
}
getModels(): string[] {
return OPENAI_COMPATIBLE_INFO.models.map((m) => m.id)
}
getDefaultModel(): string {
return 'llama3.2'
}
async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
const requestBody: Record<string, unknown> = {
model: this.model,
messages: messages.map((m) => {
const msg: Record<string, unknown> = { role: m.role, content: m.content }
if (m.role === 'tool' && m.tool_call_id) {
msg.tool_call_id = m.tool_call_id
}
if (m.role === 'assistant' && m.tool_calls) {
msg.tool_calls = m.tool_calls
}
return msg
}),
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2048,
stream: false,
}
if (options?.tools && options.tools.length > 0) {
requestBody.tools = options.tools
}
// 禁用思考模式(用于 Qwen3、DeepSeek-R1 等本地模型)
if (this.disableThinking) {
requestBody.chat_template_kwargs = { enable_thinking: false }
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
// 只有在有 API Key 时才添加 Authorization header
if (this.apiKey && this.apiKey !== 'sk-no-key-required') {
headers['Authorization'] = `Bearer ${this.apiKey}`
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody),
})
if (!response.ok) {
const error = await response.text()
throw new Error(`OpenAI Compatible API error: ${response.status} - ${error}`)
}
const data = await response.json()
const choice = data.choices?.[0]
const message = choice?.message
let finishReason: ChatResponse['finishReason'] = 'error'
if (choice?.finish_reason === 'stop') {
finishReason = 'stop'
} else if (choice?.finish_reason === 'length') {
finishReason = 'length'
} else if (choice?.finish_reason === 'tool_calls') {
finishReason = 'tool_calls'
}
let toolCalls: ToolCall[] | undefined
if (message?.tool_calls && Array.isArray(message.tool_calls)) {
toolCalls = message.tool_calls.map((tc: Record<string, unknown>) => ({
id: tc.id as string,
type: 'function' as const,
function: {
name: (tc.function as Record<string, unknown>)?.name as string,
arguments: (tc.function as Record<string, unknown>)?.arguments as string,
},
}))
}
return {
content: message?.content || '',
finishReason,
tool_calls: toolCalls,
usage: data.usage
? {
promptTokens: data.usage.prompt_tokens,
completionTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
}
: undefined,
}
}
async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<ChatStreamChunk> {
const requestBody: Record<string, unknown> = {
model: this.model,
messages: messages.map((m) => {
const msg: Record<string, unknown> = { role: m.role, content: m.content }
if (m.role === 'tool' && m.tool_call_id) {
msg.tool_call_id = m.tool_call_id
}
if (m.role === 'assistant' && m.tool_calls) {
msg.tool_calls = m.tool_calls
}
return msg
}),
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 2048,
stream: true,
}
if (options?.tools && options.tools.length > 0) {
requestBody.tools = options.tools
}
// 禁用思考模式(用于 Qwen3、DeepSeek-R1 等本地模型)
if (this.disableThinking) {
requestBody.chat_template_kwargs = { enable_thinking: false }
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (this.apiKey && this.apiKey !== 'sk-no-key-required') {
headers['Authorization'] = `Bearer ${this.apiKey}`
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody),
})
if (!response.ok) {
const error = await response.text()
throw new Error(`OpenAI Compatible API error: ${response.status} - ${error}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error('Failed to get response reader')
}
const decoder = new TextDecoder()
let buffer = ''
const toolCallsAccumulator: Map<number, { id: string; name: string; arguments: string }> = new Map()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || !trimmed.startsWith('data: ')) continue
const data = trimmed.slice(6)
if (data === '[DONE]') {
if (toolCallsAccumulator.size > 0) {
const toolCalls: ToolCall[] = Array.from(toolCallsAccumulator.values()).map((tc) => ({
id: tc.id,
type: 'function' as const,
function: {
name: tc.name,
arguments: tc.arguments,
},
}))
yield { content: '', isFinished: true, finishReason: 'tool_calls', tool_calls: toolCalls }
} else {
yield { content: '', isFinished: true, finishReason: 'stop' }
}
return
}
try {
const parsed = JSON.parse(data)
const delta = parsed.choices?.[0]?.delta
const finishReason = parsed.choices?.[0]?.finish_reason
if (delta?.content) {
yield {
content: delta.content,
isFinished: false,
}
}
if (delta?.tool_calls && Array.isArray(delta.tool_calls)) {
for (const tc of delta.tool_calls) {
const index = tc.index ?? 0
const existing = toolCallsAccumulator.get(index)
if (existing) {
if (tc.function?.arguments) {
existing.arguments += tc.function.arguments
}
} else {
toolCallsAccumulator.set(index, {
id: tc.id || '',
name: tc.function?.name || '',
arguments: tc.function?.arguments || '',
})
}
}
}
if (finishReason) {
let reason: ChatStreamChunk['finishReason'] = 'error'
if (finishReason === 'stop') {
reason = 'stop'
} else if (finishReason === 'length') {
reason = 'length'
} else if (finishReason === 'tool_calls') {
reason = 'tool_calls'
}
if (toolCallsAccumulator.size > 0) {
const toolCalls: ToolCall[] = Array.from(toolCallsAccumulator.values()).map((tc) => ({
id: tc.id,
type: 'function' as const,
function: {
name: tc.name,
arguments: tc.arguments,
},
}))
yield { content: '', isFinished: true, finishReason: reason, tool_calls: toolCalls }
} else {
yield { content: '', isFinished: true, finishReason: reason }
}
return
}
} catch {
// 忽略解析错误,继续处理下一行
}
}
}
} finally {
reader.releaseLock()
}
}
async validateApiKey(): Promise<boolean> {
try {
const headers: Record<string, string> = {}
if (this.apiKey && this.apiKey !== 'sk-no-key-required') {
headers['Authorization'] = `Bearer ${this.apiKey}`
}
// 尝试调用 models 端点验证连接
const response = await fetch(`${this.baseUrl}/models`, {
method: 'GET',
headers,
})
// 对于本地服务,即使返回 401 也可能是正常的(不需要认证)
// 所以我们主要检查服务是否可达
return response.ok || response.status === 401
} catch {
return false
}
}
}
+32 -1
View File
@@ -5,7 +5,7 @@
/**
* 支持的 LLM 提供商
*/
export type LLMProvider = 'deepseek' | 'qwen'
export type LLMProvider = 'deepseek' | 'qwen' | 'openai-compatible'
/**
* LLM 配置
@@ -167,3 +167,34 @@ export interface ProviderInfo {
}>
}
// ==================== 多配置管理相关类型 ====================
/**
* 单个 AI 服务配置
*/
export interface AIServiceConfig {
id: string // UUID
name: string // 用户自定义名称
provider: LLMProvider
apiKey: string // 可为空(本地 API 场景)
model?: string
baseUrl?: string // 自定义端点
maxTokens?: number
/** 禁用思考模式(用于本地服务,如 Qwen3、DeepSeek-R1 等) */
disableThinking?: boolean
createdAt: number // 创建时间戳
updatedAt: number // 更新时间戳
}
/**
* AI 配置存储结构
*/
export interface AIConfigStore {
configs: AIServiceConfig[]
activeConfigId: string | null
}
/**
* 最大配置数量限制
*/
export const MAX_CONFIG_COUNT = 10
+269 -136
View File
@@ -735,7 +735,14 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle(
'ai:searchMessages',
async (_, sessionId: string, keywords: string[], filter?: { startTs?: number; endTs?: number }, limit?: number, offset?: number) => {
async (
_,
sessionId: string,
keywords: string[],
filter?: { startTs?: number; endTs?: number },
limit?: number,
offset?: number
) => {
aiLogger.info('IPC', '收到搜索消息请求', {
sessionId,
keywords,
@@ -835,7 +842,14 @@ const mainIpcMain = (win: BrowserWindow) => {
*/
ipcMain.handle(
'ai:addMessage',
async (_, conversationId: string, role: 'user' | 'assistant', content: string, dataKeywords?: string[], dataMessageCount?: number) => {
async (
_,
conversationId: string,
role: 'user' | 'assistant',
content: string,
dataKeywords?: string[],
dataMessageCount?: number
) => {
try {
return aiConversations.addMessage(conversationId, role, content, dataKeywords, dataMessageCount)
} catch (error) {
@@ -869,7 +883,7 @@ const mainIpcMain = (win: BrowserWindow) => {
}
})
// ==================== LLM 服务 ====================
// ==================== LLM 服务(多配置管理)====================
/**
* 获取所有支持的 LLM 提供商
@@ -879,68 +893,135 @@ const mainIpcMain = (win: BrowserWindow) => {
})
/**
* 获取当前 LLM 配置
* 获取所有配置列表
*/
ipcMain.handle('llm:getConfig', async () => {
const config = llm.loadLLMConfig()
if (!config) return null
// 不返回完整的 API Key,只返回脱敏版本
return {
provider: config.provider,
apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '',
apiKeySet: !!config.apiKey,
model: config.model,
maxTokens: config.maxTokens,
}
ipcMain.handle('llm:getAllConfigs', async () => {
const configs = llm.getAllConfigs()
// 脱敏 API Key
return configs.map((c) => ({
...c,
apiKey: c.apiKey ? `${c.apiKey.slice(0, 4)}****${c.apiKey.slice(-4)}` : '',
apiKeySet: !!c.apiKey,
}))
})
/**
* 保存 LLM 配置
* 如果 apiKey 为空但已有配置,保留原有的 apiKey
* 获取当前激活的配置 ID
*/
ipcMain.handle('llm:saveConfig', async (_, config: { provider: llm.LLMProvider; apiKey: string; model?: string; maxTokens?: number }) => {
try {
// 如果没有提供新的 API Key,保留原有的
let apiKeyToSave = config.apiKey
if (!apiKeyToSave || apiKeyToSave.trim() === '') {
const existingConfig = llm.loadLLMConfig()
if (existingConfig?.apiKey) {
apiKeyToSave = existingConfig.apiKey
} else {
return { success: false, error: '请输入 API Key' }
}
}
ipcMain.handle('llm:getActiveConfigId', async () => {
const config = llm.getActiveConfig()
return config?.id || null
})
llm.saveLLMConfig({
...config,
apiKey: apiKeyToSave,
})
return { success: true }
/**
* 添加新配置
*/
ipcMain.handle(
'llm:addConfig',
async (
_,
config: {
name: string
provider: llm.LLMProvider
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
}
) => {
try {
const result = llm.addConfig(config)
if (result.success && result.config) {
return {
success: true,
config: {
...result.config,
apiKey: result.config.apiKey
? `${result.config.apiKey.slice(0, 4)}****${result.config.apiKey.slice(-4)}`
: '',
apiKeySet: !!result.config.apiKey,
},
}
}
return result
} catch (error) {
console.error('添加 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 更新配置
*/
ipcMain.handle(
'llm:updateConfig',
async (
_,
id: string,
updates: {
name?: string
provider?: llm.LLMProvider
apiKey?: string
model?: string
baseUrl?: string
maxTokens?: number
}
) => {
try {
// 如果 apiKey 为空字符串,表示不更新 API Key
const cleanUpdates = { ...updates }
if (cleanUpdates.apiKey === '') {
delete cleanUpdates.apiKey
}
return llm.updateConfig(id, cleanUpdates)
} catch (error) {
console.error('更新 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 删除配置
*/
ipcMain.handle('llm:deleteConfig', async (_, id?: string) => {
try {
// 兼容旧 API:如果没有传 id,删除当前激活的配置
if (!id) {
const activeConfig = llm.getActiveConfig()
if (activeConfig) {
return llm.deleteConfig(activeConfig.id)
}
return { success: false, error: '没有激活的配置' }
}
return llm.deleteConfig(id)
} catch (error) {
console.error('保存 LLM 配置失败:', error)
console.error('删除 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 删除 LLM 配置
* 设置激活的配置
*/
ipcMain.handle('llm:deleteConfig', async () => {
ipcMain.handle('llm:setActiveConfig', async (_, id: string) => {
try {
llm.deleteLLMConfig()
return true
return llm.setActiveConfig(id)
} catch (error) {
console.error('删除 LLM 配置失败:', error)
return false
console.error('设置激活配置失败:', error)
return { success: false, error: String(error) }
}
})
/**
* 验证 API Key
* 验证 API Key(支持自定义 baseUrl
*/
ipcMain.handle('llm:validateApiKey', async (_, provider: llm.LLMProvider, apiKey: string) => {
ipcMain.handle('llm:validateApiKey', async (_, provider: llm.LLMProvider, apiKey: string, baseUrl?: string) => {
try {
return await llm.validateApiKey(provider, apiKey)
const service = llm.createLLMService({ provider, apiKey, baseUrl })
return await service.validateApiKey()
} catch (error) {
console.error('验证 API Key 失败:', error)
return false
@@ -948,12 +1029,69 @@ const mainIpcMain = (win: BrowserWindow) => {
})
/**
* 检查是否已配置 LLM
* 检查是否已配置 LLM(是否有激活的配置)
*/
ipcMain.handle('llm:hasConfig', async () => {
return llm.hasLLMConfig()
return llm.hasActiveConfig()
})
// ==================== 兼容旧 APIdeprecated====================
/**
* @deprecated 使用 llm:getAllConfigs 代替
* 获取当前 LLM 配置
*/
ipcMain.handle('llm:getConfig', async () => {
const config = llm.getActiveConfig()
if (!config) return null
return {
provider: config.provider,
apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : '',
apiKeySet: !!config.apiKey,
model: config.model,
baseUrl: config.baseUrl,
maxTokens: config.maxTokens,
}
})
/**
* @deprecated 使用 llm:addConfig 或 llm:updateConfig 代替
* 保存 LLM 配置
*/
ipcMain.handle(
'llm:saveConfig',
async (
_,
config: { provider: llm.LLMProvider; apiKey: string; model?: string; baseUrl?: string; maxTokens?: number }
) => {
try {
const activeConfig = llm.getActiveConfig()
if (activeConfig) {
// 更新现有配置
const updates: Record<string, unknown> = { ...config }
if (!config.apiKey || config.apiKey.trim() === '') {
delete updates.apiKey
}
return llm.updateConfig(activeConfig.id, updates)
} else {
// 创建新配置
if (!config.apiKey || config.apiKey.trim() === '') {
return { success: false, error: '请输入 API Key' }
}
const providerInfo = llm.getProviderInfo(config.provider)
return llm.addConfig({
name: providerInfo?.name || config.provider,
...config,
})
}
} catch (error) {
console.error('保存 LLM 配置失败:', error)
return { success: false, error: String(error) }
}
}
)
/**
* 发送 LLM 聊天请求(非流式)
*/
@@ -979,51 +1117,54 @@ const mainIpcMain = (win: BrowserWindow) => {
* 发送 LLM 聊天请求(流式)
* 使用 IPC 事件发送流式数据
*/
ipcMain.handle('llm:chatStream', async (_, requestId: string, messages: llm.ChatMessage[], options?: llm.ChatOptions) => {
aiLogger.info('IPC', `收到流式聊天请求: ${requestId}`, {
messagesCount: messages.length,
options,
})
ipcMain.handle(
'llm:chatStream',
async (_, requestId: string, messages: llm.ChatMessage[], options?: llm.ChatOptions) => {
aiLogger.info('IPC', `收到流式聊天请求: ${requestId}`, {
messagesCount: messages.length,
options,
})
try {
const generator = llm.chatStream(messages, options)
aiLogger.info('IPC', `创建流式生成器: ${requestId}`)
try {
const generator = llm.chatStream(messages, options)
aiLogger.info('IPC', `创建流式生成器: ${requestId}`)
// 异步处理流式响应
;(async () => {
let chunkIndex = 0
try {
aiLogger.info('IPC', `开始迭代流式响应: ${requestId}`)
for await (const chunk of generator) {
chunkIndex++
aiLogger.debug('IPC', `发送 chunk #${chunkIndex}: ${requestId}`, {
contentLength: chunk.content?.length,
isFinished: chunk.isFinished,
finishReason: chunk.finishReason,
// 异步处理流式响应
;(async () => {
let chunkIndex = 0
try {
aiLogger.info('IPC', `开始迭代流式响应: ${requestId}`)
for await (const chunk of generator) {
chunkIndex++
aiLogger.debug('IPC', `发送 chunk #${chunkIndex}: ${requestId}`, {
contentLength: chunk.content?.length,
isFinished: chunk.isFinished,
finishReason: chunk.finishReason,
})
win.webContents.send('llm:streamChunk', { requestId, chunk })
}
aiLogger.info('IPC', `流式响应完成: ${requestId}`, { totalChunks: chunkIndex })
} catch (error) {
aiLogger.error('IPC', `流式响应出错: ${requestId}`, {
error: String(error),
chunkIndex,
})
win.webContents.send('llm:streamChunk', {
requestId,
chunk: { content: '', isFinished: true, finishReason: 'error' },
error: String(error),
})
win.webContents.send('llm:streamChunk', { requestId, chunk })
}
aiLogger.info('IPC', `流式响应完成: ${requestId}`, { totalChunks: chunkIndex })
} catch (error) {
aiLogger.error('IPC', `流式响应出错: ${requestId}`, {
error: String(error),
chunkIndex,
})
win.webContents.send('llm:streamChunk', {
requestId,
chunk: { content: '', isFinished: true, finishReason: 'error' },
error: String(error),
})
}
})()
})()
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建流式请求失败: ${requestId}`, { error: String(error) })
console.error('LLM 流式聊天失败:', error)
return { success: false, error: String(error) }
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建流式请求失败: ${requestId}`, { error: String(error) })
console.error('LLM 流式聊天失败:', error)
return { success: false, error: String(error) }
}
}
})
)
// ==================== AI Agent API ====================
@@ -1031,65 +1172,57 @@ const mainIpcMain = (win: BrowserWindow) => {
* 执行 Agent 对话(流式)
* Agent 会自动调用工具并返回最终结果
*/
ipcMain.handle(
'agent:runStream',
async (
_,
requestId: string,
userMessage: string,
context: ToolContext
) => {
aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, {
userMessage: userMessage.slice(0, 100),
sessionId: context.sessionId,
})
ipcMain.handle('agent:runStream', async (_, requestId: string, userMessage: string, context: ToolContext) => {
aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, {
userMessage: userMessage.slice(0, 100),
sessionId: context.sessionId,
})
try {
const agent = new Agent(context)
try {
const agent = new Agent(context)
// 异步执行,通过事件发送流式数据
;(async () => {
try {
const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => {
aiLogger.debug('IPC', `Agent chunk: ${requestId}`, {
type: chunk.type,
contentLength: chunk.content?.length,
toolName: chunk.toolName,
})
win.webContents.send('agent:streamChunk', { requestId, chunk })
// 异步执行,通过事件发送流式数据
;(async () => {
try {
const result = await agent.executeStream(userMessage, (chunk: AgentStreamChunk) => {
aiLogger.debug('IPC', `Agent chunk: ${requestId}`, {
type: chunk.type,
contentLength: chunk.content?.length,
toolName: chunk.toolName,
})
win.webContents.send('agent:streamChunk', { requestId, chunk })
})
// 发送完成信息
win.webContents.send('agent:complete', {
requestId,
result: {
content: result.content,
toolsUsed: result.toolsUsed,
toolRounds: result.toolRounds,
},
})
aiLogger.info('IPC', `Agent 执行完成: ${requestId}`, {
// 发送完成信息
win.webContents.send('agent:complete', {
requestId,
result: {
content: result.content,
toolsUsed: result.toolsUsed,
toolRounds: result.toolRounds,
contentLength: result.content.length,
})
} catch (error) {
aiLogger.error('IPC', `Agent 执行出错: ${requestId}`, { error: String(error) })
win.webContents.send('agent:streamChunk', {
requestId,
chunk: { type: 'error', error: String(error), isFinished: true },
})
}
})()
},
})
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建 Agent 请求失败: ${requestId}`, { error: String(error) })
return { success: false, error: String(error) }
}
aiLogger.info('IPC', `Agent 执行完成: ${requestId}`, {
toolsUsed: result.toolsUsed,
toolRounds: result.toolRounds,
contentLength: result.content.length,
})
} catch (error) {
aiLogger.error('IPC', `Agent 执行出错: ${requestId}`, { error: String(error) })
win.webContents.send('agent:streamChunk', {
requestId,
chunk: { type: 'error', error: String(error), isFinished: true },
})
}
})()
return { success: true }
} catch (error) {
aiLogger.error('IPC', `创建 Agent 请求失败: ${requestId}`, { error: String(error) })
return { success: false, error: String(error) }
}
)
})
}
export default mainIpcMain
+77 -5
View File
@@ -143,11 +143,27 @@ interface LLMProviderInfo {
models: Array<{ id: string; name: string; description?: string }>
}
// 单个 AI 服务配置(前端显示用,API Key 已脱敏)
interface AIServiceConfigDisplay {
id: string
name: string
provider: string
apiKey: string // 脱敏后的 API Key
apiKeySet: boolean
model?: string
baseUrl?: string
maxTokens?: number
createdAt: number
updatedAt: number
}
// 兼容旧 API 的配置类型
interface LLMConfig {
provider: string
apiKey: string
apiKeySet: boolean
model?: string
baseUrl?: string
maxTokens?: number
}
@@ -168,18 +184,55 @@ interface LLMChatStreamChunk {
}
interface LlmApi {
// 提供商
getProviders: () => Promise<LLMProviderInfo[]>
// 多配置管理 API
getAllConfigs: () => Promise<AIServiceConfigDisplay[]>
getActiveConfigId: () => Promise<string | null>
addConfig: (config: {
name: string
provider: string
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
}) => Promise<{ success: boolean; config?: AIServiceConfigDisplay; error?: string }>
updateConfig: (
id: string,
updates: {
name?: string
provider?: string
apiKey?: string
model?: string
baseUrl?: string
maxTokens?: number
}
) => Promise<{ success: boolean; error?: string }>
deleteConfig: (id?: string) => Promise<{ success: boolean; error?: string }>
setActiveConfig: (id: string) => Promise<{ success: boolean; error?: string }>
// 验证和检查
validateApiKey: (provider: string, apiKey: string, baseUrl?: string) => Promise<boolean>
hasConfig: () => Promise<boolean>
// 兼容旧 APIdeprecated
/** @deprecated 使用 getAllConfigs 代替 */
getConfig: () => Promise<LLMConfig | null>
/** @deprecated 使用 addConfig 或 updateConfig 代替 */
saveConfig: (config: {
provider: string
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
}) => Promise<{ success: boolean; error?: string }>
deleteConfig: () => Promise<boolean>
validateApiKey: (provider: string, apiKey: string) => Promise<boolean>
hasConfig: () => Promise<boolean>
chat: (messages: LLMChatMessage[], options?: LLMChatOptions) => Promise<{ success: boolean; content?: string; error?: string }>
// 聊天功能
chat: (
messages: LLMChatMessage[],
options?: LLMChatOptions
) => Promise<{ success: boolean; content?: string; error?: string }>
chatStream: (
messages: LLMChatMessage[],
options?: LLMChatOptions,
@@ -229,4 +282,23 @@ declare global {
}
}
export { ChatApi, Api, MergeApi, AiApi, LlmApi, AgentApi, SearchMessageResult, AIConversation, AIMessage, LLMProviderInfo, LLMConfig, LLMChatMessage, LLMChatOptions, LLMChatStreamChunk, AgentStreamChunk, AgentResult, ToolContext }
export {
ChatApi,
Api,
MergeApi,
AiApi,
LlmApi,
AgentApi,
SearchMessageResult,
AIConversation,
AIMessage,
LLMProviderInfo,
LLMConfig,
AIServiceConfigDisplay,
LLMChatMessage,
LLMChatOptions,
LLMChatStreamChunk,
AgentStreamChunk,
AgentResult,
ToolContext,
}
+136 -50
View File
@@ -42,7 +42,14 @@ const api = {
}
},
receive: (channel: string, func: (...args: unknown[]) => void) => {
const validChannels = ['show-message', 'chat:importProgress', 'merge:parseProgress', 'llm:streamChunk', 'agent:streamChunk', 'agent:complete']
const validChannels = [
'show-message',
'chat:importProgress',
'merge:parseProgress',
'llm:streamChunk',
'agent:streamChunk',
'agent:complete',
]
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (_event, ...args) => func(...args))
@@ -499,6 +506,20 @@ interface ToolContext {
timeFilter?: { startTs: number; endTs: number }
}
// AI 服务配置类型(前端用)
interface AIServiceConfigDisplay {
id: string
name: string
provider: string
apiKey: string // 脱敏后的 API Key
apiKeySet: boolean
model?: string
baseUrl?: string
maxTokens?: number
createdAt: number
updatedAt: number
}
const llmApi = {
/**
* 获取所有支持的 LLM 提供商
@@ -507,7 +528,85 @@ const llmApi = {
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
}): 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
}
): 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
*/
validateApiKey: (provider: string, apiKey: string, baseUrl?: string): Promise<boolean> => {
return ipcRenderer.invoke('llm:validateApiKey', provider, apiKey, baseUrl)
},
/**
* 检查是否已配置 LLM(是否有激活的配置)
*/
hasConfig: (): Promise<boolean> => {
return ipcRenderer.invoke('llm:hasConfig')
},
// ==================== 兼容旧 APIdeprecated====================
/**
* @deprecated 使用 getAllConfigs 代替
* 获取当前 LLM 配置
*/
getConfig: (): Promise<LLMConfig | null> => {
@@ -515,42 +614,26 @@ const llmApi = {
},
/**
* @deprecated 使用 addConfig 或 updateConfig 代替
* 保存 LLM 配置
*/
saveConfig: (config: {
provider: string
apiKey: string
model?: string
baseUrl?: string
maxTokens?: number
}): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('llm:saveConfig', config)
},
/**
* 删除 LLM 配置
*/
deleteConfig: (): Promise<boolean> => {
return ipcRenderer.invoke('llm:deleteConfig')
},
/**
* 验证 API Key
*/
validateApiKey: (provider: string, apiKey: string): Promise<boolean> => {
return ipcRenderer.invoke('llm:validateApiKey', provider, apiKey)
},
/**
* 检查是否已配置 LLM
*/
hasConfig: (): Promise<boolean> => {
return ipcRenderer.invoke('llm:hasConfig')
},
/**
* 发送 LLM 聊天请求(非流式)
*/
chat: (messages: ChatMessage[], options?: ChatOptions): Promise<{ success: boolean; content?: string; error?: string }> => {
chat: (
messages: ChatMessage[],
options?: ChatOptions
): Promise<{ success: boolean; content?: string; error?: string }> => {
return ipcRenderer.invoke('llm:chat', messages, options)
},
@@ -597,18 +680,21 @@ const llmApi = {
ipcRenderer.on('llm:streamChunk', handler)
// 发起请求
ipcRenderer.invoke('llm:chatStream', requestId, messages, options).then((result) => {
console.log('[preload] chatStream invoke 返回:', result)
if (!result.success) {
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(result)
}
// 如果 success,等待流完成(由 handler 处理 resolve
}).catch((error) => {
console.error('[preload] chatStream invoke 错误:', error)
ipcRenderer.removeListener('llm:streamChunk', handler)
resolve({ success: false, error: String(error) })
})
resolve({ success: false, error: String(error) })
})
})
},
}
@@ -641,10 +727,7 @@ const agentApi = {
}
// 监听完成事件
const completeHandler = (
_event: Electron.IpcRendererEvent,
data: { requestId: string; result: AgentResult }
) => {
const completeHandler = (_event: Electron.IpcRendererEvent, data: { requestId: string; result: AgentResult }) => {
if (data.requestId === requestId) {
console.log('[preload] Agent 完成,requestId:', requestId)
ipcRenderer.removeListener('agent:streamChunk', chunkHandler)
@@ -657,20 +740,23 @@ const agentApi = {
ipcRenderer.on('agent:complete', completeHandler)
// 发起请求
ipcRenderer.invoke('agent:runStream', requestId, userMessage, context).then((result) => {
console.log('[preload] Agent invoke 返回:', result)
if (!result.success) {
ipcRenderer
.invoke('agent:runStream', requestId, userMessage, context)
.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(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) })
})
resolve({ success: false, error: String(error) })
})
})
},
}