最近做的企微机器人,光发文字不够——客户咨询产品要发图文卡片带"查看详情"按钮,处理工单要发卡片带"已解决""未解决"按钮让客户点。这篇重点讲怎么用企微接口发交互卡片、怎么接收按钮点击回调、怎么处理回调后的业务逻辑。

底层用的是 Eyun 平台开放的企微 API,统一 POST+JSON,鉴权用 App Token 加 appid,响应封套 {code, data, detail, message, time},code 为 0 成功。

发图文卡片:sendRichText

图文卡片用消息模块的 sendRichText 接口。卡片有标题、描述、图片、跳转链接,还能带按钮:

import requests

BASE = "https://api.eyun.com"
HEADERS = {"Authorization": "Bearer eyk_xxxx", "Content-Type": "application/json"}

def send_product_card(appid, to_uin, product):
    card = {
        "title": product["name"],
        "description": f"价格:{product['price']}元\n{product['desc'][:50]}",
        "imageUrl": product["image"],
        "url": f"https://shop.example.com/product/{product['id']}",
        "buttons": [
            {"text": "查看详情", "url": f"https://shop.example.com/product/{product['id']}"},
            {"text": "立即咨询", "action": "consult", "data": {"product_id": product["id"]}}
        ]
    }
    resp = requests.post(
        f"{BASE}/wx-api/api/message/sendRichText",
        headers=HEADERS,
        json={"appid": appid, "to": to_uin, "cards": [card]}
    )
    return resp.json()["code"] == 0

buttons 里的按钮分两类:跳转链接按钮(url)和回调按钮(action + data)。回调按钮点击后会触发 Webhook 事件。

接收按钮回调:card.action.trigger

客户点回调按钮后,企微通过 Webhook 回调,事件类型是 card.action.trigger:

from flask import Flask, request

app = Flask(__name__)

@app.route("/wx-api/webhook/", methods=["POST"])
def webhook():
    event = request.headers.get("X-Eyun-Event")
    payload = request.json["data"]
    
    if event == "card.action.trigger":
        appid = payload["appid"]
        from_uin = payload["fromUin"]
        action = payload["action"]        # 按钮的 action 字段
        data = payload.get("data", {})    # 按钮的 data 字段
        handle_card_action(appid, from_uin, action, data)
    
    return "ok"

action 标识是哪个按钮被点了,data 是按钮带的业务数据(如 product_id)。根据 action 走不同业务逻辑。

处理按钮回调:按 action 分发

def handle_card_action(appid, from_uin, action, data):
    if action == "consult":
        # 客户点了"立即咨询",建咨询工单
        ticket_id = crm_api.create_consult(
            customer_uin=from_uin,
            product_id=data["product_id"]
        )
        # 回发消息确认
        send_text(appid, from_uin, f"已为您创建咨询工单 {ticket_id},客服稍后联系")
    
    elif action == "resolve":
        # 客户点了"已解决",关单
        ticket_id = data["ticket_id"]
        crm_api.close_ticket(ticket_id)
        send_text(appid, from_uin, "感谢反馈,工单已关闭")
    
    elif action == "unresolved":
        # 客户点了"未解决",升级
        ticket_id = data["ticket_id"]
        crm_api.escalate_ticket(ticket_id)
        send_text(appid, from_uin, "已为您升级工单,主管会尽快联系")

每个 action 对应一段业务逻辑。回调里的 data 是发卡片时带的,所以能知道是哪个产品、哪个工单。

发工单卡片:带解决/未解决按钮

客服处理完工单后,发卡片让客户确认:

def send_ticket_card(appid, to_uin, ticket_id):
    card = {
        "title": f"工单 {ticket_id} 处理完成",
        "description": "请问问题是否已解决?",
        "buttons": [
            {"text": "已解决", "action": "resolve", "data": {"ticket_id": ticket_id}},
            {"text": "未解决", "action": "unresolved", "data": {"ticket_id": ticket_id}}
        ]
    }
    requests.post(
        f"{BASE}/wx-api/api/message/sendRichText",
        headers=HEADERS,
        json={"appid": appid, "to": to_uin, "cards": [card]}
    )

客户点按钮,回调到 handle_card_action,根据 action 关单或升级。闭环完成。

卡片的状态更新

卡片发出去后,业务状态变了(工单关了),卡片要不要更新?企微卡片不支持动态更新,只能重新发一张新卡片,旧卡片还在聊天记录里。处理方式:

  • 新卡片带"此工单已关闭"提示

  • 旧卡片的按钮点了也返回"工单已处理"

不追求旧卡片更新,客户看最新的卡片就行。

按钮回调的幂等

客户可能连点几下按钮,回调会触发多次。回调处理要幂等:

def handle_card_action(appid, from_uin, action, data):
    # 用 action + data 做幂等键
    idem_key = f"card_action:{action}:{data.get('ticket_id', '')}"
    if not redis.set(idem_key, "1", nx=True, ex=3600):
        return  # 重复回调,跳过
    # 处理业务...

不做幂等,客户连点"已解决",工单被关多次(虽然关多次没副作用,但其他 action 可能有)。

写在最后

交互消息这套东西,本质是用好企微的卡片接口——sendRichText 发带按钮的卡片、Webhook 收 card.action.trigger 回调、按 action 分发处理业务逻辑。把卡片和回调串起来,机器人不只是发文字,还能和客户交互。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐