ZeroClaw飞书通道(Lark/Feishu)接入技术文档

4 阅读2分钟

ZeroClaw对于飞书通道的实现非常成熟,适合生产环境的企业级聊天机器人接入。
它整合了多种协议(WebSocket 长链接 + Webhook 事件)和媒体处理(图像、文件、音频、转写)逻辑,极大降低了业务方额外开发成本。下面一起看下整个通信过程。

1. 前置准备:身份认证 + Token 管理

sequenceDiagram
    participant Bot as ZeroClaw(LarkChannel)
    participant Lark as 飞书API

    Note right of Bot: 构造通道对象
    Bot->>Lark: POST /auth/v3/tenant_access_token/internal
      note right of Lark: body:{app_id,app_secret}
    Lark-->>Bot: {code:0, tenant_access_token:"xxx", expire:7200}

    alt callback /token失效
      Bot->>Lark: retry get token
    end

    Bot->>Bot: compute refresh_after = now + ttl - 120s
    Bot->>Bot: cache tenant_token

细节说明

  • LarkChannel::from_config / from_feishu_config 初始化 platform, mention_only, allowed_users
  • get_tenant_access_token()
    • 先读 tenant_tokenArc<RwLock<Option<CachedTenantToken>>
    • 若过期或不存在调用 /auth/v3/tenant_access_token/internal
  • should_refresh_lark_tenant_token(status, body)
    • HTTP 401 或 code == 99991663 表示 token 失效。
  • invalidate_token() 清缓存强制下一次刷新。
  • ensure_bot_open_id()
    • 为群组 @ 逻辑准备,通过 /bot/v3/infobot.open_id(存 resolved_bot_open_id)。

2. 消息监听与接收(WS 长连接)

sequenceDiagram
    participant Bot as ZeroClaw(LarkChannel)
    participant Lark as 飞书WS
    participant Parser as parse_event_payload(_async)

    Bot->>Lark: POST /callback/ws/endpoint (AppID/AppSecret)
    Lark-->>Bot: {URL:wss://..., ClientConfig:{PingInterval:120}}

    Bot->>Lark: WebSocket connect + initial ping(PbFrame method=0)
    Lark-->>Bot: pong + 可选 ping_interval

    loop 长连接
      Bot->>Lark: 定时 ping (heartbeat)
      Lark-->>Bot: pong / 数据帧
      Bot->>Bot: 解析 PbFrame
      Bot->>Bot: 向 Lark 发送 ACK (code=200)
      Bot->>Parser: event payload
      Parser-->>Bot: ChannelMessage
    end

细节说明

  • LarkChannel::listen_ws(tx)
    • get_ws_endpoint() 获取 wss_url
    • tokio_tungstenite::connect_async 返回 ws_stream
    • ws_stream.split()write / read
    • 心跳:
      • ping_interval = client_config.ping_interval.unwrap_or(120).max(10)
      • WS_HEARTBEAT_TIMEOUT = 300s
  • 二进制帧解析:
    • PbFrame(protobuf)
    • method=0 控制(ping/pong)
    • method=1 数据(event)
  • 事务型:
    • 立即 ACK (code=200) 避免 Feishu 断连;
    • 分片重组:message_id, sum, seq
    • 去重:ws_seen_ids 30min。

3. 消息处理

sequenceDiagram
    participant Parser as parse_event_payload(_async)
    participant Bot as ZeroClaw(LarkChannel)
    participant Storage as TranscriptionManager (可选)

    User->>Lark: im.message.receive_v1 event
    Lark-->>Parser: payload

    Parser->>Bot: check header.event_type
    Parser-->Bot: 否则退出

    Bot->>Parser: load sender.open_id
    Parser->>Bot: is_user_allowed?

    alt audio
      Parser->>Storage: download_audio_resource + transcribe
      Storage-->>Parser: text
    else other
      Parser->>Parser: text/post/image/file分支
      alt image
        Parser->>Bot: download_image_as_marker
      else file
        Parser->>Bot: download_file_as_content
      end
    end

    Parser->>Bot: ChannelMessage
    Bot->>Bot: 处理 mention_only/group @ 检查
    Bot->>Bot: 发送 ACK reaction(try_add_ack_reaction)
    Bot->>Bot: tx.send(ChannelMessage)

细节说明

3.1 类型分支

  • text:从content JSON 取 text
  • postparse_post_content_details 归一为纯文本 + 提取 mentioned_open_ids
  • image
    • image_key
    • image_download_url(image_key) 下载
    • lark_detect_image_mime 验证
    • 返回 [IMAGE:data:{mime};base64,xxx]
  • file
    • file_key + file_name
    • file_download_url(...) 下载
    • 512KiB 限制
    • 文本型(txt/json/xml/yaml/js/csv 或后缀)
    • 二进制 fallback 标识
  • audioparse_event_payload_async 处理)
    • 需要transcription_manager启用
    • download_audio_resource + stream_audio_bytes
    • 转写函数 manager.transcribe(&audio_data, filename)
    • 结果作为消息内容

3.2 过滤逻辑

  • is_user_allowed(open_id)
    • 白名单 allowed_users*
  • mention_only + group
    • should_respond_in_group(...)
    • 依赖 bot_open_idmentionspost_mentioned_open_ids
  • strip_at_placeholders 去除 _user_N
  • 空消息、无效类型直接跳过

4. 消息回复 send

sequenceDiagram
    participant Bot as ZeroClaw(LarkChannel)
    participant Lark as 飞书API

    Bot->>Lark: POST /im/v1/messages?receive_id_type=chat_id
      note right of Bot: header Bearer token
    Lark-->>Bot: {code:0}
    alt token失效
      Bot->>Bot: invalidate_token()
      Bot->>Lark: 重发
    end
    Bot-->>Bot: ensure_lark_send_success

细节说明

  • impl Channel for LarkChannel::send
    1. 取 token
    2. url = self.send_message_url()
    3. 文本分页:split_markdown_chunks(message.content, 28000)
    4. build_interactive_card_body(recipient, text)
      • {"receive_id":recipient,"msg_type":"interactive","content":card_json}
    5. send_text_once(url, token, body)
    6. ensure_lark_send_success
      • HTTP 成功 + code==0
  • 令牌过期策略:
    • should_refresh_lark_tenant_token(status,response),invalidate + 重试一次
    • 重试后仍失效则报错
  • health_check(): 仅 get_tenant_access_token().await.is_ok()