完结 hook:任务终态发一条通知
状态: 已在 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)。