import type { EnrichFieldName, IdentityPoolItem, MetadataEnrichOutput } from "@umt/contracts"; import { ExternalLink, LoaderCircle, Sparkles } from "lucide-react"; import { useId, useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { enrichPosterUrl, useApplyEnrich, useEnrichPool } from "~/api/identity.ts"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { consolePath } from "~/components/console/nav.ts"; 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, ENRICH_SOURCE_KIND, EVIDENCE_LEVEL } from "./labels.ts"; import { Chip } from "./parts.tsx"; // META-1 待识别资源 → 元数据补全:先看建议(网页搜索 + 白名单网页 + Agent 抽取,不改作品),再应用(建本地临时作品并关联)。 type Suggestion = NonNullable; 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; try { return new URL(u).hostname.replace(/^www\./, ""); } catch { return null; } }; function SourceLink({ url }: { url: string | null }) { const host = hostOf(url); if (!url || !host) return 无出处; return ( {host} ); } function valuesOf(s: Suggestion, k: EnrichFieldName): Value[] { const v = s.fields[k] as Value | Value[] | null; return Array.isArray(v) ? v : v ? [v] : []; } function FieldRow({ k, s, enrichmentId }: { k: EnrichFieldName; s: Suggestion; enrichmentId: string }) { const vals = valuesOf(s, k); if (!vals.length) return null; return (
  • {ENRICH_FIELD_LABEL[k] ?? k}
    {k === "posterUrl" ? ( 海报 ) : null} {vals.map((v, i) => (
    {k !== "posterUrl" ?

    {String(v.value)}

    : null}
    {EVIDENCE_LEVEL[v.evidenceLevel] ?? v.evidenceLevel} {v.verbatim ? null : 智能体归纳} {v.sourceKind ? {ENRICH_SOURCE_KIND[v.sourceKind] ?? v.sourceKind} : null} {v.confidence !== undefined ? ( 置信度 {Math.round(v.confidence * 100)}%{v.hosts && v.hosts > 1 ? ` · ${v.hosts} 个网站` : ""} ) : null}
    ))}
  • ); } const FIELDS: EnrichFieldName[] = [ "title", "aliases", "year", "platform", "episodeCount", "cast", "genre", "synopsis", "posterUrl", ]; export function EnrichSection({ item }: { item: IdentityPoolItem }) { const uid = useId(); const enrich = useEnrichPool(); const apply = useApplyEnrich(); const [out, setOut] = useState(null); const [lock, setLock] = useState(false); const s = out?.suggestion ?? null; const run = async (fresh: boolean) => { try { const r = await enrich.mutateAsync({ id: item.id, ...(fresh ? { fresh: true } : {}) }); setOut(r); if (r.status === "failed") toast.error(r.error ?? "没有找到可用的信息"); } catch (err) { toast.error(errorMessage(err, "补全失败")); } }; const doApply = async (mediaEntityId?: string) => { if (!out) return; try { const r = await apply.mutateAsync({ id: item.id, enrichmentId: out.enrichmentId, lock, ...(mediaEntityId ? { mediaEntityId } : {}), }); toast.success(r.created ? "已建临时作品并关联" : "已并入已有作品并关联"); setOut({ ...out, status: "applied", appliedMediaEntityId: r.mediaEntityId }); } catch (err) { toast.error(errorMessage(err, "应用失败")); } }; return ( run(false)}> {enrich.isPending ? : } {out ? "重新载入" : "补全"} } > {!out ? (

    TMDB 查不到时,按标题搜索网页,从平台和百科页面抽取字段,附出处和置信度。确认后才建作品。

    ) : (

    {out.notice} {" · "} {out.cached ? "7 天内的缓存结果" : `搜索 ${out.usage.searchCalls} 次,读取网页 ${out.usage.fetchCalls} 次`} {s ? ` · ${s.extractor === "model" ? `智能体抽取${s.model ? `(${s.model})` : ""}` : "规则抽取"}` : ""} {` · 今日 ${out.budget.usedToday}/${out.budget.dailyLimit}`}

    {out.error ?

    {out.error}

    : null} {s ? (
      {FIELDS.map((k) => ( ))}
    ) : null} {s?.sources.length ? (
    搜索到的链接({s.sources.length})
      {s.sources.map((x) => (
    • {x.fetched ? "已读取" : x.skipped ? (ENRICH_SKIPPED[x.skipped] ?? x.skipped) : "未读取"} {x.snippetUsed ? ",用了搜索摘要" : ""}
    • ))}
    ) : null} {out.appliedMediaEntityId ? ( ) : out.status === "suggested" && s ? (
    {out.existing.length ? (

    已有同名作品。点「应用」时年份不冲突的自动并入,也可以指定一个:

    {out.existing.map((e) => (
    {e.title} {e.year ? ` (${e.year})` : ""}
    ))}
    ) : null}
    ) : ( )}
    )}
    ); }