← 所有文章
教學

我讓 Claude Code 搭了一個 X 私訊和提及監聽器——完全沒碰 X 的 API — UnifyPort

X 的 API 沒有免費方案。讀取一則私訊收費 $0.015,讀取一則貼文收費 $0.005。如果你是一個兩人團隊,只想知道有人在 X 上傳了私訊或提及了你,官方開發者平台會要求你先儲值、建立專案、等待審核、開啟計量計費——你連一則訊息都還沒處理。

我跳過了這一切。打開 Claude Code,把 UnifyPort 的 webhook 介面文件貼到上下文裡,讓它用 Node.js 搭建一個 X 私訊和提及監聽器。四十分鐘後,我拿到一個 Express 伺服器,能驗證每次交付的簽章、把事件寫入結構化日誌檔、並將告警推送到 Discord 頻道。不需要 X 開發者帳號,不用按次計費,不用排隊審核。

你最終會得到什麼

一個 Node.js Express 伺服器:

  1. 接收 UnifyPort 統一 webhook 的 message.received 事件
  2. 使用 HMAC-SHA256 和 signing_secret 驗證每次交付的簽章
  3. 把每則 X 訊息寫入 messages.jsonl 檔案
  4. 透過 webhook 向 Discord 頻道發送告警

耗時:不到一小時。你需要一個 UnifyPort 工作區,透過 UnifyPort Exporter 瀏覽器擴充功能連接 X 帳號——一鍵工作階段匯入,無需開發者憑證——以及一個 Discord webhook URL。

準備工作:把文件餵給 Claude Code

在提示之前,先把 API 文件送入代理的上下文。Claude Code 能直接讀檔案,所以在專案根目錄建立 unifyport-reference.md,包含:

  • message.received 事件酬載結構
  • x-unifyport-signature 標頭和 HMAC-SHA256 簽章驗證說明
  • POST /v1/webhook-endpoints 建立呼叫

也可以把這些內容直接貼到對話中。關鍵點:真實的欄位名稱輸入,正確的程式碼輸出。沒有文件,代理會編造一個 X 專用 SDK 或不存在的 dm.received 事件類型。

Cursor、Windsurf、Copilot 用法相同——透過 @Docs、文件面板或直接貼文件到上下文。工具是次要的,文件是核心。

建構過程:逐條提示

第一個提示——骨架:

讀取 unifyport-reference.md。用 Express 寫一個 server.js,包含 POST /webhook 路由。當 event 是 “message.received” 時,印出 provider、from 和 text 欄位。所有請求回傳 200。

Claude Code 輸出:

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhook", (req, res) => {
  const evt = req.body;
  if (evt.event === "message.received") {
    console.log(`[${evt.provider}] ${evt.from}: ${evt.text}`);
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

六行處理器。因為事件結構在六個平台上完全一致,不需要任何 X 專用的解析——evt.provider 顯示 "x",但結構和 WhatsApp 或 Telegram 訊息完全相同。

第二個提示——簽章驗證:

每次交付都包含 x-unifyport-signature 標頭。用環境變數中的 signing_secret 進行 HMAC-SHA256 驗證。HMAC 必須對原始請求本體計算——不能對重新序列化的 JSON 計算。驗證失敗回傳 401。使用時間安全比較。

Claude Code 加上驗證層:

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——如果代理用了 req.body 再序列化成 JSON,HMAC 永遠不會匹配。因為提示指定了「原始請求本體」,代理一次就寫對了。

第三個提示——日誌記錄和 Discord 告警:

新增兩個功能:(1) 把每個 message.received 事件附加到 messages.jsonl——每行一個 JSON 物件,包含 timestamp、provider、from、text 和 message_id。(2) 向環境變數 DISCORD_WEBHOOK_URL 指定的 Discord webhook 發送一行告警。

完整的 server.js

import express from "express";
import crypto from "crypto";
import { appendFileSync } from "fs";

const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const DISCORD_WEBHOOK = process.env.DISCORD_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") {
    const record = JSON.stringify({
      timestamp: evt.timestamp,
      provider: evt.provider,
      from: evt.from,
      text: evt.text,
      message_id: evt.message_id,
    });
    appendFileSync("messages.jsonl", record + "\n");

    if (DISCORD_WEBHOOK) {
      await fetch(DISCORD_WEBHOOK, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          content: `**[${evt.provider}]** ${evt.from}: ${evt.text}`,
        }),
      });
    }
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

不到 50 行。簽章驗證、磁碟日誌、Discord 轉發全部就位。處理器不會對 provider 做分支判斷——事件結構相同,不管訊息來自 X、WhatsApp 還是其他平台。

執行並看到 X 訊息到達

在 UnifyPort 控制台連接你的 X 帳號。X 使用 UnifyPort Exporter 瀏覽器擴充功能進行工作階段匯入——點擊擴充功能圖示、授權,幾秒鐘內帳號就連接好了。不需要開發者後台,不需要申請 API 金鑰,不需要審核佇列。

註冊一個 webhook 端點指向你的伺服器,設定 subscribed_events: ["message.received"]signing_secret。啟動伺服器,然後從另一個帳號給你的 X 帳號發一則測試私訊。事件到達:

{
  "event": "message.received",
  "account_id": "acct_7Kp3mR",
  "provider": "x",
  "from": "user_9d4f2b",
  "text": "Hey, are you still taking freelance projects?",
  "timestamp": 1750521600,
  "message_id": "x_msg_8c1e5a"
}

你的伺服器驗證簽章、把紀錄附加到 messages.jsonl、並將告警發送到 Discord。如果簽章驗證失敗——401——檢查 UNIFYPORT_SIGNING_SECRET 是否與 webhook 端點上設定的值一致。

擴展:零程式碼改動新增 Telegram

在同一個工作區連接 Telegram 帳號,訂閱同一個 webhook 端點。Telegram 訊息以完全相同的結構到達——不同的 provider 欄位,結構一模一樣:

{
  "event": "message.received",
  "account_id": "acct_2Xn5wL",
  "provider": "telegram",
  "from": "user_4a7c8d",
  "text": "Can you hop on a call tomorrow?",
  "timestamp": 1750521660,
  "message_id": "tg_msg_3b9f1e"
}

不需要新的處理器,不需要 Telegram 專用的解析邏輯。messages.jsonl 檔案現在同時包含 X 和 Telegram 事件,使用相同的 schema,Discord 顯示兩個來源的告警。後續新增 WhatsApp、LINE、Zalo 或 TikTok 也是同樣的流程——連接帳號,現有的處理器自動處理一切。

這和直接對接 X API 的差別不僅是成本——而是準入門檻。X 要求通過審核的開發者帳號、儲值的專案和按次計費的承諾。UnifyPort 的工作階段匯入路徑透過瀏覽器連接你的個人 X 帳號,把每則私訊和提及正規化為與其他五個平台完全相同的事件結構。把 Claude Code 指向這個介面——一個事件 schema、一個簽章機制——你就能在午餐前跑起一個帶驗證、日誌、告警的監聽器。然後新增其他所有平台,程式碼一行不改。