业务收拢清单:101 项

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

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

全仓库扫描(backend、app、sdk、builder、im-bridge、infra/plugins、updater)得到 101 项手接的“写完 X 顺手做 Y”,每一项都有且只有一个去向。标记:E 发事件 + 订阅方,J 持久任务,P River 周期任务,W 开放为 webhook 事件类型,K 保持现状。行号对应盘点原稿(基于 origin/main 6eb8c0a64)。“实现”一栏写每项最终落到了哪里。分期见 events-roadmap。

pie showData
  title Where the 101 items go (an item may count in several)
  "E emit event" : 22
  "J durable job" : 22
  "P periodic job" : 9
  "W webhook type" : 23
  "K keep" : 44

要搬的:写后副作用

# 做什么 原位置 原来怎么坏 去向 实现
1 访客申请 → 给 owner 发邮件 access_requests.go:59 → request_notify.go:44 请求被整个重试过程堵住;失败即丢;Redis 限流静默丢弃 E access_request.created + J owner.notify(owner/subscriber/mail.go);每小时 5 封的突发上限改成 Postgres 名额,仍是有意丢弃
2 批准申请 → 发带码邮件 + 标记已回复 access_approval.go:86-89 邮件发了但状态没写上则不一致 E + J access_request.approval_mail 与签发码同一事务入队;发送后才写“已回复”
4 换邮箱确认邮件 email_change.go:138 发送失败留下悬空的 pending 行 J owner.email_confirmation;链接令牌在发送时才生成
5 预约 → 通知 owner booker-mcp.js:458 → invoke_background.go:53 游离 goroutine,重启即丢 E booking.created + J booking.record host op 在一个事务里写 booking.* 和一行 booking_notices;owner.notify 发送后删掉它
7 预约落库失败后删日历事件(补偿) booker-mcp.js:309,337 删失败留下孤儿日程,没人重试 J 持久的 supplier.invoke
8–10 Meili upsert / 删除 / 发布后重建 corpus_crud.go、subjectivity.go、corpus.go:139、output.go:61、seo.go:126,140 漏调、拖慢请求、进程内 dirty E corpus.note.changed + J corpus_notes 触发器 + corpus.index(P1)
11 Obsidian 导入后全量重建 obsidian.go:115,133 goroutine,重启即丢 J 逐篇的触发器事件;goroutine 删除(P1)
14 构建完成 → 首页自动发布 builds.go:242 失败只记日志不重试 E microsite.build.settled + J owner.SettleBuild 在一个事务里写构建行、事件和 pg_notify('standmeet_build_settled', owner);订阅方 microsite.homepage_publish
15 构建完成 → 重算素材引用 builds.go:247,256 同上 E + J 订阅方 microsite.asset_refs
16 构建完成 → 唤醒预览长轮询 builds.go:193 → buildnotify 进程内广播,多副本失效 E(LISTEN/NOTIFY) 按 owner 分键的 pgstore.Listener;版本号是该 owner 最近一次构建完成的持久毫秒时间戳;infra/buildnotify 删除

要搬的:后台 goroutine、周期任务、请求内外呼

# 做什么 原位置 原来怎么坏 去向 实现
22 InvokeBackground(后台 supplier 调用 + retry) invoke_background.go:49-65 重启即丢、无死信、单进程 J supplier.invoke internal/infra/sideeffect/supplier;invoke_background.go 和 notifyPolicy 删除(P4)
23 Obsidian 重建 goroutine obsidian.go:135 同 #11 J 删除(P1)
30 周期任务调度器本身 periodic.go:47-92 每副本重复跑,状态在内存 P River 周期任务(选主、启动即跑);进程内调度器删除(P1)
31 Meili 8 秒对账 corpus_index_periodic.go:40 靠进程内标记 删除 已删除(P1)
32–36 公共对话清理、gas 补充、简历草稿清理、流量保留、用量清理 各 *_periodic.go 每副本重复跑 P 以数据声明,跑在 River 上(P1)
38 启动时 Meili 建索引 + 全量回填 wire/search_index.go:22 拖慢启动 J 启动时入队 corpus.reindex(P1)
39, 62 招聘源抓取(请求内串行抓全部源) sources_write.go:133、jobfetch.go:149 慢,可能撞 30 秒写超时 每源一个 J + E jobs.fetched fetch 队列上的 jobs.fetch_source;jobs.fetch_result。不做定时抓取:job-loop.md 否决了每日自动抓取(P4)
63 出站邮件(请求内 retry.Do) mail_retry.go:49、outbound_sender.go:103 请求被退避堵住 J(覆盖 #1 #2 #4) 邮件端口 internal/infra/sideeffect/mail;internal/infra/retry 删除(P4)
98 邮件限流 mailthrottle.go:82 静默丢弃 限流命中改为 snooze 每个收件人每小时 30 封,用完 snooze 到下个窗口(P4)
101 monitor 面板的后台任务列表 jobreg_registry.go:24 重启清零 改读 River 任务表 JobRegistry 删除;instance.jobs 读 River(tasks-panel)(P1)

客户端轮询 → 事件推送(Phase 5,未做)

# 轮询 位置 间隔 去向
50 微站列表长轮询 use-microsites.ts:127 挂起请求 SSE
51 轮询构建直到完成 use-microsites.ts:290 1.5s SSE
52 侧栏申请角标 use-sidebar-badges.ts:39 60s SSE
54 Google 日历 OAuth 后等连接 use-gcal.ts:161 1s × 15 SSE
56 SDK 掉线后恢复回合 agent-adapters.ts:186 1s × 6 SSE

开放为 webhook 的事件类型(全部 thin,默认不订阅)

盘点列了 23 行,声明出来是 37 个类型,加上 corpus.note.changed 和 webhook.test,共 39 个,全部 Webhook 可见(event-model)。投递机制见 webhooks。

# 事件 期
75–76 access_request.created / .approved / .status_changed P2
77–78 code.issued / .revoked / .redeemed P2
79–82 conversation.started / .message / .pruned、ghost.accepted P2
83 booking.created / .cancelled / .rescheduled P4
84–85 application.committed、jobs.fetched P4
86–88 writing.published / .unpublished、corpus.note.changed、vault.imported P2
89–90 microsite.build.settled、page.promoted_live / .rolled_back / .unpublished、microsite.store.doc_inserted P4
91–93 api_key.issued / .revoked、supplier.connected / .disconnected / .activated、block.installed / .failed P2
94–97 gas.exhausted / .refilled、instance.upgrade_requested、owner.login / .email_changed / .recovery_requested、ip_ban.added P2

保持现状(K)

类别 项
写入只在同一库、且本身就是事实来源 #12 交叉引用重建、#13 导入回执、#17 block 失败记录、#19 推理用量、#20 对话 / 卡片 / ghost、#21 已见招聘 id
进程管道和服务器 #24–#29
必须每个节点本地跑的沙箱清理 #37
updater 的文件信号与轮询(它故意不碰数据库) #44–#46
Telegram 长轮询与 im-bridge 内存会话 #48–#49
访客必须当场拿到结果的外呼(现在只发一次,请求内不重试) #64 日历、#66 媒体抓取、#67 模型列表、#68 规格校验、#71 OAuth、#72 验证码、#73 系统探测、#74 推理
会话与缓存失效 #99–#100
升级后等重启(服务器正在重启) #55
访问监控(已决定不进总线) #18

存疑项的定案(按建议)

# 项 定案 理由
3 恢复短语邮件 K,同步 用户在登录页等结果,必须当场告诉发没发出去
6 访客预约确认邮件 K,同步 访客要当场得到确认;补偿删除改走 J(#7)
41–43 构建队列(手写 SKIP LOCKED + 租约) K 队列本身;完成后的钩子走 E builder 是跨进程的 Node sidecar,现在就是持久队列,搬到 River 收益小
47 im-bridge 15 秒轮询配置 K,P5 再考虑 SSE 配置极少变化
53 系统信息 1 秒刷新 K 实时指标,天生是轮询
57 访客工具“下次轮询出现” P5 前先查清调用方 盘点没找到客户端调用点
59–61 投递提交 / 草稿 / 公开报告的 PDF 渲染 K,同步 用户要的就是这份 PDF;投递故意“先渲染再提交”
69 市场安装 K 安装结果当场要看
18 访问流量记录 K 已决定
75–97 webhook 暴露面 全部可订阅,默认不订阅,一律 thin 都是 owner 自己实例的事实;安全类事件恰好适合告警

顺手修掉的(不在计划里)

  • SDK hydration:BlockWidget 和 use-chat-session 在首次渲染时读 localStorage(预渲染的微站上报 React #418);改为挂载后再读。
  • plugin/mount 的 knownToolSpecs 数据竞争;Meili 的 Index 每次调用拿新的索引句柄;任务详情在切换行后还作用在旧任务上。
  • 三处“wiki 对、代码错”:schema.sql 缺 visit_event / visit_viewer(schema 一致性 UT 发现的);drafts_edit.go 里一条过时的 Typst 注释;手动升级文案让用户设置一个不存在的 STANDMEET_REDEPLOY_HOOK(8 种语言)。

被搬走的操作里有 4 个原来有调用方在等结果,它们的完成信号见 completion-hooks 与 async-response-contract。