Files
UHD_Media_Tool/docs/runbook/dev.md
T

23 KiB
Raw Blame History

开发环境运行手册

前置

  • 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 已被其他项目占用,不要改用。

首次启动

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。

验证

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 调用者用)

常用命令

命令 作用
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 单容器部署

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。
  • 构建版本号: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 不会删,开发库会越积越多。清理脚本:

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);若发现明文,属于缺陷。