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
+109
View File
@@ -0,0 +1,109 @@
"""节点 7/7:压缩 + 上传阿里云 OSSoss_upload)。
在 product 之后运行:把 product 生成的成品图(composite / printed / design / basemap
压缩为 3:4 / ≥1340×1785 / <2MB 的 JPEG,上传到 config.oss 图床。
上传 key(图床路径):{国家}/{任务时间戳}/{货号}_{4位随机}.jpg
- 任务时间戳:任务开始记录(YYYYMMDDHHMMSS),state["task_timestamp"],缺失时取当前时间
- 货号:用户自定义前缀(config.product.code_prefix,默认 DG+ 3 位计数(000 起,最多 999)
- 4 位随机:大小写英文 + 数字
压缩/上传均带兜底:单图失败不影响其它;未配置 oss 或 enabled=false 时静默跳过。
"""
import random
import string
import time
from pathlib import Path
from typing import Any, Dict, List
from graph.validate import with_fallback
# design 设计稿是过程稿(不进模板),不压缩不上传;只传最终商品图
KIND_ORDER = ["composite", "printed", "basemap"]
MAX_CODE = 999 # 货号计数上限(000~998 共 999 张)
def _gen_rand4() -> str:
return "".join(random.choices(string.ascii_letters + string.digits, k=4))
@with_fallback("oss_upload")
def oss_upload_node(state: Dict[str, Any]) -> Dict[str, Any]:
products: List[Dict[str, Any]] = state.get("product") or []
config = state["config"] or {}
oss_cfg = config.get("oss") or {}
country = state.get("country", "")
if not (oss_cfg.get("oss_bucket") and oss_cfg.get("oss_key_id")):
print("[oss] 未配置 ossconfig.oss),跳过压缩上传节点。")
return {"oss": [], "stats": state.get("stats") or {}}
if not bool(oss_cfg.get("enabled", True)):
print("[oss] config.oss.enabled=false,跳过上传。")
return {"oss": [], "stats": state.get("stats") or {}}
from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss
# 任务时间戳:任务开始记录;缺失则当前时间
ts = str(state.get("task_timestamp") or time.strftime("%Y%m%d%H%M%S"))
# 货号前缀:config.product.code_prefix(默认 DG
prefix = str(((config.get("product") or {}).get("code_prefix")) or "DG").strip()
# 序号从 state 续接(一次任务内跨多次节点调用不重号)
seq = int(state.get("oss_seq") or 0)
uploaded: List[Dict[str, Any]] = []
stats = dict(state.get("stats") or {})
for r in products:
spu = r.get("spu_code", "")
sku = r.get("sku_code", "")
for kind in KIND_ORDER:
src = r.get(f"{kind}_path")
if not src or not Path(src).exists():
continue
if seq >= MAX_CODE:
print(f"[oss] 货号计数已达上限 999,停止上传后续图片({src}")
break
try:
code = f"{prefix}{seq:03d}" # 货号:前缀 + 3 位计数(000 起)
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
key = build_oss_key(country, ts, code, _gen_rand4())
url = upload_to_oss(oss_cfg, compressed, key)
if url:
r[f"{kind}_url"] = url
r["oss_code"] = code
uploaded.append({"spu_code": spu, "sku_code": sku, "kind": kind,
"code": code, "url": url})
seq += 1
except Exception as e: # noqa: BLE001
print(f"[oss] 处理失败 {src}: {e}")
# 多色:color_composites 用于模板按颜色路由——首色用主图 url/code,额外色单独上传(独立货号)
color_ups: List[Dict[str, Any]] = []
comps = r.get("color_composites") or []
if comps and r.get("composite_url"):
color_ups.append({**comps[0], "url": r["composite_url"], "code": r.get("oss_code", "")})
for cc in comps[1:]:
src = cc.get("composite_path")
if not src or not Path(src).exists():
continue
if seq >= MAX_CODE:
print("[oss] 货号计数已达上限 999,停止上传颜色图")
break
try:
code = f"{prefix}{seq:03d}"
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
key = build_oss_key(country, ts, code, _gen_rand4())
url = upload_to_oss(oss_cfg, compressed, key)
if url:
cc["url"] = url
cc["code"] = code
color_ups.append(cc)
uploaded.append({"spu_code": spu, "sku_code": cc.get("sku_code"),
"kind": "composite_color", "code": code, "url": url})
seq += 1
except Exception as e: # noqa: BLE001
print(f"[oss] 颜色图上传失败 {src}: {e}")
if color_ups:
r["color_composites"] = color_ups
stats["oss"] = {"uploaded": len(uploaded), "timestamp": ts, "prefix": prefix, "seq": seq}
return {"oss": uploaded, "product": products, "oss_seq": seq, "stats": stats}