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