# 资源识别与待识别池运行手册(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/` 的搜索标题与摘要(`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 `:<小时>`,原子自增,2 小时过期)。达到上限记 `deferred:cap_reached`、 15 分钟内复查。这些详情请求同样计入站点请求统计(`site_request_count`)。 ### 待处理 - 升级链推迟中(`pending`:每站每小时限额已满 / 入库时不抓):不开任何身份事项,之前开的从合并事项摘除。抓了但失败的不算推迟(不阻塞,照常进池 / 开事项)。 - 资源上已有 IMDb / TMDB / 豆瓣 ID 却仍无结果:不开「没识别出作品」(`identity_unresolved`),只留在池里,可手动指定。 - 有候选的歧义(`identity_ambiguous`)照旧开;资源被识别后(升级链 / 复查 / 手动)自动从合并事项摘除,组空即关闭。 ### 排查 ```sql -- 最近为识别抓详情的资源 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::<归一化标题>`,同来源同标题的资源并成一条。成员 = `identity_candidate.decision_id` 指向该事项且仍 `pending` 的条目。标题 1 条时写资源原标题,多条时写「来源「X」的 N 条资源待识别 / 待确认身份:<标题>」, `evidence.releases` 列前 50 条,`evidence.count` 为总数。 - 处理(选候选 / 手动 TMDB ID / 选本地作品):先处理代表资源(subject),再把确定的作品套到组内其它资源(都加人工锁)。 - 忽略:组内每条池条目都 `dismissed`。 - 组内某条被单独识别 / 指定 / 忽略:从组里摘除,代表资源和候选选项换成剩下的最近一条;组空了才关事项。 - 旧式单条事项(`identity:`)在该资源下次复查时并入合并事项并关闭。 收件箱:顶栏角标和待处理页缺省只算 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`);刷新会按作品重算待确认 / 滞后放行的资源,换算不了的这时才开事项。 集数待确认不再是给用户逐条处理的条目:原因在订阅详情「集数映射」与作品页「集数映射」分区按组显示,建议规则 / 智能体建议在作品页一键确认。 **规则**是作品识别词(2026-10-08 起,语法与在哪生效见 [识别词](../design/identify-words.md)):一部作品一块文本,在规则页「识别词」和作品页「集数映射」里改的是同一份(`GET/PUT /media-entities/{id}/identify-words`,试算 `POST …/identify-words/dry-run`)。 - 一行一条:`S02 >> S01 EP+24`(季集换算)、`S01E13 >> S00E01`(单集对照)、`S01 @发布组 >> 忽略`(特别篇 / 花絮 / 忽略);`@发布组` 可写多个,`*` = 任意季;越具体越优先,同一范围只能有一行。 - 规则照写的用:算出来对不上季表的资源保持集数待确认,预览里标「对不上」,这一行不会被自动停用;要停用就把这一行注释掉(可视化里关掉开关)。 - 保存带读到的版本,别处先改过返回 409;写错的行拒绝保存;保存后在同一事务里按作品重算已绑定资源(人工锁定的不动),返回重算条数、变化条数、待确认转已解决条数,资源解决后对应的事项自动关闭。改动写审计 `identify_words.updated`(增删的行)。 - 0094 迁移把旧的结构化规则(episode_mapping_rule)逐作品转成文本行后删表:同季同换算的多个发布组合成一行,单集对照一行一条,停用的规则变成注释行,智能体写的规则上方加一行注释。 **模拟预览**(`POST /media-entities/{id}/episode-mapping/preview`,纯读):取作品下最近 100 条已绑定资源(按发布时间倒序,可按季 / 发布组过滤;季按来源季或规范季任一匹配),按原标题(先过全局识别词)重解析后逐条给出原始季集 → 库里的当前结果 → 套用草稿后的结果、命中的识别词行、校验状态、是否有变化;另给季表摘要、按发布组的方式统计与待确认数、各组待确认样本推出的建议规则。`words` 传整块作品识别词草稿原文。编辑器改动后自动试算,点「保存」才写入。 **作品页「换算不了的资源」**(原待处理「集数待确认」卡片,2026-10-06 起显示在作品页集数映射分区):每组列出来源季集、原因、建议规则(同组待确认资源按发布时间对播出日期推出,全部样本成立且唯一才给)与它套用到同组资源的模拟结果。 - 采用:选项 `confirm_suggestion`,建议写成一行(如 `S01 @Lilith-Raws >> S02 EP-30`,选项的 `input.line`)追加到作品识别词末尾,上方一行注释写来历 → 事项按用户选择关闭 → 按作品重算(同一事务;仍对不上的资源另开新事项)。 - 修改:在作品页里把这一行放进编辑器草稿;从别处跳来时地址带 `?mapLine=<那一行>#episode-mapping`。 - 忽略:只关闭提醒,资源仍待确认;勾选「同时写一行忽略」时走选项 `ignore_rule`,追加 `S01 @发布组 >> 忽略`,这一季这个发布组的资源以后不参与集数换算。 处理走 `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` 服务端强制再模拟:样本有一条没解决、或会让当前已对上 / 已换算的资源变成待确认,直接拒绝不写库。通过才把这一行追加到作品识别词末尾,上方一行注释 `# <日期> 智能体(会话 <前 8 位>):<理由>`,审计记操作者为智能体会话;返回追加的原文与行号;随后按作品重算,资源都解决时事项自动关闭,系统据此核实恢复。 - 需要什么授权:后台自动调查的模式要是「授权内恢复」(`agent.autoInvestigateMode = recover`),且有一条覆盖 `episode_mapping.apply_rule` 的有效持续授权(action 写 `episode_mapping.apply_rule` 或 `*`;可用 `scope.mediaEntityIds / mediaKinds` 限定作品,按事项的作品核对;不随身份识别策略放开)。对话里没有授权时同样被拒,智能体只会说「需要你批准」。 - 没有授权(或模式是只读调查)时降级只读:智能体在会话里用 `simulate`(带事项 ID 和理由)试出的通过的那一行,会作为「智能体建议」写进事项证据(`evidence.agentSuggestion.line`,事项刷新时保留),作品页集数映射里显示这一行、理由、样本套用后的季集与模拟结果,点「采用智能体建议」(选项 `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 必须显式给出。 ```sh # 用户栈的 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 ` 只处理一部作品; `--merge-duplicates` 只做重复单元合并(不动别名和孤立单元,DH-1)。连接方式同上。 ```sh 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 ` 只处理一部作品;`--samples N` 每类样例条数(缺省 5)。 连接方式同上(用户栈的 PG 取容器 IP): ```sh 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`。原语言为空的作品只保留中英文,先对这些作品刷新元数据补上原语言再清理更稳。 ## 排查 ```sql -- 池分布 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 = ''; -- 某个合并事项的成员 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 = '' 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>:`),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 才生效。 ```sql -- 今天的补全 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 = '' 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 个聚合站给出,标「待复核」,应用前要人工核对。