export interface ModelService { id: string name: string type: 'openai' | 'claude' | 'gemini' | 'azure' | 'local' | 'dashscope' | 'volcengine' | 'custom' url: string apiKey: string status: 'connected' | 'disconnected' | 'connecting' | 'error' models?: string[] lastUsed?: Date customConfig?: string errorMessage?: string } export interface ApiResponse { success: boolean data?: T error?: string } export class ModelServiceManager { private services: Map = new Map() private static instance: ModelServiceManager static getInstance(): ModelServiceManager { if (!ModelServiceManager.instance) { ModelServiceManager.instance = new ModelServiceManager() // 自动加载保存的服务 ModelServiceManager.instance.loadFromModelStore() } return ModelServiceManager.instance } // 从 modelStore (localStorage) 加载服务配置 loadFromModelStore(): void { try { const saved = localStorage.getItem('model-providers') if (!saved) { console.log('🔍 [loadFromModelStore] 没有找到保存的服务') return } const providers = JSON.parse(saved) console.log('🔍 [loadFromModelStore] 加载服务:', providers.length, '个') providers.forEach((provider: any) => { // 将 modelStore 的 provider 格式转换为 ModelService 格式 // 关键判断逻辑: // 1. enabled === true (明确启用) // 2. connected === true (已连接) // 3. 如果两者都是 undefined,但有 apiKey,也认为是可用的 const isEnabled = provider.enabled === true || provider.connected === true const hasApiKey = provider.apiKey && provider.apiKey.length > 0 const shouldConnect = isEnabled || (provider.enabled !== false && hasApiKey) // 解析模型列表 let modelList: string[] = [] if (provider.models && Array.isArray(provider.models)) { modelList = provider.models.map((m: any) => typeof m === 'string' ? m : (m.id || m.name || '') ).filter((m: string) => m.length > 0) } const service: ModelService = { id: provider.id, name: provider.name, type: this.mapProviderType(provider.type), url: provider.baseUrl || provider.url || '', apiKey: provider.apiKey || '', status: shouldConnect ? 'connected' : 'disconnected', models: modelList } this.services.set(service.id, service) console.log('🔍 [loadFromModelStore] 添加服务:', { name: service.name, enabled: provider.enabled, connected: provider.connected, hasApiKey, shouldConnect, status: service.status, 模型数: service.models?.length, 前3个模型: service.models?.slice(0, 3) }) }) } catch (error) { console.error('❌ [loadFromModelStore] 加载失败:', error) } } // 映射 provider type 到 service type private mapProviderType(type: string): ModelService['type'] { const map: Record = { 'openai': 'openai', 'claude': 'claude', 'google': 'gemini', 'ollama': 'local', 'volcengine': 'volcengine', // 火山引擎 'dashscope': 'dashscope', // 阿里云通义千问 'azure': 'azure', 'local': 'local', 'custom': 'custom' } const mapped = map[type] || 'custom' console.log('🔍 [mapProviderType]', type, '→', mapped) return mapped } // 测试服务连接 async testConnection(service: ModelService): Promise> { try { const models = await this.fetchModels(service) return { success: true, data: { models } } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '连接失败' } } } // 测试服务连接(用于预定义模型列表的服务) private async testServiceConnection(service: ModelService): Promise { const headers: HeadersInit = { 'Content-Type': 'application/json' } // 设置认证头 switch (service.type) { case 'volcengine': case 'openai': case 'local': case 'dashscope': headers['Authorization'] = `Bearer ${service.apiKey}` break case 'claude': headers['x-api-key'] = service.apiKey headers['anthropic-version'] = '2023-06-01' break } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 10000) try { // 发送一个简单的测试请求 const testUrl = `${service.url}/chat/completions` const response = await fetch(testUrl, { method: 'POST', headers, signal: controller.signal, body: JSON.stringify({ model: service.type === 'volcengine' ? 'doubao-lite-4k' : 'test', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 }) }) clearTimeout(timeoutId) // 只要不是认证错误就算通过 if (response.status === 401 || response.status === 403) { const errorText = await response.text() throw new Error(`认证失败: ${errorText}`) } } catch (error) { clearTimeout(timeoutId) if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('连接超时') } throw error } throw new Error('连接测试失败') } } // 获取可用模型列表 private async fetchModels(service: ModelService): Promise { // 某些服务使用预定义模型列表,不需要 API 调用 const url = this.getModelsEndpoint(service) if (!url) { // 对于使用预定义模型列表的服务,发送一个测试请求验证连接 await this.testServiceConnection(service) // 返回预定义模型列表 return this.parseModelsResponse({}, service.type) } const headers: HeadersInit = { 'Content-Type': 'application/json' } // 根据服务类型设置认证头 switch (service.type) { case 'openai': case 'local': case 'dashscope': case 'volcengine': headers['Authorization'] = `Bearer ${service.apiKey}` break case 'claude': headers['x-api-key'] = service.apiKey headers['anthropic-version'] = '2023-06-01' break case 'gemini': // Gemini使用URL参数传递API密钥 break case 'azure': headers['api-key'] = service.apiKey break case 'custom': // 解析自定义配置 try { const config = JSON.parse(service.customConfig || '{}') Object.assign(headers, config.headers || {}) } catch (e) { console.warn('自定义配置解析失败:', e) } break } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 10000) // 10秒超时 try { const response = await fetch(url, { method: 'GET', headers, signal: controller.signal }) clearTimeout(timeoutId) if (!response.ok) { const errorText = await response.text() throw new Error(`HTTP ${response.status}: ${errorText}`) } const data = await response.json() return this.parseModelsResponse(data, service.type) } catch (error) { clearTimeout(timeoutId) if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error('连接超时') } throw error } throw new Error('未知错误') } } // 获取模型列表API端点 private getModelsEndpoint(service: ModelService): string { switch (service.type) { case 'openai': case 'local': return `${service.url}/models` case 'dashscope': // 阿里云 DashScope 使用 /models 端点 return `${service.url}/models` case 'volcengine': // 火山引擎使用预定义模型列表(API 不提供 /models 端点) return '' case 'claude': // Claude API 没有公开的模型列表端点,返回预定义模型 return '' case 'gemini': return `${service.url}/models?key=${service.apiKey}` case 'azure': // Azure OpenAI 使用不同的端点格式 const azureUrl = service.url.replace(/\/$/, '') return `${azureUrl}/openai/deployments?api-version=2023-12-01-preview` case 'custom': return `${service.url}/models` default: return `${service.url}/models` } } // 解析不同服务的模型响应 private parseModelsResponse(data: any, type: string): string[] { switch (type) { case 'openai': case 'local': if (data.data && Array.isArray(data.data)) { return data.data.map((model: any) => model.id).filter(Boolean) } break case 'dashscope': return [ 'qwen3-max', 'qwen3-vl-30b-a3b-thinking', 'qwen3-vl-8b-thinking', 'qwen-flash', 'qwen-turbo-latest', // 通义千问 Turbo 最新版 - 高性价比,响应快 'qwen-plus', // 通义千问增强版 - 推理能力强 'qwen-long', // 通义千问长文本版 - 支持超长上下文(1M tokens) 'qwen3-omni-flash' // 通义千问全能闪电版 - 多模态,极速响应 ] case 'volcengine': // 火山引擎推荐模型列表 // 参考: https://www.volcengine.com/docs/82379/1330310 return [ // DeepSeek-V3 系列 - 深度思考模型 'deepseek-v3-1-terminus', // DeepSeek V3.1 terminus版本 'deepseek-v3-1-250821', // DeepSeek V3.1 250821版本 'doubao-seed-1-6-flash', // 快速多模态深度思考 'doubao-seed-1-6-vision-250815', // 多模态深度思考(图片+视频+GUI) 'doubao-seed-1-6-250615', // 纯文本深度思考 'doubao-seed-1-6-thinking-250715', // 纯思考模型 ] case 'claude': // Claude 预定义模型列表 return [ 'claude-3-5-sonnet-20241022', 'claude-3-haiku-20240307', 'claude-3-sonnet-20240229', 'claude-3-opus-20240229' ] case 'gemini': if (data.models && Array.isArray(data.models)) { return data.models .map((model: any) => model.name?.replace('models/', '')) .filter(Boolean) } break case 'azure': if (data.data && Array.isArray(data.data)) { return data.data.map((deployment: any) => deployment.id).filter(Boolean) } break case 'custom': // 尝试多种可能的响应格式 if (data.models && Array.isArray(data.models)) { return data.models.map((m: any) => typeof m === 'string' ? m : m.id || m.name).filter(Boolean) } if (data.data && Array.isArray(data.data)) { return data.data.map((m: any) => typeof m === 'string' ? m : m.id || m.name).filter(Boolean) } if (Array.isArray(data)) { return data.map((m: any) => typeof m === 'string' ? m : m.id || m.name).filter(Boolean) } break } return [] } // 发送聊天请求 async sendChatRequest(serviceId: string, messages: any[], model: string): Promise> { const startTime = performance.now() console.log('⏱️ [sendChatRequest] 开始请求', { serviceId, model, messages数量: messages.length }) const service = this.services.get(serviceId) console.log('🔍 [sendChatRequest] serviceId:', serviceId, 'service:', service) if (!service || service.status !== 'connected') { return { success: false, error: '服务未连接' } } // 检查URL是否有效 if (!service.url || !service.url.startsWith('http')) { console.error('❌ [sendChatRequest] 无效的服务URL:', service.url) return { success: false, error: `服务URL无效: ${service.url}` } } try { const beforeRequest = performance.now() console.log('⏱️ [sendChatRequest] 准备耗时:', (beforeRequest - startTime).toFixed(2), 'ms') const response = await this.makeChatRequest(service, messages, model) const afterRequest = performance.now() console.log('⏱️ [sendChatRequest] 请求耗时:', (afterRequest - beforeRequest).toFixed(2), 'ms') console.log('⏱️ [sendChatRequest] 总耗时:', (afterRequest - startTime).toFixed(2), 'ms') return { success: true, data: response } } catch (error) { console.error('❌ [sendChatRequest] 请求异常:', error) return { success: false, error: error instanceof Error ? error.message : '请求失败' } } } // 发送流式聊天请求 async sendChatRequestStream( serviceId: string, messages: any[], model: string, onChunk: (chunk: string) => void, tools?: any[], signal?: AbortSignal ): Promise> { const startTime = performance.now() console.log('🚀🚀🚀 [sendChatRequestStream] === 进入流式请求方法 ===') console.log('⏱️ [sendChatRequestStream] 开始流式请求', { serviceId, model, messages数量: messages.length }) const service = this.services.get(serviceId) if (!service || service.status !== 'connected') { return { success: false, error: '服务未连接' } } if (!service.url || !service.url.startsWith('http')) { return { success: false, error: `服务URL无效: ${service.url}` } } try { const toolCalls = await this.makeChatRequestStream(service, messages, model, onChunk, tools, signal) const endTime = performance.now() console.log('⏱️ [sendChatRequestStream] 流式请求完成,总耗时:', (endTime - startTime).toFixed(2), 'ms') return { success: true, data: { toolCalls } } } catch (error) { console.error('❌ [sendChatRequestStream] 流式请求异常:', error) return { success: false, error: error instanceof Error ? error.message : '流式请求失败' } } } // 实际的聊天请求 private async makeChatRequest(service: ModelService, messages: any[], model: string): Promise { const requestStartTime = performance.now() const headers: HeadersInit = { 'Content-Type': 'application/json' } let url = '' let body: any = {} console.log('🔍 [makeChatRequest] 服务信息:', { type: service.type, name: service.name, url: service.url, model }) switch (service.type) { case 'openai': case 'local': headers['Authorization'] = `Bearer ${service.apiKey}` url = `${service.url}/chat/completions` body = { model, messages, stream: false } break case 'dashscope': headers['Authorization'] = `Bearer ${service.apiKey}` url = `${service.url}/chat/completions` body = { model, messages, stream: false } break case 'volcengine': headers['Authorization'] = `Bearer ${service.apiKey}` url = `${service.url}/chat/completions` body = { model, messages, stream: false } break case 'claude': headers['x-api-key'] = service.apiKey headers['anthropic-version'] = '2023-06-01' url = `${service.url}/messages` body = { model, messages: this.convertToClaudeFormat(messages), max_tokens: 4096 } break case 'gemini': url = `${service.url}/models/${model}:generateContent?key=${service.apiKey}` body = { contents: this.convertToGeminiFormat(messages) } break case 'azure': headers['api-key'] = service.apiKey url = `${service.url}/openai/deployments/${model}/chat/completions?api-version=2023-12-01-preview` body = { messages, stream: false } break case 'custom': try { const config = JSON.parse(service.customConfig || '{}') Object.assign(headers, config.headers || {}) } catch (e) { console.warn('自定义配置解析失败:', e) } url = `${service.url}/chat/completions` body = { model, messages, stream: false } break } console.log('🔍 [makeChatRequest] 最终请求URL:', url) console.log('🔍 [makeChatRequest] 请求体大小:', JSON.stringify(body).length, '字节') const beforeFetch = performance.now() console.log('⏱️ [makeChatRequest] 构建请求耗时:', (beforeFetch - requestStartTime).toFixed(2), 'ms') // 添加30秒超时控制 const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 30000) try { const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body), signal: controller.signal }) clearTimeout(timeoutId) const afterFetch = performance.now() console.log('⏱️ [makeChatRequest] 网络请求耗时:', (afterFetch - beforeFetch).toFixed(2), 'ms') console.log('🔍 [makeChatRequest] 响应状态:', response.status, response.statusText) if (!response.ok) { const errorText = await response.text() console.error('❌ [makeChatRequest] 请求失败:', { status: response.status, statusText: response.statusText, url, errorText }) throw new Error(`HTTP ${response.status}: ${errorText}`) } const beforeParse = performance.now() const result = await response.json() const afterParse = performance.now() console.log('⏱️ [makeChatRequest] 解析响应耗时:', (afterParse - beforeParse).toFixed(2), 'ms') console.log('⏱️ [makeChatRequest] makeChatRequest总耗时:', (afterParse - requestStartTime).toFixed(2), 'ms') return result } catch (error) { clearTimeout(timeoutId) if (error instanceof Error && error.name === 'AbortError') { throw new Error('请求超时(30秒)') } throw error } } // 流式聊天请求 private async makeChatRequestStream( service: ModelService, messages: any[], model: string, onChunk: (text: string) => void, tools?: any[], signal?: AbortSignal ): Promise { const requestStartTime = performance.now() const headers: HeadersInit = { 'Content-Type': 'application/json' } let url = '' let body: any = {} // 构建请求 (与非流式相同,但 stream: true) console.log('🎯 [makeChatRequestStream] 准备请求参数:') console.log(' 服务类型:', service.type) console.log(' 服务名称:', service.name) console.log(' 使用模型:', model) console.log(' 消息数量:', messages.length) console.log(' 工具数量:', tools?.length || 0) switch (service.type) { case 'openai': case 'local': case 'dashscope': case 'volcengine': headers['Authorization'] = `Bearer ${service.apiKey}` url = `${service.url}/chat/completions` body = { model, messages, stream: true, // ← 启用流式 ...(tools && tools.length > 0 ? { tools, tool_choice: 'auto' } : {}) } console.log('📋 [makeChatRequestStream] 请求体 model 字段:', body.model) break case 'claude': headers['x-api-key'] = service.apiKey headers['anthropic-version'] = '2023-06-01' url = `${service.url}/messages` body = { model, messages: this.convertToClaudeFormat(messages), max_tokens: 4096, stream: true } break case 'azure': headers['api-key'] = service.apiKey url = `${service.url}/openai/deployments/${model}/chat/completions?api-version=2023-12-01-preview` body = { messages, stream: true } break default: url = `${service.url}/chat/completions` body = { model, messages, stream: true } break } console.log('🔍 [makeChatRequestStream] 流式请求URL:', url) console.log('🔍 [makeChatRequestStream] 流式请求体大小:', JSON.stringify(body).length, '字节') console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') console.log('🚀 [最终确认] 即将发送请求:') console.log(' 模型:', body.model) console.log(' 服务:', service.name, `(${service.type})`) console.log(' URL:', url) console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') // 使用外部传入的 signal,或创建一个带超时的内部 controller const controller = signal ? null : new AbortController() const timeoutId = controller ? setTimeout(() => controller.abort(), 60000) : undefined // 流式请求60秒超时 try { const beforeFetch = performance.now() console.log('⏱️ [makeChatRequestStream] 构建请求耗时:', (beforeFetch - requestStartTime).toFixed(2), 'ms') const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body), signal: signal || controller?.signal }) if (timeoutId) clearTimeout(timeoutId) if (!response.ok) { const errorText = await response.text() throw new Error(`HTTP ${response.status}: ${errorText}`) } const afterFetch = performance.now() console.log('⏱️ [makeChatRequestStream] 首字节响应耗时:', (afterFetch - beforeFetch).toFixed(2), 'ms') // 读取流 const reader = response.body?.getReader() if (!reader) { throw new Error('无法获取响应流') } console.log('🌊🌊🌊 [makeChatRequestStream] === 开始读取流数据 ===') const decoder = new TextDecoder() let buffer = '' let chunkCount = 0 let totalChars = 0 const firstChunkTimeStart = performance.now() let collectedToolCalls: any[] = [] const toolCallsMap = new Map() while (true) { // 检查是否被中止(参考 cherry-studio 的实现) if (signal?.aborted) { console.log('🛑 [makeChatRequestStream] 检测到中止信号,停止读取流') reader.cancel() throw new DOMException('用户中止操作', 'AbortError') } const { done, value } = await reader.read() if (done) break chunkCount++ if (chunkCount === 1) { console.log('⚡⚡⚡ [makeChatRequestStream] 收到第一个数据块!耗时:', (performance.now() - firstChunkTimeStart).toFixed(2), 'ms') } buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.trim() === '' || line.trim() === 'data: [DONE]') { continue } if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)) // 记录第一个响应中的模型信息 if (chunkCount === 1 && data.model) { console.log('✅ [响应确认] API 返回的模型:', data.model) console.log(' 请求的模型:', body.model) console.log(' 模型匹配:', data.model === body.model ? '✓ 一致' : '✗ 不一致!') } const delta = data.choices?.[0]?.delta // 处理普通内容 const content = delta?.content if (content) { totalChars += content.length onChunk(content) } // 处理工具调用 if (delta?.tool_calls) { // 只在第一次检测到时输出日志 if (toolCallsMap.size === 0) { console.log('🔧 [makeChatRequestStream] 检测到工具调用,开始收集...') } for (const toolCall of delta.tool_calls) { const index = toolCall.index if (!toolCallsMap.has(index)) { toolCallsMap.set(index, { id: toolCall.id || '', type: toolCall.type || 'function', function: { name: toolCall.function?.name || '', arguments: '' } }) } const existing = toolCallsMap.get(index)! if (toolCall.function?.name) { existing.function.name = toolCall.function.name } if (toolCall.function?.arguments) { existing.function.arguments += toolCall.function.arguments } } } } catch (e) { // 忽略解析错误 } } } } // 收集所有工具调用 if (toolCallsMap.size > 0) { collectedToolCalls = Array.from(toolCallsMap.values()) console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') console.log('🔧 [makeChatRequestStream] 最终收集到工具调用:', collectedToolCalls.length, '个') collectedToolCalls.forEach((tc, idx) => { console.log(` 工具 [${idx}]:`, { id: tc.id, name: tc.function.name, arguments: tc.function.arguments }) }) console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') } else { console.log('⚠️ [makeChatRequestStream] 没有检测到工具调用') } const endTime = performance.now() console.log('⏱️ [makeChatRequestStream] 流式接收完成') console.log('⏱️ [makeChatRequestStream] 接收块数:', chunkCount, '总字符数:', totalChars) console.log('⏱️ [makeChatRequestStream] 流式总耗时:', (endTime - requestStartTime).toFixed(2), 'ms') return collectedToolCalls.length > 0 ? collectedToolCalls : undefined } catch (error) { if (timeoutId) clearTimeout(timeoutId) // 如果是 AbortError,直接抛出原始错误(可能是用户中止或超时) if (error instanceof Error && error.name === 'AbortError') { console.log('⚠️ [makeChatRequestStream] 请求被中止:', error.message) throw error } if (error instanceof DOMException && error.name === 'AbortError') { console.log('⚠️ [makeChatRequestStream] 请求被中止:', error.message) throw error } throw error } } // 转换消息格式为Claude格式 private convertToClaudeFormat(messages: any[]): any[] { return messages .filter(msg => msg.role !== 'system') .map(msg => ({ role: msg.role === 'assistant' ? 'assistant' : 'user', content: msg.content })) } // 转换消息格式为Gemini格式 private convertToGeminiFormat(messages: any[]): any[] { return messages .filter(msg => msg.role !== 'system') .map(msg => ({ role: msg.role === 'assistant' ? 'model' : 'user', parts: [{ text: msg.content }] })) } // 添加服务 addService(service: ModelService): void { this.services.set(service.id, service) } // 更新服务 updateService(service: ModelService): void { this.services.set(service.id, service) } // 删除服务 removeService(serviceId: string): void { this.services.delete(serviceId) } // 获取服务 getService(serviceId: string): ModelService | undefined { return this.services.get(serviceId) } // 获取所有服务 getAllServices(): ModelService[] { return Array.from(this.services.values()) } // 连接服务 async connectService(serviceId: string): Promise { const service = this.services.get(serviceId) if (!service) throw new Error('服务不存在') service.status = 'connecting' try { const result = await this.testConnection(service) if (result.success && result.data) { service.status = 'connected' service.models = result.data.models service.errorMessage = undefined service.lastUsed = new Date() } else { service.status = 'error' service.errorMessage = result.error throw new Error(result.error) } } catch (error) { service.status = 'error' service.errorMessage = error instanceof Error ? error.message : '连接失败' throw error } } // 断开服务 disconnectService(serviceId: string): void { const service = this.services.get(serviceId) if (service) { service.status = 'disconnected' service.models = [] service.errorMessage = undefined } } // 健康检测 - 测试单个模型是否可用 async testModelHealth(service: ModelService, modelId: string): Promise<{ modelId: string available: boolean latency?: number error?: string }> { const startTime = Date.now() try { // 发送一个最小的测试请求 const result = await this.sendChatRequest(service.id, [ { role: 'user', content: 'hi' } ], modelId) if (!result.success) { throw new Error(result.error || '测试失败') } const latency = Date.now() - startTime return { modelId, available: true, latency } } catch (error) { return { modelId, available: false, error: error instanceof Error ? error.message : '测试失败' } } } // 批量健康检测 - 测试所有模型 async healthCheckAllModels( service: ModelService, onProgress?: (current: number, total: number, modelId: string) => void ): Promise<{ availableModels: string[] unavailableModels: string[] results: Array<{ modelId: string available: boolean latency?: number error?: string }> }> { const models = service.models || [] const results: Array<{ modelId: string available: boolean latency?: number error?: string }> = [] for (let i = 0; i < models.length; i++) { const modelId = models[i] // 通知进度 if (onProgress) { onProgress(i + 1, models.length, modelId) } // 测试模型健康状态 const result = await this.testModelHealth(service, modelId) results.push(result) // 添加小延迟避免过快请求 if (i < models.length - 1) { await new Promise(resolve => setTimeout(resolve, 200)) } } // 统计结果 const availableModels = results.filter(r => r.available).map(r => r.modelId) const unavailableModels = results.filter(r => !r.available).map(r => r.modelId) return { availableModels, unavailableModels, results } } } // 导出单例实例 export const modelServiceManager = ModelServiceManager.getInstance()