69 lines
3.0 KiB
Python
69 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from utils.helper import UpstreamHTTPError
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UploadLimit:
|
|
blocked_until: datetime
|
|
reason: str
|
|
|
|
|
|
class UploadUnavailableError(RuntimeError):
|
|
def __init__(self, blocked_until: datetime | None = None, *, attempts_exhausted: bool = False):
|
|
message = (
|
|
"多个账号上传受限,本次自动切换已达到上限,请稍后重试。"
|
|
if attempts_exhausted else "当前没有可用于参考图上传的账号,请等待上传额度恢复或添加可用账号。"
|
|
)
|
|
if blocked_until:
|
|
message += f" 已知上传限制最早预计恢复时间:{blocked_until.astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')}。"
|
|
super().__init__(message)
|
|
|
|
|
|
def parse_upload_limit(error: Exception, now: datetime | None = None) -> UploadLimit | None:
|
|
"""Recognize file quota failures only at the upstream upload allocation endpoint."""
|
|
if not isinstance(error, UpstreamHTTPError) or error.status_code != 429 or error.context != "/backend-api/files":
|
|
return None
|
|
body = error.body
|
|
if isinstance(body, str):
|
|
try:
|
|
body = json.loads(body)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
if not isinstance(body, dict):
|
|
return None
|
|
detail = body.get("detail") or body.get("error") or body
|
|
if not isinstance(detail, dict):
|
|
return None
|
|
reason = str(detail.get("message") or "文件上传额度已用尽")
|
|
codes = {str(detail.get(key) or "").lower() for key in ("code", "error_code", "type")}
|
|
if not codes.intersection({"throttled", "file_upload_limit_exceeded", "upload_limit_exceeded"}) and not (
|
|
"上传上限" in reason or "upload limit" in reason.lower()
|
|
):
|
|
return None
|
|
now = now or datetime.now(timezone.utc)
|
|
for key in ("retry_at", "reset_at"):
|
|
value = detail.get(key)
|
|
if value:
|
|
try:
|
|
reset = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
if reset.tzinfo and reset > now:
|
|
return UploadLimit(reset, reason)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
if error.retry_after is not None and error.retry_after > 0:
|
|
seconds = min(error.retry_after, 365 * 86400)
|
|
else:
|
|
units = {"小时": 3600, "分钟": 60, "秒": 1, "天": 86400,
|
|
"day": 86400, "hour": 3600, "minute": 60, "second": 1}
|
|
matches = re.findall(r"(\d+(?:\.\d+)?)\s*(小时|分钟|秒|天|days?|hours?|minutes?|seconds?)", reason.lower())
|
|
seconds = sum(float(number) * units[unit.rstrip("s")] for number, unit in matches)
|
|
# Unknown reset time: temporary cooling, never a fabricated daily reset.
|
|
seconds = min(seconds, 365 * 86400) if seconds > 0 else 15 * 60
|
|
return UploadLimit(now + timedelta(seconds=max(1, seconds)), reason)
|