feat: 新增 Grok 图像节点、前端 UI 增强、重构 nano-banana 系列

- 新增 Grok Image 节点及客户端
- 新增 save_image_format 节点
- 新增前端 JS 扩展:画笔工具、点阵网格、侧边栏隐藏、资源切换、重命名等
- 重构 nano-banana 节点,移除 pro 版本
- 移除 multi_res_preview 节点
- 新增 http_error 工具模块
- 各客户端和节点优化改进

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Jony
2026-05-24 22:46:55 +08:00
co-authored by Claude Opus 4.6
parent c491731c99
commit 69279c654d
40 changed files with 3406 additions and 1599 deletions
+125
View File
@@ -0,0 +1,125 @@
"""
统一 HTTP 错误处理 & 退避重试模块
使用方式:
1. 对于 aiohttp 请求,用 async_request_with_retry() 包裹 POST/GET 调用
2. 对于已拿到 status code 的场景,调用 raise_for_status() 抛出友好错误
新增生图/视频节点时,请统一使用本模块处理 HTTP 错误。
"""
import asyncio
import random
from typing import Optional
import aiohttp
# ═══════════════════════════════════════════════════════════════════════════════
# 状态码 → 用户友好文案
# ═══════════════════════════════════════════════════════════════════════════════
HTTP_ERROR_MESSAGES = {
429: "模型速率超限或额度不足!",
502: "网关超时。请重试或将网络切换为美国直连",
503: "模型超载。请稍后重试!",
504: "网关超时。请稍后重试。",
}
# 可退避重试的状态码
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
# 退避重试默认参数
DEFAULT_MAX_RETRIES = 3
DEFAULT_BASE_DELAY = 2.0 # 首次重试等待秒数
DEFAULT_MAX_DELAY = 30.0 # 最大等待秒数
DEFAULT_BACKOFF_FACTOR = 2.0 # 指数退避因子
def get_friendly_message(status_code: int, raw_message: str = "") -> str:
"""根据状态码返回友好文案,未匹配则返回原始信息"""
friendly = HTTP_ERROR_MESSAGES.get(status_code)
if friendly:
return friendly
return raw_message or f"请求失败 ({status_code})"
def raise_for_status(status_code: int, raw_message: str = "", prefix: str = ""):
"""根据状态码抛出带友好文案的 RuntimeError"""
friendly = get_friendly_message(status_code, raw_message)
full_msg = f"{prefix}{friendly}" if prefix else friendly
raise RuntimeError(full_msg)
def is_retryable(status_code: int) -> bool:
return status_code in RETRYABLE_STATUS_CODES
def _compute_delay(attempt: int, base_delay: float, max_delay: float, backoff_factor: float) -> float:
"""计算第 attempt 次重试的等待时间(含 jitter)"""
delay = base_delay * (backoff_factor ** attempt)
delay = min(delay, max_delay)
jitter = random.uniform(0, delay * 0.3)
return delay + jitter
async def async_request_with_retry(
session: aiohttp.ClientSession,
method: str,
url: str,
*,
max_retries: int = DEFAULT_MAX_RETRIES,
base_delay: float = DEFAULT_BASE_DELAY,
max_delay: float = DEFAULT_MAX_DELAY,
backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
prefix: str = "",
**request_kwargs,
) -> aiohttp.ClientResponse:
"""
带退避重试的 aiohttp 请求。
仅对 RETRYABLE_STATUS_CODES (429/503/504) 进行重试。
超过最大重试次数后抛出友好 RuntimeError。
成功时返回 response 对象(调用者需在 async with 外自行处理 body)。
用法示例:
resp = await async_request_with_retry(session, "POST", url, json=body, headers=headers)
data = await resp.json()
"""
last_status: Optional[int] = None
last_message = ""
for attempt in range(max_retries + 1):
try:
resp = await session.request(method, url, **request_kwargs)
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
if attempt < max_retries:
delay = _compute_delay(attempt, base_delay, max_delay, backoff_factor)
print(f"{prefix}网络错误,{delay:.1f}s 后重试 ({attempt+1}/{max_retries})...")
await asyncio.sleep(delay)
continue
raise RuntimeError(f"{prefix}网络错误: {e}") from None
if resp.status == 200:
return resp
last_status = resp.status
try:
last_message = await resp.text()
except Exception:
last_message = ""
if is_retryable(resp.status) and attempt < max_retries:
delay = _compute_delay(attempt, base_delay, max_delay, backoff_factor)
friendly = get_friendly_message(resp.status)
print(f"{prefix}{friendly} {delay:.1f}s 后重试 ({attempt+1}/{max_retries})...")
await asyncio.sleep(delay)
continue
break
if last_status and last_status in HTTP_ERROR_MESSAGES:
raise_for_status(last_status, prefix=prefix)
raw_msg = last_message[:200] if last_message else ""
raise RuntimeError(f"{prefix}请求失败 ({last_status}): {raw_msg}")