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
@@ -1,7 +1,7 @@
|
||||
# ADR-006:生命周期事件、链式钩子与功能模块约定
|
||||
# ADR-006:生命周期事件与功能模块约定
|
||||
|
||||
- 状态:采纳(2026-10-06,用户确认方向)。
|
||||
- 关联:ADR-002(outbox 与任务引擎)、`packages/contracts/src/events.ts`(`LIFECYCLE_EVENTS`)、`packages/contracts/src/hooks.ts`(`HOOK_POINTS`)、`packages/contracts/src/features.ts`(`FEATURES`、`SLOT_POSITIONS`)、`apps/server/src/hooks/`、`apps/web/app/lib/slots.ts`。
|
||||
- 状态:采纳(2026-10-06);2026-10-12 精简通用链式钩子,保留生命周期事件和功能模块约定。
|
||||
- 关联:ADR-002(outbox 与任务引擎)、`packages/contracts/src/events.ts`(`LIFECYCLE_EVENTS`)、`packages/contracts/src/features.ts`(`FEATURES`、`SLOT_POSITIONS`)、`apps/web/app/lib/slots.ts`。
|
||||
- 参照:MoviePilot v3 `app/schemas/types.py` 的 `EventType`(广播)与 `ChainEventType`(链式),分发在 `app/runtime/events.py`。
|
||||
|
||||
## 背景
|
||||
@@ -13,23 +13,13 @@
|
||||
|
||||
## 决定
|
||||
|
||||
### 1. 两类事件
|
||||
### 1. 业务调用与事后通知
|
||||
|
||||
| | 广播事件 | 链式钩子 |
|
||||
| --- | --- | --- |
|
||||
| 对照 MP | `EventType`(transfer.complete、subscribe.added …) | `ChainEventType`(name.recognize、transfer.rename …) |
|
||||
| 时机 | 事后通知:事情已经发生并提交 | 事中:业务流程走到挂点时同步调用 |
|
||||
| 传递 | 业务事务里写 `outbox_event`,中继投递成任务(异步、至少一次) | 进程内直接调用,按注册顺序 |
|
||||
| 能做什么 | 订阅方自己建任务做后续动作 | 改写一个值(rewrite)或拒绝(intercept) |
|
||||
| 失败 | 订阅任务按引擎重试;路由函数抛错只跳过该订阅方 | 钩子抛错 / 超时只记日志、跳过该钩子,主流程照常 |
|
||||
| 去重 | outbox 幂等键(`<type>/<部分…>`),订阅任务用引擎幂等键 | 不需要 |
|
||||
同步规则由所属模块 service 明确实现,路由、能力与任务共用业务入口。跨模块需要同步取得结果时显式调用 service。
|
||||
|
||||
**何时用哪类**:只想在「X 发生之后」做点事(发通知、补字幕、预热缓存)→ 订阅广播事件;要改变正在进行的决定(换个名字、换个候选、不让整理、晚点完成)→ 注册链式钩子。拿不准就用广播:它不在主流程里,坏了也不影响主流程。
|
||||
需要在业务提交后可靠执行的动作(补字幕、通知、预热等)订阅生命周期事件。业务事务内调用 `ctx.outbox.emit(trx, …)`,与业务写入一起提交或回滚;payload 只放 ID 和少量判断事实。中继至少一次投递成任务,接收方按 ID 回查业务表并幂等执行。
|
||||
|
||||
**事务边界**:
|
||||
|
||||
- 广播事件在业务事务**里**写 outbox(`ctx.outbox.emit(trx, …)`),与业务写入一起提交或回滚;payload 只放 ID 和少量判断用的事实,订阅方按 ID 回查业务表(事件可能晚到、重复到)。
|
||||
- 钩子可能在业务事务里被调用(订阅完成判定就在行锁里):只做内存里的判断与改写,**不要发外部请求、不要写库**;缺省每个钩子 2 秒上限。需要查外部的,改成订阅广播事件后异步做。
|
||||
原先参考 MP `ChainEventType` 实现的通用 rewrite/intercept 钩子没有实际业务注册者,本轮删除其注册表、挂点与调用层。以后出现真实扩展需求时再判断抽象方式。`modules/decision-hooks.ts` 是已有待处理事项的业务处理器,语义不同,继续保留。
|
||||
|
||||
### 2. 广播事件清单
|
||||
|
||||
@@ -56,32 +46,11 @@ payload schema 都在 `EVENT_PAYLOADS`(`emit` 时校验,不合法直接抛
|
||||
|
||||
**订阅**:模块在 `outboxRoutes` 里写 `type → 路由函数`。生命周期事件可以有多个订阅方(`collectOutboxRoutes` 按模块顺序合成),其它事件(内部流程,如 `release.observed → resolveObservation`)仍只能一个模块认领。路由函数返回任务、任务数组或 `null`(不建任务);中继对第一个任务用事件的幂等键、其余用 `<键>#<任务名>`。没有订阅方的生命周期事件也会被中继认领、标记已投递,并镜像到 SSE(`worker/index.ts` 的镜像前缀)。
|
||||
|
||||
### 3. 链式钩子清单
|
||||
|
||||
挂点名称与语义在 `HOOK_POINTS`,入参 / 返回值类型在 `apps/server/src/hooks/points.ts`。
|
||||
|
||||
| 挂点 | 类型 | 位置 | 能做什么 |
|
||||
| --- | --- | --- | --- |
|
||||
| `name.recognize` | rewrite | `resolveRelease`(标题解析后、召回前)、`identifyParsed`(整理 / 接管按名称识别) | 改写解析结果(标题、年份、季集、外部 ID) |
|
||||
| `organize.rename` | rewrite | `planImport`,模板命名生成目标之后(保留原名 / 原地入库不调) | 改写主视频目标文件名;同主干的附属文件跟着改;结果带路径分隔符或空名的钩子跳过;改名后撞名转 `duplicate_target` |
|
||||
| `organize.intercept` | intercept | `planImport`,写整理记录之前(预演也调) | 返回原因即拒绝:记录为失败,`reason = intercepted:<原因>` |
|
||||
| `resource.selection` | rewrite | 订阅 / 收录版本重评(`reevaluate`)择优排序之后 | 重排或过滤候选;不认识的 ID 忽略,不能加入新候选 |
|
||||
| `subscription.completion.check` | intercept | `completeSubscription`,标记完成之前 | 追加完成条件:返回原因即这次不完成,订阅继续追踪 |
|
||||
|
||||
语义:
|
||||
|
||||
- 注册在模块启动时完成:`ServerModule.registerHooks(hooks, ctx)`,`bootstrap` 按 `SERVER_MODULES` 顺序调用;执行顺序即注册顺序(不设优先级数字,需要先后就调整模块顺序)。
|
||||
- rewrite:每个钩子拿到上一个钩子的结果,返回新值或 `undefined`(不改)。intercept:第一个返回非空原因的钩子拒绝,后面不再执行。
|
||||
- 每个钩子单独限时(缺省 2 秒,可在注册时改),超时或抛错记 warn 日志、跳过这个钩子;拦截类出错按放行处理。
|
||||
- 没有注册时调用方直接用原值(`hasHook` 为 false,不构造上下文)。
|
||||
|
||||
MP 的其它链式事件(`transfer.overwrite.check`、`resource.download`、`discover.source`、`recommend.source` 等)等有真实需求再加:在 `HOOK_POINTS` 登记、在 `points.ts` 写类型、在业务节点调用。
|
||||
|
||||
### 4. 功能模块约定
|
||||
### 3. 功能模块约定
|
||||
|
||||
一个外围功能 = 服务端一个 `ServerModule` + contracts 里一份 manifest + (可选)前端插槽注册:
|
||||
|
||||
- **manifest**(`FEATURES`):名称、说明、怎么用、订阅的事件、提供的能力、页面入口、插槽位置、钩子。服务端模块 `manifest: featureManifest("<id>")`;`modules.test.ts` 核对模块与清单一一对应、订阅的事件与 `outboxRoutes` 一致。
|
||||
- **manifest**(`FEATURES`):名称、说明、怎么用、订阅的事件、提供的能力、页面入口、插槽位置。服务端模块 `manifest: featureManifest("<id>")`;`modules.test.ts` 核对模块与清单一一对应、订阅的事件与 `outboxRoutes` 一致。
|
||||
- **功能清单只给智能体**:系统提示「能力地图」一节末尾列出外围功能(`renderFeatures`),能力只列本次可用的。**不做开关**:功能都直接启用。
|
||||
- **前端插槽**(`apps/web/app/lib/slots.ts`):位置见 `SLOT_POSITIONS`(侧栏入口、作品页操作区、作品页面板区、整理记录行操作、设置分区)。动作类位置注册 `{ label, icon, visible?, Dialog }`,页面渲染成按钮 / 菜单项、点了打开对话框;面板类位置注册组件,挂载点按顺序渲染。注册写在 `app/features/<功能>.tsx`,`app/features/index.ts` 汇总。**默认直接显示,不做开关**。已挂载:作品页操作区、作品页面板区(`library.$id.tsx`)、整理记录行菜单(`organize.tsx`);侧栏与设置分区先只定义位置。
|
||||
- 样板:补字幕(`modules/subtitles`)——manifest、订阅 `organization.finished`(认出值得补字幕的整理,自动补字幕还没做,只记调试日志不建任务)、写入后发 `subtitle.written`、前端在作品页操作区和整理记录行菜单挂「补字幕」。
|
||||
@@ -89,5 +58,4 @@ MP 的其它链式事件(`transfer.overwrite.check`、`resource.download`、`d
|
||||
## 不做
|
||||
|
||||
- 插件系统、热加载、插件市场、功能开关。
|
||||
- 钩子优先级数字、钩子里做外部请求。
|
||||
- 把广播事件放进内存队列(MP 做法):我们已有 outbox,事件要能跨进程、跨重启。
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-007: Bun 1.4.3 与 Linux 解析沙箱
|
||||
|
||||
状态:采纳。取代 ADR-004 的运行时、依赖安装与编译方式;更新 ADR-005 的权限隔离实现。
|
||||
|
||||
后端、前端开发工具及 Vitest 使用 Bun 1.4.3。根 package.json 管理 workspace 与统一依赖,bun.lock 锁定解析结果。.bun-version、packageManager 与 Docker 构建镜像固定同一版本。现有业务依赖版本保持不变。
|
||||
|
||||
Deno 全局调用改为 Node API 或 @umt/runtime 中的 Bun HTTP、WebSocket、子进程与流适配。HTTP 出口使用 proxy:false 强制直连,显式代理使用 respectNoProxy:false;环境代理由现有出口选择函数处理。
|
||||
|
||||
Bun 不提供 Deno 权限撤销。解析沙箱采用一次性子进程,仅传入 PATH 环境变量,预加载内置解析器与 WASM、接收 stdin 后才安装 Linux seccomp 白名单。策略通过 Node-API 模块实施,启用 no_new_privs 与 TSYNC,覆盖所有线程。默认 EPERM 拒绝未列出的系统调用:文件路径访问、socket/connect、exec、fork、ptrace、mount、io_uring 均不允许。clone 只允许 CLONE_THREAD,clone3 返回 ENOSYS 供 libc 使用受检的 clone。策略安装后直接核验 socket/openat/execve 被内核以 EPERM 拒绝。已有 stdin/stdout 与运行时内存管理调用允许使用。不同系统调用架构直接终止。
|
||||
|
||||
这不是完整虚拟机:内核与 Bun 内存安全仍是信任边界。环境变量不再通过运行时权限隐藏,改为父进程清空环境;输入只送到固定任务表,不执行上传代码。父进程继续实施超时与 RSS 上限并 SIGKILL。建立隔离失败直接拒绝,不回退到主进程执行。策略目前支持 Linux x64/arm64;其它系统不能执行解析沙箱任务,可使用 Linux Docker 开发。
|
||||
|
||||
编译脚本静态嵌入所有数据库迁移与 WASM,关闭可执行文件自动加载 .env/bunfig。生产部署同时携带 sandbox-policy.node 与内置 skills 目录。C 编译器及 Node-API 头文件仅在构建阶段需要。check:binary 在空环境、/tmp 工作目录验证模块图、原生策略及 WASM 可加载。
|
||||
|
||||
文件代理与解析代理共用 `@umt/runtime/worker` 的逐行 JSON 协议、消息大小限制、异常退出与有界回收。仍为一次性进程,不在通信层自动重试;文件进度停滞、SIGTERM 清理宽限与解析总时长/RSS/SIGKILL 策略由各自代理维护。服务端 FS_PROXY_MODE 缺省 subprocess,只有显式 auto 才允许进程内回退。开发指南记录抽象边界与后续提取常驻管理的条件。
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
| 设计规范(界面、领域) | [docs/design/](../design/) |
|
||||
| 怎么用、怎么运维 | [docs/runbook/](../runbook/)(开发环境见 [dev.md](../runbook/dev.md),部署见 [deploy.md](../runbook/deploy.md)) |
|
||||
| 与 MoviePilot 的业务对照(持续维护) | [docs/porting/moviepilot-v3-comparison.md](../porting/moviepilot-v3-comparison.md) 及同目录各移植文档 |
|
||||
| 版本变化 | 仓库根 [CHANGELOG.md](../../CHANGELOG.md),每个版本几行;发布前必须写好(`deno task release` 会检查) |
|
||||
| 版本变化 | 仓库根 [CHANGELOG.md](../../CHANGELOG.md),每个版本几行;发布前必须写好(`bun run release` 会检查) |
|
||||
| 经验教训 | [lessons.md](lessons.md) |
|
||||
| 证据、截图、构建日志 | `.local/`(不进 git) |
|
||||
|
||||
|
||||
+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 签发。发布时按配置核对健康接口版本。
|
||||
+80
-25
@@ -2,19 +2,19 @@
|
||||
|
||||
## 前置
|
||||
|
||||
- Deno 2.9.7(锁定;`curl -fsSL https://deno.land/install.sh | sh -s v2.9.7`)、Docker。不需要 Node.js / pnpm。
|
||||
- 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
|
||||
deno install # 按 deno.lock 装依赖到 node_modules(成员包软链到 node_modules/@umt/*)
|
||||
deno task dev:up # 起开发库 postgres:18.6(docker-compose.dev.yml)并迁移到最新;拉代码后也跑一次
|
||||
deno task dev # 同时起 server(4390)与 web(5180)
|
||||
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)
|
||||
```
|
||||
|
||||
单独起:`deno task dev:server`(`deno run --watch`,默认 `--role all`,API + worker 同进程)、`deno task dev:web`。
|
||||
单独起:`bun run dev:server`(`bun --watch`,默认 `--role all`,API + worker 同进程)、`bun run dev:web`。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -32,21 +32,76 @@ curl -H "Authorization: Bearer umt-dev-admin-key" "localhost:4390/api/v1/events?
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---|---|
|
||||
| `deno task ci` | typecheck(tsc 7.0.2 + deno check)→ lint(biome)→ test(vitest)→ check:tokens → build,提交前必须全绿 |
|
||||
| `deno task typecheck` | `deno types` 生成 `types/deno.d.ts` 后,逐个成员跑 tsc 7.0.2,再跑 `deno check`(Deno 内置 TS 6.0.3) |
|
||||
| `deno task lint` / `deno task fmt` | biome 检查 / 自动修复(含 import 排序;相对 import 必须带扩展名) |
|
||||
| `deno task test` | 逐个成员跑 vitest(`deno run -A npm:vitest run`);单包:`cd packages/<x> && deno task test` |
|
||||
| `deno task db:reset` | 清空 public schema 并重新迁移(生产环境拒绝执行) |
|
||||
| `deno task gen:types` | 从已迁移的库反射生成 `packages/db/src/types.ts` |
|
||||
| `deno task gen:tokens` | 改了 app.css 后重新生成 token 登记表 |
|
||||
| `deno task build` | web 构建到 `apps/web/build/client` + server 编译为单文件 `dist/umt-server` |
|
||||
| `cd apps/server && deno task start` | 以受限权限(net/env/sys/read)直接跑源码 |
|
||||
| `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 子进程 |
|
||||
|
||||
依赖:npm 包版本只在根 `deno.json` 的 `imports` 里登记(web 的前端依赖仍在 `apps/web/package.json`)。改完跑 `deno install` 更新 `deno.lock`。有安装脚本的新依赖需要 `deno approve-scripts` 显式批准。
|
||||
依赖:各成员包在 `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 参数仍由各入口转换;原站页面保留必须先解析详情的准备步骤。默认下载不先加载目录、作品详情或已有版本,不为三个入口引入通用流程引擎。
|
||||
|
||||
维护时先验证真实的成功、失败和恢复路径,尤其是授权、文件替换、取消与外部副作用;删除空壳或无使用者的机制时同步删除其专属测试与文档。不要为了目录对称增加转发层,也不要把不同阶段的状态合并成一个枚举。
|
||||
|
||||
## 进程角色
|
||||
|
||||
`umt-server --role api|worker|worker:agent|all`(源码:`deno run ... apps/server/src/main.ts --role ...`)。
|
||||
`umt-server --role api|worker|worker:agent|all`(源码:`bun apps/server/src/main.ts --role ...`)。
|
||||
|
||||
| 角色 | 引擎 worker | OutboxRelay / 模块后台循环 |
|
||||
|---|---|---|
|
||||
@@ -118,7 +173,7 @@ ci 逐行核对这张表与任务注册表(`apps/server/test/worker-lanes.test.t
|
||||
各保留期裁剪(`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),并连接任务引擎。数据库不可达时进程启动失败并退出。
|
||||
启动时 `bootstrap()` 装配 `apps/server/src/modules/` 下的业务模块(见 apps/server/README.md),并连接任务引擎。数据库不可达时进程启动失败并退出。
|
||||
|
||||
acquisition-organize 的后台循环只做编排:qB 同步(含阈值暂停/恢复)一个定时器;另一个定时器按阈值入队 `cleanupDownloadCache`、
|
||||
巡检补投 `submitDownload`/`verifyAndImport`、每 6h 入队 `reconcileLibrary`,重活都在任务里跑。
|
||||
@@ -138,21 +193,21 @@ docker compose up -d --build # 服务:app(4380)+ postgres(数据卷 umt-p
|
||||
curl localhost:4380/api/v1/health
|
||||
```
|
||||
|
||||
- 镜像:`denoland/deno:2.9.7` 构建 web 与 `deno compile` 出二进制,运行时 `debian:trixie-slim` + gosu + postgresql-client + 静态 ffprobe,root 启动修 `/data` 属主后降权到 PUID(缺省 10001)。
|
||||
- 镜像:`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` 拉起。
|
||||
- 二进制编译时固定权限:`--allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,cat`;镜像带静态 ffprobe;运行期写入落在 `DATA_DIR`(容器内 `/data`)。完整部署说明见 [deploy.md](deploy.md)。
|
||||
- 主进程使用非 root 用户和只读的 `/app` 产物;外部字节解析另起子进程,由原生 seccomp 策略限制文件、网络与进程访问。镜像带静态 ffprobe;运行期写入落在 `DATA_DIR`(容器内 `/data`)。完整部署说明见 [deploy.md](deploy.md)。
|
||||
- 构建版本号:`APP_VERSION=v0.1.0 docker compose build`(写入 web 的 `version.json`)。
|
||||
|
||||
## 测试数据库
|
||||
|
||||
需要 PG 的测试(db、tasks)默认连 `DATABASE_URL` 或 55433 开发库,不可达时跳过。tasks 包可用 `TASKS_TEST_DATABASE_URL` 指定独立库;每个测试文件使用带 pid+随机后缀的独立 schema,结束时删除,多份测试并发跑也不冲突。
|
||||
需要 PG 的测试默认连 `DATABASE_URL` 或 55433 开发库。单包测试在库不可达时会跳过集成用例,`bun run test:all` 会先检查连接并直接失败,不能把跳过当作通过。tasks 包可用 `TASKS_TEST_DATABASE_URL` 指定独立库;每个测试文件使用带 pid+随机后缀的独立 schema,结束时删除,多份测试并发跑也不冲突。
|
||||
|
||||
测试被中断(超时、kill、并发跑挂掉)时 schema 不会删,开发库会越积越多。清理脚本:
|
||||
|
||||
```sh
|
||||
deno run -A scripts/dev-db-clean.ts # dry-run:只打印清单
|
||||
deno run -A scripts/dev-db-clean.ts --verbose # 同上,逐个列出 schema 名
|
||||
deno run -A scripts/dev-db-clean.ts --yes # 真正删除
|
||||
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 时运行,否则直接拒绝。
|
||||
@@ -163,6 +218,6 @@ deno run -A scripts/dev-db-clean.ts --yes # 真正删除
|
||||
|
||||
- `配置无效`:按提示检查 `.env`,`SECRET_MASTER_KEY` 至少 16 字符,`DATABASE_URL` 必须是 postgres://。
|
||||
- health 返回 `db: "down"`:`docker compose -f docker-compose.dev.yml ps` 看容器健康状态。
|
||||
- `deno check` 报找不到 `Deno`:检查根目录是否误放了 `tsconfig.json`(tsc 基座在 `config/tsconfig.base.json`)。
|
||||
- tsc 报 `Deno` 未定义:先跑 `deno task types` 生成 `types/deno.d.ts`(已 gitignore)。
|
||||
- 解析沙箱提示策略模块不存在:先跑 `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);若发现明文,属于缺陷。
|
||||
@@ -1,7 +1,5 @@
|
||||
# 媒体整理(`/console/organize`)
|
||||
|
||||
原地入库为 v0.3.14 的待部署改动;当前生产 v0.3.13 的整理方式仍为复制/移动,独立就地刮削继续可用。
|
||||
|
||||
整理页只有「整理记录」一页(界面第二轮,2026-10-06)。整理队列学 MP 做成标题栏的「队列 N」按钮(N = 排队 + 执行中,含整理后的刮削,0 不写),点开右侧面板看队列;面板开关在 URL(`?queue=1`),旧链接 `?tab=queue` 打开面板、`?tab=records` 直接去掉。记录显示复制/移动/硬链接/原地整理的结果。文件管理的改名、删除、还原账本继续保存,通用记录接口仍可查询。
|
||||
|
||||
口径:本工具只负责「整理」——识别、元数据、NFO 与图片、按规则归到媒体库目录;媒体服务器有没有扫到由它自己决定,界面不写「入库」(见 `docs/design/glossary.md`)。
|
||||
@@ -82,12 +80,14 @@
|
||||
|
||||
## 手动下载、Local 元数据与一次性洗版(已部署 v0.3.10)
|
||||
|
||||
搜索、站点工作台和原站详情均可独立下载,不要求订阅。下载时默认正常入库,也可选择一次性洗版并勾选旧版本;只有一个旧版本时默认选中,多个由用户选择。媒体详情与下载窗口展示已有文件和规格。正常入库保留已有版本;不同版本路径由可编辑的命名模板决定,同名冲突仍需处理。
|
||||
搜索、站点工作台和原站详情均可独立下载,不要求订阅。点击下载按站点/默认下载器和保存目录直接提交;修改作品、下载器、目录或一次性洗版从高级下载选项进入,仅本次有效。未识别时先确认,可继续下载后整理识别,也可展开高级选项指定作品。下载时默认正常入库;一次性洗版需勾选旧版本,只有一个旧版本时默认选中,多个由用户选择。媒体详情与高级下载选项展示已有文件和规格。正常入库保留已有版本;不同版本路径由可编辑的命名模板决定,同名冲突仍需处理。
|
||||
|
||||
未识别资源确认后允许下载。整理时先匹配 TMDB,再由现有网络/Agent 元数据补全能力核对译名、原名和别名;能够找到正确条目时回到 TMDB,找不到且详情资料充分时使用 Local 身份。Local 共用现有分类、命名、复制/移动和 NFO/图片任务,并保留字段来源;信息不足或候选冲突保留待处理。Agent 搜寻依赖已配置的模型/搜索能力和现有每日补全预算;没有封面资料时不会虚构图片。Local 入库安排元数据文件写入,普通 TMDB 自动刮削保持原设置。
|
||||
|
||||
洗版在本次冻结计划中保存选中的旧文件。新文件核验登记成功后才清理所选旧版本,剧集只处理成功对应的季集与 Part;覆盖不完整的合并集保留。相同目标路径会先暂存旧文件,失败后恢复。旧版的私有字幕等随之清理,共享 NFO/图片、未选版本及下载缓存保留;按现有文件替换设置移入回收站或删除。
|
||||
|
||||
订阅自动洗版与一次性替换共用上述执行机制:按当前保留版本与实际计划季集选择旧文件,新文件核验后回收;合并集可以用同保留版本、同套已核验文件分次补齐,另一分辨率和其他发布组不参与覆盖。收录配置见 [rules.md](rules.md)。
|
||||
|
||||
## 收尾核验与资产登记
|
||||
|
||||
- 目标路径已有位置时:本次是「替换」(向导 / 待处理选替换,或洗版选中的旧版本),或旧位置属于别的作品,退役旧位置(没有别的位置时旧资产退役)并新建资产,大小、校验、版本信息都是新文件的;否则沿用同一资产。沿用的位置之前被对账判为缺失 / 不可用时恢复可用,资产事实换成这次入库的文件。
|
||||
@@ -186,7 +186,7 @@ MP 对照源:`MoviePilot-Frontend/src/views/reorganize/TransferHistoryView.vue`
|
||||
- 分组只对已加载的记录生效(同 MP 分组模式)。
|
||||
- 统计的「进行中」= 排队 + 执行中;点它时列表只筛执行中。
|
||||
|
||||
## 原地入库(v0.3.14 待部署)
|
||||
## 原地入库
|
||||
|
||||
文件管理器选择已登记媒体库目录中的视频或目录,进入整理弹窗,默认选择「原地入库 · 保留位置与名称」。识别作品、手动指定电影/剧集、TMDB 和季集的方式与复制/移动相同,生成预览后加入现有整理队列。
|
||||
|
||||
|
||||
+18
-2
@@ -29,11 +29,27 @@
|
||||
3. 层内 `!` > `&` > `|`,可加括号;表达式短路求值。未知条件视为不匹配(只可能出现在迁移前的旧数据里,新写入会被拒)。
|
||||
4. 版本目标自身的 `hardConditions` / `softPreferences.tiers` 保留,视为链首的匿名组;规则组有层时 pri 以规则组为准,升级比较口径自动切到 pri(对照 MP 洗版 `pri_order > current_priority`)。
|
||||
|
||||
**引用链**(`services/rules.ts resolveRuleChain`):订阅私有组 → 版本目标 `ruleGroupIds`(非空时)|| 订阅 `params.ruleGroupIds` || 订阅默认链 → 版本目标开了升级时再叠加洗版默认链;逐个按 scope 过滤,跳过停用/已删。搜索用 `kind: "search"` 取搜索默认链(给 `release.search` / 手动搜索接入)。
|
||||
**引用链**(`services/rules.ts resolveRuleChain`):订阅私有组 → 版本目标 `ruleGroupIds`(非空时)|| 订阅 `params.ruleGroupIds` || 订阅默认链 ;补齐阶段只使用该链。洗版阶段优先使用版本目标 `upgradeThreshold.ruleGroupIds`,未单独指定时继承收录链并追加洗版默认链;逐个按 scope 过滤,跳过停用/已删。搜索用 `kind: "search"` 取搜索默认链(给 `release.search` / 手动搜索接入)。
|
||||
|
||||
候选评估记录(artifact `candidate_evaluation`)里:`pri` 为最终 pri;`reasons` 带 `rule_group:<id>@<revision>`;`trace` 逐组逐层写明命中/未命中及每个原子的依据,如
|
||||
`规则组「片源」@r1 第1层 4K:未命中(4K✗ 分辨率 1080p)`、`规则组「片源」@r1 硬排除 REMUX 命中(REMUX✓ 来源 REMUX)`。
|
||||
|
||||
## 分类、保留版本与洗版
|
||||
|
||||
在「规则 → 收录策略」中沿用现有 movie / tv 二级分类选择适用范围。新订阅按「明确选择 → 分类默认 → 作品类型默认 → 全局默认」选择已发布策略,同层按发布时间选择;未匹配时创建默认版本。分类设置页展示当前关联。策略模板创建订阅自己的保留版本,已建版本的规则独立维护,修改默认模板不会覆盖已有版本。
|
||||
|
||||
订阅表单只选择新建时的收录策略,质量、组合规则和洗版在订阅详情的「收录版本规则」逐版本调整。顺序与已有订阅的自动补全也统一在详情维护;包含、排除仍作为订阅共同过滤。旧 API 的统一质量预设继续兼容,普通表单保存保留旧值。自动补全选择「跟随主版本规则」时,按主版本实际收录链取最后一个设置了补全开关的组;没有设置则关闭,显式开/关优先。
|
||||
|
||||
- **单版本**:一个保留版本的允许收录范围选择 1080p、2160p,组合优先级例如 `4K & H265 > 4K & H264 > 1080P & H265 > 1080P & H264`。范围内先收低档,再洗到高档;720p 即使其他条件更好也不进入范围。
|
||||
- **双版本**:建立两个保留版本,分别只允许 1080p 和 2160p,各自引用组合规则和洗版设置,例如各自用 `H265 > H264` 让本分辨率的最高档为目标档。第一个为主版本,需要自动收齐第二个时开启订阅的「自动补全其他版本」。统一质量预设不会改写策略模板的分辨率边界。
|
||||
- **组合优先级**:复用规则组的 AND / OR / NOT 与有序档位,发布组可使用结构化阶梯或现有自定义条件;编码偏好应写入档位,软偏好用于同档候选排序。多组串联仍取最后一个带档位的组,界面显示引用顺序。
|
||||
- **阶段**:追更、补齐允许不同集采用不同版本;开启「整季同组同套」后,洗版向当季选定规格收敛,多个单集可以组成一套。规格包含分辨率、来源、编码、发布组、HDR 和剪辑版,不包含集号或种子 ID。已在途的洗版规格保持冻结,结束后才选择更优的一套;同档对齐不降低来源或分辨率等级。
|
||||
- **完成与旧文件**:范围内覆盖齐且选定同套资源达到目标才算洗版完成。新文件核验后,仅回收同保留版本被完整覆盖的旧文件;旧合并集要等同套新单集全部入库,另一分辨率和异组文件不能凑覆盖。处置沿用回收站与替换失败恢复机制。
|
||||
|
||||
组合条件所依赖的分辨率、编码等未知时,不把未知当作否定条件成立,也不悄悄降到低档。资源属性事项交给现有调查通路:Agent 用 `release.inspect` 查看详情与 MediaInfo,有针对 `release.apply_facts` 的持续授权时只补仍缺失且有出处的分辨率、编码、来源或发布组,服务保存事实与出处并触发原规则重评。站点大小、做种、促销等通过详情刷新补齐。没有授权或证据不足时提供人机协同建议;Agent 不改变允许范围、保留目标或组合排序。
|
||||
|
||||
手动下载的选择只影响本次计划,沿用独立的下载与一次性替换入口。
|
||||
|
||||
## 内置关键字
|
||||
|
||||
`GET /rules/keywords` 返回全表(含 MP 上游正则/条件原文与我们的判定依据 `basis`)。
|
||||
@@ -60,7 +76,7 @@
|
||||
| `POST /rules/parse {ruleString}` | → `{layers, ruleString(规范化,有错为 null), errors[{code, message, position, layer, atom?, suggestions?}]}`;code:`empty` `empty_layer` `unknown_atom` `unsupported_atom` `unbalanced_paren` `unexpected_token` `invalid_char` |
|
||||
| `POST /rules/serialize {layers}` | → `{ruleString}`(原子大写,只加必要括号) |
|
||||
| `GET / PUT /rules/defaults` | `{acquisition[], upgrade[], search[]}`,按数组顺序串联;类型不符 400 |
|
||||
| `GET/POST /rules`、`GET/PATCH/DELETE /rules/{id}` | 列表筛选 `kind source enabled includeDeleted q`;写入可给 `ruleString`(优先)或 `layers`,`hardExcludes` 元素可以是表达式或单层规则串;PATCH 可带 `expectedRevision`(不匹配 409);同名 409;条件无效 422;DELETE 被版本目标/订阅引用 409 |
|
||||
| `GET/POST /rules`、`GET/PATCH/DELETE /rules/{id}` | 列表筛选 `kind source enabled includeDeleted q`;写入可给 `ruleString`(优先)或 `layers`,`hardExcludes` 元素可以是表达式或单层规则串;PATCH 可带 `expectedRevision`(不匹配 409);同名 409;条件无效 422;DELETE 被版本目标(含洗版)、订阅、有效策略模板引用 409 |
|
||||
| `GET /rules/{id}/revisions` | 审计回放(含迁移前的 `preference_tag.*`) |
|
||||
| `POST /rules/{id}/preview` | `{limit?, sourceReleaseIds?, titles?[{title, subtitle?}], draft?}`;缺省最近 50 条已识别资源;`draft` 试算未保存的层/硬排除 → `{sampleSize, passed, rejected, priHistogram[{pri,count}], items[{title, passed, pri, level, excludedBy, reasons, trace}]}` |
|
||||
| `GET/POST /rules/atoms`、`GET/PATCH/DELETE /rules/atoms/{id}` | 自定义条件;与内置同名 409;被规则组引用不能删 409 |
|
||||
|
||||
@@ -64,7 +64,7 @@ TMDB 条目保存时先调 `POST /files/entity-from-candidate` 建档(同一 TMD
|
||||
|
||||
## 多版本自动补全
|
||||
|
||||
- 开关 `params.autoFill: inherit | on | off`(缺省 inherit):inherit 取订阅生效规则链(私有组 → `params.ruleGroupIds` 或订阅默认链)上**最后一个设了 `auto_fill` 的组**,都没设为开。总览 `autoFill.source` 标来源(subscription / rule_group / default)。
|
||||
- 开关 `params.autoFill: inherit | on | off`(缺省 inherit):inherit 取主版本收录链(私有组 → 主版本 `ruleGroupIds` 或兼容订阅组或默认链)上**最后一个设了 `auto_fill` 的组**,都没设为关;显式 on/off 优先。总览 `autoFill.source` 标来源(subscription / rule_group / default)。
|
||||
- 预算 `params.fillBudget {unitsPerDay?, bytesPerDay?}`:按订阅、按自然日,只计自动补全计划(`acquisition_plan.evaluation.autoFill = true`,草案/取消/失败/过期不计),同时仍受全局每日预算与缓存卷阈值约束。
|
||||
- 评估(`evaluateForSlot` 建计划前 `autoFillCheck`):
|
||||
- 主版本、没有订阅的版本目标、只有一个版本目标:照旧;
|
||||
@@ -75,7 +75,11 @@ TMDB 条目保存时先调 `POST /files/entity-from-candidate` 建档(同一 TMD
|
||||
- 手动补全 `POST /subscriptions/{id}/slots/{slotId}/fill {unitKeys?}`:入队 `evaluateAcquisition{acquisitionSlotId, forceFill}`,不受开关与补全预算限制(全局预算/缓存卷照常),`unitKeys` 限定只补这些单元;订阅须在追踪中(active/pending)。
|
||||
- 定时:不新增任务。`slots.reevaluate`(到期 Slot 重评)覆盖补全与预算延后;`subscriptions.search` 按 `next_search_at` 到期搜索。
|
||||
|
||||
## 编辑表单字段(POST / PATCH `params`)
|
||||
## 订阅字段与 API 兼容(POST / PATCH `params`)
|
||||
|
||||
界面分工见设计 §3:新建时选择策略,质量、组合规则和洗版统一逐版本编辑。下面的 MP 质量与统一洗版字段仅留作 API 兼容,普通表单保存保留旧值,不重新生成。
|
||||
|
||||
存量订阅在「共同过滤与搜索词」显示旧质量预设,可明确清除:保存时解除旧预设生成且未手改的条件;已单独调整的版本规则、策略和多版本的分辨率范围保留。
|
||||
|
||||
写入时规范化,库里只存规范键(未知键原样保留)。PATCH 的 `params` 仍是整体替换。
|
||||
|
||||
@@ -107,7 +111,7 @@ TMDB 条目保存时先调 `POST /files/entity-from-candidate` 建档(同一 TMD
|
||||
- 私有组不在规则页 `GET /rules` 列出;它排在引用链最前,不影响订阅默认链 / 显式规则组的选择。
|
||||
- 迁移为存量订阅(未取消、params 有 include / exclude)各建一个私有组。评估不再读 params 的 include / exclude(`withSubscriptionFilters` 已删)。
|
||||
|
||||
## 质量预设(MP resolution / quality / effect)
|
||||
## 质量预设兼容(MP resolution / quality / effect)
|
||||
|
||||
保存时写到**该订阅自己的**版本目标(`subscription_id = 订阅`;作品共享的无主版本目标不动):
|
||||
|
||||
@@ -123,7 +127,7 @@ TMDB 条目保存时先调 `POST /files/entity-from-candidate` 建档(同一 TMD
|
||||
| atmos | 私有组包含 `Atmos` | |
|
||||
|
||||
改预设(`applySlotPreset`):新预设给出的字段覆盖;旧预设给过、新预设不再给的字段,只有版本目标上的值仍等于旧预设生成的值时才删(手改过的保留)。有变化 revision+1。
|
||||
新建订阅时作品还没有版本目标:收录策略有 `defaultSlots` 按模板建;没有模板但给了预设 → 建一个"默认版本"承载预设;都没有则不建(同以前)。
|
||||
新建订阅时收录策略有 `defaultSlots` 按模板创建,否则创建一个默认版本;作品尚未关联时,在关联后创建。旧 API 预设作用于自己的版本,不改模板或多版本已有的分辨率边界。
|
||||
|
||||
## 下载提交与整理去向
|
||||
|
||||
|
||||
Reference in new issue
Block a user