From eeeed947027a56a904cd3a82f7a3f8c3818037b4 Mon Sep 17 00:00:00 2001 From: Yuzhiran Dev Date: Sat, 11 Jul 2026 14:33:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=20Chat=20=E4=B8=BB=E5=A4=87=E5=8F=8C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20+=20Sensenova=20reasoning=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库模型迁移:删除旧 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 路径 --- server/src/routes/aiChat.js | 147 +++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 61 deletions(-) diff --git a/server/src/routes/aiChat.js b/server/src/routes/aiChat.js index 66f5c01..814f144 100755 --- a/server/src/routes/aiChat.js +++ b/server/src/routes/aiChat.js @@ -72,71 +72,21 @@ router.post('/ask', userAuthMiddleware, async (req, res) => { return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403) } - // 获取当前使用的AI模型配置 - const aiModel = await AIModel.getCurrentModel() + // 获取所有启用的AI模型(用于主备切换) + const aiModels = await AIModel.getActiveModels() - let apiUrl, apiKey, modelId, modelConfig - - if (aiModel) { - // 使用数据库配置的模型 - apiUrl = aiModel.apiUrl - apiKey = aiModel.apiKey - modelId = aiModel.modelId - modelConfig = aiModel.config - } else { + if (!aiModels || aiModels.length === 0) { // 使用环境变量配置的模型 - apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions' - apiKey = process.env.OPENAI_API_KEY - modelId = process.env.OPENAI_MODEL || 'gpt-3.5-turbo' - modelConfig = { temperature: 0.7, maxTokens: 800, topP: 1 } + const envModels = [{ + apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', + apiKey: process.env.OPENAI_API_KEY, + 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) - 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() - }) + return await tryModels(aiModels, question, dimension, res, quota) } catch (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