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