|
@@ -0,0 +1,485 @@
|
|
|
|
|
+"""早中晚好额外需求:筛选保留项 + 固定早安词,写入策略「当下供需gap-早安」。"""
|
|
|
|
|
+
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+import re
|
|
|
|
|
+from datetime import datetime
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+from typing import Any, Optional
|
|
|
|
|
+from zoneinfo import ZoneInfo
|
|
|
|
|
+
|
|
|
|
|
+from examples.demand.db_manager import exist_cluster_tree
|
|
|
|
|
+
|
|
|
|
|
+CLUSTER_NAME = "早中晚好"
|
|
|
|
|
+PLATFORM_TYPE = "piaoquan"
|
|
|
|
|
+EXTRA_COUNT = 10
|
|
|
|
|
+TOP_N = 10
|
|
|
|
|
+QWEN_FILTER_MODEL = "qwen3.6-plus"
|
|
|
|
|
+OPENROUTER_QWEN_FILTER_MODEL = "qwen/qwen3.6-plus"
|
|
|
|
|
+STRATEGY_ZAOAN = "当下供需gap-早安"
|
|
|
|
|
+FIXED_ZAOAN_DEMANDS = ["早安", "晨安", "早上好", "清晨", "晨起"]
|
|
|
|
|
+BEIJING_TZ = ZoneInfo("Asia/Shanghai")
|
|
|
|
|
+
|
|
|
|
|
+FILTER_PROMPT = """你是一个专业的视频内容解构词筛选器。你的任务是对我提供的所有“解构词”进行逐一审核,剔除那些与 “早安祝福” 主题无关或关联性过弱的词,保留能够有效体现、联想或支撑该主题的词。
|
|
|
|
|
+筛选标准(必须同时满足以下两点,方可保留): 主题相关性:该解构词必须能让人直接或明显联想到“早安祝福”的场景、情感、对象或常见元素(如:晨光、问候、美好祝愿、积极心态、崭新一天、健康平安、亲友关怀等)。
|
|
|
|
|
+意义有效性:即使该词在品类上看似合理,但如果放入早安祝福语境下无法产生具体、积极、正向的意义,或显得生硬、无关、空洞,则应判定为“不匹配”,予以剔除。
|
|
|
|
|
+判定示例(仅供理解,不限于此): 保留:阳光、微笑、晨风、好运、平安、咖啡香、鸟鸣、崭新、希望、温暖、感恩、加油、活力、朝霞、问候语……
|
|
|
|
|
+剔除:暴雨、深夜、疲惫、焦虑、折扣、促销、施工、故障、诉讼、账单、统计表……(即使某些词属于常见品类,但无法正向关联早安祝福)
|
|
|
|
|
+输出格式要求: 只输出 保留名单,按行列出每个解构词(可保留原始“品类 解构词”格式)。
|
|
|
|
|
+不输出剔除名单,不输出解释说明,不添加额外评语。
|
|
|
|
|
+待筛选的解构词列表
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+_BULLET_RE = re.compile(r"^[\s\-*\d.、))]+")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def schedule_has_zaozhongwanhao(schedule_items: list[dict] | None) -> bool:
|
|
|
|
|
+ for item in schedule_items or []:
|
|
|
|
|
+ if str(item.get("cluster_name") or "").strip() != CLUSTER_NAME:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if str(item.get("platform_type") or "").strip() != PLATFORM_TYPE:
|
|
|
|
|
+ continue
|
|
|
|
|
+ return True
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def extra_output_dir(now: Optional[datetime] = None) -> Path:
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ dt = current.astimezone(BEIJING_TZ).strftime("%Y%m%d")
|
|
|
|
|
+ return Path(__file__).parent / "result" / f"{CLUSTER_NAME}_extra" / dt
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def extra_already_done_today(now: Optional[datetime] = None) -> bool:
|
|
|
|
|
+ return (extra_output_dir(now) / "zaoan_written.txt").exists()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _join_element_names(element_names: object) -> str:
|
|
|
|
|
+ if element_names is None:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ if isinstance(element_names, list):
|
|
|
|
|
+ return " ".join(str(x).strip() for x in element_names if x is not None and str(x).strip())
|
|
|
|
|
+ return str(element_names).strip()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _decomp_term(item: dict) -> str:
|
|
|
|
|
+ joined = _join_element_names(item.get("element_names"))
|
|
|
|
|
+ if not joined:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ return f"{CLUSTER_NAME} {joined}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _find_demand_items_path(execution_id: int) -> Optional[Path]:
|
|
|
|
|
+ name = f"execution_id_{execution_id}_demand_items.json"
|
|
|
|
|
+ candidates = [
|
|
|
|
|
+ Path.cwd() / "result" / str(execution_id) / name,
|
|
|
|
|
+ Path(__file__).parent / "result" / str(execution_id) / name,
|
|
|
|
|
+ ]
|
|
|
|
|
+ for path in candidates:
|
|
|
|
|
+ if path.exists():
|
|
|
|
|
+ return path
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _load_demand_items(execution_id: int) -> list[dict]:
|
|
|
|
|
+ path = _find_demand_items_path(execution_id)
|
|
|
|
|
+ if not path:
|
|
|
|
|
+ return []
|
|
|
|
|
+ with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
+ loaded = json.load(f)
|
|
|
|
|
+ items = loaded["items"] if isinstance(loaded, dict) and isinstance(loaded.get("items"), list) else loaded
|
|
|
|
|
+ if not isinstance(items, list):
|
|
|
|
|
+ return []
|
|
|
|
|
+ return [item for item in items if isinstance(item, dict)]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _item_weight(item: dict, score_map: dict) -> float:
|
|
|
|
|
+ from examples.demand.run import _avg_score_for_joined_name, _join_element_names_to_name
|
|
|
|
|
+
|
|
|
|
|
+ name = _join_element_names_to_name(item.get("element_names")) or _join_element_names(item.get("element_names"))
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+ comma_score = _avg_score_for_joined_name(name, score_map)
|
|
|
|
|
+ space_score = _avg_score_for_joined_name(name.replace(" ", ","), score_map)
|
|
|
|
|
+ return max(comma_score, space_score)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def select_top_n_by_weight(items: list[dict], execution_id: Optional[int] = None, n: int = TOP_N) -> list[dict]:
|
|
|
|
|
+ if not items:
|
|
|
|
|
+ return []
|
|
|
|
|
+ from examples.demand.run import _load_name_score_map
|
|
|
|
|
+
|
|
|
|
|
+ score_map = _load_name_score_map(execution_id) if execution_id else {}
|
|
|
|
|
+ ranked = sorted(items, key=lambda item: _item_weight(item, score_map), reverse=True)
|
|
|
|
|
+ return ranked[: max(int(n), 0)]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _normalize_kept_line(line: str) -> str:
|
|
|
|
|
+ text = (line or "").strip()
|
|
|
|
|
+ if not text:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ text = text.strip("`\"'“”‘’")
|
|
|
|
|
+ text = _BULLET_RE.sub("", text).strip()
|
|
|
|
|
+ return text
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _parse_kept_lines(content: str) -> list[str]:
|
|
|
|
|
+ kept: list[str] = []
|
|
|
|
|
+ seen: set[str] = set()
|
|
|
|
|
+ for raw in (content or "").splitlines():
|
|
|
|
|
+ line = _normalize_kept_line(raw)
|
|
|
|
|
+ if not line:
|
|
|
|
|
+ continue
|
|
|
|
|
+ if line in seen:
|
|
|
|
|
+ continue
|
|
|
|
|
+ seen.add(line)
|
|
|
|
|
+ kept.append(line)
|
|
|
|
|
+ return kept
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _item_matches_kept(item: dict, kept_lines: list[str]) -> bool:
|
|
|
|
|
+ term = _decomp_term(item)
|
|
|
|
|
+ joined = _join_element_names(item.get("element_names"))
|
|
|
|
|
+ for line in kept_lines:
|
|
|
|
|
+ if line == term or (joined and line == joined):
|
|
|
|
|
+ return True
|
|
|
|
|
+ if joined and (line.endswith(f" {joined}") or line.endswith(f"\t{joined}")):
|
|
|
|
|
+ return True
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _filter_with_qwen(terms: list[str]) -> tuple[str, list[str]]:
|
|
|
|
|
+ import os
|
|
|
|
|
+
|
|
|
|
|
+ messages = [
|
|
|
|
|
+ {
|
|
|
|
|
+ "role": "user",
|
|
|
|
|
+ "content": FILTER_PROMPT + "\n".join(terms),
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ content = ""
|
|
|
|
|
+ if os.getenv("QWEN_API_KEY"):
|
|
|
|
|
+ from agent.llm.qwen import create_qwen_llm_call
|
|
|
|
|
+
|
|
|
|
|
+ llm_call = create_qwen_llm_call(model=QWEN_FILTER_MODEL)
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = await llm_call(
|
|
|
|
|
+ messages=messages,
|
|
|
|
|
+ model=QWEN_FILTER_MODEL,
|
|
|
|
|
+ temperature=0.1,
|
|
|
|
|
+ extra_body={"enable_thinking": False},
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ result = await llm_call(
|
|
|
|
|
+ messages=messages,
|
|
|
|
|
+ model=QWEN_FILTER_MODEL,
|
|
|
|
|
+ temperature=0.1,
|
|
|
|
|
+ )
|
|
|
|
|
+ content = str((result or {}).get("content") or "").strip()
|
|
|
|
|
+ else:
|
|
|
|
|
+ from agent.llm.openrouter import create_openrouter_llm_call
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[scheduler-extra] 未配置 QWEN_API_KEY,改用 OpenRouter {OPENROUTER_QWEN_FILTER_MODEL}")
|
|
|
|
|
+ llm_call = create_openrouter_llm_call(model=OPENROUTER_QWEN_FILTER_MODEL)
|
|
|
|
|
+ result = await llm_call(
|
|
|
|
|
+ messages=messages,
|
|
|
|
|
+ model=OPENROUTER_QWEN_FILTER_MODEL,
|
|
|
|
|
+ temperature=0.1,
|
|
|
|
|
+ )
|
|
|
|
|
+ content = str((result or {}).get("content") or "").strip()
|
|
|
|
|
+ return content, _parse_kept_lines(content)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _write_json(path: Path, payload: Any) -> None:
|
|
|
|
|
+ path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ with open(path, "w", encoding="utf-8") as f:
|
|
|
|
|
+ json.dump(payload, f, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _merge_kept_and_fixed(kept_items: list[dict]) -> list[dict]:
|
|
|
|
|
+ merged: list[dict] = []
|
|
|
|
|
+ seen: set[str] = set()
|
|
|
|
|
+ for item in kept_items or []:
|
|
|
|
|
+ name = _join_element_names(item.get("element_names"))
|
|
|
|
|
+ if not name or name in seen:
|
|
|
|
|
+ continue
|
|
|
|
|
+ seen.add(name)
|
|
|
|
|
+ merged.append(item)
|
|
|
|
|
+ for name in FIXED_ZAOAN_DEMANDS:
|
|
|
|
|
+ if name in seen:
|
|
|
|
|
+ continue
|
|
|
|
|
+ seen.add(name)
|
|
|
|
|
+ merged.append(
|
|
|
|
|
+ {
|
|
|
|
|
+ "element_names": [name],
|
|
|
|
|
+ "reason": "固定早安祝福需求",
|
|
|
|
|
+ "desc": f"用户需要「{name}」相关的早安祝福内容",
|
|
|
|
|
+ "type": "元素",
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ return merged
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_zaoan_rows(items: list[dict], execution_id: Optional[int] = None) -> list[dict]:
|
|
|
|
|
+ from examples.demand.run import (
|
|
|
|
|
+ _avg_score_for_joined_name,
|
|
|
|
|
+ _join_element_names_to_name,
|
|
|
|
|
+ _load_name_score_map,
|
|
|
|
|
+ _resolve_video_ids_by_name_and_execution_id,
|
|
|
|
|
+ _safe_truncate,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ dt_value = datetime.now(BEIJING_TZ).strftime("%Y%m%d")
|
|
|
|
|
+ score_map = _load_name_score_map(execution_id) if execution_id else {}
|
|
|
|
|
+ rows: list[dict] = []
|
|
|
|
|
+ for item in items:
|
|
|
|
|
+ name = _join_element_names_to_name(item.get("element_names"))
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ name = _join_element_names(item.get("element_names"))
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ continue
|
|
|
|
|
+ name = name.replace(",", " ")
|
|
|
|
|
+ type_str = str(item.get("type") or "元素").strip() or "元素"
|
|
|
|
|
+ reason = item.get("reason") or "固定早安祝福需求"
|
|
|
|
|
+ desc_value = item.get("desc") or f"用户需要「{name}」相关的早安祝福内容"
|
|
|
|
|
+ score = _avg_score_for_joined_name(name.replace(" ", ","), score_map) if score_map else 0.0
|
|
|
|
|
+ video_ids: list[str] = []
|
|
|
|
|
+ if execution_id:
|
|
|
|
|
+ try:
|
|
|
|
|
+ video_ids = _resolve_video_ids_by_name_and_execution_id(
|
|
|
|
|
+ name=name.replace(" ", ","),
|
|
|
|
|
+ execution_id=execution_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[scheduler-extra] 解析 video_ids 失败 name={name}: {e}")
|
|
|
|
|
+ ext_data = {
|
|
|
|
|
+ "reason": reason,
|
|
|
|
|
+ "desc": desc_value,
|
|
|
|
|
+ "type": type_str,
|
|
|
|
|
+ "video_ids": video_ids,
|
|
|
|
|
+ "strategy": STRATEGY_ZAOAN,
|
|
|
|
|
+ }
|
|
|
|
|
+ rows.append(
|
|
|
|
|
+ {
|
|
|
|
|
+ "merge_leve2": CLUSTER_NAME[:32],
|
|
|
|
|
+ "name": _safe_truncate(name, 64),
|
|
|
|
|
+ "reason": reason,
|
|
|
|
|
+ "suggestion": desc_value,
|
|
|
|
|
+ "score": float(score),
|
|
|
|
|
+ "ext_data": json.dumps(ext_data, ensure_ascii=False),
|
|
|
|
|
+ "dt": dt_value,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ return rows
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def persist_zaoan_strategy(
|
|
|
|
|
+ kept_items: list[dict],
|
|
|
|
|
+ execution_id: Optional[int] = None,
|
|
|
|
|
+ now: Optional[datetime] = None,
|
|
|
|
|
+) -> dict:
|
|
|
|
|
+ """保留需求 + 固定 5 词,写入 demand_content 和 Hive 策略「当下供需gap-早安」。"""
|
|
|
|
|
+ from examples.demand.data_query_tools import write_dwd_zaoan_demand_pool_to_hive
|
|
|
|
|
+ from examples.demand.mysql import mysql_db
|
|
|
|
|
+
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ out_dir = extra_output_dir(current)
|
|
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ merged_items = _merge_kept_and_fixed(kept_items)
|
|
|
|
|
+ rows = _build_zaoan_rows(merged_items, execution_id=execution_id)
|
|
|
|
|
+ _write_json(out_dir / "zaoan_demand_items.json", merged_items)
|
|
|
|
|
+ _write_json(out_dir / "zaoan_rows.json", rows)
|
|
|
|
|
+
|
|
|
|
|
+ if not rows:
|
|
|
|
|
+ message = "早安策略行为空,跳过入库"
|
|
|
|
|
+ print(f"[scheduler-extra] {message}")
|
|
|
|
|
+ return {"ok": False, "message": message, "mysql": 0, "hive": 0}
|
|
|
|
|
+
|
|
|
|
|
+ mysql_affected = mysql_db.insert_many("demand_content", rows)
|
|
|
|
|
+ print(f"[scheduler-extra] MySQL demand_content 写入 rows={len(rows)}, affected={mysql_affected}")
|
|
|
|
|
+
|
|
|
|
|
+ hive_written = write_dwd_zaoan_demand_pool_to_hive(rows)
|
|
|
|
|
+ print(f"[scheduler-extra] Hive {STRATEGY_ZAOAN} 写入 rows={hive_written}")
|
|
|
|
|
+
|
|
|
|
|
+ marker = (
|
|
|
|
|
+ f"dt={rows[0]['dt']}\n"
|
|
|
|
|
+ f"execution_id={execution_id}\n"
|
|
|
|
|
+ f"mysql={mysql_affected}\n"
|
|
|
|
|
+ f"hive={hive_written}\n"
|
|
|
|
|
+ f"names={','.join(r['name'] for r in rows)}\n"
|
|
|
|
|
+ )
|
|
|
|
|
+ (out_dir / "zaoan_written.txt").write_text(marker, encoding="utf-8")
|
|
|
|
|
+ return {
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "mysql": mysql_affected,
|
|
|
|
|
+ "hive": hive_written,
|
|
|
|
|
+ "count": len(rows),
|
|
|
|
|
+ "output_dir": str(out_dir),
|
|
|
|
|
+ "names": [r["name"] for r in rows],
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def filter_and_persist_zaoan(
|
|
|
|
|
+ items: list[dict],
|
|
|
|
|
+ execution_id: Optional[int] = None,
|
|
|
|
|
+ now: Optional[datetime] = None,
|
|
|
|
|
+) -> dict:
|
|
|
|
|
+ """按权重取 top10,Qwen 筛选后加上固定早安词写入策略。"""
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ out_dir = extra_output_dir(current)
|
|
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ top_items = select_top_n_by_weight(items, execution_id=execution_id, n=TOP_N)
|
|
|
|
|
+ _write_json(out_dir / "demand_items.json", items)
|
|
|
|
|
+ _write_json(out_dir / "top10_demand_items.json", top_items)
|
|
|
|
|
+ terms = [term for term in (_decomp_term(item) for item in top_items) if term]
|
|
|
|
|
+ (out_dir / "terms.txt").write_text("\n".join(terms) + ("\n" if terms else ""), encoding="utf-8")
|
|
|
|
|
+ print(f"[scheduler-extra] 候选={len(items)} 按权重取 top{TOP_N}={len(top_items)}")
|
|
|
|
|
+
|
|
|
|
|
+ if not terms:
|
|
|
|
|
+ print("[scheduler-extra] top10 为空,仍写入固定早安需求")
|
|
|
|
|
+ (out_dir / "kept.txt").write_text("", encoding="utf-8")
|
|
|
|
|
+ _write_json(out_dir / "kept_demand_items.json", [])
|
|
|
|
|
+ persist = persist_zaoan_strategy([], execution_id=execution_id, now=current)
|
|
|
|
|
+ return {
|
|
|
|
|
+ "ok": persist.get("ok", False),
|
|
|
|
|
+ "execution_id": execution_id,
|
|
|
|
|
+ "raw_count": len(items),
|
|
|
|
|
+ "top_count": 0,
|
|
|
|
|
+ "kept_count": 0,
|
|
|
|
|
+ "persist": persist,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[scheduler-extra] 用 {QWEN_FILTER_MODEL} 筛选 {len(terms)} 个解构词")
|
|
|
|
|
+ try:
|
|
|
|
|
+ raw_content, kept_lines = await _filter_with_qwen(terms)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[scheduler-extra] Qwen 筛选失败: {e}")
|
|
|
|
|
+ (out_dir / "qwen_error.txt").write_text(str(e), encoding="utf-8")
|
|
|
|
|
+ raw_content, kept_lines = "", []
|
|
|
|
|
+ (out_dir / "qwen_raw.txt").write_text(raw_content + ("\n" if raw_content else ""), encoding="utf-8")
|
|
|
|
|
+ (out_dir / "kept.txt").write_text("\n".join(kept_lines) + ("\n" if kept_lines else ""), encoding="utf-8")
|
|
|
|
|
+
|
|
|
|
|
+ kept_items = [item for item in top_items if _item_matches_kept(item, kept_lines)]
|
|
|
|
|
+ _write_json(out_dir / "kept_demand_items.json", kept_items)
|
|
|
|
|
+ persist = persist_zaoan_strategy(kept_items, execution_id=execution_id, now=current)
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[scheduler-extra] 完成 execution_id={execution_id} "
|
|
|
|
|
+ f"raw={len(items)} top={len(top_items)} kept={len(kept_items)}"
|
|
|
|
|
+ )
|
|
|
|
|
+ return {
|
|
|
|
|
+ "ok": True,
|
|
|
|
|
+ "execution_id": execution_id,
|
|
|
|
|
+ "raw_count": len(items),
|
|
|
|
|
+ "top_count": len(top_items),
|
|
|
|
|
+ "kept_count": len(kept_items),
|
|
|
|
|
+ "output_dir": str(out_dir),
|
|
|
|
|
+ "persist": persist,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def run_zaozhongwanhao_extra(now: Optional[datetime] = None) -> dict:
|
|
|
|
|
+ """
|
|
|
|
|
+ 当天没有「早中晚好」品类时:只生成 10 条(不进普通需求表),
|
|
|
|
|
+ 再筛选并写入「当下供需gap-早安」。
|
|
|
|
|
+ """
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ out_dir = extra_output_dir(current)
|
|
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ if not exist_cluster_tree(CLUSTER_NAME):
|
|
|
|
|
+ message = "获取聚类树失败,跳过额外生成"
|
|
|
|
|
+ print(f"[scheduler-extra] {CLUSTER_NAME}: {message}")
|
|
|
|
|
+ (out_dir / "error.txt").write_text(message, encoding="utf-8")
|
|
|
|
|
+ return {"ok": False, "message": message}
|
|
|
|
|
+
|
|
|
|
|
+ from examples.demand.run import main as run_demand
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[scheduler-extra] 当天无品类,只生成 {EXTRA_COUNT} 条需求(不入库)")
|
|
|
|
|
+ result = await run_demand(
|
|
|
|
|
+ CLUSTER_NAME,
|
|
|
|
|
+ PLATFORM_TYPE,
|
|
|
|
|
+ EXTRA_COUNT,
|
|
|
|
|
+ write_to_db=False,
|
|
|
|
|
+ )
|
|
|
|
|
+ execution_id = result.get("execution_id") if isinstance(result, dict) else None
|
|
|
|
|
+ if not execution_id:
|
|
|
|
|
+ message = "额外生成失败:未拿到 execution_id"
|
|
|
|
|
+ print(f"[scheduler-extra] {message}")
|
|
|
|
|
+ (out_dir / "error.txt").write_text(message, encoding="utf-8")
|
|
|
|
|
+ return {"ok": False, "message": message, "execution_id": None}
|
|
|
|
|
+
|
|
|
|
|
+ items = _load_demand_items(execution_id)
|
|
|
|
|
+ return await filter_and_persist_zaoan(items, execution_id=execution_id, now=current)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def run_zaoan_from_generated_demands(
|
|
|
|
|
+ execution_id: Optional[int] = None,
|
|
|
|
|
+ now: Optional[datetime] = None,
|
|
|
|
|
+) -> dict:
|
|
|
|
|
+ """当天品类已产生多条需求时:按权重取 top10,再筛选写入早安策略。"""
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ if not execution_id:
|
|
|
|
|
+ from examples.demand.run import get_execution_id_by_merge_level2
|
|
|
|
|
+
|
|
|
|
|
+ execution_id = get_execution_id_by_merge_level2(CLUSTER_NAME)
|
|
|
|
|
+ if not execution_id:
|
|
|
|
|
+ message = "未找到当天「早中晚好」execution_id,无法按权重取 top10"
|
|
|
|
|
+ print(f"[scheduler-extra] {message}")
|
|
|
|
|
+ persist = persist_zaoan_strategy([], execution_id=None, now=current)
|
|
|
|
|
+ return {"ok": persist.get("ok", False), "message": message, "persist": persist}
|
|
|
|
|
+
|
|
|
|
|
+ items = _load_demand_items(execution_id)
|
|
|
|
|
+ print(f"[scheduler-extra] 品类已产出 {len(items)} 条,按权重取 top{TOP_N}")
|
|
|
|
|
+ return await filter_and_persist_zaoan(items, execution_id=execution_id, now=current)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def maybe_run_zaozhongwanhao_extra(schedule_items: list[dict] | None, now: Optional[datetime] = None) -> None:
|
|
|
|
|
+ """仅处理「当天列表没有早中晚好」:补生成 10 条。有品类时等正常任务跑完再取 top10。"""
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ out_dir = extra_output_dir(current)
|
|
|
|
|
+ if extra_already_done_today(current):
|
|
|
|
|
+ print(f"[scheduler-extra] 当天早安策略已写入,跳过:{out_dir}")
|
|
|
|
|
+ return
|
|
|
|
|
+ if schedule_has_zaozhongwanhao(schedule_items):
|
|
|
|
|
+ print(f"[scheduler-extra] 当天已有「{CLUSTER_NAME}」,等正常任务产出后按权重取 top{TOP_N}")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ kept_path = out_dir / "kept_demand_items.json"
|
|
|
|
|
+ if kept_path.exists():
|
|
|
|
|
+ print(f"[scheduler-extra] 复用已有筛选结果写入早安策略:{kept_path}")
|
|
|
|
|
+ try:
|
|
|
|
|
+ kept_items = json.loads(kept_path.read_text(encoding="utf-8"))
|
|
|
|
|
+ if not isinstance(kept_items, list):
|
|
|
|
|
+ kept_items = []
|
|
|
|
|
+ persist_zaoan_strategy(kept_items, now=current)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[scheduler-extra] 复用筛选结果写入失败: {e}")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ print(f"[scheduler-extra] 当天没有「{CLUSTER_NAME}」,只产生 {EXTRA_COUNT} 条后写入 {STRATEGY_ZAOAN}")
|
|
|
|
|
+ try:
|
|
|
|
|
+ await run_zaozhongwanhao_extra(now=current)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[scheduler-extra] 额外生成失败: {e}")
|
|
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ (out_dir / "error.txt").write_text(str(e), encoding="utf-8")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def maybe_finish_zaoan_from_cluster(
|
|
|
|
|
+ execution_id: Optional[int] = None,
|
|
|
|
|
+ now: Optional[datetime] = None,
|
|
|
|
|
+) -> None:
|
|
|
|
|
+ """品类正常任务之后:按权重取 top10 写入早安策略。"""
|
|
|
|
|
+ current = now or datetime.now(BEIJING_TZ)
|
|
|
|
|
+ out_dir = extra_output_dir(current)
|
|
|
|
|
+ if extra_already_done_today(current):
|
|
|
|
|
+ print(f"[scheduler-extra] 当天早安策略已写入,跳过:{out_dir}")
|
|
|
|
|
+ return
|
|
|
|
|
+ try:
|
|
|
|
|
+ await run_zaoan_from_generated_demands(execution_id=execution_id, now=current)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[scheduler-extra] 按权重取 top{TOP_N} 失败: {e}")
|
|
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
+ (out_dir / "error.txt").write_text(str(e), encoding="utf-8")
|