Files
UHD_Media_Tool/docs/runbook/identity.md
T

52 KiB

资源识别与待识别池运行手册(DEC-1)

新观察到的资源(RSS、站点列表快照、搜索结果)先进待识别池,只在有理由时才查 TMDB。只有和订阅或本地作品相关、 又需要你决定的资源才开内部事项(2026-10-06 起待处理中心取消:搜索 / RSS 扫进来的未识别资源静默留在「未识别」里,不提示, 要处理的在媒体库「未识别」里选候选或手动指定)。对照 MoviePilot:RSS / 站点资源只在匹配订阅或用户搜索时才识别,识别失败只写日志、不告警 (app/chain/subscribe/match.py _prepare_match_torrents:未识别的种子最多重试 3 次,之后跳过)。

需求依据:REQ §4.2–4.3(观察不等于事实)、§6.3(新作品收录 / 追更补缺 / 升级都由规则触发)、§7.5(待识别池可检索、 不污染正式库、有界复查、不留死数据)、§13(待处理按作品与问题类型合并)。

识别流程(resolveObservation)

  1. 解析标题(release-parser),本地召回:显式外部 ID、title_alias 精确 / pg_trgm ≥ 0.45(本地作品 = 订阅、追踪、已登记的作品)。

  2. 判断是否查 TMDB(lookup,记在 source_release.resolution_evidence.lookup 和池条目 query.lookup):

    lookup 条件
    manual 任务输入 trigger: "manual":池里点「重新识别」、Agent / 能力 identity.resolve
    search 任务输入 trigger: "search":搜索结果
    takeover 接管的下载任务按文件名识别(identifyParsed(…, { trigger: "takeover" }),见下文「接管任务按文件名识别」)
    tracked_alias 本地召回有候选(标题命中订阅 / 本地作品的别名)
    previously_looked_up 周期复查,且这条池条目以前查过 TMDB(旧数据、手动、规则触发)
    eager 设置 identity.eagerTmdbLookup = true
    new_title_rule 有已发布、开启 newTitle 且有有效持续授权的获取策略,资源类型在规则的 mediaKinds 内,标题年份(若有)不早于窗口能覆盖的年份

    都不满足:只进池,reason = unmatched,不查 TMDB,不开待处理。

  3. 查了 TMDB 的,召回 TMDB 候选(recallTmdbCandidates:中文名 → 英文名,按类型单语言搜索,标题对不上的按年份相近度拉前几个详情比对别名 / 译名), 交给 domain.resolveIdentity 打分:auto 建档 / 关联并发 release.resolved;否则进池。打分规则与 MP 的对照见 docs/porting/moviepilot-identity-match.md(ID-2)。 站点列表 / 详情页给出的 IMDb / TMDB ID(raw._site / raw._detail)作为 trustedIds 参与打分(ID-3):候选按这个 ID 取来时, 标题对不上也至少 multi_evidence(可自动确认);年份冲突降为 needs_confirm;续作序号 / 类型冲突不放宽。 标题认不准时走「识别升级链」(见下节)。

  4. 自动绑定时把站点标题登记为作品别名,只在标题精确命中(或别名命中且另一种文字的名称也命中)时登记,分类词 / 单字 / 停用词不登记。 FIX-5 守卫(service/alias-guard.ts):逐个名称再查一遍,以下任一成立就不登记,原因记在 resolution_attempt.best.aliases.skipped (added 是登记了的):证据等级不是 rule_confirmed(low_evidence);资源年份与作品年份差超过 1 年且不是季年份(year_conflict); 作品有已接受的外部身份,且同一种文字(中文 / 拉丁)下已有确认的外部标题(主标题 / 原标题 / TMDB 译名与别名),这个名称对不上 (conflicts_external_title);这个名称已经挂在别的作品上(owned_by_other)。

标题更新(RSS 标题与列表标题不同、站点改标题):按新标题重新解析,发 release.observed(isNew=false),以 force 重新识别 (已识别的也不短路,触发来源仍是 observed);重新识别不能确认时撤销原自动绑定、进池。人工锁定的不动。

任务输入 trigger 缺省:force 为 true 视为 review(池复查),否则 observed。 搜索模块想让搜索结果直接识别,入队 resolveObservation 时传 trigger: "search"(目前搜索结果经 release.observed 进来, 按 observed 处理;命中订阅别名的照样会查)。

识别升级链:用站点详情页的 ID(ID-3)

用户反馈(2026-09-30):"既然能抓 PT 站详情页,详情有 TMDB、IMDb 或豆瓣,为啥还判断不了是什么资源?" 以前详情页只在用户打开时才抓, 标题识别不出就直接进待识别 / 待处理。现在标题识别结果是 ambiguous / no_match 时按下面的链再认一次 (service/id-escalation.ts、resolve.ts escalateIdentity):

  1. 抓详情(只在资源还没抓过详情时,detail_fetched_at 为空):走 refreshReleaseDetail——站点详情缓存、共享限流器、请求计数、 凭据失效处理都照旧;失败不阻塞识别,记在 raw._escalation {at, status, error},6 小时内不再为识别重抓。
  2. 新拿到的 IMDb / TMDB ID → TMDB 详情 / find → 按 trustedIds 打分(列表上已有的 ID 在第一轮就用过)。
  3. 还不能确认且有豆瓣 ID → kv_cache region douban.subject 里缓存的豆瓣条目(标题 / 别名 / 年份)→ 按条目名称 + 年份召回 TMDB → ID-2 打分(对照 MP chain/media/projection.py 豆瓣 → TMDB:原名 / 中文名 / 英文名 + 年份)。不请求豆瓣;条目来源只有两个: 站点详情页的豆瓣信息块(◎片名 / ◎译名 / ◎年代,详情页同时给了豆瓣 ID 时写入,source = site_detail)和元数据补全(META-2) 网页搜索结果里 douban.com/subject/<id> 的搜索标题与摘要(source = web_snippet,不覆盖 site_detail 的)。缓存 30 天。
  4. 确定(auto)→ 照常绑定:回填资源、建 / 关联作品、关闭同资源的池条目并从合并事项里摘除(组空即关)。站点标题登记为别名时 按资源自己的标题重新判断(靠 ID / 豆瓣名确认、标题对不上的不登记),FIX-5 守卫照旧。

证据记在 source_release.resolution_evidence.escalation 与池条目 query.escalation:detail(fetched / skipped:<原因> / deferred:cap_reached / failed:fetch_failed / not_now:batch_ingest)、via(site_id / douban / null)、ids、pending、douban。

什么时候抓详情

触发 抓详情?
搜索结果(trigger: "search",搜索完成后逐条入队识别) 是
手动「重新识别」、Agent identity.resolve(manual) 是
池条目「元数据补全」之前(POST /identity-pool/{id}/enrich) 是:资源没抓过详情或有豆瓣 ID 时先按手动重新识别走一次;认出来就返回 409「已识别为《X》,不需要补全」,不消耗补全预算
订阅相关的候选(命中订阅 / 本地作品别名,tracked_alias)的池复查(review) 是
RSS / 列表批量入库(observed) 否。订阅相关的推迟(not_now:batch_ingest,15 分钟内复查时再抓);其余不抓
上次因限额推迟(pending)的复查 是

每站每小时上限 identity.detailIdsPerSiteHour(设置 → 元数据 → 资源识别「识别时每站每小时最多抓详情页」,缺省 10,0 = 不抓), 计数在 kv_cache region identity.detail_ids(key <siteId>:<小时>,原子自增,2 小时过期)。达到上限记 deferred:cap_reached、 15 分钟内复查。这些详情请求同样计入站点请求统计(site_request_count)。

待处理

  • 升级链推迟中(pending:每站每小时限额已满 / 入库时不抓):不开任何身份事项,之前开的从合并事项摘除。抓了但失败的不算推迟(不阻塞,照常进池 / 开事项)。
  • 资源上已有 IMDb / TMDB / 豆瓣 ID 却仍无结果:不开「没识别出作品」(identity_unresolved),只留在池里,可手动指定。
  • 有候选的歧义(identity_ambiguous)照旧开;资源被识别后(升级链 / 复查 / 手动)自动从合并事项摘除,组空即关闭。

排查

-- 最近为识别抓详情的资源
select id, title, raw->'_escalation' esc, resolution_evidence->'escalation' ev from source_release
 where raw ? '_escalation' order by (raw->'_escalation'->>'at') desc limit 20;
-- 这一小时各站点用了多少名额
select key, value from kv_cache where region = 'identity.detail_ids' and expires_at > now();
-- 缓存的豆瓣条目
select key, value->>'title' title, value->>'year' y, value->>'source' src from kv_cache where region = 'douban.subject' limit 20;

测试:apps/server/test/sources-identity/id-escalation.test.ts(观众 DET-2 / 憨憨 HH-2 详情样本经注入 fetch、TMDB mock,零真实请求); 差分组 packages/domain/test/differential/identity/site-ids-*.json(MP search/result.py _match_source:种子 IMDb 与作品一致即命中; 我们年份 / 类型冲突时只作候选,登记为 intentional,见 site-ids-deviations.json),packages/domain/test/identity-site-ids.test.ts。

接管任务按文件名识别(QB-1)

用户手动加进 qB、贴了管理标签的任务(download_task.origin = adopted,没有 source_release / 计划)下载完成后,verifyAndImport 的 planImport 先按 MP transfer 的做法识别作品(acquisition-organize/services/takeover.ts):

  1. 主视频 = 要下载(priority≠0)的视频文件里最大的一个(跳过 sample);parseReleasePath(主视频路径) 合并 文件 → 上级 → 上上级 目录名(qB 文件名含种子根目录,相当于 MP MetaInfoPath),再用种子名补缺(mergeParsed);文件名解析不出标题时直接用种子名。
  2. identifyParsed(service/resolve.ts):同一套 ID-2 打分(本地召回 + TMDB 候选 → resolveIdentity),trigger: "takeover" 视为 允许查 TMDB;不写待识别池、不开身份事项。auto 且自动化开启 → 关联 / 新建作品(新建的入队一次 full 刷新),回填 download_task.media_entity_id,直接整理。
  3. 失败 / 歧义 / 自动化关闭:整理记录 identity_unknown,事项 organize_identity:<下载任务 id>(identity_ambiguous), 证据带种子名、主视频、解析出的标题 / 年份 / 季集、识别结果与候选。选项:候选(最多 3 个,本地作品或 TMDB)、 「选择作品(本地)」(requires: mediaEntityId)、「选择作品(TMDB)」(requires: tmdb,本地没有时建档)、 「作为临时作品入库」(建 provisional 作品 + 别名)、「忽略」。
  4. 处理(POST /decisions/{id}/resolve):整理模块注册的执行钩子(modules/decision-hooks.ts)在关闭前同步执行 —— 回填 download_task.media_entity_id → 冻结手动整理计划 → 入队执行;执行结果记在 resolution.execution。缺输入 / 作品不存在 → 400 / 404,事项保持打开。

待处理只收相关的

结果 开事项?
unmatched 否
ambiguous / proposed(查过 TMDB,有候选) 是,identity_ambiguous,级别 warning
no_match / 复查已停的 lookup_failed 只有本地召回有候选(与本地作品相关)、且资源上没有站点给的 IMDb / TMDB / 豆瓣 ID 时开 identity_unresolved,级别 warning
识别升级链推迟中(ID-3) 否(之后会抓详情再认)

合并:dedupeKey identity-group:<siteId>:<归一化标题>,同来源同标题的资源并成一条。成员 = identity_candidate.decision_id 指向该事项且仍 pending 的条目。标题 1 条时写资源原标题,多条时写「来源「X」的 N 条资源待识别 / 待确认身份:<标题>」, evidence.releases 列前 50 条,evidence.count 为总数。

  • 处理(选候选 / 手动 TMDB ID / 选本地作品):先处理代表资源(subject),再把确定的作品套到组内其它资源(都加人工锁)。
  • 忽略:组内每条池条目都 dismissed。
  • 组内某条被单独识别 / 指定 / 忽略:从组里摘除,代表资源和候选选项换成剩下的最近一条;组空了才关事项。
  • 旧式单条事项(identity:<releaseId>)在该资源下次复查时并入合并事项并关闭。

收件箱:顶栏角标和待处理页缺省只算 warning 及以上(minSeverity=warning),"仅信息"类在待处理页「级别 → 含仅信息」里看。 待处理页按问题类型 + 作品 / 来源分组,前 3 组展开,其余折叠;合并事项卡片可展开组内资源。

集数映射怎么用(集数解析批次 4)

资源绑定到剧集 / 综艺后,先按作品季表自动判定「媒体库该用哪一集」:原号对得上就用原号;元数据还没覆盖到(最新季未完结、总集数未知)先按原号放行;否则试季内 / 跨季绝对集号、按播出日期换算,唯一解才采用。都不行就是集数待确认:资源照常绑定作品,但不建集、订阅跳过、整理不按原号入库,并按「作品 + 来源季 + 发布组」开一条内部事项(只给关注中的作品开,见下面「两层」)。判定细节见 docs/plan/episode-resolution.md。

两层(2026-10-06):确定性校验 / 换算对所有已绑定作品的资源照做(无外部请求、无打扰);有成本或打扰的后续动作——拉 TMDB、 自动新建作品后的元数据刷新、开集数待确认事项、派智能体——只对关注中的作品做(isEntityWatched,service/watched.ts): 有进行中订阅(active / pending);用户明确搜索 / 手动识别过(资源 resolution_evidence.lookup 为 search / manual)、手动下载过 (acquisition_plan.evaluation.manual)或人工 / 智能体确认过外部身份(evidence_level manual);正在下载(未移除、没入库完的下载任务) 或整理(排队 / 执行中的整理记录);已在媒体库(有 active 的已入库文件)。RSS / 列表扫进来自动建档、没人关心的作品不排刷新、不开事项。 进入关注范围时补排一次 full 刷新(enqueueWatchedRefresh,幂等键 refreshMetadata:watched:<作品>,只对从没刷新过的作品): 建订阅、用户搜索 / 手动识别命中已有作品、开始下载(submitDownload);刷新会按作品重算待确认 / 滞后放行的资源,换算不了的这时才开事项。 集数待确认不再是给用户逐条处理的条目:原因在订阅详情「集数映射」与作品页「集数映射」分区按组显示,建议规则 / 智能体建议在作品页一键确认。

规则在作品页「集数映射」分区管理(GET/POST /media-entities/{id}/episode-mapping-rules、PATCH/DELETE /episode-mapping-rules/{id}):

  • 作用域:作品必填,来源季、发布组可空(空 = 任意);越具体越优先。同作品 + 季 + 发布组(发布组忽略大小写)只能有一条启用的规则,冲突返回 409,先停用或修改旧的那条。
  • 类型:偏移表达式(EP-12、EP*2-1,省略 EP 按 MP 约定补全,可指定目标季)、对照表(每行 26=S02E01,区间 27-28=S02E02,区间不能重叠)、特别篇 / 花絮 / 忽略。表达式解析不了、表格区间不合法直接拒绝。
  • 每次新建 / 修改 / 停用 / 启用 / 删除都在同一事务里按作品重算已绑定资源(人工锁定的不动),返回重算条数、变化条数、待确认转已解决条数;资源解决后对应的事项自动关闭。
  • 规则算出的集数对不上季表时自动停用(停用原因写当时的来源季集)并重新开事项;手动停用的原因写「手动停用」。

模拟预览(POST /media-entities/{id}/episode-mapping/preview,纯读):取作品下最近 100 条已绑定资源(按发布时间倒序,可按季 / 发布组过滤;季按来源季或规范季任一匹配),按原标题重解析后逐条给出原始季集 → 库里的当前结果 → 套用草稿规则后的结果、校验状态、是否有变化;另给季表摘要、按发布组的方式统计与待确认数、各组待确认样本推出的建议规则。草稿规则会替换同作用域的已有规则(修改时用 replaceRuleId 替换被改的那条)。作品页边改草稿边预览,点「应用」才写入。

作品页「换算不了的资源」(原待处理「集数待确认」卡片,2026-10-06 起显示在作品页集数映射分区):每组列出来源季集、原因、建议规则(同组待确认资源按发布时间对播出日期推出,全部样本成立且唯一才给)与它套用到同组资源的模拟结果。

  • 确认:选项 confirm_suggestion,建议规则写成 expression 规则 → 事项按用户选择关闭 → 按作品重算(同一事务;仍对不上的资源另开新事项)。
  • 修改:跳到作品页集数映射并预填草稿(?mapSeason=&mapGroup=&mapExpr=&mapTarget=#episode-mapping),在那里改完再应用。
  • 忽略:只关闭提醒,资源仍待确认;勾选「同时建忽略规则」时走选项 ignore_rule,建 special=ignore 规则,这一季这个发布组的资源以后不参与集数解析。

处理走 POST /decisions/{id}/resolve 的选项,由 sources-identity 直接执行(与身份类事项同一做法)。订阅详情有该作品 + 季的方式统计与待确认数;新建 / 编辑订阅时如果近期资源里有发布组用绝对集号,表单里提示一句。

整理向导:识别结果里的季集只是按首个本地候选映射后的预填。向导只把用户改过的季集(行内输入、批量季号、集号偏移、弹窗「指定季号」)发给服务端,当作人工指定;没改过的由服务端按最终作品映射,所以改选作品后预填会清空、改由新作品映射。识别为集数待确认的行(episodeUnresolved)在向导里未决,填季集或排除后才能生成预览。

新建作品的季表:自动识别新建的作品在关注范围内(用户搜索 / 手动识别 / 接管下载,或已被订阅等)才在事务提交后排一次 full 元数据刷新(幂等键 refreshMetadata:created:<作品>),其余等进入关注范围时补排;新建档没有季表时资源先按原号放行(unverified),刷新补齐季表后自动重算为换算结果。

智能体兜底(批次 5):确定性流程(规则 → 原号校验 → 换算)都失败、开了「集数待确认」后,智能体在完整上下文里判断一次,产出一条结构化规则;写入后同类资源由规则自动处理,智能体不逐集调用。

  • 何时触发:设置 → 智能体打开「自动调查」时,新开的 episode_mapping 事项和下载失败一样进后台调查(decision.created → resolveDecision → autoInvestigate),同一事项只跑一次。对话里也可以直接说「《X》第二季集数对不上」,走同样三个能力(Skill「集数映射」)。
  • 三个能力:episode_mapping.inspect(只读:事项内资源、同组相邻资源各最多 50 条,季表每季最多 100 集含播出日期与集标题,现有规则含停用原因,确定性建议)→ episode_mapping.simulate(只读:复用模拟预览,汇总事项内样本是否全部解决、有没有让已对上的资源变成待确认)→ episode_mapping.apply_rule(写)。
  • apply_rule 服务端强制再模拟:样本有一条没解决、或会让当前已对上 / 已换算的资源变成待确认,直接拒绝不写库。通过才写规则:created_by_type = agent,样本 ≥ 3 且全部对得上记「多证据」(multi_evidence),否则「待复核」(needs_review);证据存理由、样本数、模拟摘要与会话 ID;随后按作品重算,资源都解决时事项自动关闭,系统据此核实恢复。
  • 需要什么授权:后台自动调查的模式要是「授权内恢复」(agent.autoInvestigateMode = recover),且有一条覆盖 episode_mapping.apply_rule 的有效持续授权(action 写 episode_mapping.apply_rule 或 *;可用 scope.mediaEntityIds / mediaKinds 限定作品,按事项的作品核对;不随身份识别策略放开)。对话里没有授权时同样被拒,智能体只会说「需要你批准」。
  • 没有授权(或模式是只读调查)时降级只读:智能体在会话里用 simulate(带事项 ID 和理由)试出的通过规则,会作为「智能体建议」写进事项证据(evidence.agentSuggestion,事项刷新时保留),作品页集数映射里显示规则、理由、样本套用后的季集与模拟结果,点「确认智能体建议」(选项 confirm_agent_suggestion)即建规则 + 重算;建之前按当前资源再模拟一次,不通过返回 409 不写。人工确认的规则记操作者为管理员、证据等级「人工确认」。
  • 怎么撤销:作品页「集数映射」里智能体写的规则带「智能体 · 多证据 / 待复核」标记和依据,点「停用」:立即按作品重算(资源回到待确认并重新开事项),且同作用域有手动停用的规则时 apply_rule 一律拒绝,智能体不会再自动写回(仍可留建议给人确认)。直接删除不留这个记号,新开的事项可能再被自动写一次(受同根因自动恢复次数上限 agent.investigateMaxAttempts 约束)。要彻底停掉自动写规则,撤销那条持续授权或把自动调查模式改回只读调查。

剧集组(季集结构)

TMDB 默认季有时和发布者、媒体服务器的分季不一样(最常见:番剧两个 cour 被合成一季 25 集)。TMDB 的「剧集组」(episode group)是编辑整理的其它分法(原始播出 / 绝对集号 / DVD / 数字发行 / 故事线 / 制作季 / 电视)。作品可以选「TMDB 默认季」或某个剧集组作为季集结构,选了之后季表、季 / 集单元、集数映射、订阅、元数据刷新与刮削都按这套季集走。对照 MP 见 docs/porting/moviepilot-episode-groups.md。

在哪选:作品页「季与集」顶部的「季集结构」轨道(TMDB 有剧集组或作品已选了剧集组时才出现)。点一个非当前的结构先预览它的季卡片与每集(已入库按 TMDB 原季集对照),点「改用这套结构」确认后切换。新建订阅选季时同样有轨道 + 季卡片;选了非当前结构的季,确认后等同改作品设置,改完按新结构的同号季回填订阅。编辑订阅时季不能改。

接口:GET /media-entities/{id}/episode-groups(列表 + 当前选择)、GET /media-entities/{id}/episode-structure?groupId=(某套结构的季卡片,纯读)、PUT /media-entities/{id}/episode-group({groupId},null = 改回默认季;scope organize.execute,写审计 media_entity.episode_group)。作品上存 episode_group_id / _name / _set_at / _set_by(迁移 0075);改 TMDB 身份时一并清掉。

剧集组怎么变成季集:组的 order = 季号(重复的顺延),组内按 order 排序后从 1 编集号(同 MP get_tv_group_seasons)。每集记下 TMDB 默认季集号(单元字段 tmdbSeason / tmdbEpisode,tmdb 来源),季海报取组内首集所在默认季的海报。

切换时单元怎么迁移(service/episode-structure.ts relayoutUnits,同一事务):

  1. 每个未退役的集单元先求「TMDB 原季集」:单元上记的 tmdbSeason / tmdbEpisode 优先;否则按当前结构换算(默认季 = 自身季集;剧集组 = 当前组里这个位置的原季集)。
  2. 按目标结构把原季集换成新季集,原地改单元的季号、集号、所属季单元。单元 id 不变,资产覆盖、资源、下载计划、订阅覆盖、字段覆盖等引用全部跟着走,文件不用重新认。
  3. 编号人工锁定的单元不动;求不出原季集、或目标结构里没有这一集(比如剧集组不含特别篇)的单元保留原号、标临时(来源记为资源),切换结果里逐条列出(含有没有被引用)。同季同集已有对上的单元时,随后的元数据刷新按重复单元合并(DH-1,引用并到对上的那条)。
  4. 季单元:目标结构的每季确保有季单元(同号沿用,临时的转正),总集数直接改成新结构的集数并解除锁定;不在目标结构里的季单元标临时、清总集数(订阅等引用不动)。
  5. 记下选择 → 按作品全量重算已绑定资源(reresolveEntityEpisodes)→ 提交后 full 刷新元数据(季 / 集标题、播出日期、原季集对照按新结构写入;写进 NFO 的字段变了按原规则排「更新」重刮)。

切回默认季同样按这几步,单元回到原号、原季集对照清掉、特别篇季转正。已整理的文件路径不随之改名(S01E14 的文件现在对应 S02E02 单元),要改名用「重新整理」;刮削写的 NFO 季集号、标题、简介随单元走。

集数映射:季表就是组的季集,校验、换算、规则都按组;规则的目标季集写组内的(作品选了剧集组时,EP-12 → 第 2 季 指组的第 2 季)。额外一步换算 按 TMDB 默认季集(方式 tmdb_season):季表带原季集对照(SeasonTable.origins),发布者按 TMDB 默认季编号(S01E14)而原号对不上组的季表时,换到组内季集(S02E02);整段区间要落在同一季且连续。原号对得上时原号优先。集数映射分区的季表摘要会标「按剧集组「X」分季」。

刷新:剧集组作品的元数据刷新拉 /tv/episode_group/{id} 而不是默认季;TMDB 编辑调整过组内顺序时,集单元按记下的原季集挪到新位置(规则同切换)。组取不到(404 / 网络)时本次不动季集结构,只刷作品字段。刮削缺季集元数据时也按组取。

整理时指定:整理弹窗可以只这次按另一套结构整理(不改作品设置),或勾选同时设为作品结构,见 organize.md「整理时指定剧集组」。

没做:识别词 / 单条资源指定剧集组(MP 的 g=)——季集结构是作品级的一个选择,整理时可临时指定。

池的治理

  • 复查有上限:unmatched 每天一次本地召回,最多 3 次;查过 TMDB 的按原退避(无结果 1h 起、查询失败 15min 起,翻倍到 7 天,最多 8 次)。
  • 新证据即重试:title_alias 插入触发器 title_alias_wake_identity_pool,把归一化标题相同或相似度 ≥ 0.45 的 unmatched / no_match 池条目的 next_review_at 设为现在。建档、手动加别名、合并作品都会触发。
  • 过期归档:identity.poolArchiveDays(缺省 30)天没有新尝试的 unmatched 条目改为 archived,在池复查定时任务里做, 每进程每小时最多一次。归档不删资源,仍可按状态筛出、手动指定。
  • 检索:媒体概览 → 待识别,按原始标题 / 解析标题检索,原因筛「未命中订阅」即 unmatched。条目详情里可「重新识别」 (手动触发,会查 TMDB)、手动指定 TMDB ID / 本地作品(复用 /identity-pool/{id}/assign,加人工锁)或忽略。

设置

设置 → 元数据 → 资源识别:

  • identity.eagerTmdbLookup(缺省关):打开即旧行为,每条新资源都查 TMDB。每条约 1–2 次搜索 + 命中时 1 次详情 (识别缓存命中为 0);每小时 100 条的 RSS 约每天 2400–7200 次请求,站点独有的短剧 / 综艺多半查不到。 打开后无结果的资源仍只进池,不开事项。
  • identity.poolArchiveDays(缺省 30)。

存量清理(迁移 0047_identity_pool)

未关闭的 identity_unresolved 事项中,资源未关联作品、池里没有候选、归一化标题也不命中任何本地别名的: 事项改 dismissed,resolution = {"resolution":"moved_to_pool","by":"migration:0047_identity_pool"}; 池条目改 reason = unmatched、清 decision_id、停自动复查。相关的保留原样,下次复查时并入合并事项。

需要重建镜像 / 重启 api + worker 才生效(迁移随启动执行)。

规则更新后的存量重评(ID-2,scripts/identity-reevaluate.ts)

匹配规则更新(新镜像已部署、api + worker 已重启)后,用这个脚本处理存量。缺省只读,加 --apply 才改库;DATABASE_URL 必须显式给出。

# 用户栈的 PG 不对宿主暴露端口:取容器 IP,密码取栈的 .env
PGIP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' umt-postgres-1)
DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/identity-reevaluate.ts --rollback-wrong           # 先看清单
DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/identity-reevaluate.ts --rollback-wrong --apply   # 确认后执行
  • 待识别事项:列出 open 的 identity_ambiguous 成员,按新解析规则给出预估(只用池里存的候选,不查 TMDB);--apply 把这些池条目排入立即复查, 由运行中的服务按新规则重新识别——能自动确认的绑定作品、建映射,并从合并事项摘除(组空即关闭)。
  • --rollback-wrong:找站点来源的分类词 / 通用词别名(例如「电视剧」),对挂着它的作品用新解析 + 作品自身名称(主标题 / 原标题 / 非站点来源别名) 核对已绑定资源;对不上的解除绑定、退回池(reason = no_match,立即复查),删除该别名和由误并入资源登记的站点别名,各记一条 resolution_attempt(outcome = manual, reason = rollback_wrong_alias)。内容单元不删,只报告无资源引用的数量;引用了这些资源的其他表只报告。
  • 2026-09-29 用户库副本演练:《东北有个周东北》29 条绑定里 23 条误并入,删除 7 个站点别名(「电视剧」「动漫」「综艺」与 4 个链式英文名)。

冲突别名与临时内容单元清理(FIX-5,scripts/identity-cleanup.ts)

守卫上线前登记的错误别名、重复内容单元、孤立的临时内容单元用这个脚本清理。缺省只读,--apply 在一个事务里执行并写审计 identity.cleanup;--dry-run 显式只读;--keep-orphans 不删孤立单元;--entity <id> 只处理一部作品; --merge-duplicates 只做重复单元合并(不动别名和孤立单元,DH-1)。连接方式同上。

DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/identity-cleanup.ts            # 看清单
DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/identity-cleanup.ts --apply    # 确认后执行
# 只合并重复单元;只读演练可在连接串加 ?options=-c%20default_transaction_read_only%3Don 强制只读会话
DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/identity-cleanup.ts --merge-duplicates
  • 冲突别名:只动 source = site 的作品级别名,且不是作品自己的名字(主标题 / 原标题 / TMDB 别名)。同一归一化别名还挂在别的作品上 (或是别的作品的标题)→ 删;作品有已接受外部身份、同一种文字下已有确认外部标题而它对不上 → 删(中文标题的拼音写法保留); 本作品下带这个名字的资源都解析出年份且都与作品年份差超过 1 年 → 删。
  • 重复的内容单元(DH-1 起与 refreshMetadata 共用 apps/server/src/modules/sources-identity/service/unit-merge.ts): 同作品、同类型、同季同集号、同内容类型与分段的多条未退役单元(不论临时 / 正式),保留被引用最多的(资源、资产覆盖、下载 / 版本槽、订阅覆盖等,不含单元自身的字段来源;同数时 confirmed 优先、再取最早的),其余单元的全部引用按外键自动发现改指向保留的那条 (撞唯一约束的行——保留的单元上已有同一行——直接删),子单元改挂,然后删除。保留的单元:组内有正式的即转正式, 标题 / 播出日期 / 总集数 / metadata 为空时取被合并单元的值。成因是并发识别同一集时懒建竞态,ensureUnits 已加咨询锁。
  • 2026-09-29 用户库只读 dry-run(--merge-duplicates,未 apply):2 组删除 6 条——《征途》S01E09、S01E10 各 4 条临时单元 (同一时刻建出,都无引用),各保留最早且带 TMDB 字段来源的那条(8d5e09fb…、1132f75b…),其余 3 条合并。之后对该作品跑 元数据刷新也会自动合并。
  • 孤立的临时内容单元(FIX-6 收紧):只删资源解析建出、外部元数据不认识的临时单元。同时满足才删:provisional;没有资源 / 资产 / 订阅 / 下载 / 整理等任何引用;没有任何元数据痕迹(字段来源 / 字段覆盖 / 标题 / 播出日期 / 总集数来源 / metadata 非空); 作品没有已接受的外部身份,或这个季 / 集超出作品的外部季集结构(季号 > TMDB 季数,或集号 > 该季已知总集数; 季数未知时整部作品都不删,特别篇季 0 总在结构内);没有保留的子单元。字段来源 / 覆盖 / 单元别名随之删除。 TMDB / 元数据来源的季集账本单元永不删除,即使没有资源、状态还是 provisional。
  • 清单按类别输出:删除(作品没有外部映射 / 超出外部季集结构)与保留(仍被引用 / TMDB·元数据·人工来源 / 在外部季集结构内 / 有保留的子单元),--dry-run 显式只读(与 --apply 互斥)。先看「删除」两类里有没有正常作品,再 --apply; 审计 identity.cleanup 的 orphanUnits 记每条删除的单元与原因。
  • 事故(FIX-6,2026-09-29):FIX-5 版本只看"无引用 + 没有 TMDB 字段来源",把旧版本建的、还没回写字段来源的 TMDB 季集账本单元 (《人生切割术》S00–S03 与 20 集、《沙丘:预言》S00 与 17 集等)当孤立删除。恢复:对受影响作品跑 full 元数据刷新—— 计划页执行「元数据语言回刷」(POST /schedules/metadata.backfill_languages/run-now,停用状态也可手动执行)或逐个 POST /media-entities/{id}/refresh?full=1,季 / 集单元会按 TMDB 重建为 confirmed。季 0(特别篇)自 DH-1 起也在 full 刷新 范围内(metadata.includeSpecials 缺省开),回刷即可重建。

TMDB 多语言保留规则(2026-10-06)

用户规则:「除了简繁、英文,还有源语言之外,不需要获取其他更多的别名」,TMDB 的多语言数据整体按这个保留。 规则是一个纯函数模块 packages/adapters/src/tmdb/language-policy.ts(单测 language-policy.test.ts)。

  • 保留的语言:简中(zh-CN / zh-SG / zh-Hans,及不带地区的 zh)、繁中(zh-TW / zh-HK / zh-MO / zh-Hant)、英文(en-*)、 作品原语言(TMDB original_language,按主语言比较,如 ja → ja-JP / ja 都算;原语言是中文 / 英文时不额外增加, 原语言是中文时 zh 的其它地区也算原语言)。原名(original_title / original_name)总是保留。
  • alternative_titles(只有地区没有语言):CN / SG → 简中,TW / HK / MO → 繁中,US / GB / AU / CA / NZ / IE → 英文, 作品 origin_country 里的地区 → 原语言;再校验文字:中文要汉字且不含假名 / 谚文,带地区时看简繁特有字(明显是另一种的不要), 日文要假名或汉字,韩文要谚文,泰 / 俄 / 阿 / 希伯来 / 印地等按各自文字,英文和其它拉丁语言要拉丁字母且不含非拉丁文字。 推断不出(没有地区、地区不在上面)或文字都不符的丢弃。例外:原产地区、type 含 roma(TMDB 的 "Romaji")的罗马字别名记 <原语言>-Latn(kind romanized),资源标题常写罗马字。
  • 应用位置:TmdbClient.details()(识别比较用的 aliases / titles,match.ts 别名回退、tmdbToCandidate、 work-match.ts 按作品搜资源都只看到保留语言)、fetchRichDetail(translations / alternativeTitles 只留保留语言, 别名带推断语言)、建档 seedAliases 与刷新 tagTitleAliases 写 title_alias(语言写规范化后的值,不再写空语言的 other; 本地化名文字与语言不符时——TMDB 缺该语言译名时回退成原名——不按该语言登记)。
  • 语言链(设置 metadata.languages):缺省 zh-CN, zh-SG, zh-TW, zh-HK, en-US 全在保留范围。设置仍允许填任意 xx-YY, 但按作品收敛:不在保留范围的语言(对这部作品既不是中英文也不是原语言)从链里去掉,标题 / 简介 / 标语 / 季集名都不取它; 请求语言(链首)被去掉时请求语言下的值不拿来兜底;链全被去掉时用缺省链。季集详情不知道原语言时(链里有中英文以外的语言) 先读一次作品详情(走请求缓存)。
  • 作品与单元的 metadata 只存按语言链取到的单个值(带语言),不存 translations 列表,没有别的多语言数据。

存量清理(scripts/tmdb-alias-language-cleanup.ts)

缺省只读(--dry-run 显式只读,与 --apply 互斥);--apply 在一个事务里删除并写审计 metadata.tmdb_alias_language_cleanup (payload 列出每条删除的作品 / 别名 / 语言 / kind / 原因);--entity <id> 只处理一部作品;--samples N 每类样例条数(缺省 5)。 连接方式同上(用户栈的 PG 取容器 IP):

DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/tmdb-alias-language-cleanup.ts            # 看统计与样例
DATABASE_URL=postgres://umt:<密码>@$PGIP:5432/umt deno run -A scripts/tmdb-alias-language-cleanup.ts --apply    # 逐类目过目后执行
  • 判定口径(title_alias.source 能区分来源):只动 source = tmdb 且未锁定的行;user / site / agent / nfo / import 来源与锁定的行一律不动(清单里只报数量)。tmdb 行中:kind = original 保留;与作品主标题 / 原标题归一化相同的保留; language 非空 → 规范化后在保留范围(原语言取 media_entity.metadata.originalLanguage,没有时取 tmdb 字段来源)保留,否则删 (原因 language);language 为空(旧版建档写的 other 等)→ 汉字(不含假名 / 谚文)、拉丁字母、没有字母的保留,其它文字 只在与原语言相符时保留,否则删(原因 script)。拉丁字母分不出英 / 法 / 西 / 葡,空语言的拉丁别名一律按英文保留—— 要彻底收敛这部分,对作品跑一次元数据刷新不会删它们(刷新只增不删),需要时人工处理。
  • 字段来源:metadata_field(来源 tmdb)里语言不在保留范围的值只报告不删(删了会让最终值暂时没有来源)。对清单里的作品 跑一次元数据刷新即收敛(同字段换语言时删旧行),批量用计划页「元数据语言回刷」(metadata.backfill_languages)。
  • 输出:别名总数 / 删除 / 保留,保留按原因计数;删除按「语言 / kind」分组计数,每组列样例(作品、原语言、别名); 字段来源按「语言 / 作品或单元·字段」计数并列涉及的作品。
  • 对部署库(lessons 第 22 条):先 dry-run,协调者逐类目看清单(重点看删除组里有没有人工补录却被记成 tmdb 来源的、 原语言为空导致误删的日 / 韩 / 泰文别名)再 --apply。原语言为空的作品只保留中英文,先对这些作品刷新元数据补上原语言再清理更稳。

排查

-- 池分布
select reason, status, count(*) from identity_candidate group by 1, 2 order by 3 desc;
-- 某条资源为什么没查 TMDB
select resolution_status, resolution_evidence->>'lookup' as lookup, resolution_evidence->>'detail' as detail
  from source_release where id = '<id>';
-- 某个合并事项的成员
select p.source_release_id, r.title, p.reason from identity_candidate p join source_release r on r.id = p.source_release_id
 where p.decision_id = '<decisionId>' and p.status = 'pending';
-- 站点来源的可疑别名(分类词等)
select e.title, a.alias, a.created_at from title_alias a join media_entity e on e.id = a.media_entity_id
 where a.source = 'site' and a.normalized in ('电视剧', '动漫', '综艺', '电影', '纪录片', '音乐', '短剧');
-- 迁移搬走的事项
select count(*) from decision where resolution->>'resolution' = 'moved_to_pool';

元数据补全(META-1)

TMDB 查不到的作品(短剧、平台独播剧)按标题查网页,出一份带出处的字段建议;确认后才建本地临时作品(REQ R03 / R10、§7.3、§7.5)。 字段来源记 web(值在出处网页原文里)或 agent(Agent 归纳 / 改写,如简介),不记 tmdb。

代码:apps/server/src/modules/sources-identity/service/enrich.ts(建议 / 应用 / 自动触发 / 命中率)、service/enrich-sources.ts(META-2 来源分类、各来源抽取器、置信度合并、robots.txt、登录墙识别)、routes/enrich.ts(HTTP 与海报代理)、 apps/server/src/modules/agent/capabilities-metadata.ts(能力 metadata.enrich、模型调用)、迁移 0049_enrichment。

流程(metadata.enrich,propose,low,replay safe)

输入 {poolEntryId} 或 {title, year?, hints?},可加 fresh: true 跳过缓存。

  1. 缓存:kv_cache region enrich,key = 归一化标题 | 年份 | 归一化 hints,TTL 7 天,指向上次的补全记录。命中直接返回(cached: true),不搜索、不计预算。

  2. 前置条件:网页搜索没开(设置 → Agent → 网页搜索)直接报错,不占预算;Agent 会话里网页搜索没开时不向模型提供这个能力。

  3. 预算:identity.enrichDailyBudget(缺省 20,0 = 停用)按当天新建的 metadata_enrichment 行计(含失败的),用咨询锁防并发超额;用完返回 budget(HTTP 429)。

  4. 搜索:web.search「标题 [年份] [hints] 短剧」;如果白名单里标题完整对得上的网页不足 3 条、且摘要还不够用(集数和年份都要有两个以上网站给出,并有主演或简介),再搜「… 短剧 site:<白名单按 百科 → 豆瓣 → 平台 排序的前 6 个域名,OR 连接>」,最多 2 次。每次搜索用自己的调用方(system:enrich:<记录id>:<n>),web.fetch 白名单与 Agent 会话互不覆盖。条数跟随网页搜索设置的 maxResults(缺省 5;调到 10 摘要证据明显更多,见下方真机记录)。

  5. 来源分类(META-2,每条搜索结果一类,记在 sources[].kind):

    类别 域名 怎么用 基础置信度(网页 / 摘要)
    百科 encyclopedia baike.baidu.com、baike.sogou.com、baike.com(快懂百科)、wikipedia.org、baike.so.com 读网页(robots 允许时)+ 摘要 0.85 / 0.7
    平台 platform hongguoduanju.com(红果)、kuaikaw.cn(河马剧场)、爱奇艺、腾讯视频、优酷、芒果、B 站、抖音、快手、西瓜、MyDramaList 读网页 + 摘要 0.8 / 0.6
    豆瓣 douban douban.com 只用搜索摘要,不读页面(skipped: snippet_only) — / 0.65
    其它 other identity.enrichDomains 里的其它域名 读网页 + 摘要 0.6 / 0.5
    搜索摘要 snippet 白名单以外的网站(多为短剧聚合站) 只用搜索引擎给的标题与摘要,不打开网页;identity.enrichSnippetAllSites 关掉后不用 — / 0.35

    以上域名只有在 identity.enrichDomains 里才按对应类别处理,不在白名单里的一律按「搜索摘要」。已登记站点(site.base_url)的域名既不读也不用摘要。 没有做 Bing:Bing Search API 已停止服务,抓 Bing 结果页属于绕过访问限制,不做;「摘要模式」用 Brave 的结果摘要(描述 + 第一条 extra snippet)。

  6. 读取:只读白名单里(豆瓣除外)结果标题 / 摘要包含完整标题的网页(只有七成字对得上的多是首页、榜单页,记 irrelevant),按相关度、白名单顺序最多 3 条。读之前查 robots.txt(User-agent: * 或指名 umt-websearch 的组,最长匹配、同长 Allow 优先;按「协议 + 主机」缓存 1 天,kv_cache region enrich.robots;404 视为没有限制,5xx / 超时视为不允许),不允许的记 robots 跳过;读到登录墙 / 验证码页(正文很短且含「验证码」「请先登录」等)记 blocked 放弃,不重试、不绕过。重定向出白名单的页面丢弃。robots.txt 的读取不计入 fetchCalls。

    • 2026-09-30 录制:红果 robots.txt 对所有爬虫禁止 /series/(作品页的常见地址),/detail?series_id= 允许;百度百科对 * 全站禁止(百科只能用摘要);河马剧场禁止带查询串的地址。
  7. 抽取(每类来源一个小抽取器,enrich-sources.ts):

    • 摘要:结果标题必须包含查询标题(演员页、榜单页顺带提到的不用)。通用句式:书名号标题(用网页上的写法,如「重生60,我有情报系统」)、「共 / 全 N 集」「(N集)」、「年份:」「(2026)」「2026年热播」「上映于」、「主演:A&B」、「简介 / 剧情简介 / 影视简介:」、「类型:」、「又名 / 别名:」(拼音别名丢弃);「更新到第 30 集」不当集数,网盘 / 下载字样不进主演与简介;摘要只有一段剧情(没有标签)时按 0.7 权重当简介。豆瓣摘要按「导演: 主演: 类型: 年份: 集数:」取;百科摘要按「由 X 执导,A、B 主演的……全剧共 N 集,于 2025 年 8 月 15 日在爱奇艺首播,又名“…”」取。
    • 网页:网页标题(去掉「_高清完整版…」「_百度百科」等后缀)必须对得上;只看本作品主体(「相关短剧」「热门短剧」「猜你喜欢」之后是别的作品);集数取正文句式,没有时取选集列表「1 2 … N」;平台页的平台名按域名;海报只取 og:image。
    • 模型(可选):默认 Agent 上游做一次补全,输入是读过的网页和用上的摘要;模型给的值必须以本次的网页或摘要为出处,原文里有的按该来源置信度减 0.05,改写 / 归纳的按 0.3;海报不是网页 og:image 的丢弃。fake 上游、没有上游、失败或输出不是 JSON 时只用规则抽取(extractor: rule)。
  8. 合并:每个字段把候选值按归一化值分组,同组不同网站的置信度按 noisy-OR 叠加(同一网站只算一次),取最高的一组,低于 0.3 的丢弃;主演 / 类型 / 别名保留过线的前 10 / 5 / 10 个。每个值带 sourceKind、confidence、hosts(给出同一值的网站数)。证据等级:原文里有、由规则抽出且出处不是「搜索摘要」类 → rule_confirmed;两个以上不同网站给出 → multi_evidence;其余 needs_review。

  9. 命中率:usage.attempts 记每类来源的 results(结果标题对得上的条数)、fetched、hit(建议里至少一个字段有它支持)、fields。GET /identity/enrich-stats?days=30(read)返回最近 N 天发起过搜索的补全次数、出建议次数、各类来源的「有结果 / 命中 / 命中率」;设置 → 元数据 → 资源识别 下方有这张表。命中 7 天缓存的不新建记录,不计入。

  10. 存 metadata_enrichment(status suggested / failed、suggestion、usage:搜索 / 读取次数与 token),不写作品表。

web.fetch 输出新增 image(页面 og:image / twitter:image,绝对地址,已脱敏)。

触发

  • 待识别池条目详情 →「元数据补全」→「补全」看建议 →「应用」(可选「应用的字段同时锁定」、并入某个同名作品)。POST /identity-pool/{id}/enrich(organize.execute,经能力注册中心,授权 / 审计同 Agent)。
  • 自动:identity.autoEnrich(缺省关)打开后,识别结果是 no_match 且查 TMDB 的理由是 tracked_alias / new_title_rule 时入队引擎任务 enrichPoolEntry(幂等键 enrich:auto:<池条目>,每条一次,不重试),只出建议不建作品;预算用完 / 网页搜索没开时任务正常结束(skipped)。
  • Agent:metadata.enrich 在授权里直接放行(只存建议),受每日预算约束。

应用(POST /identity-pool/{id}/enrich/apply,只接受控制台管理员会话)

  • 作品:指定 mediaEntityId 则并入;否则按标题 / 查询标题的归一化别名找已有作品(active / provisional,年份不冲突)去重并入;都没有就建 status = provisional 的作品(缺省剧集,kind 可改)。
  • 字段:metadata_field 写 source = web | agent、source_revision = enrich:<记录id>、evidence = {sourceUrl, evidenceLevel, enrichmentId, extractor};映射 title→title、year→year、synopsis→overview、platform→platform、episodeCount→episodeCount、genre→genres、cast→cast(按 MediaCredit 写,没有 TMDB 人物 ID,id 用 -1、-2… 占位,作品详情里不链接 TMDB)、posterUrl→posterUrl,然后按原规则投影(TMDB 值存在时仍优先 TMDB,锁定的字段不变)。lock: true 把应用的字段加进 field_locks。
  • 别名:标题、查询标题、建议里的别名写 title_alias(source agent,kind other),会唤醒相近的池条目复查。
  • 单元:剧集建第 1 季 + 1..集数(最多 200)临时单元。
  • 待识别资源经 assignReleaseIdentity 关联到该作品(人工锁);记录改 applied,审计 metadata.enrichment_applied。不写外部身份。
  • 后续 TMDB 收录:provisionalEntitiesForTmdbReview() 列出补全建的、还没有 TMDB 身份的临时作品,留给身份巡检按标题 / 年份查 TMDB 后用 setEntityIdentity 补映射。

作品详情「元数据」里来源显示「网页」/「Agent」,下面一行是证据等级和出处链接;网页海报经 GET /api/v1/img/enrich/{记录id} 代理(只取该记录里白名单网页给出的图片,出处仍须在白名单;不接受任意 URL;地址守卫同 web.fetch,最多 3 跳、5 MB、只收图片)。

设置(设置 → 元数据 → 资源识别)

字段 缺省 说明
identity.autoEnrich 关 订阅 / 规则命中而 TMDB 无结果时自动出建议
identity.enrichDailyBudget 20 每日次数(手动 + 自动 + Agent);0 = 停用
identity.enrichDomains 见上 可读取的域名,不放 PT 站。META-2 起缺省加了 baike.com、kuaikaw.cn;已保存过这项设置的库不会自动加,需要时手动补
identity.enrichSnippetAllSites 开 白名单以外网站的搜索摘要也用来抽字段(只看摘要,不打开网页);旧设置没有这个键,按开

单次成本:1–2 次 Brave 搜索(搜索缓存命中不计)、最多 3 次网页读取(另有 robots.txt,每个网站每天最多 1 次)、1 次模型调用(约 3–6 千输入 token)。短剧多数只有聚合站收录,通常 0 次网页读取。

迁移 0049_enrichment:metadata_field / field_override 来源加 web、metadata_field.evidence、metadata_enrichment 表。需要重建镜像 / 重启 api + worker 才生效。

-- 今天的补全
select status, trigger, count(*), sum((usage->>'searchCalls')::int) searches from metadata_enrichment
 where created_at >= date_trunc('day', now()) group by 1, 2;
-- 某作品的补全字段与出处
select field, source, value, evidence->>'evidenceLevel' lvl, evidence->>'sourceUrl' url from metadata_field
 where media_entity_id = '<id>' and source in ('web', 'agent');

真机记录(2026-09-29,开发库写记录,用户栈只读设置 / 凭证,未应用)

默认上游 OpenCode Go / deepseek-v4.1-flash + Brave,对池里 4 条 HHClub 短剧标题各跑一次(共 8 次 Brave 调用):

标题 结果 耗时 读取 / token
我被亲戚误会破产了 failed:搜索结果全是聚合站,不在白名单(当时还没有 site: 限定) 1.8 s 0
重生60 我有情报系统 failed:加 site: 后有白名单结果,读了 3 个爱奇艺页(当时未做相关度过滤),模型判定没有这部作品的信息 6.2 s 3 次 / 1601 入 533 出
京夜喜你 failed:白名单结果标题都对不上,不读网页、不调模型 1.9 s 0
春城恋曲 同上 1.8 s 0

结论:平台自制短剧在百科 / 豆瓣 / 平台站基本没有可被搜索到的条目,补全多数会以"白名单网站里没有标题对得上的结果"结束,不消耗读取和模型; 要提高命中率可以在 identity.enrichDomains 里加入短剧平台的公开页面域名。聚合站、盗播站不要加(META-2 改为只用它们的搜索摘要,见下)。

真机记录(META-2,2026-09-30,不写库)

用户库只读(连接串加 default_transaction_read_only=on)取出待识别池里 4 条 HHClub 短剧,跑 runEnrichPipeline(规则抽取,不调模型;网页搜索设置 maxResults = 5、白名单用缺省)。 请求:Brave 12 次(探索 8 次 + 第二次 site: 搜索 4 次;第一次搜索复用探索录制的响应);hongguoduanju.com 4 次(robots.txt、1 个作品页、流水线读了首页和分类页各 1 次——之后改为只读含完整标题的结果)、baike.baidu.com 1 次(robots.txt)、kuaikaw.cn 1 次(robots.txt),同一网站间隔 ≥ 6 s。 响应脱敏(删网盘链接,只留标题 / 地址 / 摘要)后存为 apps/server/test/fixtures/enrich/,enrich-sources.test.ts 回放锁住下面的结果。

标题 命中来源 结果(置信度 / 网站数)
我被亲戚误会破产了 搜索摘要 标题(0.82 / 4)、2026(0.35 / 1)、75 集(0.35 / 1)、主演刘俊达 / 刘诣文(0.35 / 1)、简介「周远公司上市后回乡给爷爷祝寿…」(0.35 / 1)
重生60 我有情报系统 搜索摘要 标题「重生60,我有情报系统」(0.88 / 5)、2026(0.82 / 4)、80 集(0.35 / 1)、主演龙泽 / 冯韵霏(0.73 / 3)、类型年代穿越
京夜喜你 搜索摘要 标题(0.88 / 5)、2026(0.58 / 2)、41 集(0.35 / 1)、主演祁乐 / 吴思君(0.35 / 1)
春城恋曲 搜索摘要 标题(0.73 / 3)、2026(0.73 / 3)、主演宋喃 / 郑天龙(0.58 / 2);没有集数

命中率:搜索摘要 4/4,百科 / 平台 / 豆瓣 / 其它 0/4(第二次 site: 搜索只搜到别的短剧、演员页和榜单页)。每次 10 条结果时证据更多:《京夜喜你》41 集升为多证据、有简介、只需 1 次搜索;《春城恋曲》主演 4 个网站;关掉 enrichSnippetAllSites 则 4 条都抽不到字段。 集数、主演多数只有 1 个聚合站给出,标「待复核」,应用前要人工核对。