@onebots/adapter-feishu 1.0.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) 2022 凉菜
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,106 @@
1
+ # @onebots/adapter-feishu
2
+
3
+ onebots 飞书/Lark 适配器,同时支持飞书(国内版)和 Lark(国际版)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @onebots/adapter-feishu
9
+ # 或
10
+ pnpm add @onebots/adapter-feishu
11
+ ```
12
+
13
+ ## 配置
14
+
15
+ 在 `config.yaml` 中配置:
16
+
17
+ ```yaml
18
+ # 飞书(国内版)- 默认
19
+ feishu.feishu_bot:
20
+ app_id: "YOUR_APP_ID"
21
+ app_secret: "YOUR_APP_SECRET"
22
+ encrypt_key: "YOUR_ENCRYPT_KEY" # 可选,事件加密密钥
23
+ verification_token: "YOUR_VERIFICATION_TOKEN" # 可选,事件验证 Token
24
+
25
+ # Lark(国际版)
26
+ feishu.lark_bot:
27
+ app_id: "YOUR_APP_ID"
28
+ app_secret: "YOUR_APP_SECRET"
29
+ endpoint: "https://open.larksuite.com/open-apis" # Lark 端点
30
+ ```
31
+
32
+ ### 端点配置
33
+
34
+ | 端点 | URL | 说明 |
35
+ |------|-----|------|
36
+ | 飞书(默认) | `https://open.feishu.cn/open-apis` | 国内版 |
37
+ | Lark | `https://open.larksuite.com/open-apis` | 国际版 |
38
+
39
+ ### TypeScript 配置(推荐)
40
+
41
+ ```typescript
42
+ import { FeishuEndpoint } from '@onebots/adapter-feishu';
43
+
44
+ // 飞书(国内版)
45
+ {
46
+ account_id: 'feishu_bot',
47
+ app_id: 'cli_xxx',
48
+ app_secret: 'xxx',
49
+ // endpoint 可省略,默认为 FeishuEndpoint.FEISHU
50
+ }
51
+
52
+ // Lark(国际版)
53
+ {
54
+ account_id: 'lark_bot',
55
+ app_id: 'cli_xxx',
56
+ app_secret: 'xxx',
57
+ endpoint: FeishuEndpoint.LARK,
58
+ }
59
+
60
+ // 私有化部署
61
+ {
62
+ account_id: 'private_bot',
63
+ app_id: 'cli_xxx',
64
+ app_secret: 'xxx',
65
+ endpoint: 'https://your-private-feishu.com/open-apis',
66
+ }
67
+ ```
68
+
69
+ ## 使用
70
+
71
+ ```bash
72
+ onebots -r feishu
73
+ ```
74
+
75
+ ## 功能
76
+
77
+ - ✅ 单聊消息收发
78
+ - ✅ 群聊消息收发
79
+ - ✅ 文本消息
80
+ - ✅ 富文本消息(部分支持)
81
+ - ✅ 消息编辑和删除
82
+ - ✅ 群组管理(获取信息、踢出成员等)
83
+ - ✅ 事件订阅(Webhook)
84
+ - ✅ 支持飞书和 Lark 双端点
85
+
86
+ ## 获取应用凭证
87
+
88
+ ### 飞书(国内版)
89
+ 1. 访问 [飞书开放平台](https://open.feishu.cn/)
90
+ 2. 创建企业自建应用
91
+ 3. 获取 `App ID` 和 `App Secret`
92
+ 4. 配置事件订阅 URL(Webhook)
93
+ 5. 配置应用权限(消息收发、通讯录等)
94
+
95
+ ### Lark(国际版)
96
+ 1. 访问 [Lark Developer](https://open.larksuite.com/)
97
+ 2. 创建应用并获取凭证
98
+ 3. 配置方式与飞书相同,只需设置 `endpoint` 为 Lark 端点
99
+
100
+ ## 相关链接
101
+
102
+ - [飞书开放平台](https://open.feishu.cn/)
103
+ - [Lark Developer](https://open.larksuite.com/)
104
+ - [飞书 Bot 开发文档](https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN)
105
+ - [onebots 文档](https://onebots.pages.dev/)
106
+
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 飞书适配器
3
+ * 继承 Adapter 基类,实现飞书平台功能
4
+ */
5
+ import { Account } from "onebots";
6
+ import { Adapter } from "onebots";
7
+ import { BaseApp } from "onebots";
8
+ import { FeishuBot } from "./bot.js";
9
+ import { type FeishuConfig } from "./types.js";
10
+ export declare class FeishuAdapter extends Adapter<FeishuBot, "feishu"> {
11
+ constructor(app: BaseApp);
12
+ /**
13
+ * 根据端点获取对应的图标
14
+ */
15
+ private getIconForEndpoint;
16
+ /**
17
+ * 判断是否为 Lark(国际版)
18
+ */
19
+ private isLarkEndpoint;
20
+ /**
21
+ * 发送消息
22
+ */
23
+ sendMessage(uin: string, params: Adapter.SendMessageParams): Promise<Adapter.SendMessageResult>;
24
+ /**
25
+ * 删除/撤回消息
26
+ */
27
+ deleteMessage(uin: string, params: Adapter.DeleteMessageParams): Promise<void>;
28
+ /**
29
+ * 获取消息
30
+ */
31
+ getMessage(uin: string, params: Adapter.GetMessageParams): Promise<Adapter.MessageInfo>;
32
+ /**
33
+ * 更新消息
34
+ */
35
+ updateMessage(uin: string, params: Adapter.UpdateMessageParams): Promise<void>;
36
+ /**
37
+ * 获取机器人自身信息
38
+ */
39
+ getLoginInfo(uin: string): Promise<Adapter.UserInfo>;
40
+ /**
41
+ * 获取用户信息
42
+ */
43
+ getUserInfo(uin: string, params: Adapter.GetUserInfoParams): Promise<Adapter.UserInfo>;
44
+ /**
45
+ * 获取好友列表(飞书不支持)
46
+ */
47
+ getFriendList(uin: string, params?: Adapter.GetFriendListParams): Promise<Adapter.FriendInfo[]>;
48
+ /**
49
+ * 获取好友信息
50
+ */
51
+ getFriendInfo(uin: string, params: Adapter.GetFriendInfoParams): Promise<Adapter.FriendInfo>;
52
+ /**
53
+ * 获取群列表(飞书不支持)
54
+ */
55
+ getGroupList(uin: string, params?: Adapter.GetGroupListParams): Promise<Adapter.GroupInfo[]>;
56
+ /**
57
+ * 获取群信息
58
+ */
59
+ getGroupInfo(uin: string, params: Adapter.GetGroupInfoParams): Promise<Adapter.GroupInfo>;
60
+ /**
61
+ * 退出群组
62
+ */
63
+ leaveGroup(uin: string, params: Adapter.LeaveGroupParams): Promise<void>;
64
+ /**
65
+ * 获取群成员列表
66
+ */
67
+ getGroupMemberList(uin: string, params: Adapter.GetGroupMemberListParams): Promise<Adapter.GroupMemberInfo[]>;
68
+ /**
69
+ * 获取群成员信息
70
+ */
71
+ getGroupMemberInfo(uin: string, params: Adapter.GetGroupMemberInfoParams): Promise<Adapter.GroupMemberInfo>;
72
+ /**
73
+ * 踢出群成员
74
+ */
75
+ kickGroupMember(uin: string, params: Adapter.KickGroupMemberParams): Promise<void>;
76
+ /**
77
+ * 设置群名片(飞书不支持)
78
+ */
79
+ setGroupCard(uin: string, params: Adapter.SetGroupCardParams): Promise<void>;
80
+ /**
81
+ * 获取版本信息
82
+ */
83
+ getVersion(uin: string): Promise<Adapter.VersionInfo>;
84
+ /**
85
+ * 获取运行状态
86
+ */
87
+ getStatus(uin: string): Promise<Adapter.StatusInfo>;
88
+ createAccount(config: Account.Config<'feishu'>): Account<'feishu', FeishuBot>;
89
+ /**
90
+ * 处理飞书事件
91
+ */
92
+ private handleFeishuEvent;
93
+ }
94
+ declare module "onebots" {
95
+ namespace Adapter {
96
+ interface Configs {
97
+ feishu: FeishuConfig;
98
+ }
99
+ }
100
+ }
101
+ //# sourceMappingURL=adapter.d.ts.map
package/lib/adapter.js ADDED
@@ -0,0 +1,454 @@
1
+ /**
2
+ * 飞书适配器
3
+ * 继承 Adapter 基类,实现飞书平台功能
4
+ */
5
+ import { Account, AdapterRegistry, AccountStatus } from "onebots";
6
+ import { Adapter } from "onebots";
7
+ import { FeishuBot } from "./bot.js";
8
+ export class FeishuAdapter extends Adapter {
9
+ constructor(app) {
10
+ super(app, "feishu");
11
+ this.icon = "https://open.feishu.cn/favicon.ico";
12
+ }
13
+ /**
14
+ * 根据端点获取对应的图标
15
+ */
16
+ getIconForEndpoint(endpoint) {
17
+ if (endpoint.includes('larksuite.com')) {
18
+ return 'https://open.larksuite.com/favicon.ico';
19
+ }
20
+ return 'https://open.feishu.cn/favicon.ico';
21
+ }
22
+ /**
23
+ * 判断是否为 Lark(国际版)
24
+ */
25
+ isLarkEndpoint(endpoint) {
26
+ return endpoint.includes('larksuite.com');
27
+ }
28
+ // ============================================
29
+ // 消息相关方法
30
+ // ============================================
31
+ /**
32
+ * 发送消息
33
+ */
34
+ async sendMessage(uin, params) {
35
+ const account = this.getAccount(uin);
36
+ if (!account)
37
+ throw new Error(`Account ${uin} not found`);
38
+ const bot = account.client;
39
+ const { scene_id, scene_type, message } = params;
40
+ // 解析消息内容
41
+ let text = '';
42
+ const content = {};
43
+ for (const seg of message) {
44
+ if (typeof seg === 'string') {
45
+ text += seg;
46
+ }
47
+ else if (seg.type === 'text') {
48
+ text += seg.data.text || '';
49
+ }
50
+ else if (seg.type === 'at') {
51
+ const userId = seg.data.qq || seg.data.id || seg.data.user_id;
52
+ if (userId === 'all') {
53
+ text += '<at user_id="all">所有人</at>';
54
+ }
55
+ else {
56
+ text += `<at user_id="${userId}">${seg.data.name || userId}</at>`;
57
+ }
58
+ }
59
+ else if (seg.type === 'image') {
60
+ // 飞书图片消息需要先上传图片,这里简化处理
61
+ if (seg.data.url || seg.data.file) {
62
+ text += `[图片: ${seg.data.url || seg.data.file}]`;
63
+ }
64
+ }
65
+ }
66
+ // 构建飞书消息内容
67
+ content.text = text;
68
+ // 根据场景类型发送消息
69
+ let receiveIdType = 'open_id';
70
+ if (scene_type === 'private' || scene_type === 'direct') {
71
+ receiveIdType = 'open_id';
72
+ }
73
+ else if (scene_type === 'group' || scene_type === 'channel') {
74
+ receiveIdType = 'chat_id';
75
+ }
76
+ const result = await bot.sendMessage(scene_id.string, receiveIdType, content, 'text');
77
+ return {
78
+ message_id: this.createId(result.data.message_id),
79
+ };
80
+ }
81
+ /**
82
+ * 删除/撤回消息
83
+ */
84
+ async deleteMessage(uin, params) {
85
+ const account = this.getAccount(uin);
86
+ if (!account)
87
+ throw new Error(`Account ${uin} not found`);
88
+ const bot = account.client;
89
+ const msgId = params.message_id.string;
90
+ const chatId = params.scene_id?.string || '';
91
+ // 飞书删除消息 API
92
+ const http = bot.getHttpClient();
93
+ await http.delete(`/im/v1/messages/${msgId}`);
94
+ }
95
+ /**
96
+ * 获取消息
97
+ */
98
+ async getMessage(uin, params) {
99
+ const account = this.getAccount(uin);
100
+ if (!account)
101
+ throw new Error(`Account ${uin} not found`);
102
+ const bot = account.client;
103
+ const msgId = params.message_id.string;
104
+ // 飞书获取消息 API
105
+ const http = bot.getHttpClient();
106
+ const response = await http.get(`/im/v1/messages/${msgId}`);
107
+ if (response.data.code !== 0) {
108
+ throw new Error(`获取消息失败: ${response.data.msg}`);
109
+ }
110
+ const msg = response.data.data.items[0];
111
+ return {
112
+ message_id: this.createId(msg.message_id),
113
+ time: parseInt(msg.create_time),
114
+ sender: {
115
+ scene_type: msg.chat_id ? 'group' : 'private',
116
+ sender_id: this.createId(msg.sender.id),
117
+ scene_id: this.createId(msg.chat_id || msg.sender.id),
118
+ sender_name: msg.sender.id,
119
+ scene_name: '',
120
+ },
121
+ message: [{
122
+ type: 'text',
123
+ data: { text: JSON.parse(msg.body.content).text || '' },
124
+ }],
125
+ };
126
+ }
127
+ /**
128
+ * 更新消息
129
+ */
130
+ async updateMessage(uin, params) {
131
+ const account = this.getAccount(uin);
132
+ if (!account)
133
+ throw new Error(`Account ${uin} not found`);
134
+ const bot = account.client;
135
+ const msgId = params.message_id.string;
136
+ // 解析消息内容
137
+ let text = '';
138
+ for (const seg of params.message) {
139
+ if (typeof seg === 'string') {
140
+ text += seg;
141
+ }
142
+ else if (seg.type === 'text') {
143
+ text += seg.data.text || '';
144
+ }
145
+ }
146
+ // 飞书更新消息 API
147
+ const http = bot.getHttpClient();
148
+ await http.put(`/im/v1/messages/${msgId}`, {
149
+ content: JSON.stringify({ text }),
150
+ });
151
+ }
152
+ // ============================================
153
+ // 用户相关方法
154
+ // ============================================
155
+ /**
156
+ * 获取机器人自身信息
157
+ */
158
+ async getLoginInfo(uin) {
159
+ const account = this.getAccount(uin);
160
+ if (!account)
161
+ throw new Error(`Account ${uin} not found`);
162
+ const bot = account.client;
163
+ const me = bot.getCachedMe();
164
+ return {
165
+ user_id: this.createId(me?.user_id || me?.open_id || ''),
166
+ user_name: me?.name || '',
167
+ user_displayname: me?.nickname || me?.name || '',
168
+ avatar: me?.avatar_url || me?.avatar_big,
169
+ };
170
+ }
171
+ /**
172
+ * 获取用户信息
173
+ */
174
+ async getUserInfo(uin, params) {
175
+ const account = this.getAccount(uin);
176
+ if (!account)
177
+ throw new Error(`Account ${uin} not found`);
178
+ const bot = account.client;
179
+ const userId = params.user_id.string;
180
+ const user = await bot.getUserInfo(userId);
181
+ return {
182
+ user_id: this.createId(user.user_id || user.open_id),
183
+ user_name: user.name || '',
184
+ user_displayname: user.nickname || user.name || '',
185
+ avatar: user.avatar_url || user.avatar_big,
186
+ };
187
+ }
188
+ // ============================================
189
+ // 好友(私聊会话)相关方法
190
+ // ============================================
191
+ /**
192
+ * 获取好友列表(飞书不支持)
193
+ */
194
+ async getFriendList(uin, params) {
195
+ // 飞书不提供好友列表 API
196
+ return [];
197
+ }
198
+ /**
199
+ * 获取好友信息
200
+ */
201
+ async getFriendInfo(uin, params) {
202
+ const account = this.getAccount(uin);
203
+ if (!account)
204
+ throw new Error(`Account ${uin} not found`);
205
+ const bot = account.client;
206
+ const userId = params.user_id.string;
207
+ const user = await bot.getUserInfo(userId);
208
+ return {
209
+ user_id: this.createId(user.user_id || user.open_id),
210
+ user_name: user.name || '',
211
+ remark: user.nickname || user.name || '',
212
+ };
213
+ }
214
+ // ============================================
215
+ // 群组相关方法
216
+ // ============================================
217
+ /**
218
+ * 获取群列表(飞书不支持)
219
+ */
220
+ async getGroupList(uin, params) {
221
+ // 飞书不提供群列表 API,需要通过事件订阅获取
222
+ return [];
223
+ }
224
+ /**
225
+ * 获取群信息
226
+ */
227
+ async getGroupInfo(uin, params) {
228
+ const account = this.getAccount(uin);
229
+ if (!account)
230
+ throw new Error(`Account ${uin} not found`);
231
+ const bot = account.client;
232
+ const chatId = params.group_id.string;
233
+ const chat = await bot.getChatInfo(chatId);
234
+ return {
235
+ group_id: this.createId(chat.chat_id),
236
+ group_name: chat.name || '',
237
+ };
238
+ }
239
+ /**
240
+ * 退出群组
241
+ */
242
+ async leaveGroup(uin, params) {
243
+ const account = this.getAccount(uin);
244
+ if (!account)
245
+ throw new Error(`Account ${uin} not found`);
246
+ const bot = account.client;
247
+ const chatId = params.group_id.string;
248
+ // 飞书退出群组 API
249
+ const http = bot.getHttpClient();
250
+ await http.delete(`/im/v1/chats/${chatId}/members/me`);
251
+ }
252
+ /**
253
+ * 获取群成员列表
254
+ */
255
+ async getGroupMemberList(uin, params) {
256
+ const account = this.getAccount(uin);
257
+ if (!account)
258
+ throw new Error(`Account ${uin} not found`);
259
+ const bot = account.client;
260
+ const chatId = params.group_id.string;
261
+ const members = await bot.getChatMembers(chatId);
262
+ return members.map((user) => ({
263
+ group_id: params.group_id,
264
+ user_id: this.createId(user.user_id || user.open_id),
265
+ user_name: user.name || '',
266
+ card: user.nickname || user.name || '',
267
+ role: 'member',
268
+ }));
269
+ }
270
+ /**
271
+ * 获取群成员信息
272
+ */
273
+ async getGroupMemberInfo(uin, params) {
274
+ const account = this.getAccount(uin);
275
+ if (!account)
276
+ throw new Error(`Account ${uin} not found`);
277
+ const bot = account.client;
278
+ const userId = params.user_id.string;
279
+ const user = await bot.getUserInfo(userId);
280
+ return {
281
+ group_id: params.group_id,
282
+ user_id: this.createId(user.user_id || user.open_id),
283
+ user_name: user.name || '',
284
+ card: user.nickname || user.name || '',
285
+ role: 'member',
286
+ };
287
+ }
288
+ /**
289
+ * 踢出群成员
290
+ */
291
+ async kickGroupMember(uin, params) {
292
+ const account = this.getAccount(uin);
293
+ if (!account)
294
+ throw new Error(`Account ${uin} not found`);
295
+ const bot = account.client;
296
+ const chatId = params.group_id.string;
297
+ const userId = params.user_id.string;
298
+ // 飞书踢出群成员 API
299
+ const http = bot.getHttpClient();
300
+ await http.delete(`/im/v1/chats/${chatId}/members/${userId}`);
301
+ }
302
+ /**
303
+ * 设置群名片(飞书不支持)
304
+ */
305
+ async setGroupCard(uin, params) {
306
+ // 飞书不支持设置群名片
307
+ throw new Error('飞书不支持设置群名片');
308
+ }
309
+ // ============================================
310
+ // 系统相关方法
311
+ // ============================================
312
+ /**
313
+ * 获取版本信息
314
+ */
315
+ async getVersion(uin) {
316
+ const account = this.getAccount(uin);
317
+ const isLark = account ? this.isLarkEndpoint(account.client.endpoint) : false;
318
+ const platformName = isLark ? 'Lark' : '飞书';
319
+ return {
320
+ app_name: `onebots ${platformName} Adapter`,
321
+ app_version: '1.0.0',
322
+ impl: 'feishu',
323
+ version: '1.0.0',
324
+ };
325
+ }
326
+ /**
327
+ * 获取运行状态
328
+ */
329
+ async getStatus(uin) {
330
+ const account = this.getAccount(uin);
331
+ return {
332
+ online: account?.status === AccountStatus.Online,
333
+ good: account?.status === AccountStatus.Online,
334
+ };
335
+ }
336
+ // ============================================
337
+ // 账号创建
338
+ // ============================================
339
+ createAccount(config) {
340
+ const feishuConfig = {
341
+ account_id: config.account_id,
342
+ app_id: config.app_id,
343
+ app_secret: config.app_secret,
344
+ encrypt_key: config.encrypt_key,
345
+ verification_token: config.verification_token,
346
+ endpoint: config.endpoint,
347
+ };
348
+ const bot = new FeishuBot(feishuConfig);
349
+ const account = new Account(this, bot, config);
350
+ // 根据端点判断是飞书还是 Lark
351
+ const isLark = this.isLarkEndpoint(bot.endpoint);
352
+ const platformName = isLark ? 'Lark' : '飞书';
353
+ const accountIcon = this.getIconForEndpoint(bot.endpoint);
354
+ // Webhook 路由
355
+ this.app.router.post(`${account.path}/webhook`, bot.handleWebhook.bind(bot));
356
+ // 监听 Bot 事件
357
+ bot.on('ready', () => {
358
+ this.logger.info(`${platformName} Bot ${config.account_id} 已就绪 (endpoint: ${bot.endpoint})`);
359
+ });
360
+ bot.on('error', (error) => {
361
+ this.logger.error(`${platformName} Bot ${config.account_id} 错误:`, error);
362
+ });
363
+ // 监听飞书事件
364
+ bot.on('event', (event) => {
365
+ this.handleFeishuEvent(account, event);
366
+ });
367
+ // 启动时初始化 Bot
368
+ account.on('start', async () => {
369
+ try {
370
+ await bot.start();
371
+ account.status = AccountStatus.Online;
372
+ const me = bot.getCachedMe();
373
+ account.nickname = me?.name || `${platformName} Bot`;
374
+ account.avatar = me?.avatar_url || accountIcon;
375
+ }
376
+ catch (error) {
377
+ this.logger.error(`启动 ${platformName} Bot 失败:`, error);
378
+ account.status = AccountStatus.OffLine;
379
+ }
380
+ });
381
+ account.on('stop', async () => {
382
+ await bot.stop();
383
+ account.status = AccountStatus.OffLine;
384
+ });
385
+ return account;
386
+ }
387
+ /**
388
+ * 处理飞书事件
389
+ */
390
+ handleFeishuEvent(account, event) {
391
+ const eventType = event.header.event_type;
392
+ // 处理消息事件
393
+ if (eventType === 'im.message.receive_v1') {
394
+ const message = event.event.message;
395
+ if (!message)
396
+ return;
397
+ // 忽略自己发送的消息
398
+ const bot = account.client;
399
+ const me = bot.getCachedMe();
400
+ if (me && message.sender.id === me.open_id)
401
+ return;
402
+ // 打印消息接收日志
403
+ const content = JSON.parse(message.body.content || '{}').text || '';
404
+ const contentPreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
405
+ this.logger.info(`[飞书] 收到消息 | 消息ID: ${message.message_id} | ` +
406
+ `发送者: ${message.sender.id} | 内容: ${contentPreview}`);
407
+ // 构建消息段
408
+ const messageSegments = [];
409
+ if (content) {
410
+ messageSegments.push({
411
+ type: 'text',
412
+ data: { text: content },
413
+ });
414
+ }
415
+ // 判断是私聊还是群聊
416
+ const isGroup = message.chat_id && message.chat_id !== message.sender.id;
417
+ const messageType = isGroup ? 'group' : 'private';
418
+ // 转换为 CommonEvent 格式
419
+ const commonEvent = {
420
+ id: this.createId(message.message_id),
421
+ timestamp: parseInt(message.create_time) * 1000,
422
+ platform: 'feishu',
423
+ bot_id: this.createId(account.config.account_id),
424
+ type: 'message',
425
+ message_type: messageType,
426
+ sender: {
427
+ id: this.createId(message.sender.id),
428
+ name: message.sender.id,
429
+ avatar: undefined,
430
+ },
431
+ ...(isGroup ? {
432
+ group: {
433
+ id: this.createId(message.chat_id),
434
+ name: '',
435
+ },
436
+ } : {}),
437
+ message_id: this.createId(message.message_id),
438
+ raw_message: content,
439
+ message: messageSegments,
440
+ };
441
+ // 派发到协议层
442
+ account.dispatch(commonEvent);
443
+ }
444
+ }
445
+ }
446
+ AdapterRegistry.register('feishu', FeishuAdapter, {
447
+ name: 'feishu',
448
+ displayName: '飞书官方机器人',
449
+ description: '飞书官方机器人适配器,支持单聊、群聊和富文本消息',
450
+ icon: 'https://open.feishu.cn/favicon.ico',
451
+ homepage: 'https://open.feishu.cn/',
452
+ author: '凉菜',
453
+ });
454
+ //# sourceMappingURL=adapter.js.map
package/lib/bot.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * 飞书 Bot 客户端
3
+ * 基于飞书开放平台 API,使用 fetch 实现
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ import type { RouterContext, Next } from 'onebots';
7
+ import { type FeishuConfig, type FeishuSendMessageResponse, type FeishuUser, type FeishuChat } from './types.js';
8
+ export declare class FeishuBot extends EventEmitter {
9
+ private config;
10
+ private tenantAccessToken;
11
+ private tokenExpireTime;
12
+ private me;
13
+ /** 当前使用的 API 端点 */
14
+ readonly endpoint: string;
15
+ constructor(config: FeishuConfig);
16
+ /**
17
+ * 发送 HTTP 请求
18
+ */
19
+ private request;
20
+ /**
21
+ * GET 请求
22
+ */
23
+ get<T = any>(path: string, params?: Record<string, string | number | boolean>): Promise<{
24
+ data: T;
25
+ }>;
26
+ /**
27
+ * POST 请求
28
+ */
29
+ post<T = any>(path: string, body?: any, params?: Record<string, string | number | boolean>): Promise<{
30
+ data: T;
31
+ }>;
32
+ /**
33
+ * PUT 请求
34
+ */
35
+ put<T = any>(path: string, body?: any): Promise<{
36
+ data: T;
37
+ }>;
38
+ /**
39
+ * DELETE 请求
40
+ */
41
+ delete<T = any>(path: string): Promise<{
42
+ data: T;
43
+ }>;
44
+ /**
45
+ * 获取租户访问令牌
46
+ */
47
+ getTenantAccessToken(): Promise<string>;
48
+ /**
49
+ * 启动 Bot
50
+ */
51
+ start(): Promise<void>;
52
+ /**
53
+ * 停止 Bot
54
+ */
55
+ stop(): Promise<void>;
56
+ /**
57
+ * 处理 Webhook 请求
58
+ */
59
+ handleWebhook(ctx: RouterContext, next: Next): Promise<void>;
60
+ /**
61
+ * 获取缓存的 Bot 信息
62
+ */
63
+ getCachedMe(): FeishuUser | null;
64
+ /**
65
+ * 获取 Bot 信息
66
+ */
67
+ getBotInfo(): Promise<FeishuUser>;
68
+ /**
69
+ * 发送消息
70
+ */
71
+ sendMessage(receiveId: string, receiveIdType: 'open_id' | 'user_id' | 'union_id' | 'email' | 'chat_id', content: string | any, msgType?: string): Promise<FeishuSendMessageResponse>;
72
+ /**
73
+ * 获取用户信息
74
+ */
75
+ getUserInfo(userId: string, userIdType?: 'open_id' | 'user_id' | 'union_id'): Promise<FeishuUser>;
76
+ /**
77
+ * 获取群组信息
78
+ */
79
+ getChatInfo(chatId: string): Promise<FeishuChat>;
80
+ /**
81
+ * 获取群组成员列表
82
+ */
83
+ getChatMembers(chatId: string): Promise<FeishuUser[]>;
84
+ /**
85
+ * 获取 HTTP 客户端实例(返回 this 以便链式调用)
86
+ */
87
+ getHttpClient(): FeishuBot;
88
+ }
89
+ //# sourceMappingURL=bot.d.ts.map
package/lib/bot.js ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * 飞书 Bot 客户端
3
+ * 基于飞书开放平台 API,使用 fetch 实现
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ import { FeishuEndpoint } from './types.js';
7
+ export class FeishuBot extends EventEmitter {
8
+ config;
9
+ tenantAccessToken = '';
10
+ tokenExpireTime = 0;
11
+ me = null;
12
+ /** 当前使用的 API 端点 */
13
+ endpoint;
14
+ constructor(config) {
15
+ super();
16
+ this.config = config;
17
+ // 使用配置的端点,默认为飞书(国内版)
18
+ this.endpoint = config.endpoint || FeishuEndpoint.FEISHU;
19
+ }
20
+ /**
21
+ * 发送 HTTP 请求
22
+ */
23
+ async request(path, options = {}) {
24
+ const { method = 'GET', headers = {}, body, params, skipAuth = false } = options;
25
+ // 构建 URL
26
+ let url = `${this.endpoint}${path}`;
27
+ if (params) {
28
+ const searchParams = new URLSearchParams();
29
+ for (const [key, value] of Object.entries(params)) {
30
+ searchParams.append(key, String(value));
31
+ }
32
+ url += `?${searchParams.toString()}`;
33
+ }
34
+ // 构建请求头
35
+ const requestHeaders = {
36
+ 'Content-Type': 'application/json',
37
+ ...headers,
38
+ };
39
+ // 添加认证 token(除了获取 token 的请求)
40
+ if (!skipAuth) {
41
+ const token = await this.getTenantAccessToken();
42
+ requestHeaders['Authorization'] = `Bearer ${token}`;
43
+ }
44
+ // 发送请求
45
+ const response = await fetch(url, {
46
+ method,
47
+ headers: requestHeaders,
48
+ body: body ? JSON.stringify(body) : undefined,
49
+ });
50
+ return response.json();
51
+ }
52
+ /**
53
+ * GET 请求
54
+ */
55
+ async get(path, params) {
56
+ const data = await this.request(path, { params });
57
+ return { data };
58
+ }
59
+ /**
60
+ * POST 请求
61
+ */
62
+ async post(path, body, params) {
63
+ const data = await this.request(path, { method: 'POST', body, params });
64
+ return { data };
65
+ }
66
+ /**
67
+ * PUT 请求
68
+ */
69
+ async put(path, body) {
70
+ const data = await this.request(path, { method: 'PUT', body });
71
+ return { data };
72
+ }
73
+ /**
74
+ * DELETE 请求
75
+ */
76
+ async delete(path) {
77
+ const data = await this.request(path, { method: 'DELETE' });
78
+ return { data };
79
+ }
80
+ /**
81
+ * 获取租户访问令牌
82
+ */
83
+ async getTenantAccessToken() {
84
+ if (this.tenantAccessToken && Date.now() < this.tokenExpireTime) {
85
+ return this.tenantAccessToken;
86
+ }
87
+ const data = await this.request('/auth/v3/tenant_access_token/internal', {
88
+ method: 'POST',
89
+ body: {
90
+ app_id: this.config.app_id,
91
+ app_secret: this.config.app_secret,
92
+ },
93
+ skipAuth: true,
94
+ });
95
+ if (data.code !== 0) {
96
+ throw new Error(`获取租户访问令牌失败: ${data.msg}`);
97
+ }
98
+ this.tenantAccessToken = data.tenant_access_token || '';
99
+ this.tokenExpireTime = Date.now() + (data.expire - 60) * 1000; // 提前60秒刷新
100
+ return this.tenantAccessToken;
101
+ }
102
+ /**
103
+ * 启动 Bot
104
+ */
105
+ async start() {
106
+ try {
107
+ // 获取访问令牌
108
+ await this.getTenantAccessToken();
109
+ // 获取 Bot 信息
110
+ this.me = await this.getBotInfo();
111
+ this.emit('ready');
112
+ }
113
+ catch (error) {
114
+ this.emit('error', error);
115
+ throw error;
116
+ }
117
+ }
118
+ /**
119
+ * 停止 Bot
120
+ */
121
+ async stop() {
122
+ this.emit('stopped');
123
+ }
124
+ /**
125
+ * 处理 Webhook 请求
126
+ */
127
+ async handleWebhook(ctx, next) {
128
+ const body = ctx.request.body;
129
+ // 验证事件(如果配置了 verification_token)
130
+ if (this.config.verification_token && body.header?.token !== this.config.verification_token) {
131
+ ctx.status = 401;
132
+ ctx.body = { error: 'Invalid token' };
133
+ return;
134
+ }
135
+ // 处理 URL 验证(飞书首次配置 webhook 时会发送验证请求)
136
+ if (body.type === 'url_verification') {
137
+ ctx.body = { challenge: body.challenge };
138
+ return;
139
+ }
140
+ // 处理事件
141
+ const event = body;
142
+ this.emit('event', event);
143
+ ctx.body = { code: 0 };
144
+ await next();
145
+ }
146
+ /**
147
+ * 获取缓存的 Bot 信息
148
+ */
149
+ getCachedMe() {
150
+ return this.me;
151
+ }
152
+ /**
153
+ * 获取 Bot 信息
154
+ */
155
+ async getBotInfo() {
156
+ const response = await this.get('/im/v1/bots', { page_size: 1 });
157
+ if (response.data.code !== 0) {
158
+ throw new Error(`获取 Bot 信息失败: ${response.data.msg}`);
159
+ }
160
+ // 飞书没有直接的 getBotInfo API,这里返回一个占位信息
161
+ return {
162
+ user_id: this.config.app_id,
163
+ open_id: this.config.app_id,
164
+ name: 'Feishu Bot',
165
+ };
166
+ }
167
+ /**
168
+ * 发送消息
169
+ */
170
+ async sendMessage(receiveId, receiveIdType, content, msgType = 'text') {
171
+ const request = {
172
+ receive_id: receiveId,
173
+ receive_id_type: receiveIdType,
174
+ msg_type: msgType,
175
+ content: typeof content === 'string' ? JSON.stringify({ text: content }) : JSON.stringify(content),
176
+ };
177
+ const response = await this.post('/im/v1/messages', request, {
178
+ receive_id_type: receiveIdType,
179
+ });
180
+ if (response.data.code !== 0) {
181
+ throw new Error(`发送消息失败: ${response.data.msg}`);
182
+ }
183
+ return response.data;
184
+ }
185
+ /**
186
+ * 获取用户信息
187
+ */
188
+ async getUserInfo(userId, userIdType = 'open_id') {
189
+ const response = await this.get(`/contact/v3/users/${userId}`, {
190
+ user_id_type: userIdType,
191
+ });
192
+ if (response.data.code !== 0) {
193
+ throw new Error(`获取用户信息失败: ${response.data.msg}`);
194
+ }
195
+ return response.data.data.user;
196
+ }
197
+ /**
198
+ * 获取群组信息
199
+ */
200
+ async getChatInfo(chatId) {
201
+ const response = await this.get(`/im/v1/chats/${chatId}`);
202
+ if (response.data.code !== 0) {
203
+ throw new Error(`获取群组信息失败: ${response.data.msg}`);
204
+ }
205
+ return response.data.data;
206
+ }
207
+ /**
208
+ * 获取群组成员列表
209
+ */
210
+ async getChatMembers(chatId) {
211
+ const response = await this.get(`/im/v1/chats/${chatId}/members`);
212
+ if (response.data.code !== 0) {
213
+ throw new Error(`获取群组成员列表失败: ${response.data.msg}`);
214
+ }
215
+ return response.data.data.items || [];
216
+ }
217
+ /**
218
+ * 获取 HTTP 客户端实例(返回 this 以便链式调用)
219
+ */
220
+ getHttpClient() {
221
+ return this;
222
+ }
223
+ }
224
+ //# sourceMappingURL=bot.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { FeishuEndpoint, type FeishuConfig, type FeishuEndpointType } from './types.js';
2
+ export * from './adapter.js';
3
+ export * from './bot.js';
4
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // 导出类型和常量
2
+ export { FeishuEndpoint } from './types.js';
3
+ export * from './adapter.js';
4
+ export * from './bot.js';
5
+ //# sourceMappingURL=index.js.map
package/lib/types.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 飞书开放平台 API 类型定义
3
+ * 基于飞书开放平台官方 API
4
+ */
5
+ /**
6
+ * API 端点常量
7
+ * - FEISHU: 飞书(国内版)
8
+ * - LARK: Lark(国际版)
9
+ */
10
+ export declare const FeishuEndpoint: {
11
+ /** 飞书(国内版)API 端点 */
12
+ readonly FEISHU: "https://open.feishu.cn/open-apis";
13
+ /** Lark(国际版)API 端点 */
14
+ readonly LARK: "https://open.larksuite.com/open-apis";
15
+ };
16
+ export type FeishuEndpointType = typeof FeishuEndpoint[keyof typeof FeishuEndpoint];
17
+ export interface FeishuConfig {
18
+ account_id: string;
19
+ app_id: string;
20
+ app_secret: string;
21
+ encrypt_key?: string;
22
+ verification_token?: string;
23
+ /**
24
+ * API 端点,可选值:
25
+ * - FeishuEndpoint.FEISHU (默认): 'https://open.feishu.cn/open-apis'
26
+ * - FeishuEndpoint.LARK: 'https://open.larksuite.com/open-apis'
27
+ * - 或自定义端点 URL
28
+ */
29
+ endpoint?: string;
30
+ }
31
+ export interface FeishuUser {
32
+ user_id: string;
33
+ union_id?: string;
34
+ open_id: string;
35
+ name: string;
36
+ en_name?: string;
37
+ nickname?: string;
38
+ email?: string;
39
+ avatar_url?: string;
40
+ avatar_thumb?: string;
41
+ avatar_middle?: string;
42
+ avatar_big?: string;
43
+ status?: number;
44
+ }
45
+ export interface FeishuChat {
46
+ chat_id: string;
47
+ name?: string;
48
+ description?: string;
49
+ avatar?: string;
50
+ owner_id?: string;
51
+ owner_id_type?: string;
52
+ external?: boolean;
53
+ tenant_key?: string;
54
+ }
55
+ export interface FeishuMessage {
56
+ message_id: string;
57
+ root_id?: string;
58
+ parent_id?: string;
59
+ msg_type: string;
60
+ create_time: string;
61
+ update_time?: string;
62
+ deleted?: boolean;
63
+ updated?: boolean;
64
+ chat_id: string;
65
+ sender: {
66
+ id: string;
67
+ id_type: string;
68
+ sender_type: string;
69
+ tenant_key?: string;
70
+ };
71
+ body: {
72
+ content: string;
73
+ };
74
+ mentions?: Array<{
75
+ key: string;
76
+ id: string;
77
+ id_type: string;
78
+ name: string;
79
+ tenant_key?: string;
80
+ }>;
81
+ }
82
+ export interface FeishuEvent {
83
+ schema: string;
84
+ header: {
85
+ event_id: string;
86
+ event_type: string;
87
+ create_time: string;
88
+ token?: string;
89
+ app_id: string;
90
+ tenant_key: string;
91
+ };
92
+ event: any;
93
+ }
94
+ export interface FeishuTokenResponse {
95
+ code: number;
96
+ msg: string;
97
+ tenant_access_token?: string;
98
+ app_access_token?: string;
99
+ expire: number;
100
+ }
101
+ export interface FeishuSendMessageRequest {
102
+ receive_id: string;
103
+ receive_id_type: 'open_id' | 'user_id' | 'union_id' | 'email' | 'chat_id';
104
+ msg_type: 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user';
105
+ content: string | any;
106
+ uuid?: string;
107
+ }
108
+ export interface FeishuSendMessageResponse {
109
+ code: number;
110
+ msg: string;
111
+ data: {
112
+ message_id: string;
113
+ };
114
+ }
115
+ //# sourceMappingURL=types.d.ts.map
package/lib/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 飞书开放平台 API 类型定义
3
+ * 基于飞书开放平台官方 API
4
+ */
5
+ /**
6
+ * API 端点常量
7
+ * - FEISHU: 飞书(国内版)
8
+ * - LARK: Lark(国际版)
9
+ */
10
+ export const FeishuEndpoint = {
11
+ /** 飞书(国内版)API 端点 */
12
+ FEISHU: 'https://open.feishu.cn/open-apis',
13
+ /** Lark(国际版)API 端点 */
14
+ LARK: 'https://open.larksuite.com/open-apis',
15
+ };
16
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@onebots/adapter-feishu",
3
+ "version": "1.0.0",
4
+ "description": "onebots 飞书适配器",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "keywords": [
9
+ "onebots",
10
+ "feishu",
11
+ "lark",
12
+ "adapter"
13
+ ],
14
+ "author": "凉菜",
15
+ "license": "MIT",
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org"
19
+ },
20
+ "files": [
21
+ "/lib/**/*.js",
22
+ "/lib/**/*.d.ts"
23
+ ],
24
+ "devDependencies": {
25
+ "tsc-alias": "latest",
26
+ "typescript": "latest",
27
+ "@types/node": "^22.7.3"
28
+ },
29
+ "peerDependencies": {
30
+ "onebots": "1.0.0"
31
+ },
32
+ "dependencies": {},
33
+ "scripts": {
34
+ "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json",
35
+ "clean": "rm -rf lib *.tsbuildinfo"
36
+ }
37
+ }