| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039 |
- from __future__ import annotations
- import copy
- import json
- import os
- import time
- from collections import Counter
- from typing import Any
- from content_agent.dashboard_service import (
- DATA_ORIGIN_PRODUCTION_DB,
- DATA_ORIGIN_RUNTIME_EXPORT,
- DashboardService,
- )
- from content_agent.integrations.database_runtime import DatabaseRuntimeStore
- from content_agent.interfaces import RuntimeStore
- FIRST_ROUND_QUERY_METHODS = {
- "seed_term",
- "piaoquan_topic_point",
- "query_seed_point",
- "category_leaf_element",
- }
- EXTENSION_QUERY_METHODS = {
- "tag_query",
- "author_works",
- }
- SOURCE_LABELS = {
- "seed_term": "从需求单的 pattern 词来",
- "piaoquan_topic_point": "票圈帖子具体的点",
- "query_seed_point": "票圈帖子具体的点",
- "category_leaf_element": "分类叶子元素",
- }
- QUERY_METHOD_LABELS = {
- "seed_term": "从需求单的 pattern 词来",
- "piaoquan_topic_point": "票圈帖子具体的点",
- "query_seed_point": "票圈帖子具体的点",
- "category_leaf_element": "分类叶子元素",
- "tag_query": "从视频标签继续游走",
- "author_works": "从作者继续游走",
- }
- ACTION_LABELS = {
- "ADD_TO_CONTENT_POOL": "入池",
- "KEEP_CONTENT_FOR_REVIEW": "待复看",
- "REJECT_CONTENT": "淘汰",
- "TECHNICAL_RETRY_REQUIRED": "技术问题",
- }
- MEDIA_STATUS_LABELS = {
- "oss_uploaded": "已保存",
- "oss_upload_pending": "待补传",
- "oss_upload_failed": "补传失败",
- "metadata_only": "尚未拿到视频",
- "unavailable": "无可用视频",
- }
- WALK_EDGE_LABELS = {
- "hashtag_to_query": "从视频标签继续游走",
- "tag_query": "从视频标签继续游走",
- "author_to_works": "从作者继续游走",
- "video_to_author": "从作者继续游走",
- "author_works": "从作者继续游走",
- "path_stop": "到这里停止",
- "decision_to_asset": "入池沉淀",
- "budget_downgrade": "转入待复看",
- "terminal": "到这里停止",
- }
- REASON_LABELS = {
- "content_pattern_recall_required": "内容和本次需求没有形成有效关联",
- "blocked_by_rule_decision": "上一步内容未通过判断",
- "missing_content_portrait": "缺少判断所需的内容画像",
- "category_or_element_binding_required": "没有命中本次需求的类目或要素",
- "missing_score": "没有足够信息进入打分",
- "v4_query_and_platform_pass": "搜索词和平台表现都符合要求",
- "v4_query_or_score_below_threshold": "相关性或平台表现未达到要求",
- "v4_score_review_needed": "分数处在复看区间,需要人工确认",
- "v4_technical_retry_needed": "系统需要重试后再判断",
- "v4_allow_walk_denied": "不适合继续向外扩展",
- "budget_exhausted": "达到该类游走次数上限",
- "global_action_cap_reached": "已达全 run 游走次数上限",
- "edge_blocked_by_platform_profile": "该平台 profile 阻断这种游走",
- "portrait_unavailable": "拉不到作者 50+ 画像(热点宝接口失败),无法判定",
- "portrait_incomplete": "作者 50+ 画像字段缺失,无法判定",
- "query>=65/platform>=65/score>=65": "评分达到继续扩展门槛",
- "query>=70/platform>=65/score>=70": "评分达到继续扩展门槛",
- "content_decision_reused_for_walk_gate": "复用本条视频的内容判断作为扩展门槛",
- "no_decision": "暂未产生判断结果",
- }
- # 向外游走边(标签/作者),用于统计游走次数(不含 path_stop/decision_to_asset 等终端结局)。
- _WALK_EXPANSION_EDGES = {"hashtag_to_query", "author_to_works"}
- _FLOW_LEDGER_CACHE: dict[tuple[str, str], tuple[float, dict[str, Any]]] = {}
- _DEFAULT_CACHE_TTL_SECONDS = 300
- def _reason_label(reason: str, default: str) -> str:
- if reason.startswith("query>=") and "/platform>=" in reason and "/score>=" in reason:
- return "评分达到继续扩展门槛"
- return REASON_LABELS.get(reason, default)
- class FlowLedgerService:
- def __init__(self, runtime: RuntimeStore) -> None:
- dashboard = DashboardService.from_runtime(runtime)
- self.runtime = dashboard.runtime
- self.export_runtime = dashboard.export_runtime
- self.data_origin = (
- DATA_ORIGIN_PRODUCTION_DB
- if isinstance(self.runtime, DatabaseRuntimeStore)
- else DATA_ORIGIN_RUNTIME_EXPORT
- )
- def ledger(self, run_id: str, *, debug: bool = False) -> dict[str, Any]:
- cache_key = ("ledger", run_id)
- if not debug:
- cached = _cache_get(cache_key)
- if cached is not None:
- return cached
- bundle = self._bundle(run_id, cache=not debug)
- walk_out = _walk_out_counter(bundle)
- rows = [
- self._ledger_row(query, bundle, walk_out, debug=debug)
- for query in bundle["queries"]
- if _query_method(query) in FIRST_ROUND_QUERY_METHODS
- ]
- extension_queries = [
- self._extension_query(query, bundle, debug=debug)
- for query in bundle["queries"]
- if _query_method(query) in EXTENSION_QUERY_METHODS
- ]
- media_counts = Counter(_media_status(media) for media in bundle["media"])
- action_counts = Counter(_decision_action(decision) for decision in bundle["decisions"])
- gemini_counts = Counter(_text(evidence.get("decode_status") or evidence.get("gemini_status"), "unknown") for evidence in bundle["evidence"])
- payload = {
- "run_id": run_id,
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- "summary": self._summary(bundle, rows, extension_queries, media_counts, action_counts, gemini_counts),
- "demand_summary": _demand_summary(bundle),
- "rows": rows,
- "extension_queries": extension_queries,
- "debug": bundle if debug else None,
- }
- if not debug:
- _cache_set(cache_key, payload)
- return payload
- def query_videos(self, run_id: str, query_id: str, *, debug: bool = False) -> dict[str, Any]:
- bundle = self._bundle(run_id, cache=not debug)
- query = self._query_by_id(bundle, query_id)
- related = self._query_content(query_id, bundle, first_round_only=True)
- videos = [self._video(content, bundle, debug=debug) for content in related]
- return {
- "run_id": run_id,
- "query": self._query_summary(query, bundle, debug=debug) if query else None,
- "videos": videos,
- "summary": self._video_summary(videos),
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- }
- def query_walk(self, run_id: str, query_id: str, *, debug: bool = False) -> dict[str, Any]:
- bundle = self._bundle(run_id, cache=not debug)
- query = self._query_by_id(bundle, query_id)
- related_ids = self._query_related_content_ids(query_id, bundle)
- related_decision_ids = self._query_related_decision_ids(query_id, bundle)
- actions = [
- self._walk_action(action, query, bundle, debug=debug)
- for action in bundle["walk_actions"]
- if _walk_action_related(action, query_id, related_ids)
- or self._walk_action_related_to_decisions(action, related_decision_ids)
- ]
- lanes: dict[str, list[dict[str, Any]]] = {key: [] for key in ["tag_query", "author_works", "path_stop"]}
- for action in actions:
- lanes[_walk_lane(action["edge_id"])].append(action)
- target_query_ids = {_text(_record(action.get("target_query")).get("id")) for action in actions}
- extension_queries = [
- self._extension_query(item, bundle, debug=debug)
- for item in bundle["queries"]
- if _query_method(item) in EXTENSION_QUERY_METHODS
- and (
- _text(item.get("search_query_id")) in target_query_ids
- or _text(_record(item.get("raw_payload")).get("parent_search_query_id") or item.get("llm_variant_of")) == query_id
- )
- ]
- return {
- "run_id": run_id,
- "query": self._query_summary(query, bundle, debug=debug) if query else None,
- "actions": actions,
- "lanes": lanes,
- "extension_queries": extension_queries,
- "summary": {
- "total_actions": len(actions),
- "success_count": sum(1 for item in actions if item["status"] == "success"),
- "skipped_count": sum(1 for item in actions if item["status"] == "skipped"),
- "extension_query_count": len(extension_queries),
- },
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- }
- def run_walk(self, run_id: str, *, debug: bool = False) -> dict[str, Any]:
- # 全 run 游走图:不按 query 过滤,富化全部 walk_actions(full=True 不采样带回视频),并给出游走预算总览。
- bundle = self._bundle(run_id, cache=not debug)
- actions = [self._walk_action(action, None, bundle, debug=debug, full=True) for action in bundle["walk_actions"]]
- lanes: dict[str, list[dict[str, Any]]] = {key: [] for key in ["tag_query", "author_works", "path_stop"]}
- for action in actions:
- lanes[_walk_lane(action["edge_id"])].append(action)
- extension_queries = [
- self._extension_query(item, bundle, debug=debug)
- for item in bundle["queries"]
- if _query_method(item) in EXTENSION_QUERY_METHODS
- ]
- summary = _walk_budget_overview(bundle, actions, extension_queries)
- # 全run游走图按所属搜索词分组用:首轮 query id → 文案。
- summary["query_labels"] = {
- _text(item.get("search_query_id")): _text(item.get("search_query"), "搜索词待确认")
- for item in bundle["queries"]
- if _query_method(item) in FIRST_ROUND_QUERY_METHODS and _text(item.get("search_query_id"))
- }
- return {
- "run_id": run_id,
- "actions": actions,
- "lanes": lanes,
- "extension_queries": extension_queries,
- "summary": summary,
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- }
- def video_walk(self, run_id: str, content_id: str, *, debug: bool = False) -> dict[str, Any]:
- # 单视频游走子树:从该视频出发 BFS,只保留从它向外走出去的那一支(沿 source_video.id → target_videos.id 递归)。
- bundle = self._bundle(run_id, cache=not debug)
- root_content = next(
- (
- item
- for item in bundle["content_items"]
- if content_id in {_text(item.get("platform_content_id")), _text(item.get("content_discovery_id"))}
- ),
- None,
- )
- origin_label = "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存"
- if not root_content:
- return {
- "run_id": run_id,
- "root_video": None,
- "actions": [],
- "summary": {"total_actions": 0, "success_count": 0, "skipped_count": 0},
- "data_origin": self.data_origin,
- "data_origin_label": origin_label,
- }
- root_video = self._video(root_content, bundle, debug=debug)
- enriched = [self._walk_action(action, None, bundle, debug=debug, full=True) for action in bundle["walk_actions"]]
- by_source: dict[str, list[dict[str, Any]]] = {}
- for item in enriched:
- source_id = _text(_record(item.get("source_video")).get("id"))
- if source_id:
- by_source.setdefault(source_id, []).append(item)
- root_id = _text(root_video.get("id"))
- seen_videos = {root_id}
- seen_actions: set[str] = set()
- subtree: list[dict[str, Any]] = []
- frontier = [root_id]
- while frontier:
- nxt: list[str] = []
- for video_id in frontier:
- for item in by_source.get(video_id, []):
- action_id = _text(item.get("id")) or str(id(item))
- if action_id in seen_actions:
- continue
- seen_actions.add(action_id)
- subtree.append(item)
- for target in item.get("target_videos") or []:
- target_id = _text(_record(target).get("id"))
- if target_id and target_id not in seen_videos:
- seen_videos.add(target_id)
- nxt.append(target_id)
- frontier = nxt
- statuses = Counter(_text(item.get("status"), "unknown") for item in subtree)
- return {
- "run_id": run_id,
- "root_video": root_video,
- "actions": subtree,
- "summary": {
- "total_actions": len(subtree),
- "success_count": statuses["success"],
- "skipped_count": statuses["skipped"],
- },
- "data_origin": self.data_origin,
- "data_origin_label": origin_label,
- }
- def video_detail(self, run_id: str, content_id: str, *, debug: bool = False) -> dict[str, Any]:
- bundle = self._bundle(run_id, cache=not debug)
- content = next(
- (
- item
- for item in bundle["content_items"]
- if content_id in {
- _text(item.get("platform_content_id")),
- _text(item.get("content_discovery_id")),
- }
- ),
- None,
- )
- if not content:
- return {
- "run_id": run_id,
- "video": None,
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- }
- video = self._video(content, bundle, debug=debug)
- platform_id = _text(content.get("platform_content_id"))
- decision = _decision_for_content(content, bundle["decisions"])
- source_paths = [
- path
- for path in bundle["source_paths"]
- if platform_id and platform_id in {_text(path.get("from_node_id")), _text(path.get("to_node_id")), _text(path.get("decision_target_id"))}
- ]
- return {
- "run_id": run_id,
- "video": video,
- "source_paths": [_source_path(path, debug=debug) for path in source_paths],
- "decision": self._decision_summary(decision, debug=debug) if decision else None,
- "data_origin": self.data_origin,
- "data_origin_label": "生产数据库" if self.data_origin == DATA_ORIGIN_PRODUCTION_DB else "本地调试缓存",
- }
- def _bundle(self, run_id: str, *, cache: bool = True) -> dict[str, Any]:
- cache_key = ("bundle", run_id)
- if cache:
- cached = _cache_get(cache_key)
- if cached is not None:
- return cached
- payload = {
- "source_context": self._read_json(run_id, "source_context.json") or {},
- "pattern_seed_pack": self._read_json(run_id, "pattern_seed_pack.json") or {},
- "queries": self._read_jsonl(run_id, "search_queries.jsonl"),
- "content_items": self._read_jsonl(run_id, "discovered_content_items.jsonl"),
- "media": self._read_jsonl(run_id, "content_media_records.jsonl"),
- "evidence": self._read_jsonl(run_id, "pattern_recall_evidence.jsonl"),
- "decisions": self._read_jsonl(run_id, "rule_decisions.jsonl"),
- "walk_actions": self._read_jsonl(run_id, "walk_actions.jsonl"),
- "source_paths": self._read_jsonl(run_id, "source_path_records.jsonl"),
- "final_output": self._read_json(run_id, "final_output.json") or {},
- }
- if cache:
- _cache_set(cache_key, payload)
- return payload
- def _read_json(self, run_id: str, filename: str) -> dict[str, Any] | None:
- try:
- return self.runtime.read_json(run_id, filename)
- except Exception:
- if self.export_runtime:
- try:
- return self.export_runtime.read_json(run_id, filename)
- except Exception:
- return None
- return None
- def _read_jsonl(self, run_id: str, filename: str) -> list[dict[str, Any]]:
- try:
- return self.runtime.read_jsonl(run_id, filename)
- except Exception:
- if self.export_runtime:
- try:
- return self.export_runtime.read_jsonl(run_id, filename)
- except Exception:
- return []
- return []
- def _query_by_id(self, bundle: dict[str, Any], query_id: str) -> dict[str, Any] | None:
- return next((query for query in bundle["queries"] if _text(query.get("search_query_id")) == query_id), None)
- def _ledger_row(self, query: dict[str, Any], bundle: dict[str, Any], walk_out: dict[str, int], *, debug: bool) -> dict[str, Any]:
- query_id = _text(query.get("search_query_id"))
- contents = self._query_content(query_id, bundle, first_round_only=True)
- videos = [self._video(content, bundle, debug=debug) for content in contents]
- # 每条首轮视频是否真正向外走过(success 扩展边),供首页「看扩展过程」按钮判断显示。
- for video in videos:
- video["walk_out_count"] = walk_out.get(_text(video.get("platform_content_id")), 0)
- decisions = [
- decision
- for decision in (_decision_for_content(content, bundle["decisions"]) for content in contents)
- if decision
- ]
- related_ids = self._query_related_content_ids(query_id, bundle)
- actions = [action for action in bundle["walk_actions"] if _walk_action_related(action, query_id, related_ids)]
- source = self._source_summary(query, bundle, debug=debug)
- return {
- "id": query_id,
- "source": source,
- "query": self._query_summary(query, bundle, debug=debug),
- "first_round_videos": videos,
- "first_round_video_total": len(videos),
- "video_stats": self._video_summary(videos),
- "rule_summary": self._rule_summary(decisions, debug=debug),
- "walk_summary": self._walk_summary(actions),
- "asset_summary": self._asset_summary(decisions, bundle["final_output"]),
- "debug": {"query": query} if debug else None,
- }
- def _query_content(self, query_id: str, bundle: dict[str, Any], *, first_round_only: bool) -> list[dict[str, Any]]:
- items = [item for item in bundle["content_items"] if _content_belongs_to_query(item, query_id)]
- if first_round_only:
- walk_query_ids = _walk_query_ids(bundle)
- items = [item for item in items if _is_first_round_content(item, walk_query_ids)]
- return items
- def _query_related_content_ids(self, query_id: str, bundle: dict[str, Any]) -> set[str]:
- ids: set[str] = set()
- for item in self._query_content(query_id, bundle, first_round_only=True):
- ids.update(_content_ids(item))
- return {item for item in ids if item}
- def _query_related_decision_ids(self, query_id: str, bundle: dict[str, Any]) -> set[str]:
- ids: set[str] = set()
- for content in self._query_content(query_id, bundle, first_round_only=True):
- decision = _decision_for_content(content, bundle["decisions"])
- if decision:
- ids.add(_text(decision.get("decision_id")))
- return {item for item in ids if item}
- def _walk_action_related_to_decisions(self, action: dict[str, Any], decision_ids: set[str]) -> bool:
- if not decision_ids:
- return False
- raw = _record(action.get("raw_payload"))
- values = {
- _text(action.get("decision_id")),
- _text(raw.get("decision_id")),
- _text(action.get("from_node_id")),
- _text(raw.get("from_node_id")),
- }
- return bool(values & decision_ids)
- def _source_summary(self, query: dict[str, Any], bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- method = _query_method(query)
- seed_ref = _record(query.get("pattern_seed_ref"))
- raw_payload = _record(query.get("raw_payload"))
- refs = _list(raw_payload.get("query_source_refs")) or _list(seed_ref.get("query_source_refs"))
- source_text = (
- _text(seed_ref.get("query_source_text"))
- or _text(seed_ref.get("seed_term"))
- or _text(query.get("search_query"))
- )
- terms = _text_list(query.get("query_source_terms"))
- source_ref = _query_source_ref(seed_ref, refs)
- details = _source_details(method, source_text, seed_ref, source_ref, bundle)
- return {
- "type": method,
- "label": SOURCE_LABELS.get(method, "来源待确认"),
- "evidence": source_text or (terms[0] if terms else "来源证据待确认"),
- "source_ref": _source_ref_label(method, seed_ref, source_ref, bundle),
- "terms": terms,
- "details": details,
- "query_source_refs": refs,
- "debug": {"pattern_seed_ref": seed_ref} if debug else None,
- }
- def _query_summary(self, query: dict[str, Any] | None, bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- if not query:
- return {
- "id": "",
- "text": "搜索词待确认",
- "method": "unknown",
- "method_label": "生成方式待确认",
- "source_terms": [],
- "effect_label": "处理状态待确认",
- }
- method = _query_method(query)
- return {
- "id": _text(query.get("search_query_id")),
- "text": _text(query.get("search_query"), "搜索词待确认"),
- "method": method,
- "method_label": QUERY_METHOD_LABELS.get(method, "生成方式待确认"),
- "source_terms": _text_list(query.get("query_source_terms")),
- "effect_status": _text(query.get("search_query_effect_status"), "pending"),
- "effect_label": _effect_label(_text(query.get("search_query_effect_status"), "pending")),
- "debug": {"query": query} if debug else None,
- }
- def _extension_query(self, query: dict[str, Any], bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- summary = self._query_summary(query, bundle, debug=debug)
- raw_payload = _record(query.get("raw_payload"))
- summary.update({
- "parent_query_id": _text(raw_payload.get("parent_search_query_id") or query.get("llm_variant_of")),
- "trigger": _text(raw_payload.get("hashtag") or raw_payload.get("author_id") or raw_payload.get("cursor")),
- })
- return summary
- def _video(self, content: dict[str, Any], bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- platform_id = _text(content.get("platform_content_id"))
- media = _media_for_content(content, bundle["media"])
- evidence = _evidence_for_content(content, bundle["evidence"])
- decision = _decision_for_content(content, bundle["decisions"])
- status = _media_status(media)
- stats = _record(content.get("statistics"))
- decision_summary = self._decision_summary(decision, debug=debug) if decision else None
- technical_retry_detail = _technical_retry_detail(decision, evidence, media)
- # 内容审核拦截从"技术问题"里单独分出来:重试无效、原因不同,展示成独立类别。
- if (
- decision_summary
- and decision_summary.get("action") == "TECHNICAL_RETRY_REQUIRED"
- and _is_content_inspection_blocked(technical_retry_detail, evidence)
- ):
- decision_summary = {
- **decision_summary,
- "label": "模型内容审核拦截",
- "reason": "content_inspection_blocked",
- "reason_label": "视频被大模型内容安全审核拦截(非系统故障,重试无效)",
- "content_inspection_blocked": True,
- }
- return {
- "id": platform_id or _text(content.get("content_discovery_id"), "content"),
- "content_discovery_id": _text(content.get("content_discovery_id")),
- "platform_content_id": platform_id,
- "search_query_id": _text(content.get("search_query_id")),
- "progressive_batch_kind": _text(content.get("progressive_batch_kind") or _record(content.get("raw_payload")).get("progressive_batch_kind")),
- "progressive_page_number": _number(content.get("progressive_page_number") or _record(content.get("raw_payload")).get("progressive_page_number")),
- "progressive_item_rank_in_page": _number(content.get("progressive_item_rank_in_page") or _record(content.get("raw_payload")).get("progressive_item_rank_in_page")),
- "title": _text(content.get("description") or content.get("title"), "无标题视频"),
- "author": _text(content.get("author_display_name") or content.get("platform_author_id"), "未知作者"),
- "platform": _text(content.get("platform"), "unknown"),
- "platform_label": _platform_label(_text(content.get("platform"), "unknown")),
- "platform_url": _platform_content_url(content),
- "tags": _text_list(content.get("tags")),
- "statistics": {key: _number(value) for key, value in stats.items()},
- "published_at": _text(content.get("published_at") or content.get("publish_time")),
- "oss_url": _text(media.get("oss_url")) if media else "",
- "playable_url": _text(media.get("oss_url")) if media else "",
- "media_status": status,
- "media_status_label": MEDIA_STATUS_LABELS.get(status, "视频状态待确认"),
- "media_failure_reason": _media_failure_reason(media),
- "decision": decision_summary,
- "gemini": self._evidence_summary(evidence, debug=debug) if evidence else None,
- "technical_retry_detail": technical_retry_detail,
- "debug": {"content": content, "media": media, "evidence": evidence} if debug else None,
- }
- def _decision_summary(self, decision: dict[str, Any] | None, *, debug: bool) -> dict[str, Any]:
- if not decision:
- return {
- "action": "NOT_JUDGED",
- "label": "未进入判断",
- "reason": "no_decision",
- "reason_label": REASON_LABELS["no_decision"],
- }
- reason = _text(decision.get("decision_reason_code"), "no_decision")
- scorecard = _record(decision.get("scorecard"))
- score = decision.get("score") if decision.get("score") is not None else scorecard.get("total_score")
- action = _decision_action(decision)
- return {
- "id": _text(decision.get("decision_id")),
- "action": action,
- "label": ACTION_LABELS.get(action, "待判断"),
- "reason": reason,
- "reason_label": _reason_label(reason, "系统原因待补充"),
- "score": score,
- "score_label": f"{round(float(score))} 分" if isinstance(score, (int, float)) else "",
- "score_items": _decision_score_items(scorecard, score),
- "score_thresholds": _record(scorecard.get("score_thresholds")) or None,
- "allow_walk": bool(_record(decision.get("decision_replay_data")).get("allow_walk")),
- "v4_explanation": _record(decision.get("v4_explanation") or _record(decision.get("raw_payload")).get("v4_explanation")),
- "debug": {"decision": decision} if debug else None,
- }
- def _evidence_summary(self, evidence: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- status = _text(evidence.get("decode_status") or evidence.get("gemini_status"), "unknown")
- return {
- "status": status,
- "label": "Gemini 已看过" if status in {"success", "ok", "passed"} else "Gemini 未完成判断",
- "summary": _text(evidence.get("reason") or evidence.get("evidence_summary") or evidence.get("summary")),
- "debug": {"evidence": evidence} if debug else None,
- }
- def _rule_summary(self, decisions: list[dict[str, Any]], *, debug: bool) -> dict[str, Any]:
- actions = Counter(_decision_action(item) for item in decisions)
- reasons = Counter(_text(item.get("decision_reason_code") or item.get("reason"), "no_decision") for item in decisions)
- primary_reason = reasons.most_common(1)[0][0] if reasons else "no_decision"
- return {
- "total": len(decisions),
- "pool_count": actions["ADD_TO_CONTENT_POOL"],
- "review_count": actions["KEEP_CONTENT_FOR_REVIEW"],
- "technical_retry_count": actions["TECHNICAL_RETRY_REQUIRED"],
- "reject_count": actions["REJECT_CONTENT"],
- "primary_reason": primary_reason,
- "primary_reason_label": _reason_label(primary_reason, "系统原因待补充"),
- "score_summary": _score_summary(decisions),
- "headline": _rule_headline(actions, len(decisions)),
- "debug": {"decisions": decisions} if debug else None,
- }
- def _walk_summary(self, actions: list[dict[str, Any]]) -> dict[str, Any]:
- edges = Counter(_walk_lane_id(action) for action in actions)
- reasons = Counter(_text(action.get("reason_code")) for action in actions if _text(action.get("reason_code")))
- statuses = Counter(_text(action.get("walk_status"), "unknown") for action in actions)
- expansion = [a for a in actions if _text(a.get("edge_id")) in _WALK_EXPANSION_EDGES]
- return {
- "total": len(actions),
- "success_count": statuses["success"],
- "skipped_count": statuses["skipped"],
- "expansion_count": len(expansion),
- "expansion_success": sum(1 for a in expansion if _text(a.get("walk_status")) == "success"),
- "max_depth": max((_number(a.get("depth")) or 0 for a in actions), default=0),
- "edge_counts": dict(edges),
- "edge_labels": {key: WALK_EDGE_LABELS.get(key, "系统原因待补充") for key in edges},
- "stop_reason": reasons.most_common(1)[0][0] if reasons else "",
- "stop_reason_label": _reason_label(reasons.most_common(1)[0][0], "本轮没有继续扩展") if reasons else "本轮没有继续扩展",
- "headline": f"{len(actions)} 次扩展动作" if actions else "本轮没有继续扩展",
- }
- def _asset_summary(self, decisions: list[dict[str, Any]], final_output: dict[str, Any]) -> dict[str, Any]:
- decision_ids = {_text(item.get("decision_id") or item.get("id")) for item in decisions}
- if not decision_ids:
- return {
- "total": 0,
- "pool_count": 0,
- "review_count": 0,
- "technical_retry_count": 0,
- "reject_count": 0,
- "headline": _asset_headline(0, 0, 0, 0),
- }
- sections = {
- "content_assets": "入池",
- "review_records": "待复看",
- "technical_retry_records": "技术问题",
- "reject_records": "淘汰",
- }
- counts = {}
- total = 0
- for section, label in sections.items():
- rows = [
- item
- for item in _list(final_output.get(section))
- if _text(item.get("decision_id")) in decision_ids
- ]
- counts[label] = len(rows)
- total += len(rows)
- return {
- "total": total,
- "pool_count": counts.get("入池", 0),
- "review_count": counts.get("待复看", 0),
- "technical_retry_count": counts.get("技术问题", 0),
- "reject_count": counts.get("淘汰", 0),
- "headline": _asset_headline(
- counts.get("入池", 0),
- counts.get("待复看", 0),
- counts.get("技术问题", 0),
- counts.get("淘汰", 0),
- ),
- }
- def _video_summary(self, videos: list[dict[str, Any]]) -> dict[str, Any]:
- actions = Counter(_record(video.get("decision")).get("action") or "NOT_JUDGED" for video in videos)
- media = Counter(video.get("media_status") or "unknown" for video in videos)
- return {
- "total": len(videos),
- "pool_count": actions["ADD_TO_CONTENT_POOL"],
- "review_count": actions["KEEP_CONTENT_FOR_REVIEW"],
- "technical_retry_count": actions["TECHNICAL_RETRY_REQUIRED"],
- "reject_count": actions["REJECT_CONTENT"],
- "not_judged_count": actions["NOT_JUDGED"],
- "oss_uploaded_count": media["oss_uploaded"],
- "oss_pending_count": media["oss_upload_pending"],
- "oss_failed_count": media["oss_upload_failed"],
- "unavailable_count": media["unavailable"] + media["metadata_only"] + media["oss_upload_failed"],
- }
- def _walk_action(self, action: dict[str, Any], query: dict[str, Any] | None, bundle: dict[str, Any], *, debug: bool, full: bool = False) -> dict[str, Any]:
- edge = _walk_lane_id(action)
- reason = _text(action.get("reason_code"))
- status = _text(action.get("walk_status"), "unknown")
- gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason") or reason)
- source_content = _content_for_action(action, bundle)
- source_video = self._video(source_content, bundle, debug=debug) if source_content else None
- target_query = _target_query_for_action(action, bundle)
- target_query_summary = self._query_summary(target_query, bundle, debug=debug) if target_query else None
- target_contents = _target_contents_for_action(action, bundle, source_content, target_query)
- # full=True(全run游走图 / 单视频子树)不再采样,展示全部带回视频;per-query 视图仍取前 4 条。
- shown_contents = target_contents if full else target_contents[:4]
- target_videos = [self._video(content, bundle, debug=debug) for content in shown_contents]
- trigger_label = _walk_trigger_label(action, source_content, target_query_summary)
- from_label = _walk_from_label(action, query, bundle, source_content)
- score_items = _walk_score_items(action)
- result_label = _walk_result_label(action, edge, status, reason, source_video, target_query_summary, target_videos)
- reason_label = _walk_reason_label(action, reason)
- operator_display_status = _walk_operator_display_status(status, reason)
- return {
- "id": _text(action.get("walk_action_id")),
- "edge_id": edge,
- "edge_label": WALK_EDGE_LABELS.get(edge, "系统原因待补充"),
- "depth": _number(action.get("depth")),
- "source_video": source_video,
- "target_query": target_query_summary,
- "target_videos": target_videos,
- "trigger_label": trigger_label,
- "status": status,
- "status_label": _walk_status_label(status),
- "operator_display_status": operator_display_status,
- "from_label": from_label,
- "result": result_label,
- "reason": reason,
- "reason_label": reason_label,
- "impact": _walk_impact(status, reason, gate_reason),
- "score_items": score_items,
- "summary": _walk_step_summary(edge, status, source_video, trigger_label, result_label, reason_label),
- "detail_lines": _walk_detail_lines(action, edge, from_label, trigger_label, score_items, result_label),
- "debug": {"action": action} if debug else None,
- }
- def _summary(
- self,
- bundle: dict[str, Any],
- rows: list[dict[str, Any]],
- extension_queries: list[dict[str, Any]],
- media_counts: Counter[str],
- action_counts: Counter[str],
- gemini_counts: Counter[str],
- ) -> dict[str, Any]:
- query_failures = _query_failures(bundle["queries"])
- # 游走指标(run 级):游走带回视频、其中入池、向外游走次数、最深层数。
- walk_query_ids = _walk_query_ids(bundle)
- walk_items = [c for c in bundle["content_items"] if not _is_first_round_content(c, walk_query_ids)]
- pooled_targets = {
- _text(d.get("decision_target_id"))
- for d in bundle["decisions"]
- if _decision_action(d) == "ADD_TO_CONTENT_POOL"
- }
- walk_pooled = sum(1 for c in walk_items if _text(c.get("platform_content_id")) in pooled_targets)
- expansion = [a for a in bundle["walk_actions"] if _text(a.get("edge_id")) in _WALK_EXPANSION_EDGES]
- walk_max_depth = max((_number(a.get("depth")) or 0 for a in bundle["walk_actions"]), default=0)
- return {
- "first_round_query_count": len(rows),
- "walk_video_total": len(walk_items),
- "walk_pooled_count": walk_pooled,
- "walk_action_total": len(expansion),
- "walk_action_success": sum(1 for a in expansion if _text(a.get("walk_status")) == "success"),
- "walk_max_depth": walk_max_depth,
- "extension_query_count": len(extension_queries),
- "all_query_count": len(bundle["queries"]),
- "query_failure_count": len(query_failures),
- "query_failure_examples": [
- {
- "search_query_id": _text(query.get("search_query_id")),
- "search_query": _text(query.get("search_query")),
- "message": _text(_query_failure(query).get("message")),
- "error_code": _text(_query_failure(query).get("error_code")),
- }
- for query in query_failures[:5]
- ],
- "content_count": len(bundle["content_items"]),
- "oss_uploaded_count": media_counts["oss_uploaded"],
- "oss_pending_count": media_counts["oss_upload_pending"],
- "oss_failed_count": media_counts["oss_upload_failed"],
- "gemini_completed_count": gemini_counts["ok"] + gemini_counts["success"] + gemini_counts["passed"],
- "gemini_failed_count": gemini_counts["failed"],
- "pool_count": action_counts["ADD_TO_CONTENT_POOL"],
- "review_count": action_counts["KEEP_CONTENT_FOR_REVIEW"],
- "technical_retry_count": action_counts["TECHNICAL_RETRY_REQUIRED"],
- "reject_count": action_counts["REJECT_CONTENT"],
- }
- def _query_method(query: dict[str, Any]) -> str:
- return _text(query.get("search_query_generation_method"), "unknown")
- def _query_failure(query: dict[str, Any]) -> dict[str, Any]:
- failure = query.get("query_failure")
- if isinstance(failure, dict):
- return failure
- raw_failure = _record(query.get("raw_payload")).get("query_failure")
- return raw_failure if isinstance(raw_failure, dict) else {}
- def _query_failures(queries: list[dict[str, Any]]) -> list[dict[str, Any]]:
- return [query for query in queries if _query_failure(query)]
- def _content_belongs_to_query(content: dict[str, Any], query_id: str) -> bool:
- if not query_id:
- return False
- if _text(content.get("search_query_id")) == query_id:
- return True
- if query_id in _text_list(content.get("matched_search_query_ids")):
- return True
- raw_payload = _record(content.get("raw_payload"))
- if query_id in _text_list(raw_payload.get("matched_search_query_ids")):
- return True
- for source in _list(content.get("query_sources")) + _list(raw_payload.get("query_sources")):
- if _text(source.get("search_query_id")) == query_id:
- return True
- return False
- def _is_first_round_content(content: dict[str, Any], walk_query_ids: set[str] | None = None) -> bool:
- # 优先用「内容所属 query 是不是扩展(标签/作者)query」判定——这条 query→内容 的关联即使在
- # labeling 修复前的旧 run 里也正确(旧 run 把 previous_discovery_step 误标成 search_query_direct)。
- if walk_query_ids and _text(content.get("search_query_id")) in walk_query_ids:
- return False
- previous = _text(content.get("previous_discovery_step") or _record(content.get("raw_payload")).get("previous_discovery_step"))
- if not previous:
- return True
- return not any(token in previous for token in ["tag", "hashtag", "author", "next_page", "walk", "pagination"])
- def _walk_query_ids(bundle: dict[str, Any]) -> set[str]:
- return {
- _text(query.get("search_query_id"))
- for query in bundle["queries"]
- if _query_method(query) in EXTENSION_QUERY_METHODS and _text(query.get("search_query_id"))
- }
- def _content_ids(content: dict[str, Any]) -> set[str]:
- return {
- _text(content.get("content_discovery_id")),
- _text(content.get("platform_content_id")),
- _text(content.get("decision_target_id")),
- }
- def _decision_for_content(content: dict[str, Any], decisions: list[dict[str, Any]]) -> dict[str, Any] | None:
- ids = _content_ids(content)
- return next((decision for decision in decisions if _text(decision.get("decision_target_id")) in ids), None)
- def _media_for_content(content: dict[str, Any], media_rows: list[dict[str, Any]]) -> dict[str, Any] | None:
- platform_id = _text(content.get("platform_content_id"))
- return next((media for media in media_rows if _text(media.get("platform_content_id")) == platform_id), None)
- def _evidence_for_content(content: dict[str, Any], evidence_rows: list[dict[str, Any]]) -> dict[str, Any] | None:
- platform_id = _text(content.get("platform_content_id"))
- discovery_id = _text(content.get("content_discovery_id"))
- return next(
- (
- evidence
- for evidence in evidence_rows
- if _text(evidence.get("platform_content_id")) == platform_id
- or _text(evidence.get("content_discovery_id")) == discovery_id
- ),
- None,
- )
- def _walk_out_counter(bundle: dict[str, Any]) -> dict[str, int]:
- # 每个源视频 platform_content_id → 它成功向外游走(标签/作者)的次数;0 表示这条没真正走过。
- counter: Counter[str] = Counter()
- for action in bundle["walk_actions"]:
- if _text(action.get("edge_id")) not in _WALK_EXPANSION_EDGES:
- continue
- if _text(action.get("walk_status")) != "success":
- continue
- source = _content_for_action(action, bundle)
- platform_id = _text((source or {}).get("platform_content_id"))
- if platform_id:
- counter[platform_id] += 1
- return dict(counter)
- def _load_walk_caps() -> dict[str, Any]:
- # 游走预算上限(best-effort,只读 walk_policy.json;env 覆盖时数值可能与真实 run 略有出入)。
- path = os.path.join("tech_documents", "数据接口与来源", "walk_policy.json")
- try:
- with open(path, encoding="utf-8") as handle:
- data = json.load(handle)
- except Exception:
- return {}
- def _value(node: Any) -> Any:
- return node.get("value") if isinstance(node, dict) else node
- global_caps = data.get("global", {}) if isinstance(data.get("global"), dict) else {}
- edges = {
- _text(edge.get("edge_id")): edge
- for edge in data.get("edge_budgets", [])
- if isinstance(edge, dict)
- }
- return {
- "max_depth": _value(global_caps.get("max_depth")),
- "hashtag_to_query": (edges.get("hashtag_to_query") or {}).get("max_total_actions"),
- "author_to_works": (edges.get("author_to_works") or {}).get("max_total_actions"),
- }
- def _walk_budget_overview(
- bundle: dict[str, Any],
- actions: list[dict[str, Any]],
- extension_queries: list[dict[str, Any]],
- ) -> dict[str, Any]:
- caps = _load_walk_caps()
- statuses = Counter(_text(action.get("status"), "unknown") for action in actions)
- tag_used = sum(1 for a in actions if a.get("edge_id") == "hashtag_to_query" and a.get("status") == "success")
- author_used = sum(1 for a in actions if a.get("edge_id") == "author_to_works" and a.get("status") == "success")
- walk_query_ids = _walk_query_ids(bundle)
- walk_items = [c for c in bundle["content_items"] if not _is_first_round_content(c, walk_query_ids)]
- return {
- "total_actions": len(actions),
- "success_count": statuses["success"],
- "skipped_count": statuses["skipped"],
- "extension_query_count": len(extension_queries),
- "tag_used": tag_used,
- "tag_cap": caps.get("hashtag_to_query"),
- "author_used": author_used,
- "author_cap": caps.get("author_to_works"),
- "max_depth_reached": max((_number(a.get("depth")) or 0 for a in actions), default=0),
- "max_depth_cap": caps.get("max_depth"),
- "walk_video_total": len(walk_items),
- }
- def _walk_action_related(action: dict[str, Any], query_id: str, related_ids: set[str]) -> bool:
- values = {
- _text(action.get("from_node_id")),
- _text(action.get("to_node_id")),
- _text(action.get("decision_target_id")),
- }
- raw = _record(action.get("raw_payload"))
- values.update({
- _text(raw.get("parent_search_query_id")),
- _text(raw.get("search_query_id")),
- _text(raw.get("decision_target_id")),
- })
- return query_id in values or bool(values & related_ids)
- def _walk_lane_id(action: dict[str, Any]) -> str:
- return _text(action.get("edge_id") or action.get("edge_type"), "unknown")
- def _walk_lane(edge: str) -> str:
- if edge in {"hashtag_to_query", "tag_query"}:
- return "tag_query"
- if edge in {"author_to_works", "video_to_author", "author_works"}:
- return "author_works"
- return "path_stop"
- def _source_path(path: dict[str, Any], *, debug: bool) -> dict[str, Any]:
- return {
- "type": _text(path.get("source_path_type")),
- "label": "内容来源路径",
- "status": _text(path.get("status"), "unknown"),
- "debug": {"source_path": path} if debug else None,
- }
- def _decision_action(decision: dict[str, Any]) -> str:
- action = _text(decision.get("decision_action") or decision.get("action"), "NOT_JUDGED")
- if action == "ADD_CONTENT_TO_POOL":
- return "ADD_TO_CONTENT_POOL"
- return action
- def _media_status(media: dict[str, Any] | None) -> str:
- if not media:
- return "unavailable"
- return _text(media.get("content_media_status"), "metadata_only")
- def _media_failure_reason(media: dict[str, Any] | None) -> str:
- if not media:
- return ""
- raw = _record(media.get("raw_payload"))
- return _text(media.get("failure_reason") or raw.get("upload_failure_reason") or raw.get("failure_reason"))
- def _is_content_inspection_blocked(
- detail: dict[str, Any] | None,
- evidence: dict[str, Any] | None = None,
- ) -> bool:
- # 内容审核拦截:新数据 failure_type=content_inspection_blocked;旧数据从响应体文本兜底(DataInspectionFailed)。
- if isinstance(detail, dict) and _text(detail.get("failure_type")) == "content_inspection_blocked":
- return True
- evidence_raw = _record((evidence or {}).get("raw_payload"))
- if _text(evidence_raw.get("failure_type")) == "content_inspection_blocked":
- return True
- texts = [
- _text(_record(evidence_raw.get("response_body_summary")).get("text_excerpt")),
- _text(_record(_record((detail or {}).get("openrouter")).get("response_summary")).get("text_excerpt")),
- ]
- blob = " ".join(texts).lower()
- return "datainspection" in blob or "inappropriate content" in blob
- def _technical_retry_detail(
- decision: dict[str, Any] | None,
- evidence: dict[str, Any] | None,
- media: dict[str, Any] | None,
- ) -> dict[str, Any] | None:
- if _decision_action(decision or {}) != "TECHNICAL_RETRY_REQUIRED":
- return None
- scorecard = _record((decision or {}).get("scorecard"))
- replay_data = _record((decision or {}).get("decision_replay_data"))
- evidence_summary = _record((evidence or {}).get("evidence_summary"))
- evidence_raw = _record((evidence or {}).get("raw_payload"))
- media_raw = _record((media or {}).get("raw_payload"))
- merged = {**scorecard, **replay_data, **evidence_summary, **evidence_raw}
- failure_type = _text(
- merged.get("failure_type")
- or media_raw.get("oss_archive_last_error")
- or media_raw.get("failure_reason")
- or _media_failure_reason(media)
- )
- if not failure_type:
- return None
- timing = _record(merged.get("timing_metrics"))
- video_fetch = _record(timing.get("video_fetch"))
- gemini_request = _record(timing.get("gemini_request"))
- attempts = [
- {
- "attempt": item.get("attempt"),
- "status": _text(item.get("status")),
- "duration_seconds": _seconds(item.get("duration_ms")),
- "failure_type": _text(item.get("failure_type")),
- "exception_type": _text(item.get("exception_type")),
- "http_status_code": item.get("http_status_code"),
- "response_body_summary": _record(item.get("response_body_summary")),
- }
- for item in _list(gemini_request.get("attempts"))
- ]
- detail = {
- "brief_reason": _technical_retry_brief_reason(failure_type, merged, media_raw),
- "stage": _technical_retry_stage(failure_type),
- "stage_label": _technical_retry_stage_label(failure_type),
- "failure_type": failure_type,
- "failure_label": _technical_retry_failure_label(failure_type),
- "exception_type": _text(merged.get("exception_type")),
- "http_status_code": merged.get("http_status_code"),
- "error_message": _text(merged.get("error_message")),
- "retry_count": merged.get("retry_count"),
- "video_source": _text(video_fetch.get("gemini_video_source")),
- "timings": {
- "download_seconds": _seconds(video_fetch.get("download_duration_ms")),
- "ffmpeg_seconds": _seconds(video_fetch.get("ffmpeg_duration_ms")),
- "video_fetch_total_seconds": _seconds(video_fetch.get("total_duration_ms")),
- "gemini_seconds": _seconds(gemini_request.get("total_duration_ms")),
- },
- "sizes": {
- "downloaded_mb": _megabytes(video_fetch.get("downloaded_bytes")),
- "compressed_mb": _megabytes(video_fetch.get("compressed_bytes")),
- },
- "attempts": attempts,
- "openrouter": {
- "response_summary": _record(merged.get("response_body_summary")),
- },
- "oss": {
- "status": _text((media or {}).get("content_media_status")),
- "last_error": _text(media_raw.get("oss_archive_last_error") or media_raw.get("failure_reason")),
- "last_exception_type": _text(media_raw.get("oss_archive_last_exception_type")),
- "last_http_status_code": media_raw.get("oss_archive_last_http_status_code"),
- "attempt_count": media_raw.get("oss_archive_attempt_count"),
- "duration_seconds": _seconds(_record(media_raw.get("oss_timing_metrics")).get("oss_upload_duration_ms")),
- "payload_mode": _text(media_raw.get("oss_payload_mode")),
- "response_summary": _record(media_raw.get("oss_response_summary")),
- },
- "video_url": {
- "selected_path": _text(media_raw.get("selected_video_url_path")),
- "selected_host": _text(media_raw.get("selected_video_url_host")),
- "probe_status": _text(media_raw.get("selected_video_url_probe_status")),
- "content_type": _text(media_raw.get("selected_video_url_content_type")),
- "content_range": _text(media_raw.get("selected_video_url_content_range")),
- "candidate_counts": _record(media_raw.get("video_url_candidate_counts")),
- "reject_reasons": _record(media_raw.get("video_url_reject_reasons")),
- },
- }
- return detail
- def _technical_retry_brief_reason(
- failure_type: str,
- payload: dict[str, Any],
- media_raw: dict[str, Any],
- ) -> str:
- if failure_type == "content_inspection_blocked":
- return "视频被大模型内容安全审核拦截(非系统故障,重试无效)"
- if failure_type in {"portrait_unavailable", "portrait_incomplete"}:
- reason = _text(payload.get("fifty_plus_failure_reason"))
- base = "拉不到作者 50+ 画像(热点宝)" if failure_type == "portrait_unavailable" else "作者 50+ 画像数据不全"
- return f"{base}:{reason}" if reason else base
- if failure_type == "no_valid_play_url":
- return "平台结果未找到可用正片 URL"
- if failure_type == "no_play_url":
- return "平台结果没有返回可用 play_url"
- if failure_type == "qwen_client_timeout":
- return "通义千问请求写入超时"
- if failure_type == "qwen_http_error":
- status = payload.get("http_status_code")
- return f"通义千问返回 HTTP {status}" if status else "通义千问请求失败"
- if failure_type == "qwen_response_invalid":
- return "通义千问返回格式无法解析"
- if failure_type == "gemini_client_timeout":
- return "OpenRouter/Gemini 请求写入超时"
- if failure_type == "gemini_http_error":
- status = payload.get("http_status_code")
- return f"OpenRouter/Gemini 返回 HTTP {status}" if status else "OpenRouter/Gemini 请求失败"
- if failure_type == "gemini_response_invalid":
- return "OpenRouter/Gemini 返回格式无法解析"
- if failure_type == "video_fetch_failed":
- return "视频下载或压缩失败"
- if failure_type == "video_judge_timeout":
- return "视频判定 worker 超时未返回(已超兜底上限,跳过本条)"
- if failure_type == "oss_worker_timeout":
- return "OSS 归档 worker 超时未返回(已超兜底上限,跳过本条)"
- if failure_type.startswith("oss_"):
- return "OSS 转存未拿到可用视频地址"
- if media_raw.get("oss_archive_last_error"):
- return "OSS 归档失败"
- return failure_type
- def _technical_retry_stage(failure_type: str) -> str:
- if failure_type == "content_inspection_blocked":
- return "content_inspection"
- if failure_type == "video_judge_timeout":
- return "video_judge"
- if failure_type.startswith("portrait"):
- return "portrait"
- if failure_type.startswith("oss_"):
- return "oss"
- if failure_type in {"no_play_url", "no_valid_play_url"}:
- return "play_url"
- if "ffmpeg" in failure_type:
- return "ffmpeg"
- if failure_type in {"video_fetch_failed", "video_download_failed"}:
- return "video_download"
- if failure_type.startswith("qwen_"):
- return "dashscope"
- if failure_type.startswith("gemini_"):
- return "openrouter"
- return "unknown"
- def _technical_retry_stage_label(failure_type: str) -> str:
- return {
- "oss": "OSS",
- "play_url": "平台 play_url",
- "video_download": "视频下载",
- "ffmpeg": "ffmpeg",
- "dashscope": "通义千问",
- "openrouter": "OpenRouter/Gemini",
- "content_inspection": "内容审核拦截",
- "portrait": "热点宝画像",
- "video_judge": "视频判定调度",
- "unknown": "未知阶段",
- }[_technical_retry_stage(failure_type)]
- def _technical_retry_failure_label(failure_type: str) -> str:
- return {
- "content_inspection_blocked": "模型内容审核拦截",
- "qwen_client_timeout": "通义千问 客户端超时",
- "qwen_http_error": "通义千问 HTTP 错误",
- "qwen_response_invalid": "通义千问 响应无法解析",
- "gemini_client_timeout": "Gemini 客户端超时",
- "gemini_http_error": "Gemini HTTP 错误",
- "gemini_response_invalid": "Gemini 响应无法解析",
- "video_fetch_failed": "视频获取失败",
- "no_play_url": "缺少 play_url",
- "no_valid_play_url": "未找到可用正片 URL",
- "oss_upload_response_invalid": "OSS 响应无效",
- "oss_upload_http_error": "OSS HTTP 错误",
- "oss_worker_timeout": "OSS 归档 worker 超时",
- "portrait_unavailable": "拉不到作者 50+ 画像",
- "portrait_incomplete": "作者 50+ 画像数据不全",
- "video_judge_timeout": "视频判定调度超时",
- }.get(failure_type, failure_type)
- def _seconds(value: Any) -> float | None:
- try:
- return round(float(value) / 1000.0, 3)
- except (TypeError, ValueError):
- return None
- def _megabytes(value: Any) -> float | None:
- try:
- return round(float(value) / 1024.0 / 1024.0, 2)
- except (TypeError, ValueError):
- return None
- def _rule_headline(actions: Counter[str], total: int) -> str:
- if not total:
- return "首轮内容暂未进入判断"
- if actions["ADD_TO_CONTENT_POOL"]:
- return f"有 {actions['ADD_TO_CONTENT_POOL']} 条内容入池"
- if actions["KEEP_CONTENT_FOR_REVIEW"]:
- return f"有 {actions['KEEP_CONTENT_FOR_REVIEW']} 条内容待复看"
- if actions["TECHNICAL_RETRY_REQUIRED"]:
- return f"有 {actions['TECHNICAL_RETRY_REQUIRED']} 条内容出现技术问题"
- return "本轮没有可沉淀内容"
- def _asset_headline(
- pool_count: int,
- review_count: int,
- technical_retry_count: int,
- reject_count: int,
- ) -> str:
- parts = []
- if pool_count:
- parts.append(f"入池 {pool_count} 条")
- if review_count:
- parts.append(f"待复看 {review_count} 条")
- if technical_retry_count:
- parts.append(f"技术问题 {technical_retry_count} 条")
- if parts:
- return ",".join(parts)
- if reject_count:
- return f"没有入池,淘汰 {reject_count} 条"
- return "暂无可沉淀内容"
- def _effect_label(status: str) -> str:
- return {
- "success": "已找到可用结果",
- "rule_blocked": "找到结果,但未通过内容判断",
- "pending": "处理中",
- "failed": "搜索失败",
- "skipped": "已跳过",
- }.get(status, "处理状态待确认")
- def _walk_status_label(status: str) -> str:
- return {
- "success": "已执行",
- "skipped": "未继续",
- "failed": "执行失败",
- "pending": "处理中",
- }.get(status, "状态待确认")
- def _action_value(action: dict[str, Any], key: str) -> Any:
- raw = _record(action.get("raw_payload"))
- return action.get(key) if action.get(key) not in (None, "") else raw.get(key)
- def _find_content_by_id(content_id: str, bundle: dict[str, Any]) -> dict[str, Any] | None:
- if not content_id:
- return None
- return next((item for item in bundle["content_items"] if content_id in _content_ids(item)), None)
- def _decision_by_id(decision_id: str, bundle: dict[str, Any]) -> dict[str, Any] | None:
- if not decision_id:
- return None
- return next((item for item in bundle["decisions"] if _text(item.get("decision_id")) == decision_id), None)
- def _decision_for_action(action: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any] | None:
- decision_id = _text(_action_value(action, "decision_id"))
- if not decision_id and _text(_action_value(action, "from_node_type")).lower() == "ruledecision":
- decision_id = _text(_action_value(action, "from_node_id"))
- return _decision_by_id(decision_id, bundle)
- def _content_for_action(action: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any] | None:
- candidate_ids = [
- _text(_action_value(action, "decision_target_id")),
- _text(_action_value(action, "to_node_id")),
- _text(_action_value(action, "from_node_id")),
- ]
- for candidate_id in candidate_ids:
- content = _find_content_by_id(candidate_id, bundle)
- if content:
- return content
- decision = _decision_for_action(action, bundle)
- if decision:
- return _find_content_by_id(_text(decision.get("decision_target_id")), bundle)
- return None
- def _target_query_for_action(action: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any] | None:
- query_id = _text(_action_value(action, "to_node_id"))
- return next((item for item in bundle["queries"] if _text(item.get("search_query_id")) == query_id), None)
- def _target_contents_for_action(
- action: dict[str, Any],
- bundle: dict[str, Any],
- source_content: dict[str, Any] | None,
- target_query: dict[str, Any] | None,
- ) -> list[dict[str, Any]]:
- if target_query:
- query_id = _text(target_query.get("search_query_id"))
- return [item for item in bundle["content_items"] if _content_belongs_to_query(item, query_id)]
- edge = _walk_lane_id(action)
- if edge in {"author_to_works", "video_to_author", "author_works"}:
- author_id = _text(_action_value(action, "author_id") or _action_value(action, "from_node_id") or (source_content or {}).get("platform_author_id"))
- if not author_id:
- return []
- return [
- item
- for item in bundle["content_items"]
- if _text(item.get("platform_author_id")) == author_id
- and "author" in _text(item.get("previous_discovery_step") or _record(item.get("raw_payload")).get("previous_discovery_step"))
- ]
- target_content = _find_content_by_id(_text(_action_value(action, "to_node_id")), bundle)
- return [target_content] if target_content else []
- def _walk_from_label(
- action: dict[str, Any],
- query: dict[str, Any] | None,
- bundle: dict[str, Any],
- source_content: dict[str, Any] | None = None,
- ) -> str:
- from_type = _text(action.get("from_node_type")).lower()
- from_node_id = _text(action.get("from_node_id"))
- edge = _walk_lane_id(action)
- if edge in {"hashtag_to_query", "tag_query", "author_to_works", "video_to_author", "author_works"} and source_content:
- title = _text(source_content.get("description") or source_content.get("title"))
- return f"从视频《{_short_text(title, 34)}》触发" if title else "从视频内容触发"
- if "query" in from_type:
- source_query = next((item for item in bundle["queries"] if _text(item.get("search_query_id")) == from_node_id), None) or query
- return f"从搜索词《{_text((source_query or {}).get('search_query'), '搜索词待确认')}》继续"
- if "ruledecision" in from_type:
- content = _content_for_action(action, bundle)
- title = _text((content or {}).get("description") or (content or {}).get("title"))
- return f"判断视频《{_short_text(title, 34)}》后触发" if title else "由内容判断结果触发"
- if "content" in from_type or "video" in from_type or source_content:
- title = _text((source_content or {}).get("description") or (source_content or {}).get("title"))
- return f"从视频《{_short_text(title, 34)}》触发" if title else "从视频内容触发"
- if "author" in from_type:
- content = _content_for_action(action, bundle)
- author = _text((content or {}).get("author_display_name") or from_node_id, "作者待确认")
- title = _text((content or {}).get("description") or (content or {}).get("title"))
- return f"从作者《{author}》触发,来源视频《{_short_text(title, 24)}》" if title else f"从作者《{author}》触发"
- if "tag" in from_type:
- return "从视频标签触发"
- return "从上一环节出发"
- def _walk_impact(status: str, reason: str, gate_reason: str) -> str:
- if reason == "blocked_by_rule_decision" or gate_reason == "v4_allow_walk_denied":
- return "这条内容不会继续带出更多视频。"
- if reason == "budget_exhausted":
- return "达到游走次数上限,这一步不会继续新增搜索词。"
- if status == "success":
- return "这一步已经执行,结果会进入后续判断。"
- if status == "skipped":
- return "这一步没有继续执行,不影响最终沉淀。"
- return "这一步暂不影响最终沉淀。"
- def _walk_operator_display_status(status: str, reason: str) -> str:
- return "budget_blocked" if status == "skipped" and reason == "budget_exhausted" else status
- def _walk_trigger_label(
- action: dict[str, Any],
- source_content: dict[str, Any] | None = None,
- target_query: dict[str, Any] | None = None,
- ) -> str:
- hashtag = _text(_action_value(action, "hashtag"))
- if hashtag:
- return f"标签:#{hashtag.lstrip('#')}"
- edge = _walk_lane_id(action)
- if edge in {"hashtag_to_query", "tag_query"} and target_query:
- return f"标签搜索词:{_text(target_query.get('text'), '搜索词待确认')}"
- if edge in {"hashtag_to_query", "tag_query"} and source_content:
- tags = _text_list(source_content.get("tags"))
- return f"候选标签:{' / '.join(tags[:3])}" if tags else "标签未生成:这条视频未达到扩展门槛"
- author = _text(_action_value(action, "author_id") or _action_value(action, "platform_author_id"))
- if not author and edge in {"author_to_works", "video_to_author", "author_works"} and source_content:
- author = _text(source_content.get("author_display_name") or source_content.get("platform_author_id"))
- if author:
- return f"作者:{author}"
- if edge in {"author_to_works", "video_to_author", "author_works"}:
- return "作者:未进入作者作品扩展"
- cursor = _text(_action_value(action, "page_cursor") or _action_value(action, "cursor"))
- if cursor:
- return f"翻下一页:游标 {cursor}"
- to_node = _text(_action_value(action, "to_node_id"))
- if source_content and edge in {"path_stop", "decision_to_asset", "budget_downgrade"}:
- return f"视频:{_short_text(_text(source_content.get('description') or source_content.get('title'), '视频待确认'), 34)}"
- if to_node and to_node not in {"tag_query_skipped", "author_works_skipped"}:
- return f"结果对象:{to_node}"
- return "触发对象待确认"
- def _walk_reason_label(action: dict[str, Any], reason: str) -> str:
- gate_reason = _text(_action_value(action, "walk_gate_reason_code") or _action_value(action, "allow_walk_reason"))
- if gate_reason == "v4_allow_walk_denied":
- allow_reason = _text(_action_value(action, "allow_walk_reason"))
- if allow_reason and allow_reason != gate_reason:
- return f"不适合继续向外扩展:{_reason_label(allow_reason, '评分未达到继续扩展门槛')}"
- return "不适合继续向外扩展"
- if reason:
- return _reason_label(reason, "这一步没有记录额外原因")
- return "这一步没有记录额外原因"
- def _walk_result_label(
- action: dict[str, Any],
- edge: str,
- status: str,
- reason: str,
- source_video: dict[str, Any] | None,
- target_query: dict[str, Any] | None,
- target_videos: list[dict[str, Any]],
- ) -> str:
- if edge == "decision_to_asset":
- return "这条视频已进入内容池。"
- if edge == "path_stop":
- return "这条视频到这里停止,不再向外扩展。"
- if edge == "budget_downgrade":
- return "这条视频进入待复看,先不作为强信号继续扩展。"
- if status == "skipped" and reason == "budget_exhausted":
- return "内容达到扩展门槛,但已经达到游走次数上限。"
- if status == "skipped":
- return "没有继续扩展。"
- if edge in {"hashtag_to_query", "tag_query"}:
- query_text = _text((target_query or {}).get("text"))
- return f"已生成标签搜索词《{query_text}》,带回 {len(target_videos)} 条视频。" if query_text else f"已从视频标签继续游走,带回 {len(target_videos)} 条视频。"
- if edge in {"author_to_works", "video_to_author", "author_works"}:
- author = _text((source_video or {}).get("author"), "该作者")
- return f"已查看作者《{author}》更多作品,带回 {len(target_videos)} 条视频。"
- return "这一步已经执行,结果会进入后续判断。"
- def _walk_step_summary(
- edge: str,
- status: str,
- source_video: dict[str, Any] | None,
- trigger_label: str,
- result_label: str,
- reason_label: str,
- ) -> str:
- title = _text((source_video or {}).get("title"))
- if edge in {"hashtag_to_query", "tag_query"}:
- action = "从视频标签继续游走"
- elif edge in {"author_to_works", "video_to_author", "author_works"}:
- action = "从作者继续游走"
- elif edge == "decision_to_asset":
- action = "入池沉淀"
- elif edge == "budget_downgrade":
- action = "转入待复看"
- elif edge == "path_stop":
- action = "停止这条线"
- else:
- action = WALK_EDGE_LABELS.get(edge, "扩展动作")
- prefix = f"从视频《{_short_text(title, 28)}》" if title else "从当前搜索词"
- if status == "success":
- return f"{prefix}{action}:{result_label}"
- return f"{prefix}{action}:{reason_label}"
- def _walk_detail_lines(
- action: dict[str, Any],
- edge: str,
- from_label: str,
- trigger_label: str,
- score_items: list[dict[str, Any]],
- result_label: str,
- ) -> list[str]:
- lines = [
- f"动作:{WALK_EDGE_LABELS.get(edge, '扩展动作')}",
- f"起点:{from_label}",
- ]
- if trigger_label != "触发对象待确认":
- lines.append(f"扩展对象:{trigger_label}")
- if score_items:
- lines.append("扩展门槛:" + " / ".join(f"{item['label']} {item['score_label']}" for item in score_items[:3]))
- reason = _walk_reason_label(action, _text(_action_value(action, "reason_code")))
- if reason != "这一步没有记录额外原因":
- lines.append(f"判断:{reason}")
- if result_label:
- lines.append(f"结果:{result_label}")
- return lines
- def _walk_score_items(action: dict[str, Any]) -> list[dict[str, Any]]:
- snapshot = _record(action.get("walk_gate_snapshot") or _record(action.get("raw_payload")).get("walk_gate_snapshot"))
- return _score_items_from_values(
- total=snapshot.get("score"),
- query=snapshot.get("query_relevance_score"),
- platform=snapshot.get("platform_performance_score"),
- components=[],
- )
- def _decision_score_items(scorecard: dict[str, Any], score: Any) -> list[dict[str, Any]]:
- weights, fifty = _v4_display_weights(scorecard)
- return _score_items_from_values(
- total=score if score is not None else scorecard.get("total_score"),
- query=scorecard.get("query_relevance_score"),
- platform=scorecard.get("platform_performance_score"),
- components=_list(scorecard.get("platform_performance_components")),
- weights=weights,
- fifty=fifty,
- )
- def _v4_display_weights(scorecard: dict[str, Any]) -> tuple[dict[str, float], dict[str, Any] | None]:
- """展示 evaluator 已落在 scorecard 的权重;旧记录无 score_weights 时保留历史推断。"""
- weights = _display_weights_from_scorecard(scorecard)
- if "fifty_plus_status" not in scorecard:
- return weights or {"query": 0.5, "platform": 0.5}, None
- status = scorecard.get("fifty_plus_status")
- fifty: dict[str, Any] = {"status": status}
- if status == "ok":
- fifty["score"] = scorecard.get("fifty_plus_score")
- fifty["components"] = _record(scorecard.get("fifty_plus_components"))
- return weights or {"query": 0.35, "platform": 0.35, "fifty": 0.30}, fifty
- def _display_weights_from_scorecard(scorecard: dict[str, Any]) -> dict[str, float]:
- weights = scorecard.get("score_weights")
- if not isinstance(weights, dict):
- return {}
- display = {}
- mapping = {
- "query_relevance": "query",
- "platform_performance": "platform",
- "fifty_plus": "fifty",
- }
- for source_key, target_key in mapping.items():
- value = weights.get(source_key)
- if isinstance(value, (int, float)) and not isinstance(value, bool):
- display[target_key] = float(value)
- return display
- def _fifty_status_note(status: Any) -> str:
- return {
- "not_attempted": "相关性未达标(query<65),未拉取画像",
- "unavailable": "作者画像拉取失败,未评估",
- "incomplete": "作者画像数据不全,未评估",
- }.get(str(status), "未评估")
- def _score_summary(decisions: list[dict[str, Any]]) -> dict[str, Any]:
- scorecards = [_record(item.get("scorecard")) for item in decisions]
- return {
- "items": [
- _avg_score_item("总分", [item.get("score") if item.get("score") is not None else _record(item.get("scorecard")).get("total_score") for item in decisions]),
- _avg_score_item("是否契合需求", [item.get("query_relevance_score") for item in scorecards]),
- _avg_score_item("平台表现", [item.get("platform_performance_score") for item in scorecards]),
- ],
- }
- def _score_items_from_values(
- total: Any,
- query: Any,
- platform: Any,
- components: list[dict[str, Any]],
- *,
- weights: dict[str, float] | None = None,
- fifty: dict[str, Any] | None = None,
- ) -> list[dict[str, Any]]:
- weights = weights or {}
- items = [
- _score_item("总分", total, "最终用于入池 / 复看 / 淘汰的总分"),
- _score_item("是否契合需求", query, "视频内容和本次搜索词、需求的匹配程度", weight=weights.get("query"), group="main"),
- _score_item("平台表现", platform, "点赞、评论、转发、收藏等互动表现", weight=weights.get("platform"), group="main"),
- ]
- if fifty is not None:
- # 50+老年 维度卡(仅抖音):status=ok 出真实分,否则出灰卡「未评估」。
- if fifty.get("status") == "ok":
- items.append(_score_item(
- "50+老年受众", fifty.get("score"), "作者粉丝 50+ 画像(TGI × 占比)",
- weight=weights.get("fifty"), group="main", status="ok",
- ))
- else:
- items.append(_score_item(
- "50+老年受众", None, _fifty_status_note(fifty.get("status")),
- weight=weights.get("fifty"), group="main", status=fifty.get("status"), allow_none=True,
- ))
- for component in components:
- if component.get("type"):
- # M11:新打分 component 自带 label(中文)+ type(absolute/ratio)
- label = _text(component.get("label")) or _platform_component_label(_text(component.get("field")))
- detail = _platform_component_detail(component)
- else:
- # 旧 run 的 component(无 type):沿用绝对值展示
- field = _text(component.get("field"))
- label = _platform_component_label(field)
- detail = _platform_component_raw_text(field, component.get("value"))
- items.append(_score_item(
- label,
- component.get("normalized_score"),
- detail,
- weight=_float_or_none(component.get("weight")),
- group="platform",
- ))
- if fifty is not None and fifty.get("status") == "ok":
- # 50+老年怎么算:TGI 表现(60%)+ 占比表现(40%),取自 fifty_plus_components。
- comps = _record(fifty.get("components"))
- items.append(_score_item(
- "TGI 表现", comps.get("tgi_score"),
- f"50+ 粉丝 TGI≈{_num_text(comps.get('band_tgi'))}(>100 即老年粉丝偏好高于大盘;TGI 200=满分,按此折算得分)",
- weight=0.6, group="fifty",
- ))
- items.append(_score_item(
- "占比表现", comps.get("percent_score"), f"50+ 粉丝占比≈{_num_text(comps.get('band_percent'))}%",
- weight=0.4, group="fifty",
- ))
- return [item for item in items if item]
- def _score_item(
- label: str,
- value: Any,
- detail: str,
- *,
- weight: Any = None,
- group: str | None = None,
- status: Any = None,
- allow_none: bool = False,
- ) -> dict[str, Any]:
- score = _float_or_none(value)
- if score is None and not allow_none:
- return {}
- item: dict[str, Any] = {
- "label": label,
- "score": round(score, 2) if score is not None else None,
- "score_label": f"{round(score, 1):g} 分" if score is not None else "未评估",
- "detail": detail,
- }
- weight_num = _float_or_none(weight)
- if weight_num is not None:
- item["weight"] = round(weight_num, 4)
- if group:
- item["group"] = group
- if status is not None:
- item["status"] = str(status)
- return item
- def _num_text(value: Any) -> str:
- number = _float_or_none(value)
- return f"{round(number):g}" if number is not None else "—"
- def _avg_score_item(label: str, values: list[Any]) -> dict[str, Any]:
- nums = [value for value in (_float_or_none(item) for item in values) if value is not None]
- if not nums:
- return {"label": label, "score": None, "score_label": "暂无评分"}
- avg = sum(nums) / len(nums)
- return {
- "label": label,
- "score": round(avg, 2),
- "score_label": f"平均 {round(avg, 1):g} 分",
- }
- def _platform_component_label(field: str) -> str:
- return {
- "statistics.digg_count": "点赞表现",
- "statistics.like_count": "点赞表现",
- "statistics.comment_count": "评论表现",
- "statistics.share_count": "转发表现",
- "statistics.collect_count": "收藏表现",
- "statistics.play_count": "播放表现",
- }.get(field, field or "互动表现")
- def _platform_component_raw_text(field: str, value: Any) -> str:
- """该视频这项指标的原始量,自然语言(如「该视频原始 1784 赞」)。"""
- number = _num_text(value)
- unit = {
- "statistics.digg_count": "赞",
- "statistics.like_count": "赞",
- "statistics.comment_count": "条评论",
- "statistics.share_count": "次转发",
- "statistics.collect_count": "次收藏",
- "statistics.play_count": "次播放",
- }.get(field)
- return f"该视频原始 {number} {unit}" if unit else f"该视频原始值 {number}"
- def _metric_unit(field: str) -> str:
- """指标字段 → 中文短词(用于 ratio detail,如 转发/赞/播放)。"""
- return {
- "digg_count": "赞",
- "like_count": "赞",
- "share_count": "转发",
- "collect_count": "收藏",
- "comment_count": "评论",
- "play_count": "播放",
- }.get(_text(field).removeprefix("statistics."), _text(field))
- def _ratio_pct(value: Any) -> str:
- num = _float_or_none(value)
- return f"{round(num * 100, 1):g}%" if num is not None else "—"
- def _platform_component_detail(component: dict[str, Any]) -> str:
- """M11 平台分项的展示文案:ratio 出「转发 783 / 赞 1532 = 51% · 收缩后 X% · 目标 27%」;
- absolute 出体量/原始量。"""
- if _text(component.get("type")) == "ratio":
- nu = _metric_unit(component.get("numerator_field"))
- du = _metric_unit(component.get("denominator_field"))
- return (
- f"{nu} {_num_text(component.get('numerator_value'))} / {du} {_num_text(component.get('denominator_value'))}"
- f" = {_ratio_pct(component.get('raw_ratio'))}"
- f" · 收缩后 {_ratio_pct(component.get('shrunk_ratio'))}"
- f" · 目标 {_ratio_pct(component.get('target'))}"
- )
- field = _text(component.get("field"))
- value = component.get("value")
- if field == "total_interaction":
- return f"内容体量(赞+评+转+藏 共 {_num_text(value)})"
- unit = _metric_unit(field)
- return f"该视频原始 {_num_text(value)} {unit}" if unit else f"该视频原始值 {_num_text(value)}"
- def _percent_text(value: Any) -> str:
- number = _float_or_none(value)
- if number is None:
- return "待确认"
- return f"{round(number * 100):g}%"
- def _float_or_none(value: Any) -> float | None:
- try:
- if value is None or value == "":
- return None
- return float(value)
- except (TypeError, ValueError):
- return None
- def _short_text(value: str, limit: int) -> str:
- if len(value) <= limit:
- return value
- return value[: limit - 1] + "…"
- def _platform_label(platform: str) -> str:
- return {
- "douyin": "抖音",
- "tiktok": "TikTok",
- "bilibili": "B 站",
- "kuaishou": "快手",
- "xhs": "小红书",
- }.get(platform.lower(), platform or "平台待确认")
- def _platform_content_url(content: dict[str, Any]) -> str:
- raw = _record(content.get("raw_payload"))
- platform_raw = _record(content.get("platform_raw_payload") or raw.get("platform_raw_payload"))
- direct = _text(
- content.get("platform_content_url")
- or content.get("share_url")
- or content.get("url")
- or content.get("aweme_url")
- or raw.get("platform_content_url")
- or raw.get("share_url")
- or raw.get("url")
- or raw.get("aweme_url")
- )
- if direct:
- return direct
- platform = _text(content.get("platform") or raw.get("platform")).lower()
- content_id = _text(
- content.get("platform_content_id")
- or raw.get("platform_content_id")
- or platform_raw.get("aweme_id")
- )
- if platform == "douyin" and content_id:
- return f"https://www.douyin.com/video/{content_id}"
- return ""
- def _query_source_ref(seed_ref: dict[str, Any], refs: list[dict[str, Any]]) -> dict[str, Any]:
- for ref in refs:
- nested = _record(ref.get("source_ref"))
- if nested:
- return nested
- if ref:
- return ref
- return _record(seed_ref.get("source_ref"))
- def _source_details(
- method: str,
- source_text: str,
- seed_ref: dict[str, Any],
- source_ref: dict[str, Any],
- bundle: dict[str, Any],
- ) -> list[str]:
- if method == "seed_term":
- return _dedupe_texts([
- _detail("需求单号", _demand_id(bundle)),
- _detail("种子词", source_text),
- _detail("来源帖子", _source_post_id(seed_ref, source_ref, bundle)),
- ])
- if method in {"piaoquan_topic_point", "query_seed_point"}:
- point_text = _text(source_ref.get("point_text") or source_ref.get("cleaned_text") or source_text)
- return _dedupe_texts([
- _detail("帖子 ID", source_ref.get("post_id") or _source_post_id(seed_ref, source_ref, bundle)),
- _detail("内容点 ID", source_ref.get("point_id") or source_ref.get("topic_point_id")),
- _detail("内容点类型", source_ref.get("point_type")),
- _detail("内容点", point_text),
- ])
- if method == "category_leaf_element":
- binding = _category_binding(bundle, source_ref)
- category_path = _text(
- source_ref.get("category_path")
- or binding.get("category_full_path")
- or binding.get("category_path")
- )
- category_level = _text(source_ref.get("category_level") or binding.get("category_level")) or _level_from_path(category_path)
- execution_id = _text(
- seed_ref.get("pattern_execution_id")
- or _evidence_pack(bundle).get("pattern_execution_id")
- or _record(bundle.get("pattern_seed_pack")).get("pattern_execution_id")
- )
- category_id = _text(source_ref.get("category_id") or binding.get("category_id"))
- element_name = _text(source_ref.get("element_name") or binding.get("element_name") or source_text)
- return _dedupe_texts([
- f"分类树:Pattern 执行 {execution_id}" if execution_id else "分类树:当前运行记录未写入执行编号",
- _detail("分类路径", category_path),
- f"所在层级:第 {category_level} 层" if category_level else "所在层级:当前运行记录未写入,需要接分类树快照反查",
- _detail("叶子元素", element_name),
- _detail("分类节点", category_id),
- ])
- return _dedupe_texts([_detail("来源", source_text)])
- def _source_ref_label(
- method: str,
- seed_ref: dict[str, Any],
- source_ref: dict[str, Any],
- bundle: dict[str, Any],
- ) -> str:
- if method == "seed_term":
- return _text(_demand_id(bundle) or _source_post_id(seed_ref, source_ref, bundle), "来源编号待确认")
- if method in {"piaoquan_topic_point", "query_seed_point"}:
- return _text(source_ref.get("post_id") or _source_post_id(seed_ref, source_ref, bundle), "帖子编号待确认")
- if method == "category_leaf_element":
- binding = _category_binding(bundle, source_ref)
- path = _text(source_ref.get("category_path") or binding.get("category_full_path") or binding.get("category_path"))
- return path or _text(source_ref.get("category_id") or binding.get("category_id"), "分类节点待确认")
- return _text(seed_ref.get("source_post_id") or seed_ref.get("pattern_execution_id"), "来源编号待确认")
- def _demand_id(bundle: dict[str, Any]) -> str:
- source_context = _record(bundle.get("source_context"))
- return _text(source_context.get("demand_content_id") or source_context.get("id") or _evidence_pack(bundle).get("demand_content_id"))
- def _demand_summary(bundle: dict[str, Any]) -> dict[str, Any]:
- source_context = _record(bundle.get("source_context"))
- raw = _record(source_context.get("raw_demand_content")) or source_context
- ext_data = _record(raw.get("ext_data")) or _record(source_context.get("ext_data"))
- evidence = _evidence_pack(bundle)
- category_paths = _dedupe_texts([
- _text(item.get("category_full_path") or item.get("category_path"))
- for item in _list(evidence.get("category_bindings") or evidence.get("itemset_items"))
- ])
- extracted_points: list[str] = []
- for binding in _list(evidence.get("element_bindings")):
- for sample in _list(_record(binding).get("sample_elements")):
- sample_record = _record(sample)
- point_text = _text(sample_record.get("point_text") or sample_record.get("name"))
- if not point_text:
- continue
- post_id = _text(sample_record.get("post_id"))
- point_type = _text(sample_record.get("point_type"))
- suffix = " / ".join(item for item in [point_type, f"帖子 {post_id}" if post_id else ""] if item)
- extracted_points.append(f"{point_text}({suffix})" if suffix else point_text)
- # 分类叶子来源的候选搜索词(element_bindings 元素名)。前端用它对比首轮 query,
- # 讲清"分类来源的某词与前面哪条来源的同名词重复、被去重未单独显示"。
- category_leaf_terms: list[str] = []
- for binding in _list(evidence.get("element_bindings")):
- for sample in _list(_record(binding).get("sample_elements")):
- name = _text(_record(sample).get("name"))
- if name:
- category_leaf_terms.append(name)
- return {
- "id": _demand_id(bundle),
- "name": _text(raw.get("name") or source_context.get("name")),
- "description": _text(ext_data.get("desc") or raw.get("suggestion") or source_context.get("suggestion")),
- "reason": _text(ext_data.get("reason") or raw.get("reason") or source_context.get("reason")),
- "source": _text(raw.get("merge_leve2") or source_context.get("merge_leve2")),
- "score": raw.get("score") if raw.get("score") is not None else source_context.get("score"),
- "date": _text(raw.get("dt") or source_context.get("dt")),
- "type": _text(ext_data.get("type") or evidence.get("source_kind")),
- "itemset_ids": _text_list(evidence.get("itemset_ids")),
- "seed_terms": _text_list(evidence.get("seed_terms")),
- "source_post_id": _text(evidence.get("source_post_id")),
- "support": evidence.get("absolute_support") if evidence.get("absolute_support") is not None else evidence.get("support"),
- "pattern_execution_id": _text(evidence.get("pattern_execution_id")),
- "mining_config_id": _text(evidence.get("mining_config_id")),
- "category_paths": category_paths,
- "extracted_points": _dedupe_texts(extracted_points),
- "category_leaf_terms": _dedupe_texts(category_leaf_terms),
- }
- def _source_post_id(seed_ref: dict[str, Any], source_ref: dict[str, Any], bundle: dict[str, Any]) -> str:
- return _text(
- source_ref.get("post_id")
- or seed_ref.get("source_post_id")
- or _evidence_pack(bundle).get("source_post_id")
- or _record(bundle.get("pattern_seed_pack")).get("source_post_id")
- )
- def _evidence_pack(bundle: dict[str, Any]) -> dict[str, Any]:
- source_context = _record(bundle.get("source_context"))
- ext_data = _record(source_context.get("ext_data"))
- return _record(ext_data.get("evidence_pack") or source_context.get("evidence_pack"))
- def _category_binding(bundle: dict[str, Any], source_ref: dict[str, Any]) -> dict[str, Any]:
- category_id = _text(source_ref.get("category_id"))
- itemset_item_id = _text(source_ref.get("itemset_item_id"))
- element_name = _text(source_ref.get("element_name"))
- evidence = _evidence_pack(bundle)
- for key in ("category_bindings", "itemset_items"):
- matched = _find_category_record(_list(evidence.get(key)), category_id, itemset_item_id, element_name)
- if matched:
- return matched
- seed_pack = _record(bundle.get("pattern_seed_pack"))
- matched = _find_category_record(_list(seed_pack.get("category_bindings")), category_id, itemset_item_id, element_name)
- if matched:
- return matched
- for binding in _list(seed_pack.get("element_bindings")) + _list(evidence.get("element_bindings")):
- matched = _find_category_record([binding], category_id, itemset_item_id, element_name)
- if matched:
- return binding
- for sample in _list(binding.get("sample_elements")):
- matched = _find_category_record([sample], category_id, "", element_name)
- if matched:
- sample_with_binding = dict(sample)
- sample_with_binding.setdefault("itemset_item_id", binding.get("itemset_item_id"))
- sample_with_binding.setdefault("category_path", binding.get("category_path"))
- return sample_with_binding
- return {}
- def _find_category_record(
- rows: list[dict[str, Any]],
- category_id: str,
- itemset_item_id: str,
- element_name: str,
- ) -> dict[str, Any]:
- for row in rows:
- if itemset_item_id and _text(row.get("itemset_item_id")) == itemset_item_id:
- return row
- if category_id and _text(row.get("category_id")) == category_id:
- return row
- if element_name and _text(row.get("element_name") or row.get("name")) == element_name:
- return row
- return {}
- def _level_from_path(path: str) -> str:
- parts = [part for part in path.split("/") if part.strip()]
- return str(len(parts)) if parts else ""
- def _detail(label: str, value: Any) -> str:
- text = _text(value)
- return f"{label}:{text}" if text else ""
- def _dedupe_texts(values: list[str]) -> list[str]:
- result: list[str] = []
- seen: set[str] = set()
- for value in values:
- text = _text(value)
- if text and text not in seen:
- seen.add(text)
- result.append(text)
- return result
- def _text(value: Any, default: str = "") -> str:
- if value is None:
- return default
- text = str(value).strip()
- return text or default
- def _text_list(value: Any) -> list[str]:
- if isinstance(value, list):
- return [_text(item) for item in value if _text(item)]
- if isinstance(value, str) and value.strip():
- return [value.strip()]
- return []
- def _record(value: Any) -> dict[str, Any]:
- return value if isinstance(value, dict) else {}
- def _list(value: Any) -> list[dict[str, Any]]:
- return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
- def _number(value: Any) -> int:
- try:
- return int(float(value))
- except (TypeError, ValueError):
- return 0
- def _cache_ttl_seconds() -> int:
- raw = os.environ.get("CONTENT_AGENT_FLOW_LEDGER_CACHE_TTL_SECONDS")
- if raw is None:
- return _DEFAULT_CACHE_TTL_SECONDS
- try:
- return max(0, int(raw))
- except ValueError:
- return _DEFAULT_CACHE_TTL_SECONDS
- def _cache_get(key: tuple[str, str]) -> dict[str, Any] | None:
- ttl = _cache_ttl_seconds()
- if ttl <= 0:
- return None
- cached = _FLOW_LEDGER_CACHE.get(key)
- if not cached:
- return None
- expires_at, payload = cached
- if expires_at <= time.monotonic():
- _FLOW_LEDGER_CACHE.pop(key, None)
- return None
- return copy.deepcopy(payload)
- def _cache_set(key: tuple[str, str], payload: dict[str, Any]) -> None:
- ttl = _cache_ttl_seconds()
- if ttl <= 0:
- return
- _FLOW_LEDGER_CACHE[key] = (time.monotonic() + ttl, copy.deepcopy(payload))
|