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
+55 -14
View File
@@ -26,6 +26,7 @@ from graph.nodes import (
template_export_node,
)
from graph.state import AgentState
from graph.validate import with_fallback
def build_graph():
@@ -59,8 +60,16 @@ def build_graph():
def _pinterest_route(state: Dict[str, Any]) -> str:
"""图池路由:简报达标 → done;图池还有未消费图片 → analyze(继续分析,不搜索)
图池不足 → search(新一轮搜索);轮次耗尽 → done。"""
"""图池路由:简报池实时需求补——简报池还有待处理/在途简报 → 不分析不采集
简报池空闲 + 图池有未消费图片 → 补分析;简报池空闲 + 图池空 + 简报不足 → 搜索采集;
简报达标 → done。不按任务数量提前结束,先消耗图池存量,不够用了才主动采集。
致命 503(图像服务不可用)→ 立即终止,不再搜索/分析,直接收尾合成模板。"""
# 致命 503:图像服务不可用,重试无效 → 提前终止,未完成产品废弃,直接收尾
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "is_fatal_503_aborted") and pipe.is_fatal_503_aborted():
print("[pinterest_route] ⛔ 图像服务 503 已终止任务,停止搜索/分析,直接收尾合成模板")
return "done"
target = int(state.get("pinterest_target") or 0)
if target <= 0:
target = 1
@@ -72,11 +81,14 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
if max_rounds <= 0:
max_rounds = max(target * 2, 5)
if len(briefs) >= target:
print(f"[pinterest_route] 简报已达目标 {len(briefs)}/{target},结束")
return "done"
# 简报池还有待处理/在途简报 → 先让后台消化,不分析新图也不采集
if pipe is not None and hasattr(pipe, "pending_count"):
pending = pipe.pending_count()
if pending > 0:
# 日志由 pinterest_wait 节点进入时统一打印(阻塞等待消化,避免此处高频刷屏)
return "wait"
# 图池还有未消费图片 → 继续分析(不搜索
# 简报池空闲 → 检查图池还有未消费图片 → 分析(不管简报离目标差多少,先把这批用完
try:
from graph.pinterest import load_image_pool, load_used_images, pool_unused_images
pool = load_image_pool(str(state.get("output_dir") or ""), state.get("country") or "")
@@ -85,24 +97,31 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
except Exception: # noqa: BLE001
unused = []
if unused:
print(f"[pinterest_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 简报池空闲,图池还有 {len(unused)} 张未消费图片,补分析"
f"(简报 {len(briefs)}/{target}")
return "analyze"
# 图池不足 → 搜索
# 简报池空闲 + 图池空 → 检查简报是否已达标
if len(briefs) >= target:
print(f"[pinterest_route] 图池已空,简报已达目标 {len(briefs)}/{target},结束")
return "done"
# 简报池空闲 + 图池空 + 简报不达标 → 新一轮搜索采集
if rounds >= max_rounds:
print(f"[pinterest_route] 已达最大轮次 {max_rounds},简报 {len(briefs)}/{target},按现有结果继续")
print(f"[pinterest_route] 已达最大轮次 {max_rounds}图池已空,简报 {len(briefs)}/{target},按现有结果继续")
return "done"
if not terms and rounds > 0:
print(f"[pinterest_route] 无可用搜索词,停止搜索(简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 无可用搜索词,图池已空,停止搜索(简报 {len(briefs)}/{target}")
return "done"
print(f"[pinterest_route] 图池不足,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
print(f"[pinterest_route] 简报池空闲,图池不足,简报未达标,新一轮搜索(第 {rounds} 轮,简报 {len(briefs)}/{target}")
return "search"
def build_pinterest_graph():
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route] 简报不足 → 回到 pinterest_search;达标 → pinterest_finalize
→ [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由;
简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
"""
from graph.nodes import (
@@ -113,11 +132,27 @@ def build_pinterest_graph():
from graph.nodes.pinterest_finalize_node import pinterest_finalize_node
from graph.nodes.pinterest_init_node import pinterest_init_node
@with_fallback("pinterest_wait")
def _pinterest_wait(state: Dict[str, Any]) -> Dict[str, Any]:
"""简报池还有在途简报时阻塞等待后台消化(最多 60s),避免轮询刷屏。
用 pipeline.wait_idle 的 Condition 阻塞等待(_process_one 完成即唤醒),
消化完才返回,路由重新判断;超时兜底返回,避免死等。
"""
pipe = state.get("pinterest_pipeline")
if pipe is not None and hasattr(pipe, "wait_idle"):
pipe.wait_idle(60)
else:
import time as _t
_t.sleep(5)
return {}
builder = StateGraph(AgentState)
builder.add_node("pinterest_init", pinterest_init_node)
builder.add_node("pinterest_search", pinterest_search_node)
builder.add_node("pinterest_scrape", pinterest_scrape_node)
builder.add_node("pinterest_analyze", pinterest_analyze_node)
builder.add_node("pinterest_wait", _pinterest_wait)
builder.add_node("pinterest_finalize", pinterest_finalize_node)
builder.add_node("template_export", template_export_node)
@@ -126,10 +161,12 @@ def build_pinterest_graph():
builder.add_edge("pinterest_search", "pinterest_scrape")
builder.add_edge("pinterest_scrape", "pinterest_analyze")
builder.add_conditional_edges("pinterest_analyze", _pinterest_route, {
"analyze": "pinterest_analyze", # 图池有未消费图片 → 继续分析(不搜索)
"search": "pinterest_search", # 图池不足 → 新一轮搜索
"analyze": "pinterest_analyze", # 简报池空闲 + 图池有未消费图片 → 分析(不搜索)
"search": "pinterest_search", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索
"wait": "pinterest_wait", # 简报池还有在途 → 等待后台消化
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
})
builder.add_edge("pinterest_wait", "pinterest_analyze")
builder.add_edge("pinterest_finalize", "template_export")
builder.add_edge("template_export", END)
return builder.compile()
@@ -232,5 +269,9 @@ def run_pinterest_ref(
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1,
"pinterest_rounds": 0,
"pinterest_attempted": [],
"pinterest_images": {},
"pinterest_briefs": [],
"pinterest_login": {},
"pinterest_pipeline": None,
}
return compiled.invoke(state)