v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化
- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板 - 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板 - 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特 - 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试 - Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃 - 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序 - 删除对接文档.md,更新 README,gitignore 排除测试产物
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
所有请求跳过环境代理(NO_PROXY),适配用户挂 VPN 时直连国内/自建网关。
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -31,6 +32,17 @@ _FINAL_STATUS = ("succeeded", "completed", "done")
|
||||
_FAIL_STATUS = ("failed", "error")
|
||||
|
||||
|
||||
def _retry_after(resp, attempt: int) -> int:
|
||||
"""429 限流等待秒数:优先取网关 Retry-After 头,否则指数退避(3/6/9s)。"""
|
||||
ra = resp.headers.get("Retry-After")
|
||||
try:
|
||||
if ra is not None and ra.isdigit():
|
||||
return min(int(ra), 30)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return 3 * (attempt + 1)
|
||||
|
||||
|
||||
def _shrink_blob_to_2mb(img_path: str, blob: bytes, max_bytes: int = 2 * 1024 * 1024) -> bytes:
|
||||
"""把大图压缩到 <max_bytes(默认 2MB)再上传:
|
||||
- 尺寸过大先缩放(合成输入 1600x2200 内足够);
|
||||
@@ -171,8 +183,12 @@ class OpenAIImageBackend(ImageBackend):
|
||||
"n": 1,
|
||||
"size": size or cfg.get("size", "1024x1024"),
|
||||
"model": model,
|
||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
||||
}
|
||||
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
|
||||
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
|
||||
em = str(cfg.get("execution_mode") or "").strip()
|
||||
if em:
|
||||
data["execution_mode"] = em
|
||||
if seed is not None:
|
||||
data["seed"] = seed
|
||||
# 提交重试:异步路径不稳定 → 失败重试同步提交(最多 3 次);
|
||||
@@ -181,6 +197,20 @@ class OpenAIImageBackend(ImageBackend):
|
||||
for attempt in range(3):
|
||||
resp = requests.post(f"{base_url}/images/edits", headers=headers, files=files,
|
||||
data=data, timeout=300, proxies=NO_PROXY)
|
||||
if resp.status_code == 429:
|
||||
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
|
||||
wait = _retry_after(resp, attempt)
|
||||
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
if resp.status_code >= 500:
|
||||
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
|
||||
body = resp.text or ""
|
||||
last_err = f"图像 API {resp.status_code}: {body[:300]}"
|
||||
wait = 3 * (attempt + 1)
|
||||
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
if resp.status_code >= 400:
|
||||
body = resp.text or ""
|
||||
if "content_policy" in body and attempt < 2:
|
||||
@@ -193,18 +223,24 @@ class OpenAIImageBackend(ImageBackend):
|
||||
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
||||
try:
|
||||
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
||||
except json.JSONDecodeError as e:
|
||||
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
|
||||
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
|
||||
last_err = str(e)
|
||||
wait = 3 * (attempt + 1)
|
||||
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试提交 {attempt + 1}/3: {e}")
|
||||
time.sleep(wait)
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_err = str(e)
|
||||
print(f"[img] 第 {attempt + 1} 次提交异步失败,重试同步提交: {e}")
|
||||
print(f"[img] 第 {attempt + 1} 次提交异步失败,等待后重试同步提交: {e}")
|
||||
time.sleep(2 * (attempt + 1))
|
||||
raise RuntimeError(f"图像合成多次提交均失败: {last_err}")
|
||||
|
||||
def generate(self, prompt: str, out_path: str, negative: str = "", size: str = "",
|
||||
seed: Optional[int] = None) -> str:
|
||||
"""纯文生图:生成白底纯印花设计稿(standalone pure print design)。
|
||||
size: 显式尺寸覆盖(印花设计统一 1024x1024);留空用配置 size。
|
||||
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
|
||||
background: 配置 compose.background="transparent" 时传 background 参数 → 透明背景 PNG
|
||||
(gpt-image-1/2 等模型支持;网关不支持该参数时会被忽略或由网关兜底)。"""
|
||||
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。"""
|
||||
cfg = self._cfg
|
||||
api_key = cfg.get("api_key", "")
|
||||
model = cfg.get("model", "gpt-image-1")
|
||||
@@ -220,17 +256,32 @@ class OpenAIImageBackend(ImageBackend):
|
||||
"size": size or cfg.get("size", "1024x1024"),
|
||||
"model": model,
|
||||
"response_format": "b64_json",
|
||||
"execution_mode": "sync", # 强制同步(ai-media 等网关默认重负载转异步任务,不稳定)
|
||||
}
|
||||
# execution_mode 仅对明确支持的网关(如 ai-media)传;yunfei 等标准 OpenAI 网关
|
||||
# 不认识该参数,硬传会导致空响应,故默认不传,仅 config 显式配置时才带上
|
||||
em = str(cfg.get("execution_mode") or "").strip()
|
||||
if em:
|
||||
data["execution_mode"] = em
|
||||
if seed is not None:
|
||||
data["seed"] = seed
|
||||
bg = str(cfg.get("background") or "").strip()
|
||||
if bg:
|
||||
data["background"] = bg # 如 "transparent"(透明背景 PNG)
|
||||
last_err: Optional[str] = None
|
||||
for attempt in range(3):
|
||||
resp = requests.post(f"{base_url}/images/generations", headers=headers, json=data,
|
||||
timeout=300, proxies=NO_PROXY)
|
||||
if resp.status_code == 429:
|
||||
# 限流:尊重网关负载退避等待再重试,避免硬撞雪崩
|
||||
wait = _retry_after(resp, attempt)
|
||||
print(f"[img] 图像 API 429 限流,等待 {wait}s 重试 {attempt + 1}/3")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
if resp.status_code >= 500:
|
||||
# 5xx 服务端错误(500/502/503/504 超时等):网关临时故障/过载,退避后重试
|
||||
body = resp.text or ""
|
||||
last_err = f"图像 API {resp.status_code}: {body[:300]}"
|
||||
wait = 3 * (attempt + 1)
|
||||
print(f"[img] 图像 API {resp.status_code}(网关临时故障/超时),等待 {wait}s 重试 {attempt + 1}/3")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
if resp.status_code >= 400:
|
||||
body = resp.text or ""
|
||||
if "content_policy" in body and attempt < 2:
|
||||
@@ -243,7 +294,15 @@ class OpenAIImageBackend(ImageBackend):
|
||||
raise RuntimeError(f"图像 API {resp.status_code}: {body[:300]}")
|
||||
try:
|
||||
return _resolve_task_or_sync(resp.json(), base_url, headers, out_path)
|
||||
except json.JSONDecodeError as e:
|
||||
# 空/非 JSON 响应:多为网关过载返回空 body → 退避后重试,
|
||||
# 避免即时重压触发网关 429(用户实测空响应风暴 → 429)
|
||||
last_err = str(e)
|
||||
wait = 3 * (attempt + 1)
|
||||
print(f"[img] 网关空响应(JSON 解析失败),等待 {wait}s 重试生成 {attempt + 1}/3: {e}")
|
||||
time.sleep(wait)
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_err = str(e)
|
||||
print(f"[img] 第 {attempt + 1} 次生成异步失败,重试同步提交: {e}")
|
||||
print(f"[img] 第 {attempt + 1} 次生成异步失败,等待后重试同步提交: {e}")
|
||||
time.sleep(2 * (attempt + 1))
|
||||
raise RuntimeError(f"图像生成多次提交均失败: {last_err}")
|
||||
|
||||
Reference in New Issue
Block a user