Files
pod_trend_agent/graph/nodes/pinterest_search_node.py
3218485270 71a48e4ed5 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 致命终止、线程安全、原子写入等)
2026-08-28 16:24:50 +08:00

123 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pinterest 参考模式节点 1/3:按需生成单个搜索词(pinterest_search)。
按需搜索:每次只生成 1 个搜索词(LLM json_schema + 动态注入已用词防重复),
带短袖/印花设计引导,保证搜索词适合短袖 T 恤印花。
不在此处持久化已用词 —— 只有爬取成功(真正用掉)才标记已用(见 pinterest_scrape)。
兜底链:LLM json_schema → json_object → 解析失败/调用失败 → 回退种子词池随机抽样。
带 with_fallback:任何异常都不中断,返回空列表由下游跳过。
"""
import random
from typing import Any, Dict, List
from graph.llms import get_backend
from graph.pinterest import (
filter_search_terms,
load_used_terms,
merge_used,
sample_seeds,
)
from graph.validate import with_fallback
@with_fallback("pinterest_search")
def pinterest_search_node(state: Dict[str, Any]) -> Dict[str, Any]:
country = state["country"]
config = state["config"]
output_dir = state["output_dir"]
errors = list(state.get("errors") or [])
pcfg = config.get("pinterest") or {}
if not pcfg.get("enabled", True):
return {"pinterest_search_terms": [], "errors": errors}
provider = str(pcfg.get("provider") or "openai").strip().lower()
search_mode = str(pcfg.get("search_mode") or "direct").strip().lower()
_raw_suffix = str(pcfg.get("search_term_suffix") if pcfg.get("search_term_suffix") is not None else " t-shirt design").strip()
suffix = _raw_suffix if _raw_suffix else "" # 配置留空则后缀为空(不追加),不再回退默认
want = int(pcfg.get("search_terms_per_run", 1)) # 每次搜索词数量
seed_sample = int(pcfg.get("seed_sample", 40))
max_used_in_prompt = int(pcfg.get("max_used_terms_in_prompt", 100))
blacklist = config.get("blacklist") or []
# 1) 种子词池 + 已用搜索词 + 本轮已尝试词(防同轮重复,不持久化)
seeds = sample_seeds(country, seed_sample)
used = load_used_terms(output_dir, country)
attempted = [str(t).strip() for t in (state.get("pinterest_attempted") or []) if str(t).strip()]
rounds = int(state.get("pinterest_rounds") or 0) + 1
if not seeds:
print(f"[pinterest_search] {country} 无种子词,跳过搜索词生成")
return {"pinterest_search_terms": [], "pinterest_rounds": rounds, "errors": errors}
# 2) 生成搜索词:种子词不再由 LLM 给出,直接由内置国家种子词库随机抽取(优先未用过),
# 追加 " t-shirt design"(保证 Pinterest 返回真正的 T 恤印花图);llm 模式保留兼容
terms: List[str] = []
if search_mode == "direct":
from graph.pinterest import load_pinterest_seeds
pool = load_pinterest_seeds(country)
used_set = {str(u).strip().lower() for u in merge_used(used, attempted)}
fresh = [s for s in pool if s.lower() not in used_set]
if not fresh:
fresh = pool # 库内词全部用过 → 允许复用(词库有限)
terms = [f"{s} {suffix}".strip() if suffix and suffix not in s.lower() else s
for s in random.sample(fresh, min(want, len(fresh)))]
print(f"[pinterest_search] direct 模式:国家种子词库随机抽 {len(terms)} 个 + 后缀「{suffix}」({country}")
else:
used_llm = merge_used(used, attempted)
if max_used_in_prompt > 0:
used_llm = used_llm[-max_used_in_prompt:]
llm = None
if provider != "static":
try:
llm = get_backend(provider)
if hasattr(llm, "bind_config"):
llm.bind_config(config.get("llm_screen") or {})
if provider not in ("mock",) and not getattr(llm, "has_key", False):
print(f"[pinterest_search] {provider} 未配置 API key,降级 mock")
llm = get_backend("mock")
except Exception as e: # noqa: BLE001
print(f"[pinterest_search] LLM 初始化失败: {e}")
llm = None
if llm is not None and hasattr(llm, "generate_pinterest_terms"):
try:
ctx = {"country": country, "seeds": seeds, "used_terms": used_llm, "count": want,
"search_term_suffix": suffix}
res = llm.generate_pinterest_terms(ctx)
terms = [str(t).strip() for t in (res.get("search_terms") or []) if str(t).strip()]
print(f"[pinterest_search] LLM 生成搜索词 {len(terms)} 个({country},已用词注入 {len(used_llm)}")
except Exception as e: # noqa: BLE001
print(f"[pinterest_search] LLM 生成失败,回退种子词池: {e}")
terms = []
# 3) 兜底:LLM 无结果 → 种子词池随机抽样
if not terms:
terms = [f"{s} {suffix}".strip() if suffix and suffix not in s.lower() else s
for s in random.sample(seeds, min(want, len(seeds)))]
print(f"[pinterest_search] 兜底:从种子词池取 {len(terms)} 个")
# 4) 全局过滤(已用/本轮已尝试/黑名单/不适合T恤/去重)——注意:不在此处持久化已用词
# direct 模式:抽样时已避开已用词(库内词有限,全部用过后允许复用),不再额外过滤
if search_mode == "direct":
filtered = terms
else:
filtered = filter_search_terms(terms, merge_used(used, attempted), blacklist)
if not filtered and seeds:
# 生成词全被过滤 → 从种子词池补充(同样过滤)
extra = filter_search_terms(seeds, merge_used(used, attempted), blacklist)
filtered = extra[:want]
stats = dict(state.get("stats") or {})
stats["pinterest_search"] = {
"provider": provider,
"round": rounds,
"generated": len(terms),
"filtered": len(filtered),
"used_total": len(used),
}
print(f"[pinterest_search] 第 {rounds} 轮搜索词 {len(filtered)} 个(已用累计 {len(used)}: "
f"{', '.join(filtered[:3])}{'...' if len(filtered) > 3 else ''}")
return {"pinterest_search_terms": filtered, "pinterest_rounds": rounds,
"stats": stats, "errors": errors}