"""构建并编译 LangGraph,提供 run_country() 入口。 图结构(线性流水线,节点全部带兜底): START -> seed -> fetch -> filter -> score -> screen -> prompt_build -> compose(生成纯印花设计稿 + 导出简报)-> product(底图/模特/三图合成/模板) -> oss_upload(压缩 3:4 / ≥1340×1785 / <2MB + 上传阿里云 OSS)-> END """ import time from pathlib import Path from typing import Any, Dict, Optional from langgraph.graph import END, StateGraph from graph.loader import build_country_config from graph.nodes import ( compose_node, fetch_node, filter_node, oss_upload_node, product_node, prompt_node, score_node, screen_node, seed_node, seed_shot_node, template_export_node, ) from graph.state import AgentState def build_graph(): """构建 StateGraph 并编译。""" builder = StateGraph(AgentState) builder.add_node("seed", seed_node) builder.add_node("fetch", fetch_node) builder.add_node("filter", filter_node) builder.add_node("score", score_node) builder.add_node("screen", screen_node) builder.add_node("prompt_build", prompt_node) builder.add_node("product", product_node) builder.add_node("compose", compose_node) builder.add_node("oss_upload", oss_upload_node) builder.add_node("seed_shot", seed_shot_node) builder.add_node("template_export", template_export_node) builder.add_edge("__start__", "seed") builder.add_edge("seed", "fetch") builder.add_edge("fetch", "filter") builder.add_edge("filter", "score") builder.add_edge("score", "screen") builder.add_edge("screen", "prompt_build") builder.add_edge("prompt_build", "compose") # compose:生成纯印花设计稿(放前面) builder.add_edge("compose", "product") # product:底图/模特/三图合成/模板 builder.add_edge("product", "oss_upload") # oss_upload:压缩 + 上传图床 builder.add_edge("oss_upload", "seed_shot") # seed_shot:种草图生成(模板+模特特征 yaml)→ 上传 builder.add_edge("seed_shot", "template_export") # template_export:最终结果导入商品上传模板 builder.add_edge("template_export", END) return builder.compile() def run_country( country: str, global_config: Dict[str, Any], project_root: Path, output_root: Optional[Path] = None, base_image: Optional[str] = None, task_timestamp: Optional[str] = None, ) -> Dict[str, Any]: """运行单个国家的完整流水线,返回最终 state(含 errors / stats / briefs)。 project_root:数据文件根(configs / prompts,打包后为 _MEIPASS 只读目录)。 output_root :产物输出根(默认=project_root;打包后传 exe 旁运行目录, 避免把 output/ 写进临时解压目录导致重启丢失)。 task_timestamp:任务时间戳(每次点击运行 = 一个任务);None 时自动生成。 """ compiled = build_graph() cc = build_country_config(global_config, country, project_root) prompts_dir = project_root / "prompts" / country cache_dir = (output_root or project_root) / "output" / country # 缓存/去重(根目录) ts = task_timestamp or time.strftime("%Y%m%d_%H%M%S") _base = ts _i = 1 while (cache_dir / ts).exists(): # 时间戳文件夹唯一(同秒多任务防冲突/覆盖) ts = f"{_base}_{_i}" _i += 1 output_dir = cache_dir / ts # 本次任务产物(时间戳文件夹) state: Dict[str, Any] = { "country": country, "config": global_config, "country_config": cc, "prompts_dir": str(prompts_dir), "cache_dir": str(cache_dir), "output_dir": str(output_dir), "raw_rows": [], "filtered_rows": [], "scored_rows": [], "screened": [], "briefs": [], "composite": [], "designs": [], "errors": [], "stats": {}, "task_timestamp": ts, # 任务开始时间戳(OSS 路径段 / 产物文件夹名) "oss_seq": 0, # 货号计数(000 起,最多 999) } if base_image: state["base_image"] = base_image result = compiled.invoke(state) return result