<!-- source: https://deepwiki.com/qhkm/zeptoclaw/7.7-lark-integration -->

# Lark Integration

- src/channels/discord.rs
- src/channels/lark.rs
- src/channels/slack.rs
- src/channels/webhook.rs
- src/channels/whatsapp_cloud.rs
- src/providers/gemini.rs

This page documents the Lark/Feishu messaging channel implementation in ZeptoClaw. It covers the WebSocket long-connection protocol, protobuf frame handling, tenant token management, and message parsing for both Lark (international) and Feishu (China mainland).

For channel registration and lifecycle management, see [Channel Architecture](/qhkm/zeptoclaw/7.1-channel-architecture). For configuration validation, see [Configuration Basics](/qhkm/zeptoclaw/2.3-configuration-basics).

## Overview

The Lark channel (`LarkChannel`) [src/channels/lark.rs361](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L361-L361) provides bidirectional messaging integration with Lark (international) and Feishu (China) workspaces without requiring a publicly accessible HTTPS endpoint. It uses Lark's WebSocket long-connection protocol with `pbbp2` protobuf frames for inbound events and the REST API for outbound messages.

**Key characteristics:**

- No webhook endpoint required : Events arrive via persistent WebSocket connection src/channels/lark.rs 3-4
- Dual-region support : Configurable API base URLs for Lark vs. Feishu src/channels/lark.rs 8-11
- Protobuf framing : Binary pbbp2 protocol for efficient event delivery src/channels/lark.rs 13-20
- Tenant token caching : Proactive refresh to avoid mid-conversation auth failures src/channels/lark.rs 56-57
- Rich text parsing : Converts Lark post format to plain text src/channels/lark.rs 191-255
- Message deduplication : Prevents reprocessing on reconnect using a 30-minute sliding window src/channels/lark.rs 672-682

**Sources:** [src/channels/lark.rs1-41](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L1-L41) [src/channels/lark.rs361](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L361-L361) [src/channels/lark.rs672-682](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L672-L682)

## Architecture

```

```

**Diagram: LarkChannel component structure and API interactions**

The `LarkChannel` struct maintains a cached tenant access token with refresh metadata, a WebSocket message ID deduplication set (`ws_seen_ids`), and references to the shared `MessageBus` [src/channels/lark.rs361-415](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L361-L415) Inbound events flow through protobuf frame parsing and deduplication before publication to the bus [src/channels/lark.rs802-807](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L802-L807) Outbound messages trigger tenant token refresh if the API returns an invalid token error [src/channels/lark.rs908-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L908-L921)

**Sources:** [src/channels/lark.rs361-415](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L361-L415) [src/channels/lark.rs286-356](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L286-L356) [src/channels/lark.rs802-807](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L802-L807) [src/channels/lark.rs908-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L908-L921)

## Configuration

The `LarkConfig` struct specifies app credentials, region selection, and access control.

**Region-specific API endpoints:**

The `api_base()` and `ws_base()` methods select the correct endpoint based on the `feishu` flag in `LarkConfig` [src/channels/lark.rs422-438](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L422-L438)

**Sources:** [src/channels/lark.rs48-51](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L48-L51) [src/channels/lark.rs422-438](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L422-L438)

## pbbp2 Protocol

Lark uses a custom protobuf framing protocol (`pbbp2.proto`) over WebSocket. All binary frames are `PbFrame` messages [src/channels/lark.rs89-102](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L89-L102):

```

```

**Frame types** (determined by `method` field):

- method = 0 : CONTROL (ping/pong for heartbeat) src/channels/lark.rs 16
- method = 1 : DATA (event payload in JSON format) src/channels/lark.rs 17

**Heartbeat mechanism:**

The client sends periodic ping frames with `headers = [{"key": "type", "value": "ping"}]` [src/channels/lark.rs603-614](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L603-L614) The server responds with pong frames containing `WsClientConfig` JSON in the payload, which specifies the `ping_interval` [src/channels/lark.rs119-124](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L119-L124)

If no binary frame arrives within `WS_HEARTBEAT_TIMEOUT` (300s) the connection is considered dead and reconnection is triggered [src/channels/lark.rs54](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L54-L54) [src/channels/lark.rs622-627](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L622-L627)

**Sources:** [src/channels/lark.rs89-102](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L89-L102) [src/channels/lark.rs54](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L54-L54) [src/channels/lark.rs603-614](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L603-L614) [src/channels/lark.rs119-124](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L119-L124) [src/channels/lark.rs622-627](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L622-L627)

## Message Flow

```

```

**Diagram: Inbound message flow from WebSocket to AgentLoop**

**Inbound processing steps:**

1. WebSocket receive : Binary WsMsg::Binary arrives src/channels/lark.rs 631-635
2. Protobuf decode : PbFrame::decode(bytes) src/channels/lark.rs 640-642
3. Payload extraction : JSON string from frame.payload src/channels/lark.rs 653-659
4. Deduplication : Check message_id in ws_seen_ids src/channels/lark.rs 672-682
5. Event parsing : For im.message.receive_v1 , call parse_message_event src/channels/lark.rs 694-710
6. Allowlist check : is_sender_allowed(sender_open_id) src/channels/lark.rs 444-456
7. Group mention check : should_respond_in_group(mentions) for group chats src/channels/lark.rs 282-284

**Outbound processing:**

Outbound messages are sent via `send()` which calls `send_text_once` [src/channels/lark.rs878-898](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L878-L898) If the API returns `401 Unauthorized` or business code `99991663` (invalid token), the cached token is invalidated and a fresh one is fetched [src/channels/lark.rs908-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L908-L921)

**Sources:** [src/channels/lark.rs631-710](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L631-L710) [src/channels/lark.rs444-456](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L444-L456) [src/channels/lark.rs282-284](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L282-L284) [src/channels/lark.rs878-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L878-L921)

## Tenant Token Management

Lark requires a **tenant access token** for all REST API calls. ZeptoClaw implements a read-optimized cache with proactive refresh.

**Token caching strategy:**

The `fetch_tenant_token_cached` helper [src/channels/lark.rs292-306](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L292-L306) checks the cache with a read lock before attempting a network fetch. Tokens are refreshed `TOKEN_REFRESH_SKEW` (120s) before expiry [src/channels/lark.rs169-175](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L169-L175)

**Error-triggered refresh:**

When `send_text_once` receives an error indicating an expired token [src/channels/lark.rs181-183](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L181-L183) it calls `invalidate_token()` [src/channels/lark.rs474-477](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L474-L477) and retries with a fresh token [src/channels/lark.rs908-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L908-L921)

**Sources:** [src/channels/lark.rs292-306](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L292-L306) [src/channels/lark.rs169-175](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L169-L175) [src/channels/lark.rs181-183](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L181-L183) [src/channels/lark.rs474-477](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L474-L477) [src/channels/lark.rs908-921](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L908-L921)

## Message Parsing

### Rich Text (Post Format)

Lark's `post` message type carries rich text content as nested JSON. The `parse_post_content` function flattens this to plain text [src/channels/lark.rs191-255](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L191-L255) supporting:

- Text elements ( tag: "text" )
- Links ( tag: "a" )
- Mentions ( tag: "at" )

### Mention Placeholder Stripping

Feishu injects `@_user_N` placeholders in group chat messages. These are stripped by `strip_at_placeholders` [src/channels/lark.rs258-279](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L258-L279) to prevent the agent from seeing raw Lark internal IDs.

### Image Attachments

Image attachments are identified by `image_keys` in the event payload. The channel fetches the image via the Lark REST API resource endpoint [src/channels/lark.rs770-785](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L770-L785)

**Sources:** [src/channels/lark.rs191-255](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L191-L255) [src/channels/lark.rs258-279](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L258-L279) [src/channels/lark.rs770-785](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L770-L785)

## Reconnection Strategy

The `start()` method spawns a background task running an infinite reconnection loop [src/channels/lark.rs828-875](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L828-L875)

**Backoff parameters:**

- Base delay: BASE_RECONNECT_DELAY_SECS = 2s src/channels/lark.rs 66
- Max delay: MAX_RECONNECT_DELAY_SECS = 60s src/channels/lark.rs 69

**Reconnection triggers:**

1. Heartbeat timeout : No frame for 300s src/channels/lark.rs 622-627
2. Ping send failure : src/channels/lark.rs 611-614
3. Stream end or error : src/channels/lark.rs 809-817

**Sources:** [src/channels/lark.rs828-875](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L828-L875) [src/channels/lark.rs66](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L66-L66) [src/channels/lark.rs69](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L69-L69) [src/channels/lark.rs622-627](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L622-L627)

## Comparison with Other Channels

**Key differentiators:**

- Lark is the only channel using protobuf ( PbFrame ) src/channels/lark.rs 89-102
- Discord uses the standard Gateway v10 API with intents src/channels/discord.rs 45-61
- Slack uses Socket Mode envelopes requiring manual envelope_id acknowledgement src/channels/slack.rs 206-244

**Sources:** [src/channels/lark.rs89-102](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/lark.rs#L89-L102) [src/channels/discord.rs45-61](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/discord.rs#L45-L61) [src/channels/slack.rs206-244](https://github.com/qhkm/zeptoclaw/blob/2c743d31/src/channels/slack.rs#L206-L244)
