我把 Webhook 文档丢给 AI 编码助手,它直接搭出了一个能跑的自动回复机器人 — UnifyPort
你肯定熟悉这套流程。待办清单上躺着一个消息集成的活儿,你没有把文档从头读到尾,而是直接打开 Cursor——或者 Claude Code、Windsurf——开始敲提示词。2026 年,大多数人就是这么起一个项目的。真正的问题不是你会不会用 AI “氛围编程”(vibe-coding),而是你对接的那个 API,有没有给助手足够的东西去把活儿干干净。
这是一份开发实录。读完你会得到一个能跑的自动回复机器人:一个 webhook 接收端,对每一条入站消息校验签名,再通过单一的发送端点回复——大约一个下午的工作量,而且大部分是助手干的。它之所以快,是因为 UnifyPort 的 webhook 是归一化的:只有一种事件结构要学,只有一个端点要回调,助手没什么可猜的。
准备:先把文档喂给助手
最关键的一步,恰恰是大家最爱跳过的——在你开口要代码之前,先把 API 文档塞进助手的上下文。今天有三种行之有效的做法:
- 直接粘贴文档页。 把
message.received的事件结构和POST /v1/messages发送部分原样贴进对话框。 - 挂载文档。 Cursor 的
@Docs、Claude Code 的文件上下文、Windsurf 的文档面板——指向 UnifyPort 的接口文档,让助手按需读取。 - 用文档/MCP 连接器(如果你的工具支持),让助手自己去拉取文档。
回报很实在。一个真正读过事件名的助手,不会凭空造出一个 onMessage() 处理函数,或者一个根本不存在的 sendText() SDK。它会照着 message.received 和 POST /v1/messages 来写,因为这就是摆在它面前的东西。喂进去的是垃圾上下文,吐出来的就是幻觉端点——所以花那两分钟,把真东西放进去。
逐条提示词,把机器人搭起来
第一条提示词——接收端:
用我给你的 UnifyPort webhook 文档,写一个 Express 服务,带一个
POST /webhook路由接收标准事件。先做到:当event是message.received时,打印from和text。
因为事件结构是固定的,处理逻辑就是对一个字段做分支判断:
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook", (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
console.log(`${evt.provider} ${evt.from}: ${evt.text}`);
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
第二条提示词——校验签名。 这一步,粗心的实现会跳过,而读过文档的助手不会:
每次投递都用 webhook 的
signing_secret做了 HMAC-SHA256 签名。在信任请求体之前先校验。要用原始请求体,并做时间恒定的比较。
这里助手必须做对的一点:HMAC 要算在原始字节上,而不是重新序列化后的 JSON。只要提示里写了”用原始请求体”,好的助手会接上 express.json 的 verify 钩子来捕获 rawBody。如果它漏了,这就是你唯一要盯的复查点——而且第一次真实投递返回 401 时你就会发现。
第三条提示词——通过发送端点回复:
当
message.received到达时,用事件里的account_id和from,通过POST /v1/messages回复发送者。鉴权用环境变量里的 Bearer API key。
机器人就齐了。助手最终落定的完整文件:
import express from "express";
import crypto from "crypto";
const { UNIFYPORT_API_KEY, UNIFYPORT_SIGNING_SECRET } = process.env;
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", UNIFYPORT_SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post("/webhook", async (req, res) => {
if (!isValid(req)) return res.sendStatus(401);
const evt = req.body;
if (evt.event === "message.received") {
await fetch("https://api.unifyport.ai/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${UNIFYPORT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
account_id: evt.account_id,
to: evt.from,
text: "收到啦!您的消息我们已经接到,稍后会有人跟进。",
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
跑起来,看一条消息进来
连一个账号(Telegram 最快上手),注册一个指向你服务器的 webhook 端点,带上 signing_secret 和 subscribed_events: ["message.received"],然后给你的机器人发一条私信。事件到达时长这样:
{
"event": "message.received",
"account_id": "acct_8Q2vK",
"provider": "telegram",
"from": "user_3f9c1a",
"text": "请问周末营业吗?",
"timestamp": 1749427200,
"message_id": "tg_msg_5d2b7e"
}
你的服务器校验签名,调用 POST /v1/messages,回复就出现在对话里。如果签名校验失败,你会拿到 401——那就是上面那个 rawBody 的坑。修一次,这个闭环就稳了。
再扩展
归一化 webhook 的价值正是在这里兑现。加第二个平台,不是再做一次集成——而是零新增处理代码。连一个 WhatsApp、LINE 或 Zalo 账号,订阅同一个 webhook,完全相同的 message.received 分支照样跑,因为事件结构在不同平台之间不会变。provider 字段告诉你消息从哪来,而你代码的其余部分根本不用看。
想让机器人真的去回答问题,而不只是回个”收到”?再加一条提示词就行:“回复之前,把 text 发给一个大模型,用它的回复作为回复内容。” 发送调用助手已经写好了,你只是把一个字符串换成模型的输出。
这个下午的启示,不是”AI 帮你把机器人写了”。而是:AI 助手的上限,取决于你让它对接的那个接口面。一个事件名稳定、只有一个发送端点的归一化 webhook,正是助手能干净收尾的那种接口——一种结构要学,没有东西要瞎编。把你的工具指向 UnifyPort v1 API,贴进 message.received 的结构,你会在这个下午结束前拿到一个会校验、会回复的机器人——然后把它放到其余五个渠道上去。