import type { FlowLedgerApiResponse, RawRecord } from "@/lib/api/types"; import type { DemandSummary, FlowLedger, FlowLedgerRow, ScoreItem, ScoreThresholds, TechnicalRetryAttempt, TechnicalRetryDetail, VideoRef } from "./types"; import { asRecord, maybeText, scoreLabel, text, textList } from "./format"; function scoreThresholds(value: unknown): ScoreThresholds | null { if (typeof value !== "object" || value === null) return null; const row = value as Record; const num = (key: string) => (row[key] === null || row[key] === undefined ? undefined : Number(row[key])); return { pool_total: num("pool_total"), pool_query: num("pool_query"), review_total: num("review_total"), review_query: num("review_query"), walk_query: num("walk_query"), walk_platform: num("walk_platform"), walk_total: num("walk_total") }; } function countMap(value: unknown): Record { const row = asRecord(value); return Object.fromEntries(Object.entries(row).map(([key, item]) => [key, Number(item) || 0])); } function scoreItems(value: unknown): ScoreItem[] { if (!Array.isArray(value)) return []; return value .filter((item): item is RawRecord => typeof item === "object" && item !== null) .map((item) => ({ label: text(item.label, "评分项"), score: item.score === null || item.score === undefined ? null : Number(item.score), scoreLabel: text(item.score_label, "暂无评分"), detail: maybeText(item.detail), weight: item.weight === null || item.weight === undefined ? null : Number(item.weight), group: maybeText(item.group), status: maybeText(item.status) })); } function technicalRetryDetail(value: unknown): TechnicalRetryDetail | null { const row = asRecord(value); if (!Object.keys(row).length) return null; const attempts = Array.isArray(row.attempts) ? row.attempts : []; return { briefReason: text(row.brief_reason, ""), stage: text(row.stage, ""), stageLabel: text(row.stage_label, ""), failureType: text(row.failure_type, ""), failureLabel: text(row.failure_label, ""), exceptionType: text(row.exception_type, ""), httpStatusCode: row.http_status_code === null || row.http_status_code === undefined ? null : Number(row.http_status_code), errorMessage: text(row.error_message, ""), retryCount: row.retry_count === null || row.retry_count === undefined ? null : Number(row.retry_count), videoSource: text(row.video_source, ""), timings: countMapNullable(row.timings), sizes: countMapNullable(row.sizes), attempts: attempts .filter((item): item is RawRecord => typeof item === "object" && item !== null) .map((item): TechnicalRetryAttempt => ({ attempt: item.attempt === null || item.attempt === undefined ? null : Number(item.attempt), status: text(item.status, ""), durationSeconds: item.duration_seconds === null || item.duration_seconds === undefined ? null : Number(item.duration_seconds), failureType: text(item.failure_type, ""), exceptionType: text(item.exception_type, ""), httpStatusCode: item.http_status_code === null || item.http_status_code === undefined ? null : Number(item.http_status_code) })), oss: asRecord(row.oss) }; } function countMapNullable(value: unknown): Record { const row = asRecord(value); return Object.fromEntries( Object.entries(row).map(([key, item]) => [key, item === null || item === undefined || item === "" ? null : Number(item)]) ); } function videoRef(row: RawRecord): VideoRef { const decision = asRecord(row.decision); return { id: text(row.id, "content"), contentDiscoveryId: text(row.content_discovery_id, ""), platformContentId: text(row.platform_content_id, ""), title: text(row.title, "无标题视频"), author: text(row.author, "未知作者"), platform: text(row.platform, "unknown"), platformLabel: text(row.platform_label, ""), url: maybeText(row.playable_url) || maybeText(row.oss_url) || null, ossUrl: maybeText(row.oss_url) || null, mediaStatus: text(row.media_status, "unavailable"), mediaStatusLabel: text(row.media_status_label, "视频状态待确认"), mediaFailureReason: maybeText(row.media_failure_reason), tags: textList(row.tags), statistics: countMap(row.statistics), decisionAction: text(decision.action, "NOT_JUDGED"), decisionLabel: text(decision.label, "未进入判断"), decisionReason: text(decision.reason, "no_decision"), decisionReasonLabel: text(decision.reason_label, "暂未产生判断结果"), walkOutCount: Number(row.walk_out_count || 0), score: scoreLabel(decision.score), scoreItems: scoreItems(decision.score_items), scoreThresholds: scoreThresholds(decision.score_thresholds), raw: row, decision, gemini: asRecord(row.gemini), technicalRetryDetail: technicalRetryDetail(row.technical_retry_detail) }; } function rowFromApi(row: RawRecord): FlowLedgerRow { const source = asRecord(row.source); const query = asRecord(row.query); const rule = asRecord(row.rule_summary); const walk = asRecord(row.walk_summary); const asset = asRecord(row.asset_summary); const videoStats = countMap(row.video_stats); const videos = (Array.isArray(row.first_round_videos) ? row.first_round_videos : []) .filter((item): item is RawRecord => typeof item === "object" && item !== null) .map(videoRef); return { id: text(row.id, text(query.id, "")), source: { sourceKind: text(source.type, "来源待确认"), evidenceLabel: text(source.evidence, "来源证据待确认"), sourceRef: text(source.source_ref, "来源编号待确认"), terms: textList(source.terms), details: textList(source.details), raw: source }, query: { id: text(query.id, ""), text: text(query.text, "搜索词待确认"), method: text(query.method, "unknown"), sourceTerms: textList(query.source_terms), effectStatus: text(query.effect_status, "pending"), raw: query }, firstRoundVideos: videos, firstRoundVideoTotal: Number(row.first_round_video_total || videos.length), videoStats, duplicateCount: Math.max(0, Number(row.first_round_video_total || 0) - Number(videoStats.total || videos.length)), ruleSummary: { total: Number(rule.total || 0), passCount: Number(rule.pool_count || 0), reviewCount: Number(rule.review_count || 0), technicalRetryCount: Number(rule.technical_retry_count || 0), rejectCount: Number(rule.reject_count || 0), unknownCount: 0, primaryReason: text(rule.primary_reason, "no_decision"), primaryReasonLabel: text(rule.primary_reason_label, "暂未产生判断结果"), scoreLabel: "", scoreItems: scoreItems(asRecord(rule.score_summary).items), decisions: Array.isArray(asRecord(rule.debug).decisions) ? (asRecord(rule.debug).decisions as RawRecord[]) : [], headline: text(rule.headline, "") }, walkSummary: { total: Number(walk.total || 0), expansionCount: Number(walk.expansion_count || 0), maxDepth: Number(walk.max_depth || 0), edgeCounts: countMap(walk.edge_counts), statusCounts: {}, stopReasons: walk.stop_reason ? { [text(walk.stop_reason, "")]: 1 } : {}, actions: [] }, finalAssets: { assetCount: Number(asset.pool_count || 0), finalCount: Number(asset.total || 0), pathCount: 0, paths: [], reviewCount: Number(asset.review_count || 0), technicalRetryCount: Number(asset.technical_retry_count || 0), rejectCount: Number(asset.reject_count || 0), headline: text(asset.headline, "") } }; } function demandSummaryFromApi(row: RawRecord | null | undefined): DemandSummary | undefined { if (!row) return undefined; return { id: text(row.id, ""), name: text(row.name, ""), description: text(row.description, ""), reason: text(row.reason, ""), source: text(row.source, ""), score: row.score === null || row.score === undefined || row.score === "" ? undefined : text(row.score, ""), date: text(row.date, ""), type: text(row.type, ""), itemsetIds: textList(row.itemset_ids), seedTerms: textList(row.seed_terms), sourcePostId: text(row.source_post_id, ""), support: row.support === null || row.support === undefined || row.support === "" ? undefined : text(row.support, ""), patternExecutionId: text(row.pattern_execution_id, ""), miningConfigId: text(row.mining_config_id, ""), categoryPaths: textList(row.category_paths), extractedPoints: textList(row.extracted_points), categoryLeafTerms: textList(row.category_leaf_terms) }; } export function buildFlowLedger(input: FlowLedgerApiResponse): FlowLedger { const rows = (input.rows || []).map(rowFromApi); return { runId: input.run_id, dataOrigin: input.data_origin, dataOriginLabel: input.data_origin_label, summary: input.summary || {}, demandSummary: demandSummaryFromApi(input.demand_summary), extensionQueries: input.extension_queries || [], rows, raw: { queries: [], contentItems: [], decisions: [], walkActions: [], sourcePaths: [] } }; } export function findLedgerRow(ledger: FlowLedger, queryId: string): FlowLedgerRow | undefined { return ledger.rows.find((row) => row.id === queryId); } export function videoFromApi(row: RawRecord): VideoRef { return videoRef(row); }