@minitool/feishu-bot 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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ 本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 与 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [0.1.0] - 2026-04-09
6
+
7
+ ### Added
8
+
9
+ - 初始版本 🎉
10
+ - `FeishuBot` 主类,支持自动签名与延迟配置校验
11
+ - 5 种消息类型:`text` / `post` / `image` / `share_chat` / `interactive`
12
+ - `sendImage` 智能识别 `image_key` 前缀、本地路径、`Buffer`、`Uint8Array`
13
+ - 透明的图片上传:内置 `TokenManager`(`tenant_access_token` 缓存 + 并发去重)与 `ImageUploader`
14
+ - 独立的消息构造器:`buildText` / `buildPost` / `buildImage` / `buildShareChat` / `buildInteractive`
15
+ - 完整 TypeScript 类型定义,ES Module + CommonJS 双格式输出
16
+ - 错误体系:`FeishuBotError` / `FeishuConfigError` / `FeishuApiError`
17
+ - 零运行时依赖(仅使用 Node 18+ 内置能力)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hidumou
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,221 @@
1
+ # @minitool/feishu-bot
2
+
3
+ > 轻量、零运行时依赖、TypeScript 优先的飞书自定义机器人 SDK。
4
+
5
+ - ✅ 支持全部 5 种消息类型:`text` / `post` / `image` / `share_chat` / `interactive`
6
+ - ✅ 透明处理图片上传:`sendImage('./local.png')` 自动走 `im/v1/images` 接口取 `image_key` 再发送
7
+ - ✅ 自动注入签名(HMAC-SHA256)
8
+ - ✅ `tenant_access_token` 自动缓存与刷新
9
+ - ✅ 仅依赖 Node 18+ 内置 `fetch` / `FormData` / `Blob` / `node:crypto` / `node:fs/promises`,零运行时依赖
10
+ - ✅ 构造期不抛错,便于「先 new 再注入配置」
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ pnpm add @minitool/feishu-bot
16
+ # 或
17
+ npm install @minitool/feishu-bot
18
+ ```
19
+
20
+ 要求 Node.js ≥ 18。
21
+
22
+ ## 快速开始
23
+
24
+ ```ts
25
+ import { FeishuBot } from '@minitool/feishu-bot';
26
+
27
+ // 从参数读
28
+ const bot = new FeishuBot({
29
+ webhook: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx',
30
+ secret: 'your-secret', // 可选,若机器人启用了「签名校验」
31
+ });
32
+
33
+ // 或完全从环境变量读(见下方「配置」小节)
34
+ const bot2 = new FeishuBot();
35
+
36
+ await bot.sendText('Hello 飞书!');
37
+ ```
38
+
39
+ ## 配置
40
+
41
+ 所有 `FeishuBotOptions` 字段都能通过环境变量提供默认值。**显式参数优先于环境变量**。
42
+
43
+ | 字段 | 环境变量 | 必需 | 说明 |
44
+ |---|---|---|---|
45
+ | `webhook` | `FEISHU_BOT_WEBHOOK` | ✅ | 机器人 webhook URL |
46
+ | `secret` | `FEISHU_BOT_SECRET` | 可选 | 启用签名校验时必填 |
47
+ | `appId` | `FEISHU_APP_ID` | 图片上传必需 | 自建应用 App ID |
48
+ | `appSecret` | `FEISHU_APP_SECRET` | 图片上传必需 | 自建应用 App Secret |
49
+ | `fetch` | — | 可选 | 注入自定义 fetch,测试用 |
50
+ | `timeout` | — | 可选 | 请求超时,单位毫秒,默认 `10000` |
51
+ | `baseUrl` | — | 可选 | 飞书开放平台基础 URL,默认 `https://open.feishu.cn` |
52
+
53
+ > SDK 本身不引入 `dotenv`。如果你想用 `.env` 文件,可以通过 `node --env-file=.env app.js`(Node 20.6+)或在项目 devDep 里装 `dotenv` 自行预加载。
54
+
55
+ ## 发送消息
56
+
57
+ ### text 文本消息(含 @)
58
+
59
+ ```ts
60
+ await bot.sendText('部署完成 ✅');
61
+
62
+ // @ 所有人(仅群内有效)
63
+ await bot.sendText('请注意', { atAll: true });
64
+
65
+ // @ 指定用户(需要 open_id)
66
+ await bot.sendText('请看', { atUserIds: ['ou_xxxxx', 'ou_yyyyy'] });
67
+ ```
68
+
69
+ ### post 富文本
70
+
71
+ ```ts
72
+ await bot.sendPost({
73
+ zh_cn: {
74
+ title: '发布通知',
75
+ content: [
76
+ [
77
+ { tag: 'text', text: '版本 ' },
78
+ { tag: 'text', text: 'v1.2.0' },
79
+ { tag: 'text', text: ' 已上线,查看' },
80
+ { tag: 'a', text: '详情', href: 'https://example.com/release' },
81
+ ],
82
+ [{ tag: 'at', user_id: 'ou_xxx' }],
83
+ ],
84
+ },
85
+ });
86
+ ```
87
+
88
+ ### image 图片消息
89
+
90
+ `sendImage` 会根据入参类型自动选择行为:
91
+
92
+ ```ts
93
+ // 1. 已有 image_key(以 `img_` 开头)→ 直发
94
+ await bot.sendImage('img_v2_041b28e3-xxx');
95
+
96
+ // 2. 本地文件路径 → 自动上传再发(需要 appId/appSecret)
97
+ await bot.sendImage('./screenshot.png');
98
+
99
+ // 3. Buffer / Uint8Array → 自动上传再发
100
+ import { readFile } from 'node:fs/promises';
101
+ const buf = await readFile('./screenshot.png');
102
+ await bot.sendImage(buf);
103
+
104
+ // 也可以只拿 image_key,稍后自己复用
105
+ const imageKey = await bot.uploadImage('./screenshot.png');
106
+ await bot.sendImage(imageKey);
107
+ ```
108
+
109
+ > ⚠️ 图片上传需要自建应用的 App ID / App Secret,因为飞书 `im/v1/images` 接口要求 `tenant_access_token` 授权。
110
+
111
+ ### share_chat 分享群名片
112
+
113
+ ```ts
114
+ await bot.sendShareChat('oc_xxxxxxx');
115
+ ```
116
+
117
+ ### interactive 卡片
118
+
119
+ 直接透传 card 结构(支持 schema 2.0 或旧版):
120
+
121
+ ```ts
122
+ await bot.sendInteractive({
123
+ schema: '2.0',
124
+ header: {
125
+ title: { tag: 'plain_text', content: '构建完成' },
126
+ template: 'green',
127
+ },
128
+ body: {
129
+ elements: [
130
+ { tag: 'markdown', content: '**提交人**: @xxx\n**分支**: main' },
131
+ ],
132
+ },
133
+ });
134
+ ```
135
+
136
+ ## 错误处理
137
+
138
+ ```ts
139
+ import { FeishuBot, FeishuConfigError, FeishuApiError } from '@minitool/feishu-bot';
140
+
141
+ try {
142
+ await bot.sendText('hi');
143
+ } catch (err) {
144
+ if (err instanceof FeishuConfigError) {
145
+ // 缺配置:webhook / secret / appId / appSecret
146
+ console.error('配置错误:', err.message);
147
+ } else if (err instanceof FeishuApiError) {
148
+ // 业务错误:包含飞书返回的 code 与完整 response
149
+ console.error(`飞书 API 错误 code=${err.code}:`, err.message);
150
+ console.error('完整响应:', err.response);
151
+ } else {
152
+ throw err;
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## 签名校验
158
+
159
+ 在飞书机器人「安全设置 → 签名校验」开启后,记录下密钥并传给 SDK:
160
+
161
+ ```ts
162
+ const bot = new FeishuBot({ webhook: '...', secret: 'xxxx' });
163
+ // SDK 会在每次发送时自动附加 timestamp + sign
164
+ ```
165
+
166
+ 签名算法(见 `src/signer.ts`):
167
+
168
+ ```
169
+ stringToSign = `${timestamp}\n${secret}`
170
+ sign = Base64(HmacSHA256(key = stringToSign, data = ''))
171
+ ```
172
+
173
+ 注意:这是飞书反直觉的地方 —— HMAC 的 `key` 是 `stringToSign`,`data` 是空字符串。
174
+
175
+ ## API 参考
176
+
177
+ ### `new FeishuBot(options?)`
178
+
179
+ | 方法 | 说明 |
180
+ |---|---|
181
+ | `send(payload)` | 原子发送,接受已构造好的 `MessagePayload` |
182
+ | `sendText(text, { atUserIds?, atAll? })` | 文本消息 |
183
+ | `sendPost(post)` | 富文本 |
184
+ | `sendImage(input)` | 图片:`string`(`img_` 前缀→直发 / 其它→路径上传)、`Buffer`、`Uint8Array` |
185
+ | `sendShareChat(shareChatId)` | 分享群名片 |
186
+ | `sendInteractive(card)` | 卡片 |
187
+ | `uploadImage(file)` | 单独上传图片,返回 `image_key` |
188
+
189
+ 所有方法返回 `Promise<FeishuApiResponse>`;`code !== 0` 时抛 `FeishuApiError`。
190
+
191
+ ### 独立使用消息构造器
192
+
193
+ 如果你只需要构造 payload 而不发送:
194
+
195
+ ```ts
196
+ import { buildText, buildPost, buildImage } from '@minitool/feishu-bot';
197
+
198
+ const payload = buildText('hi', { atAll: true });
199
+ // => { msg_type: 'text', content: { text: 'hi <at user_id="all">所有人</at>' } }
200
+ ```
201
+
202
+ ## 频控与限制
203
+
204
+ 飞书官方规则(每个机器人独立计数):
205
+
206
+ - `100` 次/分钟
207
+ - `5` 次/秒
208
+ - body 大小 `≤ 20KB`
209
+
210
+ SDK 不做内置限流;请在调用方按需排队或节流。
211
+
212
+ ## Roadmap
213
+
214
+ 下面是计划在 v0.2 加入的特性(当前版本已评估但延后):
215
+
216
+ - **更细粒度的错误类型**:在 `FeishuApiError` 之上拆分 `FeishuNetworkError` / `FeishuTimeoutError` / `FeishuHttpError` 子类(或在当前类上加 `kind` 字段),便于调用方区分超时、网络抖动、HTTP 状态码错误与业务 code。
217
+ - **图片上传自定义元数据**:`uploadImage` / `sendImage` 支持 `{ filename, contentType }` 选项,用于 Buffer/Uint8Array 入参时指定文件名与 MIME。
218
+
219
+ ## 许可
220
+
221
+ MIT © hidumou