Files
--/services/upload_limits.py
2026-09-18 03:46:58 +08:00

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)