我让 Cursor 对着 Webhook 文档,用半小时搭了个 Telegram 转 Slack 的消息中继 — UnifyPort
想把 Telegram 消息转到 Slack,常规路径是:用 @BotFather 创建一个机器人、设置 Webhook 或 getUpdates 轮询、解析 Telegram 特有的 Update 对象、格式化后发到 Slack 的 Incoming Webhook、再处理速率限制。能跑通,但代码完全绑死在 Telegram 上。Update 对象有自己的结构,机器人只能看到它自己的对话,如果你以后还想在同一个 Slack 频道里接收 WhatsApp 消息——得从零接一套完全不同的 API。
我走了另一条路。打开 Cursor,把 UnifyPort 的 Webhook 文档贴进去,让它搭一个 Telegram 转 Slack 的消息中继。三十分钟后我拿到一个 Express 服务:对每条投递做 HMAC-SHA256 签名验证、用 Slack Block Kit 格式化消息、转发到频道——总共大约 45 行代码。同一个中继还能处理 WhatsApp、LINE、TikTok、Zalo 和 X 的消息,不需要改任何代码,因为入站事件的数据结构在六个平台上完全一样。
最终成品
一个 Node.js Express 服务,能做到:
- 接收 UnifyPort 统一 Webhook 推送的
message.received事件 - 用 HMAC-SHA256 和你的
signing_secret验证每条投递的签名 - 把每条消息格式化为带平台标签、发送者和时间戳的 Slack Block Kit 消息
- 通过 Incoming Webhook 发送到 Slack 频道
用时:约 30 分钟。你需要一个 UnifyPort 工作区,已通过二维码扫码连接 Telegram 账号(不用注册机器人),以及一个 Slack Incoming Webhook URL。
为什么不直接用 Bot API?
先看看走 Telegram Bot API 需要做什么:
- 通过 @BotFather 创建机器人 —— 拿到一个 bot token,但机器人是独立于你个人账号的实体
- 选择消息接收方式:
getUpdates轮询或 Webhook —— 两种都需要 Telegram 专属配置 - 解析
Update对象:消息正文在update.message.text,发送者在update.message.from.id,会话在update.message.chat.id—— Telegram 独有的数据结构 - 处理速率限制:全局 30 条/秒,同一群组 20 条/分钟
- 格式化并转发到 Slack
代码能跑——直到你还需要在同一个频道里收 WhatsApp 消息。这时候你得单独接入 WhatsApp Cloud API、解析另一种 Webhook 载荷、维护两套代码路径,最终都输出一条 Slack 消息。三个平台就是三套代码。
UnifyPort 的 Webhook 把这些归一了。每个平台推送同样的 message.received 事件——provider、from、text、message_id。一个处理函数通吃六个渠道。
准备:把文档喂给 Cursor
开始写 prompt 之前,先把 API 文档送进 Cursor 的上下文。用 @Docs 添加 UnifyPort Webhook 文档,重点包括:
message.received事件的载荷结构x-unifyport-signature请求头和 HMAC-SHA256 验证说明POST /v1/webhook-endpoints创建端点的接口
或者直接把这些内容粘贴到对话里。关键是:真实的字段名进去,正确的代码出来。如果没有文档,Cursor 会自己编一个 telegram.onMessage() 回调或 bot.on('text') 处理器——这些在 UnifyPort 的 API 里根本不存在。
Claude Code、Windsurf、Copilot 都一样——用文件上下文、文档面板,或者直接粘贴。工具是次要的,文档才是核心。
开始构建
第一个 prompt——中继骨架:
根据 UnifyPort webhook 文档,写一个 Express 服务,POST /webhook 路由。当 event 为 “message.received” 时,把格式化的消息发到环境变量 SLACK_WEBHOOK_URL 指定的 Slack incoming webhook。包含 provider 名称、发送者和消息正文。所有事件都返回 200。
Cursor 读取文档后生成:
import express from "express";
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const app = express();
app.use(express.json());
app.post("/webhook", async (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `*[${evt.provider}]* ${evt.from}: ${evt.text}`,
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
十五行处理逻辑。因为事件结构在所有六个平台上完全一样,不需要任何 Telegram 专属的解析——evt.provider 的值是 "telegram",但结构跟 WhatsApp 或 LINE 的消息完全一致。
第二个 prompt——签名验证:
每次投递都包含一个 x-unifyport-signature 请求头。用环境变量中的 signing_secret 进行 HMAC-SHA256 验证。HMAC 必须基于原始请求体计算——不能是重新序列化的 JSON。验证失败返回 401。使用时序安全的比较方法。
Cursor 添加了验证层:
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
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", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
verify 回调在 Express 解析 JSON 之前捕获原始 buffer。如果 Cursor 用了 req.body 再重新序列化,空白字符差异会导致 HMAC 永远匹配不上。prompt 里说了”原始请求体”,所以它一次就做对了。
第三个 prompt——更丰富的 Slack 格式:
把 Slack 消息改成 Block Kit 格式。用标签显示 provider,加粗显示发送者 ID。添加一个 context block 显示时间戳和 message_id。
完整的 server.js:
import express from "express";
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
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", 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" && SLACK_WEBHOOK) {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*[${evt.provider}]* from *${evt.from}*\n${evt.text}`,
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `${evt.message_id} · ${new Date(evt.timestamp * 1000).toISOString()}`,
},
],
},
],
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
不到 50 行。签名验证、Slack Block Kit 格式化、转发所有入站消息。处理函数从不判断 provider 的值,因为不需要。
运行它,看 Telegram 消息进来
在 UnifyPort 工作区连接一个 Telegram 账号——Telegram 支持二维码认证,用 Telegram App 扫码即可,几秒钟完成。不用 @BotFather,不用 bot token,不用管理 API 凭证。连接的是你的真实 Telegram 账号,不是一个独立的机器人。
注册一个 Webhook 端点指向你的服务器,设置 subscribed_events: ["message.received"] 和 signing_secret。启动服务,然后让另一个用户给你的 Telegram 账号发一条消息。事件到达:
{
"event": "message.received",
"account_id": "acct_5Qm8nR",
"provider": "telegram",
"from": "user_7c3d9e",
"text": "明天下午三点还碰面吗?",
"timestamp": 1750521600,
"message_id": "tg_msg_2a6f4b"
}
服务器验证签名、格式化 Block Kit 载荷、发送到 Slack。消息出现在你的频道里,带有平台标签、发送者、时间戳和完整消息内容。如果签名验证失败返回 401——检查 UNIFYPORT_SIGNING_SECRET 是否与 Webhook 端点上设置的值一致。
架构收益:加 WhatsApp 不用改代码
到这里,这个中继跟 Bot API 方案的差距就显现了。在同一个工作区连接一个 WhatsApp 账号,订阅同一个 Webhook 端点。WhatsApp 消息到达时,结构完全一样:
{
"event": "message.received",
"account_id": "acct_3Xk1wL",
"provider": "whatsapp",
"from": "user_9b4e7a",
"text": "发票已发,请确认收到",
"timestamp": 1750521660,
"message_id": "wa_msg_8d2c5f"
}
同一个处理函数。同一个 Slack 频道。Slack 里 [whatsapp] 标签区别于 [telegram],但代码路径完全相同。如果是 Telegram Bot API 方案,你还需要单独做一套集成——不同的 Webhook 载荷、不同的解析逻辑、不同的错误处理。而这个中继用同样的方式处理 LINE、TikTok、Zalo 和 X,因为事件结构在不同平台之间不会变化。
这就是对接单一平台 API 和对接归一化层的区别。Bot API 路径把你的中继绑死在 Telegram 上。而 UnifyPort 的 Webhook 文档给 Cursor(或任何 AI 编码工具)提供了一个足够简洁的接口,让它一次就能写对——写出来的代码,你后续连接的每个渠道都能直接用。把 UnifyPort API 文档 喂给你的工具,贴入 message.received 的结构定义,午饭前中继就能跑起来。