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:
2026-08-28 10:28:35 +08:00
parent 685b7b0862
commit 2a96ec0870
28 changed files with 1187 additions and 729 deletions
+22 -2
View File
@@ -12,7 +12,7 @@ import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from graph.validate import with_fallback
from graph.validate import ThreadSafeErrors, with_fallback
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
@@ -29,6 +29,18 @@ def _notify_400(on_400, exc) -> None:
pass
def _notify_503(on_503, exc) -> None:
"""致命图像服务错误(503 / 账户不可用)时触发 on_503 回调(供调用方提前终止任务)。"""
if on_503 is None:
return
try:
from graph.pinterest_pipeline import PinterestPipeline
if PinterestPipeline.is_fatal_503(exc):
on_503()
except Exception: # noqa: BLE001
pass
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
lines = [
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
@@ -112,6 +124,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
errors: List[Dict[str, Any]] = None,
seed: Optional[int] = None,
on_400=None,
on_503=None,
size: str = "1024x1024") -> Optional[str]:
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
@@ -123,6 +136,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
on_503: 致命图像服务错误(503/账户不可用)时回调(供调用方提前终止任务)。
"""
try:
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
@@ -145,6 +159,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
except Exception as e: # noqa: BLE001
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
_notify_400(on_400, e)
_notify_503(on_503, e)
out_path = ib.generate(
img_prompt, str(design_dir / f"{out_stem}_design.png"),
brief.get("composite_negative", ""), size=size, seed=seed)
@@ -161,6 +176,7 @@ def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
return out_path
except Exception as e: # noqa: BLE001
_notify_400(on_400, e)
_notify_503(on_503, e)
if errors is not None:
errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
@@ -238,12 +254,13 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
def _gen_one(i: int, b: Dict[str, Any]):
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
state.get("errors"), seed=_seed,
_safe_errors, seed=_seed,
size=compose_cfg.get("design_size", "1024x1024"))
if out_path is None:
return i, b, None, None
return i, b, out_path, None
_safe_errors = ThreadSafeErrors()
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
@@ -263,6 +280,9 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
b["design_path"] = out_path
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
print(f"[compose] 印花设计稿已生成: {out_path}")
# 合并并发线程收集的错误(线程安全收集器 → 主线程统一追加)
if len(_safe_errors):
state.setdefault("errors", []).extend(list(_safe_errors))
stats = dict(state.get("stats") or {})
stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}