Files
UHD_Media_Tool/apps/web/app/components/library/labels.ts
T
rootandClaude Opus 5.5 f4666a4671 feat(identity): web metadata enrichment for titles missing on TMDB (META-1)
- metadata.enrich (propose, low, replay safe): normalized-title cache (kv_cache
  region enrich, 7 days) -> daily budget -> web.search (title + 短剧 / 电视剧 平台,
  second query limited to whitelist via site:) -> web.fetch of up to 3 relevant
  whitelisted pages (registered site domains never read, redirects out of the
  whitelist dropped) -> upstream model extraction with rule fallback -> server-side
  evidence check (rule_confirmed / multi_evidence / needs_review). Suggestion only.
- POST /identity-pool/{id}/enrich (suggest) and /enrich/apply (console session):
  provisional entity or same-title merge, season/episode units, field-level
  metadata with source web/agent + evidence, aliases, release assignment.
  Poster proxy GET /img/enrich/{id} (og:image from whitelisted pages only).
- Auto-enrich once per pool entry when subscription/rule lookups find nothing on
  TMDB (identity.autoEnrich, off by default); identity.enrichDailyBudget,
  identity.enrichDomains settings; web.fetch now returns og:image.
- Pool sheet "元数据补全" section; entity detail shows evidence links.
- Migration 0049_enrichment (metadata_enrichment, metadata_field.evidence, source
  web). Requires image rebuild / api + worker restart to take effect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 15:57:48 +08:00

311 lines
11 KiB
TypeScript

import type {
AcquisitionPriority,
DecisionKind,
DecisionStatus,
DownloadStatus,
IdentityCandidateReason,
IdentityStatus,
LocationStatus,
MediaEntityStatus,
MediaKind,
OrganizationStatus,
PolicyStatus,
PreferenceKind,
PreferenceOrigin,
PreferenceStatus,
SeasonScope,
Severity,
SlotStatus,
SubscriptionMode,
SubscriptionStatus,
} from "@umt/contracts";
import type { StatusTone } from "~/components/console/status-badge.tsx";
// W3-2 各页共用的取值 → 中文文案 / 状态语气。状态色只表示状态,文案必填。
type ToneLabel = { label: string; tone: StatusTone };
export const MEDIA_KIND: Record<MediaKind, string> = {
movie: "电影",
series: "剧集",
variety: "综艺",
collection: "合集",
other: "其他",
};
export const ENTITY_STATUS: Record<MediaEntityStatus, ToneLabel> = {
provisional: { label: "临时建档", tone: "warning" },
active: { label: "正常", tone: "success" },
merged: { label: "已合并", tone: "neutral" },
archived: { label: "已归档", tone: "neutral" },
};
/** Slot 状态(REQ §6.1):每个状态有明确文本,不只靠颜色 */
export const SLOT_STATUS: Record<SlotStatus, ToneLabel> = {
empty: { label: "空缺", tone: "neutral" },
reserved: { label: "已预留", tone: "neutral" },
downloading: { label: "下载中", tone: "neutral" },
verifying: { label: "核验中", tone: "neutral" },
satisfied: { label: "已满足", tone: "success" },
upgrade_pending: { label: "等待升级", tone: "warning" },
attention: { label: "需处理", tone: "destructive" },
};
export const SLOT_STATUS_ORDER: SlotStatus[] = [
"attention",
"empty",
"upgrade_pending",
"reserved",
"downloading",
"verifying",
"satisfied",
];
export const IDENTITY_STATUS: Record<IdentityStatus, ToneLabel> = {
accepted: { label: "已确认", tone: "success" },
proposed: { label: "待确认", tone: "warning" },
conflicted: { label: "冲突", tone: "destructive" },
retired: { label: "已退役", tone: "neutral" },
};
export const EVIDENCE_LEVEL: Record<string, string> = {
manual: "人工确认",
rule_confirmed: "规则确认",
multi_evidence: "多证据",
needs_review: "待复核",
needs_confirm: "待确认",
rejected: "已排除",
};
export const VERIFY_STATUS: Record<string, ToneLabel> = {
unverified: { label: "未校验", tone: "neutral" },
ok: { label: "校验通过", tone: "success" },
not_found: { label: "外部不存在", tone: "warning" },
type_mismatch: { label: "类型不符", tone: "destructive" },
error: { label: "校验出错", tone: "warning" },
};
export const LOCATION_STATUS: Record<LocationStatus, ToneLabel> = {
available: { label: "可访问", tone: "success" },
unavailable: { label: "暂不可访问", tone: "warning" },
suspected_missing: { label: "疑似缺失", tone: "warning" },
confirmed_missing: { label: "已确认缺失", tone: "destructive" },
removed: { label: "已移除", tone: "neutral" },
};
export const LOCATION_KIND: Record<string, string> = {
library: "媒体库",
download_cache: "下载缓存",
hardlink: "硬链接",
external: "外部",
};
export const DOWNLOAD_STATUS: Record<DownloadStatus, ToneLabel> = {
queued: { label: "排队", tone: "neutral" },
submitting: { label: "提交中", tone: "neutral" },
downloading: { label: "下载中", tone: "neutral" },
paused: { label: "已暂停", tone: "warning" },
stalled: { label: "无进展", tone: "warning" },
completed: { label: "下载完成", tone: "neutral" },
importing: { label: "入库中", tone: "neutral" },
imported: { label: "已入库", tone: "success" },
failed: { label: "失败", tone: "destructive" },
removed: { label: "已移除", tone: "neutral" },
missing: { label: "下载器中缺失", tone: "warning" },
};
export const ORG_STATUS: Record<OrganizationStatus, ToneLabel> = {
queued: { label: "排队", tone: "neutral" },
running: { label: "整理中", tone: "neutral" },
succeeded: { label: "核验通过", tone: "success" },
partial: { label: "部分成功", tone: "warning" },
failed: { label: "失败", tone: "destructive" },
cancelled: { label: "已取消", tone: "neutral" },
};
export const CANDIDATE_DECISION: Record<string, ToneLabel> = {
accept: { label: "接受", tone: "success" },
upgrade: { label: "可升级", tone: "success" },
reject: { label: "拒绝", tone: "destructive" },
unknown_field: { label: "字段未知", tone: "warning" },
skipped: { label: "跳过", tone: "neutral" },
deferred: { label: "延后:已有计划在途", tone: "warning" },
};
export const DECISION_KIND: Record<DecisionKind, string> = {
identity_ambiguous: "身份歧义",
identity_unresolved: "待识别",
download_stalled: "下载无进展",
download_failed: "下载失败",
path_conflict: "路径冲突",
space_insufficient: "空间不足",
approval_required: "需要审批",
metadata_conflict: "元数据冲突",
source_unreachable: "来源不可达",
other: "其他",
};
export const DECISION_STATUS: Record<DecisionStatus, ToneLabel> = {
open: { label: "待处理", tone: "warning" },
investigating: { label: "Agent 调查中", tone: "neutral" },
awaiting_user: { label: "等你决定", tone: "warning" },
resolved: { label: "已处理", tone: "success" },
dismissed: { label: "已忽略", tone: "neutral" },
expired: { label: "已过期", tone: "neutral" },
};
export const SEVERITY: Record<Severity, ToneLabel> = {
info: { label: "提示", tone: "neutral" },
warning: { label: "警告", tone: "warning" },
critical: { label: "严重", tone: "destructive" },
};
export const POOL_REASON: Record<IdentityCandidateReason, ToneLabel> = {
ambiguous: { label: "多个候选", tone: "warning" },
no_match: { label: "没有匹配", tone: "neutral" },
proposed: { label: "待确认", tone: "warning" },
lookup_failed: { label: "查询失败", tone: "destructive" },
unmatched: { label: "未命中订阅", tone: "neutral" },
};
export const POOL_STATUS: Record<string, ToneLabel> = {
pending: { label: "待识别", tone: "warning" },
resolved: { label: "已识别", tone: "success" },
dismissed: { label: "已忽略", tone: "neutral" },
archived: { label: "已归档", tone: "neutral" },
};
export const PREF_KIND: Record<PreferenceKind, { label: string; hint: string }> = {
soft: { label: "软偏好", hint: "影响排序,不排除" },
hard: { label: "硬规则", hint: "命中即排除" },
version: { label: "版本偏好", hint: "同一作品内选哪种版本" },
target: { label: "版本目标", hint: "为作品新建一个版本目标" },
};
export const PREF_ORIGIN: Record<PreferenceOrigin, string> = {
manual: "手动",
conversation: "对话确认",
agent_inferred: "Agent 推断",
};
export const PREF_STATUS: Record<PreferenceStatus, ToneLabel> = {
active: { label: "生效中", tone: "success" },
disabled: { label: "已停用", tone: "neutral" },
deleted: { label: "已删除", tone: "neutral" },
};
export const POLICY_STATUS: Record<PolicyStatus, ToneLabel> = {
draft: { label: "草案", tone: "warning" },
published: { label: "已发布", tone: "success" },
superseded: { label: "已被替代", tone: "neutral" },
rolled_back: { label: "已回滚", tone: "neutral" },
archived: { label: "已归档", tone: "neutral" },
};
export const SUB_MODE: Record<SubscriptionMode, string> = { once: "单次收录", continuous: "持续追踪" };
export const SEASON_SCOPE: Record<SeasonScope, string> = {
current_season: "仅本季",
following_seasons: "接续后续季",
};
export const PRIORITY: Record<AcquisitionPriority, string> = {
immediate: "立即",
tracking: "追更",
new_work: "新作品",
upgrade: "升级",
};
export const SUB_STATUS: Record<SubscriptionStatus, ToneLabel> = {
active: { label: "进行中", tone: "success" },
paused: { label: "已暂停", tone: "warning" },
pending: { label: "待定(元数据未就绪)", tone: "warning" },
completed: { label: "已完成", tone: "neutral" },
cancelled: { label: "已取消", tone: "neutral" },
};
export const SUB_ORIGIN: Record<string, string> = { user: "用户", agent: "Agent", policy: "策略", import: "导入" };
export const METADATA_SOURCE: Record<string, string> = {
tmdb: "TMDB",
user: "人工",
agent: "Agent",
nfo: "NFO",
site: "站点",
import: "导入",
web: "网页",
};
/** 可覆盖/锁定的作品字段(contracts EntityMetadataField) */
export const ENTITY_FIELD_LABEL: Record<string, string> = {
title: "标题",
originalTitle: "原名",
year: "年份",
overview: "简介",
releaseDate: "上映/首播日期",
originalLanguage: "原始语言",
originCountry: "出品地区",
posterPath: "海报",
backdropPath: "背景图",
platform: "播出平台",
episodeCount: "集数",
genres: "类型",
posterUrl: "网页海报",
};
/** 补全字段(contracts EnrichFieldName) */
export const ENRICH_FIELD_LABEL: Record<string, string> = {
title: "标题",
aliases: "别名",
year: "年份",
platform: "播出平台",
episodeCount: "集数",
synopsis: "简介",
posterUrl: "海报",
genre: "类型",
};
/** 补全来源链接被跳过的原因 */
export const ENRICH_SKIPPED: Record<string, string> = {
not_allowed: "不在白名单",
site: "已登记站点",
irrelevant: "标题对不上",
limit: "超出读取条数",
error: "读取失败",
};
export const UNIT_KIND: Record<string, string> = { season: "季", episode: "集", special: "特别篇", part: "分段" };
export const SEGMENT: Record<string, string> = {
full: "",
upper: "上",
middle: "中",
lower: "下",
unknown: "分段未知",
};
export const CONTENT_TYPE: Record<string, string> = {
main: "正片",
extra: "花絮",
behind_the_scenes: "幕后",
performance_only: "纯享",
preview: "预告",
reaction: "reaction",
unknown: "类型未知",
};
export const opts = <K extends string>(rec: Record<K, string | { label: string }>) =>
(Object.keys(rec) as K[]).map((value) => {
const v = rec[value];
return { value, label: typeof v === "string" ? v : v.label };
});
/** 版本目标 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
const SLOT_REASON: Record<string, string> = {
"blocked:no_main_video": "没有可入库的主视频",
"deferred:slot_busy": "已有计划在途,等它结束再评估",
verified: "已核验入库",
verified_partial: "部分核验入库",
hard_conditions_met: "硬条件全部满足",
};
export function slotReasonText(raw: string): string {
return raw
.split(";")
.filter((p) => !p.startsWith("inflight:"))
.map((p) => SLOT_REASON[p] ?? p)
.join(";");
}