mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-21 05:40:23 +08:00
feat: 优化AI配置
This commit is contained in:
+180
-22
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
// ==================== 兼容旧 API(deprecated)====================
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
})
|
||||
|
||||
// ==================== 兼容旧 API(deprecated)====================
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
||||
Vendored
+77
-5
@@ -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>
|
||||
|
||||
// 兼容旧 API(deprecated)
|
||||
/** @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
@@ -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')
|
||||
},
|
||||
|
||||
// ==================== 兼容旧 API(deprecated)====================
|
||||
|
||||
/**
|
||||
* @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) })
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user