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 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 mentions into streamed interactive-card replies.
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`), `@`-mentions (including `@all`)
11
- - Image and file attachments (downloaded server-side and staged for the model)
12
- - Threading via `root_id` / `parent_id`
13
- - `event_id` deduplication (handles Feishu's at-least-once retries)
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
- - Streaming interactive card (patched live during the turn) default
17
- - Static single-shot card reply — configurable
18
- - Threaded replies preserve the original `root_id`
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
- - Bot self-message suppression
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 with one button per option (`primary` / `default` / `danger` styles map to Feishu button types). 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 the next inbound message in the same chat is intercepted as the answer. After an answer, the card is patched in place (buttons removed, selected option shown with a green ✓).
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-text inbound beyond image / file / audio: sticker / share_chat / share_user / interactive cards (ack-and-skip). Audio inbound is transcribed when an `asrProvider` is configured; without one it is also ack-and-skip.
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
- | `ackReaction` | `string \| readonly string[] \| false` | no | `"TYPING"` | — |
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 an interactive card on the first delta and live-patches it through Feishu's CardKit v2 (`schema: "2.0"` + `streaming_mode`). Best live UX this channel ships. Card text renders smaller than native chat messages — Feishu treats cards as "structured content".
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
- - Card action button handling (interactive forms, confirmation flows)
187
- - Audio / media inbound transcription
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 (429/5xx/401), nock-equivalent mock
372
+ ├── lark-client.spec.ts # token mutex, retry policy, CardKit, reactions, resources
241
373
  ├── streaming-controller.spec.ts # FSM transitions, throttle, fallback
242
- ├── card.spec.ts # card builders
243
- ├── channel.spec.ts # end-to-end webhook: verify, decrypt, dedup, session start, streaming wire-up
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`)、`@`-提及(包括 `@all`)
11
- - 图片和文件附件(服务端下载并 stage 给模型)
12
- - 通过 `root_id` / `parent_id` 跟踪线程
13
- - `event_id` 去重(应对飞书 at-least-once 重试)
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
- - 流式交互卡片(对话过程中实时 patch)—— 可选模式
17
- - `post` 富文本消息(**默认**,渲染为原生聊天消息大小,支持 markdown)
18
- - 静态一次性卡片 —— 可选
19
- - 线程回复保留原始 `root_id`
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
- - 抑制 bot 自己发的消息
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 会把提示渲染成一张飞书交互卡片,每个选项对应一个按钮(option.style `primary` / `default` / `danger` 直接映射到飞书 button type)。用户点击触发 `card.action.trigger` 回调,channel 把答案作为 `InputResponse` 发回 eve,parked session 恢复。`allowFreeform: true` 允许用户直接回复普通聊天消息代替点击——下一条同 chat 内的消息会被拦截为答案。回答后卡片原地更新(移除按钮,选中项以绿色 标记)。
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
- 以下功能**未实现**——需要的话请提 issue:
34
- - 图片 / 文件 / 音频以外的非文本入站:sticker / share_chat / share_user / 交互卡片(ack-and-skip)。音频入站在配置了 `asrProvider` 时会转写;没配置则同样是 ack-and-skip。
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 时创建交互卡片,通过飞书 CardKit v2(`schema: "2.0"` + `streaming_mode`)实时 patch。**是这个 channel 能提供的最好的实时 UX**。卡片文字比原生消息字号小(飞书把卡片当作「结构化内容」)。
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
- - 完全自定义的卡片交互(agent 自渲染表单、确认流)
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 build → dist/
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(429/5xx/401)、mock fetch
372
+ ├── lark-client.spec.ts # token mutex、retry policy、CardKit、reaction、resource
243
373
  ├── streaming-controller.spec.ts # FSM 状态转换、节流、降级
244
- ├── card.spec.ts # 卡片构造器
245
- ├── feishu-emoji.spec.ts # 飞书 emoji 白名单
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