Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
+201
-3
@@ -11,6 +11,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
import aiohttp
|
||||
@@ -21,23 +22,125 @@ import aiohttp
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
HTTP_ERROR_MESSAGES = {
|
||||
402: "账户余额或模型额度不足,请充值或检查令牌额度。",
|
||||
429: "模型速率超限或额度不足!",
|
||||
422: "输入内容未通过安全检查,请调整提示词或参考素材。",
|
||||
502: "网关超时。请重试或将网络切换为美国直连",
|
||||
503: "模型超载。请稍后重试!",
|
||||
504: "网关超时。请稍后重试。",
|
||||
529: "MiniMax 上游模型过载,请稍后重试。",
|
||||
}
|
||||
|
||||
# 【o1key 图片生成】节点的模型无关错误封装。GPT Image 与所有
|
||||
# Nano Banana 分支都必须使用同一份映射,不能按模型拆分。
|
||||
O1KEY_IMAGE_ERROR_CONTENT_MESSAGES = {
|
||||
"content rejected: the image was flagged as unsafe by the content safety system": "内容被拒绝:该图像被内容安全系统标记为不安全。",
|
||||
"Your request was rejected by the safety system": "您的请求已被安全系统拒绝",
|
||||
"insufficient balance": "上游额度不足!",
|
||||
"Image generation returned empty response": "图片生成过程中被内容审查机制拒绝!",
|
||||
"The provided prompt is considered unsafe and it cannot be used to generate content": "提供的提示被认为是不安全的,不能用于生成内容。",
|
||||
}
|
||||
|
||||
# 错误内容关键词 → 用户友好文案(优先于状态码匹配)
|
||||
ERROR_CONTENT_MESSAGES = {
|
||||
"Your request was rejected by the safety system": "请求被安全系统拦截:请调整提示词,避免敏感、违规、血腥、色情、仇恨、未成年人或真实人物等高风险内容。",
|
||||
**O1KEY_IMAGE_ERROR_CONTENT_MESSAGES,
|
||||
"safety system": "请求被安全系统拦截:请调整提示词,避免敏感、违规、血腥、色情、仇恨、未成年人或真实人物等高风险内容。",
|
||||
"unexpected end of JSON input": "通常重试能解决;反复出现就降低分辨率、数量或换网络线路。",
|
||||
"unexpected end of JSON input": "通常重试能解决;反复出现就降低分辨率、数量,或在 API密钥设置中切换全局线路。",
|
||||
"The current model has a high load": "模型过载,请稍后重试!",
|
||||
"system error": "系统错误,请稍后重试。",
|
||||
}
|
||||
|
||||
|
||||
def format_o1key_image_error(value: Any) -> str:
|
||||
"""Format provider errors for every model in the o1key image generator."""
|
||||
message = str(value or "生成失败")
|
||||
message_lower = message.lower()
|
||||
for keyword, friendly_message in O1KEY_IMAGE_ERROR_CONTENT_MESSAGES.items():
|
||||
if keyword.lower() in message_lower:
|
||||
return friendly_message
|
||||
return message
|
||||
|
||||
|
||||
O1KEY_VIDEO_COPYRIGHT_MESSAGES = {
|
||||
"audio": "请求失败,输出视频中音频触发版权限制!",
|
||||
"video": "请求失败,输出视频触发版权限制!",
|
||||
"content": "请求失败,提示词触发版权限制!",
|
||||
"real": "请求失败,真人内容触发版权限制!",
|
||||
"unknown": "请求失败,生成内容触发版权限制!",
|
||||
}
|
||||
O1KEY_VIDEO_REVIEW_MESSAGES = {
|
||||
"audio": "请求失败,输出视频中音频触发审查!",
|
||||
"video": "请求失败,输出视频触发审查!",
|
||||
"content": "请求失败,提示词触发审查!",
|
||||
"real": "请求失败,真人内容触发审查!",
|
||||
"unknown": "请求失败,生成内容触发审查!",
|
||||
}
|
||||
O1KEY_VIDEO_REVIEW_MARKERS = (
|
||||
"sensitive",
|
||||
"safety",
|
||||
"moderation",
|
||||
"policy violation",
|
||||
"policy_violation",
|
||||
"policyviolation",
|
||||
"unsafe",
|
||||
"censor",
|
||||
"review",
|
||||
)
|
||||
O1KEY_VIDEO_SUBJECT_REVIEW_MARKERS = ("rejected", "blocked")
|
||||
_O1KEY_VIDEO_FIELD_RE = re.compile(
|
||||
r"(?:[\"']?(?:field|type|category|source)[\"']?\s*[:=]\s*[\"']?)"
|
||||
r"(audio|video|content|real)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _o1key_video_error_subject(message: str) -> str:
|
||||
"""Identify which part of a video request triggered an upstream review."""
|
||||
explicit_field = _O1KEY_VIDEO_FIELD_RE.search(message)
|
||||
if explicit_field:
|
||||
return explicit_field.group(1).lower()
|
||||
|
||||
message_lower = message.lower()
|
||||
if re.search(r"\baudio\b", message_lower):
|
||||
return "audio"
|
||||
if re.search(r"\breal\b|\breal[-_ ]?person\b|真人", message_lower):
|
||||
return "real"
|
||||
if re.search(r"\bprompt\b|input[-_ ]?content|提示词", message_lower):
|
||||
return "content"
|
||||
if "outputvideo" in message_lower or "output_video" in message_lower:
|
||||
return "video"
|
||||
if re.search(r"output[-_ ]+video|\bvideo\b", message_lower):
|
||||
return "video"
|
||||
if re.search(r"\bcontent\b", message_lower):
|
||||
return "content"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def format_o1key_video_error(value: Any) -> str:
|
||||
"""Format review errors for every model in the o1key video generator.
|
||||
|
||||
Upstream providers use several envelope shapes, but their error text or
|
||||
field values consistently identify the reviewed subject as audio, video,
|
||||
content (the prompt), or real-person content. Copyright takes precedence
|
||||
over the broader safety-review markers.
|
||||
"""
|
||||
message = str(value or "视频生成失败")
|
||||
message_lower = message.lower()
|
||||
subject = _o1key_video_error_subject(message)
|
||||
if "copyright" in message_lower:
|
||||
return O1KEY_VIDEO_COPYRIGHT_MESSAGES[subject]
|
||||
has_review_marker = any(
|
||||
marker in message_lower for marker in O1KEY_VIDEO_REVIEW_MARKERS
|
||||
)
|
||||
has_subject_rejection = subject != "unknown" and any(
|
||||
marker in message_lower for marker in O1KEY_VIDEO_SUBJECT_REVIEW_MARKERS
|
||||
)
|
||||
if has_review_marker or has_subject_rejection:
|
||||
return O1KEY_VIDEO_REVIEW_MESSAGES[subject]
|
||||
return message
|
||||
|
||||
# 可退避重试的状态码
|
||||
RETRYABLE_STATUS_CODES = {429, 502, 503, 504, 524}
|
||||
RETRYABLE_STATUS_CODES = {429, 502, 503, 504, 524, 529}
|
||||
|
||||
# 退避重试默认参数
|
||||
DEFAULT_MAX_RETRIES = 3
|
||||
@@ -125,6 +228,101 @@ def is_retryable(status_code: int) -> bool:
|
||||
return status_code in RETRYABLE_STATUS_CODES
|
||||
|
||||
|
||||
def extract_error_detail(payload: Any) -> dict:
|
||||
"""Extract error_detail from async task payloads."""
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
queue = [payload]
|
||||
seen = set()
|
||||
while queue:
|
||||
current = queue.pop(0)
|
||||
if not isinstance(current, dict):
|
||||
continue
|
||||
|
||||
obj_id = id(current)
|
||||
if obj_id in seen:
|
||||
continue
|
||||
seen.add(obj_id)
|
||||
|
||||
for key in ("error_detail", "errorDetail", "error_details", "errorDetails"):
|
||||
detail = current.get(key)
|
||||
if isinstance(detail, dict):
|
||||
return detail
|
||||
|
||||
for key in ("data", "result", "response", "output", "task_result", "content"):
|
||||
nested = current.get(key)
|
||||
if isinstance(nested, dict):
|
||||
queue.append(nested)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _coerce_int(value: Any) -> Optional[int]:
|
||||
if isinstance(value, bool) or value is None:
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return int(value)
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return int(float(text))
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def extract_error_status_code(error_detail: Any) -> Optional[int]:
|
||||
"""Extract the real upstream HTTP status from error_detail."""
|
||||
if not isinstance(error_detail, dict):
|
||||
return None
|
||||
for key in (
|
||||
"upstream_status",
|
||||
"upstreamStatus",
|
||||
"upstream_status_code",
|
||||
"status_code",
|
||||
"statusCode",
|
||||
"http_status",
|
||||
"httpStatus",
|
||||
"status",
|
||||
):
|
||||
status_code = _coerce_int(error_detail.get(key))
|
||||
if status_code is not None:
|
||||
return status_code
|
||||
return None
|
||||
|
||||
|
||||
def is_error_detail_retryable(error_detail: Any) -> bool:
|
||||
if not isinstance(error_detail, dict):
|
||||
return False
|
||||
retryable = error_detail.get("retryable")
|
||||
if retryable is True:
|
||||
return True
|
||||
if isinstance(retryable, str):
|
||||
return retryable.strip().lower() in ("true", "1", "yes")
|
||||
return False
|
||||
|
||||
|
||||
def extract_retry_after_seconds(error_detail: Any) -> Optional[float]:
|
||||
if not isinstance(error_detail, dict):
|
||||
return None
|
||||
for key in ("retry_after_seconds", "retryAfterSeconds", "retry_after", "retryAfter"):
|
||||
value = error_detail.get(key)
|
||||
if isinstance(value, bool) or value is None:
|
||||
continue
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if seconds >= 0:
|
||||
return seconds
|
||||
return None
|
||||
|
||||
|
||||
def _compute_delay(attempt: int, base_delay: float, max_delay: float, backoff_factor: float) -> float:
|
||||
"""计算第 attempt 次重试的等待时间(含 jitter)"""
|
||||
delay = base_delay * (backoff_factor ** attempt)
|
||||
|
||||
Reference in New Issue
Block a user