@zhin.js/adapter-lark 1.0.96 → 1.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 +641 -0
- package/README.md +70 -83
- package/adapters/lark.js +36 -0
- package/adapters/lark.ts +41 -0
- package/agent/PERMITS.md +22 -0
- package/agent/tools/add_members.ts +20 -0
- package/agent/tools/create_chat.ts +18 -0
- package/agent/tools/dissolve_chat.ts +16 -0
- package/agent/tools/get_user.ts +16 -0
- package/agent/tools/remove_managers.ts +20 -0
- package/agent/tools/set_managers.ts +20 -0
- package/agent/tools/update_chat.ts +21 -0
- package/agent/tools/upload_file.ts +23 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +12 -0
- package/lib/client.js +2 -0
- package/lib/endpoint.d.ts +100 -0
- package/lib/endpoint.js +497 -0
- package/lib/index.d.ts +6 -15
- package/lib/index.js +6 -209
- package/lib/lark-endpoint-commands.d.ts +1 -0
- package/lib/lark-endpoint-commands.js +19 -0
- package/lib/lark-runtime-state.d.ts +1 -0
- package/lib/lark-runtime-state.js +6 -0
- package/lib/media-upload.d.ts +15 -0
- package/lib/media-upload.js +58 -0
- package/lib/platform-permit.d.ts +14 -0
- package/lib/platform-permit.js +35 -0
- package/lib/protocol.d.ts +123 -0
- package/lib/protocol.js +273 -0
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +73 -0
- package/package.json +63 -20
- package/plugin.js +19 -0
- package/schema.json +97 -0
- package/src/client.ts +16 -0
- package/src/endpoint.ts +590 -0
- package/src/index.ts +51 -227
- package/src/lark-endpoint-commands.ts +20 -0
- package/src/lark-runtime-state.ts +7 -0
- package/src/media-upload.ts +69 -0
- package/src/platform-permit.ts +50 -0
- package/src/protocol.ts +402 -0
- package/src/webhook.ts +110 -0
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -45
- package/lib/adapter.js.map +0 -1
- package/lib/bot.d.ts +0 -82
- package/lib/bot.d.ts.map +0 -1
- package/lib/bot.js +0 -638
- package/lib/bot.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -63
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -55
- package/src/bot.ts +0 -712
- package/src/types.ts +0 -58
- /package/{skills/lark/SKILL.md → agent/skills/lark.md} +0 -0
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* LarkEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { basename } from 'node:path';
|
|
7
|
+
import { createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
8
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
|
+
import { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
|
|
10
|
+
import { formatInboundContent, formatOutboundBody, generateMessageId, larkInboundConversation, resolveChatType, resolveSender, } from './protocol.js';
|
|
11
|
+
import { registerLarkWebhookRoutes } from './webhook.js';
|
|
12
|
+
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
13
|
+
const OUTBOUND_TIMEOUT_MS = 30_000;
|
|
14
|
+
/** Feishu/Lark OpenAPI client surface carried by every Endpoint event. */
|
|
15
|
+
export class LarkClient {
|
|
16
|
+
api;
|
|
17
|
+
constructor(api) {
|
|
18
|
+
this.api = api;
|
|
19
|
+
}
|
|
20
|
+
getUserInfo = (id, type) => this.api.getUserInfo(id, type);
|
|
21
|
+
getChatInfo = (id) => this.api.getChatInfo(id);
|
|
22
|
+
uploadFile = (path, type) => this.api.uploadFile(path, type);
|
|
23
|
+
createChat = (name, users, owner) => this.api.createChat(name, users, owner);
|
|
24
|
+
updateChatInfo = (id, options) => this.api.updateChatInfo(id, options);
|
|
25
|
+
addChatMembers = (id, users) => this.api.addChatMembers(id, users);
|
|
26
|
+
removeChatMembers = (id, users) => this.api.removeChatMembers(id, users);
|
|
27
|
+
getChatMembers = (id) => this.api.getChatMembers(id);
|
|
28
|
+
dissolveChat = (id) => this.api.dissolveChat(id);
|
|
29
|
+
setChatManagers = (id, users) => this.api.setChatManagers(id, users);
|
|
30
|
+
removeChatManagers = (id, users) => this.api.removeChatManagers(id, users);
|
|
31
|
+
}
|
|
32
|
+
export class LarkEndpoint extends Endpoint {
|
|
33
|
+
client = new LarkClient({
|
|
34
|
+
getUserInfo: (id, type) => this.getUserInfo(id, type),
|
|
35
|
+
getChatInfo: (id) => this.getChatInfo(id),
|
|
36
|
+
uploadFile: (path, type) => this.uploadFile(path, type),
|
|
37
|
+
createChat: (name, users, owner) => this.createChat(name, users, owner),
|
|
38
|
+
updateChatInfo: (id, options) => this.updateChatInfo(id, options),
|
|
39
|
+
addChatMembers: (id, users) => this.addChatMembers(id, users),
|
|
40
|
+
removeChatMembers: (id, users) => this.removeChatMembers(id, users),
|
|
41
|
+
getChatMembers: (id) => this.getChatMembers(id),
|
|
42
|
+
dissolveChat: (id) => this.dissolveChat(id),
|
|
43
|
+
setChatManagers: (id, users) => this.setChatManagers(id, users),
|
|
44
|
+
removeChatManagers: (id, users) => this.removeChatManagers(id, users),
|
|
45
|
+
});
|
|
46
|
+
#logger;
|
|
47
|
+
#options;
|
|
48
|
+
#fetch;
|
|
49
|
+
/**
|
|
50
|
+
* Console 社交面语义端口。Lark OpenAPI 仅接读取类列表能力:
|
|
51
|
+
* - listGroups:GET /im/v1/chats(bot 所在群列表,分页归一为 {group_id: chat_id, name})
|
|
52
|
+
* - listGroupMembers:GET /im/v1/chats/:chat_id/members(open_id 分页,返回平台原生成员形状)
|
|
53
|
+
* 不接的项:listFriends(Lark 无好友概念,contact API 为组织通讯录而非社交关系)、
|
|
54
|
+
* listChannels(无频道/guild 概念)、写操作(审批/踢人/禁言等本轮不在范围内)。
|
|
55
|
+
*/
|
|
56
|
+
management = Object.freeze({
|
|
57
|
+
listGroups: async () => {
|
|
58
|
+
const items = await this.#listAllPages('/im/v1/chats');
|
|
59
|
+
const groups = [];
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const chatId = typeof item.chat_id === 'string' ? item.chat_id : '';
|
|
62
|
+
if (!chatId)
|
|
63
|
+
continue;
|
|
64
|
+
groups.push({
|
|
65
|
+
group_id: chatId,
|
|
66
|
+
name: typeof item.name === 'string' && item.name ? item.name : chatId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return groups;
|
|
70
|
+
},
|
|
71
|
+
listGroupMembers: (groupId) => this.#listAllPages(`/im/v1/chats/${encodeURIComponent(groupId)}/members`, { member_id_type: 'open_id' }),
|
|
72
|
+
});
|
|
73
|
+
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
74
|
+
#routeReleases = [];
|
|
75
|
+
#accessToken = { token: '', expires_in: 0, timestamp: 0 };
|
|
76
|
+
#refreshPromise = null;
|
|
77
|
+
#open = false;
|
|
78
|
+
#started = false;
|
|
79
|
+
constructor(options) {
|
|
80
|
+
super();
|
|
81
|
+
this.#logger = getAdapterLogger('lark', options.config.id);
|
|
82
|
+
this.#options = options;
|
|
83
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
84
|
+
}
|
|
85
|
+
/** Used by webhook handler. */
|
|
86
|
+
get isOpen() {
|
|
87
|
+
return this.#open;
|
|
88
|
+
}
|
|
89
|
+
get config() {
|
|
90
|
+
return this.#options.config;
|
|
91
|
+
}
|
|
92
|
+
async start() {
|
|
93
|
+
if (this.#started)
|
|
94
|
+
return;
|
|
95
|
+
this.#started = true;
|
|
96
|
+
try {
|
|
97
|
+
if (!this.#options.config.encryptKey && !this.#options.config.verificationToken) {
|
|
98
|
+
// encryptKey / verificationToken 都未配置时 webhook 完全无鉴权,任何人可伪造事件
|
|
99
|
+
this.#logger.warn(formatCompact({
|
|
100
|
+
endpoint: this.#options.config.id,
|
|
101
|
+
op: 'webhook',
|
|
102
|
+
ok: false,
|
|
103
|
+
error: 'neither encryptKey nor verificationToken configured; webhook is unauthenticated',
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
await this.#refreshAccessToken();
|
|
107
|
+
this.#routeReleases.push(...registerLarkWebhookRoutes(this.#options.http, this));
|
|
108
|
+
this.#logger.debug(formatCompact({
|
|
109
|
+
endpoint: this.#options.config.id,
|
|
110
|
+
op: 'webhook',
|
|
111
|
+
path: this.#options.config.webhookPath,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
await this.stop();
|
|
116
|
+
this.#logger.error('Failed to connect Lark endpoint:', error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
open() {
|
|
121
|
+
this.#open = true;
|
|
122
|
+
}
|
|
123
|
+
close() {
|
|
124
|
+
this.#open = false;
|
|
125
|
+
}
|
|
126
|
+
async stop() {
|
|
127
|
+
this.#open = false;
|
|
128
|
+
for (const release of this.#routeReleases.splice(0))
|
|
129
|
+
release();
|
|
130
|
+
this.#started = false;
|
|
131
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
132
|
+
}
|
|
133
|
+
async send({ conversation, payload }) {
|
|
134
|
+
const materialized = await this.#materializeOutboundMedia(payload);
|
|
135
|
+
const content = formatOutboundBody(materialized);
|
|
136
|
+
const data = await this.#request('/im/v1/messages', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
params: { receive_id_type: 'chat_id' },
|
|
139
|
+
body: {
|
|
140
|
+
receive_id: conversation.id,
|
|
141
|
+
msg_type: content.msg_type,
|
|
142
|
+
content: content.content,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (data.code !== 0) {
|
|
146
|
+
throw new Error(`Failed to send message: ${data.msg}`);
|
|
147
|
+
}
|
|
148
|
+
const messageId = data.data?.message_id || `${Date.now()}`;
|
|
149
|
+
this.#logger.debug(formatCompact({ op: 'send', to: conversation.id,
|
|
150
|
+
id: messageId,
|
|
151
|
+
}));
|
|
152
|
+
return messageId;
|
|
153
|
+
}
|
|
154
|
+
async recallMessage(messageId) {
|
|
155
|
+
if (!messageId || messageId.startsWith('outbound:'))
|
|
156
|
+
return;
|
|
157
|
+
await this.#request(`/im/v1/messages/${messageId}`, { method: 'DELETE' });
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* im/v1 消息 image 段只接受 image_key:canonical MediaRef(base64/本地路径/URL)
|
|
161
|
+
* 先经 /im/v1/images 物化,产物回写为 MediaRef kind=file(平台不透明引用);
|
|
162
|
+
* 上传失败降级为文本(alt 优先),不阻断发送。
|
|
163
|
+
*/
|
|
164
|
+
async #materializeOutboundMedia(payload) {
|
|
165
|
+
if (!Array.isArray(payload))
|
|
166
|
+
return payload;
|
|
167
|
+
return Promise.all(payload.map(async (item) => {
|
|
168
|
+
if (typeof item === 'string' || !item || typeof item !== 'object')
|
|
169
|
+
return item;
|
|
170
|
+
const seg = item;
|
|
171
|
+
if (seg.type !== 'image')
|
|
172
|
+
return item;
|
|
173
|
+
const data = seg.data ?? {};
|
|
174
|
+
const media = readOutboundImageMedia(data);
|
|
175
|
+
if (!media)
|
|
176
|
+
return item;
|
|
177
|
+
const imageKey = await this.uploadImage(media);
|
|
178
|
+
if (!imageKey) {
|
|
179
|
+
const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
|
|
180
|
+
return { type: 'text', data: { text: alt } };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
type: 'image',
|
|
184
|
+
data: { media: { kind: 'file', value: imageKey } },
|
|
185
|
+
};
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
/** POST /im/v1/images(image_type=message),返回 image_key;失败返回 null。 */
|
|
189
|
+
async uploadImage(media) {
|
|
190
|
+
try {
|
|
191
|
+
await this.#ensureAccessToken();
|
|
192
|
+
const binary = await resolveMediaBinary(media);
|
|
193
|
+
const form = buildImageUploadForm(binary);
|
|
194
|
+
const url = `${this.#options.config.apiBaseUrl}/im/v1/images`;
|
|
195
|
+
const response = await this.#fetch(url, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { Authorization: `Bearer ${this.#accessToken.token}` },
|
|
198
|
+
body: form,
|
|
199
|
+
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
200
|
+
});
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
if (data.code === 0) {
|
|
203
|
+
return data.data?.image_key || null;
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`Image upload failed: ${data.msg}`);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
this.#logger.error('Failed to upload image:', error);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Test / internal: admit a parsed message when open (non-webhook path). */
|
|
213
|
+
admit(msg) {
|
|
214
|
+
if (!this.#open)
|
|
215
|
+
return;
|
|
216
|
+
const conversation = larkInboundConversation(String(this.#options.id), msg);
|
|
217
|
+
void this.emit('message.receive', {
|
|
218
|
+
conversation,
|
|
219
|
+
message: { conversation, id: generateMessageId(msg) },
|
|
220
|
+
content: formatInboundContent(msg),
|
|
221
|
+
sender: { id: resolveSender(msg) },
|
|
222
|
+
endpointId: this.#options.config.id,
|
|
223
|
+
metadata: Object.freeze({
|
|
224
|
+
messageType: msg.message_type,
|
|
225
|
+
chatType: resolveChatType(msg.chat_id, msg.chat_type),
|
|
226
|
+
}),
|
|
227
|
+
}).catch((err) => {
|
|
228
|
+
this.#logger.warn(formatCompact({
|
|
229
|
+
op: 'lark_gateway_receive_failed',
|
|
230
|
+
conversation: conversation.id,
|
|
231
|
+
error: err instanceof Error ? err.message : String(err),
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
admitPlatform(name, event) {
|
|
236
|
+
if (!this.#open)
|
|
237
|
+
return;
|
|
238
|
+
void this.emitPlatform(name, event).catch((error) => {
|
|
239
|
+
this.#logger.warn(formatCompact({
|
|
240
|
+
op: 'lark_platform_event_failed',
|
|
241
|
+
event: name,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async getUserInfo(userId, userIdType = 'open_id') {
|
|
247
|
+
try {
|
|
248
|
+
const data = await this.#request(`/contact/v3/users/${userId}`, {
|
|
249
|
+
method: 'GET',
|
|
250
|
+
params: { user_id_type: userIdType },
|
|
251
|
+
});
|
|
252
|
+
return data.data?.user ?? null;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
this.#logger.error('Failed to get user info:', error);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async getChatInfo(chatId) {
|
|
260
|
+
try {
|
|
261
|
+
const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'GET' });
|
|
262
|
+
return data.data ?? null;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.#logger.error('Failed to get chat info:', error);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async uploadFile(filePath, fileType = 'file') {
|
|
270
|
+
try {
|
|
271
|
+
await this.#ensureAccessToken();
|
|
272
|
+
const buf = await readFile(filePath);
|
|
273
|
+
const form = new FormData();
|
|
274
|
+
form.append('file', new Blob([buf]), basename(filePath));
|
|
275
|
+
form.append('file_type', fileType);
|
|
276
|
+
const url = `${this.#options.config.apiBaseUrl}/im/v1/files`;
|
|
277
|
+
const response = await this.#fetch(url, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: { Authorization: `Bearer ${this.#accessToken.token}` },
|
|
280
|
+
body: form,
|
|
281
|
+
});
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
if (data.code === 0) {
|
|
284
|
+
return data.data?.file_key || null;
|
|
285
|
+
}
|
|
286
|
+
throw new Error(`Upload failed: ${data.msg}`);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
this.#logger.error('Failed to upload file:', error);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async createChat(name, userIds, ownerId) {
|
|
294
|
+
try {
|
|
295
|
+
const data = await this.#request('/im/v1/chats', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
body: {
|
|
298
|
+
name,
|
|
299
|
+
user_id_list: userIds,
|
|
300
|
+
...(ownerId ? { owner_id: ownerId } : {}),
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
if (data.code === 0)
|
|
304
|
+
return data.data?.chat_id || null;
|
|
305
|
+
throw new Error(`Failed to create chat: ${data.msg}`);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
this.#logger.error('Failed to create chat:', error);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async updateChatInfo(chatId, options) {
|
|
313
|
+
try {
|
|
314
|
+
const data = await this.#request(`/im/v1/chats/${chatId}`, {
|
|
315
|
+
method: 'PUT',
|
|
316
|
+
body: options,
|
|
317
|
+
});
|
|
318
|
+
if (data.code === 0)
|
|
319
|
+
return true;
|
|
320
|
+
throw new Error(`Failed to update chat: ${data.msg}`);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
this.#logger.error('Failed to update chat:', error);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async addChatMembers(chatId, userIds) {
|
|
328
|
+
try {
|
|
329
|
+
const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
body: { id_list: userIds },
|
|
332
|
+
});
|
|
333
|
+
if (data.code === 0)
|
|
334
|
+
return true;
|
|
335
|
+
throw new Error(`Failed to add members: ${data.msg}`);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
this.#logger.error('Failed to add chat members:', error);
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async removeChatMembers(chatId, userIds) {
|
|
343
|
+
try {
|
|
344
|
+
const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
|
|
345
|
+
method: 'DELETE',
|
|
346
|
+
body: { id_list: userIds },
|
|
347
|
+
});
|
|
348
|
+
if (data.code === 0)
|
|
349
|
+
return true;
|
|
350
|
+
throw new Error(`Failed to remove members: ${data.msg}`);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
this.#logger.error('Failed to remove chat members:', error);
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async getChatMembers(chatId) {
|
|
358
|
+
try {
|
|
359
|
+
const data = await this.#request(`/im/v1/chats/${chatId}/members`, { method: 'GET' });
|
|
360
|
+
if (data.code === 0)
|
|
361
|
+
return data.data?.items || [];
|
|
362
|
+
throw new Error(`Failed to get members: ${data.msg}`);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
this.#logger.error('Failed to get chat members:', error);
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async dissolveChat(chatId) {
|
|
370
|
+
try {
|
|
371
|
+
const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'DELETE' });
|
|
372
|
+
if (data.code === 0)
|
|
373
|
+
return true;
|
|
374
|
+
throw new Error(`Failed to dissolve chat: ${data.msg}`);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
this.#logger.error('Failed to dissolve chat:', error);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async setChatManagers(chatId, userIds) {
|
|
382
|
+
try {
|
|
383
|
+
const data = await this.#request(`/im/v1/chats/${chatId}/managers/add_managers`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
body: { manager_ids: userIds },
|
|
386
|
+
});
|
|
387
|
+
if (data.code === 0)
|
|
388
|
+
return true;
|
|
389
|
+
throw new Error(`Failed to set managers: ${data.msg}`);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
this.#logger.error('Failed to set chat managers:', error);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async removeChatManagers(chatId, userIds) {
|
|
397
|
+
try {
|
|
398
|
+
const data = await this.#request(`/im/v1/chats/${chatId}/managers/delete_managers`, {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
body: { manager_ids: userIds },
|
|
401
|
+
});
|
|
402
|
+
if (data.code === 0)
|
|
403
|
+
return true;
|
|
404
|
+
throw new Error(`Failed to remove managers: ${data.msg}`);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
this.#logger.error('Failed to remove chat managers:', error);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** 拉取分页列表(items + has_more/page_token);code!==0 时抛错交由 RPC 层透出。 */
|
|
412
|
+
async #listAllPages(path, params = {}) {
|
|
413
|
+
const items = [];
|
|
414
|
+
let pageToken = '';
|
|
415
|
+
do {
|
|
416
|
+
const data = await this.#request(path, {
|
|
417
|
+
method: 'GET',
|
|
418
|
+
params: {
|
|
419
|
+
...params,
|
|
420
|
+
page_size: 100,
|
|
421
|
+
...(pageToken ? { page_token: pageToken } : {}),
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
if (data.code !== 0) {
|
|
425
|
+
throw new Error(`Lark API ${path} failed: ${data.msg ?? 'unknown'} (${data.code})`);
|
|
426
|
+
}
|
|
427
|
+
const page = data.data ?? {};
|
|
428
|
+
if (Array.isArray(page.items)) {
|
|
429
|
+
for (const item of page.items) {
|
|
430
|
+
if (item !== null && typeof item === 'object') {
|
|
431
|
+
items.push(item);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
pageToken = page.has_more === true && typeof page.page_token === 'string'
|
|
436
|
+
? page.page_token
|
|
437
|
+
: '';
|
|
438
|
+
} while (pageToken);
|
|
439
|
+
return items;
|
|
440
|
+
}
|
|
441
|
+
async #request(path, options = {}) {
|
|
442
|
+
await this.#ensureAccessToken();
|
|
443
|
+
const { method = 'GET', params = {}, body } = options;
|
|
444
|
+
const urlParams = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)])));
|
|
445
|
+
const query = urlParams.toString();
|
|
446
|
+
const url = `${this.#options.config.apiBaseUrl}${path}${query ? `?${query}` : ''}`;
|
|
447
|
+
const response = await this.#fetch(url, {
|
|
448
|
+
method,
|
|
449
|
+
headers: {
|
|
450
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
451
|
+
Authorization: `Bearer ${this.#accessToken.token}`,
|
|
452
|
+
},
|
|
453
|
+
body: body && method !== 'GET' ? JSON.stringify(body) : undefined,
|
|
454
|
+
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
455
|
+
});
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
const text = await response.text().catch(() => '');
|
|
458
|
+
throw new Error(`Lark API error ${response.status}: ${text}`);
|
|
459
|
+
}
|
|
460
|
+
return await response.json();
|
|
461
|
+
}
|
|
462
|
+
async #ensureAccessToken() {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
if (this.#accessToken.token
|
|
465
|
+
&& now < this.#accessToken.timestamp + (this.#accessToken.expires_in - 300) * 1000) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (this.#refreshPromise) {
|
|
469
|
+
await this.#refreshPromise;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.#refreshPromise = this.#refreshAccessToken()
|
|
473
|
+
.then(() => this.#accessToken.token)
|
|
474
|
+
.finally(() => { this.#refreshPromise = null; });
|
|
475
|
+
await this.#refreshPromise;
|
|
476
|
+
}
|
|
477
|
+
async #refreshAccessToken() {
|
|
478
|
+
const { appId, appSecret, apiBaseUrl } = this.#options.config;
|
|
479
|
+
const url = `${apiBaseUrl}/auth/v3/tenant_access_token/internal`;
|
|
480
|
+
const response = await this.#fetch(url, {
|
|
481
|
+
method: 'POST',
|
|
482
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
483
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
484
|
+
});
|
|
485
|
+
const data = await response.json();
|
|
486
|
+
if (data.code === 0 && data.tenant_access_token) {
|
|
487
|
+
this.#accessToken = {
|
|
488
|
+
token: data.tenant_access_token,
|
|
489
|
+
expires_in: data.expire ?? 7200,
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
};
|
|
492
|
+
this.#logger.debug('Access token refreshed successfully');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
throw new Error(`Failed to get access token: ${data.msg} (${data.code})`);
|
|
496
|
+
}
|
|
497
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
interface Adapters {
|
|
9
|
-
lark: LarkAdapter;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export * from "./types.js";
|
|
13
|
-
export { LarkBot } from "./bot.js";
|
|
14
|
-
export { LarkAdapter } from "./adapter.js";
|
|
15
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, type AccessToken, type LarkAdapterConfig, type LarkApiResponse, type LarkEventBody, type LarkMessage, type LarkSendBody, type LarkWireSegment, type ResolvedLarkConfig, } from './protocol.js';
|
|
2
|
+
export { larkClient, type LarkClientEventMap } from './client.js';
|
|
3
|
+
export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
|
|
4
|
+
export { LarkClient, LarkEndpoint, type LarkClientApi, type LarkEndpointOptions, type LarkFetch, } from './endpoint.js';
|
|
5
|
+
export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, } from './media-upload.js';
|
|
6
|
+
export { registerLarkWebhookRoutes, handleLarkWebhookRequest, type LarkWebhookHandler, } from './webhook.js';
|