"""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() 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} t-shirt design" if "t-shirt design" not in s.lower() else s for s in random.sample(fresh, min(want, len(fresh)))] print(f"[pinterest_search] direct 模式:国家种子词库随机抽 {len(terms)} 个 + t-shirt design({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} 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} t-shirt design" if "t-shirt design" 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}