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