Просмотр исходного кода

Redesign walk ledger for operators

Sam Lee 3 недель назад
Родитель
Сommit
5e68da3aef

+ 223 - 27
content_agent/flow_ledger_service.py

@@ -28,12 +28,12 @@ EXTENSION_QUERY_METHODS = {
 
 
 SOURCE_LABELS = {
 SOURCE_LABELS = {
     "seed_term": "Pattern 种子词",
     "seed_term": "Pattern 种子词",
-    "piaoquan_topic_point": "票圈内容点",
+    "piaoquan_topic_point": "票圈帖子具体的点",
     "category_leaf_element": "分类叶子元素",
     "category_leaf_element": "分类叶子元素",
 }
 }
 QUERY_METHOD_LABELS = {
 QUERY_METHOD_LABELS = {
     "seed_term": "Pattern 种子词",
     "seed_term": "Pattern 种子词",
-    "piaoquan_topic_point": "票圈内容点",
+    "piaoquan_topic_point": "票圈帖子具体的点",
     "category_leaf_element": "分类叶子元素",
     "category_leaf_element": "分类叶子元素",
     "query_next_page": "翻页继续找",
     "query_next_page": "翻页继续找",
     "tag_query": "根据标签继续搜",
     "tag_query": "根据标签继续搜",
@@ -58,6 +58,8 @@ WALK_EDGE_LABELS = {
     "video_to_author": "查看作者更多作品",
     "video_to_author": "查看作者更多作品",
     "author_works": "查看作者更多作品",
     "author_works": "查看作者更多作品",
     "path_stop": "到这里停止",
     "path_stop": "到这里停止",
+    "decision_to_asset": "入池沉淀",
+    "budget_downgrade": "转入待复看",
     "terminal": "到这里停止",
     "terminal": "到这里停止",
 }
 }
 REASON_LABELS = {
 REASON_LABELS = {
@@ -73,6 +75,7 @@ REASON_LABELS = {
     "v4_allow_walk_denied": "不适合继续向外扩展",
     "v4_allow_walk_denied": "不适合继续向外扩展",
     "budget_exhausted": "本轮扩展名额已用完",
     "budget_exhausted": "本轮扩展名额已用完",
     "query>=70/platform>=65/score>=70": "评分达到继续扩展门槛",
     "query>=70/platform>=65/score>=70": "评分达到继续扩展门槛",
+    "content_decision_reused_for_walk_gate": "复用本条视频的内容判断作为扩展门槛",
     "no_decision": "暂未产生判断结果",
     "no_decision": "暂未产生判断结果",
 }
 }
 
 
@@ -142,18 +145,25 @@ class FlowLedgerService:
         bundle = self._bundle(run_id, cache=not debug)
         bundle = self._bundle(run_id, cache=not debug)
         query = self._query_by_id(bundle, query_id)
         query = self._query_by_id(bundle, query_id)
         related_ids = self._query_related_content_ids(query_id, bundle)
         related_ids = self._query_related_content_ids(query_id, bundle)
+        related_decision_ids = self._query_related_decision_ids(query_id, bundle)
         actions = [
         actions = [
             self._walk_action(action, query, bundle, debug=debug)
             self._walk_action(action, query, bundle, debug=debug)
             for action in bundle["walk_actions"]
             for action in bundle["walk_actions"]
             if _walk_action_related(action, query_id, related_ids)
             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 ["query_next_page", "tag_query", "author_works", "path_stop"]}
         lanes: dict[str, list[dict[str, Any]]] = {key: [] for key in ["query_next_page", "tag_query", "author_works", "path_stop"]}
         for action in actions:
         for action in actions:
             lanes[_walk_lane(action["edge_id"])].append(action)
             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 = [
         extension_queries = [
             self._extension_query(item, bundle, debug=debug)
             self._extension_query(item, bundle, debug=debug)
             for item in bundle["queries"]
             for item in bundle["queries"]
             if _query_method(item) in EXTENSION_QUERY_METHODS
             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 {
         return {
             "run_id": run_id,
             "run_id": run_id,
@@ -292,6 +302,26 @@ class FlowLedgerService:
             ids.update(_content_ids(item))
             ids.update(_content_ids(item))
         return {item for item in ids if 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]:
     def _source_summary(self, query: dict[str, Any], bundle: dict[str, Any], *, debug: bool) -> dict[str, Any]:
         method = _query_method(query)
         method = _query_method(query)
         seed_ref = _record(query.get("pattern_seed_ref"))
         seed_ref = _record(query.get("pattern_seed_ref"))
@@ -485,24 +515,36 @@ class FlowLedgerService:
         reason = _text(action.get("reason_code"))
         reason = _text(action.get("reason_code"))
         status = _text(action.get("walk_status"), "unknown")
         status = _text(action.get("walk_status"), "unknown")
         gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason") or reason)
         gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason") or reason)
-        trigger_label = _walk_trigger_label(action)
-        from_label = _walk_from_label(action, query, bundle)
+        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)
+        target_videos = [self._video(content, bundle, debug=debug) for content in target_contents[:4]]
+        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)
         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)
         return {
         return {
             "id": _text(action.get("walk_action_id")),
             "id": _text(action.get("walk_action_id")),
             "edge_id": edge,
             "edge_id": edge,
             "edge_label": WALK_EDGE_LABELS.get(edge, "系统原因待补充"),
             "edge_label": WALK_EDGE_LABELS.get(edge, "系统原因待补充"),
             "depth": _number(action.get("depth")),
             "depth": _number(action.get("depth")),
+            "source_video": source_video,
+            "target_query": target_query_summary,
+            "target_videos": target_videos,
             "trigger_label": trigger_label,
             "trigger_label": trigger_label,
             "status": status,
             "status": status,
             "status_label": _walk_status_label(status),
             "status_label": _walk_status_label(status),
             "from_label": from_label,
             "from_label": from_label,
-            "result": "已带入后续流程" if status == "success" else "没有继续执行",
+            "result": result_label,
             "reason": reason,
             "reason": reason,
-            "reason_label": _walk_reason_label(action, reason),
+            "reason_label": reason_label,
             "impact": _walk_impact(status, reason, gate_reason),
             "impact": _walk_impact(status, reason, gate_reason),
             "score_items": score_items,
             "score_items": score_items,
-            "detail_lines": _walk_detail_lines(action, edge, from_label, trigger_label, 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,
             "debug": {"action": action} if debug else None,
         }
         }
 
 
@@ -676,16 +718,98 @@ def _walk_status_label(status: str) -> str:
     }.get(status, "状态待确认")
     }.get(status, "状态待确认")
 
 
 
 
-def _walk_from_label(action: dict[str, Any], query: dict[str, Any] | None, bundle: dict[str, Any]) -> str:
+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_type = _text(action.get("from_node_type")).lower()
     from_node_id = _text(action.get("from_node_id"))
     from_node_id = _text(action.get("from_node_id"))
     if "query" in from_type:
     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
         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 "content" in from_type or "video" in from_type:
-        content = next((item for item in bundle["content_items"] if from_node_id in _content_ids(item)), None)
+        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"))
         title = _text((content or {}).get("description") or (content or {}).get("title"))
-        return f"从视频“{_short_text(title, 34)}”触发" if title else "从视频内容触发"
+        return f"从作者《{author}》触发,来源视频《{_short_text(title, 24)}》" if title else f"从作者《{author}》触发"
     if "tag" in from_type:
     if "tag" in from_type:
         return "从视频标签触发"
         return "从视频标签触发"
     return "从上一环节出发"
     return "从上一环节出发"
@@ -703,32 +827,42 @@ def _walk_impact(status: str, reason: str, gate_reason: str) -> str:
     return "这一步暂不影响最终沉淀。"
     return "这一步暂不影响最终沉淀。"
 
 
 
 
-def _walk_trigger_label(action: dict[str, Any]) -> str:
-    raw = _record(action.get("raw_payload"))
-    hashtag = _text(action.get("hashtag") or raw.get("hashtag"))
+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:
     if hashtag:
         return f"标签:#{hashtag.lstrip('#')}"
         return f"标签:#{hashtag.lstrip('#')}"
     edge = _walk_lane_id(action)
     edge = _walk_lane_id(action)
-    if edge in {"hashtag_to_query", "tag_query"}:
-        return "标签:未生成(先被扩展门槛拦下)"
-    author = _text(action.get("author_id") or raw.get("author_id") or action.get("platform_author_id") or raw.get("platform_author_id"))
+    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:
     if author:
         return f"作者:{author}"
         return f"作者:{author}"
     if edge in {"author_to_works", "video_to_author", "author_works"}:
     if edge in {"author_to_works", "video_to_author", "author_works"}:
         return "作者:未进入作者作品扩展"
         return "作者:未进入作者作品扩展"
-    cursor = _text(action.get("page_cursor") or raw.get("page_cursor") or action.get("cursor") or raw.get("cursor"))
+    cursor = _text(_action_value(action, "page_cursor") or _action_value(action, "cursor"))
     if cursor:
     if cursor:
-        return f"翻页游标:{cursor}"
-    to_node = _text(action.get("to_node_id"))
+        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"}:
     if to_node and to_node not in {"tag_query_skipped", "author_works_skipped"}:
-        return f"产生结果:{to_node}"
+        return f"结果对象:{to_node}"
     return "触发对象待确认"
     return "触发对象待确认"
 
 
 
 
 def _walk_reason_label(action: dict[str, Any], reason: str) -> str:
 def _walk_reason_label(action: dict[str, Any], reason: str) -> str:
-    gate_reason = _text(action.get("walk_gate_reason_code") or action.get("allow_walk_reason"))
+    gate_reason = _text(_action_value(action, "walk_gate_reason_code") or _action_value(action, "allow_walk_reason"))
     if gate_reason == "v4_allow_walk_denied":
     if gate_reason == "v4_allow_walk_denied":
-        allow_reason = _text(action.get("allow_walk_reason"))
+        allow_reason = _text(_action_value(action, "allow_walk_reason"))
         if allow_reason and allow_reason != gate_reason:
         if allow_reason and allow_reason != gate_reason:
             return f"不适合继续向外扩展:{REASON_LABELS.get(allow_reason, '评分未达到继续扩展门槛')}"
             return f"不适合继续向外扩展:{REASON_LABELS.get(allow_reason, '评分未达到继续扩展门槛')}"
         return "不适合继续向外扩展"
         return "不适合继续向外扩展"
@@ -737,24 +871,86 @@ def _walk_reason_label(action: dict[str, Any], reason: str) -> str:
     return "这一步没有记录额外原因"
     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 == "query_next_page":
+        return f"已翻到下一页,带回 {len(target_videos)} 条视频。"
+    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 == "query_next_page":
+        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(
 def _walk_detail_lines(
     action: dict[str, Any],
     action: dict[str, Any],
     edge: str,
     edge: str,
     from_label: str,
     from_label: str,
     trigger_label: str,
     trigger_label: str,
     score_items: list[dict[str, Any]],
     score_items: list[dict[str, Any]],
+    result_label: str,
 ) -> list[str]:
 ) -> list[str]:
     lines = [
     lines = [
         f"动作:{WALK_EDGE_LABELS.get(edge, '扩展动作')}",
         f"动作:{WALK_EDGE_LABELS.get(edge, '扩展动作')}",
-        f"上一环节:{from_label}",
+        f"起点:{from_label}",
     ]
     ]
     if trigger_label != "触发对象待确认":
     if trigger_label != "触发对象待确认":
-        lines.append(f"触发对象:{trigger_label}")
+        lines.append(f"扩展对象:{trigger_label}")
     if score_items:
     if score_items:
         lines.append("扩展门槛:" + " / ".join(f"{item['label']} {item['score_label']}" for item in score_items[:3]))
         lines.append("扩展门槛:" + " / ".join(f"{item['label']} {item['score_label']}" for item in score_items[:3]))
-    reason = _walk_reason_label(action, _text(action.get("reason_code")))
+    reason = _walk_reason_label(action, _text(_action_value(action, "reason_code")))
     if reason != "这一步没有记录额外原因":
     if reason != "这一步没有记录额外原因":
         lines.append(f"判断:{reason}")
         lines.append(f"判断:{reason}")
+    if result_label:
+        lines.append(f"结果:{result_label}")
     return lines
     return lines
 
 
 
 

+ 7 - 1
tests/test_flow_ledger_api.py

@@ -270,6 +270,7 @@ def test_flow_ledger_api_returns_business_v4_ledger(tmp_path, monkeypatch):
     assert row["first_round_videos"][0]["decision"]["score_items"][1]["label"] == "是否契合需求"
     assert row["first_round_videos"][0]["decision"]["score_items"][1]["label"] == "是否契合需求"
     assert row["first_round_videos"][0]["decision"]["score_items"][3]["label"] == "点赞表现"
     assert row["first_round_videos"][0]["decision"]["score_items"][3]["label"] == "点赞表现"
     piaoquan = next(item for item in ledger["rows"] if item["id"] == "q_003")
     piaoquan = next(item for item in ledger["rows"] if item["id"] == "q_003")
+    assert piaoquan["source"]["label"] == "票圈帖子具体的点"
     assert "帖子 ID:post_888" in piaoquan["source"]["details"]
     assert "帖子 ID:post_888" in piaoquan["source"]["details"]
     assert "内容点 ID:point_456" in piaoquan["source"]["details"]
     assert "内容点 ID:point_456" in piaoquan["source"]["details"]
     category = next(item for item in ledger["rows"] if item["id"] == "q_004")
     category = next(item for item in ledger["rows"] if item["id"] == "q_004")
@@ -283,9 +284,14 @@ def test_flow_ledger_api_returns_business_v4_ledger(tmp_path, monkeypatch):
 
 
     walk = client.get(f"/runs/{run_id}/flow-ledger/queries/q_001/walk").json()
     walk = client.get(f"/runs/{run_id}/flow-ledger/queries/q_001/walk").json()
     assert walk["summary"]["total_actions"] == 1
     assert walk["summary"]["total_actions"] == 1
+    assert walk["summary"]["extension_query_count"] == 1
     assert walk["lanes"]["tag_query"][0]["edge_label"] == "根据标签继续搜"
     assert walk["lanes"]["tag_query"][0]["edge_label"] == "根据标签继续搜"
+    assert walk["lanes"]["tag_query"][0]["source_video"]["title"] == "睡前 5 分钟肩颈拉伸"
     assert walk["lanes"]["tag_query"][0]["trigger_label"] == "标签:#拉伸"
     assert walk["lanes"]["tag_query"][0]["trigger_label"] == "标签:#拉伸"
-    assert "上一环节:从视频“睡前 5 分钟肩颈拉伸”触发" in walk["lanes"]["tag_query"][0]["detail_lines"]
+    assert walk["lanes"]["tag_query"][0]["target_query"]["text"] == "肩颈放松"
+    assert walk["lanes"]["tag_query"][0]["target_videos"][0]["title"] == "扩展搜到的视频"
+    assert "起点:从视频《睡前 5 分钟肩颈拉伸》触发" in walk["lanes"]["tag_query"][0]["detail_lines"]
+    assert "结果:已生成标签搜索词《肩颈放松》,带回 1 条视频。" in walk["lanes"]["tag_query"][0]["detail_lines"]
     assert walk["lanes"]["tag_query"][0]["score_items"][0]["score_label"] == "82 分"
     assert walk["lanes"]["tag_query"][0]["score_items"][0]["score_label"] == "82 分"
 
 
     detail = client.get(f"/runs/{run_id}/flow-ledger/videos/douyin_001").json()
     detail = client.get(f"/runs/{run_id}/flow-ledger/videos/douyin_001").json()

+ 299 - 0
web2/app/globals.css

@@ -995,6 +995,305 @@ a:focus-visible {
   color: var(--muted);
   color: var(--muted);
 }
 }
 
 
+.walk-ledger-board {
+  margin: 12px 20px 24px;
+  padding: 12px;
+  border: 1px solid #dbeafe;
+  border-radius: 6px;
+  background: #f8fbff;
+}
+
+.walk-ledger-root {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  padding: 9px 10px;
+  border: 1px solid #cbd5e1;
+  border-radius: 4px;
+  background: #fff;
+}
+
+.walk-ledger-root strong {
+  color: #0f172a;
+}
+
+.walk-ledger-root small {
+  color: var(--muted);
+}
+
+.walk-ledger-groups {
+  display: grid;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.walk-ledger-group {
+  position: relative;
+  display: grid;
+  grid-template-columns: 22px minmax(240px, 310px) minmax(0, 1fr);
+  gap: 10px;
+  align-items: start;
+}
+
+.walk-ledger-spine {
+  position: relative;
+  min-height: 100%;
+}
+
+.walk-ledger-spine::before {
+  content: "";
+  position: absolute;
+  top: -10px;
+  bottom: -10px;
+  left: 10px;
+  width: 2px;
+  background: #bfdbfe;
+}
+
+.walk-ledger-spine::after {
+  content: "";
+  position: absolute;
+  top: 19px;
+  left: 5px;
+  width: 12px;
+  height: 12px;
+  border: 2px solid #2563eb;
+  border-radius: 999px;
+  background: #fff;
+}
+
+.walk-source-card {
+  display: grid;
+  gap: 8px;
+  padding: 10px;
+  border: 1px solid #cbd5e1;
+  border-radius: 5px;
+  background: #fff;
+}
+
+.walk-source-head {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 7px;
+  align-items: center;
+}
+
+.walk-source-head strong {
+  color: #111827;
+  font-size: 13px;
+  line-height: 1.45;
+}
+
+.walk-source-meta {
+  display: grid;
+  gap: 5px;
+  color: #64748b;
+  font-size: 11.5px;
+  line-height: 1.45;
+}
+
+.walk-branch-list {
+  display: grid;
+  gap: 8px;
+}
+
+.walk-branch-card {
+  display: grid;
+  gap: 8px;
+  padding: 10px;
+  border: 1px solid #dbeafe;
+  border-radius: 5px;
+  background: #fff;
+}
+
+.walk-branch-card.tag {
+  border-color: #bbf7d0;
+  background: #f0fdf4;
+}
+
+.walk-branch-card.author {
+  border-color: #ddd6fe;
+  background: #f5f3ff;
+}
+
+.walk-branch-card.page {
+  border-color: #bfdbfe;
+  background: #eff6ff;
+}
+
+.walk-branch-card.asset {
+  border-color: #bbf7d0;
+  background: #ecfdf5;
+}
+
+.walk-branch-card.stop,
+.walk-branch-card.stopped {
+  border-color: #fde68a;
+  background: #fffbeb;
+}
+
+.walk-branch-head {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 7px;
+  align-items: center;
+}
+
+.walk-edge-icon {
+  width: 24px;
+  height: 24px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+  color: #1e40af;
+  background: #dbeafe;
+}
+
+.walk-branch-card.tag .walk-edge-icon {
+  color: #065f46;
+  background: #bbf7d0;
+}
+
+.walk-branch-card.author .walk-edge-icon {
+  color: #5b21b6;
+  background: #ddd6fe;
+}
+
+.walk-branch-card.stop .walk-edge-icon,
+.walk-branch-card.stopped .walk-edge-icon {
+  color: #92400e;
+  background: #fde68a;
+}
+
+.walk-branch-head strong {
+  color: #0f172a;
+}
+
+.walk-branch-summary {
+  margin: 0;
+  color: #334155;
+  font-size: 12.5px;
+  font-weight: 750;
+  line-height: 1.55;
+}
+
+.walk-branch-facts {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 6px;
+}
+
+.walk-fact-card {
+  min-width: 0;
+  display: grid;
+  gap: 4px;
+  padding: 7px 8px;
+  border: 1px solid rgba(148, 163, 184, 0.45);
+  border-radius: 4px;
+  background: rgba(255, 255, 255, 0.72);
+}
+
+.walk-fact-card span {
+  color: #64748b;
+  font-size: 11px;
+  font-weight: 800;
+}
+
+.walk-fact-card strong {
+  color: #1f2937;
+  font-size: 11.5px;
+  line-height: 1.45;
+  overflow-wrap: anywhere;
+}
+
+.walk-result-query,
+.walk-result-title {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 7px;
+  align-items: center;
+  color: #1e3a8a;
+  font-size: 12px;
+  font-weight: 800;
+}
+
+.walk-result-query {
+  padding: 7px 8px;
+  border: 1px solid #bfdbfe;
+  border-radius: 4px;
+  background: #fff;
+}
+
+.walk-result-query strong {
+  color: #0f172a;
+}
+
+.walk-result-videos {
+  display: grid;
+  gap: 6px;
+}
+
+.walk-result-video {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto;
+  gap: 3px 8px;
+  align-items: center;
+  padding: 7px 8px;
+  border: 1px solid #e2e8f0;
+  border-radius: 4px;
+  background: #fff;
+}
+
+.walk-result-video:hover {
+  border-color: #93c5fd;
+  background: #f8fbff;
+}
+
+.walk-result-video b {
+  color: #111827;
+  font-size: 12px;
+  line-height: 1.45;
+}
+
+.walk-result-video small {
+  grid-column: 1 / 2;
+  color: var(--muted);
+  font-size: 11px;
+}
+
+.walk-result-video svg {
+  grid-column: 2;
+  grid-row: 1 / span 2;
+  color: #94a3b8;
+}
+
+.walk-result-video.single {
+  grid-template-columns: minmax(0, 1fr);
+}
+
+@media (max-width: 900px) {
+  .walk-ledger-group {
+    grid-template-columns: 18px minmax(0, 1fr);
+  }
+
+  .walk-source-card,
+  .walk-branch-list {
+    grid-column: 2;
+  }
+
+  .walk-branch-facts {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+}
+
+@media (max-width: 520px) {
+  .walk-branch-facts {
+    grid-template-columns: 1fr;
+  }
+}
+
 .video-detail-grid {
 .video-detail-grid {
   display: grid;
   display: grid;
   grid-template-columns: repeat(2, minmax(280px, 1fr));
   grid-template-columns: repeat(2, minmax(280px, 1fr));

+ 153 - 73
web2/features/QueryWalkPage.tsx

@@ -1,6 +1,8 @@
 "use client";
 "use client";
 
 
-import { type CSSProperties, useCallback, useEffect, useState } from "react";
+import Link from "next/link";
+import { ChevronRight, GitBranch, Hash, ListVideo, Search, UserSearch } from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
 import { AppShell } from "@/components/AppShell";
 import { AppShell } from "@/components/AppShell";
 import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
 import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
 import { getFlowLedgerWalk } from "@/lib/api/client";
 import { getFlowLedgerWalk } from "@/lib/api/client";
@@ -8,6 +10,14 @@ import type { FlowLedgerWalkResponse, RawRecord } from "@/lib/api/types";
 import { methodLabel } from "@/lib/flow-ledger/business";
 import { methodLabel } from "@/lib/flow-ledger/business";
 import { asRecord, text } from "@/lib/flow-ledger/format";
 import { asRecord, text } from "@/lib/flow-ledger/format";
 
 
+type WalkGroup = {
+  id: string;
+  video?: RawRecord;
+  title: string;
+  subtitle: string;
+  actions: RawRecord[];
+};
+
 export function QueryWalkPage({ runId, queryId }: { runId: string; queryId: string }) {
 export function QueryWalkPage({ runId, queryId }: { runId: string; queryId: string }) {
   const [data, setData] = useState<FlowLedgerWalkResponse | null>(null);
   const [data, setData] = useState<FlowLedgerWalkResponse | null>(null);
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
@@ -31,7 +41,8 @@ export function QueryWalkPage({ runId, queryId }: { runId: string; queryId: stri
   }, [load]);
   }, [load]);
 
 
   const query = asRecord(data?.query);
   const query = asRecord(data?.query);
-  const actions = [...(data?.actions || [])].sort((a, b) => actionDepth(a) - actionDepth(b));
+  const actions = useMemo(() => [...(data?.actions || [])].sort((a, b) => actionDepth(a) - actionDepth(b)), [data?.actions]);
+  const groups = useMemo(() => buildWalkGroups(actions), [actions]);
 
 
   return (
   return (
     <AppShell title="继续扩展过程" subtitle={query.text ? `搜索词:${text(query.text)}` : "查看系统有没有继续向外找内容"} runId={runId} showBack onRefresh={load}>
     <AppShell title="继续扩展过程" subtitle={query.text ? `搜索词:${text(query.text)}` : "查看系统有没有继续向外找内容"} runId={runId} showBack onRefresh={load}>
@@ -44,52 +55,27 @@ export function QueryWalkPage({ runId, queryId }: { runId: string; queryId: stri
             <summary>
             <summary>
               <span className="kw">扩展</span> <b>{text(query.text, "搜索词待确认")}</b>
               <span className="kw">扩展</span> <b>{text(query.text, "搜索词待确认")}</b>
               <span className="decl-purpose">{methodLabel(text(query.method, ""))}</span>
               <span className="decl-purpose">{methodLabel(text(query.method, ""))}</span>
-              <span className="tag-mini">{actions.length ? `动作 ${actions.length}` : "没有继续扩展"}</span>
+              <span className="tag-mini">{actions.length ? `记录 ${actions.length} 步` : "没有继续扩展"}</span>
             </summary>
             </summary>
             <div className="decl-body">
             <div className="decl-body">
-              <Summary label="扩展结论" value={`${Number(data.summary.total_actions || 0)} 次动作,产生 ${Number(data.summary.extension_query_count || 0)} 个扩展搜索词`} />
-              <Summary label="执行情况" value={`已执行 ${Number(data.summary.success_count || 0)} · 未继续 ${Number(data.summary.skipped_count || 0)}`} />
+              <Summary label="本页怎么看" value="先看是哪条视频触发,再看系统选择了标签、翻页、作者作品,最后看带回了什么或为什么停止。" />
+              <Summary label="执行结果" value={`已执行 ${Number(data.summary.success_count || 0)} · 未继续 ${Number(data.summary.skipped_count || 0)} 步 · 新增扩展搜索词 ${Number(data.summary.extension_query_count || 0)} 个`} />
               <Summary label="数据来源" value={data.data_origin_label} />
               <Summary label="数据来源" value={data.data_origin_label} />
             </div>
             </div>
           </details>
           </details>
-          <section className="walk-tree-wrap">
-            <div className="walk-tree-root">
-              <span className="chip dark">首轮搜索词</span>
+
+          <section className="walk-ledger-board">
+            <div className="walk-ledger-root">
+              <span className="chip dark">起点搜索词</span>
               <strong>{text(query.text, "搜索词待确认")}</strong>
               <strong>{text(query.text, "搜索词待确认")}</strong>
               <small>{methodLabel(text(query.method, ""))}</small>
               <small>{methodLabel(text(query.method, ""))}</small>
             </div>
             </div>
-            <div className="walk-tree">
-              {actions.map((action, index) => (
-                <WalkTreeNode action={action} index={index} key={`${text(action.id, "walk")}-${index}`} />
+            <div className="walk-ledger-groups">
+              {groups.map((group, index) => (
+                <WalkGroupCard group={group} index={index} runId={runId} key={group.id} />
               ))}
               ))}
             </div>
             </div>
           </section>
           </section>
-          <section className="detail-table-wrap">
-            <table className="detail-table">
-              <thead>
-                <tr>
-                  <th>发生了什么</th>
-                  <th>从哪里来</th>
-                  <th>结果</th>
-                  <th>为什么</th>
-                  <th>对产出有什么影响</th>
-                  <th>评分门槛</th>
-                </tr>
-              </thead>
-              <tbody>
-                {actions.map((action, index) => (
-                  <tr key={`walk-${index}`}>
-                    <td>{text(action.edge_label, "扩展动作")}</td>
-                    <td>{text(action.from_label, "从上一环节出发")}</td>
-                    <td><span className="chip blue">{text(action.status_label, "状态待确认")}</span></td>
-                    <td>{text(action.reason_label, "这一步没有记录额外原因")}</td>
-                    <td>{text(action.impact, "这一步暂不影响最终沉淀")}</td>
-                    <td>{scoreLine(action)}</td>
-                  </tr>
-                ))}
-              </tbody>
-            </table>
-          </section>
         </>
         </>
       ) : null}
       ) : null}
     </AppShell>
     </AppShell>
@@ -105,55 +91,149 @@ function Summary({ label, value }: { label: string; value: string }) {
   );
   );
 }
 }
 
 
-function WalkTreeNode({ action, index }: { action: RawRecord; index: number }) {
-  const depth = actionDepth(action);
-  const lines = detailLines(action);
+function WalkGroupCard({ group, index, runId }: { group: WalkGroup; index: number; runId: string }) {
   return (
   return (
-    <article className={`walk-tree-node ${text(action.status, "") === "success" ? "success" : "skipped"}`} style={{ "--depth": String(Math.min(depth, 6)) } as CSSProperties}>
-      <div className="walk-node-spine" />
-      <div className="walk-node-card">
-        <div className="walk-node-title">
-          <span className="idx">第 {index + 1} 步</span>
-          <strong>{text(action.edge_label, "扩展动作")}</strong>
-          <span className={`chip ${text(action.status, "") === "success" ? "green" : "yellow"}`}>{text(action.status_label, "状态待确认")}</span>
+    <article className="walk-ledger-group">
+      <div className="walk-ledger-spine" />
+      <div className="walk-source-card">
+        <div className="walk-source-head">
+          <span className="idx">起点 {index + 1}</span>
+          <strong>{group.title}</strong>
         </div>
         </div>
-        <div className="walk-node-meta">
-          <span>{text(action.from_label, "从上一环节触发")}</span>
-          <span>{text(action.trigger_label, "触发对象待确认")}</span>
+        <div className="walk-source-meta">
+          <span>{group.subtitle}</span>
+          {group.video ? <span>{scoreSummary(asRecord(group.video.decision).score_items)}</span> : null}
         </div>
         </div>
-        <ul>
-          {lines.map((line) => (
-            <li key={line}>{line}</li>
-          ))}
-        </ul>
-        <p>{text(action.impact, "这一步暂不影响最终沉淀")}</p>
+      </div>
+      <div className="walk-branch-list">
+        {group.actions.map((action, actionIndex) => (
+          <WalkActionCard action={action} actionIndex={actionIndex} runId={runId} key={`${text(action.id, "walk")}-${actionIndex}`} />
+        ))}
       </div>
       </div>
     </article>
     </article>
   );
   );
 }
 }
 
 
+function WalkActionCard({ action, actionIndex, runId }: { action: RawRecord; actionIndex: number; runId: string }) {
+  const edgeId = text(action.edge_id, "");
+  const targetQuery = asRecord(action.target_query);
+  const targetVideos = Array.isArray(action.target_videos) ? action.target_videos.filter(isRecord) : [];
+  const sourceVideo = asRecord(action.source_video);
+  const status = text(action.status, "");
+  const isSuccess = status === "success";
+  return (
+    <article className={`walk-branch-card ${edgeTone(edgeId)} ${isSuccess ? "success" : "stopped"}`}>
+      <div className="walk-branch-head">
+        <span className="idx">分支 {actionIndex + 1}</span>
+        <span className="walk-edge-icon">{edgeIcon(edgeId)}</span>
+        <strong>{text(action.edge_label, "扩展动作")}</strong>
+        <span className={`chip ${isSuccess ? "green" : "yellow"}`}>{text(action.status_label, "状态待确认")}</span>
+      </div>
+      <p className="walk-branch-summary">{cleanBusinessText(text(action.summary) || text(action.result, "这一步没有记录结果"))}</p>
+      <div className="walk-branch-facts">
+        <Fact label="从哪来" value={cleanBusinessText(text(action.from_label, "从当前搜索词触发"))} />
+        <Fact label="扩展对象" value={cleanBusinessText(text(action.trigger_label, "扩展对象待确认"))} />
+        <Fact label="判断门槛" value={scoreSummary(action.score_items)} />
+        <Fact label={isSuccess ? "带回结果" : "停止原因"} value={cleanBusinessText(isSuccess ? text(action.result, "已执行") : text(action.reason_label, "没有继续扩展"))} />
+      </div>
+      {targetQuery.id ? (
+        <div className="walk-result-query">
+          <Search size={15} />
+          <span>新增搜索词</span>
+          <strong>{text(targetQuery.text, "搜索词待确认")}</strong>
+        </div>
+      ) : null}
+      {targetVideos.length ? (
+        <div className="walk-result-videos">
+          <div className="walk-result-title">
+            <ListVideo size={15} />
+            <span>带回视频 {targetVideos.length} 条</span>
+          </div>
+          {targetVideos.slice(0, 4).map((video) => (
+            <Link className="walk-result-video" href={`/runs/${encodeURIComponent(runId)}/videos/${encodeURIComponent(text(video.id, ""))}`} key={text(video.id, text(video.title, "video"))}>
+              <b>{text(video.title, "无标题视频")}</b>
+              <small>{text(video.author, "未知作者")} · {text(asRecord(video.decision).label, "未判断")}</small>
+              <ChevronRight size={14} />
+            </Link>
+          ))}
+        </div>
+      ) : null}
+      {!targetQuery.id && !targetVideos.length && sourceVideo.id && edgeId !== "hashtag_to_query" && edgeId !== "tag_query" ? (
+        <div className="walk-result-video single">
+          <b>{text(sourceVideo.title, "视频待确认")}</b>
+          <small>{text(sourceVideo.author, "未知作者")} · {text(asRecord(sourceVideo.decision).label, "未判断")}</small>
+        </div>
+      ) : null}
+    </article>
+  );
+}
+
+function Fact({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="walk-fact-card">
+      <span>{label}</span>
+      <strong>{value}</strong>
+    </div>
+  );
+}
+
+function buildWalkGroups(actions: RawRecord[]): WalkGroup[] {
+  const groups = new Map<string, WalkGroup>();
+  actions.forEach((action, index) => {
+    const video = asRecord(action.source_video);
+    const id = text(video.id) || `search-${text(action.from_label, "root")}-${index}`;
+    if (!groups.has(id)) {
+      groups.set(id, {
+        id,
+        video: video.id ? video : undefined,
+        title: video.id ? text(video.title, "无标题视频") : cleanBusinessText(text(action.from_label, "从当前搜索词触发")),
+        subtitle: video.id ? `${text(video.author, "未知作者")} · ${text(asRecord(video.decision).label, "未判断")}` : "搜索词级扩展",
+        actions: []
+      });
+    }
+    groups.get(id)!.actions.push(action);
+  });
+  return [...groups.values()];
+}
+
 function actionDepth(action: RawRecord): number {
 function actionDepth(action: RawRecord): number {
   const value = Number(action.depth || 0);
   const value = Number(action.depth || 0);
   return Number.isFinite(value) ? value : 0;
   return Number.isFinite(value) ? value : 0;
 }
 }
 
 
-function detailLines(action: RawRecord): string[] {
-  const value = action.detail_lines;
-  if (Array.isArray(value)) {
-    return value.map((item) => text(item)).filter(Boolean);
-  }
-  return [
-    `动作:${text(action.edge_label, "扩展动作")}`,
-    `上一环节:${text(action.from_label, "从上一环节触发")}`,
-    `触发对象:${text(action.trigger_label, "触发对象待确认")}`,
-    `判断:${text(action.reason_label, "这一步没有记录额外原因")}`
-  ];
+function scoreSummary(value: unknown): string {
+  const items = Array.isArray(value) ? value.filter(isRecord) : [];
+  const visible = items.filter((item) => text(item.score_label));
+  if (!visible.length) return "本步没有评分门槛";
+  return visible.slice(0, 3).map((item) => `${shortScoreLabel(text(item.label, "评分"))} ${text(item.score_label)}`).join(" / ");
+}
+
+function shortScoreLabel(label: string): string {
+  if (label === "是否契合需求") return "需求";
+  if (label === "平台表现") return "平台";
+  return label;
+}
+
+function cleanBusinessText(value: string): string {
+  if (!value || value === "系统原因待补充") return "这一步没有记录额外说明";
+  return value.replaceAll("“", "《").replaceAll("”", "》");
+}
+
+function edgeTone(edgeId: string): string {
+  if (edgeId === "hashtag_to_query" || edgeId === "tag_query") return "tag";
+  if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") return "author";
+  if (edgeId === "query_next_page") return "page";
+  if (edgeId === "decision_to_asset") return "asset";
+  return "stop";
+}
+
+function edgeIcon(edgeId: string) {
+  if (edgeId === "hashtag_to_query" || edgeId === "tag_query") return <Hash size={15} />;
+  if (edgeId === "author_to_works" || edgeId === "author_works" || edgeId === "video_to_author") return <UserSearch size={15} />;
+  if (edgeId === "query_next_page") return <Search size={15} />;
+  return <GitBranch size={15} />;
 }
 }
 
 
-function scoreLine(action: RawRecord): string {
-  const items = Array.isArray(action.score_items) ? action.score_items : [];
-  const labels = items
-    .filter((item): item is RawRecord => typeof item === "object" && item !== null)
-    .map((item) => `${text(item.label, "评分")} ${text(item.score_label, "暂无评分")}`);
-  return labels.length ? labels.join(" · ") : "暂无评分";
+function isRecord(value: unknown): value is RawRecord {
+  return typeof value === "object" && value !== null;
 }
 }

+ 6 - 2
web2/lib/flow-ledger/business.ts

@@ -27,7 +27,7 @@ const REASON_LABELS: Record<string, string> = {
 
 
 const SOURCE_LABELS: Record<string, string> = {
 const SOURCE_LABELS: Record<string, string> = {
   seed_term: "Pattern 种子词",
   seed_term: "Pattern 种子词",
-  piaoquan_topic_point: "票圈内容点",
+  piaoquan_topic_point: "票圈帖子具体的点",
   category_leaf_element: "分类叶子元素",
   category_leaf_element: "分类叶子元素",
   pattern_itemset: "来自需求 Pattern",
   pattern_itemset: "来自需求 Pattern",
   pattern_search_query: "来自需求 Pattern",
   pattern_search_query: "来自需求 Pattern",
@@ -40,7 +40,7 @@ const SOURCE_LABELS: Record<string, string> = {
 
 
 const METHOD_LABELS: Record<string, string> = {
 const METHOD_LABELS: Record<string, string> = {
   seed_term: "Pattern 种子词",
   seed_term: "Pattern 种子词",
-  piaoquan_topic_point: "票圈内容点",
+  piaoquan_topic_point: "票圈帖子具体的点",
   category_leaf_element: "分类叶子元素",
   category_leaf_element: "分类叶子元素",
   query_next_page: "翻页继续找",
   query_next_page: "翻页继续找",
   tag_query: "根据标签继续搜",
   tag_query: "根据标签继续搜",
@@ -61,8 +61,12 @@ const EFFECT_LABELS: Record<string, string> = {
 const WALK_EDGE_LABELS: Record<string, string> = {
 const WALK_EDGE_LABELS: Record<string, string> = {
   query_next_page: "翻下一页找更多",
   query_next_page: "翻下一页找更多",
   path_stop: "到这里停止",
   path_stop: "到这里停止",
+  decision_to_asset: "入池沉淀",
+  budget_downgrade: "转入待复看",
   tag_query: "根据视频标签继续搜",
   tag_query: "根据视频标签继续搜",
+  hashtag_to_query: "根据视频标签继续搜",
   author_works: "查看作者更多作品",
   author_works: "查看作者更多作品",
+  author_to_works: "查看作者更多作品",
   terminal: "到这里停止"
   terminal: "到这里停止"
 };
 };
 
 

+ 1 - 1
web2/lib/flow-ledger/format.ts

@@ -43,7 +43,7 @@ export function compactCountMap(counts: Record<string, number>, limit = 3): stri
 export function label(value: string): string {
 export function label(value: string): string {
   const labels: Record<string, string> = {
   const labels: Record<string, string> = {
     seed_term: "Pattern 种子词",
     seed_term: "Pattern 种子词",
-    piaoquan_topic_point: "票圈内容点",
+    piaoquan_topic_point: "票圈帖子具体的点",
     category_leaf_element: "分类叶子元素",
     category_leaf_element: "分类叶子元素",
     item_single: "使用原始需求词",
     item_single: "使用原始需求词",
     tag_query: "根据标签继续搜",
     tag_query: "根据标签继续搜",