File size: 6,712 Bytes
d3cadd5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
"""错误处理模块 - 统一的错误分类和处理
检测各种 Kiro API 错误类型:
- 账号封禁 (TEMPORARILY_SUSPENDED)
- 配额超限 (Rate Limit)
- 内容过长 (CONTENT_LENGTH_EXCEEDS_THRESHOLD)
- 认证失败 (Unauthorized)
- 服务不可用 (Service Unavailable)
"""
import re
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Tuple
class ErrorType(str, Enum):
"""错误类型"""
ACCOUNT_SUSPENDED = "account_suspended" # 账号被封禁
RATE_LIMITED = "rate_limited" # 配额超限
CONTENT_TOO_LONG = "content_too_long" # 内容过长
AUTH_FAILED = "auth_failed" # 认证失败
SERVICE_UNAVAILABLE = "service_unavailable" # 服务不可用
MODEL_UNAVAILABLE = "model_unavailable" # 模型不可用
UNKNOWN = "unknown" # 未知错误
@dataclass
class KiroError:
"""Kiro API 错误"""
type: ErrorType
status_code: int
message: str
user_message: str # 用户友好的消息
should_disable_account: bool = False # 是否应该禁用账号
should_switch_account: bool = False # 是否应该切换账号
should_retry: bool = False # 是否应该重试
cooldown_seconds: int = 0 # 冷却时间
def classify_error(status_code: int, error_text: str) -> KiroError:
"""分类 Kiro API 错误
Args:
status_code: HTTP 状态码
error_text: 错误响应文本
Returns:
KiroError 对象
"""
error_lower = error_text.lower()
# 1. 账号封禁检测 (最严重)
# 检测: AccountSuspendedException, 423 状态码, temporarily_suspended, suspended
is_suspended = (
status_code == 423 or
"accountsuspendedexception" in error_lower or
"temporarily_suspended" in error_lower or
"suspended" in error_lower
)
if is_suspended:
# 提取 User ID
user_id_match = re.search(r'User ID \(([^)]+)\)', error_text)
user_id = user_id_match.group(1) if user_id_match else "unknown"
return KiroError(
type=ErrorType.ACCOUNT_SUSPENDED,
status_code=status_code,
message=error_text,
user_message=f"⚠️ 账号已被封禁 (User ID: {user_id})。请联系 AWS 支持解封: https://support.aws.amazon.com/#/contacts/kiro",
should_disable_account=True,
should_switch_account=True,
)
# 2. 402 Payment Required - 额度用尽(不触发冷却,仅切换账号)
if status_code == 402 or "payment required" in error_lower or "insufficient" in error_lower:
return KiroError(
type=ErrorType.RATE_LIMITED,
status_code=status_code,
message=error_text,
user_message="账号额度已用尽,已切换到其他账号",
should_switch_account=False, # 不自动切换,让上层逻辑处理
cooldown_seconds=0, # 不触发冷却
)
# 3. 配额超限检测 (仅 429 触发冷却)
if status_code == 429:
return KiroError(
type=ErrorType.RATE_LIMITED,
status_code=status_code,
message=error_text,
user_message="请求过于频繁,账号已进入冷却期",
should_switch_account=True,
cooldown_seconds=30, # 基础冷却时间,实际由 QuotaManager 动态管理
)
# 4. 内容过长检测
if "content_length_exceeds_threshold" in error_lower or (
"too long" in error_lower and ("input" in error_lower or "content" in error_lower)
):
return KiroError(
type=ErrorType.CONTENT_TOO_LONG,
status_code=status_code,
message=error_text,
user_message="对话历史过长,请使用 /clear 清空对话",
should_retry=True,
)
# 5. 认证失败检测
if status_code == 401 or "unauthorized" in error_lower or "invalid token" in error_lower:
return KiroError(
type=ErrorType.AUTH_FAILED,
status_code=status_code,
message=error_text,
user_message="Token 已过期或无效,请刷新 Token",
should_switch_account=True,
)
# 6. 模型不可用检测
if "model_temporarily_unavailable" in error_lower or "unexpectedly high load" in error_lower:
return KiroError(
type=ErrorType.MODEL_UNAVAILABLE,
status_code=status_code,
message=error_text,
user_message="模型暂时不可用,请稍后重试",
should_retry=True,
)
# 7. 服务不可用检测
if status_code in (502, 503, 504) or "service unavailable" in error_lower:
return KiroError(
type=ErrorType.SERVICE_UNAVAILABLE,
status_code=status_code,
message=error_text,
user_message="服务暂时不可用,请稍后重试",
should_retry=True,
)
# 8. 未知错误
return KiroError(
type=ErrorType.UNKNOWN,
status_code=status_code,
message=error_text,
user_message=f"API 错误 ({status_code})",
)
def is_account_suspended(status_code: int, error_text: str) -> bool:
"""检查是否为账号封禁错误"""
error = classify_error(status_code, error_text)
return error.type == ErrorType.ACCOUNT_SUSPENDED
def get_anthropic_error_response(error: KiroError) -> dict:
"""生成 Anthropic 格式的错误响应"""
error_type_map = {
ErrorType.ACCOUNT_SUSPENDED: "authentication_error",
ErrorType.RATE_LIMITED: "rate_limit_error",
ErrorType.CONTENT_TOO_LONG: "invalid_request_error",
ErrorType.AUTH_FAILED: "authentication_error",
ErrorType.SERVICE_UNAVAILABLE: "api_error",
ErrorType.MODEL_UNAVAILABLE: "overloaded_error",
ErrorType.UNKNOWN: "api_error",
}
return {
"type": "error",
"error": {
"type": error_type_map.get(error.type, "api_error"),
"message": error.user_message
}
}
def format_error_log(error: KiroError, account_id: str = None) -> str:
"""格式化错误日志"""
lines = [
f"[{error.type.value.upper()}]",
f" Status: {error.status_code}",
f" Message: {error.user_message}",
]
if account_id:
lines.insert(1, f" Account: {account_id}")
if error.should_disable_account:
lines.append(" Action: 账号已被禁用")
elif error.should_switch_account:
lines.append(" Action: 切换到其他账号")
return "\n".join(lines)
|