1010 lines
32 KiB
TypeScript
1010 lines
32 KiB
TypeScript
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()
|