mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 增加故障转移和重试,并整理代码
This commit is contained in:
+34
-2
@@ -3,6 +3,20 @@
|
||||
* @description 统一定义服务器错误码及其对应的中文消息和 HTTP 状态码
|
||||
*/
|
||||
|
||||
/**
|
||||
* 错误类型枚举 (OpenAI 标准)
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const ERROR_TYPES = {
|
||||
/** 无效请求 */
|
||||
INVALID_REQUEST: 'invalid_request_error',
|
||||
/** 服务器错误 */
|
||||
SERVER_ERROR: 'server_error',
|
||||
/** 限流错误 */
|
||||
RATE_LIMIT: 'rate_limit_error',
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误码枚举
|
||||
* @readonly
|
||||
@@ -31,56 +45,74 @@ export const ERROR_CODES = {
|
||||
RECAPTCHA: 'RECAPTCHA',
|
||||
/** 服务器内部错误 */
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
/** 生成失败 */
|
||||
GENERATION_FAILED: 'GENERATION_FAILED',
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误详情映射表
|
||||
* @type {Record<string, {message: string, status: number}>}
|
||||
* @type {Record<string, {message: string, status: number, type: string}>}
|
||||
*/
|
||||
const ERROR_DETAILS = {
|
||||
[ERROR_CODES.UNAUTHORIZED]: {
|
||||
message: '未授权(Token 无效或缺失)',
|
||||
status: 401,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.BROWSER_NOT_INITIALIZED]: {
|
||||
message: '浏览器未初始化',
|
||||
status: 503,
|
||||
type: ERROR_TYPES.SERVER_ERROR,
|
||||
},
|
||||
[ERROR_CODES.SERVER_BUSY]: {
|
||||
message: '服务器繁忙(队列已满)',
|
||||
status: 429,
|
||||
type: ERROR_TYPES.RATE_LIMIT,
|
||||
},
|
||||
[ERROR_CODES.NO_MESSAGES]: {
|
||||
message: '请求参数缺少 messages',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.NO_USER_MESSAGES]: {
|
||||
message: 'messages 中缺少 role=user 的消息',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.TOO_MANY_IMAGES]: {
|
||||
message: '图片数量超过限制',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.INVALID_MODEL]: {
|
||||
message: '模型无效/后端不支持',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.IMAGE_REQUIRED]: {
|
||||
message: '该模型需要参考图',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.IMAGE_FORBIDDEN]: {
|
||||
message: '该模型不支持图片输入',
|
||||
status: 400,
|
||||
type: ERROR_TYPES.INVALID_REQUEST,
|
||||
},
|
||||
[ERROR_CODES.RECAPTCHA]: {
|
||||
message: '触发人机验证(reCAPTCHA)',
|
||||
status: 500,
|
||||
status: 403,
|
||||
type: ERROR_TYPES.SERVER_ERROR,
|
||||
},
|
||||
[ERROR_CODES.INTERNAL_ERROR]: {
|
||||
message: '服务器内部错误',
|
||||
status: 500,
|
||||
type: ERROR_TYPES.SERVER_ERROR,
|
||||
},
|
||||
[ERROR_CODES.GENERATION_FAILED]: {
|
||||
message: '图片生成失败',
|
||||
status: 502,
|
||||
type: ERROR_TYPES.SERVER_ERROR,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+12
-12
@@ -66,34 +66,34 @@ export function sendHeartbeat(res, mode, modelName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送统一 API 错误响应
|
||||
* 发送统一 API 错误响应 (OpenAI 标准格式)
|
||||
* @param {import('http').ServerResponse} res - HTTP 响应对象
|
||||
* @param {object} options - 错误选项
|
||||
* @param {string} [options.code] - 错误码(使用 ERROR_CODES 枚举)
|
||||
* @param {string} [options.error] - 自定义错误消息(如提供则覆盖 code 对应的消息)
|
||||
* @param {string} [options.message] - 自定义错误消息(如提供则覆盖 code 对应的消息)
|
||||
* @param {number} [options.status] - 自定义 HTTP 状态码
|
||||
* @param {boolean} [options.isStreaming=false] - 是否为流式响应
|
||||
* @param {string} [options.raw] - 原始错误信息(用于调试)
|
||||
*/
|
||||
export function sendApiError(res, options) {
|
||||
const { code, error, status, isStreaming = false, raw } = options;
|
||||
const { code, message, status, isStreaming = false } = options;
|
||||
|
||||
// 获取错误详情
|
||||
const details = code ? getErrorDetails(code) : null;
|
||||
const errorMessage = error || (details ? details.message : '未知错误');
|
||||
const errorMessage = message || (details ? details.message : '未知错误');
|
||||
const errorType = details?.type || 'server_error';
|
||||
const httpStatus = status || (details ? details.status : 500);
|
||||
|
||||
// 构造错误响应体
|
||||
// 构造 OpenAI 标准错误响应体
|
||||
const payload = {
|
||||
error: errorMessage,
|
||||
code: code || 'INTERNAL_ERROR',
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: errorType,
|
||||
code: code || 'INTERNAL_ERROR'
|
||||
}
|
||||
};
|
||||
if (raw) {
|
||||
payload.raw = raw;
|
||||
}
|
||||
|
||||
if (isStreaming) {
|
||||
// 流式响应
|
||||
// 流式响应:发送错误事件然后结束
|
||||
sendSse(res, payload);
|
||||
sendSseDone(res);
|
||||
} else {
|
||||
|
||||
@@ -81,13 +81,13 @@ export function createRouter(context) {
|
||||
// 区分错误类型
|
||||
if (err.message.includes('Worker 不存在') || err.message.includes('Worker not found')) {
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.BAD_REQUEST,
|
||||
error: err.message
|
||||
code: ERROR_CODES.INVALID_MODEL,
|
||||
message: err.message
|
||||
});
|
||||
} else {
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.INTERNAL_ERROR,
|
||||
error: err.message
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function createRouter(context) {
|
||||
logger.warn('服务器', '非流式请求被拒绝 (队列已满)', { id: requestId, queueSize: status.total });
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.SERVER_BUSY,
|
||||
error: `服务器繁忙(队列: ${status.total}/${queueManager.maxQueueSize})。请使用流式模式 (stream: true) 或稍后重试。`
|
||||
message: `服务器繁忙(队列: ${status.total}/${queueManager.maxQueueSize})。请使用流式模式 (stream: true) 或稍后重试。`
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export function createRouter(context) {
|
||||
if (!parseResult.success) {
|
||||
sendApiError(res, {
|
||||
code: parseResult.error.code,
|
||||
error: parseResult.error.error,
|
||||
message: parseResult.error.error,
|
||||
isStreaming
|
||||
});
|
||||
return;
|
||||
@@ -171,7 +171,7 @@ export function createRouter(context) {
|
||||
logger.error('服务器', '请求处理失败', { id: requestId, error: err.message });
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.INTERNAL_ERROR,
|
||||
error: err.message
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -114,19 +114,27 @@ export function createQueueManager(queueConfig, callbacks) {
|
||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||
|
||||
// 处理结果
|
||||
let finalContent = '';
|
||||
|
||||
if (result.error) {
|
||||
// 适配器层已归一化错误,直接构造错误响应
|
||||
finalContent = `[生成错误] ${result.error}`;
|
||||
} else if (result.image) {
|
||||
// 生成失败:使用标准错误格式返回
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.GENERATION_FAILED,
|
||||
message: result.error,
|
||||
status: result.retryable ? 503 : 502,
|
||||
isStreaming
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成成功
|
||||
let finalContent = '';
|
||||
if (result.image) {
|
||||
finalContent = ``;
|
||||
logger.info('服务器', '图片已准备就绪 (Base64)', { id });
|
||||
} else {
|
||||
finalContent = result.text || '生成失败';
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
// 发送成功响应
|
||||
if (isStreaming) {
|
||||
const chunk = buildChatCompletionChunk(finalContent, modelName);
|
||||
sendSse(res, chunk);
|
||||
@@ -143,7 +151,7 @@ export function createQueueManager(queueConfig, callbacks) {
|
||||
logger.error('服务器', '任务处理失败', { id, error: err.message });
|
||||
sendApiError(res, {
|
||||
code: ERROR_CODES.INTERNAL_ERROR,
|
||||
error: err.message,
|
||||
message: err.message,
|
||||
isStreaming
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user