Files
map-client-vue/web/src/services/modelServiceManager.ts
2025-10-15 15:07:45 +08:00

1010 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<T = any> {
success: boolean
data?: T
error?: string
}
export class ModelServiceManager {
private services: Map<string, ModelService> = 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<string, ModelService['type']> = {
'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<ApiResponse<{ models: string[] }>> {
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<void> {
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<string[]> {
// 某些服务使用预定义模型列表,不需要 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<ApiResponse<any>> {
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<ApiResponse<{ toolCalls?: any[] }>> {
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<any> {
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<any[] | undefined> {
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<number, any>()
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<void> {
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()