Parcourir la source

Show technical retry counts in run ledger

Sam Lee il y a 2 mois
Parent
commit
f35838e4fe

+ 6 - 1
content_agent/dashboard_service.py

@@ -609,6 +609,7 @@ def _business_summary(
         "content_count": counts.get("discovered_content_items", 0),
         "kept_count": int(summary.get("pooled_content_count") or 0),
         "review_count": int(summary.get("review_content_count") or 0),
+        "technical_retry_count": int(summary.get("technical_retry_content_count") or 0),
         "rejected_count": int(summary.get("rejected_content_count") or 0),
         "asset_count": int(summary.get("author_asset_count") or 0),
         "primary_failure_reason": primary_failure_reason,
@@ -685,7 +686,11 @@ def _stage_conclusions(
             "label": "资产",
             "status": "success" if files.get("final_output.json") else "pending",
             "headline": f"沉淀 {final_summary.get('pooled_content_count', 0) or 0} 条内容资产",
-            "detail": f"待复看 {final_summary.get('review_content_count', 0) or 0},淘汰 {final_summary.get('rejected_content_count', 0) or 0}",
+            "detail": (
+                f"待复看 {final_summary.get('review_content_count', 0) or 0},"
+                f"技术重试 {final_summary.get('technical_retry_content_count', 0) or 0},"
+                f"淘汰 {final_summary.get('rejected_content_count', 0) or 0}"
+            ),
             "metric": f"作者资产 {final_summary.get('author_asset_count', 0) or 0}",
         },
         {

+ 17 - 1
content_agent/flow_ledger_service.py

@@ -43,6 +43,7 @@ ACTION_LABELS = {
     "ADD_TO_CONTENT_POOL": "入池",
     "KEEP_CONTENT_FOR_REVIEW": "待复看",
     "REJECT_CONTENT": "淘汰",
+    "TECHNICAL_RETRY_REQUIRED": "技术重试",
 }
 MEDIA_STATUS_LABELS = {
     "oss_uploaded": "已保存",
@@ -451,6 +452,7 @@ class FlowLedgerService:
             "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_LABELS.get(primary_reason, "系统原因待补充"),
@@ -479,6 +481,7 @@ class FlowLedgerService:
         sections = {
             "content_assets": "入池",
             "review_records": "待复看",
+            "technical_retry_records": "技术重试",
             "reject_records": "淘汰",
         }
         counts = {}
@@ -495,10 +498,12 @@ class FlowLedgerService:
             "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),
             ),
         }
@@ -510,6 +515,7 @@ class FlowLedgerService:
             "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"],
@@ -577,6 +583,7 @@ class FlowLedgerService:
             "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"],
         }
 
@@ -705,15 +712,24 @@ def _rule_headline(actions: Counter[str], total: int) -> str:
         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, reject_count: int) -> str:
+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:

+ 1 - 1
tests/test_api.py

@@ -248,7 +248,7 @@ def test_api_config_readonly_endpoints():
     assert rule_packs["source_file"].endswith("douyin_rule_packs.v1.json")
     # M4 砍包受控变化:4 future dispatch/binding 已删,仅剩 content。
     assert len(rule_packs["data"]["rule_pack_dispatch"]) == 1
-    assert len(rule_packs["data"]["effect_status_mapping"]) == 5
+    assert len(rule_packs["data"]["effect_status_mapping"]) == 7
 
     walk = client.get("/config/walk-strategy").json()
     assert len(walk["data"]["walk_rule_pack_binding"]) == 1

+ 7 - 3
web2/features/LedgerPage.tsx

@@ -160,6 +160,7 @@ function HeaderSummaryChips({ rows, dataOriginLabel }: { rows: FlowLedgerRow[];
       <span className="chip blue">搜索词 {rows.length}</span>
       <span className="chip green">首轮视频 {rows.reduce((sum, row) => sum + row.firstRoundVideoTotal, 0)}</span>
       <span className="chip yellow">待复看 {rows.reduce((sum, row) => sum + row.ruleSummary.reviewCount, 0)}</span>
+      <span className="chip red">技术重试 {rows.reduce((sum, row) => sum + row.ruleSummary.technicalRetryCount, 0)}</span>
       <span className="chip">入池 {rows.reduce((sum, row) => sum + row.finalAssets.assetCount, 0)}</span>
       <span className="chip blue">{dataOriginLabel}</span>
     </div>
@@ -277,6 +278,7 @@ function VideoDecisionCell({
 function decisionChipClass(action: string): string {
   if (action === "ADD_TO_CONTENT_POOL") return "green";
   if (action === "KEEP_CONTENT_FOR_REVIEW") return "yellow";
+  if (action === "TECHNICAL_RETRY_REQUIRED") return "red";
   if (action === "REJECT_CONTENT") return "red";
   return "";
 }
@@ -290,8 +292,8 @@ function scoreRuleDrawer(row: FlowLedgerRow, video?: VideoRef | null): BusinessD
         label: video ? "当前视频结果" : "本组结果",
         value: video
           ? `${video.decisionLabel}${current ? `,${current}` : ""}。`
-          : `${ruleHeadline(row)}。入池 ${row.ruleSummary.passCount} 条,待复看 ${row.ruleSummary.reviewCount} 条,淘汰 ${row.ruleSummary.rejectCount} 条。`,
-        tone: video?.decisionAction === "ADD_TO_CONTENT_POOL" ? "good" : video?.decisionAction === "REJECT_CONTENT" ? "bad" : row.ruleSummary.reviewCount ? "warn" : "neutral"
+          : `${ruleHeadline(row)}。入池 ${row.ruleSummary.passCount} 条,待复看 ${row.ruleSummary.reviewCount} 条,技术重试 ${row.ruleSummary.technicalRetryCount} 条,淘汰 ${row.ruleSummary.rejectCount} 条。`,
+        tone: video?.decisionAction === "ADD_TO_CONTENT_POOL" ? "good" : video?.decisionAction === "REJECT_CONTENT" || video?.decisionAction === "TECHNICAL_RETRY_REQUIRED" ? "bad" : row.ruleSummary.reviewCount ? "warn" : "neutral"
       },
       {
         label: "总分怎么来",
@@ -320,7 +322,8 @@ function scoreRuleDrawer(row: FlowLedgerRow, video?: VideoRef | null): BusinessD
         label: "怎么判定",
         items: [
           "入池:通常总分达到 70 分以上,内容可以进入后续使用。",
-          "待复看:通常 55-69 分,或系统认为还需要人工确认。",
+          "待复看:通常 55-69 分,系统已经完成判断但还需要人工确认。",
+          "技术重试:视频下载、压缩或模型接口失败,系统没有完成内容判断。",
           "淘汰:通常低于 55 分,或内容明显不契合本次需求。"
         ],
         tone: "neutral"
@@ -358,6 +361,7 @@ function videoWalkStatus(video: VideoRef | null, allowWalk: boolean): { title: s
   if (allowWalk) return { title: "通过游走门槛", detail: "可从标签、作者或翻页继续找" };
   if (video.decisionAction === "ADD_TO_CONTENT_POOL") return { title: "入池但未游走", detail: "本条可沉淀,未继续向外扩展" };
   if (video.decisionAction === "KEEP_CONTENT_FOR_REVIEW") return { title: "未游走:待复看停止", detail: "需要人工确认后再决定是否继续" };
+  if (video.decisionAction === "TECHNICAL_RETRY_REQUIRED") return { title: "未游走:技术重试", detail: "视频下载、压缩或模型判断失败,等待系统重试" };
   if (video.decisionAction === "REJECT_CONTENT") return { title: "未游走:淘汰停止", detail: "不再继续带回新内容" };
   return { title: "未进入游走", detail: "当前没有继续扩展记录" };
 }

+ 1 - 1
web2/features/QueryVideosPage.tsx

@@ -51,7 +51,7 @@ export function QueryVideosPage({ runId, queryId }: { runId: string; queryId: st
               <span className="tag-mini">首轮视频 {Number(data.summary.total || videos.length)}</span>
             </summary>
             <div className="decl-body">
-              <Summary label="判断汇总" value={`入池 ${Number(data.summary.pool_count || 0)} · 待复看 ${Number(data.summary.review_count || 0)} · 淘汰 ${Number(data.summary.reject_count || 0)}`} />
+              <Summary label="判断汇总" value={`入池 ${Number(data.summary.pool_count || 0)} · 待复看 ${Number(data.summary.review_count || 0)} · 技术重试 ${Number(data.summary.technical_retry_count || 0)} · 淘汰 ${Number(data.summary.reject_count || 0)}`} />
               <Summary label="视频保存" value={`${mediaStatusLabel("oss_uploaded")} ${Number(data.summary.oss_uploaded_count || 0)} · ${mediaStatusLabel("oss_upload_pending")} ${Number(data.summary.oss_pending_count || 0)} · ${mediaStatusLabel("oss_upload_failed")} ${Number(data.summary.oss_failed_count || 0)}`} />
               <Summary label="数据来源" value={data.data_origin_label} />
             </div>

+ 2 - 0
web2/lib/flow-ledger/build.ts

@@ -84,6 +84,7 @@ function rowFromApi(row: RawRecord): FlowLedgerRow {
       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"),
@@ -107,6 +108,7 @@ function rowFromApi(row: RawRecord): FlowLedgerRow {
       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, "")
     }

+ 5 - 0
web2/lib/flow-ledger/business.ts

@@ -197,6 +197,7 @@ export function ruleHeadline(row: FlowLedgerRow): string {
   if (!total) return "首轮内容暂未进入判断";
   if (row.ruleSummary.passCount) return `有 ${row.ruleSummary.passCount} 条内容可进入沉淀`;
   if (row.ruleSummary.reviewCount) return `有 ${row.ruleSummary.reviewCount} 条内容需要人工复看`;
+  if (row.ruleSummary.technicalRetryCount) return `有 ${row.ruleSummary.technicalRetryCount} 条内容需要技术重试`;
   return "本轮没有可沉淀内容";
 }
 
@@ -210,10 +211,12 @@ export function walkSummaryText(row: FlowLedgerRow): string {
 export function assetSummaryText(row: FlowLedgerRow): string {
   const pool = row.finalAssets.assetCount || 0;
   const review = row.finalAssets.reviewCount || 0;
+  const technical = row.finalAssets.technicalRetryCount || 0;
   const reject = row.finalAssets.rejectCount || 0;
   const parts = [];
   if (pool) parts.push(`入池 ${pool} 条`);
   if (review) parts.push(`待复看 ${review} 条`);
+  if (technical) parts.push(`技术重试 ${technical} 条`);
   if (parts.length) return parts.join(",");
   if (reject) return `没有入池,淘汰 ${reject} 条`;
   return "暂无可沉淀内容";
@@ -222,10 +225,12 @@ export function assetSummaryText(row: FlowLedgerRow): string {
 export function assetDetailText(row: FlowLedgerRow): string {
   const pool = row.finalAssets.assetCount || 0;
   const review = row.finalAssets.reviewCount || 0;
+  const technical = row.finalAssets.technicalRetryCount || 0;
   const reject = row.finalAssets.rejectCount || 0;
   if (pool && review) return "入池内容可后续使用,待复看需要人工确认";
   if (pool) return "已进入后续内容池";
   if (review) return "需人工确认后,再决定是否进入后续使用";
+  if (technical) return "视频下载、压缩或模型判断失败,需要系统重试";
   if (reject) return "本轮内容已淘汰,没有进入后续使用";
   return "这轮没有内容进入后续使用";
 }

+ 2 - 0
web2/lib/flow-ledger/types.ts

@@ -58,6 +58,7 @@ export type RuleSummary = {
   total: number;
   passCount: number;
   reviewCount: number;
+  technicalRetryCount: number;
   rejectCount: number;
   unknownCount: number;
   primaryReason: string;
@@ -83,6 +84,7 @@ export type AssetSummary = {
   pathCount: number;
   paths: RawRecord[];
   reviewCount?: number;
+  technicalRetryCount?: number;
   rejectCount?: number;
   headline?: string;
 };