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 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).
@@ -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)。