339 lines
9.7 KiB
Markdown
339 lines
9.7 KiB
Markdown
# 聊天404错误修复报告
|
|
|
|
## 问题描述
|
|
用户在发送聊天消息时遇到404错误:
|
|
```
|
|
Failed to load resource: the server responded with a status of 404 () (completions, line 0)
|
|
```
|
|
|
|
## 根本原因分析
|
|
|
|
### 问题1: 服务选择逻辑错误
|
|
**位置**: `/web/src/services/chatService.ts` 第492-530行 `callModel()` 方法
|
|
|
|
**原因**:
|
|
- 用户在聊天界面选择了特定的AI模型(例如: `doubao-seed-1-6-flash-250828`)
|
|
- `callModel()` 方法接收到 `model` 参数,但**没有根据模型名称查找对应的服务**
|
|
- 代码直接使用 `services[0]`(第一个连接的服务)
|
|
- 如果第一个服务没有该模型,就会发送错误的请求
|
|
|
|
**示例场景**:
|
|
1. 用户配置了两个服务: DashScope 和 Volcengine
|
|
2. DashScope先连接,成为 `services[0]`
|
|
3. 用户在聊天界面选择 Volcengine 的模型 `doubao-seed-1-6-flash-250828`
|
|
4. 代码将这个模型发送给 DashScope 服务
|
|
5. DashScope 不认识这个模型,返回404或其他错误
|
|
|
|
### 问题2: 缺少调试日志
|
|
**位置**:
|
|
- `/web/src/services/chatService.ts` `callModel()` 方法
|
|
- `/web/src/services/modelServiceManager.ts` `sendChatRequest()` 和 `makeChatRequest()` 方法
|
|
|
|
**原因**:
|
|
- 没有日志输出当前使用的服务和模型
|
|
- 难以追踪请求的完整路径
|
|
- 无法快速定位URL构建问题
|
|
|
|
## 解决方案
|
|
|
|
### 修复1: 智能服务匹配
|
|
**文件**: `/web/src/services/chatService.ts`
|
|
|
|
**改动**:
|
|
```typescript
|
|
// 原代码
|
|
const service = services[0] // 使用第一个可用服务
|
|
const selectedModel = model || service.models?.[0] || 'default'
|
|
|
|
// 修复后
|
|
let service = services[0] // 默认使用第一个可用服务
|
|
let selectedModel = model || service.models?.[0] || 'default'
|
|
|
|
// 如果指定了模型,尝试找到拥有该模型的服务
|
|
if (model) {
|
|
const foundService = services.find(s =>
|
|
s.models && s.models.includes(model)
|
|
)
|
|
if (foundService) {
|
|
service = foundService
|
|
selectedModel = model
|
|
} else {
|
|
console.warn(`⚠️ 未找到包含模型 "${model}" 的服务,使用默认服务`)
|
|
}
|
|
}
|
|
|
|
console.log('🔍 [callModel] 使用服务:', service.name, '模型:', selectedModel)
|
|
```
|
|
|
|
**效果**:
|
|
- ✅ 根据模型名称自动匹配正确的服务
|
|
- ✅ 避免将模型发送给错误的服务
|
|
- ✅ 提供降级方案(找不到服务时使用默认)
|
|
- ✅ 记录调试日志
|
|
|
|
### 修复2: 增强调试日志
|
|
**文件**: `/web/src/services/modelServiceManager.ts`
|
|
|
|
#### 2.1 `sendChatRequest()` 方法
|
|
```typescript
|
|
// 添加的日志
|
|
console.log('🔍 [sendChatRequest] serviceId:', serviceId, 'service:', service)
|
|
|
|
// 添加URL验证
|
|
if (!service.url || !service.url.startsWith('http')) {
|
|
console.error('❌ [sendChatRequest] 无效的服务URL:', service.url)
|
|
return {
|
|
success: false,
|
|
error: `服务URL无效: ${service.url}`
|
|
}
|
|
}
|
|
|
|
// 添加异常日志
|
|
console.error('❌ [sendChatRequest] 请求异常:', error)
|
|
```
|
|
|
|
#### 2.2 `makeChatRequest()` 方法
|
|
```typescript
|
|
// 请求前日志
|
|
console.log('🔍 [makeChatRequest] 服务信息:', {
|
|
type: service.type,
|
|
name: service.name,
|
|
url: service.url,
|
|
model
|
|
})
|
|
|
|
console.log('🔍 [makeChatRequest] 最终请求URL:', url)
|
|
console.log('🔍 [makeChatRequest] 请求体:', body)
|
|
|
|
// 响应日志
|
|
console.log('🔍 [makeChatRequest] 响应状态:', response.status, response.statusText)
|
|
|
|
// 错误日志
|
|
console.error('❌ [makeChatRequest] 请求失败:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url,
|
|
errorText
|
|
})
|
|
```
|
|
|
|
**效果**:
|
|
- ✅ 完整记录请求流程
|
|
- ✅ 输出服务名称、URL、模型
|
|
- ✅ 显示最终构建的URL
|
|
- ✅ 记录响应状态和错误详情
|
|
- ✅ 使用emoji图标便于快速识别
|
|
|
|
## 验证步骤
|
|
|
|
1. **配置多个服务**:
|
|
- 添加 DashScope 服务(阿里云通义千问)
|
|
- 添加 Volcengine 服务(字节跳动豆包)
|
|
- 确保两个服务都已连接
|
|
|
|
2. **测试模型匹配**:
|
|
- 在聊天界面选择 Volcengine 的模型(如 `doubao-seed-1-6-flash-250828`)
|
|
- 发送消息
|
|
- 打开浏览器控制台
|
|
- 应该看到日志: `🔍 [callModel] 使用服务: 火山引擎 模型: doubao-seed-1-6-flash-250828`
|
|
|
|
3. **测试URL构建**:
|
|
- 检查控制台日志中的URL
|
|
- 应该是: `https://ark.cn-beijing.volces.com/api/v3/chat/completions`
|
|
- 不应该是: `/completions` 或其他错误格式
|
|
|
|
4. **测试错误处理**:
|
|
- 暂时输入错误的API Key
|
|
- 应该看到详细的错误日志
|
|
- 包括状态码、URL、错误响应
|
|
|
|
## 预期效果
|
|
|
|
修复后,用户应该能够:
|
|
- ✅ 在聊天界面选择任意已配置服务的模型
|
|
- ✅ 系统自动找到正确的服务发送请求
|
|
- ✅ 看到清晰的调试日志(便于问题追踪)
|
|
- ✅ 收到正确的AI回复
|
|
- ✅ 不再看到404错误
|
|
|
|
## 相关文件
|
|
|
|
### 修改的文件
|
|
1. `/web/src/services/chatService.ts`
|
|
- 修改 `callModel()` 方法
|
|
- 新增服务匹配逻辑
|
|
|
|
2. `/web/src/services/modelServiceManager.ts`
|
|
- 修改 `sendChatRequest()` 方法
|
|
- 修改 `makeChatRequest()` 方法
|
|
- 新增URL验证
|
|
- 新增调试日志
|
|
|
|
### 涉及的方法调用链
|
|
```
|
|
ChatLayout.vue (用户发送消息)
|
|
↓
|
|
chatService.sendMessageStream(model: string)
|
|
↓
|
|
chatService.callModelStream(model: string)
|
|
↓
|
|
chatService.callModel(model: string) ← 修复点1: 服务匹配
|
|
↓
|
|
modelServiceManager.sendChatRequest(serviceId, messages, model) ← 修复点2: 日志
|
|
↓
|
|
modelServiceManager.makeChatRequest(service, messages, model) ← 修复点3: 日志
|
|
↓
|
|
fetch(url) → AI服务API
|
|
```
|
|
|
|
## 技术细节
|
|
|
|
### 服务匹配算法
|
|
```typescript
|
|
// 查找包含指定模型的服务
|
|
const foundService = services.find(s =>
|
|
s.models && s.models.includes(model)
|
|
)
|
|
```
|
|
|
|
**特点**:
|
|
- 使用 `Array.find()` 查找第一个匹配的服务
|
|
- 检查 `s.models` 存在性(避免空指针)
|
|
- 使用 `includes()` 精确匹配模型名称
|
|
- 找不到时提供降级方案
|
|
|
|
### URL构建规则
|
|
不同服务类型的endpoint:
|
|
- **OpenAI/Local**: `{baseUrl}/chat/completions`
|
|
- **DashScope**: `{baseUrl}/chat/completions`
|
|
- **Volcengine**: `{baseUrl}/chat/completions`
|
|
- **Claude**: `{baseUrl}/messages`
|
|
- **Gemini**: `{baseUrl}/models/{model}:generateContent?key={apiKey}`
|
|
- **Azure**: `{baseUrl}/openai/deployments/{model}/chat/completions?api-version=2023-12-01-preview`
|
|
|
|
### 预设URL
|
|
```typescript
|
|
const defaultUrls = {
|
|
openai: 'https://api.openai.com/v1',
|
|
claude: 'https://api.anthropic.com/v1',
|
|
gemini: 'https://generativelanguage.googleapis.com/v1',
|
|
azure: 'https://your-resource.openai.azure.com',
|
|
dashscope: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
volcengine: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
local: 'http://localhost:1234/v1'
|
|
}
|
|
```
|
|
|
|
## 后续优化建议
|
|
|
|
### 1. 缓存服务-模型映射
|
|
**目的**: 避免每次都遍历查找
|
|
```typescript
|
|
// 在 modelServiceManager 中维护映射
|
|
private modelToServiceMap: Map<string, string> = new Map()
|
|
|
|
// 更新映射
|
|
updateModelMapping() {
|
|
this.modelToServiceMap.clear()
|
|
this.services.forEach(service => {
|
|
service.models?.forEach(model => {
|
|
this.modelToServiceMap.set(model, service.id)
|
|
})
|
|
})
|
|
}
|
|
|
|
// 快速查找
|
|
getServiceByModel(model: string): ModelService | undefined {
|
|
const serviceId = this.modelToServiceMap.get(model)
|
|
return serviceId ? this.services.get(serviceId) : undefined
|
|
}
|
|
```
|
|
|
|
### 2. 用户选择优先级
|
|
**目的**: 当多个服务有相同模型时,使用用户最后使用的服务
|
|
```typescript
|
|
private lastUsedService: Map<string, string> = new Map() // model -> serviceId
|
|
|
|
callModel(conversation, model) {
|
|
// 优先使用用户最后一次使用的服务
|
|
const lastServiceId = this.lastUsedService.get(model)
|
|
let service = services.find(s => s.id === lastServiceId) ||
|
|
services.find(s => s.models?.includes(model)) ||
|
|
services[0]
|
|
|
|
// 记录使用历史
|
|
this.lastUsedService.set(model, service.id)
|
|
}
|
|
```
|
|
|
|
### 3. 模型别名支持
|
|
**目的**: 支持同一模型的不同名称
|
|
```typescript
|
|
private modelAliases: Map<string, string[]> = new Map([
|
|
['gpt-4', ['gpt-4-0613', 'gpt-4-32k']],
|
|
['doubao', ['doubao-seed-1-6', 'doubao-seed-1-6-flash']]
|
|
])
|
|
|
|
findServiceByModel(model: string): ModelService | undefined {
|
|
// 尝试直接匹配
|
|
let service = services.find(s => s.models?.includes(model))
|
|
|
|
// 尝试别名匹配
|
|
if (!service) {
|
|
const aliases = this.modelAliases.get(model) || []
|
|
service = services.find(s =>
|
|
s.models?.some(m => aliases.includes(m))
|
|
)
|
|
}
|
|
|
|
return service
|
|
}
|
|
```
|
|
|
|
### 4. 更智能的错误提示
|
|
**目的**: 帮助用户快速定位配置问题
|
|
```typescript
|
|
if (!foundService) {
|
|
const availableModels = services
|
|
.flatMap(s => s.models || [])
|
|
.join(', ')
|
|
|
|
throw new Error(
|
|
`未找到支持模型 "${model}" 的服务。\n` +
|
|
`当前可用模型: ${availableModels}\n` +
|
|
`请检查模型服务配置或选择其他模型。`
|
|
)
|
|
}
|
|
```
|
|
|
|
## 测试场景
|
|
|
|
### 场景1: 单服务单模型
|
|
- **配置**: 1个DashScope服务
|
|
- **操作**: 选择 `qwen-turbo`
|
|
- **预期**: 正常工作
|
|
|
|
### 场景2: 多服务不同模型
|
|
- **配置**: DashScope + Volcengine
|
|
- **操作**: 交替选择两个服务的模型
|
|
- **预期**: 自动切换服务,都能正常工作
|
|
|
|
### 场景3: 多服务相同模型名
|
|
- **配置**: 两个OpenAI兼容服务,都有 `gpt-3.5-turbo`
|
|
- **操作**: 选择 `gpt-3.5-turbo`
|
|
- **预期**: 使用第一个找到的服务(可后续优化为用户选择)
|
|
|
|
### 场景4: 模型不存在
|
|
- **配置**: DashScope服务
|
|
- **操作**: 选择不存在的模型 `nonexistent-model`
|
|
- **预期**: 降级使用默认服务,输出警告日志
|
|
|
|
### 场景5: 服务URL错误
|
|
- **配置**: 服务URL为空或不含http
|
|
- **操作**: 发送消息
|
|
- **预期**: 立即返回错误,不发送请求
|
|
|
|
## 更新历史
|
|
- **2024-01-XX**: 初始版本,修复服务匹配和调试日志问题
|
|
- **待定**: 实现缓存和优先级优化
|