# Lark guide

> Bring your own Lark (open.larksuite.com) or Feishu (open.feishu.cn) custom or App Store app. Configure its event URL and connect a direct chat or group. App Store bots also require an enabled installation in that organization.

## Prerequisites

Use a **custom or App Store app** in the [Lark Developer Console](https://open.larksuite.com/app) (or the Feishu console) with the **Bot** capability enabled. Grant the permissions below; each item explains what stops working without that permission:

- `im:message`, `im:message.p2p_msg:readonly` — the bot receives nothing in a direct chat.
- `im:message.group_at_msg:readonly` — the bot receives nothing when mentioned in a group.
- `im:message.group_msg` — ordinary group follow-ups without a bot mention are not delivered; requires administrator approval.
- `im:message:send_as_bot` — the bot cannot reply.
- `im:message:update` — a streamed reply cannot be revised — it arrives as one block at the end.
- `im:message:recall` — a reply that gets shorter leaves its stale tail behind.
- `im:message.reactions:write_only` or `im:message` — add and remove working markers (OnIt → DONE).
- `im:message.reactions:read` or `im:message:readonly` — recover interrupted removals when the marker’s handle is known. Normal confirmed add/remove works with write permission alone; uncertain operations may require administrator recovery, and an uncertain removal blocks re-adding that same marker even after cleanup.

Automated status reactions belong to their current turn. An explicit reaction request that collides with one returns `409 reaction_owner_conflict`; choose a different emoji. The service removes the previous turn’s status before starting the next one, and leaves explicitly added reactions alone. Finishing a Lark turn requires the matching `marker_ref` from its reply instructions; a reply without that reference still sends, but does not clear the status.

  Permissions take effect when you publish an app version. Request the all-group-messages scope only when you want group conversations to continue without a mention; it permits receiving messages in every group containing the bot. With mention-only permission, mention the bot on every turn. See the [official SDK permission guidance](https://github.com/larksuite/node-sdk/blob/main/docs/channel.md#common-issues).
- Its **App ID** and **App Secret** (Credentials & Basic Info) and the **Verification Token** from Event Subscriptions. If you set an **Encrypt Key** there, pass it too — deliveries then arrive encrypted and signed.
- An AgentSky API token with write scope.

## Register the channel app

The [developer console](https://platform.agentsky.dev/developer/apps/new?platform=lark) walks the same checklist, or over the API:

```bash
curl -s -X POST https://api.agentsky.dev/v1/channels/apps \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "label": "My Lark app", "credentials": {"app_id": "cli_...", "app_secret": "...", "verification_token": "...", "domain": "lark"}}'
# -> setup: { "events_url": "https://.../webhooks/lark/app_..." }
```

Custom-app registration mints a tenant token and reads the bot's identity — a wrong secret answers `400 lark_error`. The stored credential keys grow `bot_open_id` and `bot_name`. `domain` is `lark` (default) or `feishu`; `encrypt_key` is optional.

**Finish in the console.** Lark has no API for event subscriptions: paste `setup.events_url` as the **Request URL** under Event Subscriptions (Lark verifies it with a challenge immediately), add the **`im.message.receive_v1`** event, then publish an app version so the bot is available to your organization.

## App Store applications

Choose **App Store app** when the bot is installed in multiple organizations and
set `app_mode: store`. Registration returns the events URL; it does not prove
that an organization has installed the app. Send `app_ticket`, `app_open`,
`app_status_change` and `app_uninstalled` events to that same URL, in addition
to `im.message.receive_v1`. Messaging is refused until the tenant is enabled.
Removing or stopping an installation prevents later sends and queued file reads.

To migrate an existing installation, an administrator may register its current
`app_ticket` and adopt the existing conversation with `adopt.tenant_key`.
The service checks the provider's current application and chat state before
binding that organization. A missing, disabled or mismatched installation is
refused; registration alone is never a completed migration.

## Files

Upload a file through the Files API, then send its `file_id` as a
`{type: "file", file_id}` part on the existing thread-messages endpoint.
The file must be visible in the connection's workspace. Lark receives the original
bytes; incoming file/image resources become scoped Files identifiers in the signed
event. Images are limited to 10 MiB each and other files to 30 MiB each; the combined
message attachment budget is 32 MiB. Queued downloads recheck
the current app credentials, so disabled apps cannot retrieve previously queued
resources. If delivery reports a partial or unknown result, inspect the chat
before sending again with a new idempotency key.

## Connect

```bash
curl -s -X POST https://api.agentsky.dev/v1/channels/connections \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "app": "app_...", "destination": {"session": "sess-..."}}'
# -> connect: { "url": "https://applink.larksuite.com/client/bot/open?appId=cli_...", "code": "LINK:…", "app_id": "cli_...", "expires_at": "…" }
```

`connect.url` opens the bot's chat inside Lark; the end user sends `connect.code` there and the chat claims the connection. To connect a **group**, add the bot to the group and send the code in the group with an @mention of the bot. The connection's thread is then `lark:{app}:GROUP:{chat_id}` and the service subscribes that conversation after the claim. With administrator-approved `im:message.group_msg` permission, subsequent messages need no mention; with mention-only permission, every follow-up must mention the bot. Unconnected, unmentioned groups are not routed to an agent. Codes expire after 15 minutes, and a code minted for your app is refused if sent to any other bot. The [hosted connect page](https://agentsky.dev/docs/channels/routing.md#the-hosted-connect-flow) renders this ceremony for you.

## Capabilities

| threads | reactions | markers | proactive | markdown | modals | ephemeral | streaming |
|---|---|---|---|---|---|---|---|
| ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |

Markdown renders as an interactive card (Lark's `markdown` element: bold, italic, links, inline code, code blocks, lists, headings), which is also what lets replies be edited in place. A `raw` part is sent as a plain text message. Working markers use Lark's reaction set — **OnIt** while working, **DONE** on completion, **CrossMark** on failure. Thread ids are `lark:P2P:{chat_id}` / `lark:GROUP:{chat_id}` for the main conversation (with the app id as the second segment on your own app). Private replies carry `lark:P2P:{chat_id}:REPLY:{root_message_id}` for a flat reply or `lark:P2P:{chat_id}:TOPIC:{root_message_id}` for a Lark sub-thread; group replies use the corresponding `lark:GROUP:{chat_id}:REPLY:{root_message_id}` or `lark:GROUP:{chat_id}:TOPIC:{root_message_id}`. Use the matching chat-level channel id (`lark:P2P:{chat_id}` or `lark:GROUP:{chat_id}`) to send to the top-level conversation regardless of which reply-scoped thread id delivered the inbound message.

## Troubleshooting

- **The console refuses the Request URL** — the app was registered with a different verification token than the console shows; rotate the app's credentials (`PATCH /v1/channels/apps/{id}`) with the current token and try again.
- **Nothing arrives from a group** — the bot only receives group messages that @mention it unless the app holds the receive-all-group-messages scope; mention the bot, and check the event subscription includes `im.message.receive_v1`.
- **Replies fail with a permission error** — grant `im:message:send_as_bot` (and the reactions scope for markers) and re-publish the app version; permissions take effect on publish.
- **Encrypted deliveries answer 401** — the `encrypt_key` credential does not match the console's Encrypt Key.
