Initial project import
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user