feat(identity): source-aware short-drama enrichment with snippet mode and hit rate (META-2)

- classify enrich sources: encyclopedia, platform pages (hongguo, kuaikaw, video
  platforms), douban (search snippet only) and other sites' search snippets
  (snippet mode, pages never opened); one extractor per source, fields carry
  source kind and confidence, merged by noisy-OR across hosts
- new cast field (applied as MediaCredit with placeholder ids)
- robots.txt check (cached per host 1 day) and login-wall/captcha give-up
- per-enrichment source attempts + GET /identity/enrich-stats, 30-day hit-rate
  table under settings -> metadata -> identity
- setting identity.enrichSnippetAllSites (default on); default domains add
  baike.com and kuaikaw.cn
- replay fixtures from the real run on the 4 user-library short dramas
  (sanitized Brave responses, hongguo page, robots.txt)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 18:40:19 +08:00
1 parent 081db4651c
commit 9489e40e0c
23 files changed
+2615 -323

No files matched your search

@@ -10,13 +10,21 @@ import { Button } from "~/components/ui/button.tsx";
import { Label } from "~/components/ui/label.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { ENRICH_FIELD_LABEL, ENRICH_SKIPPED, EVIDENCE_LEVEL } from "./labels.ts";
import { ENRICH_FIELD_LABEL, ENRICH_SKIPPED, ENRICH_SOURCE_KIND, EVIDENCE_LEVEL } from "./labels.ts";
import { Chip } from "./parts.tsx";
// META-1 待识别资源 → 元数据补全:先看建议(网页搜索 + 白名单网页 + Agent 抽取,不改作品),再应用(建本地临时作品并关联)。
type Suggestion = NonNullable<MetadataEnrichOutput["suggestion"]>;
type Value = { value: unknown; sourceUrl: string | null; evidenceLevel: string; verbatim: boolean };
type Value = {
value: unknown;
sourceUrl: string | null;
evidenceLevel: string;
verbatim: boolean;
sourceKind?: string;
confidence?: number;
hosts?: number;
};
const hostOf = (u: string | null) => {
if (!u) return null;
@@ -69,6 +77,12 @@ function FieldRow({ k, s, enrichmentId }: { k: EnrichFieldName; s: Suggestion; e
<div className="flex flex-wrap items-center gap-1.5">
<Chip>{EVIDENCE_LEVEL[v.evidenceLevel] ?? v.evidenceLevel}</Chip>
{v.verbatim ? null : <Chip>智能体归纳</Chip>}
{v.sourceKind ? <Chip>{ENRICH_SOURCE_KIND[v.sourceKind] ?? v.sourceKind}</Chip> : null}
{v.confidence !== undefined ? (
<span className="text-meta tabular-nums text-muted-foreground">
置信度 {Math.round(v.confidence * 100)}%{v.hosts && v.hosts > 1 ? ` · ${v.hosts} 个网站` : ""}
</span>
) : null}
<SourceLink url={v.sourceUrl} />
</div>
</div>
@@ -84,6 +98,7 @@ const FIELDS: EnrichFieldName[] = [
"year",
"platform",
"episodeCount",
"cast",
"genre",
"synopsis",
"posterUrl",
@@ -134,7 +149,8 @@ export function EnrichSection({ item }: { item: IdentityPoolItem }) {
>
{!out ? (
<p className="text-caption text-muted-foreground">
TMDB 查不到时,按标题搜索网页,只读平台和百科网站,抽取字段并附出处。先看建议,确认后才建作品。
TMDB
查不到时,按标题搜索网页:读平台和百科网站,其它网站只用搜索摘要,抽取字段并附出处和置信度。先看建议,确认后才建作品。
</p>
) : (
<div className="space-y-3">
@@ -162,6 +178,7 @@ export function EnrichSection({ item }: { item: IdentityPoolItem }) {
<SourceLink url={x.url} />
<span>
{x.fetched ? "已读取" : x.skipped ? (ENRICH_SKIPPED[x.skipped] ?? x.skipped) : "未读取"}
{x.snippetUsed ? ",用了搜索摘要" : ""}
</span>
</li>
))}
+13
View File
@@ -281,6 +281,16 @@ export const ENRICH_FIELD_LABEL: Record<string, string> = {
synopsis: "简介",
posterUrl: "海报",
genre: "类型",
cast: "主演",
};
/** 补全来源类别(META-2) */
export const ENRICH_SOURCE_KIND: Record<string, string> = {
encyclopedia: "百科",
platform: "平台页",
douban: "豆瓣摘要",
snippet: "搜索摘要",
other: "其它网站",
};
/** 补全来源链接被跳过的原因 */
@@ -290,6 +300,9 @@ export const ENRICH_SKIPPED: Record<string, string> = {
irrelevant: "标题对不上",
limit: "超出读取条数",
error: "读取失败",
snippet_only: "只用搜索摘要",
robots: "网站不允许抓取",
blocked: "要求登录或验证",
};
export const UNIT_KIND: Record<string, string> = { season: "季", episode: "集", special: "特别篇", part: "分段" };