v89-v91 模板增强 + 图源映射 + 多模态提示词可配置化

- 图源映射统一:热点采集与 Pinterest 模式均走 config.product.mark_dirs 配置,按任务序号随机抽模特图/平铺图
- 商品产地固定:统一为「中国大陆」+「产地省份=广东省」(不再读站点/字典映射)
- 模板 SKU 字段检测:按建议售价同一套路检测 SKU分类/SKU数量/SKU数量单位,必填时填入单品/1/件
- 多模态分析提示词可配置:prompts/pinterest_analyze_system.md + user.md,支持国家覆盖,不丢文件回退内置
- 自定义图片模式:新增 pinterest_custom_load_node,图片数量硬校验,选品清单 ≤ 有效图片数
- 模板导出优化:写入前按货号末 3 位升序排序,不再产生空白 xlsx
- 修复 v90 project review 10 项(503 致命终止、线程安全、原子写入等)
This commit is contained in:
2026-08-28 16:24:50 +08:00
parent 2a96ec0870
commit 71a48e4ed5
40 changed files with 780 additions and 200 deletions
+89 -14
View File
@@ -117,18 +117,57 @@ def _pinterest_route(state: Dict[str, Any]) -> str:
return "search"
def build_pinterest_graph():
def _pinterest_custom_route(state: Dict[str, Any]) -> str:
"""自定义模式图池路由:只消耗本地图片池,从不搜索/采集。
简报池还有在途 → wait(等待后台消化);简报达标 → done;
简报池空闲 + 图池有未消费图片 → analyze;图池空 → done(自定义模式不采集)。
致命 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_custom_route] ⛔ 图像服务 503 已终止任务,停止分析,直接收尾合成模板")
return "done"
target = int(state.get("pinterest_target") or 1) or 1
briefs = state.get("briefs") or []
if len(briefs) >= target:
print(f"[pinterest_custom_route] 简报已达目标 {len(briefs)}/{target},结束")
return "done"
if pipe is not None and hasattr(pipe, "pending_count") and pipe.pending_count() > 0:
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 "")
used = load_used_images(str(state.get("output_dir") or ""), state.get("country") or "")
unused = pool_unused_images(pool, used)
except Exception: # noqa: BLE001
unused = []
if unused:
print(f"[pinterest_custom_route] 图池还有 {len(unused)} 张未消费图片,继续分析(简报 {len(briefs)}/{target}")
return "analyze"
print(f"[pinterest_custom_route] 图池已空,简报 {len(briefs)}/{target},结束(自定义模式不采集)")
return "done"
def build_pinterest_graph(custom_mode: bool = False):
"""Pinterest 参考模式图(按需搜索循环 + 简报池并发生成):
pinterest_init(建简报池)→ pinterest_search → pinterest_scrape → pinterest_analyze
→ [pinterest_route] 简报池还有在途 → wait(等待后台消化)→ 回到路由;
简报池空闲 + 图池有图 → analyze;图池空 + 简报不足 → search;达标 → pinterest_finalize
(排空简报池、后台并发生成 设计→三合一→OSS→种草图)→ template_export
custom_mode=True:自定义模式,不搜索不采集 ——
pinterest_init → pinterest_custom_load(本地文件夹校验+入库)→ pinterest_analyze
→ [pinterest_custom_route] wait / analyze / done(图池空或简报达标即结束,从不 search)。
其余下游流程(analyze→设计→三合一→OSS→种草图→模板)与普通 Pinterest 模式完全一致。
"""
from graph.nodes import (
pinterest_analyze_node,
pinterest_scrape_node,
pinterest_search_node,
)
from graph.nodes import pinterest_custom_load_node
from graph.nodes.pinterest_finalize_node import pinterest_finalize_node
from graph.nodes.pinterest_init_node import pinterest_init_node
@@ -149,23 +188,34 @@ def build_pinterest_graph():
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)
builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_init", "pinterest_search")
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", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索
"wait": "pinterest_wait", # 简报池还有在途 → 等待后台消化
"done": "pinterest_finalize", # 简报达标/轮次耗尽 → 收尾(排空简报池)
})
if custom_mode:
builder.add_node("pinterest_custom_load", pinterest_custom_load_node)
builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_init", "pinterest_custom_load")
builder.add_edge("pinterest_custom_load", "pinterest_analyze")
builder.add_conditional_edges("pinterest_analyze", _pinterest_custom_route, {
"analyze": "pinterest_analyze",
"wait": "pinterest_wait",
"done": "pinterest_finalize",
})
else:
builder.add_node("pinterest_search", pinterest_search_node)
builder.add_node("pinterest_scrape", pinterest_scrape_node)
builder.add_edge("__start__", "pinterest_init")
builder.add_edge("pinterest_init", "pinterest_search")
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", # 简报池空闲 + 图池空 + 简报不足 → 新一轮搜索
"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)
@@ -231,14 +281,39 @@ def run_pinterest_ref(
project_root: Path,
output_root: Optional[Path] = None,
task_timestamp: Optional[str] = None,
custom_image_dir: Optional[str] = None,
) -> Dict[str, Any]:
"""Pinterest 参考模式入口:独立于 Google Trends 的完整流程。
种子词 → LLM 搜索词(json_schema + 动态注入防重复)→ 爬图 → LLM 分析图片
→ 设计简报 → 设计稿 → 产品图 → 上传 → 种草图 → 模板导出。
参数语义与 run_country 一致(project_root=数据根,output_root=产物根)。
custom_image_dir 非空 或 config.pinterest.mode=="custom" → 进入自定义模式:
不搜索不采集,直接把指定文件夹的有效图片送多模态分析,沿用 Pinterest 后续所有步骤。
数量硬校验(有效图片数 ≥ 选品清单总数)在启动前执行,不满足直接抛错。
"""
compiled = build_pinterest_graph()
target = len((global_config.get("product") or {}).get("spu_tasks") or []) \
or int((global_config.get("product") or {}).get("spu_count") or 0) or 1
pinterest_cfg = global_config.get("pinterest") or {}
mode = str(pinterest_cfg.get("mode") or "scrape").strip().lower()
custom_dir = str(custom_image_dir or "").strip() \
or str(pinterest_cfg.get("custom_image_dir") or "").strip()
custom_mode = bool(custom_dir) or (mode == "custom")
if custom_mode:
if not custom_dir:
raise ValueError(
"自定义模式需要填写 pinterest.custom_image_dir(选择上传的图片文件夹),"
"当前为空,无法启动")
from graph.pinterest import validate_custom_images
ok, valid_n, msg = validate_custom_images(custom_dir, target)
if not ok:
raise ValueError(f"自定义模式数量校验未通过:{msg}(选品清单总数 {target}")
global_config.setdefault("pinterest", {})["custom_image_dir"] = custom_dir
print(f"[run_pinterest_ref] 自定义模式启用:{custom_dir} 有效图片 {valid_n} 张 ≥ 选品清单 {target}")
compiled = build_pinterest_graph(custom_mode=custom_mode)
cc = build_country_config(global_config, country, project_root)
prompts_dir = project_root / "prompts" / country
cache_dir = (output_root or project_root) / "output" / country