← 所有文章
教程

我让 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、一个签名机制——你就能在午饭前跑起一个带验证、日志、告警的监听器。然后添加其他所有平台,代码一行不改。