feat: 增加故障转移和重试,并整理代码

This commit is contained in:
foxhui
2025-12-16 02:38:36 +08:00
Unverified
parent 6eb8229ae3
commit 1c4fc1a314
26 changed files with 1746 additions and 1339 deletions
+34 -2
View File
@@ -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
View File
@@ -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 {
+6 -6
View File
@@ -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
View File
@@ -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 = `![generated](${result.image})`;
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
});
}