Skip to content

Commit 2933565

Browse files
authored
feat: docs: add Lark / Feishu adapter (#517)
## Summary Adds [`@larksuite/vercel-chat-adapter`](https://www.npmjs.com/package/@larksuite/vercel-chat-adapter), the Lark / Feishu adapter for Chat SDK, as a **vendor-official community adapter**. - **Package**: `@larksuite/vercel-chat-adapter` — published on npm under the official `larksuite` scope - **Built on**: [`@larksuiteoapi/node-sdk`](https://www.npmjs.com/package/@larksuiteoapi/node-sdk)'s `LarkChannel`, the official Lark Node SDK - **Docs source**: external README referenced by `adapters.json` lives in [`larksuite/node-sdk`](https://github.com/larksuite/node-sdk/tree/cbc4adf13cbcb93b389db01faf428e3b3cef053c/docs/vercel-chat-adapter) (the official Lark vendor-owned GitHub org, pinned at commit `cbc4adf1`); the in-tree MDX in this PR is the rendered detail page (`mdxBody: true`) - **Capabilities**: native cardkit typewriter streaming, interactive cards, reactions, edit / delete, message history (via SDK `normalize()`), DM detection, mention handling, and scan-to-create app onboarding through `registerLarkApp` ## Changes | File | Change | |---|---| | `apps/docs/adapters.json` | Add Lark / Feishu entry (`community: true`, `vendorOfficial: true`) | | `apps/docs/content/adapters/vendor-official/lark.mdx` | New hand-authored MDX detail page (frontmatter with full features matrix, install / quick start / configuration / transport / streaming / ID encoding / history / safety / limitations / FeatureSupport) | | `apps/docs/content/adapters/vendor-official/meta.json` | Append `"lark"` to the sidebar `pages` array | | `packages/integration-tests/src/docs-adapters.test.ts` | Append `"lark"` to the hardcoded vendor-official slug list asserted by `Vendor-Official adapter MDX › contains exactly the expected adapters` | No icon registered in `adapters.json` / `iconMap` / `adapterLogos` — matches the existing pattern for vendor-official adapters (Beeper, Resend, Liveblocks, Zernio, Photon). ## Vendor Official tier Per `docs/contributing/building.mdx` (Qualifications for vendor official tier): - ✅ **Commitment for continued maintenance** — owned by the Lark / Feishu team - ✅ **GitHub hosting in official vendor-owned org** — adapter README lives in [`larksuite/node-sdk`](https://github.com/larksuite/node-sdk), the official Lark org - ✅ **Documentation in primary vendor docs** — will be cross-linked from the official Lark Open Platform developer documentation - ✅ **Announcement** — will be announced through Lark developer changelog / channels ## A note on source visibility The adapter source is not currently open-sourced due to internal release-process requirements. What is public: - The npm package itself (consumable by any user) - The README, hosted in `larksuite/node-sdk` (official Lark org) - The underlying [`@larksuiteoapi/node-sdk`](https://github.com/larksuite/node-sdk) on which it is built — this *is* fully open-source ## Test plan - [x] `pnpm --filter docs build` — docs app builds cleanly; `/en/adapters/vendor-official/lark` and `/en/adapters/vendor-official/lark/og` routes are generated - [x] `pnpm typecheck` — passes (33 tasks) - [x] `pnpm check` (Ultracite / Biome) — 438 files, no fixes - [x] `pnpm --filter @chat-adapter/integration-tests test docs-adapters` — 232 tests pass (frontmatter, vendor-official roster, adapters.json ↔ MDX sync) - [x] Manual: `/adapters` lists the Lark / Feishu card in the **Vendor Official** section; `/adapters/vendor-official/lark` renders the MDX detail page with the FeatureSupport matrix
1 parent bd38498 commit 2933565

6 files changed

Lines changed: 245 additions & 1 deletion

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

apps/docs/adapters.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,5 +294,16 @@
294294
"author": "AgentPhone",
295295
"readme": "https://github.com/AgentPhone-AI/chat-sdk-adapter",
296296
"vendorOfficial": true
297+
},
298+
{
299+
"name": "Lark / Feishu",
300+
"slug": "lark",
301+
"type": "platform",
302+
"community": true,
303+
"description": "Lark / Feishu adapter for Chat SDK with native cardkit streaming, interactive cards, and reactions.",
304+
"packageName": "@larksuite/vercel-chat-adapter",
305+
"author": "Lark / Feishu",
306+
"readme": "https://github.com/larksuite/node-sdk/tree/cbc4adf13cbcb93b389db01faf428e3b3cef053c/docs/vercel-chat-adapter",
307+
"vendorOfficial": true
297308
}
298309
]
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
title: Lark / Feishu
3+
description: Chat SDK adapter for Lark / Feishu. WebSocket long-connection event subscription, native cardkit typewriter streaming, interactive cards, and reactions.
4+
packageName: "@larksuite/vercel-chat-adapter"
5+
slug: lark
6+
type: platform
7+
tagline: Chat SDK adapter for Lark / Feishu, built on the official @larksuiteoapi/node-sdk. WebSocket long-connection event delivery, native cardkit streaming, interactive cards, and reactions.
8+
community: true
9+
vendorOfficial: true
10+
author: Lark / Feishu
11+
mdxBody: true
12+
features:
13+
postMessage: yes
14+
editMessage: yes
15+
deleteMessage: yes
16+
fileUploads:
17+
status: partial
18+
label: Via SDK channel.send
19+
streaming:
20+
status: yes
21+
label: Native cardkit typewriter
22+
scheduledMessages: no
23+
cardFormat:
24+
status: yes
25+
label: Lark interactive cards
26+
buttons: yes
27+
linkButtons: yes
28+
selectMenus:
29+
status: yes
30+
label: Card select / overflow
31+
tables:
32+
status: partial
33+
label: Markdown tables
34+
fields:
35+
status: yes
36+
label: Card section fields
37+
imagesInCards:
38+
status: yes
39+
label: ImageElement
40+
modals: no
41+
slashCommands: no
42+
mentions: yes
43+
addReactions: yes
44+
removeReactions: yes
45+
typingIndicator: no
46+
directMessages: yes
47+
ephemeralMessages: no
48+
userLookup: no
49+
customApiEndpoint: no
50+
fetchMessages: yes
51+
fetchSingleMessage: yes
52+
fetchThreadInfo: yes
53+
fetchChannelMessages: yes
54+
listThreads:
55+
status: yes
56+
label: Client-side grouping
57+
fetchChannelInfo: yes
58+
postChannelMessage: no
59+
---
60+
61+
## Install
62+
63+
<PackageInstall package="@larksuite/vercel-chat-adapter" />
64+
65+
## Quick start
66+
67+
```typescript title="lib/bot.ts" lineNumbers
68+
import { Chat } from "chat";
69+
import { createMemoryState } from "@chat-adapter/state-memory";
70+
import { createLarkAdapter } from "@larksuite/vercel-chat-adapter";
71+
72+
const bot = new Chat({
73+
userName: "mybot",
74+
adapters: {
75+
lark: createLarkAdapter(),
76+
},
77+
state: createMemoryState(),
78+
});
79+
80+
bot.onNewMention(async (thread, message) => {
81+
await thread.subscribe();
82+
await thread.post(`You said: ${message.text}`);
83+
});
84+
85+
bot.onDirectMessage(async (thread, message) => {
86+
await thread.post(`Got your DM: ${message.text}`);
87+
});
88+
89+
await bot.initialize();
90+
```
91+
92+
`bot.initialize()` opens the Lark WebSocket connection and keeps it alive until `bot.shutdown()` is called. The process stays alive as long as the WS is open, so no separate server is needed in a long-running environment.
93+
94+
The adapter auto-detects `LARK_APP_ID`, `LARK_APP_SECRET`, and `LARK_BOT_USERNAME` from environment variables when no explicit config is passed.
95+
96+
## Creating a Lark app
97+
98+
### Option A — scan-to-create (recommended)
99+
100+
`registerLarkApp` drives Lark's official scan-to-create flow: the SDK generates a one-time URL, you render it as a QR code, the user scans with the Lark mobile app and approves, and you get back `client_id` / `client_secret` — with the permissions and event subscriptions this adapter needs already configured.
101+
102+
```typescript title="scripts/register-app.ts" lineNumbers
103+
import {
104+
registerLarkApp,
105+
createLarkAdapter,
106+
} from "@larksuite/vercel-chat-adapter";
107+
import qrcode from "qrcode-terminal"; // pnpm add -D qrcode-terminal
108+
109+
const { client_id, client_secret } = await registerLarkApp({
110+
onQRCodeReady: ({ url }) => {
111+
console.log("Scan this QR with your Lark mobile app:");
112+
qrcode.generate(url, { small: true });
113+
},
114+
onStatusChange: ({ status }) => console.log("status:", status),
115+
});
116+
117+
console.log("LARK_APP_ID=", client_id);
118+
console.log("LARK_APP_SECRET=", client_secret);
119+
```
120+
121+
You only need to run this once. Persist the returned credentials and feed them back via `LARK_APP_ID` / `LARK_APP_SECRET` in subsequent runs.
122+
123+
### Option B — create via developer console
124+
125+
Go to the developer console and create an **Intelligent Agent** app:
126+
127+
- Lark: [open.larksuite.com/app](https://open.larksuite.com/app)
128+
- Feishu: [open.feishu.cn/app](https://open.feishu.cn/app)
129+
130+
Grab the app's `client_id` and `client_secret` and pass them as `appId` / `appSecret` (or set `LARK_APP_ID` / `LARK_APP_SECRET`).
131+
132+
## Configuration
133+
134+
<TypeTable
135+
type={{
136+
appId: {
137+
type: "string",
138+
description:
139+
"Lark app ID. Auto-detected from `LARK_APP_ID` when omitted.",
140+
},
141+
appSecret: {
142+
type: "string",
143+
description:
144+
"Lark app secret. Auto-detected from `LARK_APP_SECRET` when omitted.",
145+
},
146+
userName: {
147+
type: "string",
148+
description:
149+
"Bot display name. Defaults to `LARK_BOT_USERNAME` or `\"bot\"`.",
150+
},
151+
logger: {
152+
type: "Logger",
153+
description:
154+
"Chat SDK-compatible logger. Defaults to `ConsoleLogger(\"info\", \"lark\")`.",
155+
},
156+
}}
157+
/>
158+
159+
### Environment variables
160+
161+
| Variable | Description |
162+
| --- | --- |
163+
| `LARK_APP_ID` | Lark app ID. Overridden by `config.appId`. |
164+
| `LARK_APP_SECRET` | Lark app secret. Overridden by `config.appSecret`. |
165+
| `LARK_BOT_USERNAME` | Bot display name. Overridden by `config.userName`. |
166+
167+
## Transport
168+
169+
WebSocket only. `handleWebhook()` returns HTTP 501. Webhook transport is on the roadmap; for now, Lark's "long-connection" mode is the intended delivery channel and works in production.
170+
171+
This means you can run a Lark bot without exposing an HTTP endpoint — the SDK initiates an outbound WebSocket to Lark's servers and receives events through it. Long-running environments (a Node process, a worker, a VM) are the natural fit. Serverless platforms that recycle the process on every request won't keep the connection alive.
172+
173+
## Streaming
174+
175+
`bot.adapter.stream()` uses Lark's native **cardkit typewriter** API. Chunks emitted from your stream handler are appended directly inside a single card message; no `post + edit` polling is involved.
176+
177+
```typescript
178+
await thread.stream(async (controller) => {
179+
for await (const chunk of llmStream) {
180+
controller.write(chunk);
181+
}
182+
});
183+
```
184+
185+
If the thread has a `rootId`, the streamed reply is posted as a thread reply (via the SDK's `replyTo` parameter).
186+
187+
## ID encoding
188+
189+
Lark thread IDs encode as `lark:{chatId}:{rootId}`:
190+
191+
- `chatId``oc_*` for both group and p2p chats; `ou_*` for `openDM()` placeholders before the first message is delivered.
192+
- `rootId` — the message's `root_id` if it is a reply, otherwise its own `message_id` (the message is its own root).
193+
194+
Lark's native `thread_id` (topic containers, `omt_*`) is **not** used as the `rootId` segment — it's a topic container ID, not a message ID, and can't be used as `replyTo` on the send API.
195+
196+
### DM detection
197+
198+
Lark's p2p chat IDs share the `oc_*` prefix with group chats, so `isDM()` relies on a chat-type cache populated by inbound events. The first DM after a process restart may route through `onNewMention` until the cache catches up.
199+
200+
## Message history
201+
202+
`fetchMessages` is implemented on top of `im.v1.messages.list` plus the SDK's `normalize()` — which covers Lark's 23 native message types and produces the same `NormalizedMessage` shape as live events.
203+
204+
`listThreads` is derived client-side by grouping list results on `root_id`. Paginate carefully for very active chats; there is no native server-side list-threads API.
205+
206+
`author.isMe` is resolved consistently for **historical** bot-authored messages, not just live events — the adapter maps the historical entry's `app_id` back to the bot's `open_id` via the SDK's `botIdentity` resolver.
207+
208+
## Safety layer
209+
210+
`LarkChannel`'s built-in safety features (stale-message detection, dedup, per-chat queue, text batch) are **disabled** by the adapter. Chat SDK's per-thread lock plus the state adapter handles message deduplication and subscription consistency — running the SDK's safety on top of Chat SDK's would double-process or drop messages.
211+
212+
## Multi-app / multi-tenant
213+
214+
Single-app only at present. A future version may support `setInstallation()` for multi-tenant fan-out — open an issue if you need it.
215+
216+
## Limitations
217+
218+
The following operations are not supported and throw `NotImplementedError`:
219+
220+
- `handleWebhook` — returns HTTP 501; WebSocket transport only.
221+
- `startTyping` — Lark has no typing-indicator API.
222+
- `postChannelMessage` — Lark requires every message to belong to a chat (no channel-level top-level messages distinct from threads).
223+
- `scheduleMessage`, `openModal`, `postEphemeral` — not yet implemented.
224+
225+
## Feature support
226+
227+
<FeatureSupport />

apps/docs/content/adapters/vendor-official/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"liveblocks",
77
"resend",
88
"zernio",
9-
"agentphone"
9+
"agentphone",
10+
"lark"
1011
]
1112
}

packages/integration-tests/src/docs-adapters.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe("Vendor-Official adapter MDX", () => {
114114
[
115115
"agentphone",
116116
"imessage",
117+
"lark",
117118
"liveblocks",
118119
"matrix",
119120
"resend",

packages/integration-tests/src/documentation-test-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export const VALID_DOC_PACKAGES = [
110110
"chat-adapter-sendblue",
111111
"@bitbasti/chat-adapter-webex",
112112
"chat-adapter-zalo",
113+
"@larksuite/vercel-chat-adapter",
114+
"qrcode-terminal",
113115
];
114116

115117
export function extractTypeScriptBlocks(markdown: string): string[] {

0 commit comments

Comments
 (0)