build.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import type { FlowLedgerApiResponse, RawRecord } from "@/lib/api/types";
  2. import type { DemandSummary, FlowLedger, FlowLedgerRow, ScoreItem, ScoreThresholds, TechnicalRetryAttempt, TechnicalRetryDetail, VideoRef } from "./types";
  3. import { asRecord, maybeText, scoreLabel, text, textList } from "./format";
  4. function scoreThresholds(value: unknown): ScoreThresholds | null {
  5. if (typeof value !== "object" || value === null) return null;
  6. const row = value as Record<string, unknown>;
  7. const num = (key: string) => (row[key] === null || row[key] === undefined ? undefined : Number(row[key]));
  8. return {
  9. pool_total: num("pool_total"),
  10. pool_query: num("pool_query"),
  11. review_total: num("review_total"),
  12. review_query: num("review_query"),
  13. walk_query: num("walk_query"),
  14. walk_platform: num("walk_platform"),
  15. walk_total: num("walk_total")
  16. };
  17. }
  18. function countMap(value: unknown): Record<string, number> {
  19. const row = asRecord(value);
  20. return Object.fromEntries(Object.entries(row).map(([key, item]) => [key, Number(item) || 0]));
  21. }
  22. function scoreItems(value: unknown): ScoreItem[] {
  23. if (!Array.isArray(value)) return [];
  24. return value
  25. .filter((item): item is RawRecord => typeof item === "object" && item !== null)
  26. .map((item) => ({
  27. label: text(item.label, "评分项"),
  28. score: item.score === null || item.score === undefined ? null : Number(item.score),
  29. scoreLabel: text(item.score_label, "暂无评分"),
  30. detail: maybeText(item.detail),
  31. weight: item.weight === null || item.weight === undefined ? null : Number(item.weight),
  32. group: maybeText(item.group),
  33. status: maybeText(item.status)
  34. }));
  35. }
  36. function technicalRetryDetail(value: unknown): TechnicalRetryDetail | null {
  37. const row = asRecord(value);
  38. if (!Object.keys(row).length) return null;
  39. const attempts = Array.isArray(row.attempts) ? row.attempts : [];
  40. return {
  41. briefReason: text(row.brief_reason, ""),
  42. stage: text(row.stage, ""),
  43. stageLabel: text(row.stage_label, ""),
  44. failureType: text(row.failure_type, ""),
  45. failureLabel: text(row.failure_label, ""),
  46. exceptionType: text(row.exception_type, ""),
  47. httpStatusCode: row.http_status_code === null || row.http_status_code === undefined ? null : Number(row.http_status_code),
  48. errorMessage: text(row.error_message, ""),
  49. retryCount: row.retry_count === null || row.retry_count === undefined ? null : Number(row.retry_count),
  50. videoSource: text(row.video_source, ""),
  51. timings: countMapNullable(row.timings),
  52. sizes: countMapNullable(row.sizes),
  53. attempts: attempts
  54. .filter((item): item is RawRecord => typeof item === "object" && item !== null)
  55. .map((item): TechnicalRetryAttempt => ({
  56. attempt: item.attempt === null || item.attempt === undefined ? null : Number(item.attempt),
  57. status: text(item.status, ""),
  58. durationSeconds: item.duration_seconds === null || item.duration_seconds === undefined ? null : Number(item.duration_seconds),
  59. failureType: text(item.failure_type, ""),
  60. exceptionType: text(item.exception_type, ""),
  61. httpStatusCode: item.http_status_code === null || item.http_status_code === undefined ? null : Number(item.http_status_code)
  62. })),
  63. oss: asRecord(row.oss)
  64. };
  65. }
  66. function countMapNullable(value: unknown): Record<string, number | null> {
  67. const row = asRecord(value);
  68. return Object.fromEntries(
  69. Object.entries(row).map(([key, item]) => [key, item === null || item === undefined || item === "" ? null : Number(item)])
  70. );
  71. }
  72. function videoRef(row: RawRecord): VideoRef {
  73. const decision = asRecord(row.decision);
  74. return {
  75. id: text(row.id, "content"),
  76. contentDiscoveryId: text(row.content_discovery_id, ""),
  77. platformContentId: text(row.platform_content_id, ""),
  78. title: text(row.title, "无标题视频"),
  79. author: text(row.author, "未知作者"),
  80. platform: text(row.platform, "unknown"),
  81. platformLabel: text(row.platform_label, ""),
  82. url: maybeText(row.playable_url) || maybeText(row.oss_url) || null,
  83. ossUrl: maybeText(row.oss_url) || null,
  84. mediaStatus: text(row.media_status, "unavailable"),
  85. mediaStatusLabel: text(row.media_status_label, "视频状态待确认"),
  86. mediaFailureReason: maybeText(row.media_failure_reason),
  87. tags: textList(row.tags),
  88. statistics: countMap(row.statistics),
  89. decisionAction: text(decision.action, "NOT_JUDGED"),
  90. decisionLabel: text(decision.label, "未进入判断"),
  91. decisionReason: text(decision.reason, "no_decision"),
  92. decisionReasonLabel: text(decision.reason_label, "暂未产生判断结果"),
  93. walkOutCount: Number(row.walk_out_count || 0),
  94. score: scoreLabel(decision.score),
  95. scoreItems: scoreItems(decision.score_items),
  96. scoreThresholds: scoreThresholds(decision.score_thresholds),
  97. raw: row,
  98. decision,
  99. gemini: asRecord(row.gemini),
  100. technicalRetryDetail: technicalRetryDetail(row.technical_retry_detail)
  101. };
  102. }
  103. function rowFromApi(row: RawRecord): FlowLedgerRow {
  104. const source = asRecord(row.source);
  105. const query = asRecord(row.query);
  106. const rule = asRecord(row.rule_summary);
  107. const walk = asRecord(row.walk_summary);
  108. const asset = asRecord(row.asset_summary);
  109. const videoStats = countMap(row.video_stats);
  110. const videos = (Array.isArray(row.first_round_videos) ? row.first_round_videos : [])
  111. .filter((item): item is RawRecord => typeof item === "object" && item !== null)
  112. .map(videoRef);
  113. return {
  114. id: text(row.id, text(query.id, "")),
  115. source: {
  116. sourceKind: text(source.type, "来源待确认"),
  117. evidenceLabel: text(source.evidence, "来源证据待确认"),
  118. sourceRef: text(source.source_ref, "来源编号待确认"),
  119. terms: textList(source.terms),
  120. details: textList(source.details),
  121. raw: source
  122. },
  123. query: {
  124. id: text(query.id, ""),
  125. text: text(query.text, "搜索词待确认"),
  126. method: text(query.method, "unknown"),
  127. sourceTerms: textList(query.source_terms),
  128. effectStatus: text(query.effect_status, "pending"),
  129. raw: query
  130. },
  131. firstRoundVideos: videos,
  132. firstRoundVideoTotal: Number(row.first_round_video_total || videos.length),
  133. videoStats,
  134. duplicateCount: Math.max(0, Number(row.first_round_video_total || 0) - Number(videoStats.total || videos.length)),
  135. ruleSummary: {
  136. total: Number(rule.total || 0),
  137. passCount: Number(rule.pool_count || 0),
  138. reviewCount: Number(rule.review_count || 0),
  139. technicalRetryCount: Number(rule.technical_retry_count || 0),
  140. rejectCount: Number(rule.reject_count || 0),
  141. unknownCount: 0,
  142. primaryReason: text(rule.primary_reason, "no_decision"),
  143. primaryReasonLabel: text(rule.primary_reason_label, "暂未产生判断结果"),
  144. scoreLabel: "",
  145. scoreItems: scoreItems(asRecord(rule.score_summary).items),
  146. decisions: Array.isArray(asRecord(rule.debug).decisions) ? (asRecord(rule.debug).decisions as RawRecord[]) : [],
  147. headline: text(rule.headline, "")
  148. },
  149. walkSummary: {
  150. total: Number(walk.total || 0),
  151. expansionCount: Number(walk.expansion_count || 0),
  152. maxDepth: Number(walk.max_depth || 0),
  153. edgeCounts: countMap(walk.edge_counts),
  154. statusCounts: {},
  155. stopReasons: walk.stop_reason ? { [text(walk.stop_reason, "")]: 1 } : {},
  156. actions: []
  157. },
  158. finalAssets: {
  159. assetCount: Number(asset.pool_count || 0),
  160. finalCount: Number(asset.total || 0),
  161. pathCount: 0,
  162. paths: [],
  163. reviewCount: Number(asset.review_count || 0),
  164. technicalRetryCount: Number(asset.technical_retry_count || 0),
  165. rejectCount: Number(asset.reject_count || 0),
  166. headline: text(asset.headline, "")
  167. }
  168. };
  169. }
  170. function demandSummaryFromApi(row: RawRecord | null | undefined): DemandSummary | undefined {
  171. if (!row) return undefined;
  172. return {
  173. id: text(row.id, ""),
  174. name: text(row.name, ""),
  175. description: text(row.description, ""),
  176. reason: text(row.reason, ""),
  177. source: text(row.source, ""),
  178. score: row.score === null || row.score === undefined || row.score === "" ? undefined : text(row.score, ""),
  179. date: text(row.date, ""),
  180. type: text(row.type, ""),
  181. itemsetIds: textList(row.itemset_ids),
  182. seedTerms: textList(row.seed_terms),
  183. sourcePostId: text(row.source_post_id, ""),
  184. support: row.support === null || row.support === undefined || row.support === "" ? undefined : text(row.support, ""),
  185. patternExecutionId: text(row.pattern_execution_id, ""),
  186. miningConfigId: text(row.mining_config_id, ""),
  187. categoryPaths: textList(row.category_paths),
  188. extractedPoints: textList(row.extracted_points),
  189. categoryLeafTerms: textList(row.category_leaf_terms)
  190. };
  191. }
  192. export function buildFlowLedger(input: FlowLedgerApiResponse): FlowLedger {
  193. const rows = (input.rows || []).map(rowFromApi);
  194. return {
  195. runId: input.run_id,
  196. dataOrigin: input.data_origin,
  197. dataOriginLabel: input.data_origin_label,
  198. summary: input.summary || {},
  199. demandSummary: demandSummaryFromApi(input.demand_summary),
  200. extensionQueries: input.extension_queries || [],
  201. rows,
  202. raw: {
  203. queries: [],
  204. contentItems: [],
  205. decisions: [],
  206. walkActions: [],
  207. sourcePaths: []
  208. }
  209. };
  210. }
  211. export function findLedgerRow(ledger: FlowLedger, queryId: string): FlowLedgerRow | undefined {
  212. return ledger.rows.find((row) => row.id === queryId);
  213. }
  214. export function videoFromApi(row: RawRecord): VideoRef {
  215. return videoRef(row);
  216. }