后台任务面板

更新于 · 在 sijie.xyz 查看原条目 ↗

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

队列、周期任务、事件流、投递日志都在 admin 里看得见、点得动。参考 River UI 的信息结构,但不引入它(它是独立服务 + 自己的认证);用 dispatcher ops 实现,后台和 MCP 同时可用。owner 也可以直接问自己的 AI“有没有卡住的任务?”。

面板结构

flowchart TB
  T["admin · Tasks (settings group, after traffic)"]
  T --> O["Overview<br/>job counts per state · oldest pending age<br/>events backlog · poisoned · table sizes · alerts"]
  T --> P["Periodic jobs<br/>name · interval · last / next run · result<br/>run now"]
  T --> J["Job list<br/>filter by kind (every declared kind, even before it ran) / state<br/>pending · running · retryable · completed · discarded · cancelled"]
  J --> JD["Job detail (?job=id)<br/>args · error of every attempt · next retry time<br/>retry now · cancel"]
  T --> E["Event stream<br/>recent events · type · subject · subscribers fanned out to"]
  E --> ED["Event detail<br/>payload · relay error · job state per subscriber<br/>requeue (poisoned only)"]
  W["admin · Webhooks (integrations group)<br/>endpoints · delivery log · send test · re-deliver"]
  W --> JD
  ED --> JD
  • ?job=<id> 直接打开某个任务,webhook 投递日志就链到这里。
  • Webhooks 是集成组下单独的后台页(webhooks),不是这个面板的一个标签。
  • 总览上的告警:events_backlog(未扇出超过 1000 条,或最老的超过 5 分钟)、events_poisoned(有隔离的事件)、jobs_discarded(有 discarded 任务)。见 storage-bounds。

取代了进程内注册表

monitor 面板里原来的周期任务列表由进程内 JobRegistry 驱动,重启即丢。注册表和进程内调度器都已删除。周期任务跑在 River 上,instance.jobs(System 面板)改读 River 的持久记录。

ops(声明一次,投影到 admin 与 MCP)

这些 ops 放在 stats 域(internal/stats/ops/tasks.go、tasks_events.go)。后台经 /api/admin/tasks* 和 /api/admin/events* 访问。

classDiagram
  class TasksOps {
    «dispatcher ops · stats domain»
    +tasks.overview(kind) counts, oldest_age, backlog, sizes, alerts
    +tasks.list(kind, state, limit)
    +tasks.get(id)
    +tasks.retry(id)  «action»
    +tasks.cancel(id)  «action»
    +tasks.periodic() name, every, last_run, next_run, status
    +tasks.run_periodic(name)  «action»
    +events.list(type, subject, limit)
    +events.get(id) payload + fan-out job states
    +events.requeue(id)  «action»
  }
  class WebhooksOps {
    «dispatcher ops · owner domain»
    +webhooks.deliveries(endpoint_id)
    +webhooks.redeliver(endpoint_id)  «action»
  }
  class Inspector {
    «jobs port»
  }
  class EventsBus {
    «infra/events · reads»
  }
  TasksOps ..> Inspector : jobs
  TasksOps ..> EventsBus : events, backlog
  WebhooksOps ..> Inspector : kind = webhook.deliver
  • 读:tasks.overview、tasks.list、tasks.get、tasks.periodic、events.list、events.get。
  • 动作:tasks.retry、tasks.cancel、tasks.run_periodic、events.requeue。
  • 计划里任务的「丢弃」就是 cancel:River 要永久停掉一个任务只能取消它,再加一个动词做的是同一件事。
  • webhooks.deliveries 是 webhook.deliver 任务的筛选视图。见 webhooks。
  • tasks.get 只给任务状态;属于某个域的结果形状由它自己的 op 返回(jobs.fetch_result,completion-hooks)。

这些 ops 走 Inspector 端口,面板不直接依赖 River(queue-behind-ports)。

权限:只在 owner 平面

这些 ops 只有 owner 读和 owner 动作,不进 API-key 面;任务参数和事件 payload 本来就是 thin 的,面板不显示语料正文(event-model)。

手动重试一条卡住的投递

sequenceDiagram
  actor Owner
  participant UI as admin · Tasks
  participant D as dispatcher
  participant R as River
  participant WD as webhook.deliver
  Owner->>UI: overview shows "1 retryable, oldest 2 hours"
  UI->>D: tasks.list(state = retryable)
  D-->>UI: job + latest error (receiver 503)
  Owner->>UI: receiver is fixed, clicks "Retry now"
  UI->>D: tasks.retry(id)
  D->>R: JobRetry (run now)
  R->>WD: Work
  WD-->>R: 200
  UI-->>Owner: state becomes completed

重试策略与失败分类见 retry-has-one-owner。

验收

e2e tasks-panel 和 tasks-panel-more 走真实 UI,每条断言页面上看得见的内容:

  1. 总览数字与实际一致:造 3 个 retryable、1 个 discarded,总览显示 3 和 1,并显示最老待处理年龄。
  2. 任务列表按状态和 kind 筛选,结果只含所选。kind 筛选列出所有已声明的 kind,没跑过的也列。
  3. 任务详情显示每次尝试的错误和下次重试时间。
  4. 点「立即重试」→ 状态变 completed,且对应副作用真发生。
  5. 点「取消」→ 任务不再执行,副作用没有发生(用哨兵任务证明队列在跑)。
  6. 周期任务列表显示上次 / 下次运行;点「立即运行」后上次运行时间更新。
  7. 重启 backend 后面板仍显示重启前的周期任务运行记录。
  8. 事件流:改一篇笔记 → 出现对应事件,详情里列出它扇出到的订阅方及各自状态。
  9. webhook 投递日志可跳到任务详情;「一键重投」把该端点的 discarded 全部送达。
  10. 告警:积压超阈值时面板显示告警,排空后消失。
  11. 权限:用 API key(非 owner)调 tasks.* 被拒。
  12. 完结 hook:批准申请后行状态从「发送中」变「已发送」;mail mock 先失败时显示「失败」,重试后变「已发送」;jobs.fetch_new 超过等待上限时返回回执,jobs.fetch_result 返回同形状的结果(completion-hooks · async-response-contract)。

实现中顺手修的:切换行之后任务详情还作用在旧任务上;已修。