@zhin.js/adapter-lark 1.0.1

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,10 @@
1
+ # @zhin.js/adapter-lark
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [efdd58a]
8
+ - @zhin.js/types@1.0.1
9
+ - zhin.js@1.0.1
10
+ - @zhin.js/http@1.0.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 凉菜
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,651 @@
1
+ # @zhin.js/adapter-lark
2
+
3
+ Zhin.js 飞书/Lark 适配器,支持飞书(中国版)和 Lark(国际版)机器人开发。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @zhin.js/adapter-lark
9
+ ```
10
+
11
+ ## 配置
12
+
13
+ ### 基础配置
14
+
15
+ ```typescript
16
+ import { LarkBotConfig } from '@zhin.js/adapter-lark';
17
+
18
+ const config: LarkBotConfig = {
19
+ context: 'lark',
20
+ name: 'my-lark-bot',
21
+ appId: 'YOUR_APP_ID', // 飞书应用 ID
22
+ appSecret: 'YOUR_APP_SECRET', // 飞书应用密钥
23
+ webhookPath: '/lark/webhook', // Webhook 路径
24
+ }
25
+ ```
26
+
27
+ ### 完整配置
28
+
29
+ ```typescript
30
+ const config: LarkBotConfig = {
31
+ context: 'lark',
32
+ name: 'my-lark-bot',
33
+ appId: 'YOUR_APP_ID',
34
+ appSecret: 'YOUR_APP_SECRET',
35
+ webhookPath: '/lark/webhook',
36
+
37
+ // 安全配置(推荐)
38
+ encryptKey: 'YOUR_ENCRYPT_KEY', // 事件推送加密密钥
39
+ verificationToken: 'YOUR_VERIFICATION_TOKEN', // 事件推送验证令牌
40
+
41
+ // API 配置
42
+ isFeishu: true, // true=飞书(中国版), false=Lark(国际版), 默认false
43
+ apiBaseUrl: 'https://open.feishu.cn/open-apis' // 自定义API地址(可选)
44
+ }
45
+ ```
46
+
47
+ ### 配置参数说明
48
+
49
+ - `appId` (必需): 飞书应用 ID,在开发者后台获取
50
+ - `appSecret` (必需): 飞书应用密钥,在开发者后台获取
51
+ - `webhookPath` (必需): Webhook 路径,如 `/lark/webhook`
52
+ - `encryptKey` (推荐): 事件推送加密密钥,用于签名验证
53
+ - `verificationToken` (推荐): 事件推送验证令牌,额外安全验证
54
+ - `isFeishu` (可选): 是否为飞书中国版,默认 `false` (Lark 国际版)
55
+ - `apiBaseUrl` (可选): 自定义 API 基础地址
56
+
57
+ ## 获取配置信息
58
+
59
+ ### 创建飞书/Lark 应用
60
+
61
+ #### 飞书(中国版)
62
+ 1. 访问 [飞书开放平台](https://open.feishu.cn/app)
63
+ 2. 点击「创建应用」,选择「自建应用」
64
+ 3. 填写应用信息并创建
65
+
66
+ #### Lark(国际版)
67
+ 1. 访问 [Lark Developer Console](https://open.larksuite.com/app)
68
+ 2. 点击「Create App」,选择「Custom App」
69
+ 3. 填写应用信息并创建
70
+
71
+ ### 获取应用凭证
72
+
73
+ 在应用详情页面的「凭证与基础信息」中获取:
74
+ - **App ID**: 应用唯一标识
75
+ - **App Secret**: 应用密钥(注意保密)
76
+
77
+ ### 配置机器人
78
+
79
+ 1. **启用机器人功能**:
80
+ - 在应用管理页面,进入「功能配置」→「机器人」
81
+ - 启用机器人功能
82
+
83
+ 2. **配置事件订阅**:
84
+ - 进入「事件订阅」页面
85
+ - 设置请求网址:`https://yourdomain.com/lark/webhook`
86
+ - 配置加密策略(推荐启用)
87
+ - 订阅需要的事件类型:
88
+ - `接收消息` - 接收用户发送的消息
89
+ - `消息已读` - 消息已读事件
90
+ - 其他所需事件
91
+
92
+ 3. **配置权限**:
93
+ - 在「权限管理」中申请所需权限:
94
+ - `以应用的身份发消息` - 发送消息
95
+ - `获取与发送单聊、群组消息` - 收发消息
96
+ - `读取用户通讯录基本信息` - 获取用户信息
97
+ - 其他业务需要的权限
98
+
99
+ 4. **发布应用**:
100
+ - 完成配置后,提交审核并发布应用
101
+
102
+ ## 使用示例
103
+
104
+ ### 基础使用
105
+
106
+ ```typescript
107
+ import { createApp } from 'zhin.js';
108
+ import '@zhin.js/adapter-lark';
109
+ import '@zhin.js/http'; // 需要 HTTP 插件支持
110
+
111
+ const app = createApp();
112
+
113
+ // 先加载 HTTP 插件
114
+ app.plugin(require('@zhin.js/http'));
115
+
116
+ // 配置飞书适配器
117
+ app.adapter('lark', {
118
+ context: 'lark',
119
+ name: 'my-bot',
120
+ appId: process.env.LARK_APP_ID!,
121
+ appSecret: process.env.LARK_APP_SECRET!,
122
+ webhookPath: '/lark/webhook',
123
+ encryptKey: process.env.LARK_ENCRYPT_KEY,
124
+ verificationToken: process.env.LARK_VERIFICATION_TOKEN,
125
+ isFeishu: true // 使用飞书中国版
126
+ });
127
+
128
+ // 基础命令
129
+ app.command('ping').action((session) => {
130
+ session.send('pong! 🏓');
131
+ });
132
+
133
+ // 处理用户消息
134
+ app.middleware((session, next) => {
135
+ console.log(`收到来自 ${session.$sender.name} 的消息`);
136
+ return next();
137
+ });
138
+
139
+ app.start();
140
+ ```
141
+
142
+ ### 高级功能使用
143
+
144
+ ```typescript
145
+ import { createApp } from 'zhin.js';
146
+ import '@zhin.js/adapter-lark';
147
+
148
+ const app = createApp();
149
+
150
+ app.plugin(require('@zhin.js/http'));
151
+
152
+ const bot = app.adapter('lark', {
153
+ context: 'lark',
154
+ name: 'advanced-bot',
155
+ appId: process.env.LARK_APP_ID!,
156
+ appSecret: process.env.LARK_APP_SECRET!,
157
+ webhookPath: '/lark/webhook',
158
+ encryptKey: process.env.LARK_ENCRYPT_KEY,
159
+ isFeishu: true
160
+ });
161
+
162
+ // 帮助命令
163
+ app.command('help').action(async (session) => {
164
+ await session.send([
165
+ { type: 'text', data: { content: '🤖 机器人帮助\\n\\n' } },
166
+ { type: 'text', data: { content: '📝 /help - 显示此帮助\\n' } },
167
+ { type: 'text', data: { content: '🏓 /ping - 测试连通性\\n' } },
168
+ { type: 'text', data: { content: '👤 /me - 查看个人信息\\n' } },
169
+ { type: 'text', data: { content: '📊 /stats - 查看统计信息' } }
170
+ ]);
171
+ });
172
+
173
+ // 个人信息查询
174
+ app.command('me').action(async (session) => {
175
+ const userInfo = await bot.getUserInfo(session.$sender.id);
176
+ if (userInfo) {
177
+ await session.send(`👤 用户信息:\\n姓名: ${userInfo.name}\\n邮箱: ${userInfo.email || '未设置'}`);
178
+ } else {
179
+ await session.send('❌ 无法获取用户信息');
180
+ }
181
+ });
182
+
183
+ // 处理 @提及
184
+ app.middleware((session, next) => {
185
+ const mentions = session.content.filter(seg => seg.type === 'at');
186
+ if (mentions.length > 0) {
187
+ console.log('收到提及:', mentions.map(m => m.data.name));
188
+ session.send(`👋 我看到你提及了 ${mentions.length} 个用户`);
189
+ }
190
+ return next();
191
+ });
192
+
193
+ // 处理图片消息
194
+ app.middleware((session, next) => {
195
+ const images = session.content.filter(seg => seg.type === 'image');
196
+ if (images.length > 0) {
197
+ session.send(`📷 收到了 ${images.length} 张图片!`);
198
+ }
199
+ return next();
200
+ });
201
+
202
+ // 处理文件消息
203
+ app.middleware((session, next) => {
204
+ const files = session.content.filter(seg => seg.type === 'file');
205
+ if (files.length > 0) {
206
+ const fileNames = files.map(f => f.data.file_name).join(', ');
207
+ session.send(`📎 收到文件: ${fileNames}`);
208
+ }
209
+ return next();
210
+ });
211
+
212
+ // 发送卡片消息
213
+ app.command('card').action(async (session) => {
214
+ await session.send([
215
+ {
216
+ type: 'card',
217
+ data: {
218
+ config: {
219
+ wide_screen_mode: true
220
+ },
221
+ elements: [
222
+ {
223
+ tag: 'div',
224
+ text: {
225
+ content: '**这是一个交互式卡片**\\n\\n点击下方按钮体验功能',
226
+ tag: 'lark_md'
227
+ }
228
+ },
229
+ {
230
+ tag: 'action',
231
+ actions: [
232
+ {
233
+ tag: 'button',
234
+ text: {
235
+ content: '点赞 👍',
236
+ tag: 'plain_text'
237
+ },
238
+ type: 'primary',
239
+ value: {
240
+ action: 'like'
241
+ }
242
+ },
243
+ {
244
+ tag: 'button',
245
+ text: {
246
+ content: '查看详情',
247
+ tag: 'plain_text'
248
+ },
249
+ type: 'default',
250
+ value: {
251
+ action: 'detail'
252
+ }
253
+ }
254
+ ]
255
+ }
256
+ ],
257
+ header: {
258
+ title: {
259
+ content: '🎉 欢迎使用飞书机器人',
260
+ tag: 'plain_text'
261
+ },
262
+ template: 'blue'
263
+ }
264
+ }
265
+ }
266
+ ]);
267
+ });
268
+
269
+ app.start();
270
+ ```
271
+
272
+ ### 文件上传示例
273
+
274
+ ```typescript
275
+ // 上传并发送图片
276
+ app.command('upload-image').action(async (session) => {
277
+ try {
278
+ const fileKey = await bot.uploadFile('./path/to/image.jpg', 'image');
279
+ if (fileKey) {
280
+ await session.send([
281
+ { type: 'image', data: { file_key: fileKey } }
282
+ ]);
283
+ }
284
+ } catch (error) {
285
+ await session.send('❌ 文件上传失败');
286
+ }
287
+ });
288
+
289
+ // 上传并发送文件
290
+ app.command('upload-file').action(async (session) => {
291
+ try {
292
+ const fileKey = await bot.uploadFile('./path/to/document.pdf', 'file');
293
+ if (fileKey) {
294
+ await session.send([
295
+ { type: 'file', data: { file_key: fileKey } }
296
+ ]);
297
+ }
298
+ } catch (error) {
299
+ await session.send('❌ 文件上传失败');
300
+ }
301
+ });
302
+ ```
303
+
304
+ ## 支持的消息类型
305
+
306
+ ### 接收消息类型
307
+
308
+ - **文本消息**: 支持纯文本和富文本格式
309
+ - **图片消息**: 支持各种格式的图片
310
+ - **文件消息**: 支持各种类型的文件附件
311
+ - **音频消息**: 支持语音和音频文件
312
+ - **视频消息**: 支持视频文件
313
+ - **贴纸消息**: 支持飞书表情包
314
+ - **卡片消息**: 支持交互式卡片和富文本卡片
315
+ - **@提及**: 支持用户提及解析
316
+ - **链接消息**: 自动解析文本中的链接
317
+ - **富文本**: 支持格式化文本内容
318
+
319
+ ### 发送消息类型
320
+
321
+ - **文本消息**: 发送纯文本内容
322
+ - **图片消息**: 发送图片(需要先上传获取 file_key)
323
+ - **文件消息**: 发送文件(需要先上传获取 file_key)
324
+ - **卡片消息**: 发送交互式卡片和富文本卡片
325
+ - **@提及**: 在消息中提及特定用户
326
+ - **链接消息**: 发送包含链接的富文本
327
+
328
+ ## 聊天类型支持
329
+
330
+ - `private`: 私聊(单聊)
331
+ - `group`: 群聊
332
+
333
+ ## 飞书特色功能
334
+
335
+ ### 交互式卡片
336
+
337
+ 飞书支持丰富的交互式卡片,可以创建按钮、表单、图表等:
338
+
339
+ ```typescript
340
+ app.command('interactive-card').action(async (session) => {
341
+ await session.send([
342
+ {
343
+ type: 'card',
344
+ data: {
345
+ config: {
346
+ wide_screen_mode: true
347
+ },
348
+ elements: [
349
+ // 文本内容
350
+ {
351
+ tag: 'div',
352
+ text: {
353
+ content: '请选择您的偏好设置:',
354
+ tag: 'plain_text'
355
+ }
356
+ },
357
+ // 分割线
358
+ {
359
+ tag: 'hr'
360
+ },
361
+ // 选择器
362
+ {
363
+ tag: 'div',
364
+ fields: [
365
+ {
366
+ is_short: true,
367
+ text: {
368
+ content: '**语言偏好**\\nChinese',
369
+ tag: 'lark_md'
370
+ }
371
+ },
372
+ {
373
+ is_short: true,
374
+ text: {
375
+ content: '**通知设置**\\n开启',
376
+ tag: 'lark_md'
377
+ }
378
+ }
379
+ ]
380
+ },
381
+ // 按钮组
382
+ {
383
+ tag: 'action',
384
+ actions: [
385
+ {
386
+ tag: 'button',
387
+ text: {
388
+ content: '保存设置',
389
+ tag: 'plain_text'
390
+ },
391
+ type: 'primary',
392
+ value: {
393
+ action: 'save_settings'
394
+ }
395
+ },
396
+ {
397
+ tag: 'button',
398
+ text: {
399
+ content: '重置',
400
+ tag: 'plain_text'
401
+ },
402
+ type: 'danger',
403
+ value: {
404
+ action: 'reset'
405
+ }
406
+ }
407
+ ]
408
+ }
409
+ ],
410
+ header: {
411
+ title: {
412
+ content: '⚙️ 设置面板',
413
+ tag: 'plain_text'
414
+ },
415
+ template: 'green'
416
+ }
417
+ }
418
+ }
419
+ ]);
420
+ });
421
+ ```
422
+
423
+ ### 文件操作
424
+
425
+ 飞书支持多种文件操作:
426
+
427
+ ```typescript
428
+ // 获取文件信息
429
+ app.command('file-info <file_key>').action(async (session) => {
430
+ const fileKey = session.argv.file_key;
431
+
432
+ try {
433
+ const response = await bot.axiosInstance.get(\`/im/v1/files/\${fileKey}\`);
434
+ const fileInfo = response.data.data;
435
+
436
+ await session.send(\`📄 文件信息:\\n文件名: \${fileInfo.file_name}\\n大小: \${fileInfo.file_size} bytes\\n类型: \${fileInfo.file_type}\`);
437
+ } catch (error) {
438
+ await session.send('❌ 获取文件信息失败');
439
+ }
440
+ });
441
+
442
+ // 下载文件
443
+ app.command('download <file_key>').action(async (session) => {
444
+ const fileKey = session.argv.file_key;
445
+
446
+ try {
447
+ const response = await bot.axiosInstance.get(\`/im/v1/files/\${fileKey}/download\`);
448
+ // 处理文件下载逻辑
449
+ await session.send('✅ 文件下载完成');
450
+ } catch (error) {
451
+ await session.send('❌ 文件下载失败');
452
+ }
453
+ });
454
+ ```
455
+
456
+ ### 用户和群组管理
457
+
458
+ ```typescript
459
+ // 获取群组成员列表
460
+ app.command('members').action(async (session) => {
461
+ if (session.$channel.type !== 'group') {
462
+ await session.send('❌ 此命令只能在群聊中使用');
463
+ return;
464
+ }
465
+
466
+ try {
467
+ const chatInfo = await bot.getChatInfo(session.$channel.id);
468
+ const memberCount = chatInfo?.member_count || 0;
469
+
470
+ await session.send(\`👥 群组信息:\\n群名称: \${chatInfo?.name || '未知'}\\n成员数量: \${memberCount}\`);
471
+ } catch (error) {
472
+ await session.send('❌ 获取群组信息失败');
473
+ }
474
+ });
475
+
476
+ // 获取用户详细信息
477
+ app.command('user-info [user_id]').action(async (session) => {
478
+ const userId = session.argv.user_id || session.$sender.id;
479
+
480
+ try {
481
+ const userInfo = await bot.getUserInfo(userId);
482
+ if (userInfo) {
483
+ await session.send([
484
+ { type: 'text', data: { content: \`👤 用户详情:\\n\` } },
485
+ { type: 'text', data: { content: \`姓名: \${userInfo.name}\\n\` } },
486
+ { type: 'text', data: { content: \`邮箱: \${userInfo.email || '未设置'}\\n\` } },
487
+ { type: 'text', data: { content: \`部门: \${userInfo.department_ids?.join(', ') || '未知'}\` } }
488
+ ]);
489
+ }
490
+ } catch (error) {
491
+ await session.send('❌ 获取用户信息失败');
492
+ }
493
+ });
494
+ ```
495
+
496
+ ## 最佳实践
497
+
498
+ ### 1. 安全配置
499
+
500
+ ```typescript
501
+ // 生产环境建议启用所有安全选项
502
+ const config: LarkBotConfig = {
503
+ context: 'lark',
504
+ name: 'production-bot',
505
+ appId: process.env.LARK_APP_ID!,
506
+ appSecret: process.env.LARK_APP_SECRET!,
507
+ webhookPath: '/lark/webhook',
508
+
509
+ // 必须配置的安全选项
510
+ encryptKey: process.env.LARK_ENCRYPT_KEY!,
511
+ verificationToken: process.env.LARK_VERIFICATION_TOKEN!,
512
+
513
+ isFeishu: true
514
+ };
515
+ ```
516
+
517
+ ### 2. 错误处理
518
+
519
+ ```typescript
520
+ // 全局错误处理
521
+ app.middleware(async (session, next) => {
522
+ try {
523
+ await next();
524
+ } catch (error) {
525
+ console.error('Command execution error:', error);
526
+ await session.send('❌ 命令执行出错,请稍后重试');
527
+ }
528
+ });
529
+
530
+ // API 调用错误处理
531
+ app.command('safe-api').action(async (session) => {
532
+ try {
533
+ const userInfo = await bot.getUserInfo(session.$sender.id);
534
+ // 处理成功逻辑
535
+ } catch (error) {
536
+ if (error.response?.status === 403) {
537
+ await session.send('❌ 权限不足,请联系管理员');
538
+ } else if (error.response?.status === 429) {
539
+ await session.send('❌ 请求频率过高,请稍后重试');
540
+ } else {
541
+ await session.send('❌ 服务暂时不可用');
542
+ }
543
+ }
544
+ });
545
+ ```
546
+
547
+ ### 3. 性能优化
548
+
549
+ ```typescript
550
+ // 缓存 access_token(已在适配器内部实现)
551
+ // 批量处理消息
552
+ const messageQueue: string[] = [];
553
+
554
+ app.middleware((session, next) => {
555
+ // 简单的消息队列示例
556
+ messageQueue.push(session.$content.map(s => s.data?.content || '').join(''));
557
+
558
+ // 每10条消息批量处理一次
559
+ if (messageQueue.length >= 10) {
560
+ console.log('Processing batch messages:', messageQueue.length);
561
+ messageQueue.length = 0; // 清空队列
562
+ }
563
+
564
+ return next();
565
+ });
566
+ ```
567
+
568
+ ## 故障排除
569
+
570
+ ### 常见问题
571
+
572
+ 1. **Webhook 验证失败**
573
+ ```
574
+ Invalid verification token in webhook
575
+ ```
576
+ - 检查 `verificationToken` 配置是否正确
577
+ - 确认飞书后台的验证令牌设置
578
+
579
+ 2. **签名验证失败**
580
+ ```
581
+ Invalid signature in webhook
582
+ ```
583
+ - 检查 `encryptKey` 配置是否正确
584
+ - 确认飞书后台的加密设置
585
+
586
+ 3. **Token 获取失败**
587
+ ```
588
+ Failed to get access token
589
+ ```
590
+ - 检查 `appId` 和 `appSecret` 是否正确
591
+ - 确认应用是否已发布且状态正常
592
+ - 检查网络连接和 API 地址
593
+
594
+ 4. **消息发送失败**
595
+ ```
596
+ Failed to send message: no permission
597
+ ```
598
+ - 检查应用权限配置
599
+ - 确认机器人是否在目标群组中
600
+ - 验证用户是否允许机器人发消息
601
+
602
+ 5. **文件上传失败**
603
+ ```
604
+ Upload failed: file too large
605
+ ```
606
+ - 检查文件大小是否超出限制
607
+ - 确认文件格式是否支持
608
+ - 检查应用是否有文件上传权限
609
+
610
+ ### 调试技巧
611
+
612
+ 1. **启用详细日志**:
613
+ ```typescript
614
+ // 在配置中启用调试模式
615
+ app.plugin(require('@zhin.js/logger'), {
616
+ level: 'debug'
617
+ });
618
+ ```
619
+
620
+ 2. **查看原始事件数据**:
621
+ ```typescript
622
+ app.middleware((session, next) => {
623
+ console.log('Raw event:', session.$raw);
624
+ return next();
625
+ });
626
+ ```
627
+
628
+ 3. **测试 Webhook 连通性**:
629
+ 使用工具如 ngrok 在本地测试 Webhook 接收
630
+
631
+ ## 注意事项
632
+
633
+ 1. **应用权限**: 确保在飞书开发者后台配置了正确的权限
634
+ 2. **网络环境**: 飞书和 Lark 使用不同的 API 域名,确保网络能正常访问
635
+ 3. **消息限制**: 注意飞书的消息发送频率限制
636
+ 4. **文件大小**: 文件上传有大小限制,通常为 30MB
637
+ 5. **安全配置**: 生产环境强烈建议启用签名验证和令牌验证
638
+ 6. **应用审核**: 某些功能可能需要应用通过审核才能使用
639
+
640
+ ## 更新日志
641
+
642
+ ### v1.0.0
643
+ - 🎉 初始版本发布
644
+ - 🔐 完整的安全验证机制
645
+ - 📱 支持飞书和 Lark 双平台
646
+ - 💬 丰富的消息类型支持
647
+ - 🎛️ 交互式卡片支持
648
+ - 📁 文件上传下载功能
649
+ - 👥 用户和群组管理
650
+ - 🚀 使用 `useContext('router')` 集成 HTTP 服务
651
+ - ⚡ 自动 token 管理和刷新