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_token(Arc<RwLock<Option<CachedTenantToken>>) - 若过期或不存在调用
/auth/v3/tenant_access_token/internal。
- 先读
should_refresh_lark_tenant_token(status, body):- HTTP 401 或
code == 99991663表示 token 失效。
- HTTP 401 或
invalidate_token()清缓存强制下一次刷新。ensure_bot_open_id():- 为群组 @ 逻辑准备,通过
/bot/v3/info得bot.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_ids30min。
- 立即 ACK (
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:从contentJSON 取textpost:parse_post_content_details归一为纯文本 + 提取mentioned_open_idsimage:- 取
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 标识
- 取
audio(parse_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_id、mentions、post_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- 取 token
url = self.send_message_url()- 文本分页:
split_markdown_chunks(message.content, 28000) build_interactive_card_body(recipient, text):{"receive_id":recipient,"msg_type":"interactive","content":card_json}
send_text_once(url, token, body)ensure_lark_send_success:- HTTP 成功 +
code==0
- HTTP 成功 +
- 令牌过期策略:
- 若
should_refresh_lark_tenant_token(status,response),invalidate + 重试一次 - 重试后仍失效则报错
- 若
health_check(): 仅get_tenant_access_token().await.is_ok()