refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
+13
-81
@@ -1,31 +1,26 @@
|
||||
# 部署运行手册(W4-B)
|
||||
# 部署运行手册
|
||||
|
||||
形态:**核心应用在一个容器里,PostgreSQL 与浏览器分别部署**。镜像 `uhd-media-tool` 内一个 `deno compile` 出的二进制 `umt-server`,
|
||||
形态:**核心应用在一个容器里,PostgreSQL 与浏览器分别部署**。镜像 `uhd-media-tool` 内一个 `bun build --compile` 出的二进制 `umt-server`,
|
||||
容器里同时跑 api、worker(io 组)、worker:agent(Agent 会话)三个进程,api 同端口托管 web 静态产物(`WEB_ROOT=/app/public`),对外只有 **4380** 一个端口。
|
||||
compose 默认有三个服务:`app` + `postgres` + `camofox`(浏览器服务,只在内网,不映射端口;可关掉或换成自有实例,见 §2.1);`tls` profile 可额外启用 Caddy 提供 HTTPS。
|
||||
|
||||
## 1. 构建镜像
|
||||
|
||||
构建阶段会下载 Deno 运行时等外网资源。直连不稳时在 `.env.deploy` 配 `BUILD_PROXY`(如经宿主中继 `http://172.17.0.1:7891`),`deno task release` 会自动带上 `--build-arg HTTP(S)_PROXY`;代理只用于构建,不进镜像。
|
||||
构建阶段需要 Bun 镜像、依赖包与构建工具等资源。直连不稳时在 `.env.deploy` 配 `BUILD_PROXY`(如经宿主中继 `http://172.17.0.1:7891`),`bun run release` 会自动带上 `--build-arg HTTP(S)_PROXY`;代理只用于构建,不进镜像。
|
||||
|
||||
```sh
|
||||
docker build -t uhd-media-tool:latest . # 或 docker compose --env-file .env.deploy build
|
||||
APP_VERSION=v0.3.11 docker compose --env-file .env.deploy build app # 示例:发布时填写对应版本,写进 web 的 version.json
|
||||
```
|
||||
|
||||
- 多阶段:`denoland/deno:2.9.7`(装依赖 → web 构建 → server 编译)→ `debian:trixie-slim` 运行镜像。
|
||||
- 多阶段:`oven/bun:1.4.3`(装依赖 → web 构建 → server 编译)→ `debian:trixie-slim` 运行镜像。
|
||||
- 运行镜像额外装 `gosu`(降权)及 PG 客户端动态库,从固定的 `postgres:18.6-trixie` 官方镜像复制 pg_dump/pg_restore/psql 与 libpq 18,
|
||||
外加 `/usr/local/bin/ffprobe`:从 `mwader/static-ffmpeg:9.0.2` 拷单个静态二进制(约 135 MB;Debian `ffmpeg` 包要 200+ 个依赖约 450 MB)。
|
||||
换来源用 `--build-arg FFPROBE_IMAGE=<含 /ffprobe 的镜像>`。
|
||||
- `/app` 下的二进制、静态产物、脚本全部 root 属主只读;运行用户改不了它们。
|
||||
- 二进制编译时固定权限:`--allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,cat,/app/umt-server`
|
||||
(库根/下载目录由用户挂载,位置不固定,所以读写不按路径限制;写保护靠只读 `/app` + 非 root 运行)。
|
||||
子进程只放三个:ffprobe(入库后探测视频流)、sha256sum(coreutils 自带;目标已存在/跨卷恢复时的全量哈希在子进程里算,
|
||||
不占 worker 事件循环;不可用时回退进程内流式哈希,整理结果里 `hashVia` 记录实际方式)、二进制自身
|
||||
(fsproxy 挂死隔离:入库的每个文件操作起一次 `/app/umt-server --role fsworker`,约 150 ms;进度停滞超过 `FS_HANG_TIMEOUT_MS`
|
||||
→ SIGTERM → `FS_KILL_GRACE_MS` 后 SIGKILL → 按 TIMEOUT 有限次重试,超限建待处理)。Deno 的 `--allow-run` 按绝对路径放行,
|
||||
所以二进制必须位于 `/app/umt-server`;挪到别处运行时 spawn 被拒(NotCapable),自动回退进程内看门狗(卡死的系统调用线程无法回收),
|
||||
日志 `organize.transfer` 的 `fsproxy.mode/workerKind/fallbackReason` 可看出实际方式。
|
||||
- 构建同时生成解析隔离模块 `sandbox-policy.node`,与二进制放在 `/app`。主进程以非 root 运行,外部字节解析另起子进程并安装 seccomp 策略;详见 ADR-005 与 ADR-007。Bun 主进程不提供 Deno 的运行时权限开关。
|
||||
- 镜像带静态 ffprobe;全量文件哈希优先走 sha256sum 子进程,不可用时回退流式哈希,整理结果的 `hashVia` 记录实际方式。
|
||||
- 文件操作通过 `/app/umt-server --role fsworker` 一次性子进程执行,进度停滞超过 `FS_HANG_TIMEOUT_MS` 后先 SIGTERM、再按 `FS_KILL_GRACE_MS` 发送 SIGKILL。`FS_PROXY_MODE` 缺省 `subprocess`,启动失败报错;只有显式选择 `auto` 才允许进程内回退。业务服务决定重试与恢复,通信层不自动重做副作用。
|
||||
|
||||
## 2. 启动
|
||||
|
||||
@@ -146,14 +141,13 @@ docker compose --env-file .env.deploy logs app | grep 'setup token'
|
||||
|
||||
发布前检查旧镜像的实际在途任务:在任务页核对 queued / running / waiting,尤其是本次变更的任务名称和版本。优先等它们完成;需要中止的任务先保留原输入、冻结计划和操作记录,确认中止后通过现有入口重新发起。已经产生文件操作证据的任务要先核实执行结果,再决定后续处理。没有旧在途任务时不长期保留旧 handler;业务表前滚不能替代任务版本兼容判断。
|
||||
|
||||
本次整理来源功能增加 `scanOrganizeSource@1` 和迁移 `0059_organize_source_snapshot`,沿用 `verifyAndImport`。迁移不会扫描或整理已有目录;用户新增来源后先建立基线,存量必须单独触发。
|
||||
|
||||
整理来源新增后先建立基线,存量扫描必须单独触发;迁移本身不扫描或整理已有目录。
|
||||
|
||||
本机测试环境的标准做法:
|
||||
|
||||
```sh
|
||||
cd <仓库> && git pull
|
||||
deno task release # 工作区须干净、CHANGELOG 已有本版条目;打 tag → 构建 → 只替换 app → 核对健康版本
|
||||
bun run release # 工作区须干净、CHANGELOG 已有本版条目;打 tag → 构建 → 只替换 app → 核对健康版本
|
||||
```
|
||||
|
||||
`scripts/release.sh --no-build` 用已有镜像只重建容器。真有要保留的数据时,先 `docker compose --env-file .env.deploy exec app umt backup` 再发布。手动等价命令:
|
||||
@@ -176,60 +170,6 @@ docker compose --env-file .env.deploy ps && docker compose --env-file .env.deplo
|
||||
- 一次性子命令:`docker compose --env-file .env.deploy exec app umt migrate|preflight|export-mappings|backup`、
|
||||
`docker compose --env-file .env.deploy run --rm --no-deps app restore <备份>`。
|
||||
|
||||
## 验证记录
|
||||
|
||||
2026-09-27,W4-B,Docker 29 / compose v2,在干净 worktree(HEAD 8c6a922 + 本次改动)构建,
|
||||
临时项目名 `umtw4b`、app 端口 127.0.0.1:4390、pg 端口 127.0.0.1:55440,`/media` 绑定一个属主 10001 的临时目录。
|
||||
|
||||
| 步骤 | 结果 |
|
||||
|---|---|
|
||||
| `docker build --build-arg APT_MIRROR=mirrors.ustc.edu.cn` | 成功(缓存命中 2 分 24 秒);镜像 489 MB(压缩后 107 MB):二进制 213 MB、gosu+postgresql-client 79 MB、web 2 MB |
|
||||
| `compose up -d` | postgres healthy 后 app 启动,约 6 秒 healthy;22 个迁移(编号 0017 空缺)+ 引擎迁移,`启动自检通过 dataDir=/data sessionDir=/data/agent-sessions` |
|
||||
| 进程 | 两个 `umt-server`(api、worker)uid/gid 均为 10001;`/data/*` 属主 10001;`/app/umt-server` 为 `r-xr-xr-x root`,运行用户写入 → Permission denied |
|
||||
| `GET /` 与 `/console/settings` | 200,SPA 由同端口托管 |
|
||||
| setup token(日志)→ `POST /auth/setup` → `POST /auth/login` | 成功,拿到会话与 csrf cookie |
|
||||
| 会话 + csrf `POST /api-keys {scopes:[admin]}` | 返回 `umt_…` 明文一次 |
|
||||
| API Key `GET /organization-records` | 200 |
|
||||
| API Key `POST /library-roots {localPath:/media}` | 成功,宿主目录出现 `.uhd-root`(属主 10001);容器内 root 属主目录登记 → 400 `库根不可写 … EACCES` |
|
||||
| `PATCH /settings {pi:{provider:fake}}` → `POST /agent/runs {mode:plan_only}` | run `succeeded`,会话文件 `/data/agent-sessions/<runId>.jsonl`(18.7 KB,属主 10001) |
|
||||
| 宿主 `chmod 555` 媒体目录后 `umt preflight` | 退出码 0,`warn 挂载路径不可用: verify-media 不可写: EACCES`,审计 `preflight.path_unavailable` 一条;恢复权限后 warnings=0 |
|
||||
| `umt backup` | `db.dump` 245 KB + `mappings.json` + `agent-sessions.tar.gz` + `manifest.txt` + `SHA256SUMS`;pg_dump 17.11 对服务端 16.14 |
|
||||
| 清库 | `drop schema public cascade` + 删 `openworkflow` schema → 0 张表 |
|
||||
| app 运行中 `umt restore` | 拒绝:`数据库还有 13 个其它连接` |
|
||||
| `compose stop app` → `compose run --rm --no-deps app restore <ts> --yes` | 退出码 0;63 张表恢复;抽查 admin_user 1 / api_key 1 / library_root 1 / media_entity 3 / external_identity 3 / agent_run 1 / agent_run_event 9 与备份前一致(audit_event +1 为恢复审计、settings +1 为 automation 行);映射清单逐表比对"一致";`automation={"readOnlyReconcile": true}`,审计 `ops.read_only_reconcile_on` |
|
||||
| 恢复后 `up -d app` | healthy;`GET /settings` → `automation {enabled: true, readOnlyReconcile: true}` |
|
||||
| `BACKUP_KEEP=3` 连续备份 3 次 | 最旧一份被轮转删除,剩 3 份 |
|
||||
| `PUID=1234 PGID=1234` 空卷启动 | `/data` 及子目录 chown 为 1234,进程 uid 1234;`PUID=0` → 退出码 2 |
|
||||
| `compose stop app` | 0.8 秒,api/worker 均收到 SIGTERM 正常退出 |
|
||||
| 清理 | `compose down -v`、删除测试镜像;未 prune 全局 build cache |
|
||||
|
||||
首次演练发现:镜像里的 pg_restore 17 直连 16 版服务端会因 `SET transaction_timeout` 失败(单事务回滚,库未被破坏),
|
||||
已改为 pg_restore 输出 SQL、过滤该行后由 psql 单事务回放,复测通过。
|
||||
|
||||
2026-09-27,W4-D,临时项目名 `umtw4d`、app 127.0.0.1:4391、pg 127.0.0.1:55441(验证后已 `down -v` 并删除镜像)。
|
||||
|
||||
| 步骤 | 结果 |
|
||||
|---|---|
|
||||
| `docker build --build-arg APT_MIRROR=mirrors.ustc.edu.cn` | 成功;镜像内容 160.5 MB 压缩(W4-B 107 MB),新增层:静态 ffprobe 135 MB(未压缩),二进制 214 MB |
|
||||
| `compose up -d` | 约 8 秒 healthy;`/api/v1/health` → `status=ok role=api db=ok` |
|
||||
| 进程 | `entrypoint.sh serve` 下三个 `umt-server`:`--role api`、`--role worker`(日志 `group=io`,9 个任务,并发 4,模块循环 4 个)、`--role worker:agent`(`runAgent@1`/`resolveDecision@1`,并发 2,无模块循环),均为 uid 10001 |
|
||||
| ffprobe | 容器内 `gosu 10001 ffprobe` 探测一个 2 KB WAV → `pcm_s16le`;`ffprobe version 7.1.1` |
|
||||
| 编译权限 | 同一组编译参数的探针二进制在容器内以 10001 运行:node `execFile("ffprobe")` 成功、`Deno.Command("sha256sum")` 成功、`Deno.Command("ls")` → `NotCapable` |
|
||||
|
||||
2026-09-27,W5-E(生产 fsworker),临时项目名 `umtw5e`、app 127.0.0.1:4395、pg 127.0.0.1:55442,`/media` 绑定属主 10001 的临时目录;
|
||||
在干净 worktree(HEAD 89ea684 + 本次改动)构建。`FS_HANG_TIMEOUT_MS=3000`、`FS_KILL_GRACE_MS=500`。
|
||||
|
||||
| 步骤 | 结果 |
|
||||
|---|---|
|
||||
| `docker build --build-arg APT_MIRROR=mirrors.ustc.edu.cn` | 成功(约 2 分 30 秒,缓存命中依赖层) |
|
||||
| setup → login → admin API Key → `POST /library-roots {localPath:/media}` → 改 `managed`;SQL 插 3 个 movie 作品 | 成功 |
|
||||
| 手动整理 `POST /organization-records` dryRun → 不带 dryRun 执行(Copy,1.5 GB 稀疏文件 `/media/_incoming/Check.Movie.2021…mkv`) | 记录 succeeded,1.8 s;日志 `organize.transfer` 带 `fsproxy:{mode:"subprocess",workerKind:"self",spawned:5,timeouts:0,fallbackReason:null}` |
|
||||
| 以 `UMT_FS_TEST_HANG_AFTER_BYTES=1048576 UMT_FS_TEST_HOOKS=1` 重建 app 后整理 60 MB 文件 | 容器内 `/proc` 看到 `/app/umt-server --role fsworker`(uid 10001,父进程为 `--role worker`)存活约 3–4 s 后消失,无残留子进程 |
|
||||
| 超时链路 | 每次尝试:停滞 3 s → SIGTERM(worker 屏蔽)→ SIGKILL → `TIMEOUT`;共 3 次(`FS_TRANSFER_MAX_ATTEMPTS`)后账本 `{"code":"TIMEOUT","message":"fs copy 挂死:进度停滞超过 3000 ms"}`,`fsproxy.timeouts=3 killed=3`,待处理「文件操作挂死,重试已达上限」;目标目录只留被 KILL 的 `.uhd-partial`(下次写前按年龄清理),无最终名半成品 |
|
||||
| 清理 | `compose down -v`、删除测试镜像与临时目录 |
|
||||
|
||||
`UMT_FS_TEST_HANG_AFTER_BYTES` 只用于这类验收(让数据类操作的 worker 读到 n 字节后卡死 JS 线程并屏蔽 SIGTERM),必须同时设置 `UMT_FS_TEST_HOOKS=1` 才生效;单独设置不生效,但 `preflight` 会打 warn。生产不要设置。
|
||||
|
||||
## 容器经宿主代理访问境外站点(可选)
|
||||
|
||||
宿主上的代理(如 mihomo)常只监听 `127.0.0.1`,容器从网桥够不到。改动最小的做法:加一个只绑在 docker 网桥地址上的中继,再放行 docker 网段到该端口;代理本身配置不动。写在本机 `docker-compose.override.yml`(不进 git):
|
||||
@@ -253,7 +193,7 @@ services:
|
||||
|
||||
compose 已给 app 设了缺省 `NO_PROXY`(`UMT_NO_PROXY` 可改),override 里重写 `NO_PROXY` 时别漏掉下载器的服务名。
|
||||
|
||||
防火墙只放行 docker 网段:`ufw allow from 172.16.0.0/12 to 172.17.0.1 port 7891 proto tcp`。compose 默认网络已固定在 `UMT_SUBNET`(缺省 172.30.0.0/24,与本机其他网络冲突时在 `.env.deploy` 改成 172.16.0.0/12 内的空闲网段,如 172.16.80.0/24),否则 compose 自动分配的 192.168.x 网段不在放行范围内。Deno 的 fetch 会读 `HTTP(S)_PROXY`/`NO_PROXY`;TMDB、境外 Pi 上游、camofox 访问境外站都经此代理,站内(postgres/camofox)不走代理。
|
||||
防火墙只放行 docker 网段:`ufw allow from 172.16.0.0/12 to 172.17.0.1 port 7891 proto tcp`。compose 默认网络已固定在 `UMT_SUBNET`(缺省 172.30.0.0/24,与本机其他网络冲突时在 `.env.deploy` 改成 172.16.0.0/12 内的空闲网段,如 172.16.80.0/24),否则 compose 自动分配的 192.168.x 网段不在放行范围内。应用的外部请求统一读取 `HTTP(S)_PROXY`/`NO_PROXY`;TMDB、境外 Pi 上游、camofox 访问境外站都经此代理,站内(postgres/camofox)不走代理。
|
||||
|
||||
## 测试用 qB(override 示例)
|
||||
|
||||
@@ -263,7 +203,7 @@ compose 已给 app 设了缺省 `NO_PROXY`(`UMT_NO_PROXY` 可改),override 里
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
# 配了 HTTP(S)_PROXY 时必须把 qbittorrent 加进 NO_PROXY:Deno 按主机名匹配,不会因解析到内网 IP 而直连
|
||||
# 配了 HTTP(S)_PROXY 时必须把 qbittorrent 加进 NO_PROXY:代理按主机名匹配,不会因解析到内网 IP 而直连
|
||||
NO_PROXY: localhost,127.0.0.1,::1,postgres,camofox,proxy-relay,qbittorrent,qbittorrent-nox,qb,transmission,deluge,aria2,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
qbittorrent:
|
||||
image: lscr.io/linuxserver/qbittorrent:5.2.4 # v4 / 5.0–5.2 都兼容(QB-1 实测 5.1.2、5.2.4)
|
||||
@@ -319,14 +259,6 @@ API Key 在 qB 5.2+ WebUI 选项 → WebUI → API Key 处生成 / 轮换(等价
|
||||
5.2.4 账密 / API Key / 无认证 / 失败分类,各跑 登录 → 添加(testkit `makeTorrent({ realPieces: true })` 真实 piece 哈希,
|
||||
内容写进保存目录,qB 校验即完成)→ 查询 → 标签 → 暂停恢复 → 删除(不删数据)。
|
||||
|
||||
## v0.3.10 发布补充
|
||||
## 本机测试入口
|
||||
|
||||
2026-10-01 已发布 v0.3.10,执行 `0060_manual_acquisition`:`acquisition_plan.slot_id` 允许为空,支持独立手动下载;`download_task.import_plan_id` 保存本次入库规划,并以外键引用计划。部署前备份与校验通过,只替换 app 容器;线上接口、版本一致及页面冒烟通过。后续普通 Compose 启动使用已更新的 `.env.deploy` 版本与源码标记。具体证据见 发布记录(已删除,见 git `ceb66f8`)。
|
||||
|
||||
### 当前测试入口
|
||||
|
||||
用户要求测试自签 HTTPS 后,4380 已由 Caddy 接管为 `https://服务器地址:4380`;原 443 入口保留,app 本机 HTTP 调试入口为 `http://127.0.0.1:4381`。配置保存在 `.env.deploy` 与本机 `docker-compose.override.yml`,重建了 app/Caddy 容器,镜像及数据库版本保持 v0.3.10/0060。证书由原 Caddy 内部 CA 签发,健康、管理员接口和 HTTPS 页面冒烟通过。
|
||||
|
||||
## v0.3.11 修复发布
|
||||
|
||||
搜索结果标题接入已有资源详情抽屉,已发布 v0.3.11;数据库保持 `0060`。浏览器桌面修复镜像为 `umt-browser-desktop:1.18.0-umt.1`,内置有效的最大化配置,已验证上下黑边消失。`.env.deploy` 已记录当前应用版本、源码标记和浏览器镜像,4380 HTTPS/443 HTTPS/本机 4381 HTTP 映射保留。具体验证见 发布记录(已删除,见 git `ceb66f8`)。
|
||||
本机测试部署由 Caddy 提供 `https://<服务器地址>:4380`,同时保留 443 HTTPS;app 的本机 HTTP 调试入口为 `http://127.0.0.1:4381`。配置在 `.env.deploy` 与本机 `docker-compose.override.yml`,证书由 Caddy 内部 CA 签发。发布时按配置核对健康接口版本。
|
||||
Reference in new issue
Block a user