business.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import type { FlowLedgerRow, ScoreItem, VideoRef } from "./types";
  2. import { asRecord, SOURCE_TYPE_LABELS, textList } from "./format";
  3. export type BusinessTone = "neutral" | "good" | "warn" | "bad" | "info";
  4. export type BusinessSection = {
  5. label: string;
  6. value?: string;
  7. tone?: BusinessTone;
  8. items?: string[];
  9. };
  10. const UNKNOWN = "系统原因待补充";
  11. const ACTION_LABELS: Record<string, string> = {
  12. ADD_TO_CONTENT_POOL: "入池",
  13. KEEP_CONTENT_FOR_REVIEW: "待复看",
  14. REJECT_CONTENT: "淘汰",
  15. TECHNICAL_RETRY_REQUIRED: "技术问题",
  16. NOT_JUDGED: "未进入判断",
  17. 未判断: "未进入判断"
  18. };
  19. const REASON_LABELS: Record<string, string> = {
  20. no_decision: "暂未产生判断结果",
  21. 无原因: "暂未产生判断原因"
  22. };
  23. const SOURCE_LABELS: Record<string, string> = {
  24. ...SOURCE_TYPE_LABELS,
  25. pattern_itemset: "来自需求 Pattern",
  26. pattern_search_query: "来自需求 Pattern",
  27. pattern_seed_ref: "来自需求 Pattern",
  28. demand_pattern: "来自需求 Pattern",
  29. source_seed: "来自需求种子",
  30. search_query_direct: "直接搜索",
  31. 来源待确认: "来源待确认"
  32. };
  33. const METHOD_LABELS: Record<string, string> = {
  34. ...SOURCE_TYPE_LABELS,
  35. query_next_page: "翻页继续找",
  36. tag_query: "从视频标签继续游走",
  37. author_works: "从作者继续游走",
  38. item_single: "使用原始需求词",
  39. manual: "人工指定搜索词",
  40. unknown: "生成方式待确认"
  41. };
  42. const EFFECT_LABELS: Record<string, string> = {
  43. success: "已找到可用结果",
  44. rule_blocked: "找到结果,但未通过内容判断",
  45. pending: "等待处理",
  46. failed: "搜索失败",
  47. skipped: "已跳过"
  48. };
  49. const WALK_EDGE_LABELS: Record<string, string> = {
  50. query_next_page: "翻下一页找更多",
  51. path_stop: "到这里停止",
  52. decision_to_asset: "入池沉淀",
  53. budget_downgrade: "转入待复看",
  54. tag_query: "从视频标签继续游走",
  55. hashtag_to_query: "从视频标签继续游走",
  56. author_works: "从作者继续游走",
  57. author_to_works: "从作者继续游走",
  58. terminal: "到这里停止"
  59. };
  60. export const WALK_LANE_KEYS = [
  61. "query_next_page",
  62. "tag_query",
  63. "author_works",
  64. "path_stop"
  65. ];
  66. const WALK_STATUS_LABELS: Record<string, string> = {
  67. success: "已执行",
  68. skipped: "未继续",
  69. failed: "执行失败",
  70. pending: "等待处理"
  71. };
  72. const PLATFORM_LABELS: Record<string, string> = {
  73. douyin: "抖音",
  74. tiktok: "TikTok",
  75. bilibili: "B 站",
  76. kuaishou: "快手",
  77. shipinhao: "视频号",
  78. xhs: "小红书"
  79. };
  80. export function actionLabel(value: string): string {
  81. return ACTION_LABELS[value] || "未进入判断";
  82. }
  83. export function actionTone(value: string): BusinessTone {
  84. if (value === "ADD_TO_CONTENT_POOL") return "good";
  85. if (value === "KEEP_CONTENT_FOR_REVIEW") return "warn";
  86. if (value === "REJECT_CONTENT") return "bad";
  87. return "neutral";
  88. }
  89. export function reasonLabel(value: string): string {
  90. return REASON_LABELS[value] || UNKNOWN;
  91. }
  92. export function sourceLabel(value: string): string {
  93. return SOURCE_LABELS[value] || SOURCE_LABELS.来源待确认;
  94. }
  95. export function methodLabel(value: string): string {
  96. return METHOD_LABELS[value] || METHOD_LABELS.unknown;
  97. }
  98. export function effectLabel(value: string): string {
  99. return EFFECT_LABELS[value] || "处理状态待确认";
  100. }
  101. export function walkEdgeLabel(value: string): string {
  102. return WALK_EDGE_LABELS[value] || UNKNOWN;
  103. }
  104. export function walkStatusLabel(value: string): string {
  105. return WALK_STATUS_LABELS[value] || "状态待确认";
  106. }
  107. export function scoreBusinessText(value?: string): string | undefined {
  108. if (!value || value === "无分数") return undefined;
  109. return `总分 ${value}`;
  110. }
  111. export function platformLabel(value?: string | null): string {
  112. if (!value) return "平台待确认";
  113. return PLATFORM_LABELS[value.toLowerCase()] || value;
  114. }
  115. export function mediaStatusLabel(value?: string | null): string {
  116. const labels: Record<string, string> = {
  117. oss_uploaded: "已保存",
  118. oss_upload_pending: "待补传",
  119. oss_upload_failed: "补传失败",
  120. metadata_only: "尚未拿到视频",
  121. unavailable: "无可用视频"
  122. };
  123. return value ? labels[value] || "视频状态待确认" : "视频状态待确认";
  124. }
  125. export function scoreItemsText(items: ScoreItem[]): string {
  126. const visible = items.filter((item) => item.scoreLabel && item.scoreLabel !== "暂无评分");
  127. if (!visible.length) return "暂无评分";
  128. return visible.slice(0, 3).map((item) => `${displayScoreLabel(item.label)} ${item.scoreLabel}`).join(" · ");
  129. }
  130. export function compactScoreItemsList(items: ScoreItem[]): string[] {
  131. // 总分 + 各加权维度(group=main:相关性/平台/抖音的适老性),不再截断到 3 项。
  132. const wanted = items.filter(
  133. (item) =>
  134. item.scoreLabel &&
  135. item.scoreLabel !== "暂无评分" &&
  136. (displayScoreLabel(item.label) === "总分" || item.group === "main")
  137. );
  138. return wanted.map((item) => `${compactScoreLabel(item.label)} ${item.scoreLabel.replace(/\s*分$/, "")}`);
  139. }
  140. export function compactScoreItemsText(items: ScoreItem[]): string {
  141. const list = compactScoreItemsList(items);
  142. return list.length ? list.join(" · ") : "暂无评分";
  143. }
  144. export function scoreItemRows(items: ScoreItem[]): ScoreItem[] {
  145. return items.filter((item) => item.scoreLabel && item.scoreLabel !== "暂无评分");
  146. }
  147. export function displayScoreLabel(label: string): string {
  148. if (label === "total_score") return "总分";
  149. if (label === "query_relevance_score") return "是否契合需求";
  150. if (label === "platform_performance_score") return "平台表现";
  151. return label;
  152. }
  153. function compactScoreLabel(label: string): string {
  154. const displayLabel = displayScoreLabel(label);
  155. if (displayLabel === "是否契合需求") return "query相关性";
  156. if (displayLabel === "平台表现") return "平台数据表现";
  157. if (displayLabel === "50+老年受众") return "适老性";
  158. return displayLabel;
  159. }
  160. export function metricLabel(video: VideoRef): string {
  161. const digg = video.statistics.digg_count || video.statistics.like_count || 0;
  162. const comment = video.statistics.comment_count || 0;
  163. const share = video.statistics.share_count || 0;
  164. return `点赞 ${digg} · 评论 ${comment} · 转发 ${share}`;
  165. }
  166. export function sourceEvidence(row: FlowLedgerRow): string {
  167. if (row.source.details.length) return row.source.details.join(" · ");
  168. if (row.source.terms.length) return `依据:${row.source.terms.join(" / ")}`;
  169. if (row.source.evidenceLabel && row.source.evidenceLabel !== "来源证据待确认") return row.source.evidenceLabel;
  170. return "来源证据待确认";
  171. }
  172. export function ruleHeadline(row: FlowLedgerRow): string {
  173. const total = row.ruleSummary.total;
  174. if (!total) return "首轮内容暂未进入判断";
  175. if (row.ruleSummary.passCount) return `有 ${row.ruleSummary.passCount} 条内容可进入沉淀`;
  176. if (row.ruleSummary.reviewCount) return `有 ${row.ruleSummary.reviewCount} 条内容需要人工复看`;
  177. if (row.ruleSummary.technicalRetryCount) return `有 ${row.ruleSummary.technicalRetryCount} 条内容出现技术问题`;
  178. return "本轮没有可沉淀内容";
  179. }
  180. export function walkSummaryText(row: FlowLedgerRow): string {
  181. if (!row.walkSummary.total) return "本轮没有继续扩展";
  182. const stop = Object.entries(row.walkSummary.stopReasons).sort((a, b) => b[1] - a[1])[0]?.[0];
  183. if (stop) return `${row.walkSummary.total} 次扩展动作,主要因为“${reasonLabel(stop)}”停止`;
  184. return `${row.walkSummary.total} 次扩展动作`;
  185. }
  186. export function assetSummaryText(row: FlowLedgerRow): string {
  187. const pool = row.finalAssets.assetCount || 0;
  188. const review = row.finalAssets.reviewCount || 0;
  189. const technical = row.finalAssets.technicalRetryCount || 0;
  190. const reject = row.finalAssets.rejectCount || 0;
  191. const parts = [];
  192. if (pool) parts.push(`入池 ${pool} 条`);
  193. if (review) parts.push(`待复看 ${review} 条`);
  194. if (technical) parts.push(`技术问题 ${technical} 条`);
  195. if (parts.length) return parts.join(",");
  196. if (reject) return `没有入池,淘汰 ${reject} 条`;
  197. return "暂无可沉淀内容";
  198. }
  199. export function assetDetailText(row: FlowLedgerRow): string {
  200. const pool = row.finalAssets.assetCount || 0;
  201. const review = row.finalAssets.reviewCount || 0;
  202. const technical = row.finalAssets.technicalRetryCount || 0;
  203. const reject = row.finalAssets.rejectCount || 0;
  204. if (pool && review) return "入池内容可后续使用,待复看需要人工确认";
  205. if (pool) return "已进入后续内容池";
  206. if (review) return "需人工确认后,再决定是否进入后续使用";
  207. if (technical) return "视频下载、压缩或模型判断失败,需要系统重试";
  208. if (reject) return "本轮内容已淘汰,没有进入后续使用";
  209. return "这轮没有内容进入后续使用";
  210. }
  211. export function clueText(row: FlowLedgerRow): string {
  212. const clue = asRecord(row.query.clue);
  213. const queries = textList(clue.generated_queries);
  214. if (queries.length) return `系统还尝试了 ${queries.length} 个相近搜索词`;
  215. return "";
  216. }