← 所有文章
教學

我畀 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": "Invoice 已寄出,請確認收到",
  "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 嘅結構定義,食晏之前中繼就跑得起。