← 所有文章
教學

我讓 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 工作區,已透過 QR Code 掃碼連結 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 支援 QR Code 認證,用 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 的結構定義,午餐前中繼就能跑起來。