eve-lark 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -23
- package/README.zh-CN.md +153 -28
- package/dist/card.d.ts +167 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/channel.d.ts +28 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/dedup.d.ts +28 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/event-policy.d.ts +29 -0
- package/dist/event-policy.d.ts.map +1 -0
- package/dist/feishu-emoji.d.ts +16 -0
- package/dist/feishu-emoji.d.ts.map +1 -0
- package/dist/index.d.ts +24 -399
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2847 -192
- package/dist/index.js.map +1 -1
- package/dist/lark-client.d.ts +220 -0
- package/dist/lark-client.d.ts.map +1 -0
- package/dist/long-connection.d.ts +98 -0
- package/dist/long-connection.d.ts.map +1 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/outbound.d.ts +90 -0
- package/dist/outbound.d.ts.map +1 -0
- package/dist/parse.d.ts +8 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/streaming-controller.d.ts +201 -0
- package/dist/streaming-controller.d.ts.map +1 -0
- package/dist/target.d.ts +31 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/types.d.ts +521 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -2,39 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
A [Lark](https://www.larksuite.com) / [Feishu](https://www.feishu.cn) channel for the [eve](https://eve.dev) agent framework. Drop the factory into `agent/channels/lark.ts` and eve will mount a Lark webhook that turns inbound DMs and group
|
|
5
|
+
A [Lark](https://www.larksuite.com) / [Feishu](https://www.feishu.cn) channel for the [eve](https://eve.dev) agent framework. Drop the factory into `agent/channels/lark.ts` and eve will mount a Lark webhook that turns inbound DMs and group messages into agent replies.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
**Inbound**
|
|
10
|
-
- Text, rich-text (`post`),
|
|
11
|
-
- Image
|
|
12
|
-
-
|
|
13
|
-
- `
|
|
10
|
+
- Text, rich-text (`post`), and `@` mentions, including bot mention stripping and `@all`
|
|
11
|
+
- Image/file attachments; audio, video, stickers, shared cards, locations, todo, vote, system messages, interactive cards, and merge-forward messages are converted into readable placeholders or summaries. Interactive cards and merge-forward children are expanded through the message API when available. Audio/media are transcribed first when an `asrProvider` is configured
|
|
12
|
+
- Message reactions as synthetic user input
|
|
13
|
+
- Threading via `root_id` / `parent_id`, per-chat serialization, and quote replies to the triggering message
|
|
14
|
+
- DM sender allowlists, group chat allowlists, per-group sender allowlists, `requireMention`, `allowBots`, and `systemPrompt` injection
|
|
15
|
+
- `event_id` deduplication and stale-event rejection
|
|
14
16
|
|
|
15
17
|
**Outbound**
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
18
|
+
- CardKit v2 streaming replies — default, using CardKit entities, `card_id` delivery, element sequence updates, and terminal streaming-mode shutdown
|
|
19
|
+
- `post` rich-text replies and static one-shot card replies — configurable
|
|
20
|
+
- `createLarkSender()` outbound sender: chat/open_id/user_id targets, encoded reply/thread targets, text chunking, native `channelData.feishu.card` cards, image/file/audio/video upload + send, ordered multi-media orchestration, paged/cached chat-member mention normalization, and required peer mention injection
|
|
21
|
+
- `createLarkMessageActions()` action adapter for agent/tool calls: `send`, `react`, `reactions`, `delete`, `unsend`, and `forward`
|
|
22
|
+
- Low-level `LarkClient` APIs: upload/send media, forward, delete, chat metadata/member management, chat member listing, CardKit, resources, and reaction listing
|
|
23
|
+
- Inbound ack reactions, plus message reaction add/remove/list APIs
|
|
24
|
+
- Custom business card action handling via `cardActionHandler`, with reply/follow-up/edit helpers
|
|
19
25
|
|
|
20
26
|
**Security**
|
|
21
27
|
- `X-Lark-Signature` verification (`sha256(timestamp + nonce + encrypt_key + body)`, constant-time)
|
|
22
28
|
- AES-256-CBC decryption of the `encrypt` envelope when `encryptKey` is configured
|
|
23
|
-
- Timestamp skew window (5 min default)
|
|
24
|
-
-
|
|
29
|
+
- Timestamp skew window (5 min default) and event-age window (10 min default)
|
|
30
|
+
- Event `app_id` ownership validation
|
|
31
|
+
- Bot self-message suppression plus mention-gated bot-to-bot group traffic
|
|
32
|
+
- Outbound remote media URL safety checks for localhost/private IP/DNS results, plus realpath-checked local media file root allowlists
|
|
25
33
|
|
|
26
|
-
**Interactive ask_question** — when the model calls eve's built-in `ask_question` tool, eve-lark renders the prompt as a Feishu interactive card
|
|
34
|
+
**Interactive ask_question** — when the model calls eve's built-in `ask_question` tool, eve-lark renders the prompt as a Feishu interactive card. Single questions use button/select-style cards; multiple simultaneous questions are rendered as one submit form, including multi-select fields. Clicks come back via `card.action.trigger` and resume the parked session. `allowFreeform: true` lets the user reply with a normal chat message instead of clicking. Pending cards show a submitting state, expire after `askInputTtlMs`, can restrict submission to a specific `submitterOpenId`, and failed synthetic resume attempts restore the card so it remains retryable.
|
|
35
|
+
|
|
36
|
+
**Custom card actions** — pass `cardActionHandler` to handle `card.action.trigger` callbacks that are not produced by eve-lark's built-in ask cards. The handler receives the raw event, `action.value`, chat/message/user ids, and `respond.reply`, `respond.followUp`, and `respond.editMessage` helpers. This is a lightweight channel hook; it is not openclaw-lark's plugin-wide interactive registry.
|
|
37
|
+
|
|
38
|
+
**Commands and diagnostics** — `/lark help`, `/lark start`, `/lark doctor`, `/lark auth`, `/lark trace <message_id>`, and the legacy `/lark-diagnose` are handled by the channel and are not forwarded to the agent. `/lark doctor` reports token status, channel runtime config, and required IM/CardKit/media/reaction scopes and events.
|
|
27
39
|
|
|
28
40
|
**Both Feishu and Lark** are supported via a single `baseUrl` switch.
|
|
29
41
|
|
|
30
42
|
### Out of scope (v1)
|
|
31
43
|
|
|
32
|
-
These are intentionally **not** shipped — file an issue if you need them:
|
|
33
|
-
- Non-
|
|
44
|
+
These are intentionally **not** shipped, or are outside the IM-channel v1 scope — file an issue if you need them:
|
|
45
|
+
- Non-IM channel surfaces such as Drive comments and VC meeting invitations.
|
|
46
|
+
- Full streaming image URL resolver with async upload placeholders.
|
|
47
|
+
- Full i18n for all HITL/diagnostic copy.
|
|
34
48
|
- Multi-account configuration
|
|
35
49
|
- Per-user OAuth (`user_access_token` device flow)
|
|
36
50
|
- Feishu API tools (docs / bitable / calendar / tasks / drive)
|
|
37
|
-
- Fully custom agent-authored card schemas (interactive forms beyond `ask_question` buttons, which shipped in 0.3.0+)
|
|
38
51
|
|
|
39
52
|
## Quick start
|
|
40
53
|
|
|
@@ -97,9 +110,47 @@ All fields can be supplied as options or read from the matching env var (options
|
|
|
97
110
|
| `maxRetries` | `number` | no | `2` | — |
|
|
98
111
|
| `tokenRefreshBufferMs` | `number` | no | `300_000` (5 min) | — |
|
|
99
112
|
| `signatureSkewMs` | `number` | no | `300_000` (5 min) | — |
|
|
100
|
-
| `
|
|
113
|
+
| `eventMaxAgeMs` | `number` | no | `600_000` (10 min) | — |
|
|
114
|
+
| `askInputTtlMs` | `number` | no | `300_000` (5 min) | — |
|
|
115
|
+
| `ackReaction` | `string \| readonly string[] \| false` | no | `"Typing"` | — |
|
|
116
|
+
| `allowFrom` | `readonly string[]` | no | all DMs allowed | — |
|
|
117
|
+
| `groupAllowFrom` | `readonly string[]` | no | all groups allowed | — |
|
|
118
|
+
| `allowBots` | `boolean \| "mentions"` | no | `"mentions"` | — |
|
|
119
|
+
| `groupConfigs` | `readonly { chatId: string; allowFrom?: readonly string[]; requireMention?: boolean; respondToMentionAll?: boolean; allowBots?: boolean \| "mentions"; systemPrompt?: string }[]` | no | — | — |
|
|
120
|
+
| `asrProvider` | `{ transcribe(bytes, mediaType): Promise<string> }` | no | — | — |
|
|
121
|
+
| `cardActionHandler` | `(ctx) => unknown \| Promise<unknown>` | no | — | — |
|
|
122
|
+
| `mediaLocalRoots` | `readonly string[]` | no | local file media disabled | — |
|
|
123
|
+
| `mediaHostResolver` | `(hostname) => Promise<readonly string[]>` | no | Node DNS lookup | — |
|
|
101
124
|
| `fetch` | `typeof fetch` | no | `globalThis.fetch` | — |
|
|
102
125
|
|
|
126
|
+
## Outbound helpers
|
|
127
|
+
|
|
128
|
+
`createLarkSender()` is the direct channel sender. It accepts either the legacy `chatId` or a richer `to` target:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const sender = createLarkSender({ appId, appSecret, verificationToken });
|
|
132
|
+
|
|
133
|
+
await sender.sendPayload({
|
|
134
|
+
to: "open_id:ou_xxx",
|
|
135
|
+
text: "hello",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await sender.sendPayload({
|
|
139
|
+
to: "oc_xxx#__feishu_reply_to=om_xxx",
|
|
140
|
+
channelData: { feishu: { card: { schema: "2.0", body: { elements: [] } } } },
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Target forms:
|
|
145
|
+
|
|
146
|
+
- `oc_xxx` or `chat:oc_xxx` → `receive_id_type=chat_id`
|
|
147
|
+
- `ou_xxx`, `open_id:ou_xxx`, or `feishu:ou_xxx` → `receive_id_type=open_id`
|
|
148
|
+
- `user:employee_id` or `{ id: "employee_id", idType: "user_id" }` → `receive_id_type=user_id`
|
|
149
|
+
- `#__feishu_reply_to=om_xxx` encodes a quote-reply target
|
|
150
|
+
- `#__feishu_reply_to=om_xxx&__feishu_thread_id=omt_xxx` replies inside a thread with `reply_in_thread=true`
|
|
151
|
+
|
|
152
|
+
`createLarkMessageActions()` exposes the same sending layer as a small agent/tool action adapter with `send`, `react`, `reactions`, `delete`, `unsend`, and `forward`.
|
|
153
|
+
|
|
103
154
|
## Feishu vs Lark (international)
|
|
104
155
|
|
|
105
156
|
The two deployments speak the same API. Switch with `baseUrl`:
|
|
@@ -115,7 +166,7 @@ Or via env: `LARK_BASE_URL=https://open.larksuite.com`.
|
|
|
115
166
|
|
|
116
167
|
## Reply modes
|
|
117
168
|
|
|
118
|
-
- **`streaming-v2`** (default): the channel creates
|
|
169
|
+
- **`streaming-v2`** (default): the channel creates a CardKit v2 entity on the first delta, sends the IM message by `card_id`, streams text through CardKit element sequence updates, then closes `streaming_mode` before writing the terminal card. It tracks reasoning text separately, renders tool trace rows, includes optional footer metrics, and stops intermediate streaming on CardKit unavailable/table-limit errors while keeping the terminal CardKit update. Best live UX this channel ships. `ask_question` prompts are still sent as separate ask cards/forms.
|
|
119
170
|
- **`streaming`**: same live-patch UX as `streaming-v2` but on the older v1 card schema. Slightly smaller font than v2. Opt in only if you have a specific reason to avoid CardKit v2.
|
|
120
171
|
- **`post`**: the channel waits for `message.completed` and delivers the reply as a `msg_type: "post"` rich-text message. **Renders at native chat-message size** with full markdown support (bold, links, code, `<font>` color tags). No live streaming — the user sees the reply only when the turn completes.
|
|
121
172
|
- **`static`**: same wait-for-completion delivery as `post`, but uses an interactive card instead of a post. Useful if you need card features (buttons, multi-column layout) and don't mind the smaller text.
|
|
@@ -157,6 +208,33 @@ Inbound image/file messages are converted into eve `UserContent` file parts. The
|
|
|
157
208
|
|
|
158
209
|
If you want URL parts to pass through without staged bytes (e.g., when running outside eve's sandbox), don't set `encryptKey` and inspect `attributes` in your tools instead.
|
|
159
210
|
|
|
211
|
+
## Group controls
|
|
212
|
+
|
|
213
|
+
Use `allowFrom` for DM sender allowlists and `groupAllowFrom` for group chat allowlists. Group messages from users are accepted with or without an `@` mention by default; if `botOpenId` is configured, a leading bot mention is stripped before the text reaches the agent.
|
|
214
|
+
|
|
215
|
+
`allowBots` controls messages sent by other Feishu/Lark app bots. The default is `"mentions"`: bot DMs pass through, but group bot messages must directly @ this bot. Set `allowBots: true` to allow bot senders without an @ in allowed groups, or `false` to drop bot senders entirely. When replying to an allowed group bot sender, eve-lark injects a structured peer `<at user_id="...">` and sends a normal group message so the peer bot receives it.
|
|
216
|
+
|
|
217
|
+
`groupConfigs` lets you attach per-group sender and mention policy plus `systemPrompt`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
createLarkChannel({
|
|
221
|
+
// ...credentials...
|
|
222
|
+
groupAllowFrom: ["oc_xxx"],
|
|
223
|
+
groupConfigs: [
|
|
224
|
+
{
|
|
225
|
+
chatId: "oc_xxx",
|
|
226
|
+
allowFrom: ["ou_alice"],
|
|
227
|
+
requireMention: true,
|
|
228
|
+
respondToMentionAll: false,
|
|
229
|
+
allowBots: "mentions",
|
|
230
|
+
systemPrompt: "You are the support assistant for this group. Be concise.",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
When `requireMention` is true, only direct bot mentions wake the agent. `@all` only wakes the agent when `respondToMentionAll` is also true. The prompt is passed to eve as `send()` context for matching group messages only. DMs ignore `groupConfigs`.
|
|
237
|
+
|
|
160
238
|
## Errors
|
|
161
239
|
|
|
162
240
|
eve-lark throws a small typed hierarchy:
|
|
@@ -177,14 +255,15 @@ The webhook handler returns structured HTTP responses for predictable server-sid
|
|
|
177
255
|
| 400 | Invalid JSON / decrypt failure |
|
|
178
256
|
| 401 | Signature missing/invalid or verification token mismatch |
|
|
179
257
|
| 408 | Timestamp skew window exceeded |
|
|
258
|
+
| 413 | Request body exceeds the 1 MB limit |
|
|
180
259
|
|
|
181
260
|
## Limitations & roadmap
|
|
182
261
|
|
|
183
262
|
**v1 limitations**: see [Out of scope](#out-of-scope-v1).
|
|
184
263
|
|
|
185
264
|
**Planned for v2** (open an issue if you'd like to prioritize any):
|
|
186
|
-
-
|
|
187
|
-
-
|
|
265
|
+
- Streaming image URL resolver
|
|
266
|
+
- Broader HITL i18n
|
|
188
267
|
- Optional Redis-backed dedup for multi-instance deployments
|
|
189
268
|
- Per-user OAuth (`user_access_token`) for Feishu API tools
|
|
190
269
|
|
|
@@ -194,11 +273,55 @@ The webhook handler returns structured HTTP responses for predictable server-sid
|
|
|
194
273
|
pnpm install
|
|
195
274
|
pnpm test # run the vitest suite
|
|
196
275
|
pnpm test:watch # interactive watch mode
|
|
197
|
-
pnpm typecheck # tsc --noEmit
|
|
276
|
+
pnpm typecheck # TypeScript 7 RC tsc --noEmit
|
|
198
277
|
pnpm lint # eslint
|
|
199
|
-
pnpm build # tsup build → dist/
|
|
278
|
+
pnpm build # tsup JS build + TypeScript 7 declarations → dist/
|
|
200
279
|
```
|
|
201
280
|
|
|
281
|
+
`typescript` remains pinned to the JavaScript compiler for tools that import the TypeScript API, while `typescript-7` powers the CLI typecheck and declaration emit.
|
|
282
|
+
|
|
283
|
+
## Real Lark E2E tests
|
|
284
|
+
|
|
285
|
+
`pnpm test:e2e` loads `.env.e2e.local`, but the real Lark cases only run when `E2E_LARK=1` is set. Without that flag, Vitest collects the file and skips the suite.
|
|
286
|
+
|
|
287
|
+
Use a disposable test chat. The suite sends real messages, cards, reactions, and files to `E2E_LARK_CHAT_ID`, and posts a start/end summary message in that chat.
|
|
288
|
+
|
|
289
|
+
Required local setup:
|
|
290
|
+
|
|
291
|
+
- `lark-cli` is installed and logged in as a user that belongs to the test chat. The tests use it with `--as user` to send text/files, list messages, add/delete/list reactions, and discover the bot member.
|
|
292
|
+
- The app bot is installed in the same chat.
|
|
293
|
+
- The app is in **long-connection** event mode and subscribes to `im.message.receive_v1`, `card.action.trigger`, `im.message.reaction.created_v1`, and `im.message.reaction.deleted_v1`.
|
|
294
|
+
- The app's bot token can send/reply to IM messages, send interactive cards, use CardKit v2 card APIs, add/remove/list message reactions, upload IM images/files, download message resources, forward/delete messages sent by the bot, and list chat members. In the Feishu console this means enabling the corresponding IM/CardKit permissions; for file resource APIs the current console permission is `im:resource`.
|
|
295
|
+
- The default E2E suite does not rename chats or add/remove chat members. `updateChat`, `addChatMembers`, and `removeChatMembers` are covered by unit tests to avoid mutating the shared test chat.
|
|
296
|
+
- Local ports starting at `E2E_LARK_PORT` are free. The default base is `23080`, and the suite increments from there.
|
|
297
|
+
|
|
298
|
+
`.env.e2e.local` is gitignored. A minimal file looks like:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
E2E_LARK=1
|
|
302
|
+
E2E_LARK_CHAT_ID=oc_xxx
|
|
303
|
+
|
|
304
|
+
LARK_APP_ID=cli_xxx
|
|
305
|
+
LARK_APP_SECRET=xxx
|
|
306
|
+
LARK_VERIFICATION_TOKEN=xxx
|
|
307
|
+
LARK_ENCRYPT_KEY=xxx # optional unless your app has encryption enabled
|
|
308
|
+
LARK_BASE_URL=https://open.feishu.cn
|
|
309
|
+
|
|
310
|
+
# Optional. If omitted, the suite uses lark-cli to find the single bot in the chat.
|
|
311
|
+
E2E_LARK_BOT_OPEN_ID=ou_xxx
|
|
312
|
+
|
|
313
|
+
# Optional. Defaults to 23080.
|
|
314
|
+
E2E_LARK_PORT=23080
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Run:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
pnpm test:e2e
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The suite currently covers outbound text/post/card/reaction/media APIs, `createLarkSender().sendPayload()` text + native card + media orchestration, decorated mention normalization, encoded thread replies, message action adapter thread replies, local media realpath safety, non-destructive forward/delete/list-member actions, CardKit v2 streaming, long-connection inbound replies, bot-to-bot `allowBots` gating and peer mention delivery, ack reaction, per-chat queueing, quote replies, group `@` and non-`@` messages, group `requireMention`, group-level `systemPrompt`, group allowlist behavior, slash commands, custom card action reply/follow-up/edit handling, HITL text/select/multi-select form/freeform/retry/TTL flows, reaction events as synthetic input, and file inbound/resource download. Unit tests additionally cover open_id/user_id targets, the message action adapter, audio/video duration inference, private media URL rejection, merge-forward expansion hooks, full-card fetch hooks, doctor scope/event output, and streaming metrics/unavailable guards.
|
|
324
|
+
|
|
202
325
|
## Smoke testing against a real Feishu app
|
|
203
326
|
|
|
204
327
|
See [`examples/README.md`](./examples/README.md) for a complete walkthrough. The TL;DR matches the [Quick start](#quick-start) above: install deps, fill `.env`, run `eve dev`. Send `ping` to the bot; expect `pong` as a streaming card.
|
|
@@ -233,14 +356,23 @@ Test layout:
|
|
|
233
356
|
|
|
234
357
|
```
|
|
235
358
|
test/
|
|
359
|
+
├── allowlist.spec.ts # DM/group allowlists, requireMention, and per-group systemPrompt
|
|
360
|
+
├── ask-card.spec.ts # ask_question card builders
|
|
361
|
+
├── ask-flow.spec.ts # ask_question render/callback/freeform/retry/TTL flows
|
|
362
|
+
├── asr.spec.ts # optional audio/media transcription
|
|
363
|
+
├── authorization.spec.ts # eve authorization cards
|
|
364
|
+
├── cardkit-v2.spec.ts # CardKit v2 builders
|
|
236
365
|
├── crypto.spec.ts # signature & AES vectors (including a round-trip helper)
|
|
237
366
|
├── dedup.spec.ts # TTL, FIFO eviction, lazy sweep
|
|
367
|
+
├── diagnose.spec.ts # /lark command interception and diagnostics
|
|
368
|
+
├── event-policy.spec.ts # app ownership, event expiry, abort text, reactions
|
|
238
369
|
├── options.spec.ts # env fallback, defaults, validation
|
|
370
|
+
├── outbound.spec.ts # outbound sender/media/payload/mention/action helpers
|
|
239
371
|
├── parse.spec.ts # text/image/file/post/mention fixtures
|
|
240
|
-
├── lark-client.spec.ts # token mutex, retry policy
|
|
372
|
+
├── lark-client.spec.ts # token mutex, retry policy, CardKit, reactions, resources
|
|
241
373
|
├── streaming-controller.spec.ts # FSM transitions, throttle, fallback
|
|
242
|
-
├──
|
|
243
|
-
├──
|
|
374
|
+
├── channel.spec.ts # webhook handling, queueing, abort, ack reaction
|
|
375
|
+
├── e2e/lark-real.spec.ts # opt-in real Feishu/Lark E2E suite
|
|
244
376
|
└── helpers/
|
|
245
377
|
├── encrypt.ts # test-only AES cipher mirror
|
|
246
378
|
└── mock-fetch.ts # tiny mock fetch used in place of nock for native-fetch compat
|
package/README.zh-CN.md
CHANGED
|
@@ -2,40 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
一个为 [eve](https://eve.dev) agent 框架打造的 [Lark](https://www.larksuite.com) / [Feishu](https://www.feishu.cn) 通道。把工厂函数放到 `agent/channels/lark.ts`,eve 就会挂载一个 Lark webhook
|
|
5
|
+
一个为 [eve](https://eve.dev) agent 框架打造的 [Lark](https://www.larksuite.com) / [Feishu](https://www.feishu.cn) 通道。把工厂函数放到 `agent/channels/lark.ts`,eve 就会挂载一个 Lark webhook,把收到的私聊消息和群消息转成 agent 回复。
|
|
6
6
|
|
|
7
7
|
## 特性
|
|
8
8
|
|
|
9
9
|
**入站**
|
|
10
|
-
- 文本、富文本(`post
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- `
|
|
10
|
+
- 文本、富文本(`post`)、`@` 提及,包括 bot mention stripping 和 `@all`
|
|
11
|
+
- 图片/文件附件;音频、视频、sticker、分享卡片、位置、todo、vote、system、交互卡片和 merge-forward 消息会被转成可读占位或摘要;可用消息 API 时会展开完整交互卡片内容和 merge-forward 子消息;配置 `asrProvider` 后音频/媒体优先转写成文本
|
|
12
|
+
- 消息 reaction 作为 synthetic user input
|
|
13
|
+
- 通过 `root_id` / `parent_id` 跟踪线程、同 chat 串行队列、引用触发消息回复
|
|
14
|
+
- DM 发送人白名单、群白名单、群内 sender 白名单、`requireMention`、`allowBots` 和群级 `systemPrompt` 注入
|
|
15
|
+
- `event_id` 去重和过期事件丢弃
|
|
14
16
|
|
|
15
17
|
**出站**
|
|
16
|
-
-
|
|
17
|
-
- `post`
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- CardKit v2 流式回复 —— 默认,走 CardKit entity、`card_id` 发送、element sequence 更新和终态关闭 streaming mode
|
|
19
|
+
- `post` 富文本回复和静态一次性卡片 —— 可配置
|
|
20
|
+
- `createLarkSender()` 出站发送器:chat/open_id/user_id target、encoded reply/thread target、text chunk、`channelData.feishu.card` 原生卡片、图片/文件/音频/视频 upload + send、多媒体顺序编排、分页缓存的群成员 mention normalization、强制 peer mention 注入
|
|
21
|
+
- `createLarkMessageActions()` agent/tool action adapter:`send`、`react`、`reactions`、`delete`、`unsend`、`forward`
|
|
22
|
+
- `LarkClient` 低层 API:upload/send media、forward、delete、chat metadata/member 管理、chat member list、CardKit、resource、reaction list
|
|
23
|
+
- 入站 ack reaction,以及消息 reaction 添加/删除/列出 API
|
|
24
|
+
- 通过 `cardActionHandler` 处理自定义业务卡片 action,并提供 reply/follow-up/edit helper
|
|
20
25
|
|
|
21
26
|
**安全**
|
|
22
27
|
- `X-Lark-Signature` 校验(`sha256(timestamp + nonce + encrypt_key + body)`,constant-time)
|
|
23
28
|
- 当配置了 `encryptKey` 时,AES-256-CBC 解密 `encrypt` 信封
|
|
24
|
-
- 时间戳偏差窗口(默认 5 分钟)
|
|
25
|
-
-
|
|
29
|
+
- 时间戳偏差窗口(默认 5 分钟)和事件年龄窗口(默认 10 分钟)
|
|
30
|
+
- 事件 `app_id` 归属校验
|
|
31
|
+
- 抑制 bot 自己发的消息,并支持 mention-gated 的 bot-to-bot 群消息
|
|
32
|
+
- 出站 remote media URL 的 localhost/私网 IP/DNS 结果校验,以及按 realpath 校验的 local media file root allowlist
|
|
26
33
|
|
|
27
|
-
**交互式 ask_question**——当模型调用 eve 内置的 `ask_question` 工具时,eve-lark
|
|
34
|
+
**交互式 ask_question**——当模型调用 eve 内置的 `ask_question` 工具时,eve-lark 会把提示渲染成飞书交互卡片。单问题走按钮/选择卡片;同一轮多个问题会渲染成一张统一提交表单,也支持 multi-select 字段。用户点击触发 `card.action.trigger` 回调,channel 把答案作为 `InputResponse` 发回 eve,parked session 恢复。`allowFreeform: true` 允许用户直接回复普通聊天消息代替点击。pending 卡片会显示提交中状态,在 `askInputTtlMs` 后过期,可通过 `submitterOpenId` 限定提交者;synthetic resume 失败时会恢复原卡片保持可重试。
|
|
35
|
+
|
|
36
|
+
**自定义卡片 action**——传入 `cardActionHandler` 后,eve-lark 会把非内置 ask 卡片产生的 `card.action.trigger` 回调交给它处理。handler 能拿到原始事件、`action.value`、chat/message/user id,并可用 `respond.reply`、`respond.followUp`、`respond.editMessage` 回复或改卡。这是轻量 channel hook,不是 openclaw-lark 那套插件级 interactive registry。
|
|
37
|
+
|
|
38
|
+
**命令与诊断**——`/lark help`、`/lark start`、`/lark doctor`、`/lark auth`、`/lark trace <message_id>` 和兼容保留的 `/lark-diagnose` 由 channel 直接处理,不转发给 agent。`/lark doctor` 会输出 token 状态、channel 运行配置,以及 IM/CardKit/media/reaction 所需权限和事件清单。
|
|
28
39
|
|
|
29
40
|
**Feishu(飞书)和 Lark(国际版)** 通过单一的 `baseUrl` 切换支持。
|
|
30
41
|
|
|
31
42
|
### 不在 v1 范围内
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
-
|
|
44
|
+
以下功能**未实现或不作为 channel v1 默认范围**——需要的话请提 issue:
|
|
45
|
+
- Drive comment、VC meeting invited 等非 IM channel 入口。
|
|
46
|
+
- 完整 streaming image URL resolver 与异步上传占位。
|
|
47
|
+
- HITL / diagnostics 的完整 i18n 文案。
|
|
35
48
|
- 多账号配置
|
|
36
49
|
- 用户级 OAuth(`user_access_token` device flow)
|
|
37
50
|
- 飞书 API 工具(docs / bitable / calendar / tasks / drive)
|
|
38
|
-
- agent 自渲染的完全自定义卡片 schema(交互表单——`ask_question` 的卡片按钮已在 0.3.0+ 发出,这里指的是超出这个范围的)
|
|
39
51
|
|
|
40
52
|
## 快速开始
|
|
41
53
|
|
|
@@ -98,9 +110,47 @@ eve dev
|
|
|
98
110
|
| `maxRetries` | `number` | 否 | `2` | — |
|
|
99
111
|
| `tokenRefreshBufferMs` | `number` | 否 | `300_000`(5 分钟) | — |
|
|
100
112
|
| `signatureSkewMs` | `number` | 否 | `300_000`(5 分钟) | — |
|
|
113
|
+
| `eventMaxAgeMs` | `number` | 否 | `600_000`(10 分钟) | — |
|
|
114
|
+
| `askInputTtlMs` | `number` | 否 | `300_000`(5 分钟) | — |
|
|
101
115
|
| `ackReaction` | `string \| readonly string[] \| false` | 否 | `"Typing"` | — |
|
|
116
|
+
| `allowFrom` | `readonly string[]` | 否 | 允许所有 DM | — |
|
|
117
|
+
| `groupAllowFrom` | `readonly string[]` | 否 | 允许所有群 | — |
|
|
118
|
+
| `allowBots` | `boolean \| "mentions"` | 否 | `"mentions"` | — |
|
|
119
|
+
| `groupConfigs` | `readonly { chatId: string; allowFrom?: readonly string[]; requireMention?: boolean; respondToMentionAll?: boolean; allowBots?: boolean \| "mentions"; systemPrompt?: string }[]` | 否 | — | — |
|
|
120
|
+
| `asrProvider` | `{ transcribe(bytes, mediaType): Promise<string> }` | 否 | — | — |
|
|
121
|
+
| `cardActionHandler` | `(ctx) => unknown \| Promise<unknown>` | 否 | — | — |
|
|
122
|
+
| `mediaLocalRoots` | `readonly string[]` | 否 | 禁用本地文件媒体路径 | — |
|
|
123
|
+
| `mediaHostResolver` | `(hostname) => Promise<readonly string[]>` | 否 | Node DNS lookup | — |
|
|
102
124
|
| `fetch` | `typeof fetch` | 否 | `globalThis.fetch` | — |
|
|
103
125
|
|
|
126
|
+
## 出站 helper
|
|
127
|
+
|
|
128
|
+
`createLarkSender()` 是直接 channel sender。它兼容旧的 `chatId`,也支持更完整的 `to` target:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const sender = createLarkSender({ appId, appSecret, verificationToken });
|
|
132
|
+
|
|
133
|
+
await sender.sendPayload({
|
|
134
|
+
to: "open_id:ou_xxx",
|
|
135
|
+
text: "hello",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await sender.sendPayload({
|
|
139
|
+
to: "oc_xxx#__feishu_reply_to=om_xxx",
|
|
140
|
+
channelData: { feishu: { card: { schema: "2.0", body: { elements: [] } } } },
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
target 形式:
|
|
145
|
+
|
|
146
|
+
- `oc_xxx` 或 `chat:oc_xxx` → `receive_id_type=chat_id`
|
|
147
|
+
- `ou_xxx`、`open_id:ou_xxx` 或 `feishu:ou_xxx` → `receive_id_type=open_id`
|
|
148
|
+
- `user:employee_id` 或 `{ id: "employee_id", idType: "user_id" }` → `receive_id_type=user_id`
|
|
149
|
+
- `#__feishu_reply_to=om_xxx` 表示引用回复目标
|
|
150
|
+
- `#__feishu_reply_to=om_xxx&__feishu_thread_id=omt_xxx` 表示在线程内回复,并带上 `reply_in_thread=true`
|
|
151
|
+
|
|
152
|
+
`createLarkMessageActions()` 把同一套发送层暴露成轻量 agent/tool action adapter,支持 `send`、`react`、`reactions`、`delete`、`unsend`、`forward`。
|
|
153
|
+
|
|
104
154
|
## Feishu vs Lark(国际版)
|
|
105
155
|
|
|
106
156
|
两个部署用同一套 API。通过 `baseUrl` 切换:
|
|
@@ -116,7 +166,7 @@ createLarkChannel({
|
|
|
116
166
|
|
|
117
167
|
## 回复模式
|
|
118
168
|
|
|
119
|
-
- **`streaming-v2`**(默认):channel 在第一个 delta
|
|
169
|
+
- **`streaming-v2`**(默认):channel 在第一个 delta 时创建 CardKit v2 entity,通过 `card_id` 发送 IM 消息,再用 CardKit element sequence 更新正文;终态会先关闭 `streaming_mode` 再更新完整卡片。它会独立展示 reasoning、渲染 tool trace、支持可选 footer metrics,并在 CardKit unavailable/table-limit 错误后停止中间帧流式但保留终态 CardKit 更新。**是这个 channel 能提供的最好的实时 UX**。`ask_question` 会单独发送问题卡/表单。
|
|
120
170
|
- **`streaming`**:和 `streaming-v2` 一样的实时 patch UX,但走老的 v1 卡片 schema,字号比 v2 略小。仅在你有特定原因想避开 CardKit v2 时才选。
|
|
121
171
|
- **`post`**:channel 等 `message.completed`,把回复作为 `msg_type: "post"` 富文本消息发出。**渲染为原生聊天消息大小**,完整支持 markdown(粗体、链接、代码、`<font>` 颜色 tag)。代价:不能流式——用户在 turn 完成时才看到回复。
|
|
122
172
|
- **`static`**:和 `post` 一样等完成再发,但用交互卡片而非 post。适合需要卡片特性(按钮、多列布局)且不在乎字号小的场景。
|
|
@@ -158,6 +208,33 @@ oc_xxx:om_yyy — 在 om_yyy 线程里的回复
|
|
|
158
208
|
|
|
159
209
|
如果你想让 URL 部分直接透传(比如在 eve sandbox 外运行),不要设 `encryptKey`,改在工具里读 `attributes`。
|
|
160
210
|
|
|
211
|
+
## 群控制
|
|
212
|
+
|
|
213
|
+
`allowFrom` 用于 DM 发送人白名单,`groupAllowFrom` 用于群 chat_id 白名单。被允许的群里,用户消息默认 `@ bot` 和不 `@ bot` 都会进入 agent;配置了 `botOpenId` 时,文本开头的 bot mention 会在进入 agent 前被去掉。
|
|
214
|
+
|
|
215
|
+
`allowBots` 控制其他飞书/Lark 应用 bot 发来的消息。默认是 `"mentions"`:bot 私聊会进入 agent,但群内 bot 消息必须直接 @ 当前 bot。设为 `true` 时,被允许群里的 bot sender 即使不 @ 也会进入;设为 `false` 时丢弃所有 bot sender。回复被允许的群 bot sender 时,eve-lark 会注入结构化 peer `<at user_id="...">`,并发送普通群消息,确保对方 bot 收到。
|
|
216
|
+
|
|
217
|
+
`groupConfigs` 可以为不同群配置 sender 白名单、mention 策略和 `systemPrompt`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
createLarkChannel({
|
|
221
|
+
// ...credentials...
|
|
222
|
+
groupAllowFrom: ["oc_xxx"],
|
|
223
|
+
groupConfigs: [
|
|
224
|
+
{
|
|
225
|
+
chatId: "oc_xxx",
|
|
226
|
+
allowFrom: ["ou_alice"],
|
|
227
|
+
requireMention: true,
|
|
228
|
+
respondToMentionAll: false,
|
|
229
|
+
allowBots: "mentions",
|
|
230
|
+
systemPrompt: "你是这个群的支持助手,回答要简洁。",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`requireMention` 为 true 时,只有直接 @ bot 才会唤醒 agent。`@all` 只有在 `respondToMentionAll` 也为 true 时才会唤醒。提示词会作为 eve `send()` 的 `context` 传给匹配的群消息。DM 不会读取 `groupConfigs`。
|
|
237
|
+
|
|
161
238
|
## 错误
|
|
162
239
|
|
|
163
240
|
eve-lark 抛出一组类型化错误:
|
|
@@ -185,8 +262,8 @@ webhook handler 返回结构化 HTTP 响应,方便服务端处理:
|
|
|
185
262
|
**v1 限制**:见[不在范围内](#不在-v1-范围内)。
|
|
186
263
|
|
|
187
264
|
**v2 计划**(想优先哪个就开 issue):
|
|
188
|
-
-
|
|
189
|
-
-
|
|
265
|
+
- streaming image URL resolver
|
|
266
|
+
- 更完整的 HITL i18n
|
|
190
267
|
- 可选的 Redis 后端去重,支持多实例部署
|
|
191
268
|
- 用户级 OAuth(`user_access_token`)用于飞书 API 工具
|
|
192
269
|
|
|
@@ -196,11 +273,55 @@ webhook handler 返回结构化 HTTP 响应,方便服务端处理:
|
|
|
196
273
|
pnpm install
|
|
197
274
|
pnpm test # 跑 vitest 测试套件
|
|
198
275
|
pnpm test:watch # 交互式 watch
|
|
199
|
-
pnpm typecheck # tsc --noEmit
|
|
276
|
+
pnpm typecheck # TypeScript 7 RC tsc --noEmit
|
|
200
277
|
pnpm lint # eslint
|
|
201
|
-
pnpm build # tsup
|
|
278
|
+
pnpm build # tsup 产 JS + TypeScript 7 产声明 → dist/
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`typescript` 仍保留给需要 TypeScript API 的 JS 工具链,`typescript-7` 负责 CLI typecheck 和声明文件生成。
|
|
282
|
+
|
|
283
|
+
## 真实 Lark/飞书 E2E 测试
|
|
284
|
+
|
|
285
|
+
`pnpm test:e2e` 会加载 `.env.e2e.local`,但只有设置 `E2E_LARK=1` 时才会真正跑飞书 E2E。没设置这个开关时,Vitest 只收集文件并跳过 suite。
|
|
286
|
+
|
|
287
|
+
请使用一次性的测试群。这个 suite 会往 `E2E_LARK_CHAT_ID` 发送真实消息、卡片、reaction 和文件,并在测试开始/结束时往群里发摘要消息。
|
|
288
|
+
|
|
289
|
+
本地前置条件:
|
|
290
|
+
|
|
291
|
+
- 已安装 `lark-cli`,并且当前 user 身份已登录、在测试群内。测试会用 `--as user` 发文本/文件、拉取消息、添加/删除/列出 reaction、发现群内 bot。
|
|
292
|
+
- 应用 bot 已加入同一个测试群。
|
|
293
|
+
- 应用事件订阅使用**长连接**模式,并订阅 `im.message.receive_v1`、`card.action.trigger`、`im.message.reaction.created_v1` 和 `im.message.reaction.deleted_v1`。
|
|
294
|
+
- 应用 bot token 需要能发送/回复 IM 消息、发送交互卡片、调用 CardKit v2 卡片 API、添加/删除/列出消息 reaction、上传 IM 图片/文件、下载消息资源、转发/删除 bot 自己的消息、列群成员。飞书后台里打开对应的 IM/CardKit 权限即可;文件资源相关当前是 `im:resource`,不是 `im:resource:upload`。
|
|
295
|
+
- 默认 E2E 不会改群名、拉人或踢人;`updateChat`、`addChatMembers`、`removeChatMembers` 只做单元测试,避免破坏测试群状态。
|
|
296
|
+
- 从 `E2E_LARK_PORT` 开始的一组本地端口需要空闲。默认基准端口是 `23080`,suite 会从这里递增使用。
|
|
297
|
+
|
|
298
|
+
`.env.e2e.local` 已加入 gitignore。最小配置示例:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
E2E_LARK=1
|
|
302
|
+
E2E_LARK_CHAT_ID=oc_xxx
|
|
303
|
+
|
|
304
|
+
LARK_APP_ID=cli_xxx
|
|
305
|
+
LARK_APP_SECRET=xxx
|
|
306
|
+
LARK_VERIFICATION_TOKEN=xxx
|
|
307
|
+
LARK_ENCRYPT_KEY=xxx # 如果应用后台启用了加密就填;否则可不填
|
|
308
|
+
LARK_BASE_URL=https://open.feishu.cn
|
|
309
|
+
|
|
310
|
+
# 可选。不填时 suite 会用 lark-cli 在群里寻找唯一 bot。
|
|
311
|
+
E2E_LARK_BOT_OPEN_ID=ou_xxx
|
|
312
|
+
|
|
313
|
+
# 可选。默认 23080。
|
|
314
|
+
E2E_LARK_PORT=23080
|
|
202
315
|
```
|
|
203
316
|
|
|
317
|
+
运行:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
pnpm test:e2e
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
当前 suite 覆盖:出站 text/post/card/reaction/media API、`createLarkSender().sendPayload()` 的 text + 原生卡片 + media 编排、decorated mention normalization、encoded thread reply、message action adapter thread reply、本地 media realpath 安全边界、forward/delete/list members 的非破坏性动作、CardKit v2 streaming、长连接入站回复、bot-to-bot `allowBots` gate 和 peer mention delivery、ackReaction、同 chat 连续消息排队、引用回复、群聊 `@` 和非 `@` 消息、群 `requireMention`、群级 `systemPrompt`、群白名单、slash 命令、自定义卡片 action 的 reply/follow-up/edit、HITL text/select/multi-select 表单、freeform/重试/TTL、reaction 事件作为 synthetic input、文件入站和 resource download。单元测试额外覆盖 open_id/user_id target、message action adapter、音视频 duration 推断、私网 media URL 拒绝、merge-forward 展开 hook、完整卡片 fetch hook、doctor 权限/事件输出、streaming metrics/unavailable guard。
|
|
324
|
+
|
|
204
325
|
## 真实飞书应用冒烟测试
|
|
205
326
|
|
|
206
327
|
完整流程见 [`examples/README.md`](./examples/README.md)。TL;DR 跟[快速开始](#快速开始)一样:装依赖、填 `.env`、跑 `eve dev`。给 bot 发 `ping`,应该收到 `pong` 回复。
|
|
@@ -235,19 +356,23 @@ eve deploy # 或:在有公网 URL 的服务器上跑 eve start
|
|
|
235
356
|
|
|
236
357
|
```
|
|
237
358
|
test/
|
|
359
|
+
├── allowlist.spec.ts # DM/群白名单、requireMention 和群级 systemPrompt
|
|
360
|
+
├── ask-card.spec.ts # ask_question 卡片构造器
|
|
361
|
+
├── ask-flow.spec.ts # ask_question 渲染、回调、freeform、重试、TTL
|
|
362
|
+
├── asr.spec.ts # 可选音频/媒体转写
|
|
363
|
+
├── authorization.spec.ts # eve 授权卡片
|
|
364
|
+
├── cardkit-v2.spec.ts # CardKit v2 构造器
|
|
238
365
|
├── crypto.spec.ts # 签名 & AES 测试向量(含 round-trip helper)
|
|
239
366
|
├── dedup.spec.ts # TTL、FIFO 淘汰、惰性 sweep
|
|
367
|
+
├── diagnose.spec.ts # /lark 命令拦截和诊断
|
|
368
|
+
├── event-policy.spec.ts # app 归属、事件过期、abort 文本、reaction
|
|
240
369
|
├── options.spec.ts # env 回退、默认值、校验
|
|
370
|
+
├── outbound.spec.ts # 出站 sender/media/payload/mention/action helper
|
|
241
371
|
├── parse.spec.ts # text/image/file/post/mention fixtures
|
|
242
|
-
├── lark-client.spec.ts # token mutex、retry policy
|
|
372
|
+
├── lark-client.spec.ts # token mutex、retry policy、CardKit、reaction、resource
|
|
243
373
|
├── streaming-controller.spec.ts # FSM 状态转换、节流、降级
|
|
244
|
-
├──
|
|
245
|
-
├──
|
|
246
|
-
├── launcher-detection.spec.ts # eve start launcher 进程识别
|
|
247
|
-
├── long-connection.spec.ts # WSClient 单例守卫、转发签名/AES
|
|
248
|
-
├── channel.spec.ts # 端到端 webhook:校验、解密、去重、session 启动、ack reaction
|
|
249
|
-
├── ask-card.spec.ts # ask_question 卡片构造器
|
|
250
|
-
├── ask-flow.spec.ts # ask_question 端到端:渲染、点击回调、freeform 拦截
|
|
374
|
+
├── channel.spec.ts # webhook 处理、队列、abort、ack reaction
|
|
375
|
+
├── e2e/lark-real.spec.ts # opt-in 真实飞书/Lark E2E suite
|
|
251
376
|
└── helpers/
|
|
252
377
|
├── encrypt.ts # 仅测试用的 AES cipher 镜像
|
|
253
378
|
└── mock-fetch.ts # 替代 nock 的迷你 mock fetch
|