完结 hook:任务终态发一条通知

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

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

任何任务进入终态都发一条 Postgres 通知。原来等同步结果的调用方,改为等这条通知,等到上限就给回执。业务状态跟着完结走,不跟着入队走。

谁在等,现在拿到什么

查了代码:要搬的 13 类操作里,有 4 个的调用方在等结果,变异步后都需要完结信号。

操作 原来谁在等 实现后的完结信号
批准申请的带码邮件 后台显示“已发送给申请人”或报错;MCP access_requests.approve 返回结果 码同步签发,access_request.approval_mail 任务在同一事务里入队(mail_job_id)。该行显示 request-mail-state:发送中、已发送或失败。发送成功后才标记“已回复”(经 UpdateAccessRequestStatus)。MCP 最多等 2 秒,然后给回执。
换邮箱确认邮件 提示“确认邮件已发到 X” pending 行同步写入并带 pending_email_job_id;提示改为“已排队”;pending 行上显示发送状态。owner.email_confirmation 任务在发送时才生成链接令牌,过期的任务什么都不发。
jobs.fetch_new MCP 直接返回抓到的职位列表;后台等请求返回 每个职位源一个 jobs.fetch_source 任务。MCP 最多等 20 秒,跑完就返回列表;否则返回 {job_ids, pending: true}。jobs.fetch_result {job_ids} 返回完整形状;tasks.get 只给状态。
访客侧检索(Meili)写后即读 retrieval-search-consistency.spec.ts 断言写完就能搜到 corpus.create、corpus.update、corpus.promote 的回执带 indexed 和 index_job_id;请求内最多等 2 秒(见 async-response-contract)

其余没人在等:访客申请通知、预约通知、补偿删除、Obsidian 重建、构建后钩子、启动回填。恢复短语邮件也在等结果,已定案保持同步。

机制:终态是一条通知,不是事件

  • 任务进入 completed、discarded 或 cancelled 时发 pg_notify('standmeet_job_final', 任务 id)。
  • 每个进程一条共享的 LISTEN 连接(pgstore.Listener)唤醒等这个 id 的等待者。同时最多 64 个等待者,超了调用方立刻拿回执。
  • 没有 job.completed / job.discarded 这类 outbox 事件。每个任务记一条,就会为每个任务再扇出一遍。任务行本身是面板和 tasks.get 读取的持久记录。
  • UI 状态标签目前轮询任务状态(P5 换 SSE)。
  • 请求会等的队列(index、notify)每 100 毫秒轮询一次。等待者通常在这次轮询加 River 250 毫秒的完结批次内拿到结果(concurrency-control)。
  • 业务状态跟着完结走:“已回复”这类状态由任务 handler 在发送之后写,不在发送前写。
sequenceDiagram
  actor Owner
  participant UI as Admin · requests list
  participant UC as access_requests.approve
  participant Q as Queue
  participant M as approval_mail job
  participant L as pgstore.Listener (shared LISTEN)
  Owner->>UI: approve
  UI->>UC: approve
  UC->>UC: same transaction, issue code + Record(access_request.approved) + Enqueue(mail)
  UC->>L: wait on job id, up to 2 s
  Q->>M: Work
  M->>M: send, then mark the request replied
  M-->>Q: completed
  Q->>L: pg_notify standmeet_job_final(job id)
  L-->>UC: wake (or the 2 s pass, then the receipt)
  UC-->>UI: code + link + mail_job_id
  UI-->>Owner: row state sending → sent (polls the job state)

已更新的现有 spec

原则:只改“什么时候看”,不改“看到什么”——同步断言改成等待或轮询,断言的结果不变。

  • 批准与邮件:mail-supplier、mail-throttle-recipient、admin-requests(没配邮件时返回 400 这条保持同步)、access-request-notifies-owner(精确计数改为等终态)。
  • 换邮箱 / 恢复:account-email-change-needs-confirmation、account-email-pending-lifecycle、account-recovery-row-tells-the-truth、account-edit。
  • 检索:retrieval-search-consistency,并复查了 retrieval-acl、corpus-grep、subjectivity-not-cited、corpus-search-cjk-not-silent。
  • 招聘抓取:job-fetch-*、integration-job-loop、application-status-persist、admin-listings-dedup。
  • booking-owner-notify:等到终态再断言数量。
  • norm-outward-toolset 的 golden 加上了 tasks.*、events.*、webhooks.* 和 jobs.fetch_result 工具。

任务面板的 e2e(tasks-panel-more)覆盖这些:批准申请后行状态从“发送中”变“已发送”;mail mock 先失败时显示“失败”,重试后变“已发送”;jobs.fetch_new 超过等待上限时返回回执,jobs.fetch_result 返回同形状的结果(events-test-plan)。