176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Translate API error messages to user-friendly Chinese.
|
||
|
|
*
|
||
|
|
* Classifies errors into categories and provides user-friendly messages
|
||
|
|
* with suggested recovery actions.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export type TaskErrorCategory =
|
||
|
|
| "content_policy"
|
||
|
|
| "auth_failure"
|
||
|
|
| "insufficient_balance"
|
||
|
|
| "unsupported_model"
|
||
|
|
| "concurrency_busy"
|
||
|
|
| "invalid_asset"
|
||
|
|
| "network_failure"
|
||
|
|
| "timeout"
|
||
|
|
| "cancelled"
|
||
|
|
| "unknown";
|
||
|
|
|
||
|
|
export interface TaskErrorInfo {
|
||
|
|
category: TaskErrorCategory;
|
||
|
|
message: string;
|
||
|
|
action: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ERROR_RULES: Array<{
|
||
|
|
pattern: RegExp;
|
||
|
|
category: TaskErrorCategory;
|
||
|
|
message: string;
|
||
|
|
action: string;
|
||
|
|
}> = [
|
||
|
|
// Content policy
|
||
|
|
{
|
||
|
|
pattern: /violated? our (?:relevant )?policies|content policies|violated? content policy|content.*filter|safety.*filter|moderation|blocked by.*filter|nsfw|inappropriate|explicit.*content|adult.*content/i,
|
||
|
|
category: "content_policy",
|
||
|
|
message: "输入词汇包含违规信息,已停止生成",
|
||
|
|
action: "修改提示词后重试",
|
||
|
|
},
|
||
|
|
// Auth failure
|
||
|
|
{
|
||
|
|
pattern: /unauthorized|authentication.*fail|invalid.*token|token.*expired|session.*expired|401|login.*required/i,
|
||
|
|
category: "auth_failure",
|
||
|
|
message: "登录已过期,请重新登录",
|
||
|
|
action: "重新登录",
|
||
|
|
},
|
||
|
|
// Insufficient balance
|
||
|
|
{
|
||
|
|
pattern: /insufficient.*balance|余额不足|积分不足|INSUFFICIENT_BALANCE|balance.*not.*enough|402/i,
|
||
|
|
category: "insufficient_balance",
|
||
|
|
message: "余额不足,请充值后重试",
|
||
|
|
action: "去充值",
|
||
|
|
},
|
||
|
|
// Concurrency busy
|
||
|
|
{
|
||
|
|
pattern: /concurrency pool.*full|pool is full|concurrency.*limit|too many.*concurrent|排队繁忙/i,
|
||
|
|
category: "concurrency_busy",
|
||
|
|
message: "当前模型排队繁忙,请稍后重试或切换其他模型",
|
||
|
|
action: "稍后重试或切换模型",
|
||
|
|
},
|
||
|
|
// Rate limit
|
||
|
|
{
|
||
|
|
pattern: /rate limit|too many requests|429/i,
|
||
|
|
category: "concurrency_busy",
|
||
|
|
message: "请求过于频繁,请稍后再试",
|
||
|
|
action: "稍后重试",
|
||
|
|
},
|
||
|
|
// Unsupported model
|
||
|
|
{
|
||
|
|
pattern: /unsupported.*model|model.*not.*support|model.*not.*found|ENTERPRISE_VIDEO_MODEL_NOT_ALLOWED|not.*available/i,
|
||
|
|
category: "unsupported_model",
|
||
|
|
message: "当前模型暂不可用,请切换其他模型重试",
|
||
|
|
action: "切换模型",
|
||
|
|
},
|
||
|
|
// Invalid asset / upload
|
||
|
|
{
|
||
|
|
pattern: /upload.*fail|asset.*fail|素材|invalid.*image|invalid.*video|file.*too.*large/i,
|
||
|
|
category: "invalid_asset",
|
||
|
|
message: "素材上传失败,请重新上传后重试",
|
||
|
|
action: "重新上传素材",
|
||
|
|
},
|
||
|
|
// Network failure
|
||
|
|
{
|
||
|
|
pattern: /network|connection|fetch failed|ECONNREFUSED|ENOTFOUND|socket.*hang/i,
|
||
|
|
category: "network_failure",
|
||
|
|
message: "网络错误,请检查网络后重试",
|
||
|
|
action: "检查网络后重试",
|
||
|
|
},
|
||
|
|
// Timeout
|
||
|
|
{
|
||
|
|
pattern: /timeout|timed? out|ETIMEDOUT/i,
|
||
|
|
category: "timeout",
|
||
|
|
message: "任务超时,请稍后在任务历史中查看结果",
|
||
|
|
action: "稍后重试",
|
||
|
|
},
|
||
|
|
// Quota exceeded
|
||
|
|
{
|
||
|
|
pattern: /quota exceeded|quota.*limit/i,
|
||
|
|
category: "insufficient_balance",
|
||
|
|
message: "配额已用完,请联系管理员",
|
||
|
|
action: "联系管理员",
|
||
|
|
},
|
||
|
|
// Cancelled
|
||
|
|
{
|
||
|
|
pattern: /cancelled|已取消/i,
|
||
|
|
category: "cancelled",
|
||
|
|
message: "已取消",
|
||
|
|
action: "重新开始",
|
||
|
|
},
|
||
|
|
// All providers failed
|
||
|
|
{
|
||
|
|
pattern: /all.*providers.*failed|provider.*fail/i,
|
||
|
|
category: "concurrency_busy",
|
||
|
|
message: "所有可用模型均暂时不可用,请稍后重试",
|
||
|
|
action: "稍后重试",
|
||
|
|
},
|
||
|
|
// Upstream / service error
|
||
|
|
{
|
||
|
|
pattern: /upstream.*error|文本服务返回|服务返回.*HTTP|openai_error|internal.*server.*error|500|502|503/i,
|
||
|
|
category: "network_failure",
|
||
|
|
message: "AI 服务暂时不可用,请稍后重试",
|
||
|
|
action: "稍后重试",
|
||
|
|
},
|
||
|
|
// Access denied / forbidden
|
||
|
|
{
|
||
|
|
pattern: /access.*denied|forbidden|403|permission.*denied|权限/i,
|
||
|
|
category: "auth_failure",
|
||
|
|
message: "模型权限未开通,请联系管理员",
|
||
|
|
action: "联系管理员",
|
||
|
|
},
|
||
|
|
// Image format / size issues
|
||
|
|
{
|
||
|
|
pattern: /image.*too.*large|image.*format|图片.*大小|图片.*格式|invalid.*file.*type/i,
|
||
|
|
category: "invalid_asset",
|
||
|
|
message: "图片格式或大小不符合要求,请调整后重试",
|
||
|
|
action: "调整图片后重试",
|
||
|
|
},
|
||
|
|
// Aborted (user or timeout abort)
|
||
|
|
{
|
||
|
|
pattern: /aborted|abort/i,
|
||
|
|
category: "timeout",
|
||
|
|
message: "请求已中断,请重试",
|
||
|
|
action: "重试",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Classify an API error into a structured result with category, message, and action.
|
||
|
|
*/
|
||
|
|
export function classifyTaskError(error: string | undefined | null): TaskErrorInfo {
|
||
|
|
if (!error) {
|
||
|
|
return { category: "unknown", message: "任务失败,请重试", action: "重试" };
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const rule of ERROR_RULES) {
|
||
|
|
if (rule.pattern.test(error)) {
|
||
|
|
return { category: rule.category, message: rule.message, action: rule.action };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const hasChinese = /[一-鿿]/.test(error);
|
||
|
|
if (hasChinese) {
|
||
|
|
const truncated = error.length > 80 ? `${error.slice(0, 80)}...` : error;
|
||
|
|
return { category: "unknown", message: truncated, action: "重试" };
|
||
|
|
}
|
||
|
|
|
||
|
|
return { category: "unknown", message: "服务异常,请稍后重试", action: "重试" };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Translate an API error message to a user-friendly Chinese message.
|
||
|
|
* Convenience wrapper around classifyTaskError.
|
||
|
|
*/
|
||
|
|
export function translateTaskError(error: string | undefined | null): string {
|
||
|
|
return classifyTaskError(error).message;
|
||
|
|
}
|