fix: AI Chat 主备双模型 + Sensenova reasoning 兼容

- 数据库模型迁移:删除旧 Opencode/api.qnaigc 配置
- 主模型: Sensenova (sensenova-6.7-flash-lite) ✓
- 备模型: NVIDIA (stepfun-ai/step-3.5-flash) ✓
- aiChat.js 主备切换逻辑:主模型失败自动降级到备模型
- 兼容 Sensenova 的 reasoning 字段(非标准 content)
- API URL 统一补全 /chat/completions 路径
This commit is contained in:
Yuzhiran Dev
2026-07-11 14:33:45 +08:00
parent 1d522df07e
commit eeeed94702
+86 -61
View File
@@ -72,71 +72,21 @@ router.post('/ask', userAuthMiddleware, async (req, res) => {
return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403) return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403)
} }
// 获取当前使用的AI模型配置 // 获取所有启用的AI模型(用于主备切换)
const aiModel = await AIModel.getCurrentModel() const aiModels = await AIModel.getActiveModels()
let apiUrl, apiKey, modelId, modelConfig if (!aiModels || aiModels.length === 0) {
if (aiModel) {
// 使用数据库配置的模型
apiUrl = aiModel.apiUrl
apiKey = aiModel.apiKey
modelId = aiModel.modelId
modelConfig = aiModel.config
} else {
// 使用环境变量配置的模型 // 使用环境变量配置的模型
apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions' const envModels = [{
apiKey = process.env.OPENAI_API_KEY apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
modelId = process.env.OPENAI_MODEL || 'gpt-3.5-turbo' apiKey: process.env.OPENAI_API_KEY,
modelConfig = { temperature: 0.7, maxTokens: 800, topP: 1 } modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo',
config: { temperature: 0.7, maxTokens: 800, topP: 1 }
}]
return await tryModels(envModels, question, dimension, res, quota)
} }
// 检查API配置(opencode.ai免费API无需key return await tryModels(aiModels, question, dimension, res, quota)
const needsAuth = apiKey && apiKey !== 'sk-dummy' && apiKey !== '***'
// 构建系统提示词
const systemPrompt = getSystemPromptByDimension(dimension)
// 构建请求头
const headers = {
'Content-Type': 'application/json'
}
if (needsAuth) {
headers['Authorization'] = `Bearer ${apiKey}`
}
// 调用AI API
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({
model: modelId,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
temperature: modelConfig.temperature || 0.7,
max_tokens: modelConfig.maxTokens || 800,
top_p: modelConfig.topP || 1
})
})
if (!response.ok) {
const errorData = await response.json()
logger.error('OpenAI API错误:', errorData)
return ApiResponse.error(res, 'AI服务响应异常', 500)
}
const data = await response.json()
const answer = data.choices[0].message.content
// 扣除一次使用次数
await quota.useQuota()
return ApiResponse.success(res, {
answer,
remainingQuota: quota.getRemainingQuota()
})
} catch (error) { } catch (error) {
logger.error('AI问答失败:', error) logger.error('AI问答失败:', error)
@@ -144,6 +94,81 @@ router.post('/ask', userAuthMiddleware, async (req, res) => {
} }
}) })
/**
* 遍历模型列表尝试问答(主备切换)
*/
async function tryModels(models, question, dimension, res, quota) {
const systemPrompt = getSystemPromptByDimension(dimension)
let lastError = null
for (const model of models) {
try {
const apiUrl = model.apiUrl
const apiKey = model.apiKey
const modelId = model.modelId
const modelConfig = model.config || {}
// 检查是否需要 Authorization(免费/无需key的API跳过)
const needsAuth = apiKey && apiKey !== 'sk-dummy' && apiKey !== '***'
// 构建请求头
const headers = { 'Content-Type': 'application/json' }
if (needsAuth) {
headers['Authorization'] = `Bearer ${apiKey}`
}
logger.info(`[AI Chat] 尝试模型: ${model.name} (${modelId})`)
// 调用AI API
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({
model: modelId,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
temperature: modelConfig.temperature || 0.7,
max_tokens: modelConfig.maxTokens || 800,
top_p: modelConfig.topP || 1
})
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.warn(`[AI Chat] 模型 ${model.name} 失败:`, response.status, errorData)
lastError = { status: response.status, data: errorData }
continue // 尝试下一个模型
}
const data = await response.json()
const message = data.choices[0].message
// 兼容标准 content 和 sensenova 的 reasoning 字段
const answer = message.content || message.reasoning || ''
// 扣除一次使用次数
await quota.useQuota()
logger.info(`[AI Chat] 模型 ${model.name} 回答成功`)
return ApiResponse.success(res, {
answer,
remainingQuota: quota.getRemainingQuota()
})
} catch (err) {
logger.warn(`[AI Chat] 模型 ${model.name} 异常:`, err.message)
lastError = { message: err.message }
continue // 尝试下一个模型
}
}
// 所有模型都失败了
logger.error('所有AI模型均失败:', lastError)
return ApiResponse.error(res, 'AI服务暂时不可用,请稍后再试', 500)
}
/** /**
* 分享获得问答次数 * 分享获得问答次数
* POST /api/ai-chat/share-gain * POST /api/ai-chat/share-gain