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 致命终止、线程安全、原子写入等)
This commit is contained in:
2026-08-28 16:24:50 +08:00
parent 2a96ec0870
commit 71a48e4ed5
40 changed files with 780 additions and 200 deletions
+6 -3
View File
@@ -152,19 +152,22 @@ class MockBackend:
seeds = [str(s).strip() for s in (context.get("seeds") or []) if str(s).strip()]
used = {str(u).strip().lower() for u in (context.get("used_terms") or [])}
count = int(context.get("count", 10))
suffix = str(context.get("search_term_suffix") or "").strip()
pool = [s for s in seeds if s.lower() not in used]
random.shuffle(pool)
terms = pool[:count]
# 不足时用「种子词 + 风格词」组合补足(视觉导向,避免与已用重复)
style_tail = ["t-shirt design", "graphic tee", "print art", "vintage tee", "flat design"]
style_tail = ["graphic tee", "print art", "vintage tee", "flat design"]
i = 0
while len(terms) < count and pool:
combo = f"{pool[i % len(pool)]} {style_tail[(i // len(pool)) % len(style_tail)]}"
if combo.lower() not in used and combo not in terms:
terms.append(combo)
i += 1
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms]
# 自动追加自定义后缀(config.pinterest.search_term_suffix,默认 t-shirt design):
# 让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
if suffix:
terms = [f"{t} {suffix}" if suffix not in t.lower() else t for t in terms]
return {"search_terms": terms}
def analyze_pinterest_images(self, image_paths, term="", country="", on_400=None):
+44 -10
View File
@@ -17,7 +17,7 @@ import requests
# 模型调用一律直连:用户常开 VPN(系统代理),LLM 网关多为国内/自建,走代理会被拦截或变慢。
# 仅请求级 proxies=NO_PROXY 直连,不设置进程级 NO_PROXY 环境变量(避免影响 Google Trends 等外部采集)。
from .base import LLMBackend
from graph.paths import runtime_root
from graph.paths import project_root, runtime_root
# 直连策略:忽略环境代理(用户挂 VPN 时代理会拦截国内/自建网关的请求)
NO_PROXY = {"http": None, "https": None}
@@ -260,7 +260,7 @@ that are DIRECTLY usable as reference for a t-shirt print design.
RULES:
- Generate EXACTLY the requested number of search terms (usually 1 per call).
- Every term MUST be a "t-shirt design" style query: think of it as if the user typed "<concept> t-shirt design" on
- Every term MUST be a "__SUFFIX__" style query: think of it as if the user typed "<concept>__SUFFIX__" on
Pinterest, so the scraped images are actual t-shirt graphics / flat print artworks, NOT lifestyle photos, scenery,
architecture, food plates, or anything that cannot become a clean chest print.
- Terms MUST be suitable for a SHORT-SLEEVE T-SHIRT PRINT: a flat, graphic, print-ready concept (illustration, mascot,
@@ -309,7 +309,7 @@ def build_pinterest_term_user_prompt(context: Dict[str, Any]) -> str:
f"Generate {count} new, diverse, non-overlapping Pinterest search term(s) "
f"that are suitable for a SHORT-SLEEVE T-SHIRT PRINT design "
f"(flat, graphic, print-ready motif that works as a chest print). "
f"Each term should read like \"<concept> t-shirt design\" so Pinterest returns "
f"Each term should read like \"<concept>__SUFFIX__\" so Pinterest returns "
f"actual t-shirt graphics / flat print artwork as reference.",
]
return "\n".join(lines)
@@ -394,7 +394,35 @@ PINTEREST_ANALYZE_SCHEMA = {
}
def build_pinterest_analyze_user_prompt() -> str:
def _pinterest_analyze_prompt_file(country: str, filename: str) -> str:
"""定位多模态分析提示词文件:优先运行根 prompts(exe 旁,可编辑),回退数据根 prompts。
支持国家覆盖(prompts/<country>/<filename>)优先于全局(prompts/<filename>)。
找不到则返回空串,由调用方回退内置默认。
"""
for base in (runtime_root(), project_root()):
if country:
p = base / "prompts" / country / filename
if p.exists():
return p.read_text(encoding="utf-8").strip()
p = base / "prompts" / filename
if p.exists():
return p.read_text(encoding="utf-8").strip()
return ""
def resolve_pinterest_analyze_system_prompt(country: str = "") -> str:
"""多模态分析系统提示词(可配置):命中 prompts/pinterest_analyze_system.md(国家覆盖优先),
否则回退内置 PINTEREST_ANALYZE_SYSTEM_PROMPT。"""
text = _pinterest_analyze_prompt_file(country, "pinterest_analyze_system.md")
return text if text else PINTEREST_ANALYZE_SYSTEM_PROMPT
def build_pinterest_analyze_user_prompt(country: str = "") -> str:
"""多模态分析用户提示词(可配置):命中 prompts/pinterest_analyze_user.md 否则内置默认。"""
text = _pinterest_analyze_prompt_file(country, "pinterest_analyze_user.md")
if text:
return text
return (
"Analyze the attached image and produce one ORIGINAL T-shirt print design brief "
"that captures its visual vibe without copying it."
@@ -583,15 +611,21 @@ class OpenAICompatBackend(LLMBackend):
max_used = int((cfg or {}).get("max_used_terms_in_prompt", 100) or 100)
if max_used > 0:
ctx["used_terms"] = used[-max_used:]
# 后缀占位符:读 ctx 配置(允许为空=不追加后缀);仅在确有后缀时替换占位
suffix = str(ctx.get("search_term_suffix") or "").strip()
sys_prompt = PINTEREST_TERM_SYSTEM_PROMPT.replace("__SUFFIX__", f" {suffix}" if suffix else "")
user_prompt = build_pinterest_term_user_prompt(ctx).replace("__SUFFIX__", f" {suffix}" if suffix else "")
messages = [
{"role": "system", "content": PINTEREST_TERM_SYSTEM_PROMPT},
{"role": "user", "content": build_pinterest_term_user_prompt(ctx)},
{"role": "system", "content": sys_prompt},
{"role": "user", "content": user_prompt},
]
raw = _retry(lambda: call_openai_compatible_structured(cfg, messages, PINTEREST_TERM_SCHEMA, timeout=120))
parsed = _extract_json(raw)
terms = [str(x).strip() for x in (parsed.get("search_terms", []) or []) if str(x).strip()]
# 自动追加 " t-shirt design":让 Pinterest 返回真正的 T 恤印花图(更适合作印花设计参考)
terms = [f"{t} t-shirt design" if "t-shirt design" not in t.lower() else t for t in terms]
# 自动追加自定义后缀(config.pinterest.search_term_suffix,默认 t-shirt design):
# 让 Pinterest 返回真正的印花图(更适合作印花设计参考)
if suffix:
terms = [f"{t} {suffix}" if suffix not in t.lower() else t for t in terms]
return {"search_terms": terms}
def analyze_pinterest_images(self, image_paths: List[str], term: str, country: str = "",
@@ -650,13 +684,13 @@ class OpenAICompatBackend(LLMBackend):
def _call() -> str:
user_content: List[Any] = [
{"type": "text", "text": build_pinterest_analyze_user_prompt()},
{"type": "text", "text": build_pinterest_analyze_user_prompt(country)},
]
user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
payload = {
"model": model,
"messages": [
{"role": "system", "content": PINTEREST_ANALYZE_SYSTEM_PROMPT},
{"role": "system", "content": resolve_pinterest_analyze_system_prompt(country)},
{"role": "user", "content": user_content},
],
"temperature": 0.5,