Files
UHD_Media_Tool/docs/runbook/dev.md
T

234 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发环境运行手册
## 前置
- Bun 1.4.3(锁定)、Docker;Linux 解析沙箱构建需要 C 编译器和 Node-API 头文件(Debian: `build-essential libnode-dev`)。不需要单独安装 Node.js / pnpm。
- 端口:web 5180、API 4390、PG 55433。4380 留给本机部署版(Caddy),本机 3000/5175/5176/55432 已被其他项目占用,不要改用。
## 首次启动
```sh
cp .env.example .env # 按需修改;server 与 db 脚本都读仓库根 .env
bun install --frozen-lockfile # 按 bun.lock 装 workspace 依赖
bun run dev:up # 起开发库 postgres:18.6(docker-compose.dev.yml)并迁移到最新;拉代码后也跑一次
bun run dev # 同时起 server(4390)与 web(5180)
```
单独起:`bun run dev:server`(`bun --watch`,默认 `--role all`,API + worker 同进程)、`bun run dev:web`。
## 验证
```sh
curl localhost:4390/api/v1/health
# 开发模式可用 .env 的 DEV_ADMIN_KEY 作 admin
curl -N -H "Authorization: Bearer umt-dev-admin-key" "localhost:4390/api/v1/events?after=0" # SSE 流(控制台用)
curl -H "Authorization: Bearer umt-dev-admin-key" "localhost:4390/api/v1/events?after=0&limit=50" # JSON 补读页(API Key 调用者用)
```
- OpenAPI:http://localhost:4390/api/openapi.json,文档页 http://localhost:4390/api/docs
- 控制台:http://localhost:5180,右上角钥匙按钮设置 API Key(存 localStorage `umt-api-key`)
## 常用命令
| 命令 | 作用 |
|---|---|
| `bun run ci` | typecheck(tsc 7.0.2)→ lint(biome)→ test(vitest)→ check:tokens / check:copy → build → check:binary,提交前必须全绿 |
| `bun run typecheck` | 逐个成员跑 tsc 7.0.2 |
| `bun run lint` / `bun run fmt` | biome 检查 / 自动修复(含 import 排序;相对 import 必须带扩展名) |
| `bun run test` | 逐个成员跑 vitest(`bun x --bun vitest run`);单包:`cd packages/<x> && bun run test` |
| `bun run db:reset` | 清空 public schema 并重新迁移(生产环境拒绝执行) |
| `bun run gen:types` | 从已迁移的库反射生成 `packages/db/src/types.ts` |
| `bun run gen:tokens` | 改了 app.css 后重新生成 token 登记表 |
| `bun run build` | web 构建到 `apps/web/build/client` + server 编译为单文件 `dist/umt-server` |
| `cd apps/server && bun run start` | 直接跑源码;外部字节解析另走 Linux seccomp 子进程 |
依赖:各成员包在 `package.json` 登记依赖,根 `package.json` 管理 workspace 与共用工具。修改依赖后运行 `bun install` 更新 `bun.lock`;正常拉取使用 `bun install --frozen-lockfile`。解析沙箱测试前运行 `bun run sandbox:build`(`test:all` 会自动构建)。
## 工程设计与适度抽象
适度抽象用于减少重复修改、统一可靠行为,方便后续维护与开发。判断依据是已有实现的共同语义,不只看代码外形相似。
- 有稳定的重复机制时及时提取,例如协议分帧、消息上限、进程退出与回收;新增业务任务应能复用这些行为,不必重复写管道循环。
- 调用方各自决定的策略保持显式。文件操作按进度停滞判断超时,解析按总时长和 RSS 判断;权限、业务重试、取消后的文件清理不放进通信层。
- 共用机制放在双方可依赖的最小内部模块,按用途导出。不要为了复用创建完整框架,也不要用大量模式开关把不同业务揉成一个类。
- 保留清晰的类型边界。协议载荷在共用层是 unknown,具体输入、进度和结果由业务模块定义与校验;业务代码不直接依赖传输细节。
- 只有第二个实际使用方出现相同的管理逻辑时,再提取排队、常驻 worker 管理或进程池。没有性能证据时,不因为别的项目复用了进程就提前改运行方式。
- 持久业务任务与本地执行任务分别建模。下载、整理等流程的恢复与去重归任务引擎和业务记录;文件/解析 worker 只完成一次执行。通信失败不能自动重做有副作用的操作。
- 定时任务的触发与执行分开考虑:短巡检可直接运行;长耗时、需要恢复或产生重要副作用的任务应入持久队列。新任务不要在调度器里另建一套重试与恢复逻辑。
- 抽象后的测试覆盖真实边界:分块传输、异常退出、取消、大小超限、故障回收;业务模块继续验证各自的超时与副作用。不要只测试一层转发调用。
当前共用入口为 `@umt/runtime/worker`(`packages/runtime/src/worker.ts`):一次性连接、逐行 JSON 协议及有界回收。文件代理和解析代理分别选择启动环境、消息上限和终止策略;仍使用一次性子进程,未引入常驻池或自动重试。
协议为 `{id,task,input}` 请求,以及 `{id,type:"progress",value}` / `{id,type:"result",result}` / `{id,type:"error",error:{code,message,detail?}}` 响应。每条消息必须以换行结束。文件协议单条上限 16 MiB(内容走文件系统),解析协议单条上限 128 MiB(包括 base64);stderr 持续排空且仅保留末尾 2000 字符用于诊断。大小限制不替代任务的解压与资源限制。
服务端 `FS_PROXY_MODE` 缺省为 `subprocess`,启动失败报错。`auto` 的进程内回退仅在明确选择时使用,不能保证回收阻塞在系统调用中的线程。解析隔离失败始终报错。
### 模块与业务入口
- 业务操作由所属模块的 service 提供,负责业务不变量、写入和事务边界。HTTP 路由负责协议、访问权限、参数与响应;能力处理器负责能力输入与授权上下文,然后调用同一业务入口。服务不从路由文件或 Agent 会话编排里取业务实现。
- 跨模块的同步操作可以显式调用已有 service,无需为一次调用再建接口、注册表或消息总线。需要随业务提交可靠触发的异步后续动作,在同一事务写 outbox;接收方按业务 ID 回查并幂等执行。
- 共用计划绑定、持续授权范围和预算检查放在 `src/authorization/grants.ts`;Agent 专有的会话、审批交互策略留在 `modules/agent/authorize.ts`。共用数据检查不依赖 Agent 会话装配。
- 示例:策略预演/发布由 `acquisition-organize/services/policies.ts` 提供,路由与能力共用;整理入口仍是 `services/organize.ts`,内部按计划、执行、收尾拆分。首次完成与恢复重放共用收尾处理,移除 qB 的不同判断留在各自调用处。
- 任务由所属模块用 `defineTask` 注册真实 handler,`@umt/tasks` 只提供执行机制;未注册任务明确报错,不注册空壳兜底。同步业务规则直接实现,出现实际扩展需求后再判断是否需要钩子。
- 同一含义的类型只定义一份,例如 domain 与 contracts 共用 `ContentType`。`@umt/mapping` 只处理真实语义差异,例如下载暂停原因或 `undefined ↔ unknown`,不为同值枚举保留转换表。
### 状态与配置归属
每个阶段维护自己的业务事实,引擎成功只表示本次任务尝试完成。业务仍在等待授权、资源或磁盘空间时,不能因为引擎成功就显示业务已完成。时间线与页面应按这些事实生成只读投影,不要另存一套总流程状态。
| 事实 | 维护方 |
|---|---|
| 发布资源解析、身份绑定与证据 | sources-identity 的资源/身份记录 |
| 订阅目标、单元满足度、候选与收录计划 | acquisition-organize 的 subscription / slot / acquisition_plan |
| 下载提交与下载进度 | download_task 与下载服务 |
| 整理计划、执行结果与文件证据 | organization_record / organization_file_result 与整理服务 |
| 刮削结果 | scrape_record 与刮削服务 |
| 步骤、租约、重试与唤醒 | 任务引擎;不替代上述业务状态 |
部署环境负责连接地址、密钥、基础目录等;settings 负责日常业务策略;单次请求只覆盖明确允许的选项,需要恢复的执行计划冻结本次有效值。读取与校验放在所属模块已有的配置入口,调用方不各写一遍合并规则。存量配置的优先级仍按各模块现有约定执行,有实际重复再统一;对用户有影响的覆盖应说明有效值与来源。
收录策略的适度抽象:`domain/acquisition/policy.ts` 维护分类选择顺序,`domain/slot/season-release.ts` 维护规格身份与排序,候选判断仍在 `evaluateCandidate`。`services/collection-strategy.ts` 统一策略模板到订阅版本的创建入口,分类查询在事务外,版本创建在订阅锁内。追更与洗版通过同一 `SlotSpec` 的阶段规则表达,不创建两套计划执行器。季级目标从资产与计划事实计算,计划冻结在途规格;分集完成、界面和旧文件处置都使用这些事实。
资源属性属于 sources-identity:`service/release-facts.ts` 合成解析输入,Agent 补充只存 `raw._facts` 并保留出处,重解析不会抹掉它,新的明确解析值优先。`release-facts-admin.ts` 提供窄的补全入口,调查编排和持续授权仍由 agent 模块处理;两者不互相承接业务状态。修改规则编辑器时应保留未编辑的 JSON 字段,同时显式清除用户删除的已编辑字段。
后续优先扩展现有纯判断和业务服务;出现共同不变量时抽成有明确输入输出的函数,文件 IO、事务锁、授权和重试继续留在执行阶段。默认策略更新只影响后续模板创建,不隐式改写已建订阅版本;需要调整已有作品时直接编辑其保留版本。新增规则字段必须同时检查候选评估、完成口径、覆盖展示和物理替换,避免各自形成第二套规则。
配置收敛先确定编辑归属:订阅表单维护追踪与下载安排,具体版本维护质量、组合规则和洗版;删掉重复入口时同步删除草稿与序列化,不留下两套普通写入路径。旧导入字段保留在兼容入口,普通保存不得清除或重建它们。总览与执行复用 `resolveRuleChain` 的版本优先级,订阅级自动补全继承主版本的收录链;显式开关仍优先。业务记录跳转复用 `subjectLink`,异常状态复用现有决策状态,不另造处理中心。
手动下载以默认值直接提交,高级选项仅覆盖本次操作。搜索与工作台复用 `ManualDownloadActions` 的提交反馈、未识别确认和高级选项窗口,资源摘要与 API 参数仍由各入口转换;原站页面保留必须先解析详情的准备步骤。默认下载不先加载目录、作品详情或已有版本,不为三个入口引入通用流程引擎。
整理恢复区分两种情况:文件执行中断从原冻结计划和持久文件结果继续;作品或季集事实纠正后,通过 `retryOrganizeRecord` 从原业务请求重新规划。同一原记录的重规划串行,避免事件和巡检同时冻结重复计划。原请求保存一次性目标、操作方式和覆写,函数、任务身份与路径守卫不序列化,恢复时重新检查来源边界。已成功文件与资产保持原状,恢复主文件失败状态时也恢复其附属文件依赖。
来源身份复用既有 `external_identity`,只持久化经过 TMDB 校验的辅助来源映射。Bangumi/豆瓣允许同一作品有多个分季身份,来源 ID 的全局唯一归属保持不变;需要单值 ID 的命名、刮削等消费者必须显式处理多值,不能随机取一条。身份解析不顺带改写资源季集,季集继续由所属模块换算。
历史记录恢复仍走现有整理入口:从原请求、可信旧任务输入到冻结计划依次提取事实,区分可直接重试与需要补齐的预填。文件管理通过 `recoveryRecordId` 交给业务层恢复下载关联、洗版与未编辑覆写,表单只覆盖明确支持的本次选项;来源范围和隐含参数共同参与预览校验。前端共用恢复选项的纯转换,不另造恢复向导或第二套请求模型;默认开始整理和手动预览共用冻结计划与入队函数。
身份重识别的幂等判断同时覆盖季集、日期、发布组和识别词命中;作品候选相同不代表解析事实相同,不能跳过事实变更后的季集重算。
季集计算和识别词归 sources-identity,整理请求、文件操作与入库状态归 acquisition-organize。整理把真实失败文件和同批正常文件样本交给映射事项,模拟与写入共用同一判断;映射重算后以 `media.episode_mapping_changed` 通知整理模块,只有季集已经确定的未完成记录重新规划。恢复预检与规划复用 `planFileEpisodes`,保留原资源的发布时间等解析上下文。Agent 的调查运行、预算和授权沿用现有机制,不另外创建恢复框架;身份补齐、重新排队和实际入库完成分别按业务事实核实。
维护时先验证真实的成功、失败和恢复路径,尤其是授权、文件替换、取消与外部副作用;删除空壳或无使用者的机制时同步删除其专属测试与文档。不要为了目录对称增加转发层,也不要把不同阶段的状态合并成一个枚举。
## 进程角色
`umt-server --role api|worker|worker:agent|all`(源码:`bun apps/server/src/main.ts --role ...`)。
| 角色 | 引擎 worker | OutboxRelay / 模块后台循环 |
|---|---|---|
| `api` | 不起(只入队/信号/取消) | 不跑 |
| `worker`(io 组) | 除 agent 道外的全部任务,分文件 / 网络 / 维护三条道(见下) | 跑 |
| `worker:agent` | 只跑 agent 道(`runAgent`/`resolveDecision`),并发 `WORKER_AGENT_CONCURRENCY`(缺省 2) | 不跑 |
| `all` | io 三条道 + agent 共四个池(开发缺省) | 跑 |
分组 worker 只认领自己的任务(`@umt/tasks` 按任务名过滤认领),Agent 长会话不占文件复制的槽,反之亦然。
worker 里有事件循环延迟探针:偏差超过 5s 打 `worker 事件循环延迟超过阈值` warn,次数随心跳事件(`loopLagWarnings`)发出;
每 15s 的进程心跳另记延迟与峰值(`process_heartbeat` / `process_heartbeat_sample`),运行状态页可看历史。
### IO Worker 分道(WK-1)
io worker 进程内按任务声明的 `lane` 起三个引擎 worker,各自独立并发、各自排队,一条道占满不影响另一条道领取:
| 道 | 缺省并发 | 放什么 |
|---|---|---|
| 文件 `file` | 2 | 整理(复制/硬链接/移动)、ffprobe、校验、接管整理、下载盘阈值清理 |
| 网络 `network` | 4 | 站点快照、RSS、TMDB、元数据刷新、搜索、下载器提交、识别(单站限流照旧另计,见 sites.md) |
| 维护 `maintenance` | 1 | 回收站清理、资产对账、调度器里内联的清理 / 回填 / 保留期裁剪;**空闲时跑** |
- **声明**:任务在 `defineTask({ lane })` 里显式声明(`@umt/tasks` 的 `TASK_LANES`)。没声明的任务运行时归网络道并在启动日志里
warn「这些任务没有声明 lane」;ci 里 `apps/server/test/worker-lanes.test.ts` 会失败(注册表与下表逐项核对,新增任务必须同时改这里)。
选"ci 报错 + 运行时兜底网络道"而不是"启动即失败":漏声明不该让生产进程起不来,但不能悄悄进 main。
- **维护道让路**:维护道每次领取前查全库文件道 + 网络道有没有已到点排队或正在运行的 run(1s 缓存),有就不领;已经在跑的维护任务
不打断。连续让路超过 10 分钟放行一个(日志「维护道让路超时」),防止网络道长期有活时维护永远跑不上。实现是给维护道 worker 的
认领加闸门(`StartWorkerOptions.claimGate`),不改 OpenWorkflow;取舍:按"全库有无活"判断,多个 io 实例时别的实例忙也会让路,
判断有 1s 滞后,够用且简单。
- **调度器内联的维护任务**:`ScheduledJobDef.lane = "maintenance"` 的定时任务(下表)在调度器里直接执行,与维护道共用并发与让路规则:
进不了维护道就保持到期、下一轮再试;在计划页点「立即执行」不让路。其余定时任务只入队或很快结束,不占道。
- **并发配置**:设置 → 系统 →「后台任务并发」(`settings.automation.workerLanes`),保存后约 5 秒内热生效(每道开 16 个槽,
按设置值收)。环境变量 `WORKER_FILE_CONCURRENCY` / `WORKER_NETWORK_CONCURRENCY` / `WORKER_MAINTENANCE_CONCURRENCY` 设了就以变量为准
(运行状态页悬停提示"由环境变量指定")。旧变量 `WORKER_CONCURRENCY` 映射到**网络道**(`WORKER_NETWORK_CONCURRENCY` 未设时):它原来
是 io 组总并发,缺省 4,与网络道缺省相同;文件 / 维护道按设置页走。compose 不再给 `WORKER_CONCURRENCY` 缺省值。
- **观测**:运行状态页「进程」表的并发列,io 进程按道显示「运行中/并发 · 排队 N」,维护道让路时标「让路中」;数据在心跳行
`concurrency.{file,network,maintenance}` 与 `extra.lanes`(排队是全库数,运行中是本进程数)。
- **拆进程的触发条件**(未实施):整理期间心跳事件循环延迟峰值持续 > 200ms,或整理需要贴近存储部署到另一台机器。
#### 任务归属表
ci 逐行核对这张表与任务注册表(`apps/server/test/worker-lanes.test.ts`),改任务的 lane 或新增任务时同步改。
| 任务 | 道 | 说明 |
|---|---|---|
| `verifyAndImport` | file | 下载完成后的校验、ffprobe、复制/硬链接/移动入库(含接管整理) |
| `cleanupDownloadCache` | file | 下载盘到暂停线时的清理;要尽快腾空间,不放维护道 |
| `scrapeMedia` | file | 刮削:写 NFO 与图片到媒体库目录(入库后自动 / 作品详情 / 文件管理),图片下载限并发 4 |
| `ingestFeed` | network | RSS 抓取 |
| `resolveObservation` | network | 标题识别 + TMDB |
| `enrichPoolEntry` | network | 未识别资源的元数据补全 |
| `refreshMetadata` | network | TMDB 元数据刷新 |
| `prewarmMetadata` | network | 关注作品预热:详情 / 各季 / 剧集组 / 海报背景标志进缓存(docs/runbook/cache.md) |
| `reresolveIdentifyWords` | network | 全局识别词保存后分批扫资源,标题命中改动行的重新识别(docs/design/identify-words.md) |
| `searchSubscription` | network | 订阅搜索 |
| `snapshotSite` | network | 站点快照 |
| `refreshSiteAccount` | network | 站点登录 / 用户数据刷新 |
| `evaluateAcquisition` | network | 获取策略评估(查库为主,随上游链路放网络道) |
| `submitDownload` | network | 提交下载器 |
| `identifyRemote` | network | 文件管理「识别」的 TMDB 查询 |
| `refreshPiTokens` | network | 模型服务 OAuth 令牌刷新 |
| `reconcileLibrary` | maintenance | 资产对账(扫库) |
| `purgeTrash` | maintenance | 回收站到期清除 |
| `scanOrganizeSource` | maintenance | 整理来源基线与增量快照,候选整理进入文件道 |
| `runAgent` | agent | Agent 会话(worker:agent) |
| `resolveDecision` | agent | 决策处理(worker:agent) |
调度器内联、走维护道的定时任务:`cache.sweep`、`cache.images_clean`、`cache.images_lru`、`auth.cleanup`、`system.heartbeat_cleanup`、
各保留期裁剪(`system/retention.ts` 的 `RETENTION_JOBS`)、`metadata.backfill_languages`(元数据语言回刷)。
只入队的定时任务(如 `trash.purge`、`cache.threshold`、`library.reconcile`、`pi.token_refresh`)按所入队任务的道执行。
启动时 `bootstrap()` 装配 `apps/server/src/modules/` 下的业务模块(见 apps/server/README.md),并连接任务引擎。数据库不可达时进程启动失败并退出。
acquisition-organize 的后台循环只做编排:qB 同步(含阈值暂停/恢复)一个定时器;另一个定时器按阈值入队 `cleanupDownloadCache`、
巡检补投 `submitDownload`/`verifyAndImport`、每 6h 入队 `reconcileLibrary`,重活都在任务里跑。
`POST /download-cache/cleanup`、`POST /library/reconcile` 同样只入队,返回 `{taskId, runId}`(202)。
HTTP 调用外部 IO 型能力(`release.search` 实时搜索、`release.inspect` 刷新详情、`identity.resolve`、`catalog.lookup` includeRemote)
限时 15s,站点并发 2、TMDB 并发 4,超时 502 / 排队满 429。
一次性子命令(容器入口用):`--role migrate`(迁移到最新,迁移文件已编进二进制;随后迁移任务引擎的 openworkflow schema)、`--role preflight`(启动自检:生产环境拒绝占位符密钥与 root、DB 可达、`WEB_ROOT/index.html` 存在)、`--role probe`(打本机 `/api/v1/health`,HEALTHCHECK 用)。
设置 `WEB_ROOT` 后 API 进程在同一端口托管 web 构建产物:存在的文件直接返回,其余非 `/api` 路径回退 `index.html`。
## Docker 单容器部署
```sh
export SECRET_MASTER_KEY=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 16)
docker compose up -d --build # 服务:app(4380)+ postgres(数据卷 umt-pg-data)
curl localhost:4380/api/v1/health
```
- 镜像:`oven/bun:1.4.3` 构建 web 与 `bun build --compile` 出二进制,并构建 `sandbox-policy.node`,运行时 `debian:trixie-slim` + gosu + postgresql-client + 静态 ffprobe,root 启动修 `/data` 属主后降权到 PUID(缺省 10001)。
- 入口 `docker/entrypoint.sh`:迁移 → 自检 → 同时起 api、worker、worker:agent 三个进程;任一退出则停掉其余并以非 0 退出,由 `restart: unless-stopped` 拉起。
- 主进程使用非 root 用户和只读的 `/app` 产物;外部字节解析另起子进程,由原生 seccomp 策略限制文件、网络与进程访问。镜像带静态 ffprobe;运行期写入落在 `DATA_DIR`(容器内 `/data`)。完整部署说明见 [deploy.md](deploy.md)。
- 构建版本号:`APP_VERSION=v0.1.0 docker compose build`(写入 web 的 `version.json`)。
## 测试数据库
需要 PG 的测试默认连 `DATABASE_URL` 或 55433 开发库。单包测试在库不可达时会跳过集成用例,`bun run test:all` 会先检查连接并直接失败,不能把跳过当作通过。tasks 包可用 `TASKS_TEST_DATABASE_URL` 指定独立库;每个测试文件使用带 pid+随机后缀的独立 schema,结束时删除,多份测试并发跑也不冲突。
测试被中断(超时、kill、并发跑挂掉)时 schema 不会删,开发库会越积越多。清理脚本:
```sh
bun scripts/dev-db-clean.ts # dry-run:只打印清单
bun scripts/dev-db-clean.ts --verbose # 同上,逐个列出 schema 名
bun scripts/dev-db-clean.ts --yes # 真正删除
```
- 只在 `DATABASE_URL`(缺省 `postgres://umt:umt@localhost:55433/umt`)指向 localhost:55433 时运行,否则直接拒绝。
- 删除的 schema:`tasks_test_`、`test_`、`e2e_`、`umt_test_`、`ao_test_`、`fm_test_`、`w2a_test_`、`w2b_`、`w7a_mig_`、`w9g_search_`、`server_test_` 前缀。`public`、`openworkflow` 不动;有其它会话持锁的 schema(测试正在跑)跳过,但最好在没有测试运行时执行。
- 删除的 public 数据:名字以 `w32-`、`w33-`、`fe4-`、`qa-` 开头的作品(title)、站点(name)、订阅(title_query),以及按 restrict 外键挂在它们下面的行,在一个事务里删。`admin_user`(`w31-admin`)、`pi_upstream`、没有这些前缀的站点(fake 上游)和手工登记的数据不动;要牵连到受保护表时脚本中止。
## 排障
- `配置无效`:按提示检查 `.env`,`SECRET_MASTER_KEY` 至少 16 字符,`DATABASE_URL` 必须是 postgres://。
- health 返回 `db: "down"`:`docker compose -f docker-compose.dev.yml ps` 看容器健康状态。
- 解析沙箱提示策略模块不存在:先跑 `bun run sandbox:build`;编译部署需把 `sandbox-policy.node` 放在二进制旁边。
- tsc 使用 `config/tsconfig.base.json`;运行时类型来自 Node / Bun,不再生成 Deno 声明。
- 日志中的 passkey/token/cookie/authorization/password 字段与 URL query 参数会被替换为 `[REDACTED]`(含 `logger.child()` 绑定字段与 `err` 的 message/stack);若发现明文,属于缺陷。