From fe3cc65b718c4ea670e50883570243d22ea20ce8 Mon Sep 17 00:00:00 2001 From: o1key <951565127@qq.com> Date: Wed, 22 Apr 2026 18:10:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E5=8C=96=E3=80=81GPT=20Image=20=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E6=9F=A5=E8=AF=A2=E3=80=81Nano=20Banana=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- clients/gemini_client.py | 6 +- clients/gpt_image_client.py | 32 +++++++++++ nodes/gpt_image.py | 108 ++++++++++++++++++++---------------- nodes/nano_banana_pro.py | 47 +++++++++++----- 4 files changed, 127 insertions(+), 66 deletions(-) diff --git a/clients/gemini_client.py b/clients/gemini_client.py index 43ef26a..3b085d9 100644 --- a/clients/gemini_client.py +++ b/clients/gemini_client.py @@ -115,13 +115,13 @@ class GeminiAPIClient(BaseAPIClient): return "/v1beta/models/gemini-3-pro-image-preview:generateContent" 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: - return "该型号资源已耗尽,但只是暂时的,请稍后重试。" + return "此型号资源暂时耗尽,继续重试即可" if status_code == 503: return "此型号目前需求量较大。需求高峰通常是暂时的。请稍后再试。" if status_code == 504: - return "服务无法在截止期限内完成处理。请稍后重试。" + return "服务无法在截止期限内完成处理。可能原因是:您的提示词过大,无法及时处理。" return None def build_request_body( diff --git a/clients/gpt_image_client.py b/clients/gpt_image_client.py index 3b13d78..23fa308 100644 --- a/clients/gpt_image_client.py +++ b/clients/gpt_image_client.py @@ -417,3 +417,35 @@ class GptImageClient: raise RuntimeError( 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}" diff --git a/nodes/gpt_image.py b/nodes/gpt_image.py index 647d852..c44b88f 100644 --- a/nodes/gpt_image.py +++ b/nodes/gpt_image.py @@ -141,20 +141,44 @@ class O1keyGPTImage: raise ValueError("未授权!") from None raise - # ── 4. 解析批量提示词 ───────────────────────────────────────────────── - batch_prompts = parse_batch_prompts(prompt) + try: + # ── 4. 解析批量提示词 ───────────────────────────────────────────── + batch_prompts = parse_batch_prompts(prompt) - # ── 5. 调用 API ─────────────────────────────────────────────────────── - all_pil_images = [] + # ── 5. 调用 API ─────────────────────────────────────────────────── + all_pil_images = [] - if batch_prompts: - # 批量模式:逐条提示词调用 - total = len(batch_prompts) - print(f"[o1key GPT Image] 批量模式 | {total} 条提示词 | 每条生成 {生图数量} 张") - for idx, p in enumerate(batch_prompts, 1): + if batch_prompts: + # 批量模式:逐条提示词调用 + total = len(batch_prompts) + print(f"[o1key GPT Image] 批量模式 | {total} 条提示词 | 每条生成 {生图数量} 张") + for idx, p in enumerate(batch_prompts, 1): + try: + pil_images = client.run_sync( + prompt=p, + model=模型, + quality=quality, + background="auto", + size=size, + n=生图数量, + seed=seed, + image_tensor=图片, + mask_tensor=遮罩, + ) + all_pil_images.extend(pil_images) + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}") + except Exception as e: + error_msg = str(e).split('\n')[0] + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ❌ {snippet} → {error_msg}") + else: + # 单提示词模式 + if not prompt or not prompt.strip(): + raise ValueError("提示词不能为空") try: pil_images = client.run_sync( - prompt=p, + prompt=prompt, model=模型, quality=quality, background="auto", @@ -165,47 +189,35 @@ class O1keyGPTImage: mask_tensor=遮罩, ) all_pil_images.extend(pil_images) - snippet = p[:30] + ("..." if len(p) >= 30 else "") - print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}") except Exception as e: error_msg = str(e).split('\n')[0] - snippet = p[:30] + ("..." if len(p) >= 30 else "") - print(f"[o1key GPT Image] [{idx}/{total}] ❌ {snippet} → {error_msg}") - else: - # 单提示词模式 - if not prompt or not prompt.strip(): - raise ValueError("提示词不能为空") - try: - pil_images = client.run_sync( - prompt=prompt, - model=模型, - quality=quality, - background="auto", - size=size, - n=生图数量, - seed=seed, - image_tensor=图片, - mask_tensor=遮罩, - ) - all_pil_images.extend(pil_images) - except Exception as e: - error_msg = str(e).split('\n')[0] - print(f"[o1key GPT Image] ❌ {error_msg}") - raise RuntimeError(error_msg) from None + print(f"[o1key GPT Image] ❌ {error_msg}") + raise RuntimeError(error_msg) from None - # ── 6. 检查是否有可用图像 ───────────────────────────────────────────── - if not all_pil_images: - raise RuntimeError("所有提示词均生成失败,无可用图像输出") + # ── 6. 检查是否有可用图像 ───────────────────────────────────────── + if not all_pil_images: + raise RuntimeError("所有提示词均生成失败,无可用图像输出") - # ── 7. PIL → tensor ─────────────────────────────────────────────────── - output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images) + # ── 7. PIL → tensor ─────────────────────────────────────────────── + output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images) - # ── 8. 完成日志 ─────────────────────────────────────────────────────── - elapsed = time.time() - start_time - print( - f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s," - f"输出 {output_tensor.shape[0]} 张 " - f"{output_tensor.shape[2]}×{output_tensor.shape[1]}" - ) + # ── 8. 完成日志 ─────────────────────────────────────────────────── + elapsed = time.time() - start_time + print( + f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s," + f"输出 {output_tensor.shape[0]} 张 " + f"{output_tensor.shape[2]}×{output_tensor.shape[1]}" + ) - 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 diff --git a/nodes/nano_banana_pro.py b/nodes/nano_banana_pro.py index 495e29d..a6b3196 100644 --- a/nodes/nano_banana_pro.py +++ b/nodes/nano_banana_pro.py @@ -52,7 +52,7 @@ except ImportError: # ============================================================================ # 是否启用调试日志(打印完整的 API 响应内容) # 设置为 True 以启用调试日志,False 以禁用 -DEBUG_LOG_ENABLED = True +DEBUG_LOG_ENABLED = False # 是否启用请求体日志(打印发送给 API 的请求体,base64 图片数据将自动截断) # 设置为 True 以启用请求体日志,False 以禁用 REQUEST_LOG_ENABLED = False @@ -665,20 +665,37 @@ class NanoBananaPro: else: # 单提示词模式 if 生图数量 == 1: - # 单张:同步生成,输出 tensor - generated_images = self.client.generate_sync( - prompt=prompt, - model=模型, - resolution=分辨率, - aspect_ratio=宽高比, - batch_size=1, - images=input_images, - progress_callback=progress_callback, - debug=DEBUG_LOG_ENABLED, - debug_request=REQUEST_LOG_ENABLED, - enable_grounding=enable_grounding, - enable_image_search=enable_image_search, - ) + # 单张:同步生成,自动重试(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( + prompt=prompt, + model=模型, + resolution=分辨率, + aspect_ratio=宽高比, + batch_size=1, + images=input_images, + progress_callback=progress_callback, + debug=DEBUG_LOG_ENABLED, + debug_request=REQUEST_LOG_ENABLED, + enable_grounding=enable_grounding, + 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: # 多张:异步并发,内存输出