← 所有文章
教程

我让 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 服务,能做到:

  1. 接收 UnifyPort 统一 Webhook 推送的 message.received 事件
  2. 用 HMAC-SHA256 和你的 signing_secret 验证每条投递的签名
  3. 把每条消息格式化为带平台标签、发送者和时间戳的 Slack Block Kit 消息
  4. 通过 Incoming Webhook 发送到 Slack 频道

用时:约 30 分钟。你需要一个 UnifyPort 工作区,已通过二维码扫码连接 Telegram 账号(不用注册机器人),以及一个 Slack Incoming Webhook URL。

为什么不直接用 Bot API?

先看看走 Telegram Bot API 需要做什么:

  1. 通过 @BotFather 创建机器人 —— 拿到一个 bot token,但机器人是独立于你个人账号的实体
  2. 选择消息接收方式getUpdates 轮询或 Webhook —— 两种都需要 Telegram 专属配置
  3. 解析 Update 对象:消息正文在 update.message.text,发送者在 update.message.from.id,会话在 update.message.chat.id —— Telegram 独有的数据结构
  4. 处理速率限制:全局 30 条/秒,同一群组 20 条/分钟
  5. 格式化并转发到 Slack

代码能跑——直到你还需要在同一个频道里收 WhatsApp 消息。这时候你得单独接入 WhatsApp Cloud API、解析另一种 Webhook 载荷、维护两套代码路径,最终都输出一条 Slack 消息。三个平台就是三套代码。

UnifyPort 的 Webhook 把这些归一了。每个平台推送同样的 message.received 事件——providerfromtextmessage_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 的结构定义,午饭前中继就能跑起来。