Webhook 推送入口:/send 与 AppKey 路由设计

2026年1月25日
2 分钟阅读
作者:ixNieStudio

Webhook 推送入口:/send 与 AppKey 路由设计

这篇文章只讲一件事:如何把“发微信通知”做成一个你愿意长期用的 webhook。

看起来最直觉的实现是:

提供一个 /send 接口,传标题和内容,然后调用微信 API。

但真正做下来你会发现,推送入口的设计,决定了:

  • 它到底是不是“脚本友好”(能不能一行 curl 搞定)
  • 它到底是不是“能长期跑”(会不会因为边缘环境/域名/缓存而随机炸)
  • 它到底有没有“可控风险”(弱认证不可避免,但要知道边界在哪)

实现入口在:

  • node-functions/send/[[key]].ts

我希望这个接口长什么样(接口契约)

最理想的调用形态是两种都支持:

  • GET:脚本最喜欢(不用构造 JSON)
  • POST:复杂系统最喜欢(结构化、可扩展)

因此契约是:

  • 必填:title
  • 可选:desp

对应示例:

curl "https://your-domain.com/send/{appKey}?title=部署成功&desp=prod 已上线"
 
curl -X POST "https://your-domain.com/send/{appKey}" \
  -H "Content-Type: application/json" \
  -d '{"title":"部署成功","desp":"prod 已上线"}'

路由为什么要“看起来有点多余”

你会看到项目兼容了多种 URL 形态(核心原因是边缘部署的路由匹配差异):

  • /send/{appKey}:最常规、最推荐
  • /{appKey}.send:适配某些“按文件/后缀匹配”的路由习惯
  • /{appKey}:某些 catch-all 场景下 ctx.path 只剩 key

这类兼容乍看“有点脏”,但它解决的是一个非常现实的问题:

  • 你不希望用户在部署成功后,因为“路由没对齐”而卡在第一步

换句话说:推送入口是产品的第一体验,这里越宽容,整体转化越高。

CORS:宁可简单,也要稳定

推送入口允许跨域的目标很明确:

  • 让浏览器、HomeAssistant、脚本都能直接调用

因此做了最小化 CORS,并让 OPTIONS 直接短路返回:

  • Access-Control-Allow-Origin: *
  • OPTIONS -> 204

在边缘函数里,越少分支越少不确定性。

核心取舍:为什么用 AppKey 做“弱认证”

推送入口如果做成“强鉴权”(管理员 token 或登录态),会立刻变得很难用:

  • 脚本需要先登录获取 token
  • token 过期要刷新
  • CI/HA 里管理 token 变成额外负担

所以这里选的是“弱认证”:

  • URL 自带 appKey
  • 后端用 appKey 找到 App 配置与关联的 Channel

它不是“安全”,而是一种风险可控的工程妥协

弱认证的风险是什么

  • AppKey 泄露 = 任意人可发消息(最常见就是日志里打印了 key、或写进了公开仓库)

怎么把风险变得可控

  • 把 AppKey 当成 Secret:只放在私有环境变量/密钥管理里
  • 必要时加一层网关:IP 白名单 / 反代 BasicAuth / 内网调用
  • 把滥用可观测化:至少要能在消息历史里看到异常频率(后续可扩展限流)

一张图:一次推送请求到底发生了什么

sequenceDiagram autonumber participant C as Caller participant S as /send Route participant P as pushService participant K as KV (via Edge) participant W as WeChat API C->>S: GET/POST /send/appKey S->>S: 解析 title/desp + 参数校验 S->>P: push(appKey, message) P->>K: get app by appKey P->>K: list openids by appId P->>K: get channel by channelId P->>W: send (custom/template) W-->>P: per-recipient result P->>K: save message history P-->>S: PushResult S-->>C: { code:0, data:{ success/failed/results } }

这张图里有两个“关键工程点”:

  • 尽早校验参数:避免 KV / 外部 API 资源浪费
  • 一定要落历史:否则你永远不知道“到底发生了什么”

失败语义:不要把“失败”当成一个布尔值

推送失败大致分三类,处理方式完全不同:

  • 可自愈:token 失效(可以强制刷新后重试一次)
  • 需要用户动作:用户未关注/拉黑(你重试一百次也没用)
  • 系统性问题:KV_BASE_URL/内部鉴权错误(所有请求都失败)

因此接口返回更适合给出“批量投递结果”而不是一个 ok

启发:把 webhook 设计成“产品入口”,不是“后端接口”

  • 对调用者:越像 curl,越容易被接入到各类系统里。
  • 对系统:越简单的外部接口,越需要内部的校验/隔离/观测。
  • 对安全:弱认证不是原罪,不透明的弱认证才是(你要知道风险在哪,怎么兜底)。