Files
UHD_Media_Tool/CHANGELOG.md
T
2026-09-28 13:53:33 +08:00

23 KiB
Raw Blame History

Changelog

版本号遵循 SemVer。所有成员包(apps/*、packages/*)与根 deno.json 使用同一个版本号;/api/v1/health 与备份清单里的版本取自 apps/server/deno.json。

[0.2.0] - 2026-09-28

Agent 上游全覆盖与订阅登录、控制台信息架构重排、定时任务看板、全站文案第二轮审校。基线见 docs/plan/08-v0.2-closeout.md(W8-A)、docs/design/{console-ia,pi-providers,agent-ui-reference}.md、docs/runbook/{schedules,agent}.md。

Agent

  • 全局入口:侧栏顶部常驻 Agent 入口(展开与折叠态均带活跃会话数角标);右下角悬浮按钮(带活跃计数、最新状态摘要),在 Agent 会话页与迷你坞展开时隐藏;Ctrl/Cmd+J 切换抽屉、Esc 关闭,打开即聚焦输入框;抽屉首屏给出随页面变化的快捷目标。
  • Agent 上游:新增 pi_upstream 表与 CRUD,一个上游 = 一个 pi provider(41 个内置之一,或 openai-compatible 自定义兼容端点,或 fake)+ 一份凭证;同一 provider 可建多个上游,各自独立凭证存储;authKind 分 api_key / oauth / none;默认上游可指定,runAgent 只从上游选模型。
  • 订阅登录:8 种(Anthropic Claude Pro/Max、OpenAI Codex、GitHub Copilot、xAI、Kimi Code、Meta Muse、OpenRouter、Radius),browser 与 device 两种交互,登录会话 15 分钟;条款提示只陈述事实(借用官方客户端身份、可能违反条款、有限流或封号风险)。
  • 设置页 Agent 分区:上游列表、新建向导(选 provider → 凭据与模型 → 登录 / 测试 / 设默认)、编辑抽屉(登录 / 重新登录 / 退出、测试、设默认、删除)。
  • 子会话:agent.spawn 派生子会话(独立 agent_run,同一 runAgent 任务),agent.await 取子会话状态与脱敏汇报;能力白名单继承父会话并取交集;并发上限 maxConcurrent、派生层数上限 maxDepth、单会话最多 10 个子会话;预算从父会话划拨;中止父会话递归取消未结束子会话。
  • agent.await 落定时挂起会话而不占 worker 槽:drive 返回 suspended,由 waitForFact 每 2 秒查一次子会话落定 / 超时 / 父会话取消,再以 resumedAwaits 重开会话续跑;单会话最多挂起 100 次。
  • 没有候选的待识别资源(无法解析,或候选为 0,或查询失败且自动复查已停)开 identity_unresolved 待处理事项:可手动指定 TMDB ID、选本地作品或忽略。
  • 旧 settings.pi.provider / baseUrl / model / thinkingLevel / apiKeySecretRef 弃用:schema 保留兼容旧数据,PATCH /settings 丢弃,设置分区不再展示;runAgent 没有上游直接失败并提示「未配置 Agent 上游」。
  • Agent 汇报里的下载链接改指向 /console/downloads/:id,不再经 /console/tasks/:id 404 探测再跳转。

控制台信息架构

  • 侧栏改为 7 组 19 项(概览、资源、订阅、下载、整理、Agent、系统),按业务流程排列;新增"定时任务"入口。
  • 站点工作台合并快照与账号:详情页 4 个页签(整理视图 / 原站视图 / 快照与 Feed / 账号)。
  • 原"下载与任务"拆分为下载队列、下载计划、业务任务三页;订阅页"收录策略"页签独立为收录规则页。
  • 身份判定收口到待处理中心:身份页"待识别"改为只读,已进待处理的行标注跳转,判定动作只在待处理执行。
  • 设置页改为 9 个左侧分区(自动化、站点与账号、下载器、媒体库与目录、媒体服务器、元数据、Agent、API Key、账户与安全),用 URL hash 定位到分区与节;旧 ?tab= 全部重定向到新分区 hash,settingsPath() 直接生成 #分区[/节]。

定时任务看板

  • scheduled_job / scheduled_job_run 表驱动全部周期任务,worker(io 组)内唯一调度器按表调度(pg_try_advisory_lock 选出执行者);控制台"定时任务"页(/console/schedules)查看与调整。
  • 已登记 16 个任务,分 5 组(采集、下载、整理、维护、Agent);Agent 组含 3 个默认关闭的巡检(待处理巡检、下载诊断、订阅与站点周报)。
  • 模块自建计时器全部并入注册表:pi.token_refresh(上游登录刷新,维护组,10 分钟)与 auth.cleanup(登录会话清理,维护组,1 小时)改为 registerScheduledJob,模块里不再有 setInterval。
  • 支持立即执行、跳过本次、开关与周期自定义(customized 标记,改过之后再登记不覆盖,PATCH {reset:true} 恢复默认);自定义巡检可增删,内置任务只能停用。
  • 调度器中断恢复:新执行者拿到锁时,把上一个执行者留下的 running 状态改为 failed。

文案与术语

  • 全站第二轮文案审校:控制台、设置、Agent、服务端消息统一术语表用词(待处理 / 待识别 / 整理记录 / 运行记录 / 凭证 / 子会话 / Agent 上游),精简说明文字、空态、确认按钮与 toast。
  • 内置 Agent 巡检的目标模板改短、改具体。
  • 实例(下载器 / 媒体服务器)CRUD 报错改用可读名词,不再显示表名。
  • 审计日志补 pi_upstream / 定时任务 / 持续授权相关动作的显示名。
  • 术语表新增:上游、订阅登录、凭证、定时任务、巡检、运行记录、子会话、停止、持续授权。

修复

  • 设置页深链改用分区 hash 定位,不再依赖 ?tab= 查询参数(旧链接仍可打开,经重定向落到新分区)。
  • Agent 汇报的下载任务链接直接指向下载详情页,不再先探测业务任务详情再判断是否为下载任务。
  • 生产容器静态 ffprobe 升级到 9.0.2;.gitignore 忽略 compose override 文件。

已知限制

完整清单见 docs/plan/08-v0.2-closeout.md"仍遗留"节。

  • 41 个 provider 里只有 fake 与 OpenAI 兼容本地端点有自动化覆盖;8 种订阅登录只测了 fake 的 OAuth 状态机;Codex _randomBytes 在 Deno 下的行为待 spike(docs/design/pi-providers.md)。
  • agent.spawn {await:true} 仍在进程内轮询等待,占 worker 槽到超时;要挂起需要和 agent.await 相同的关闭会话 + 重放机制,但 spawn 是 replay=never,重放会得到"结果未知",需另行设计。
  • 子会话挂起期间父会话状态保持 running(避免被祖先会话的 agent.await 当成已落定);运行时长预算按每段 drive 计,不含挂起等待。
  • 查询失败但仍在自动复查期内的资源不开待识别事项,避免 TMDB 抖动刷屏,复查耗尽后才开。
  • 身份页"待识别"详情抽屉的提示句仍写"选择候选或忽略",未识别类(无候选)读起来不准确,留给下一轮文案。
  • 旧 settings.pi 字段只是弃用未删除,真正删除需要一次迁移清理里面的旧键。
  • 原站视图仍是占位(IA-1 遗留)。
  • 控制台账户弹层的二次验证(TOTP)开启时只显示密钥与 otpauth 链接,不渲染二维码。

[0.1.0] - 2026-09-28

第一个可用版本:REQ v0.7 的 M0 + M1 纵切。全链路在没有真实 PT 账号的情况下用 mock(RSS / 站点 / TMDB / qBittorrent / Emby)验证。范围与验收依据见 docs/plan/00-first-version-plan.md §3.1 和 docs/plan/04-v0.1-acceptance.md。

W0 底座

  • Deno workspace 单仓(apps/server、apps/web、packages/{adapters,agent,contracts,db,domain,mapping,tasks,testkit}),迁入 uhd_web 设计系统。
  • OpenWorkflow 与 Pi SDK 兼容性 spike,结论写入 ADR-002 / ADR-003;Deno 2.9.7 + TypeScript 7 spike 写入 ADR-004。
  • MoviePilot / MovieClaw 迁移对照账本(识别、刮削、转移入库、站点框架)。

W1 领域与适配

  • @umt/domain:发布标题解析、身份判定、Slot 状态机与完成条件、收录模式判定、路径模板与路径绑定、整理计划、下载状态机,全部是纯函数。
  • @umt/adapters:qBittorrent(多实例、管理标签、带数据删除前的安全检查)、文件系统(Copy/Move、跨卷 Copy→核验→删源、EXDEV 不静默降级、全量 sha256)、TMDB、Emby、RSS、PT 站点(NexusPHP 观众、馒头 API,移植 MoviePilot 开放站点框架)。@umt/testkit 提供对应 mock。
  • @umt/db:36 张表的全量迁移;@umt/contracts:共享 zod 契约与 API 注册表;@umt/mapping:domain 与 contracts 之间的映射集中在一处。
  • @umt/tasks:基于 OpenWorkflow 的持久任务引擎、任务注册表、outbox 中继、操作账本 fencing(旧 attempt 的提交会被拒绝)。
  • @umt/agent:能力注册中心、Pi AgentRuntimePort、fake 模型、输出脱敏。

W2 服务编排

  • 认证与设置:管理员登录门禁(argon2id、会话 cookie、CSRF 双提交、登录限速、可选 TOTP)、API Key scope(read / organize.read / organize.execute / admin)、密钥加密存储、实例与路径映射、审计、PG 通知驱动的 SSE(带序号,可补读)、自动化总开关与只读对账模式、工作流引擎面板 API。
  • 来源与身份:站点与 Feed 管理、多 RSS 抓取与多层去重、站点快照与实时补查、TMDB 识别与本地实体优先、待识别池、人工纠错映射。
  • 规则、下载与入库:订阅、Slot、策略版本(发布即生成持续授权)、确定性 evaluateAcquisition、每日预算与并发、qB 同步与标签接管、缓存卷硬阈值与恢复水位、verifyAndImport(Copy/Move)、路径绑定固定、整理记录与公开增量 API、媒体库与首页聚合。
  • Agent:14 个能力接到服务,服务端授权(持续授权 / 一次性审批),会话 API + SSE 增量,后台事件触发有界调查任务。

W3 控制台

  • 20 个路由:首页、媒体库、订阅与规则、偏好标签、待处理中心、身份与元数据、PT 站点工作台、来源 / Feed / 快照、下载与任务、整理记录、Agent 会话、系统设置、工作流引擎面板、审计。

W4 集成验收与修复

  • 单容器打包(API + worker + 静态前端,compose 只有 app 和 pg),DATA_DIR,备份与恢复脚本,runbook。
  • 控制台接入真实端点;Playwright 全路由冒烟(deno task test:web:smoke)。
  • BrowserProvider 接口与 camofox-browser 适配骨架(只落接口)。
  • 主循环解耦:后台循环只做编排,清理 / 对账 / 入库一律入队到任务引擎;io / agent worker 分组;外部 IO 能力调用限流;事件循环延迟探针。
  • 安全修复:审批只能由管理员会话决定,共享一份脱敏表,TMDB 错误信息不带 api_key,生产构建不能打开 Agent mock。
  • 授权内异常调查与恢复:有持续授权时,调查结论可在授权范围内自动重试并继续。
  • 手动整理守卫(POST /organization-records):sourcePaths 必须在已登记的媒体库根或下载器路径映射之下(realpath 后按目录前缀匹配,拒绝 .. 与符号链接逃逸,dryRun 同样受限);Move 需要管理员会话,或 API Key 同时有覆盖目标作品的持续授权 / 与本次请求一致的一次性审批;来源是纳管下载任务且做种义务未满足时返回 409 并建待处理,不改成 Copy。
  • 剧集完成判定:总集数未知(集单元都是临时单元)时 Slot 不判满足,订阅继续追踪;覆盖按实际文件登记,整季包不因包名覆盖整季;已在库的相同文件算作核验到的内容。
  • 工作流引擎面板支持重跑:POST /engine/runs/{id}/retry(仅 admin,新幂等键 retry:<id>:<序号>,写审计),run 详情页有"重跑"按钮。
  • 新增 e2e:E7 常规剧集纵切、E8 管理标签移除与 qB 任务消失(AT24)。

文件管理(/console/files,00 §6 追加范围)

  • 后端 /files 模块共 18 个端点:根 / 列表 / stat(行内带资产、Slot、下载任务状态)、mkdir、rename、move、copy、organize、delete、回收站(列表 / 还原 / 还原到… / 清理)、identify、probe、scrape、records、download。
  • 路径守卫:realpath 必须落在已登记根内,拒绝 ..、URL 编码、符号链接逃逸与对根本身的写操作;执行前再做一次 realpath(TOCTOU);根离线返回 503,不返回空列表;fs 竞态错误映射为 503/404/403/400。
  • 两阶段写操作:move / copy / organize / delete 先 dryRun 产出 planId 与源事实指纹,执行时只认 planId,源变化返回 plan_stale;全部写操作带幂等键并写审计。
  • move / copy 经 planImport 的 transfer 变体生成整理记录,由 verifyAndImport 执行:逐文件账本可续跑、经 fsproxy、跨卷目录 Move 为逐文件 Copy→核验→删源;资产位置逐文件跟随,整目录搬走时修订路径绑定。请求最多等待 FILES_TRANSFER_WAIT_SEC,超出返回 executed.pending。
  • organize 支持逐项覆盖季 / 集 / 内容单元 / 目标目录 / 操作 / Slot;delete / 回收站 / 还原写整理记录(迁移 0026)。
  • 回收站:<root>/.umt-trash/<day>/<uuid>/,trash_item 表(迁移 0024),删除前核对引用,做种中的文件不能改名、删除或 Move;过期清理由 purgeTrash 任务执行。永久删除默认关闭(FILES_ALLOW_PERMANENT_DELETE)且只接受管理员会话;API Key 删除每次都要一次性审批。
  • download:仅管理员会话,nfo / 字幕 / 图片白名单,FILES_DOWNLOAD_MAX_BYTES 上限,nosniff + CSP sandbox,写审计。
  • 前端:目录树、虚拟滚动网格、面包屑、排序与 glob 搜索、多选 / 右键菜单 / 拖拽后 dryRun 预览、键盘操作(F2 / Del / Shift 区间 / Ctrl+A)、离线与空态、移动端布局;改名 / 移动 / 复制 / 删除对话框,4 步整理向导(未决项挡住下一步),回收站页;SSE files.changed 触发刷新。?mock=1 内存后端只在 DEV 构建生效。
  • 运行手册 docs/runbook/files.md。

订阅增强(对照 MoviePilot,docs/plan/05-subscription-gap.md)

  • 总集数账本:季单元记录 episode_total 及来源 / 锁定 / 时间(迁移 0025);resolveEpisodeTotal 只增不减,TMDB 回退时以已覆盖的最大集号为下限,手动值可锁定。
  • 完成判定按账本:总集数未知时不结束并记疑似缺集;pending(元数据未就绪)的订阅可搜可评估但不会完成;once 与持续订阅的当季完成都写 subscription_history。
  • Slot × 单元预留:缺口 = 发布单元 ∩ 集范围 − 已覆盖 − 在途;剧集 Slot 可同时有多个互不重叠的在途计划,重叠或忙时延后(slot_deferral),计划结束或周期重评时再评估。
  • 硬条件与优先级:排除来源、HDR / 编码集合、每集体积、include/exclude 正则(白名单校验)、站点范围、最少做种、仅免费;MoviePilot 规则组分级映射为 0–100 优先级并带逐级 trace;分集洗版(slot_unit 账本)。
  • 季包选文件:提交后读取 qB 文件列表,把已覆盖或在途的集设为不下载;无法判定时整包下载并记录原因。
  • 主动搜索:建订阅时、每 searchIntervalHours(默认 24h)和 POST /subscriptions/{id}/search 入队 searchSubscription;元数据每 6h 刷新并写季总集数。
  • 前端订阅抽屉:立即搜索、追更进度(每季总集数与来源、每集状态、在途 / 延后计数)、设置并锁定季总集数。
  • 新增 e2e E9 持续追更剧集。

转移语义与文件系统隔离(docs/porting/moviepilot-rust-transfer.md)

  • FsProxy + fsworker:文件操作在可杀子进程里执行;复制 / 哈希按进度停滞看门狗(FS_HANG_TIMEOUT_MS),快操作按总超时;超时后 SIGTERM → SIGKILL → FsError('TIMEOUT'),stat / 根探测超时不会被当作"不存在"。
  • 生产二进制内置 --role fsworker(/app/umt-server 自身 spawn,容器演练见 docs/runbook/deploy.md)。
  • copyWithVerify 支持 conflict / skip / replace:replace 先硬链接留底再原子 rename,旧文件进 .umt-trash 并登记 trash_item;写前清理超过 24h 的 .uhd-partial。
  • 核验读失败抛错,不再当作"不匹配";对账错误不再误报 EEXIST;TIMEOUT / IO 有限重试(FS_TRANSFER_MAX_ATTEMPTS)后建待处理,不删记录。
  • 复制流 1 MiB 块;强制全量 sha256 走 sha256sum 子进程;手动整理遍历有上限(ORGANIZE_WALK_MAX_FILES)。
  • 字幕 .default 标记、同集其他版本作为洗版处置候选。

Agent 会话页

  • 移植 agentui(MIT,LICENSE-agentui.md)组件并换成本项目语义 token:流式光标、可展开的思考块、带类别 / scope / 风险 / 幂等徽章的工具卡、服务端拒绝的醒目态、压缩标记、预算条、审批卡(仅管理员会话可操作)、"模型声明 vs 系统核实"双栏最终汇报、跟随滚动与回到底部。
  • 输入框(Enter 发送、中止需确认、追加、执行 / 只要方案切换、从方案一键执行)、会话列表与状态筛选、断线重连横幅;侧栏抽屉按路由带入上下文,可最小化为迷你坞。
  • 设置页新增 autoInvestigateMode(调查 / 授权内恢复)与最大恢复次数、agent.sessionDir。

识别与站点解析补迁(MoviePilot 差分)

  • 标题识别:动漫前缀标签、多种集号写法、首括号作标题或组名、原盘判定、{[tmdbid=…]} 显式块与 bangumi/anilist id、流媒体平台表等;繁简表补字。标题识别剔除登记项后 100% 对齐,missing / our_bug 清零。
  • 站点:18 个站点定义的行选择器收紧、字幕搜索分支(只到解析层)、体积解析的银行家舍入。差分快照与样本来源登记在迁移账本。

稳定性与部署

  • 容器内三个受监管进程(api + worker(io)+ worker:agent),静态 ffprobe。
  • qB 同步在事务内 for update 重读下载任务后再合并,修复与入库并发时把 imported 写回 importing 的丢失更新。
  • 授权内恢复的核实结论与 run 终态同一次写入,观察者不会读到中间态。
  • 完成判定只保留 applySlotEvent(verify_passed) 一条路径,整理流程不再自行把订阅置为完成。
  • server 测试分为并行的 unit 组和文件级串行的 pg 组,packages/tasks 同样串行;deno task ci 连续 3 次全绿(约 340–370 秒)。

发布前安全修复(W6-B,docs/plan/07-v0.1-final-review.md §1.2)

  • 多源 Move 授权:/files/move 与 /files/organize(action=move)按每个源所属作品(声明作品 ∪ 源下已登记资产的作品)逐一核对持续授权,任一不覆盖整体 403 并列出未授权项;无法归属作品的源只接受一次性审批;organize 先整体预检,不会只执行一部分组。
  • GET /files/download、POST /files/trash/purge 在注册表标 auth:"session",由中间件统一只放行管理员会话(DEV key 与 admin scope 的 API Key 一律 403);永久删除同口径;api-registry.test 新增反向断言,未标 session 的端点不得对 admin scope 返回 403。
  • 脱敏键表只剩一份:rss 与 browser 的参数判定改用 @umt/contracts 的 SENSITIVE_QUERY_PARAMS / isSensitiveQueryParam,补齐 pass_key、c_secure_pass、refresh_token、password、uid_key、auth 等写法,加三处一致性测试。
  • 回收站"还原到…"只允许可写的 managed 库根;原位还原逐级重建父目录且不跟随符号链接;跨卷还原受 FILES_TRASH_CROSS_MAX_BYTES 上限,回收站进出的跨卷复制经 fsproxy。
  • /files/identify 的远端查询经进程共享的 TMDB 限流器,单次请求最多 20 个远端查询;POST /files/probe 改为需要 organize.execute(会写 media_info)。
  • fsworker 阶段完成的进度一律立即上报,不再被节流吞掉最后一跳(全量 ci 下偶发的 fsproxy 用例失败)。
  • fsproxy 挂死验收开关 UMT_FS_TEST_HANG_AFTER_BYTES 必须同时设置 UMT_FS_TEST_HOOKS=1 才生效,preflight 发现已设置会告警。
  • 控制台账户弹层新增"二次验证"入口(生成密钥 → 验证码确认开启;凭密码 + 验证码关闭),不显示二维码,手动输入密钥或 otpauth 链接。

已知限制

完整清单与出处见 docs/plan/07-v0.1-final-review.md §8;范围外内容以 docs/plan/00-first-version-plan.md §3.2 与 §6 为准。

范围外(00 §3.2 / 02 后置)

  1. 国产综艺分段、require_distinct_asset 组合校验、浏览器 / camofox 与原站视图、HHClub / CHD、旧库分范围接管、Agent 主观选片与规则学习、多用户。
  2. Passkey、主密钥轮换、子代理、Transmission、TTG 自定义适配、馒头 adult 模式。

订阅(05 §4 遗留)

  1. 完成判定前不会强制刷新元数据;titleQuery 订阅不会自动建档回填;季范围语义(current_season 在没有季单元时固定)未做;偏好标签不参与评估。
  2. 同作品同季重复订阅没有查重;prefer_pack、回退等待期、Agent 选片理由与独立预算、队列按优先级出队、实体级识别词都未做;没有"清账重置"(只能把状态改回 active 重新订阅)。
  3. (已解决)完成判定的双路径已在 W6-A 合并为一条,编号保留以便与 07 §8 对照。

文件管理

  1. NFO / 图片投影(scrape 的 writeNfo/images 返回 not_implemented)、硬链接、蓝光原盘整目录操作未做;路径守卫没有下沉到 adapters。
  2. 从回收站还原不会恢复资产状态,需要重新整理登记;永久删除开关是环境变量,不是设置项。
  3. 跨卷进回收站与跨卷还原(都受 FILES_TRASH_CROSS_MAX_BYTES 上限、复制经 fsproxy)、永久删除、identify(已经走 TMDB 限流器,单次最多 20 个远端查询)、probe(串行,每次 30 秒超时)仍在 HTTP 请求内执行;浏览、根探测、readdir 与永久删除的 rm 不经 fsproxy。计划 v0.1.1 改为入队(07 S-7)。多项永久删除没有服务端的逐项确认(单项要求 confirmName)。
  4. 前端没有整理向导 e2e 和移动端快照,页面行为靠 model 单测和冒烟覆盖。

转移与文件系统

  1. planImport 规划期的 stat / 哈希、资产对账和文件管理不经 fsproxy;没有根级熔断;失效软链一律按冲突处理;hardlink、organize 源侧空目录上溯清理、蓝光原盘未做。
  2. 二进制必须位于 /app/umt-server,否则 fsproxy 回退为进程内看门狗(能按时报 TIMEOUT,但卡住的线程无法回收)。基准 B1(50 GB)、B2(并发)、B3(真实 FUSE 冻结)没有跑。

Agent 与安全

  1. (已解决,W6-B)控制台 TOTP 入口已补;开启时只显示密钥与 otpauth 链接,不渲染二维码。
  2. (已解决,W6-B)rss 和 browser 已改用 contracts 的共享键表;全仓凭证泄漏的故障注入测试留到 M2。
  3. GET /inventory/media-servers 在请求内串行查 Emby;能力调用超时后,底层调用仍会跑完。
  4. Agent 页的幂等键来自审计(最多 3 页),审批卡片的 planId 靠解析工具摘要得到,较早的运行可能取不到。
  5. 自动恢复只核实到下载恢复,入库由后续流程负责;恢复白名单只有 task.retry、download.recheck、identity.resolve。

识别与站点

  1. 字幕搜索只到解析层;RSS 不带时区的时间与 HTML 命名实体未处理;过滤器的 tmdb 放行、SPECSUB 等属于 Slot 能力扩展。

质量与运维

  1. 恢复流程只有人工演练;流程版本升级没有产品侧测试;下载状态机 ALLOWED 未收紧;scanIncremental 出错时仍返回 complete;AT18 恶意标题 e2e、AT42 未纳管占用没有测试;vertical.test 的用例依赖执行顺序;web 单测少(账户弹层的 TOTP 对话框只有类型检查,没有组件测试);E6 自动恢复只核实到 recheck 后回到下载中,engine-retry 用的是 mock enqueue。
  2. deno task ci 约 6 分钟(W6-B 两次全绿 357 s / 341 s),server 测试(pg 组文件级串行)是主要耗时。