全部文章

Webhook 挂仓库,邮件挂在人身上

在 GitHub 上 @ 一个数字员工,它怎么知道被叫了?Webhook 挂仓库,App 挂应用,只有邮件挂在人身上。邮箱能推送之后,它才是够格的入口。

手绘:仓库和应用的线各自停在一个小盒子上,只有邮件那条一路接到数字员工胸前的工牌

我想给一个数字员工派一件在 GitHub 上的活:在 issue 里 @ 它一下,它就接着干。

难的是它怎么知道自己被叫了。翻了一圈接入方式,能收到事件的路有三条,没有一条是发给它这个人的。不解决这一格,它只能每隔一分钟去问一次有没有人叫我。

数字员工接外部系统的事件,第一入口应该是它自己的邮箱,因为只有邮件是按人投递的。

先把底交代在前面:这条链路我一环都没有跑通过。下面是我翻各家文档、读别人开源实现之后的选型判断,不是实测报告。

先问这个事件挂在谁身上

同一件事,三条接法,差别不在难易,在事件挂在谁身上。

Webhook 建在一个具体的仓库、组织、GitHub App 或 Marketplace / Sponsors 账号上,不建在人身上:这个库里发生了什么,全播出去,谁被 @ 了要自己从 payload 里捞。GitHub App 挂在应用上:装好之后它收到装它的那些库的 issue 和 PR 事件,权限按应用算,范围比全库播报窄一圈。「有没有 @ 我」仍然得自己在 payload 里判,而且被 @ 的是一个应用,不是一个同事。第三条是另一种做法:让它有一个跟人一样的账号,人在 issue 里能被 @,它也能。这一条的事件出口是邮件通知。

接法 事件挂在 能不能表达这是发给某个人的 推送
Webhook 仓库 / 组织 不能,得自己从 payload 里猜 有,平台签名
GitHub App 应用 能,但应用不是人 有,平台签名
邮件通知 人(账号) 能,头里直接写了你为什么收到 看邮箱,默认没有

这张表比的是 GitHub 上这三条现成的路,不是一句放之四海的排序。换一个平台,格子里的东西会变。

选第三条不只是为了好看。工作不止 GitHub 这一个环节:它在这边把代码补完,还要去钉钉的群里说一声,还要在那边被 @ 着追问。两边得看上去是同一个人,而且必须真是同一个人。不然谁干的这个问题没法回答。

想要人级,又想要推送,只剩一条路

GitHub 给「人」的事件出口只有两个:网页上的 Notifications,和邮件。Webhook 是给仓库和应用的,不是给人的。

给人的那条也有 API,但官方就是按轮询设计的:响应里带一个 X-Poll-Interval 告诉你多久才允许再拉一次(示例是 60 秒),文档明确要求遵守它。配合 Last-Modified 做条件请求,没变化时返回 304、不算配额。省下的是配额,延迟一分不少。

所以想要一个能被 @ 的「人」,又想要推送,路径只剩:用机器账号当这个人,把它的通知路由到一个能推的邮箱。机器账号这条是合规的,GitHub 条款允许每个自然人在个人账号之外再持有一个免费机器账号(再多就要另算了,这是后面规模化时的一笔账)。

邮件还顺手把「人级」变成了机器能判的东西。GitHub 每封通知都带 X-GitHub-Reason,取值是 mentionassignreview_requestedteam_mention 这些;同一个原因还写进第二个 Cc 地址 [reason]@noreply.github.com,仓库写进 List-Id;回这封信就是给那条会话加一条评论。GitLab 同构X-GitLab-NotificationReasonmentioned / assigned / review_requested 等,另有 X-GitLab-Project-Path 和支持回信的 X-GitLab-Reply-Key

为什么是我收到这封,在邮件里是一个字段,在 Webhook 里是一段业务逻辑。

卡在连接器这一层,直到邮箱自己会推

绑邮箱是容易的,GitHub 支持绑多个。往下就卡住了。

我找过的邮箱都很勤快地支持了连接器,能读、能搜、能列。可读不等于会推。 只读的连接器意味着 Agent 只能自己去问,于是又绕回轮询。

真正让这条路走通的,是邮箱自己会推。我是在常驻的 Grok Bot 上看到一个叫 AgentMail 的插件时想明白这件事的:它给收件箱配 Webhook,十类事件(message.receivedmessage.sentmessage.bounced 等)可以按组织、按 pod、按单个收件箱三级订阅,签名走 Svix;没有公网 URL 的本地 Agent 还能改用 WebSocket 收同样的事件。有了推送,主动性才谈得上。 事件来了它就动,不必定好闹钟去问。

两条通道的配套要求不一样:Webhook 要一个能被公网打到的接收端点,WebSocket 要一条自己维持的长连接。所以最省事的落点,是一个本来就常驻、本来就有协调者的平台,Webhook 直接配在上面。没有这种落点,还有两条路:给本地节点找一个能被公网打到的中继,多一跳,安全性得重新摆上桌;或者干脆走 WebSocket 那条,本地自己维持长连接,不要公网 URL。

为什么不逐个接各家的 API

往回想一层:为什么要把这些事件收敛到一个账号身上,而不是老老实实一家一家接?

因为每家 Webhook 的 schema 都不一样,接十家就是十套解析。

这里有一条像样的反驳:接仓库级 Webhook,自己在 payload 里按账号过滤,一样能做出人级效果。技术上完全成立,我没法说它不行。

但它和开头那句判断说的不是同一件事。平台自己按人寻址,和你从全量事件里把人过滤出来,是两回事。 前者是平台在投递时就知道这封该给谁,理由写在头里;后者是平台照旧全播,由你重建那个「谁」。GitHub 的 webhook 文档写明 webhook 建在仓库、组织、App 或 Marketplace / Sponsors 账号上,没有「建在某个人身上」这一档;而通知邮件每封都带着收信人为什么收到。我说只有邮件按人投递,说的是前一种。

自己过滤那条路的代价只是换了个地方:每家一套 schema、一套过滤规则、一套权限申请,而且你得在每个平台上都拿得到装应用或配 Webhook 的权限,这在别人家的组织里往往拿不到。邮箱那条把这些折叠成一个收件箱里的过滤规则。所以在「能不能做出人级效果」这一层,两条都行;在「谁来承担那份重建工作」这一层,才分出高下。

反过来看邮箱这条。GitHub 和 GitLab 各自把「为什么是你收到」写进了邮件头:X-GitHub-Reason 告诉 Agent 这是提及还是指派,List-Id 告诉它来自哪个库;GitLab 那边是 X-GitLab-NotificationReasonX-GitLab-Project-Path两家素不相干,却都把这个信息放在了邮件这一侧。 我没有第三家的证据,但这两家足够让我先按这条路走。

这里要修正我自己一开始的判断。我原本以为是别家没有 Webhook 能力,查完发现不是:Gmail 有 users.watch,把变更发到 Cloud Pub/Sub 再推给你的 HTTPS 端点(每个用户约一条每秒,watch 七天到期要续);Microsoft Graph 能对收件箱建 change notification 订阅,同样推到公网端点。两家都推得动。

缺的是另一件事:按 Agent 编程开一个新邮箱。AgentMail 在它公开的供应商对比里把这条列成 Gmail API 的硬约束。这是供应商说竞品,立场要打折看。 我自己在两家的接口文档里也没找到开箱入口,但查不到不等于没有。能力在,形态不对。

一个 IM 加一个邮箱,但这个邮箱得能设规则

收敛之后,接入面小得出乎意料:接一个协同平台的 IM,再接一个邮箱,邮箱负责连海外那一堆系统。再往前一步,我会把企业邮箱本身注册到各大平台:注册用它,通知也回到它,Agent 从这一个口子知道发生了什么。

关键点在后半句:这个邮箱本身是不是 AI ready。能不能设受限规则、过滤规则,决定它是一个入口还是一个垃圾桶。

所以我把 AgentMail 的文档和它的开源仓库翻了一遍,想知道它除了带 Webhook 的邮箱之外还做了什么。答案是四件事:

  • 按 API 开箱:一次调用给一个 Agent 一个地址,身份跟着 Agent 走,不是几个 Agent 共用一个人的邮箱。
  • 三级收发名单:方向(send / receive / reply)乘类型(allow / block)六种,组织、pod、单箱三级,单箱覆盖上级。最讲究的是它按 In-Reply-To 分流:来信是回它自己发出去的信,只查 reply 名单;是陌生来信,只查 receive 名单。自己约来的和别人塞进来的,本来就该按两套规矩。
  • 标签当工作流状态needs-replyrepliedescalated 这类标签挂在线程上,状态不用写回正文再解析。按提示词自动打标在文档里还标着 coming soon,现在不能当已有能力用。
  • 推送两条通道:签名 Webhook 给有公网的服务端,WebSocket 给没有公网的本地 Agent。

生态里已经有人把它接成了落地形状。dsh-agentmail 把邮箱做成 DeepSeek Harness 的插件,一封邮件线程绑一个会话:来信到了,活的就注入,睡着的就唤醒,都没有就从 API 重建一个。AgentMail 就是那个存储,所以插件自己不留映射表。

轮询顶不住的地方

回头说说为什么轮询一定要绕开,这不是洁癖。

先是浪费。多数时候没有真事件,拉一次就是白拉一次。然后是规模:五六七八个平台一直在那边拉,全拉一遍,源越多这笔账越难看。最后是及时性:拉得再密也是隔一拍,密到贴脸又回到前两个问题。这三条是我不愿意让它去轮询的理由,不是我测出来的结论。

光把推送这条路打通本身就有价值,它不是优化项。 主动性是被推出来的,不是被拉出来的。

哪些系统的邮件真能当入口

邮箱不是万能入口,得挑。我用四条判据看一家的通知系统够不够格:事件覆盖全不全、有没有机器能过滤的头、能不能按上下文分流、回信能不能变成动作。只收查得到官方出处的:

两个方向要分开看,混在一起比不出东西。 出站是平台把通知发给这个账号,入站是往一个地址发信能让平台做事。

出站通知(平台 → 员工的收件箱):

平台 机器可过滤的头 能不能按上下文分流
GitHub X-GitHub-ReasonList-Id(每封都带) 能,按组织选一个已验证邮箱
GitLab X-GitLab-NotificationReasonX-GitLab-Project-PathX-GitLab-Reply-Key 能,按项目 / 组

入站(往一个地址发信 → 平台里出现动作):

平台 来信能不能变成动作
GitHub 能,回通知邮件就是给那条会话加一条评论
Linear 能,团队 intake 邮箱把来信变 issue

两张表是照 2026-09-17 至 09-18 读到的官方文档逐格填的,比的是文档写明的能力,不是我跑出来的结果;格子里只有我在文档上看到的东西,查不到的没往上写。要复现就按文末参考里那几页对一遍。

Linear 的那条我读了原页:团队专属 intake 邮箱,来信直接进 Triage,同一页写明回到那个地址的回信不会再建一个 issue。入站是单向的,别指望它当会话用。

Stripe 是另一头:它的文档从头到尾教你怎么接一个 webhook 端点,事件面就建在那上面。这类平台没必要硬绕邮箱,该接原生事件就接原生事件。

这里还要修正我自己的第二处。我本以为 GitHub 能按仓库把通知路由到不同邮箱,查下来是按组织,一个组织选一个已验证地址。想按仓库区分,得在邮箱那边用 List-Id 过滤。路由的智能在邮箱里,不在源平台里。 这反过来正好说明,邮箱能不能设规则是整件事的前提,不是加分项。

统一了入口,也统一了攻击面

这是这条路最硬的一个反对意见,我没法绕开。

Webhook 是平台签名的、schema 固定的、只有平台能写。邮件地址是任何知道它的人都能写入的。把感知面统一到邮箱,等于把一条认证过的事件流,换成一个开放的投稿口。

AgentMail 自己在公开的威胁模型里把邮件提示词注入列为 critical,而且写得很老实:用分隔符把不可信内容框起来能降低概率,但不是安全边界;收发名单是一层防御,不足以单独成立(被冒充的、或者本身账号被攻陷的合法发件人,照样把东西递进来)。

能用的缓解是叠起来的,没有一条单独成立:逐箱收发名单、最小权限(读信的 Agent 不给删数据转账的工具)、出站前扫一遍密钥、高风险动作走草稿人审、来信内容包在明确标了不可信的围栏里。dsh-agentmail 那套可以直接抄:每封来信包进 <email-content untrusted="true">,围栏序列在正文里被中和掉,注入的那句话原样留着,作为可以上报的内容,而不是被当成指令递过去。这只是降低概率,不是把它拦住了。真正拦住动作的,仍然是那道独立的权限闸门。它还默认不让来信唤醒空闲的 Agent,要打开得显式配置,理由是自动唤醒会把垃圾邮件变成一次带预算的提示词注入。

所以判断要收窄:邮箱适合当感知入口,不适合当授权入口。 事件从邮箱进来,能不能动手是另一道闸门的事。

最后一公里还有一条。Webhook 那一段的重试写在文档里,AgentMail 走 Svix,非 2xx 会重试;邮件那一段我没有拿到同样的东西。在拿到之前,要紧的链路别把最后一公里押在邮件上。

这对做协同平台的人意味着什么

对钉钉、飞书这种要做数字员工协作平台的,一个好用的邮箱系统、一套邮件和 IM 绑定的体系,是很大的一张牌。算的是接入成本:对 GitHub、GitLab 这类把通知发到账号邮箱、还把原因写进邮件头的平台,有这套体系就少写很多连接器;没有,就得一家一家写。

具体接哪些,得回到客户的场景里数。电商会不会走邮箱通知,哪些系统该被邮箱的火力覆盖,这是要坐下来数清楚的事,不是一句原则能定的。

这一篇站在哪儿

截至 2026-09-18,上面每一条都来自各家的官方文档和别人的开源实现,出处在文末列着;这条链路我自己一环都没有跑通过,所以标的置信度是「很可能」,不是「确定」。样本就是文里点过名的那几家,别当成对所有平台的盘点。

要落地的人,先拿一个仓库、一个机器账号、一个能推的收件箱,把最短的那一跳跑出来再说。

参考

  1. About webhooks · GitHub Docs(查阅 2026-09-18)— webhook 必须建在仓库、组织、App 或 Marketplace / Sponsors 账号上,不建在个人身上。
  2. Email notification headers · GitHub Docs(查阅 2026-09-17)— X-GitHub-Reason 的取值、reason 写进第二个 Cc、List-Id 标仓库、回信变评论。
  3. REST API endpoints for notifications · GitHub Docs(查阅 2026-09-17)— 给人的那条通道只能轮询,响应带 X-Poll-Interval
  4. Managing organization notifications · GitHub Docs(查阅 2026-09-17)— 自定义路由按组织选已验证邮箱,不是按仓库。
  5. GitHub Terms of Service(查阅 2026-09-17)— 每个自然人在个人账号之外最多再持有一个免费机器账号。
  6. Notification emails · GitLab Docs(查阅 2026-09-17)— X-GitLab-NotificationReasonX-GitLab-Reply-Key 等可过滤的头。
  7. Configure push notifications with the Gmail API · Google for Developers(查阅 2026-09-17)— users.watch 经 Cloud Pub/Sub 推到 HTTPS 端点,每用户约一条每秒,watch 七天要续。
  8. Receive change notifications through webhooks · Microsoft Graph(查阅 2026-09-17)— 可对收件箱建订阅,把新邮件推到公网端点。
  9. Introduction · AgentMail Documentation(查阅 2026-09-17)— 按 API 开箱、标签、检索与抽取;自动打标标为 coming soon。
  10. Webhooks Overview · AgentMail Documentation(查阅 2026-09-17)— 十类事件、三级作用域、Svix 签名、WebSocket 免公网 URL。
  11. How do I set up allowlists and blocklists? · AgentMail Documentation(查阅 2026-09-17)— 六类收发名单、单箱覆盖上级、按 In-Reply-To 分流。
  12. agentmail-to/agentmail-skills(查阅 2026-09-17)— 公开的 agent-email-patterns 与威胁模型:注入列 critical,分隔符不是安全边界,名单不足以单独成立。
  13. agentmail-to/dsh-agentmail(查阅 2026-09-17)— 一封邮件线程绑一个会话,来信包在不可信围栏里,默认不唤醒空闲 Agent。
  14. Create issues via email · Linear(查阅 2026-09-17)— 团队 intake 邮箱把来信变 issue,回信不会再建新 issue。
  15. Receive Stripe events in your webhook endpoint · Stripe Docs(查阅 2026-09-17)— 事件面以 Webhook 为主,文档教的是接 webhook 端点。

相关阅读

选择 打开esc 关闭