eve-lark 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/README.zh-CN.md +222 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +1075 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eve-lark contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# eve-lark
|
|
2
|
+
|
|
3
|
+
English | [简体中文](./README.zh-CN.md)
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
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)
|
|
14
|
+
|
|
15
|
+
**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`
|
|
19
|
+
|
|
20
|
+
**Security**
|
|
21
|
+
- `X-Lark-Signature` verification (`sha256(timestamp + nonce + encrypt_key + body)`, constant-time)
|
|
22
|
+
- AES-256-CBC decryption of the `encrypt` envelope when `encryptKey` is configured
|
|
23
|
+
- Timestamp skew window (5 min default)
|
|
24
|
+
- Bot self-message suppression
|
|
25
|
+
|
|
26
|
+
**Both Feishu and Lark** are supported via a single `baseUrl` switch.
|
|
27
|
+
|
|
28
|
+
### Out of scope (v1)
|
|
29
|
+
|
|
30
|
+
These are intentionally **not** shipped — file an issue if you need them:
|
|
31
|
+
- Audio / media / sticker / share_chat / share_user inbound (ack-and-skip only)
|
|
32
|
+
- Multi-account configuration
|
|
33
|
+
- Per-user OAuth (`user_access_token` device flow)
|
|
34
|
+
- Feishu API tools (docs / bitable / calendar / tasks / drive)
|
|
35
|
+
- Card action buttons (no interactive form handling)
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
Install:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add eve-lark
|
|
43
|
+
# or: npm install eve-lark / yarn add eve-lark
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Create the channel in your eve agent:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// agent/channels/lark.ts
|
|
50
|
+
import { createLarkChannel } from "eve-lark";
|
|
51
|
+
|
|
52
|
+
export default createLarkChannel({
|
|
53
|
+
appId: process.env.LARK_APP_ID!,
|
|
54
|
+
appSecret: process.env.LARK_APP_SECRET!,
|
|
55
|
+
verificationToken: process.env.LARK_VERIFICATION_TOKEN!,
|
|
56
|
+
encryptKey: process.env.LARK_ENCRYPT_KEY,
|
|
57
|
+
botOpenId: process.env.LARK_BOT_OPEN_ID,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then in the [Feishu developer console](https://open.feishu.cn/app) (or [Lark developer console](https://open.larksuite.com/app)):
|
|
62
|
+
|
|
63
|
+
1. Create a **Custom App**. Note the `App ID` and `App Secret`.
|
|
64
|
+
2. Under **Event Subscriptions**, set the request URL to your agent's `/lark/webhook` (override with the `webhookPath` option).
|
|
65
|
+
3. Generate a **Verification Token** and an **Encrypt Key** — copy both into your env.
|
|
66
|
+
4. Subscribe to the `im.message.receive_v1` event.
|
|
67
|
+
5. Add the bot to a chat or DM it directly.
|
|
68
|
+
|
|
69
|
+
## Configuration reference
|
|
70
|
+
|
|
71
|
+
All fields can be supplied as options or read from the matching env var (options win).
|
|
72
|
+
|
|
73
|
+
| Field | Type | Required | Default | Env var |
|
|
74
|
+
|---|---|---|---|---|
|
|
75
|
+
| `appId` | `string` | yes | — | `LARK_APP_ID` |
|
|
76
|
+
| `appSecret` | `string` | yes | — | `LARK_APP_SECRET` |
|
|
77
|
+
| `verificationToken` | `string` | yes | — | `LARK_VERIFICATION_TOKEN` |
|
|
78
|
+
| `encryptKey` | `string` | no | — | `LARK_ENCRYPT_KEY` |
|
|
79
|
+
| `baseUrl` | `string` | no | `https://open.feishu.cn` | `LARK_BASE_URL` |
|
|
80
|
+
| `botOpenId` | `string` | no | — | `LARK_BOT_OPEN_ID` |
|
|
81
|
+
| `webhookPath` | `string` | no | `/lark/webhook` | — |
|
|
82
|
+
| `replyMode` | `"streaming" \| "static"` | no | `"streaming"` | — |
|
|
83
|
+
| `streamPatchIntervalMs` | `number` | no | `1000` | — |
|
|
84
|
+
| `streamCreateThresholdMs` | `number` | no | `400` | — |
|
|
85
|
+
| `dedupTtlMs` | `number` | no | `1_800_000` (30 min) | — |
|
|
86
|
+
| `dedupMaxEntries` | `number` | no | `5_000` | — |
|
|
87
|
+
| `requestTimeoutMs` | `number` | no | `15_000` | — |
|
|
88
|
+
| `maxRetries` | `number` | no | `2` | — |
|
|
89
|
+
| `tokenRefreshBufferMs` | `number` | no | `300_000` (5 min) | — |
|
|
90
|
+
| `signatureSkewMs` | `number` | no | `300_000` (5 min) | — |
|
|
91
|
+
| `fetch` | `typeof fetch` | no | `globalThis.fetch` | — |
|
|
92
|
+
|
|
93
|
+
## Feishu vs Lark (international)
|
|
94
|
+
|
|
95
|
+
The two deployments speak the same API. Switch with `baseUrl`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
createLarkChannel({
|
|
99
|
+
baseUrl: "https://open.larksuite.com", // international
|
|
100
|
+
// ...
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or via env: `LARK_BASE_URL=https://open.larksuite.com`.
|
|
105
|
+
|
|
106
|
+
## Streaming vs static mode
|
|
107
|
+
|
|
108
|
+
- **`streaming`** (default): the channel creates an interactive card on the first delta, throttles live patches (~1s), and finalizes when the turn completes. Best UX.
|
|
109
|
+
- **`static`**: the channel waits for `message.completed` and delivers a single card with the full answer. Lower API call volume; useful if you hit Feishu's PATCH rate limit.
|
|
110
|
+
|
|
111
|
+
Tune the throttle with `streamPatchIntervalMs` (lower = smoother, more API calls).
|
|
112
|
+
|
|
113
|
+
## Continuation tokens & threading
|
|
114
|
+
|
|
115
|
+
eve-lark uses the chat id plus the threaded root message id as the session continuation token:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<chat_id>:<root_message_id>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For top-level conversations the root is `_`:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
oc_xxx:_ — top-level
|
|
125
|
+
oc_xxx:om_yyy — reply inside the om_yyy thread
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A reply inside a thread keeps the thread anchor across turns. The token is namespaced by the channel id (eve's framework prepends the channel file stem), so it's safe to ship multiple custom channels alongside `lark`.
|
|
129
|
+
|
|
130
|
+
## Security model
|
|
131
|
+
|
|
132
|
+
- **Signature verification**: when `encryptKey` is set, every inbound webhook must carry a valid `X-Lark-Signature` header. Mismatch returns HTTP 401.
|
|
133
|
+
- **AES decryption**: with `encryptKey` set, the `encrypt` envelope is decrypted using AES-256-CBC with `key = SHA256(encrypt_key)` and the first 16 bytes as IV.
|
|
134
|
+
- **Timestamp skew**: requests older than `signatureSkewMs` are rejected with HTTP 408.
|
|
135
|
+
- **Dedup**: `event_id` is remembered for `dedupTtlMs`. Replays return 200 without re-starting a turn.
|
|
136
|
+
- **Serverless caveat**: dedup is in-process. Multi-instance deployments may double-process an event under rare timing windows — make your tools idempotent.
|
|
137
|
+
|
|
138
|
+
## File & image inbound
|
|
139
|
+
|
|
140
|
+
Inbound image/file messages are converted into eve `UserContent` file parts. The `data` field is a `URL` pointing at the Lark resource endpoint, so eve's pipeline calls the channel's `fetchFile` hook (which uses the bot's `tenant_access_token`) to stage the bytes for the model.
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
## Errors
|
|
145
|
+
|
|
146
|
+
eve-lark throws a small typed hierarchy:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
LarkChannelError
|
|
150
|
+
├── LarkConfigError — missing required option
|
|
151
|
+
├── LarkSignatureError — signature verify failed (rarely thrown; usually a 401 Response)
|
|
152
|
+
├── LarkDecryptError — AES decrypt failed
|
|
153
|
+
└── LarkApiError — Lark API call failed (carries .code, .status, .body)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The webhook handler returns structured HTTP responses for predictable server-side handling:
|
|
157
|
+
|
|
158
|
+
| Status | Cause |
|
|
159
|
+
|---|---|
|
|
160
|
+
| 200 | Ack (success or intentionally ignored event) |
|
|
161
|
+
| 400 | Invalid JSON / decrypt failure |
|
|
162
|
+
| 401 | Signature missing/invalid or verification token mismatch |
|
|
163
|
+
| 408 | Timestamp skew window exceeded |
|
|
164
|
+
|
|
165
|
+
## Limitations & roadmap
|
|
166
|
+
|
|
167
|
+
**v1 limitations**: see [Out of scope](#out-of-scope-v1).
|
|
168
|
+
|
|
169
|
+
**Planned for v2** (open an issue if you'd like to prioritize any):
|
|
170
|
+
- Card action button handling (interactive forms, confirmation flows)
|
|
171
|
+
- Audio / media inbound transcription
|
|
172
|
+
- Optional Redis-backed dedup for multi-instance deployments
|
|
173
|
+
- Per-user OAuth (`user_access_token`) for Feishu API tools
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm test # run the vitest suite
|
|
180
|
+
pnpm test:watch # interactive watch mode
|
|
181
|
+
pnpm typecheck # tsc --noEmit
|
|
182
|
+
pnpm lint # eslint
|
|
183
|
+
pnpm build # tsup build → dist/
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Smoke testing against a real Feishu app
|
|
187
|
+
|
|
188
|
+
See [`examples/README.md`](./examples/README.md) for a two-process setup that uses the Feishu long-connection transport (no public webhook URL needed). The TL;DR:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pnpm build # build eve-lark so the agent can import it
|
|
192
|
+
cp examples/agent/.env.example examples/agent/.env
|
|
193
|
+
$EDITOR examples/agent/.env # fill in credentials
|
|
194
|
+
cd examples/agent && pnpm install
|
|
195
|
+
# Terminal A:
|
|
196
|
+
cd examples/agent && pnpm dev # eve dev server
|
|
197
|
+
# Terminal B (from repo root):
|
|
198
|
+
pnpm tsx examples/ws-forwarder.ts # Feishu WS → localhost HTTP
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Send `ping` to the bot in Feishu; expect `pong` back as a streaming card.
|
|
202
|
+
|
|
203
|
+
Test layout:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
test/
|
|
207
|
+
├── crypto.spec.ts # signature & AES vectors (including a round-trip helper)
|
|
208
|
+
├── dedup.spec.ts # TTL, FIFO eviction, lazy sweep
|
|
209
|
+
├── options.spec.ts # env fallback, defaults, validation
|
|
210
|
+
├── parse.spec.ts # text/image/file/post/mention fixtures
|
|
211
|
+
├── lark-client.spec.ts # token mutex, retry policy (429/5xx/401), nock-equivalent mock
|
|
212
|
+
├── streaming-controller.spec.ts # FSM transitions, throttle, fallback
|
|
213
|
+
├── card.spec.ts # card builders
|
|
214
|
+
├── channel.spec.ts # end-to-end webhook: verify, decrypt, dedup, session start, streaming wire-up
|
|
215
|
+
└── helpers/
|
|
216
|
+
├── encrypt.ts # test-only AES cipher mirror
|
|
217
|
+
└── mock-fetch.ts # tiny mock fetch used in place of nock for native-fetch compat
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT — see [LICENSE](./LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# eve-lark
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
一个为 [eve](https://eve.dev) agent 框架打造的 [Lark](https://www.larksuite.com) / [Feishu](https://www.feishu.cn) 通道。把工厂函数放到 `agent/channels/lark.ts`,eve 就会挂载一个 Lark webhook,把收到的私聊消息和群组 @ 提及转换成流式交互卡片回复。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
**入站**
|
|
10
|
+
- 文本、富文本(`post`)、`@` 提及(包括 `@all`)
|
|
11
|
+
- 图片和文件附件(服务端下载并暂存给模型使用)
|
|
12
|
+
- 通过 `root_id` / `parent_id` 实现的话题回复
|
|
13
|
+
- `event_id` 去重(处理 Feishu 的 at-least-once 重试)
|
|
14
|
+
|
|
15
|
+
**出站**
|
|
16
|
+
- 流式交互卡片(在对话过程中实时 patch 更新)—— 默认模式
|
|
17
|
+
- 静态一次性卡片回复 —— 可配置
|
|
18
|
+
- 话题回复保留原始 `root_id`
|
|
19
|
+
|
|
20
|
+
**安全**
|
|
21
|
+
- `X-Lark-Signature` 签名校验(`sha256(timestamp + nonce + encrypt_key + body)`,恒定时间比较)
|
|
22
|
+
- 当配置了 `encryptKey` 时,对 `encrypt` 信封进行 AES-256-CBC 解密
|
|
23
|
+
- 时间戳偏移窗口(默认 5 分钟)
|
|
24
|
+
- 抑制 bot 自身发出的消息
|
|
25
|
+
|
|
26
|
+
**Feishu 和 Lark 都支持**,只需切换一个 `baseUrl` 即可。
|
|
27
|
+
|
|
28
|
+
### v1 暂不支持
|
|
29
|
+
|
|
30
|
+
以下能力**有意**没有发布 —— 如果需要请提 issue:
|
|
31
|
+
- 入站的音频 / 媒体 / 贴纸 / share_chat / share_user(仅 ack 并跳过)
|
|
32
|
+
- 多账号配置
|
|
33
|
+
- 用户级 OAuth(`user_access_token` 设备流程)
|
|
34
|
+
- Feishu API 工具(文档 / 多维表格 / 日历 / 任务 / 云盘)
|
|
35
|
+
- 卡片动作按钮(不支持交互式表单)
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add eve-lark
|
|
43
|
+
# 或者:npm install eve-lark / yarn add eve-lark
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
在你的 eve agent 中创建通道:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// agent/channels/lark.ts
|
|
50
|
+
import { createLarkChannel } from "eve-lark";
|
|
51
|
+
|
|
52
|
+
export default createLarkChannel({
|
|
53
|
+
appId: process.env.LARK_APP_ID!,
|
|
54
|
+
appSecret: process.env.LARK_APP_SECRET!,
|
|
55
|
+
verificationToken: process.env.LARK_VERIFICATION_TOKEN!,
|
|
56
|
+
encryptKey: process.env.LARK_ENCRYPT_KEY,
|
|
57
|
+
botOpenId: process.env.LARK_BOT_OPEN_ID,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
然后在 [Feishu 开发者后台](https://open.feishu.cn/app)(或 [Lark 开发者后台](https://open.larksuite.com/app))中:
|
|
62
|
+
|
|
63
|
+
1. 创建一个**自建应用**,记录 `App ID` 和 `App Secret`。
|
|
64
|
+
2. 在**事件订阅**中,把请求 URL 设置为你的 agent 的 `/lark/webhook`(可通过 `webhookPath` 选项覆盖)。
|
|
65
|
+
3. 生成 **Verification Token** 和 **Encrypt Key** —— 都复制到你的环境变量里。
|
|
66
|
+
4. 订阅 `im.message.receive_v1` 事件。
|
|
67
|
+
5. 把 bot 拉进群组或直接私聊。
|
|
68
|
+
|
|
69
|
+
## 配置参考
|
|
70
|
+
|
|
71
|
+
所有字段都可以通过选项传入,或从对应的环境变量读取(选项优先)。
|
|
72
|
+
|
|
73
|
+
| 字段 | 类型 | 必填 | 默认值 | 环境变量 |
|
|
74
|
+
|---|---|---|---|---|
|
|
75
|
+
| `appId` | `string` | 是 | — | `LARK_APP_ID` |
|
|
76
|
+
| `appSecret` | `string` | 是 | — | `LARK_APP_SECRET` |
|
|
77
|
+
| `verificationToken` | `string` | 是 | — | `LARK_VERIFICATION_TOKEN` |
|
|
78
|
+
| `encryptKey` | `string` | 否 | — | `LARK_ENCRYPT_KEY` |
|
|
79
|
+
| `baseUrl` | `string` | 否 | `https://open.feishu.cn` | `LARK_BASE_URL` |
|
|
80
|
+
| `botOpenId` | `string` | 否 | — | `LARK_BOT_OPEN_ID` |
|
|
81
|
+
| `webhookPath` | `string` | 否 | `/lark/webhook` | — |
|
|
82
|
+
| `replyMode` | `"streaming" \| "static"` | 否 | `"streaming"` | — |
|
|
83
|
+
| `streamPatchIntervalMs` | `number` | 否 | `1000` | — |
|
|
84
|
+
| `streamCreateThresholdMs` | `number` | 否 | `400` | — |
|
|
85
|
+
| `dedupTtlMs` | `number` | 否 | `1_800_000`(30 分钟) | — |
|
|
86
|
+
| `dedupMaxEntries` | `number` | 否 | `5_000` | — |
|
|
87
|
+
| `requestTimeoutMs` | `number` | 否 | `15_000` | — |
|
|
88
|
+
| `maxRetries` | `number` | 否 | `2` | — |
|
|
89
|
+
| `tokenRefreshBufferMs` | `number` | 否 | `300_000`(5 分钟) | — |
|
|
90
|
+
| `signatureSkewMs` | `number` | 否 | `300_000`(5 分钟) | — |
|
|
91
|
+
| `fetch` | `typeof fetch` | 否 | `globalThis.fetch` | — |
|
|
92
|
+
|
|
93
|
+
## Feishu 与 Lark(国际版)
|
|
94
|
+
|
|
95
|
+
两套部署使用相同的 API。通过 `baseUrl` 切换:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
createLarkChannel({
|
|
99
|
+
baseUrl: "https://open.larksuite.com", // 国际版
|
|
100
|
+
// ...
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
或通过环境变量:`LARK_BASE_URL=https://open.larksuite.com`。
|
|
105
|
+
|
|
106
|
+
## 流式 vs 静态模式
|
|
107
|
+
|
|
108
|
+
- **`streaming`**(默认):通道在第一个 delta 时创建交互卡片,节流地实时 patch(约 1 秒一次),并在回合结束时收尾。用户体验最好。
|
|
109
|
+
- **`static`**:通道等待 `message.completed`,然后一次性发送包含完整答案的卡片。API 调用量更低;当你撞上 Feishu 的 PATCH 限流时有用。
|
|
110
|
+
|
|
111
|
+
通过 `streamPatchIntervalMs` 调节节流间隔(值越小越平滑,API 调用越多)。
|
|
112
|
+
|
|
113
|
+
## 续接 token 与话题
|
|
114
|
+
|
|
115
|
+
eve-lark 使用 chat id 加上话题根消息 id 作为会话续接 token:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<chat_id>:<root_message_id>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
对于顶层会话,root 是 `_`:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
oc_xxx:_ — 顶层对话
|
|
125
|
+
oc_xxx:om_yyy — om_yyy 话题中的回复
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
话题中的回复会跨回合保持话题锚点。该 token 按通道 id 命名空间化(eve 框架会前置通道文件名),所以同时部署多个自定义通道与 `lark` 共存是安全的。
|
|
129
|
+
|
|
130
|
+
## 安全模型
|
|
131
|
+
|
|
132
|
+
- **签名校验**:当设置了 `encryptKey` 时,每个入站 webhook 必须携带有效的 `X-Lark-Signature` 头。不匹配返回 HTTP 401。
|
|
133
|
+
- **AES 解密**:设置了 `encryptKey` 时,使用 AES-256-CBC 解密 `encrypt` 信封,其中 `key = SHA256(encrypt_key)`,IV 取前 16 字节。
|
|
134
|
+
- **时间戳偏移**:早于 `signatureSkewMs` 的请求会以 HTTP 408 拒绝。
|
|
135
|
+
- **去重**:`event_id` 会被记住 `dedupTtlMs` 时间。重放返回 200 但不会重新启动回合。
|
|
136
|
+
- **Serverless 注意事项**:去重是进程内的。多实例部署在极端时序窗口下可能会重复处理同一事件 —— 请把你的工具做成幂等的。
|
|
137
|
+
|
|
138
|
+
## 文件与图片入站
|
|
139
|
+
|
|
140
|
+
入站的图片/文件消息会被转换成 eve 的 `UserContent` 文件 part。其 `data` 字段是一个指向 Lark 资源端点的 `URL`,所以 eve 的管道会调用通道的 `fetchFile` 钩子(使用 bot 的 `tenant_access_token`)把字节暂存给模型。
|
|
141
|
+
|
|
142
|
+
如果你希望 URL part 直接透传而不暂存字节(例如在 eve sandbox 之外运行),不要设置 `encryptKey`,并在你的工具里检查 `attributes`。
|
|
143
|
+
|
|
144
|
+
## 错误
|
|
145
|
+
|
|
146
|
+
eve-lark 抛出一个小的有类型层次结构:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
LarkChannelError
|
|
150
|
+
├── LarkConfigError — 缺少必填选项
|
|
151
|
+
├── LarkSignatureError — 签名校验失败(很少抛出;通常返回 401 Response)
|
|
152
|
+
├── LarkDecryptError — AES 解密失败
|
|
153
|
+
└── LarkApiError — Lark API 调用失败(携带 .code、.status、.body)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
webhook 处理器返回结构化的 HTTP 响应,方便服务端处理:
|
|
157
|
+
|
|
158
|
+
| 状态码 | 原因 |
|
|
159
|
+
|---|---|
|
|
160
|
+
| 200 | Ack(成功或故意忽略的事件) |
|
|
161
|
+
| 400 | JSON 无效 / 解密失败 |
|
|
162
|
+
| 401 | 签名缺失/无效,或 verification token 不匹配 |
|
|
163
|
+
| 408 | 超出时间戳偏移窗口 |
|
|
164
|
+
|
|
165
|
+
## 限制与路线图
|
|
166
|
+
|
|
167
|
+
**v1 限制**:见 [暂不支持](#v1-暂不支持)。
|
|
168
|
+
|
|
169
|
+
**v2 规划**(如果你希望优先实现某项,欢迎提 issue):
|
|
170
|
+
- 卡片动作按钮处理(交互式表单、确认流程)
|
|
171
|
+
- 音频 / 媒体入站转写
|
|
172
|
+
- 可选的 Redis 支持的多实例去重
|
|
173
|
+
- 用户级 OAuth(`user_access_token`),用于 Feishu API 工具
|
|
174
|
+
|
|
175
|
+
## 开发
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pnpm install
|
|
179
|
+
pnpm test # 运行 vitest 测试套件
|
|
180
|
+
pnpm test:watch # 交互式 watch 模式
|
|
181
|
+
pnpm typecheck # tsc --noEmit
|
|
182
|
+
pnpm lint # eslint
|
|
183
|
+
pnpm build # tsup 构建 → dist/
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 对接真实 Feishu 应用做冒烟测试
|
|
187
|
+
|
|
188
|
+
参见 [`examples/README.md`](./examples/README.md),其中介绍了一种双进程的搭建方式,使用 Feishu 的长连接传输(无需公网 webhook URL)。简单来说:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pnpm build # 构建 eve-lark,让 agent 能 import
|
|
192
|
+
cp examples/agent/.env.example examples/agent/.env
|
|
193
|
+
$EDITOR examples/agent/.env # 填入凭据
|
|
194
|
+
cd examples/agent && pnpm install
|
|
195
|
+
# 终端 A:
|
|
196
|
+
cd examples/agent && pnpm dev # eve dev server
|
|
197
|
+
# 终端 B(在 repo 根目录):
|
|
198
|
+
pnpm tsx examples/ws-forwarder.ts # Feishu WS → localhost HTTP
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
在 Feishu 中向 bot 发送 `ping`,应该能收到 `pong` 的流式卡片回复。
|
|
202
|
+
|
|
203
|
+
测试布局:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
test/
|
|
207
|
+
├── crypto.spec.ts # 签名 & AES 向量(包含一个 round-trip 辅助函数)
|
|
208
|
+
├── dedup.spec.ts # TTL、FIFO 淘汰、惰性清理
|
|
209
|
+
├── options.spec.ts # env 回退、默认值、校验
|
|
210
|
+
├── parse.spec.ts # text/image/file/post/mention fixture
|
|
211
|
+
├── lark-client.spec.ts # token 互斥锁、重试策略(429/5xx/401)、nock 等价的 mock
|
|
212
|
+
├── streaming-controller.spec.ts # FSM 状态转换、节流、降级
|
|
213
|
+
├── card.spec.ts # 卡片构建器
|
|
214
|
+
├── channel.spec.ts # 端到端 webhook:校验、解密、去重、会话启动、流式串联
|
|
215
|
+
└── helpers/
|
|
216
|
+
├── encrypt.ts # 仅测试用的 AES 加密镜像
|
|
217
|
+
└── mock-fetch.ts # 替代 nock 的微型 mock fetch(兼容原生 fetch)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 协议
|
|
221
|
+
|
|
222
|
+
MIT —— 见 [LICENSE](./LICENSE)。
|