我畀 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": "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 嘅結構定義,食晏之前中繼就跑得起。