feat: 错误提示中文化、GPT Image 余额查询、Nano Banana 自动重试
- gemini_client: 更新 429/504 中文错误文案,与产品文案对齐 - gpt_image_client: 新增 query_balance_sync / format_balance_info 余额查询方法 - gpt_image: 每次执行后打印余额日志(finally 块保证触发) - nano_banana_pro: 单张生成支持自动重试,遇 429/503/504 最多重试 4 次,指数退避 2-32s,终端显示显眼重试状态 - nano_banana_pro: 关闭调试日志(DEBUG_LOG_ENABLED = False) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
949f7bb180
commit
fe3cc65b71
@@ -115,13 +115,13 @@ class GeminiAPIClient(BaseAPIClient):
|
|||||||
return "/v1beta/models/gemini-3-pro-image-preview:generateContent"
|
return "/v1beta/models/gemini-3-pro-image-preview:generateContent"
|
||||||
|
|
||||||
def get_http_error_message(self, status_code: int, error_message: str) -> Optional[str]:
|
def get_http_error_message(self, status_code: int, error_message: str) -> Optional[str]:
|
||||||
"""Gemini 请求 429/503 时返回图中约定的多行错误框文案。"""
|
"""Gemini 请求 429/503/504 时返回中文错误文案。"""
|
||||||
if status_code == 429:
|
if status_code == 429:
|
||||||
return "该型号资源已耗尽,但只是暂时的,请稍后重试。"
|
return "此型号资源暂时耗尽,继续重试即可"
|
||||||
if status_code == 503:
|
if status_code == 503:
|
||||||
return "此型号目前需求量较大。需求高峰通常是暂时的。请稍后再试。"
|
return "此型号目前需求量较大。需求高峰通常是暂时的。请稍后再试。"
|
||||||
if status_code == 504:
|
if status_code == 504:
|
||||||
return "服务无法在截止期限内完成处理。请稍后重试。"
|
return "服务无法在截止期限内完成处理。可能原因是:您的提示词过大,无法及时处理。"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def build_request_body(
|
def build_request_body(
|
||||||
|
|||||||
@@ -417,3 +417,35 @@ class GptImageClient:
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"o1key GPT Image 请求超时(>{_REQUEST_TIMEOUT}s),请检查网络或稍后重试"
|
f"o1key GPT Image 请求超时(>{_REQUEST_TIMEOUT}s),请检查网络或稍后重试"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── 余额查询 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _query_balance_async(self) -> dict:
|
||||||
|
url = f"{self.base_url}/api/usage/token"
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
||||||
|
async with session.get(url, headers=self._auth_headers()) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"余额查询失败 HTTP {resp.status}")
|
||||||
|
return await resp.json()
|
||||||
|
|
||||||
|
def query_balance_sync(self) -> dict:
|
||||||
|
def _run():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return loop.run_until_complete(self._query_balance_async())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
return executor.submit(_run).result(timeout=15)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_balance_info(balance_data: dict) -> str:
|
||||||
|
data = balance_data.get("data", {})
|
||||||
|
api_name = data.get("name", "未知")
|
||||||
|
total_available = data.get("total_available", 0)
|
||||||
|
balance_in_dollars = total_available / 500000
|
||||||
|
return f"当前余额:{balance_in_dollars:.2f} | API:{api_name}"
|
||||||
|
|||||||
+17
-5
@@ -141,10 +141,11 @@ class O1keyGPTImage:
|
|||||||
raise ValueError("未授权!") from None
|
raise ValueError("未授权!") from None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# ── 4. 解析批量提示词 ─────────────────────────────────────────────────
|
try:
|
||||||
|
# ── 4. 解析批量提示词 ─────────────────────────────────────────────
|
||||||
batch_prompts = parse_batch_prompts(prompt)
|
batch_prompts = parse_batch_prompts(prompt)
|
||||||
|
|
||||||
# ── 5. 调用 API ───────────────────────────────────────────────────────
|
# ── 5. 调用 API ───────────────────────────────────────────────────
|
||||||
all_pil_images = []
|
all_pil_images = []
|
||||||
|
|
||||||
if batch_prompts:
|
if batch_prompts:
|
||||||
@@ -193,14 +194,14 @@ class O1keyGPTImage:
|
|||||||
print(f"[o1key GPT Image] ❌ {error_msg}")
|
print(f"[o1key GPT Image] ❌ {error_msg}")
|
||||||
raise RuntimeError(error_msg) from None
|
raise RuntimeError(error_msg) from None
|
||||||
|
|
||||||
# ── 6. 检查是否有可用图像 ─────────────────────────────────────────────
|
# ── 6. 检查是否有可用图像 ─────────────────────────────────────────
|
||||||
if not all_pil_images:
|
if not all_pil_images:
|
||||||
raise RuntimeError("所有提示词均生成失败,无可用图像输出")
|
raise RuntimeError("所有提示词均生成失败,无可用图像输出")
|
||||||
|
|
||||||
# ── 7. PIL → tensor ───────────────────────────────────────────────────
|
# ── 7. PIL → tensor ───────────────────────────────────────────────
|
||||||
output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images)
|
output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images)
|
||||||
|
|
||||||
# ── 8. 完成日志 ───────────────────────────────────────────────────────
|
# ── 8. 完成日志 ───────────────────────────────────────────────────
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
print(
|
print(
|
||||||
f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s,"
|
f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s,"
|
||||||
@@ -209,3 +210,14 @@ class O1keyGPTImage:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (output_tensor,)
|
return (output_tensor,)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._print_balance(client)
|
||||||
|
|
||||||
|
def _print_balance(self, client):
|
||||||
|
try:
|
||||||
|
balance_data = client.query_balance_sync()
|
||||||
|
balance_info = client.format_balance_info(balance_data)
|
||||||
|
print(f"[o1key GPT Image] {balance_info}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ except ImportError:
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 是否启用调试日志(打印完整的 API 响应内容)
|
# 是否启用调试日志(打印完整的 API 响应内容)
|
||||||
# 设置为 True 以启用调试日志,False 以禁用
|
# 设置为 True 以启用调试日志,False 以禁用
|
||||||
DEBUG_LOG_ENABLED = True
|
DEBUG_LOG_ENABLED = False
|
||||||
# 是否启用请求体日志(打印发送给 API 的请求体,base64 图片数据将自动截断)
|
# 是否启用请求体日志(打印发送给 API 的请求体,base64 图片数据将自动截断)
|
||||||
# 设置为 True 以启用请求体日志,False 以禁用
|
# 设置为 True 以启用请求体日志,False 以禁用
|
||||||
REQUEST_LOG_ENABLED = False
|
REQUEST_LOG_ENABLED = False
|
||||||
@@ -665,7 +665,11 @@ class NanoBananaPro:
|
|||||||
else:
|
else:
|
||||||
# 单提示词模式
|
# 单提示词模式
|
||||||
if 生图数量 == 1:
|
if 生图数量 == 1:
|
||||||
# 单张:同步生成,输出 tensor
|
# 单张:同步生成,自动重试(429/503/504)
|
||||||
|
_RETRY_CODES = ("429", "503", "504")
|
||||||
|
_MAX_RETRIES = 5
|
||||||
|
for _attempt in range(1, _MAX_RETRIES + 1):
|
||||||
|
try:
|
||||||
generated_images = self.client.generate_sync(
|
generated_images = self.client.generate_sync(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=模型,
|
model=模型,
|
||||||
@@ -679,6 +683,19 @@ class NanoBananaPro:
|
|||||||
enable_grounding=enable_grounding,
|
enable_grounding=enable_grounding,
|
||||||
enable_image_search=enable_image_search,
|
enable_image_search=enable_image_search,
|
||||||
)
|
)
|
||||||
|
break
|
||||||
|
except RuntimeError as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if any(code in error_msg for code in _RETRY_CODES) and _attempt < _MAX_RETRIES:
|
||||||
|
_wait = 2 ** _attempt
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
print(f"⚠️ Nano Banana Pro 自动重试 [{_attempt}/{_MAX_RETRIES - 1}]")
|
||||||
|
print(f" 原因:{error_msg}")
|
||||||
|
print(f" 等待 {_wait}s 后重试...")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
time.sleep(_wait)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
# 多张:异步并发,内存输出
|
# 多张:异步并发,内存输出
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user