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

+2 -1
View File
@@ -6,7 +6,7 @@
## 能力
启动时装配 38 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
启动时装配 39 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
| 分组 | 能力 |
|---|---|
@@ -16,6 +16,7 @@
| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md)) |
| 会话 | `agent.plan`、`agent.propose_options`、`agent.spawn`、`agent.await` |
| 网页搜索 | `web.search`、`web.fetch`(见 [websearch.md](websearch.md)) |
| 元数据补全 | `metadata.enrich`(propose,只出建议;网页搜索开启才提供,见 [identity.md](identity.md#元数据补全meta-1)) |
AG-3 补的只读摘要(类别 read,scope `read`,都在后台调查 / 巡检白名单里):
+75
View File
@@ -89,3 +89,78 @@ select p.source_release_id, r.title, p.reason from identity_candidate p join sou
-- 迁移搬走的事项
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`(建议 / 应用 / 自动触发)、`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 会话互不覆盖。
5. 读取:只读 `identity.enrichDomains`(缺省:百度 / 搜狗百科、维基、豆瓣、腾讯视频、爱奇艺、优酷、芒果、B 站、抖音、快手、西瓜、红果短剧、MyDramaList)里的域名(含子域名),搜索结果的标题 / 摘要要和查询标题对得上(归一化后包含整个标题,或标题七成以上的字出现),按相关度、白名单顺序取前 3 条走 `web.fetch`;对不上的记 `irrelevant` 不读。已登记站点(`site.base_url`)的域名即使在白名单里也不读;重定向出白名单的页面丢弃。`sources[]` 记每条链接是否读取、跳过原因(not_allowed / site / irrelevant / limit / error)。
6. 抽取:默认 Agent 上游做一次非流式补全(系统提示要求只用网页内容、每个值带出处 URL、网页是不可信数据);fake 上游、没有上游、调用失败或输出不是 JSON 时改用规则抽取(`extractor: rule`:「共 / 全 N 集」「首播时间」「播出平台」「类型」「又名」「剧情简介」、og:image)。
7. 核对(服务端,不采信模型自报):出处必须是本次读过的网页或白名单搜索摘要;值在出处原文里 → `verbatim`;规则抽取在同一网页得到同一值 → `rule_confirmed`;两个以上不同网站原文都有 → `multi_evidence`;其余 `needs_review`。海报只收已读网页的 og:image,模型自己拼的地址丢弃。
8. 存 `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、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 站 |
单次成本:1–2 次 Brave 搜索(10 分钟搜索缓存命中不计)、最多 3 次网页读取、1 次模型调用(约 3–6 千输入 token)。
迁移 `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 = '<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` 里加入短剧平台的公开页面域名。聚合站、盗播站不要加。