业务收拢清单: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。