Step 2: API Fallback 自动切换机制完成

This commit is contained in:
淑婷陈 2026-03-07 16:04:31 +08:00
parent 70cbd30e7b
commit 5749122cd1
5 changed files with 164 additions and 27 deletions

2
backend/.env Normal file
View File

@ -0,0 +1,2 @@
PRIMARY_API_KEY=你的主密钥
FALLBACK_API_KEY=你的备用密钥

View File

@ -1,19 +1,21 @@
// 模型路由配置(统一走云雾API中转 // 模型路由配置(支持主备API自动切换
const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions'; const PRIMARY_API_KEY = process.env.PRIMARY_API_KEY;
const YUNWU_KEY = process.env.YUNWU_API_KEY; const FALLBACK_API_KEY = process.env.FALLBACK_API_KEY;
const MODELS = { const MODELS = {
deepseek: { deepseek: {
name: 'DeepSeek Chat', name: 'DeepSeek Chat',
apiUrl: YUNWU_API_URL, apiUrl: 'https://api.deepseek.com/v1/chat/completions',
apiKey: YUNWU_KEY, primaryApiKey: PRIMARY_API_KEY,
fallbackApiKey: FALLBACK_API_KEY,
model: 'deepseek-chat', model: 'deepseek-chat',
maxTokens: 4096 maxTokens: 4096
}, },
gpt4o_mini: { gpt4o_mini: {
name: 'GPT-4o-mini', name: 'GPT-4o-mini',
apiUrl: YUNWU_API_URL, apiUrl: 'https://api.deepseek.com/v1/chat/completions',
apiKey: YUNWU_KEY, primaryApiKey: PRIMARY_API_KEY,
fallbackApiKey: FALLBACK_API_KEY,
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
maxTokens: 4096 maxTokens: 4096
} }

View File

@ -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
};

View File

@ -6,12 +6,71 @@ const { MODELS, DEFAULT_MODEL } = require('../config/models');
router.get('/test', (req, res) => { router.get('/test', (req, res) => {
res.json({ res.json({
status: 'ok', status: 'ok',
message: '模型路由正常 ❤️', message: '模型路由正常 ❤️ (支持主备自动切换)',
available_models: Object.keys(MODELS), available_models: Object.keys(MODELS),
default_model: DEFAULT_MODEL 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) => { router.post('/chat', async (req, res) => {
try { try {
const { message, model_name } = req.body; const { message, model_name } = req.body;
@ -21,33 +80,22 @@ router.post('/chat', async (req, res) => {
return res.status(400).json({ error: '未知模型: ' + model_name }); return res.status(400).json({ error: '未知模型: ' + model_name });
} }
if (!model.apiKey) { if (!model.primaryApiKey) {
return res.status(503).json({ return res.status(503).json({
error: '模型 ' + model.name + ' 的 API Key 尚未配置', error: '模型 ' + model.name + ' 的 API Key 尚未配置',
hint: '请检查 .env 文件中的 YUNWU_API_KEY' hint: '请检查 .env 文件中的 PRIMARY_API_KEY'
}); });
} }
const response = await axios.post( const result = await callAPIWithFallback(model, message);
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({ res.json({
model: model.name, model: model.name,
reply: response.data.choices[0].message.content reply: result.data.choices[0].message.content,
fallback_used: result.fallback || false
}); });
} catch (err) { } catch (err) {
console.error('[API Error]', err.message);
res.status(500).json({ res.status(500).json({
error: err.message, error: err.message,
detail: err.response ? err.response.data : null detail: err.response ? err.response.data : null

View File

@ -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;