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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 15:57:48 +08:00
1 parent 18a5cf1de7
commit f4666a4671
36 files changed
+2514 -25

No files matched your search

+27 -2
View File
@@ -136,7 +136,7 @@ export type ExternalIdentityVerifyStatus = "unverified" | "ok" | "not_found" | "
export type FeedSourcePurpose = "movie" | "series" | "variety" | "promotion" | "search" | "generic";
export type FeedSourceKind = "rss" | "atom" | "list" | "query";
export type FeedSourceHealthStatus = "unknown" | "healthy" | "degraded" | "failing" | "circuit_open" | "disabled";
export type FieldOverrideSource = "tmdb" | "user" | "agent" | "nfo" | "site" | "import";
export type FieldOverrideSource = "tmdb" | "user" | "agent" | "nfo" | "site" | "import" | "web";
export type FieldOverrideActorType = "user" | "agent" | "system" | "api_key";
export type IdentityCandidateStatus = "pending" | "resolved" | "dismissed" | "archived";
export type IdentityCandidateReason = "ambiguous" | "no_match" | "proposed" | "lookup_failed" | "unmatched";
@@ -160,7 +160,9 @@ export type MediaEntityKind = "movie" | "series" | "variety" | "collection" | "o
export type MediaEntityStatus = "provisional" | "active" | "merged" | "archived";
export type MediaServerInstanceKind = "emby" | "jellyfin";
export type MediaServerInstanceHealthStatus = "unknown" | "healthy" | "degraded" | "offline";
export type MetadataFieldSource = "tmdb" | "user" | "agent" | "nfo" | "site" | "import";
export type MetadataEnrichmentStatus = "suggested" | "applied" | "failed";
export type MetadataEnrichmentTrigger = "manual" | "auto" | "agent";
export type MetadataFieldSource = "tmdb" | "user" | "agent" | "nfo" | "site" | "import" | "web";
export type ObservationStatus = "new" | "linked" | "ignored" | "error";
export type OrganizationFileResultRole = "main" | "subtitle" | "audio" | "bluray_dir" | "nfo" | "image" | "other";
export type OrganizationFileResultMode =
@@ -924,6 +926,24 @@ export interface MediaServerInstanceTable {
updated_at: GeneratedTimestamptz;
}
export interface MetadataEnrichmentTable {
id: Generated<string>;
identity_candidate_id: string | null;
normalized_title: string;
query: Json;
status: Generated<MetadataEnrichmentStatus>;
trigger: Generated<MetadataEnrichmentTrigger>;
suggestion: Json | null;
error: string | null;
usage: Generated<Json>;
media_entity_id: string | null;
applied_at: Timestamptz | null;
applied_by: string | null;
created_by: string | null;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
}
export interface MetadataFieldTable {
id: Generated<string>;
media_entity_id: string | null;
@@ -936,6 +956,7 @@ export interface MetadataFieldTable {
fetched_at: GeneratedTimestamptz;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
evidence: Json | null;
}
export interface NamingTemplateVersionTable {
@@ -1671,6 +1692,7 @@ export interface DB {
media_asset: MediaAssetTable;
media_entity: MediaEntityTable;
media_server_instance: MediaServerInstanceTable;
metadata_enrichment: MetadataEnrichmentTable;
metadata_field: MetadataFieldTable;
naming_template_version: NamingTemplateVersionTable;
observation: ObservationTable;
@@ -1832,6 +1854,9 @@ export type MediaEntityUpdate = Updateable<MediaEntityTable>;
export type MediaServerInstanceRow = Selectable<MediaServerInstanceTable>;
export type NewMediaServerInstance = Insertable<MediaServerInstanceTable>;
export type MediaServerInstanceUpdate = Updateable<MediaServerInstanceTable>;
export type MetadataEnrichmentRow = Selectable<MetadataEnrichmentTable>;
export type NewMetadataEnrichment = Insertable<MetadataEnrichmentTable>;
export type MetadataEnrichmentUpdate = Updateable<MetadataEnrichmentTable>;
export type MetadataFieldRow = Selectable<MetadataFieldTable>;
export type NewMetadataField = Insertable<MetadataFieldTable>;
export type MetadataFieldUpdate = Updateable<MetadataFieldTable>;