diff --git a/backend/.env b/backend/.env new file mode 100644 index 00000000..6720a98a --- /dev/null +++ b/backend/.env @@ -0,0 +1,2 @@ +PRIMARY_API_KEY=你的主密钥 +FALLBACK_API_KEY=你的备用密钥 diff --git a/backend/config/models.js b/backend/config/models.js index 6ef24385..03d8eb07 100644 --- a/backend/config/models.js +++ b/backend/config/models.js @@ -1,19 +1,21 @@ -// 模型路由配置(统一走云雾API中转) -const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions'; -const YUNWU_KEY = process.env.YUNWU_API_KEY; +// 模型路由配置(支持主备API自动切换) +const PRIMARY_API_KEY = process.env.PRIMARY_API_KEY; +const FALLBACK_API_KEY = process.env.FALLBACK_API_KEY; const MODELS = { deepseek: { name: 'DeepSeek Chat', - apiUrl: YUNWU_API_URL, - apiKey: YUNWU_KEY, + apiUrl: 'https://api.deepseek.com/v1/chat/completions', + primaryApiKey: PRIMARY_API_KEY, + fallbackApiKey: FALLBACK_API_KEY, model: 'deepseek-chat', maxTokens: 4096 }, gpt4o_mini: { name: 'GPT-4o-mini', - apiUrl: YUNWU_API_URL, - apiKey: YUNWU_KEY, + apiUrl: 'https://api.deepseek.com/v1/chat/completions', + primaryApiKey: PRIMARY_API_KEY, + fallbackApiKey: FALLBACK_API_KEY, model: 'gpt-4o-mini', maxTokens: 4096 } diff --git a/backend/config/models.js.bak b/backend/config/models.js.bak new file mode 100644 index 00000000..6ef24385 --- /dev/null +++ b/backend/config/models.js.bak @@ -0,0 +1,27 @@ +// 模型路由配置(统一走云雾API中转) +const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions'; +const YUNWU_KEY = process.env.YUNWU_API_KEY; + +const MODELS = { + deepseek: { + name: 'DeepSeek Chat', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'deepseek-chat', + maxTokens: 4096 + }, + gpt4o_mini: { + name: 'GPT-4o-mini', + apiUrl: YUNWU_API_URL, + apiKey: YUNWU_KEY, + model: 'gpt-4o-mini', + maxTokens: 4096 + } +}; + +const DEFAULT_MODEL = 'deepseek'; + +module.exports = { + MODELS, + DEFAULT_MODEL +}; diff --git a/backend/routes/router.js b/backend/routes/router.js index 2189677c..1b759f8a 100644 --- a/backend/routes/router.js +++ b/backend/routes/router.js @@ -6,12 +6,71 @@ const { MODELS, DEFAULT_MODEL } = require('../config/models'); router.get('/test', (req, res) => { res.json({ status: 'ok', - message: '模型路由正常 ❤️', + message: '模型路由正常 ❤️ (支持主备自动切换)', available_models: Object.keys(MODELS), default_model: DEFAULT_MODEL }); }); +// 带 fallback 的 API 调用函数 +async function callAPIWithFallback(model, message) { + // 第一次尝试:主密钥 + try { + console.log(`[API] 尝试使用主密钥调用 ${model.name}`); + const response = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.primaryApiKey, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: response.data }; + } catch (primaryErr) { + // 判断是否需要 fallback(余额不足/超时/401/403) + const shouldFallback = + primaryErr.response?.status === 401 || + primaryErr.response?.status === 403 || + primaryErr.response?.data?.error?.type === 'insufficient_quota' || + primaryErr.code === 'ECONNABORTED' || + primaryErr.message.includes('timeout'); + + if (!shouldFallback || !model.fallbackApiKey) { + // 不需要 fallback 或没有备用密钥,直接抛出错误 + throw primaryErr; + } + + // 需要 fallback,尝试备用密钥 + console.log(`[API Fallback] 从主密钥切换到备用密钥 (${model.name})`); + try { + const fallbackResponse = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.fallbackApiKey, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: fallbackResponse.data, fallback: true }; + } catch (fallbackErr) { + // 备用也失败了,抛出备用错误 + throw fallbackErr; + } + } +} + router.post('/chat', async (req, res) => { try { const { message, model_name } = req.body; @@ -21,33 +80,22 @@ router.post('/chat', async (req, res) => { return res.status(400).json({ error: '未知模型: ' + model_name }); } - if (!model.apiKey) { + if (!model.primaryApiKey) { return res.status(503).json({ - error: '模型 ' + model.name + ' 的 API Key 尚未配置', - hint: '请检查 .env 文件中的 YUNWU_API_KEY' + error: '模型 ' + model.name + ' 的主 API Key 尚未配置', + hint: '请检查 .env 文件中的 PRIMARY_API_KEY' }); } - const response = await axios.post( - model.apiUrl, - { - model: model.model, - messages: [{ role: 'user', content: message }], - max_tokens: model.maxTokens - }, - { - headers: { - 'Authorization': 'Bearer ' + model.apiKey, - 'Content-Type': 'application/json' - } - } - ); - + const result = await callAPIWithFallback(model, message); + res.json({ model: model.name, - reply: response.data.choices[0].message.content + reply: result.data.choices[0].message.content, + fallback_used: result.fallback || false }); } catch (err) { + console.error('[API Error]', err.message); res.status(500).json({ error: err.message, detail: err.response ? err.response.data : null diff --git a/backend/routes/router.js.bak b/backend/routes/router.js.bak new file mode 100644 index 00000000..2189677c --- /dev/null +++ b/backend/routes/router.js.bak @@ -0,0 +1,58 @@ +const express = require('express'); +const axios = require('axios'); +const router = express.Router(); +const { MODELS, DEFAULT_MODEL } = require('../config/models'); + +router.get('/test', (req, res) => { + res.json({ + status: 'ok', + message: '模型路由正常 ❤️', + available_models: Object.keys(MODELS), + default_model: DEFAULT_MODEL + }); +}); + +router.post('/chat', async (req, res) => { + try { + const { message, model_name } = req.body; + const model = MODELS[model_name || DEFAULT_MODEL]; + + if (!model) { + return res.status(400).json({ error: '未知模型: ' + model_name }); + } + + if (!model.apiKey) { + return res.status(503).json({ + error: '模型 ' + model.name + ' 的 API Key 尚未配置', + hint: '请检查 .env 文件中的 YUNWU_API_KEY' + }); + } + + const response = await axios.post( + model.apiUrl, + { + model: model.model, + messages: [{ role: 'user', content: message }], + max_tokens: model.maxTokens + }, + { + headers: { + 'Authorization': 'Bearer ' + model.apiKey, + 'Content-Type': 'application/json' + } + } + ); + + res.json({ + model: model.name, + reply: response.data.choices[0].message.content + }); + } catch (err) { + res.status(500).json({ + error: err.message, + detail: err.response ? err.response.data : null + }); + } +}); + +module.exports = router;