← 所有文章
教程

我把 API 文档丢给 Claude Code,它帮我搭了一个 Zalo Webhook 接收器 — UnifyPort

Zalo 没有公开的私信 API。如果你的业务面向越南市场——或者任何需要和越南客户沟通的出海团队——这是你遇到的第一堵墙。官方路径是注册 Official Account,但那是一套广播体系:认证审核、消息模板、互动窗口期、按条计费。如果你只需要接收客户主动发来的消息,OA 解决的是一个比你实际需求大得多的问题。

所以我换了个思路。打开 Claude Code,把 UnifyPort 的 webhook 参考文档贴进上下文,让它用 Python 写一个 Zalo webhook 接收器。五十分钟后,我拿到了一个 Flask 服务:验证每次推送的签名、把 Zalo 消息写入结构化 JSON 文件、同时转发到 Slack 频道。整个代码大约 60 行。

你最终会得到什么

一个 Python Flask 服务,功能包括:

  1. 接收 UnifyPort webhook 推送的 message.received 事件
  2. 用 HMAC-SHA256 和你的 signing_secret 验证每次推送的签名
  3. 把每条 Zalo 消息写入结构化 JSON 文件
  4. 通过 Slack incoming webhook 把消息摘要推送到 Slack 频道

时间成本:不到一小时,大部分是 Claude Code 在干活。你需要一个 UnifyPort 工作空间,里面连接一个 Zalo 账号(扫二维码即可——不需要提供密码,不需要企业认证),以及一个 Slack incoming webhook URL。

准备工作:把文档放进 Claude Code 的上下文

动手之前,先把 API 文档喂给 agent。Claude Code 可以直接读取本地文件,最快的方式是:

  1. 把 webhook 事件参考文档——特别是 message.received 事件结构、签名验证部分、以及 POST /v1/webhook-endpoints 创建接口——复制到项目根目录的 unifyport-reference.md 文件里。
  2. 或者在开始对话时,直接把相关内容粘贴进聊天窗口。

Claude Code 必须看到的关键内容:message.received 事件的 payload 结构、x-unifyport-signature 请求头、以及 HMAC-SHA256 配合 signing_secret 的验证方式。少了这些,agent 会猜字段名——而且一定猜错。

其他 AI 编程工具同理。在 Cursor 里用 @Docs 指向参考文档,在 Windsurf 里用文档面板。核心逻辑一样:喂进去真实的字段名,输出正确的代码。

逐步构建,一个 prompt 一个 prompt 来

第一个 prompt——骨架:

读 unifyport-reference.md。用 Flask 写一个 app.py,包含一个 POST /webhook 路由。当 event 字段是 “message.received” 时,把 provider、from、text 打印到标准输出。所有事件返回 200。

Claude Code 读完参考文档后生成:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
def webhook():
    evt = request.get_json()
    if evt.get("event") == "message.received":
        print(f"[{evt['provider']}] {evt['from']}: {evt['text']}")
    return "", 200

if __name__ == "__main__":
    app.run(port=3000)

七行处理逻辑。因为事件结构是统一的,只需要判断一个字段——不需要针对不同平台做解析。

第二个 prompt——签名验证:

每次 webhook 推送都会带一个 x-unifyport-signature 请求头。用环境变量里的 signing_secret,通过 HMAC-SHA256 验证签名。HMAC 必须对原始请求体计算,不是重新序列化的 JSON。验证失败返回 401。使用常数时间比较。

这一步把玩具变成了可以上线运行的东西。Claude Code 之所以能正确实现,是因为它读过参考文档:

import hmac
import hashlib
import os

SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]

def verify_signature(req):
    signature = req.headers.get("x-unifyport-signature", "")
    expected = hmac.new(
        SIGNING_SECRET.encode(),
        req.get_data(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

req.get_data() 返回的是 Flask 解析 JSON 之前的原始字节——这个细节至关重要。如果 agent 用了 request.get_json() 然后重新序列化,HMAC 永远对不上。提示词里写了”原始请求体”,它第一次就写对了。

第三个 prompt——结构化日志和 Slack 转发:

加两个功能:(1) 把每个 message.received 事件追加写入 messages.jsonl 文件,每行一个 JSON 对象,包含 timestamp、provider、from、text。(2) 把一行摘要发到环境变量 SLACK_WEBHOOK_URL 指定的 Slack incoming webhook。

Claude Code 一次搞定。完整的 app.py

import hmac
import hashlib
import json
import os
import requests
from flask import Flask, request

app = Flask(__name__)
SIGNING_SECRET = os.environ["UNIFYPORT_SIGNING_SECRET"]
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL", "")

def verify_signature(req):
    signature = req.headers.get("x-unifyport-signature", "")
    expected = hmac.new(
        SIGNING_SECRET.encode(),
        req.get_data(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route("/webhook", methods=["POST"])
def webhook():
    if not verify_signature(request):
        return "", 401

    evt = request.get_json()
    if evt.get("event") == "message.received":
        record = {
            "timestamp": evt["timestamp"],
            "provider": evt["provider"],
            "from": evt["from"],
            "text": evt.get("text", ""),
            "message_id": evt["message_id"],
        }
        with open("messages.jsonl", "a") as f:
            f.write(json.dumps(record) + "\n")

        if SLACK_WEBHOOK_URL:
            requests.post(SLACK_WEBHOOK_URL, json={
                "text": f"[{evt['provider']}] {evt['from']}: {evt.get('text', '')}"
            })

    return "", 200

if __name__ == "__main__":
    app.run(port=3000)

六十行代码,签名验证、结构化日志、Slack 转发全部就位。整个 handler 是平台无关的——evt["provider"] 告诉你这是 Zalo 消息,但代码本身从不判断平台。

启动服务,看一条 Zalo 消息进来

在 UnifyPort 工作空间里连接一个 Zalo 账号——Zalo 用扫码认证,用 Zalo App 扫一下就连上了,几秒钟的事。不需要企业认证,不需要注册 OA,不需要管理任何密码。注册一个 webhook 端点,设置 subscribed_events: ["message.received"] 和一个 signing_secret

启动服务,然后从一个 Zalo 联系人那里发一条消息。事件推送长这样:

{
  "event": "message.received",
  "account_id": "acct_4Xn9kL",
  "provider": "zalo",
  "from": "user_7b2e4f",
  "text": "Sản phẩm này còn hàng không ạ?",
  "timestamp": 1750262400,
  "message_id": "zalo_msg_9d3a1c"
}

服务验证签名、把记录追加到 messages.jsonl、把摘要发到 Slack。如果签名验证失败返回 401——检查 UNIFYPORT_SIGNING_SECRET 是否和 webhook 端点设置的一致。

扩展:加上 WhatsApp,不改一行 handler 代码

统一 webhook 的价值在这里体现。在同一个工作空间里连接一个 WhatsApp 账号,订阅同一个 webhook 端点,完全相同的 /webhook 路由就能处理两个平台。WhatsApp 消息以相同的 message.received 结构到达——provider 字段不同,结构完全一样:

{
  "event": "message.received",
  "account_id": "acct_2Rm7wQ",
  "provider": "whatsapp",
  "from": "user_5c8d3a",
  "text": "Do you ship to Thailand?",
  "timestamp": 1750262460,
  "message_id": "wa_msg_6f1b2e"
}

不需要新的 handler。不需要条件导入。不需要 WhatsApp 专属的解析逻辑。messages.jsonl 文件里现在同时包含 Zalo 和 WhatsApp 消息,格式完全一样,Slack 频道里两个平台的消息同步显示。之后加 LINE 或 Telegram 也是一样——连接账号,已有的 handler 全部搞定。

重点不是 Claude Code 替你写了服务器。重点是,当 API 接口简洁到可以完整放进 agent 上下文时,AI 编程工具就能一次做对。一种事件结构、一套签名机制、一个发送端点——这种目标,给了 prompt 的 agent 不会搞砸。把 UnifyPort API 参考文档喂给你的工具,给它 message.received 的 schema,午饭前你就能拿到一个签名验证、日志记录、消息转发的 webhook 接收器——然后不改代码就能接入其他所有渠道