- POST /files/scrape 改为对目录 / 视频文件刮削:已登记用作品,否则按路径识别(本地 + TMDB), 认不准返回候选,选定后再提交;未登记直接用 TMDB 字段,addToLibrary 顺手建作品 - 作品详情重新刮削、入库后自动刮削改为对作品目录刮削,同一实现 - 迁移 0058_scrape_record 替代 media_scrape;GET /files/scrape-records - 与 MP ScrapingChain 的文件布局差分(7 个场景,3 处登记偏离) Co-Authored-By: Claude Opus 5.5 <[email protected]>
@umt/db
PostgreSQL 访问层:Kysely 客户端与连接池、表类型(src/types.ts)、迁移文件(migrations/)与迁移器。表名 snake_case,与 REQ §12 实体名一致。
规则
- 迁移是唯一事实来源。所有 schema 变更只能新增迁移,已发布迁移不得修改。迁移用原生 DDL 书写(
src/migration-helpers.ts:entityTable()统一加 uuid 主键gen_random_uuid()、created_at/updated_at与 updated_at 触发器;oneOf()生成状态 CHECK)。migrations/内相对导入必须带.ts扩展名(Kysely 用原生import()加载)。 src/types.ts是生成物:deno task db:migrate && deno task gen:types。CHECK 枚举自动变成字面量联合(如AcquisitionSlotStatus)。test/schema.test.ts会在隔离 schema 里重新反射并比对,漂移即失败;packages/contracts/test/db-sync.test.ts再校验 contracts 的 Row schema 与枚举和这里一致。- 状态列一律
text + CHECK,不用 PG enum(便于迁移加值)。JSONB 只放证据、规则参数、原文快照与演进字段;稳定查询字段用关系列。 - 凭证只存
secret_ref(→secret.id),明文不入库、不进日志。 createDb()把int8解析为number(超出 2^53 抛错)、date保持YYYY-MM-DD字符串。写 jsonb 数组时须先JSON.stringify(pg 会把 JS 数组当 PG 数组)。- 测试对真实库(默认
postgres://umt:umt@localhost:55433/umt)在临时 schema 中跑迁移往返;无库环境设UMT_SKIP_DB_TESTS=1。在非 public schema 迁移时要传createMigrator(db, { migrationTableSchema })。
迁移清单
| 文件 | 内容 |
|---|---|
0001_init |
settings、api_key、audit_event、outbox_event |
0002_foundation |
pgcrypto / pg_trgm、umt_touch_updated_at()、secret;outbox 投递字段(ADR-002);api_key.updated_at |
0003_instances |
library_root、downloader_instance、media_server_instance、path_mapping |
0004_content |
media_entity、content_unit、title_alias |
0005_identity |
external_identity、identity_revision |
0006_sources |
site、site_account、feed_source、source_release、observation |
0007_assets |
media_asset、asset_unit_coverage、asset_location、path_binding |
0008_policy |
policy_revision、subscription、acquisition_slot、preference_tag、preference_suppression |
0009_execution |
task、acquisition_plan、download_task、asset_operation;回填 media_asset.download_task_id、acquisition_slot.reserved_plan_id |
0010_agent |
agent_run、artifact、decision、approval、automation_grant |
0011_ops |
reconcile_run、server_mapping |
0012_organization |
organization_record(change_seq 序列 + 触发器)、organization_file_result;回填 asset_operation.organization_record_id |
0013_tasks_tables |
并入 @umt/tasks 自有表:tasks_run_meta、tasks_fact、tasks_op_ledger(if not exists,可接管 W1 旧库) |
0014_agent_run_event |
agent_run_event:Agent 运行事件,主键 (run_id, seq),删 run 级联 |
0018_sources_snapshot |
site.adapter 增加 api;source_release.resolution_locked(人工锁);site_snapshot(快照/补漏/实时补查的覆盖范围) |
0019_identity_pool |
identity_candidate(待识别池)、resolution_attempt(识别尝试)、identity_redirect(合并跳转) |
0020_metadata_fields |
metadata_field(来源候选字段)、field_override(用户覆盖与锁定) |
表清单(45 张)
底座 / 运维
| 表 | 用途与关键不变量 |
|---|---|
settings |
全局设置 key→jsonb;key 为 GlobalSettings 的分组名(automation/cache/organize/retention/pi/budget) |
api_key |
API Key(只存 hash);scope ⊂ read/organize.read/organize.execute/admin,可撤销 |
audit_event |
审计日志,只追加 |
outbox_event |
业务事务内写入的事件;分发器扫 status='pending' and next_attempt_at<=now(),idempotency_key 唯一,作为引擎入队幂等键 |
secret |
加密凭证(ciphertext/nonce/auth_tag/kdf/key_version);其他表只存 secret_ref |
reconcile_run |
五类校验器的运行记录,resume_cursor 支持中断续扫;mode=read_only 用于恢复后对账 |
server_mapping |
Emby 兼容库存条目 ↔ 本地对象;(media_server_instance_id, server_item_id) 唯一;未知 ≠ 缺失,不构成纳管授权 |
实例
| 表 | 用途与关键不变量 |
|---|---|
library_root |
媒体库根(执行进程可访问路径);management_mode register_only/managed,只登记范围不被全局规则隐式升级为自动下载 |
downloader_instance |
qB 实例;managed_tag 管理标签;cache_volume_key 聚合共用缓存卷的水位 |
media_server_instance |
Emby/Jellyfin 实例 |
path_mapping |
下载器/媒体服务器路径前缀 → 本地路径;同实例同 remote_prefix 唯一;view_kind 与实例列一致(CHECK) |
内容 / 身份
| 表 | 用途与关键不变量 |
|---|---|
media_entity |
本地作品,UUID 为主身份;合并时 status=merged 且 merged_into_id 指向目标(旧 ID 跳转);field_locks 人工锁字段刷新不得覆盖 |
content_unit |
季/集/特别篇/分段;编号(season/episode/source_issue/air_date)可修订,不是唯一身份;numbering_locked 人工锁 |
title_alias |
原名/译名/简称/站点原标题;normalized 上建 gin trgm 索引做模糊召回 |
external_identity |
本地对象 ↔ 外部 ID;(provider, entity_type, external_id) 对 accepted 部分唯一,同一本地对象每个 provider+type 只有一个 accepted;候选(proposed/conflicted)可并存;404/超时只记 verify_status |
identity_revision |
身份每次变更的修订历史,(external_identity_id, revision) 唯一,只追加 |
identity_candidate |
待识别池:每条 source_release 至多一条;候选只存 JSON(不建正式作品),保存原因/证据版本/复查时间(有界退避) |
resolution_attempt |
每次识别尝试的结果与证据版本,只追加 |
identity_redirect |
作品合并后的旧 ID 跳转,from_entity_id 唯一 |
metadata_field |
各来源的候选字段值(来源/语言/时间/来源修订);刷新只写这里 |
field_override |
用户字段覆盖与锁定,优先于自动来源;目标 + 字段唯一 |
来源
| 表 | 用途与关键不变量 |
|---|---|
site |
站点(adapter/限流/熔断/快照游标);关闭站点即停止网络访问与新候选 |
site_account |
站点账号,凭证走 secret_ref |
feed_source |
RSS/列表/查询入口;真实 URL(含 passkey)只在 url_secret_ref,对外只给 url_display;按 next_fetch_at 调度 |
source_release |
站点资源;(site_id, site_release_id) 唯一,多 Feed 观察汇到同一行;source_published_at/first_seen_at 分开 |
observation |
一次原始观察;(feed_id, guid) 唯一,content_digest 变化即条目更新 |
site_snapshot |
一次列表快照 / 补漏 / 实时补查:页码、条目数、状态与错误(新鲜度来源) |
文件
| 表 | 用途与关键不变量 |
|---|---|
media_asset |
文件资产(与位置分离);附属文件 parent_asset_id 指向主视频;内容版本 content_edition 与交付特征 delivery 分开 |
asset_unit_coverage |
资产覆盖哪些内容单元(合集/分段),(media_asset_id, content_unit_id) 唯一 |
asset_location |
资产的一个位置;同一库根 + 相对路径在非 removed 状态下唯一;状态 available/unavailable/suspected_missing/confirmed_missing/removed |
path_binding |
作品/季目录绑定;一旦建立不因元数据刷新改变,只有显式整理可替换(旧行标 superseded);同作品同季只有一个 active,同一路径只绑定一个 active;历史双目录用 conflict |
规则
| 表 | 用途与关键不变量 |
|---|---|
policy_revision |
策略版本(草案/发布/回滚);(policy_key, revision) 唯一,每个 policy_key 只有一个 published |
subscription |
追踪目标(单次/持续,仅本季/后续季,优先级);作品未建档时用 title_query |
acquisition_slot |
独立收录目标;状态 empty/reserved/downloading/verifying/satisfied/upgrade_pending/attention;(media_entity_id, content_unit_id, name) 唯一(NULLS NOT DISTINCT) |
preference_tag |
前端可编辑偏好标签;fingerprint 在未删除标签中唯一 |
preference_suppression |
取消标签的抑制记录;active 时 Agent 不得据同一 fingerprint 自动加回 |
执行
| 表 | 用途与关键不变量 |
|---|---|
task |
业务工作单/时间线投影(引擎只管推进);(kind, idempotency_key) 唯一;按 (status, next_run_at) 调度 |
acquisition_plan |
下载计划 + Slot/预算预留;每个 Slot 同时只能有一个 reserved/submitted 计划(部分唯一);idempotency_key 唯一 |
download_task |
下载器任务映射;(instance_id, torrent_hash) 唯一;pause_reason 区分用户/阈值暂停;next_check_at 对账调度 |
asset_operation |
副作用账本;operation_id 唯一;attempt_token 做 fencing |
人工 / AI
| 表 | 用途与关键不变量 |
|---|---|
agent_run |
Agent 运行、预算/用量、会话状态、事件序号 |
artifact |
证据/计划/提案产物;提案不直接晋升为事实 |
decision |
待处理中心条目;同一 dedupe_key 同时只有一个未关闭条目 |
approval |
单次审批,绑定 plan + payload_hash + expected_revision |
automation_grant |
持续授权(动作 + 范围 + 预算 + 策略版本),可撤销 |
agent_run_event |
Agent 运行事件(9 种,见 contracts AgentRunEventType);(run_id, seq) 主键,先落库再推 SSE;payload = 事件主体去掉 type |
任务引擎(@umt/tasks 内部使用,不经 API 暴露)
| 表 | 用途与关键不变量 |
|---|---|
tasks_run_meta |
引擎 run 的业务侧元数据(traceId/优先级/取消标志) |
tasks_fact |
signal 前先落库的事实,waitForFact 以它为准 |
tasks_op_ledger |
操作账本 fencing:attempt_token,旧 attempt 提交被拒 |
整理
| 表 | 用途与关键不变量 |
|---|---|
organization_record |
整理批次(公开整理记录 API 的来源);change_seq 在任何 update(含文件明细变化)时取新序列值并唯一,change_xid 记录变更事务号 |
organization_file_result |
逐文件前后路径与结果;增删改会刷新父记录 change_seq |
游标读取:where change_seq > $after and change_xid < pg_snapshot_xmin(pg_current_snapshot()) order by change_seq limit $n。第二个条件排除仍在进行中的事务,避免"序号小但提交晚"被跳过;返回的最大 change_seq 即 nextCursor。