POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底

- 缓存热点批量流程(有采集缓存不触发 Google)
- 简报不足直接从采集缓存生成(轻量补齐)
- 三图合成(模特/印花/底图)+ 底图压缩 <2MB
- 热点去重→风格去重自动切换 + 不适合类目 review 兜底
- 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述)
- 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
2026-08-22 14:14:01 +08:00
commit f493bde8a9
98 changed files with 10280 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
"""节点级兜底校验工具。
设计目标:LangGraph 流水线里每个节点都必须"失败不影响整体"
提供两类兜底:
1. with_fallback(node_name):装饰器,节点函数抛异常时捕获,把错误写入 state['errors']
并返回最小更新(不破坏其它字段),整图继续往下走。
2. 数据校验函数:validate_rows / validate_brief,对节点产出的数据进行结构校验,
剔除非法记录并记录原因,保证下游拿到的数据"形状正确"
"""
import functools
import traceback
from typing import Any, Dict, List
def with_fallback(node_name: str):
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
节点内部仍建议自己做精细兜底(降级/默认),with_fallback 是最后一道保险:
任何未预料的异常都不会让整张图中断。
"""
def deco(fn):
@functools.wraps(fn)
def wrapper(state: Dict[str, Any]):
try:
return fn(state)
except Exception as e: # noqa: BLE001
tb = traceback.format_exc(limit=3)
err = {
"node": node_name,
"type": type(e).__name__,
"message": str(e)[:300],
"trace": tb[-400:],
}
errors = list(state.get("errors") or [])
errors.append(err)
# 只更新 errors,其它字段保持上一节点结果,下游继续
return {"errors": errors}
return wrapper
return deco
def validate_rows(rows: List[Dict[str, Any]], node: str) -> List[Dict[str, Any]]:
"""校验抓取/过滤后的行结构,剔除缺 topic 或非法记录,返回干净列表。
同时保证每个 row 至少含 country/topic/source/kind/raw_score,缺失时给默认。
"""
clean: List[Dict[str, Any]] = []
dropped = 0
for r in rows or []:
if not isinstance(r, dict):
dropped += 1
continue
topic = (r.get("topic") or "").strip()
if not topic:
dropped += 1
continue
r.setdefault("country", "")
r.setdefault("source", "unknown")
r.setdefault("kind", "unknown")
r.setdefault("raw_score", 0.0)
if r.get("raw_score") is None:
r["raw_score"] = 0.0
clean.append(r)
if dropped:
# 简单记录到返回数据的副作用里(调用方会再汇总到 stats)
pass
return clean
def validate_brief(b: Dict[str, Any]) -> Dict[str, Any]:
"""校验单条设计简报结构,补齐缺失字段,保证下游 compose 不会因缺键崩溃。"""
b = dict(b)
b.setdefault("topic", "")
b.setdefault("country", "")
b.setdefault("design_category", "Niche")
b.setdefault("risk_level", "safe")
b.setdefault("motif", b.get("topic", ""))
b.setdefault("art_style", "clean vector illustration")
b.setdefault("color_palette", "balanced modern palette")
b.setdefault("composition", "centered emblem with balanced negative space")
b.setdefault("concept", b.get("topic", ""))
b.setdefault("negative_prompt", "")
b.setdefault("image_prompt", "")
b.setdefault("wearable_prompt", "")
b.setdefault("composite_prompt", "")
b.setdefault("composite_negative", "")
return b
def safe_get(state: Dict[str, Any], key: str, default=None):
return state.get(key, default)