Discord 有两样东西都被叫做“webhook”,但行为天差地别。一个是用来发消息的“发完即忘”URL。另一个是交互端点,每当用户运行斜杠命令时就会收到带签名的 POST。前者在 localhost 上能用,后者永远不行。本指南讲的是后者——因为它才需要隧道。
Discord 的两种 webhook
出站 webhook 是你向其 POST 的 URL。你发一条 JSON 消息,它出现在频道里。无需隧道,因为流量从你的机器向外流出。
交互端点 方向相反。你在 Discord 注册一个 URL;每当用户运行 /yourcommand,Discord 就向它 POST。该端点必须是 HTTPS、必须在三秒内响应,并对每个请求验证 Ed25519 签名。localhost 自身做不到这些。
为什么交互端点比大多数更严格
Discord 不仅要 HTTPS——它要求你在接受该 URL 前证明你控制该端点。验证发生在注册时:Discord 发送一个 PING 交互,你的服务器必须用带签名的 PONG 回应。只要 Ed25519 验证失败一次,Discord 就拒绝保存该 URL。
你第一次不会搞定 Ed25519 那部分。几乎没人能一次成功。
Ed25519 验证,最咬人的部分
Discord 使用 Ed25519(不是 HMAC SHA-256),并在每个交互上提供两个请求头:
X-Signature-Ed25519— 十六进制签名X-Signature-Timestamp— 以秒计的时间戳
你用 Discord 在开发者门户里展示的公钥对 timestamp + rawBody 进行签名。大多数语言都有相应的库。在 Node 中:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
验证失败的头号原因——又是——在这段代码运行前就解析请求体的中间件。你需要 Discord 发来的原始字符串,而不是把解析后的对象重新序列化的版本。
分步:在 localhost 上的交互端点
- 在本地启动你机器人的 HTTP 服务器(任意端口)。
- 运行
npx portpreview 3000获取一个公共 HTTPS 网址。 - 在 Discord 开发者门户打开你的应用,把隧道网址加上你的交互路径粘贴到 Interactions Endpoint URL。
- 点击 Save。Discord 会立即发出一个
PING。如果你的 Ed25519 验证有效,URL 就会保存;否则该字段显示错误。 - 保存后,在任意有你机器人的服务器里运行一个斜杠命令。请求会落到你的处理器里。
如果保存反复失败,检查捕获到的请求——请求头、请求体和你服务器的响应。十有八九,请求体在验证前被一个 JSON 中间件悄悄改动了。
三秒期限
Discord 要求在三秒内响应,否则就把该交互算作失败。对于耗时更久的,先发一个 type-5 的初始响应(延迟),稍后再用 PATCH 给原始交互发真正的回复。本地测试一眼就能看出哪些处理器太慢——请求捕获会显示确切的延迟。
出站 webhook 仍然适合测试
如果你只是想从本地代码向频道发消息,出站 webhook URL 无需隧道即可工作。POST JSON,消息出现。我们用它从本地开发运行发送日志告警。
何时这套配置不够用了
对于只有少数命令的单个机器人,用隧道做本地测试绰绰有余。如果你维护一个服务数百万用户的公开机器人,你最终会想要一个带稳定 URL 的预发布部署,免得隧道会话轮换时 Discord 每次都要重新验证。
Discord 的三秒超时与 GitHub 的十秒窗口属于同一类问题。要看长篇版本,请阅读我们关于 webhook 重试与幂等性 的看法。加入 PortPreview 等候名单,在一个 CLI 里同时获得隧道与捕获。