@zhin.js/adapter-lark 4.0.1 → 4.0.3

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +59 -84
  3. package/adapters/lark.ts +26 -0
  4. package/agent/tools/add_members.ts +22 -0
  5. package/agent/tools/create_chat.ts +20 -0
  6. package/agent/tools/dissolve_chat.ts +18 -0
  7. package/agent/tools/get_user.ts +18 -0
  8. package/agent/tools/remove_managers.ts +22 -0
  9. package/agent/tools/set_managers.ts +22 -0
  10. package/agent/tools/update_chat.ts +23 -0
  11. package/agent/tools/upload_file.ts +27 -0
  12. package/lib/endpoint.d.ts +41 -72
  13. package/lib/endpoint.js +219 -563
  14. package/lib/index.d.ts +5 -15
  15. package/lib/index.js +5 -216
  16. package/lib/lark-agent-deps.d.ts +27 -0
  17. package/lib/lark-agent-deps.js +30 -0
  18. package/lib/platform-permit.d.ts +1 -2
  19. package/lib/platform-permit.js +4 -2
  20. package/lib/protocol.d.ts +111 -0
  21. package/lib/protocol.js +204 -0
  22. package/lib/webhook.d.ts +13 -0
  23. package/lib/webhook.js +67 -0
  24. package/package.json +50 -21
  25. package/plugin.ts +12 -0
  26. package/schema.json +25 -0
  27. package/src/endpoint.ts +354 -705
  28. package/src/index.ts +46 -236
  29. package/src/lark-agent-deps.ts +62 -0
  30. package/src/platform-permit.ts +1 -2
  31. package/src/protocol.ts +332 -0
  32. package/src/webhook.ts +101 -0
  33. package/lib/adapter.d.ts +0 -20
  34. package/lib/adapter.d.ts.map +0 -1
  35. package/lib/adapter.js +0 -48
  36. package/lib/adapter.js.map +0 -1
  37. package/lib/endpoint.d.ts.map +0 -1
  38. package/lib/endpoint.js.map +0 -1
  39. package/lib/index.d.ts.map +0 -1
  40. package/lib/index.js.map +0 -1
  41. package/lib/platform-permit.d.ts.map +0 -1
  42. package/lib/platform-permit.js.map +0 -1
  43. package/lib/segment-mapper.d.ts +0 -2
  44. package/lib/segment-mapper.d.ts.map +0 -1
  45. package/lib/segment-mapper.js +0 -2
  46. package/lib/segment-mapper.js.map +0 -1
  47. package/lib/types.d.ts +0 -63
  48. package/lib/types.d.ts.map +0 -1
  49. package/lib/types.js +0 -2
  50. package/lib/types.js.map +0 -1
  51. package/plugin.yml +0 -3
  52. package/src/adapter.ts +0 -57
  53. package/src/segment-mapper.ts +0 -1
  54. package/src/types.ts +0 -58
  55. /package/{skills/lark → agent}/PERMITS.md +0 -0
  56. /package/{skills/lark/SKILL.md → agent/skills/lark.md} +0 -0
package/lib/endpoint.js CHANGED
@@ -1,677 +1,333 @@
1
1
  /**
2
- * 飞书/Lark Endpoint 实现
2
+ * LarkEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
3
3
  */
4
- import { registerFetchRoute } from "@zhin.js/host-router/router";
5
- import axios from "axios";
6
- import { createHash } from "crypto";
7
- import { formatCompact, Message, segment, expandInteractiveSegmentsInContent, } from 'zhin.js';
8
- import { normalizeLarkSenderForPermit } from "./platform-permit.js";
9
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { basename } from 'node:path';
6
+ import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import { registerLarkAgentEndpoint } from './lark-agent-deps.js';
8
+ import { formatInboundContent, formatOutboundBody, generateMessageId, resolveChatType, resolveSender, resolveTarget, } from './protocol.js';
9
+ import { registerLarkWebhookRoutes } from './webhook.js';
10
+ const logger = getLogger('lark');
10
11
  export class LarkEndpoint {
11
- adapter;
12
- $config;
13
- $connected;
14
- router;
15
- accessToken;
16
- axiosInstance;
17
- get logger() {
18
- return this.adapter.plugin.logger;
12
+ #options;
13
+ #fetch;
14
+ #routeReleases = [];
15
+ #accessToken = { token: '', expires_in: 0, timestamp: 0 };
16
+ #refreshPromise = null;
17
+ #open = false;
18
+ #started = false;
19
+ #unregisterAgent;
20
+ constructor(options) {
21
+ this.#options = options;
22
+ this.#fetch = options.fetch ?? globalThis.fetch;
19
23
  }
20
- get $id() {
21
- return this.$config.name;
24
+ /** Used by webhook handler. */
25
+ get isOpen() {
26
+ return this.#open;
22
27
  }
23
- constructor(adapter, router, $config) {
24
- this.adapter = adapter;
25
- this.$config = $config;
26
- this.router = router;
27
- this.$connected = false;
28
- this.accessToken = { token: '', expires_in: 0, timestamp: 0 };
29
- // 设置 API 基础 URL
30
- const baseURL = $config.apiBaseUrl || ($config.isFeishu ?
31
- 'https://open.feishu.cn/open-apis' :
32
- 'https://open.larksuite.com/open-apis');
33
- this.axiosInstance = axios.create({
34
- baseURL,
35
- timeout: 30000,
36
- headers: {
37
- 'Content-Type': 'application/json; charset=utf-8'
38
- }
39
- });
40
- // 设置请求拦截器,自动添加 access_token
41
- this.axiosInstance.interceptors.request.use(async (config) => {
42
- await this.ensureAccessToken();
43
- config.headers = config.headers ?? {};
44
- config.headers['Authorization'] = `Bearer ${this.accessToken.token}`;
45
- return config;
46
- });
47
- // 设置 webhook 路由
48
- this.setupWebhookRoute();
49
- }
50
- setupWebhookRoute() {
51
- registerFetchRoute(this.router, "POST", this.$config.webhookPath, (ctx) => {
52
- void this.handleWebhook(ctx);
53
- });
54
- }
55
- async handleWebhook(ctx) {
56
- try {
57
- const body = ctx.request.body;
58
- // 验证请求(如果配置了验证令牌)
59
- if (this.$config.verificationToken) {
60
- const token = ctx.get('x-lark-request-token');
61
- if (token !== this.$config.verificationToken) {
62
- this.logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid verification token' }));
63
- ctx.status = 403;
64
- ctx.body = 'Forbidden';
65
- return;
66
- }
67
- }
68
- // 签名验证(如果配置了加密密钥)
69
- if (this.$config.encryptKey) {
70
- const timestamp = ctx.get('x-lark-request-timestamp');
71
- const nonce = ctx.get('x-lark-request-nonce');
72
- const signature = ctx.get('x-lark-signature');
73
- const bodyStr = JSON.stringify(body);
74
- if (!timestamp || !nonce || !signature || !this.verifySignature(timestamp, nonce, bodyStr, signature)) {
75
- this.logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid signature' }));
76
- ctx.status = 403;
77
- ctx.body = 'Forbidden';
78
- return;
79
- }
80
- }
81
- const event = body;
82
- // URL 验证挑战(首次配置 webhook 时)
83
- if (event.type === 'url_verification') {
84
- ctx.body = { challenge: event.challenge };
85
- return;
86
- }
87
- // 处理消息事件
88
- if (event.type === 'event_callback' && event.event) {
89
- await this.handleEvent(event.event);
90
- }
91
- ctx.status = 200;
92
- ctx.body = { code: 0, msg: 'success' };
93
- }
94
- catch (error) {
95
- this.logger.error('Webhook error:', error);
96
- ctx.status = 500;
97
- ctx.body = { code: -1, msg: 'Internal Server Error' };
98
- }
99
- }
100
- verifySignature(timestamp, nonce, body, signature) {
101
- if (!this.$config.encryptKey)
102
- return true;
103
- try {
104
- const stringToSign = `${timestamp}${nonce}${this.$config.encryptKey}${body}`;
105
- const calculatedSignature = createHash('sha256').update(stringToSign).digest('hex');
106
- return calculatedSignature === signature;
107
- }
108
- catch (error) {
109
- this.logger.error('Signature verification error:', error);
110
- return false;
111
- }
112
- }
113
- chatPermitCache = new Map();
114
- async enrichGroupSender(message, msg) {
115
- if (message.$channel.type !== 'group' || !msg.chat_id)
116
- return;
117
- const senderId = msg.sender?.sender_id?.open_id;
118
- if (!senderId)
119
- return;
120
- const now = Date.now();
121
- let ownerId;
122
- let managers = [];
123
- const cached = this.chatPermitCache.get(msg.chat_id);
124
- if (cached && now - cached.at < 60_000) {
125
- ownerId = cached.ownerId;
126
- managers = cached.managers;
127
- }
128
- else {
129
- try {
130
- const chat = await this.getChatInfo(msg.chat_id);
131
- ownerId = chat?.owner_id;
132
- managers = Array.isArray(chat?.user_manager_id_list) ? chat.user_manager_id_list : [];
133
- this.chatPermitCache.set(msg.chat_id, { at: now, ownerId, managers });
134
- }
135
- catch {
136
- return;
137
- }
138
- }
139
- const permit = normalizeLarkSenderForPermit({
140
- isOwner: ownerId === senderId,
141
- isAdmin: managers.includes(senderId),
142
- });
143
- message.$sender.role = permit.role;
144
- message.$sender.permissions = permit.permissions;
28
+ get config() {
29
+ return this.#options.config;
145
30
  }
146
- async handleEvent(event) {
147
- // 处理消息事件
148
- if (event.message) {
149
- const message = this.$formatMessage(event.message, event);
150
- await this.enrichGroupSender(message, event.message);
151
- this.adapter.emit('message.receive', message);
152
- this.logger.debug(formatCompact({
153
- op: 'recv',
154
- endpoint: this.$config.name,
155
- channel: message.$channel.type,
156
- id: message.$channel.id,
157
- len: segment.raw(message.$content).length,
158
- }));
159
- }
160
- }
161
- // ================================================================================================
162
- // Token 管理
163
- // ================================================================================================
164
- async ensureAccessToken() {
165
- const now = Date.now();
166
- // 提前 5 分钟刷新 token
167
- if (this.accessToken.token && now < (this.accessToken.timestamp + (this.accessToken.expires_in - 300) * 1000)) {
31
+ async start() {
32
+ if (this.#started)
168
33
  return;
169
- }
170
- await this.refreshAccessToken();
171
- }
172
- async refreshAccessToken() {
34
+ this.#started = true;
173
35
  try {
174
- const response = await axios.post(`${this.$config.apiBaseUrl || (this.$config.isFeishu ?
175
- 'https://open.feishu.cn/open-apis' :
176
- 'https://open.larksuite.com/open-apis')}/auth/v3/tenant_access_token/internal`, {
177
- app_id: this.$config.appId,
178
- app_secret: this.$config.appSecret
179
- });
180
- if (response.data.code === 0) {
181
- this.accessToken = {
182
- token: response.data.tenant_access_token,
183
- expires_in: response.data.expire,
184
- timestamp: Date.now()
185
- };
186
- this.logger.debug('Access token refreshed successfully');
187
- }
188
- else {
189
- throw new Error(`Failed to get access token: ${response.data.msg}`);
190
- }
36
+ await this.#refreshAccessToken();
37
+ this.#unregisterAgent = registerLarkAgentEndpoint(this.#options.config.name, this);
38
+ this.#routeReleases.push(...registerLarkWebhookRoutes(this.#options.http, this));
39
+ logger.debug(formatCompact({
40
+ endpoint: this.#options.config.name,
41
+ op: 'webhook',
42
+ path: this.#options.config.webhookPath,
43
+ }));
191
44
  }
192
45
  catch (error) {
193
- this.logger.error('Failed to refresh access token:', error);
46
+ await this.stop();
47
+ logger.error('Failed to connect Lark endpoint:', error);
194
48
  throw error;
195
49
  }
196
50
  }
197
- // ================================================================================================
198
- // 消息格式化
199
- // ================================================================================================
200
- $formatMessage(msg, event) {
201
- const wire = this.parseMessageContent(msg);
202
- const content = toCanonicalSegments(wire);
203
- // 确定聊天类型
204
- const chatType = msg.chat_id?.startsWith('oc_') ? 'group' : 'private';
205
- return Message.from(msg, {
206
- $id: msg.message_id || Date.now().toString(),
207
- $adapter: 'lark',
208
- $endpoint: this.$config.name,
209
- $sender: {
210
- id: msg.sender?.sender_id?.open_id || 'unknown',
211
- name: msg.sender?.sender_id?.user_id || msg.sender?.sender_id?.open_id || 'Unknown User'
212
- },
213
- $channel: {
214
- id: msg.chat_id || 'unknown',
215
- type: chatType
216
- },
217
- $content: content,
218
- $raw: JSON.stringify(msg),
219
- $timestamp: msg.create_time ? parseInt(msg.create_time) : Date.now(),
220
- $recall: async () => {
221
- await this.$recallMessage(msg.message_id || '');
222
- },
223
- $reply: async (content) => {
224
- return await this.adapter.sendMessage({
225
- context: 'lark',
226
- endpoint: this.$config.name,
227
- id: msg.chat_id || 'unknown',
228
- type: chatType,
229
- content: content
230
- });
231
- }
232
- });
51
+ open() {
52
+ this.#open = true;
233
53
  }
234
- parseMessageContent(msg) {
235
- const content = [];
236
- if (!msg.content || !msg.message_type) {
237
- return content;
238
- }
239
- try {
240
- const messageContent = JSON.parse(msg.content);
241
- switch (msg.message_type) {
242
- case 'text':
243
- if (messageContent.text) {
244
- content.push(segment('text', { content: messageContent.text }));
245
- // 处理 @提及
246
- if (msg.mentions) {
247
- for (const mention of msg.mentions) {
248
- if (mention.key && messageContent.text.includes(mention.key)) {
249
- content.push(segment('at', {
250
- id: mention.id?.open_id,
251
- name: mention.name
252
- }));
253
- }
254
- }
255
- }
256
- }
257
- break;
258
- case 'image':
259
- content.push(segment('image', {
260
- file_key: messageContent.image_key,
261
- url: `https://open.feishu.cn/open-apis/im/v1/messages/${msg.message_id}/resources/${messageContent.image_key}`
262
- }));
263
- break;
264
- case 'file':
265
- content.push(segment('file', {
266
- file_key: messageContent.file_key,
267
- file_name: messageContent.file_name,
268
- file_size: messageContent.file_size
269
- }));
270
- break;
271
- case 'audio':
272
- content.push(segment('audio', {
273
- file_key: messageContent.file_key,
274
- duration: messageContent.duration
275
- }));
276
- break;
277
- case 'video':
278
- content.push(segment('video', {
279
- file_key: messageContent.file_key,
280
- duration: messageContent.duration,
281
- width: messageContent.width,
282
- height: messageContent.height
283
- }));
284
- break;
285
- case 'sticker':
286
- content.push(segment('sticker', {
287
- file_key: messageContent.file_key
288
- }));
289
- break;
290
- case 'rich_text':
291
- // 富文本消息处理(简化)
292
- if (messageContent.content) {
293
- this.parseRichTextContent(messageContent.content, content);
294
- }
295
- break;
296
- case 'post':
297
- // 卡片消息处理
298
- content.push(segment('card', messageContent));
299
- break;
300
- default:
301
- content.push(segment('text', { content: `[不支持的消息类型: ${msg.message_type}]` }));
302
- break;
303
- }
304
- }
305
- catch (error) {
306
- this.logger.error('Failed to parse message content:', error);
307
- content.push(segment('text', { content: '[消息解析失败]' }));
308
- }
309
- return content;
54
+ close() {
55
+ this.#open = false;
310
56
  }
311
- parseRichTextContent(richContent, content) {
312
- // 简化的富文本解析
313
- if (Array.isArray(richContent)) {
314
- for (const block of richContent) {
315
- if (block.tag === 'text' && block.text) {
316
- content.push(segment('text', { content: block.text }));
317
- }
318
- else if (block.tag === 'a' && block.href) {
319
- content.push(segment('link', {
320
- url: block.href,
321
- text: block.text || block.href
322
- }));
323
- }
324
- else if (block.tag === 'at' && block.user_id) {
325
- content.push(segment('at', {
326
- id: block.user_id,
327
- name: block.user_name
328
- }));
329
- }
330
- }
331
- }
57
+ async stop() {
58
+ this.#open = false;
59
+ for (const release of this.#routeReleases.splice(0))
60
+ release();
61
+ this.#unregisterAgent?.();
62
+ this.#unregisterAgent = undefined;
63
+ this.#started = false;
64
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
332
65
  }
333
- // ================================================================================================
334
- // 消息发送
335
- // ================================================================================================
336
- async $sendMessage(options) {
337
- const chatId = options.id;
338
- const canonical = expandInteractiveSegmentsInContent(options.content);
339
- const wire = fromCanonicalSegments(canonical);
340
- const content = this.formatSendContent(wire);
341
- try {
342
- const response = await this.axiosInstance.post('/im/v1/messages', {
343
- receive_id: chatId,
344
- receive_id_type: 'chat_id',
66
+ async send({ target, payload }) {
67
+ const content = formatOutboundBody(payload);
68
+ const data = await this.#request('/im/v1/messages', {
69
+ method: 'POST',
70
+ params: { receive_id_type: 'chat_id' },
71
+ body: {
72
+ receive_id: target,
345
73
  msg_type: content.msg_type,
346
- content: content.content
347
- });
348
- if (response.data.code !== 0) {
349
- throw new Error(`Failed to send message: ${response.data.msg}`);
350
- }
351
- this.logger.debug('Message sent successfully:', response.data.data?.message_id);
352
- return response.data.data?.message_id || '';
353
- }
354
- catch (error) {
355
- this.logger.error('Failed to send message:', error);
356
- throw error;
357
- }
358
- }
359
- async $recallMessage(id) {
360
- await this.axiosInstance.post('/im/v1/messages/recall', {
361
- message_id: id
74
+ content: content.content,
75
+ },
362
76
  });
77
+ if (data.code !== 0) {
78
+ throw new Error(`Failed to send message: ${data.msg}`);
79
+ }
80
+ const messageId = data.data?.message_id || `${Date.now()}`;
81
+ logger.debug(formatCompact({
82
+ op: 'send',
83
+ endpoint: this.#options.config.name,
84
+ to: target,
85
+ id: messageId,
86
+ }));
87
+ return messageId;
363
88
  }
364
- formatSendContent(content) {
365
- if (typeof content === 'string') {
366
- return {
367
- msg_type: 'text',
368
- content: JSON.stringify({ text: content })
369
- };
370
- }
371
- if (Array.isArray(content)) {
372
- const textParts = [];
373
- let hasMedia = false;
374
- let mediaContent = null;
375
- for (const item of content) {
376
- if (typeof item === 'string') {
377
- textParts.push(item);
378
- }
379
- else {
380
- const segment = item;
381
- switch (segment.type) {
382
- case 'text':
383
- textParts.push(segment.data.content || segment.data.text || '');
384
- break;
385
- case 'at':
386
- textParts.push(`<at user_id="${segment.data.id}">${segment.data.name || segment.data.id}</at>`);
387
- break;
388
- case 'image':
389
- if (!hasMedia) {
390
- hasMedia = true;
391
- mediaContent = {
392
- msg_type: 'image',
393
- content: JSON.stringify({
394
- image_key: segment.data.file_key || segment.data.key
395
- })
396
- };
397
- }
398
- break;
399
- case 'file':
400
- if (!hasMedia) {
401
- hasMedia = true;
402
- mediaContent = {
403
- msg_type: 'file',
404
- content: JSON.stringify({
405
- file_key: segment.data.file_key || segment.data.key
406
- })
407
- };
408
- }
409
- break;
410
- case 'card':
411
- if (!hasMedia) {
412
- hasMedia = true;
413
- mediaContent = {
414
- msg_type: 'interactive',
415
- content: JSON.stringify(segment.data)
416
- };
417
- }
418
- break;
419
- }
420
- }
421
- }
422
- // 优先发送媒体内容
423
- if (hasMedia && mediaContent) {
424
- return mediaContent;
425
- }
426
- // 否则发送文本内容
427
- return {
428
- msg_type: 'text',
429
- content: JSON.stringify({ text: textParts.join('') })
430
- };
431
- }
432
- return {
433
- msg_type: 'text',
434
- content: JSON.stringify({ text: String(content) })
435
- };
436
- }
437
- // ================================================================================================
438
- // Endpoint 生命周期
439
- // ================================================================================================
440
- async $connect() {
441
- try {
442
- // 获取 access token
443
- await this.refreshAccessToken();
444
- this.$connected = true;
445
- this.logger.info(formatCompact({ endpoint: this.$config.name }));
446
- this.logger.info(formatCompact({ op: 'webhook', path: this.$config.webhookPath }));
447
- }
448
- catch (error) {
449
- this.logger.error('Failed to connect Lark bot:', error);
450
- throw error;
451
- }
452
- }
453
- async $disconnect() {
454
- try {
455
- this.$connected = false;
456
- this.logger.info(formatCompact({ op: 'disconnect', endpoint: this.$config.name }));
457
- }
458
- catch (error) {
459
- this.logger.error('Error disconnecting Lark bot:', error);
460
- }
89
+ /** Test / internal: admit a parsed message when open (non-webhook path). */
90
+ admit(msg) {
91
+ if (!this.#open)
92
+ return;
93
+ const target = resolveTarget(msg);
94
+ void this.#options.gateway.receive({
95
+ adapter: this.#options.id,
96
+ target,
97
+ content: formatInboundContent(msg),
98
+ sender: resolveSender(msg),
99
+ id: generateMessageId(msg),
100
+ metadata: Object.freeze({
101
+ messageType: msg.message_type,
102
+ chatType: resolveChatType(msg.chat_id),
103
+ endpoint: this.#options.config.name,
104
+ }),
105
+ }).catch((err) => {
106
+ logger.warn(formatCompact({
107
+ op: 'lark_gateway_receive_failed',
108
+ target,
109
+ error: err instanceof Error ? err.message : String(err),
110
+ }));
111
+ });
461
112
  }
462
- // ================================================================================================
463
- // 工具方法
464
- // ================================================================================================
465
- // 获取用户信息
466
113
  async getUserInfo(userId, userIdType = 'open_id') {
467
114
  try {
468
- const response = await this.axiosInstance.get(`/contact/v3/users/${userId}`, {
469
- params: { user_id_type: userIdType }
115
+ const data = await this.#request(`/contact/v3/users/${userId}`, {
116
+ method: 'GET',
117
+ params: { user_id_type: userIdType },
470
118
  });
471
- return response.data.data?.user;
119
+ return data.data?.user ?? null;
472
120
  }
473
121
  catch (error) {
474
- this.logger.error('Failed to get user info:', error);
122
+ logger.error('Failed to get user info:', error);
475
123
  return null;
476
124
  }
477
125
  }
478
- // 获取群聊信息
479
126
  async getChatInfo(chatId) {
480
127
  try {
481
- const response = await this.axiosInstance.get(`/im/v1/chats/${chatId}`);
482
- return response.data.data;
128
+ const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'GET' });
129
+ return data.data ?? null;
483
130
  }
484
131
  catch (error) {
485
- this.logger.error('Failed to get chat info:', error);
132
+ logger.error('Failed to get chat info:', error);
486
133
  return null;
487
134
  }
488
135
  }
489
- // 上传文件
490
136
  async uploadFile(filePath, fileType = 'file') {
491
137
  try {
492
- const FormData = require('form-data');
493
- const fs = require('fs');
138
+ await this.#ensureAccessToken();
139
+ const buf = await readFile(filePath);
494
140
  const form = new FormData();
495
- form.append('file', fs.createReadStream(filePath));
141
+ form.append('file', new Blob([buf]), basename(filePath));
496
142
  form.append('file_type', fileType);
497
- const response = await this.axiosInstance.post('/im/v1/files', form, {
498
- headers: {
499
- ...form.getHeaders()
500
- }
143
+ const url = `${this.#options.config.apiBaseUrl}/im/v1/files`;
144
+ const response = await this.#fetch(url, {
145
+ method: 'POST',
146
+ headers: { Authorization: `Bearer ${this.#accessToken.token}` },
147
+ body: form,
501
148
  });
502
- if (response.data.code === 0) {
503
- return response.data.data.file_key;
149
+ const data = await response.json();
150
+ if (data.code === 0) {
151
+ return data.data?.file_key || null;
504
152
  }
505
- throw new Error(`Upload failed: ${response.data.msg}`);
153
+ throw new Error(`Upload failed: ${data.msg}`);
506
154
  }
507
155
  catch (error) {
508
- this.logger.error('Failed to upload file:', error);
156
+ logger.error('Failed to upload file:', error);
509
157
  return null;
510
158
  }
511
159
  }
512
- // ==================== 群组管理 API ====================
513
- /**
514
- * 创建群聊
515
- * @param name 群名
516
- * @param userIds 成员 open_id 列表
517
- * @param ownerId 群主 open_id
518
- */
519
160
  async createChat(name, userIds, ownerId) {
520
161
  try {
521
- const response = await this.axiosInstance.post('/im/v1/chats', {
522
- name,
523
- user_id_list: userIds,
524
- owner_id: ownerId
162
+ const data = await this.#request('/im/v1/chats', {
163
+ method: 'POST',
164
+ body: {
165
+ name,
166
+ user_id_list: userIds,
167
+ ...(ownerId ? { owner_id: ownerId } : {}),
168
+ },
525
169
  });
526
- if (response.data.code === 0) {
527
- this.logger.debug(formatCompact({ op: 'create_chat', chat: response.data.data.chat_id }));
528
- return response.data.data.chat_id;
529
- }
530
- throw new Error(`Failed to create chat: ${response.data.msg}`);
170
+ if (data.code === 0)
171
+ return data.data?.chat_id || null;
172
+ throw new Error(`Failed to create chat: ${data.msg}`);
531
173
  }
532
174
  catch (error) {
533
- this.logger.error('Failed to create chat:', error);
175
+ logger.error('Failed to create chat:', error);
534
176
  return null;
535
177
  }
536
178
  }
537
- /**
538
- * 更新群信息
539
- * @param chatId 群聊 ID
540
- * @param options 更新选项
541
- */
542
179
  async updateChatInfo(chatId, options) {
543
180
  try {
544
- const response = await this.axiosInstance.put(`/im/v1/chats/${chatId}`, options);
545
- if (response.data.code === 0) {
546
- this.logger.debug(formatCompact({ op: 'update_chat', chat: chatId }));
181
+ const data = await this.#request(`/im/v1/chats/${chatId}`, {
182
+ method: 'PUT',
183
+ body: options,
184
+ });
185
+ if (data.code === 0)
547
186
  return true;
548
- }
549
- throw new Error(`Failed to update chat: ${response.data.msg}`);
187
+ throw new Error(`Failed to update chat: ${data.msg}`);
550
188
  }
551
189
  catch (error) {
552
- this.logger.error('Failed to update chat:', error);
190
+ logger.error('Failed to update chat:', error);
553
191
  return false;
554
192
  }
555
193
  }
556
- /**
557
- * 添加群成员
558
- * @param chatId 群聊 ID
559
- * @param userIds 用户 ID 列表
560
- */
561
194
  async addChatMembers(chatId, userIds) {
562
195
  try {
563
- const response = await this.axiosInstance.post(`/im/v1/chats/${chatId}/members`, {
564
- id_list: userIds
196
+ const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
197
+ method: 'POST',
198
+ body: { id_list: userIds },
565
199
  });
566
- if (response.data.code === 0) {
567
- this.logger.debug(formatCompact({ op: 'add_member', chat: chatId }));
200
+ if (data.code === 0)
568
201
  return true;
569
- }
570
- throw new Error(`Failed to add members: ${response.data.msg}`);
202
+ throw new Error(`Failed to add members: ${data.msg}`);
571
203
  }
572
204
  catch (error) {
573
- this.logger.error('Failed to add chat members:', error);
205
+ logger.error('Failed to add chat members:', error);
574
206
  return false;
575
207
  }
576
208
  }
577
- /**
578
- * 移除群成员
579
- * @param chatId 群聊 ID
580
- * @param userIds 用户 ID 列表
581
- */
582
209
  async removeChatMembers(chatId, userIds) {
583
210
  try {
584
- const response = await this.axiosInstance.delete(`/im/v1/chats/${chatId}/members`, {
585
- data: { id_list: userIds }
211
+ const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
212
+ method: 'DELETE',
213
+ body: { id_list: userIds },
586
214
  });
587
- if (response.data.code === 0) {
588
- this.logger.debug(formatCompact({ op: 'remove_member', chat: chatId }));
215
+ if (data.code === 0)
589
216
  return true;
590
- }
591
- throw new Error(`Failed to remove members: ${response.data.msg}`);
217
+ throw new Error(`Failed to remove members: ${data.msg}`);
592
218
  }
593
219
  catch (error) {
594
- this.logger.error('Failed to remove chat members:', error);
220
+ logger.error('Failed to remove chat members:', error);
595
221
  return false;
596
222
  }
597
223
  }
598
- /**
599
- * 获取群成员列表
600
- * @param chatId 群聊 ID
601
- */
602
224
  async getChatMembers(chatId) {
603
225
  try {
604
- const response = await this.axiosInstance.get(`/im/v1/chats/${chatId}/members`);
605
- if (response.data.code === 0) {
606
- return response.data.data.items || [];
607
- }
608
- throw new Error(`Failed to get members: ${response.data.msg}`);
226
+ const data = await this.#request(`/im/v1/chats/${chatId}/members`, { method: 'GET' });
227
+ if (data.code === 0)
228
+ return data.data?.items || [];
229
+ throw new Error(`Failed to get members: ${data.msg}`);
609
230
  }
610
231
  catch (error) {
611
- this.logger.error('Failed to get chat members:', error);
232
+ logger.error('Failed to get chat members:', error);
612
233
  return [];
613
234
  }
614
235
  }
615
- /**
616
- * 解散群聊
617
- * @param chatId 群聊 ID
618
- */
619
236
  async dissolveChat(chatId) {
620
237
  try {
621
- const response = await this.axiosInstance.delete(`/im/v1/chats/${chatId}`);
622
- if (response.data.code === 0) {
623
- this.logger.debug(formatCompact({ op: 'delete_chat', chat: chatId }));
238
+ const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'DELETE' });
239
+ if (data.code === 0)
624
240
  return true;
625
- }
626
- throw new Error(`Failed to dissolve chat: ${response.data.msg}`);
241
+ throw new Error(`Failed to dissolve chat: ${data.msg}`);
627
242
  }
628
243
  catch (error) {
629
- this.logger.error('Failed to dissolve chat:', error);
244
+ logger.error('Failed to dissolve chat:', error);
630
245
  return false;
631
246
  }
632
247
  }
633
- /**
634
- * 设置群管理员
635
- * @param chatId 群聊 ID
636
- * @param userIds 用户 ID 列表
637
- */
638
248
  async setChatManagers(chatId, userIds) {
639
249
  try {
640
- const response = await this.axiosInstance.post(`/im/v1/chats/${chatId}/managers/add_managers`, {
641
- manager_ids: userIds
250
+ const data = await this.#request(`/im/v1/chats/${chatId}/managers/add_managers`, {
251
+ method: 'POST',
252
+ body: { manager_ids: userIds },
642
253
  });
643
- if (response.data.code === 0) {
644
- this.logger.debug(formatCompact({ op: 'set_admin', chat: chatId }));
254
+ if (data.code === 0)
645
255
  return true;
646
- }
647
- throw new Error(`Failed to set managers: ${response.data.msg}`);
256
+ throw new Error(`Failed to set managers: ${data.msg}`);
648
257
  }
649
258
  catch (error) {
650
- this.logger.error('Failed to set chat managers:', error);
259
+ logger.error('Failed to set chat managers:', error);
651
260
  return false;
652
261
  }
653
262
  }
654
- /**
655
- * 移除群管理员
656
- * @param chatId 群聊 ID
657
- * @param userIds 用户 ID 列表
658
- */
659
263
  async removeChatManagers(chatId, userIds) {
660
264
  try {
661
- const response = await this.axiosInstance.post(`/im/v1/chats/${chatId}/managers/delete_managers`, {
662
- manager_ids: userIds
265
+ const data = await this.#request(`/im/v1/chats/${chatId}/managers/delete_managers`, {
266
+ method: 'POST',
267
+ body: { manager_ids: userIds },
663
268
  });
664
- if (response.data.code === 0) {
665
- this.logger.debug(formatCompact({ op: 'remove_admin', chat: chatId }));
269
+ if (data.code === 0)
666
270
  return true;
667
- }
668
- throw new Error(`Failed to remove managers: ${response.data.msg}`);
271
+ throw new Error(`Failed to remove managers: ${data.msg}`);
669
272
  }
670
273
  catch (error) {
671
- this.logger.error('Failed to remove chat managers:', error);
274
+ logger.error('Failed to remove chat managers:', error);
672
275
  return false;
673
276
  }
674
277
  }
278
+ async #request(path, options = {}) {
279
+ await this.#ensureAccessToken();
280
+ const { method = 'GET', params = {}, body } = options;
281
+ const urlParams = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)])));
282
+ const query = urlParams.toString();
283
+ const url = `${this.#options.config.apiBaseUrl}${path}${query ? `?${query}` : ''}`;
284
+ const response = await this.#fetch(url, {
285
+ method,
286
+ headers: {
287
+ 'Content-Type': 'application/json; charset=utf-8',
288
+ Authorization: `Bearer ${this.#accessToken.token}`,
289
+ },
290
+ body: body && method !== 'GET' ? JSON.stringify(body) : undefined,
291
+ });
292
+ if (!response.ok) {
293
+ const text = await response.text().catch(() => '');
294
+ throw new Error(`Lark API error ${response.status}: ${text}`);
295
+ }
296
+ return await response.json();
297
+ }
298
+ async #ensureAccessToken() {
299
+ const now = Date.now();
300
+ if (this.#accessToken.token
301
+ && now < this.#accessToken.timestamp + (this.#accessToken.expires_in - 300) * 1000) {
302
+ return;
303
+ }
304
+ if (this.#refreshPromise) {
305
+ await this.#refreshPromise;
306
+ return;
307
+ }
308
+ this.#refreshPromise = this.#refreshAccessToken()
309
+ .then(() => this.#accessToken.token)
310
+ .finally(() => { this.#refreshPromise = null; });
311
+ await this.#refreshPromise;
312
+ }
313
+ async #refreshAccessToken() {
314
+ const { appId, appSecret, apiBaseUrl } = this.#options.config;
315
+ const url = `${apiBaseUrl}/auth/v3/tenant_access_token/internal`;
316
+ const response = await this.#fetch(url, {
317
+ method: 'POST',
318
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
319
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
320
+ });
321
+ const data = await response.json();
322
+ if (data.code === 0 && data.tenant_access_token) {
323
+ this.#accessToken = {
324
+ token: data.tenant_access_token,
325
+ expires_in: data.expire ?? 7200,
326
+ timestamp: Date.now(),
327
+ };
328
+ logger.debug('Access token refreshed successfully');
329
+ return;
330
+ }
331
+ throw new Error(`Failed to get access token: ${data.msg} (${data.code})`);
332
+ }
675
333
  }
676
- // 定义 Adapter 类
677
- //# sourceMappingURL=endpoint.js.map