@zhin.js/adapter-dingtalk 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 +56 -0
  2. package/README.md +58 -335
  3. package/adapters/dingtalk.ts +26 -0
  4. package/agent/tools/add_chat_members.ts +22 -0
  5. package/agent/tools/create_chat.ts +23 -0
  6. package/agent/tools/dept_info.ts +17 -0
  7. package/agent/tools/get_dept_users.ts +18 -0
  8. package/agent/tools/get_user.ts +17 -0
  9. package/agent/tools/list_departments.ts +18 -0
  10. package/agent/tools/send_work_notice.ts +20 -0
  11. package/agent/tools/update_chat.ts +27 -0
  12. package/lib/dingtalk-agent-deps.d.ts +26 -0
  13. package/lib/dingtalk-agent-deps.js +30 -0
  14. package/lib/endpoint.d.ts +45 -36
  15. package/lib/endpoint.js +199 -434
  16. package/lib/index.d.ts +5 -15
  17. package/lib/index.js +5 -219
  18. package/lib/platform-permit.d.ts +1 -2
  19. package/lib/platform-permit.js +4 -2
  20. package/lib/protocol.d.ts +121 -0
  21. package/lib/protocol.js +221 -0
  22. package/lib/webhook.d.ts +13 -0
  23. package/lib/webhook.js +48 -0
  24. package/package.json +51 -19
  25. package/plugin.ts +12 -0
  26. package/schema.json +23 -0
  27. package/src/dingtalk-agent-deps.ts +58 -0
  28. package/src/endpoint.ts +263 -479
  29. package/src/index.ts +46 -235
  30. package/src/platform-permit.ts +1 -2
  31. package/src/protocol.ts +338 -0
  32. package/src/webhook.ts +76 -0
  33. package/lib/adapter.d.ts +0 -19
  34. package/lib/adapter.d.ts.map +0 -1
  35. package/lib/adapter.js +0 -40
  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 -58
  48. package/lib/types.d.ts.map +0 -1
  49. package/lib/types.js +0 -5
  50. package/lib/types.js.map +0 -1
  51. package/plugin.yml +0 -3
  52. package/src/adapter.ts +0 -46
  53. package/src/segment-mapper.ts +0 -1
  54. package/src/types.ts +0 -56
  55. /package/{skills/dingtalk → agent}/PERMITS.md +0 -0
  56. /package/{skills/dingtalk/SKILL.md → agent/skills/dingtalk.md} +0 -0
package/lib/endpoint.js CHANGED
@@ -1,418 +1,141 @@
1
- /**
2
- * 钉钉 Endpoint 实现
3
- */
4
- import { formatCompact, Message, segment, expandInteractiveSegmentsInContent, } from 'zhin.js';
5
- import { registerFetchRoute } from "@zhin.js/host-router/router";
6
- import { createHmac } from "crypto";
7
- import { normalizeDingtalkSenderForPermit } from "./platform-permit.js";
8
- import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { registerDingtalkAgentEndpoint } from './dingtalk-agent-deps.js';
3
+ import { normalizeDingtalkSenderForPermit } from './platform-permit.js';
4
+ import { formatInboundContent, formatOutboundBody, generateMessageId, isDingtalkBotMentioned, resolveChatType, resolveSender, resolveTarget, } from './protocol.js';
5
+ import { registerDingTalkWebhookRoutes } from './webhook.js';
6
+ const logger = getLogger('dingtalk');
9
7
  export class DingTalkEndpoint {
10
- adapter;
11
- $config;
12
- $connected;
13
- router;
14
- accessToken;
15
- baseURL;
16
- sessionWebhooks = new Map();
17
- get $id() {
18
- return this.$config.name;
8
+ #options;
9
+ #fetch;
10
+ #routeReleases = [];
11
+ #accessToken = { token: '', expires_in: 0, timestamp: 0 };
12
+ #refreshPromise = null;
13
+ #sessionWebhooks = new Map();
14
+ #open = false;
15
+ #started = false;
16
+ #unregisterAgent;
17
+ constructor(options) {
18
+ this.#options = options;
19
+ this.#fetch = options.fetch ?? globalThis.fetch;
19
20
  }
20
- get logger() {
21
- return this.adapter.plugin.logger;
21
+ /** Used by webhook handler. */
22
+ get isOpen() {
23
+ return this.#open;
22
24
  }
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
- this.baseURL = $config.apiBaseUrl || "https://oapi.dingtalk.com";
30
- this.setupWebhookRoute();
25
+ get config() {
26
+ return this.#options.config;
31
27
  }
32
- async request(path, options = {}) {
33
- await this.ensureAccessToken();
34
- const { method = "GET", params = {}, body } = options;
35
- const urlParams = new URLSearchParams({
36
- ...params,
37
- access_token: this.accessToken.token,
38
- });
39
- const url = `${this.baseURL}${path}?${urlParams.toString()}`;
40
- const fetchOptions = {
41
- method,
42
- headers: {
43
- "Content-Type": "application/json; charset=utf-8",
44
- },
45
- };
46
- if (body && method === "POST") {
47
- fetchOptions.body = JSON.stringify(body);
48
- }
49
- const response = await fetch(url, fetchOptions);
50
- return await response.json();
51
- }
52
- setupWebhookRoute() {
53
- registerFetchRoute(this.router, "POST", this.$config.webhookPath, (ctx) => {
54
- void this.handleWebhook(ctx);
55
- });
56
- }
57
- async handleWebhook(ctx) {
28
+ async start() {
29
+ if (this.#started)
30
+ return;
31
+ this.#started = true;
58
32
  try {
59
- const body = ctx.request.body;
60
- const timestamp = ctx.get("timestamp");
61
- const sign = ctx.get("sign");
62
- if (timestamp && sign) {
63
- if (!this.verifySignature(timestamp, sign)) {
64
- this.logger.warn(formatCompact({ op: "webhook", ok: false, error: "invalid signature" }));
65
- ctx.status = 403;
66
- ctx.body = { code: -1, msg: "Forbidden" };
67
- return;
68
- }
69
- }
70
- const event = body;
71
- if (event.msgtype) {
72
- await this.handleEvent(event);
73
- }
74
- ctx.status = 200;
75
- ctx.body = { code: 0, msg: "success" };
33
+ await this.#refreshAccessToken();
34
+ this.#unregisterAgent = registerDingtalkAgentEndpoint(this.#options.config.name, this);
35
+ this.#routeReleases.push(...registerDingTalkWebhookRoutes(this.#options.http, this));
36
+ logger.debug(formatCompact({
37
+ endpoint: this.#options.config.name,
38
+ op: 'webhook',
39
+ path: this.#options.config.webhookPath,
40
+ }));
76
41
  }
77
42
  catch (error) {
78
- this.logger.error("Webhook error:", error);
79
- ctx.status = 500;
80
- ctx.body = { code: -1, msg: "Internal Server Error" };
43
+ await this.stop();
44
+ logger.error('Failed to connect DingTalk endpoint:', error);
45
+ throw error;
81
46
  }
82
47
  }
83
- verifySignature(timestamp, sign) {
84
- try {
85
- const stringToSign = `${timestamp}\n${this.$config.appSecret}`;
86
- const hmac = createHmac("sha256", this.$config.appSecret);
87
- hmac.update(stringToSign);
88
- const calculatedSign = hmac.digest("base64");
89
- return calculatedSign === sign;
90
- }
91
- catch (error) {
92
- this.logger.error("Signature verification error:", error);
93
- return false;
94
- }
48
+ open() {
49
+ this.#open = true;
95
50
  }
96
- async handleEvent(event) {
97
- if (event.sessionWebhook && event.conversationId) {
98
- this.sessionWebhooks.set(event.conversationId, event.sessionWebhook);
99
- }
100
- const message = this.$formatMessage(event);
101
- this.adapter.emit("message.receive", message);
102
- this.logger.debug(formatCompact({
103
- op: "recv",
104
- endpoint: this.$config.name,
105
- channel: message.$channel.type,
106
- id: message.$channel.id,
107
- len: segment.raw(message.$content).length,
108
- }));
51
+ close() {
52
+ this.#open = false;
109
53
  }
110
- async ensureAccessToken() {
111
- const now = Date.now();
112
- if (this.accessToken.token &&
113
- now <
114
- this.accessToken.timestamp +
115
- (this.accessToken.expires_in - 300) * 1000) {
116
- return;
117
- }
118
- await this.refreshAccessToken();
54
+ async stop() {
55
+ this.#open = false;
56
+ this.#sessionWebhooks.clear();
57
+ for (const release of this.#routeReleases.splice(0))
58
+ release();
59
+ this.#unregisterAgent?.();
60
+ this.#unregisterAgent = undefined;
61
+ this.#started = false;
62
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
119
63
  }
120
- async refreshAccessToken() {
121
- try {
122
- const baseURL = this.$config.apiBaseUrl || "https://oapi.dingtalk.com";
123
- const params = new URLSearchParams({
124
- appkey: this.$config.appKey,
125
- appsecret: this.$config.appSecret,
64
+ async send({ target, payload }) {
65
+ const content = formatOutboundBody(payload);
66
+ const sessionWebhook = this.#sessionWebhooks.get(target);
67
+ if (sessionWebhook) {
68
+ const response = await this.#fetch(sessionWebhook, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
71
+ body: JSON.stringify(content),
126
72
  });
127
- const url = `${baseURL}/gettoken?${params.toString()}`;
128
- const response = await fetch(url);
129
73
  const data = await response.json();
130
- if (data.errcode === 0) {
131
- this.accessToken = {
132
- token: data.access_token,
133
- expires_in: data.expires_in,
134
- timestamp: Date.now(),
135
- };
136
- this.logger.debug("Access token refreshed successfully");
137
- }
138
- else {
139
- throw new Error(`Failed to get access token: ${data.errmsg}`);
74
+ if (data.errcode !== 0) {
75
+ throw new Error(`Failed to send message via session webhook: ${data.errmsg}`);
140
76
  }
77
+ logger.debug(formatCompact({
78
+ op: 'send',
79
+ endpoint: this.#options.config.name,
80
+ via: 'sessionWebhook',
81
+ to: target,
82
+ }));
83
+ return data.msgId || `${Date.now()}`;
84
+ }
85
+ const body = {
86
+ ...content,
87
+ ...(this.#options.config.robotCode
88
+ ? { robotCode: this.#options.config.robotCode }
89
+ : {}),
90
+ };
91
+ const data = await this.#request('/robot/send', {
92
+ method: 'POST',
93
+ body: body,
94
+ });
95
+ if (data.errcode !== 0) {
96
+ throw new Error(`Failed to send message: ${data.errmsg}`);
141
97
  }
142
- catch (error) {
143
- this.logger.error("Failed to refresh access token:", error);
144
- throw error;
145
- }
98
+ logger.debug(formatCompact({ op: 'send', endpoint: this.#options.config.name, to: target }));
99
+ return data.msgId || `${Date.now()}`;
146
100
  }
147
- $formatMessage(msg) {
148
- const wire = this.parseMessageContent(msg);
149
- const content = toCanonicalSegments(wire);
150
- const chatType = msg.conversationType === "2" ? "group" : "private";
151
- const permit = normalizeDingtalkSenderForPermit({ isAdmin: msg.isAdmin === true });
152
- return Message.from(msg, {
153
- $id: msg.msgId || Date.now().toString(),
154
- $adapter: "dingtalk",
155
- $endpoint: this.$config.name,
156
- $sender: {
157
- id: msg.senderId || msg.senderStaffId || "unknown",
158
- name: msg.senderNick || msg.senderId || "Unknown User",
101
+ /** Test / internal: admit a parsed event when open (non-webhook path). */
102
+ admit(event) {
103
+ if (!this.#open)
104
+ return;
105
+ if (event.sessionWebhook && event.conversationId) {
106
+ this.#sessionWebhooks.set(event.conversationId, event.sessionWebhook);
107
+ }
108
+ const target = resolveTarget(event);
109
+ const chatType = resolveChatType(event.conversationType);
110
+ const permit = normalizeDingtalkSenderForPermit({ isAdmin: event.isAdmin === true });
111
+ void this.#options.gateway.receive({
112
+ adapter: this.#options.id,
113
+ target,
114
+ content: formatInboundContent(event),
115
+ sender: resolveSender(event),
116
+ id: generateMessageId(event),
117
+ metadata: Object.freeze({
118
+ msgtype: event.msgtype,
119
+ chatType,
120
+ endpoint: this.#options.config.name,
121
+ senderNick: event.senderNick,
159
122
  role: permit.role,
160
123
  permissions: permit.permissions,
161
- },
162
- $channel: {
163
- id: msg.conversationId || "unknown",
164
- type: chatType,
165
- },
166
- $content: content,
167
- $raw: JSON.stringify(msg),
168
- $timestamp: msg.createAt || Date.now(),
169
- $recall: async () => {
170
- await this.$recallMessage(msg.msgId || "");
171
- },
172
- $reply: async (content) => {
173
- return await this.adapter.sendMessage({
174
- context: "dingtalk",
175
- endpoint: this.$config.name,
176
- id: msg.conversationId || msg.senderId || "unknown",
177
- type: chatType,
178
- content: content,
179
- });
180
- },
124
+ conversationType: event.conversationType,
125
+ ...(isDingtalkBotMentioned(event, this.#options.config.robotCode) ? { mentioned: true } : {}),
126
+ }),
127
+ }).catch((err) => {
128
+ logger.warn(formatCompact({
129
+ op: 'dingtalk_gateway_receive_failed',
130
+ target,
131
+ error: err instanceof Error ? err.message : String(err),
132
+ }));
181
133
  });
182
134
  }
183
- parseMessageContent(msg) {
184
- const content = [];
185
- if (!msg.msgtype)
186
- return content;
187
- try {
188
- switch (msg.msgtype) {
189
- case "text":
190
- if (msg.text?.content) {
191
- content.push(segment("text", { content: msg.text.content }));
192
- if (msg.atUsers && msg.atUsers.length > 0) {
193
- for (const atUser of msg.atUsers) {
194
- content.push(segment("at", {
195
- id: atUser.dingtalkId || atUser.staffId,
196
- name: atUser.dingtalkId || atUser.staffId,
197
- }));
198
- }
199
- }
200
- }
201
- break;
202
- case "picture":
203
- if (msg.content) {
204
- content.push(segment("image", {
205
- url: msg.content.downloadCode ||
206
- msg.content.pictureDownloadCode,
207
- file: msg.content.downloadCode ||
208
- msg.content.pictureDownloadCode,
209
- }));
210
- }
211
- break;
212
- case "file":
213
- if (msg.content) {
214
- content.push(segment("file", {
215
- file: msg.content.downloadCode,
216
- name: msg.content.fileName,
217
- size: msg.content.fileSize,
218
- }));
219
- }
220
- break;
221
- case "audio":
222
- if (msg.content) {
223
- content.push(segment("audio", {
224
- file: msg.content.downloadCode,
225
- duration: msg.content.duration,
226
- }));
227
- }
228
- break;
229
- case "video":
230
- if (msg.content) {
231
- content.push(segment("video", {
232
- file: msg.content.downloadCode,
233
- duration: msg.content.duration,
234
- size: msg.content.videoSize,
235
- }));
236
- }
237
- break;
238
- case "richText":
239
- if (msg.content?.richText) {
240
- for (const item of msg.content.richText) {
241
- if (item.text) {
242
- content.push(segment("text", { content: item.text }));
243
- }
244
- }
245
- }
246
- break;
247
- case "markdown":
248
- if (msg.content?.text) {
249
- content.push(segment("markdown", {
250
- content: msg.content.text,
251
- title: msg.content.title,
252
- }));
253
- }
254
- break;
255
- default:
256
- content.push(segment("text", {
257
- content: `[不支持的消息类型: ${msg.msgtype}]`,
258
- }));
259
- break;
260
- }
261
- }
262
- catch (error) {
263
- this.logger.error("Failed to parse message content:", error);
264
- content.push(segment("text", { content: "[消息解析失败]" }));
265
- }
266
- return content;
267
- }
268
- async $sendMessage(options) {
269
- const conversationId = options.id;
270
- const canonical = expandInteractiveSegmentsInContent(options.content);
271
- const wire = fromCanonicalSegments(canonical);
272
- const content = this.formatSendContent(wire);
273
- try {
274
- const sessionWebhook = this.sessionWebhooks.get(conversationId);
275
- if (sessionWebhook) {
276
- const response = await fetch(sessionWebhook, {
277
- method: "POST",
278
- headers: {
279
- "Content-Type": "application/json; charset=utf-8",
280
- },
281
- body: JSON.stringify(content),
282
- });
283
- const data = await response.json();
284
- if (data.errcode !== 0) {
285
- throw new Error(`Failed to send message via session webhook: ${data.errmsg}`);
286
- }
287
- this.logger.debug("Message sent via session webhook");
288
- return data.msgId || Date.now().toString();
289
- }
290
- const data = await this.request("/robot/send", {
291
- method: "POST",
292
- body: {
293
- ...content,
294
- robotCode: this.$config.robotCode,
295
- },
296
- });
297
- if (data.errcode !== 0) {
298
- throw new Error(`Failed to send message: ${data.errmsg}`);
299
- }
300
- this.logger.debug("Message sent successfully");
301
- return data.msgId || Date.now().toString();
302
- }
303
- catch (error) {
304
- this.logger.error("Failed to send message:", error);
305
- throw error;
306
- }
307
- }
308
- async $recallMessage(id) {
309
- this.logger.warn(formatCompact({ op: "recall", ok: false, error: "not supported" }));
310
- }
311
- formatSendContent(content) {
312
- if (typeof content === "string") {
313
- return { msgtype: "text", text: { content } };
314
- }
315
- if (Array.isArray(content)) {
316
- const textParts = [];
317
- const atUserIds = [];
318
- let hasMedia = false;
319
- let mediaContent = null;
320
- for (const item of content) {
321
- if (typeof item === "string") {
322
- textParts.push(item);
323
- }
324
- else {
325
- const seg = item;
326
- switch (seg.type) {
327
- case "text":
328
- textParts.push(seg.data.content || seg.data.text || "");
329
- break;
330
- case "at":
331
- const userId = seg.data.id || seg.data.userId;
332
- if (userId) {
333
- atUserIds.push(userId);
334
- textParts.push(`@${seg.data.name || userId} `);
335
- }
336
- break;
337
- case "image":
338
- if (!hasMedia) {
339
- hasMedia = true;
340
- mediaContent = {
341
- msgtype: "picture",
342
- picture: {
343
- picURL: seg.data.url || seg.data.file,
344
- },
345
- };
346
- }
347
- break;
348
- case "markdown":
349
- if (!hasMedia) {
350
- hasMedia = true;
351
- mediaContent = {
352
- msgtype: "markdown",
353
- markdown: {
354
- title: seg.data.title || "消息",
355
- text: seg.data.content || seg.data.text,
356
- },
357
- };
358
- }
359
- break;
360
- case "link":
361
- if (!hasMedia) {
362
- hasMedia = true;
363
- mediaContent = {
364
- msgtype: "link",
365
- link: {
366
- title: seg.data.title || "链接",
367
- text: seg.data.text || seg.data.content || "",
368
- messageUrl: seg.data.url,
369
- picUrl: seg.data.picUrl,
370
- },
371
- };
372
- }
373
- break;
374
- }
375
- }
376
- }
377
- if (hasMedia && mediaContent)
378
- return mediaContent;
379
- const result = {
380
- msgtype: "text",
381
- text: { content: textParts.join("") },
382
- };
383
- if (atUserIds.length > 0) {
384
- result.at = { atUserIds, isAtAll: false };
385
- }
386
- return result;
387
- }
388
- return { msgtype: "text", text: { content: String(content) } };
389
- }
390
- async $connect() {
391
- try {
392
- await this.refreshAccessToken();
393
- this.$connected = true;
394
- this.logger.info(formatCompact({ endpoint: this.$config.name }));
395
- this.logger.info(formatCompact({ op: "webhook", path: this.$config.webhookPath }));
396
- }
397
- catch (error) {
398
- this.logger.error("Failed to connect DingTalk bot:", error);
399
- throw error;
400
- }
401
- }
402
- async $disconnect() {
403
- try {
404
- this.sessionWebhooks.clear();
405
- this.$connected = false;
406
- this.logger.info(formatCompact({ op: "disconnect", endpoint: this.$config.name }));
407
- }
408
- catch (error) {
409
- this.logger.error("Error disconnecting DingTalk bot:", error);
410
- }
411
- }
412
135
  async getUserInfo(userId) {
413
136
  try {
414
- const data = await this.request("/topapi/v2/user/get", {
415
- method: "POST",
137
+ const data = await this.#request('/topapi/v2/user/get', {
138
+ method: 'POST',
416
139
  body: { userid: userId },
417
140
  });
418
141
  if (data.errcode === 0)
@@ -420,50 +143,50 @@ export class DingTalkEndpoint {
420
143
  throw new Error(`Failed to get user info: ${data.errmsg}`);
421
144
  }
422
145
  catch (error) {
423
- this.logger.error("Failed to get user info:", error);
146
+ logger.error('Failed to get user info:', error);
424
147
  return null;
425
148
  }
426
149
  }
427
150
  async getDepartmentUsers(deptId) {
428
151
  try {
429
- const data = await this.request("/topapi/user/listid", {
430
- method: "POST",
152
+ const data = await this.#request('/topapi/user/listid', {
153
+ method: 'POST',
431
154
  body: { dept_id: deptId },
432
155
  });
433
- if (data.errcode === 0)
434
- return data.result.userid_list || [];
156
+ if (data.errcode === 0) {
157
+ const result = data.result;
158
+ return result?.userid_list || [];
159
+ }
435
160
  throw new Error(`Failed to get department users: ${data.errmsg}`);
436
161
  }
437
162
  catch (error) {
438
- this.logger.error("Failed to get department users:", error);
163
+ logger.error('Failed to get department users:', error);
439
164
  return [];
440
165
  }
441
166
  }
442
167
  async sendWorkNotice(userIdList, content) {
443
168
  try {
444
- const data = await this.request("/topapi/message/corpconversation/asyncsend_v2", {
445
- method: "POST",
169
+ const data = await this.#request('/topapi/message/corpconversation/asyncsend_v2', {
170
+ method: 'POST',
446
171
  body: {
447
- agent_id: this.$config.robotCode,
448
- userid_list: userIdList.join(","),
172
+ agent_id: this.#options.config.robotCode,
173
+ userid_list: userIdList.join(','),
449
174
  msg: content,
450
175
  },
451
176
  });
452
- if (data.errcode === 0) {
453
- this.logger.debug("Work notice sent successfully");
177
+ if (data.errcode === 0)
454
178
  return true;
455
- }
456
179
  throw new Error(`Failed to send work notice: ${data.errmsg}`);
457
180
  }
458
181
  catch (error) {
459
- this.logger.error("Failed to send work notice:", error);
182
+ logger.error('Failed to send work notice:', error);
460
183
  return false;
461
184
  }
462
185
  }
463
186
  async getDepartmentList(deptId = 1) {
464
187
  try {
465
- const data = await this.request("/topapi/v2/department/listsub", {
466
- method: "POST",
188
+ const data = await this.#request('/topapi/v2/department/listsub', {
189
+ method: 'POST',
467
190
  body: { dept_id: deptId },
468
191
  });
469
192
  if (data.errcode === 0)
@@ -471,14 +194,14 @@ export class DingTalkEndpoint {
471
194
  throw new Error(`Failed to get department list: ${data.errmsg}`);
472
195
  }
473
196
  catch (error) {
474
- this.logger.error("Failed to get department list:", error);
197
+ logger.error('Failed to get department list:', error);
475
198
  return [];
476
199
  }
477
200
  }
478
201
  async getDepartmentInfo(deptId) {
479
202
  try {
480
- const data = await this.request("/topapi/v2/department/get", {
481
- method: "POST",
203
+ const data = await this.#request('/topapi/v2/department/get', {
204
+ method: 'POST',
482
205
  body: { dept_id: deptId },
483
206
  });
484
207
  if (data.errcode === 0)
@@ -486,35 +209,29 @@ export class DingTalkEndpoint {
486
209
  throw new Error(`Failed to get department info: ${data.errmsg}`);
487
210
  }
488
211
  catch (error) {
489
- this.logger.error("Failed to get department info:", error);
212
+ logger.error('Failed to get department info:', error);
490
213
  return null;
491
214
  }
492
215
  }
493
216
  async createChat(name, ownerUserId, userIdList) {
494
217
  try {
495
- const data = await this.request("/topapi/chat/create", {
496
- method: "POST",
497
- body: {
498
- name,
499
- owner: ownerUserId,
500
- useridlist: userIdList,
501
- },
218
+ const data = await this.#request('/topapi/chat/create', {
219
+ method: 'POST',
220
+ body: { name, owner: ownerUserId, useridlist: userIdList },
502
221
  });
503
- if (data.errcode === 0) {
504
- this.logger.debug(formatCompact({ op: "create_chat", chat: data.chatid }));
505
- return data.chatid;
506
- }
222
+ if (data.errcode === 0)
223
+ return data.chatid || null;
507
224
  throw new Error(`Failed to create chat: ${data.errmsg}`);
508
225
  }
509
226
  catch (error) {
510
- this.logger.error("Failed to create chat:", error);
227
+ logger.error('Failed to create chat:', error);
511
228
  return null;
512
229
  }
513
230
  }
514
231
  async getChatInfo(chatId) {
515
232
  try {
516
- const data = await this.request("/topapi/chat/get", {
517
- method: "POST",
233
+ const data = await this.#request('/topapi/chat/get', {
234
+ method: 'POST',
518
235
  body: { chatid: chatId },
519
236
  });
520
237
  if (data.errcode === 0)
@@ -522,26 +239,74 @@ export class DingTalkEndpoint {
522
239
  throw new Error(`Failed to get chat info: ${data.errmsg}`);
523
240
  }
524
241
  catch (error) {
525
- this.logger.error("Failed to get chat info:", error);
242
+ logger.error('Failed to get chat info:', error);
526
243
  return null;
527
244
  }
528
245
  }
529
246
  async updateChat(chatId, options) {
530
247
  try {
531
- const data = await this.request("/topapi/chat/update", {
532
- method: "POST",
248
+ const data = await this.#request('/topapi/chat/update', {
249
+ method: 'POST',
533
250
  body: { chatid: chatId, ...options },
534
251
  });
535
- if (data.errcode === 0) {
536
- this.logger.debug(formatCompact({ op: "update_chat", chat: chatId }));
252
+ if (data.errcode === 0)
537
253
  return true;
538
- }
539
254
  throw new Error(`Failed to update chat: ${data.errmsg}`);
540
255
  }
541
256
  catch (error) {
542
- this.logger.error("Failed to update chat:", error);
257
+ logger.error('Failed to update chat:', error);
543
258
  return false;
544
259
  }
545
260
  }
261
+ async #request(path, options = {}) {
262
+ await this.#ensureAccessToken();
263
+ const { method = 'GET', params = {}, body } = options;
264
+ const urlParams = new URLSearchParams({
265
+ ...Object.fromEntries(Object.entries(params).map(([key, value]) => [key, String(value)])),
266
+ access_token: this.#accessToken.token,
267
+ });
268
+ const url = `${this.#options.config.apiBaseUrl}${path}?${urlParams.toString()}`;
269
+ const response = await this.#fetch(url, {
270
+ method,
271
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
272
+ body: body && method === 'POST' ? JSON.stringify(body) : undefined,
273
+ });
274
+ if (!response.ok) {
275
+ const text = await response.text().catch(() => '');
276
+ throw new Error(`DingTalk API error ${response.status}: ${text}`);
277
+ }
278
+ return await response.json();
279
+ }
280
+ async #ensureAccessToken() {
281
+ const now = Date.now();
282
+ if (this.#accessToken.token
283
+ && now < this.#accessToken.timestamp + (this.#accessToken.expires_in - 300) * 1000) {
284
+ return;
285
+ }
286
+ if (this.#refreshPromise) {
287
+ await this.#refreshPromise;
288
+ return;
289
+ }
290
+ this.#refreshPromise = this.#refreshAccessToken()
291
+ .then(() => this.#accessToken.token)
292
+ .finally(() => { this.#refreshPromise = null; });
293
+ await this.#refreshPromise;
294
+ }
295
+ async #refreshAccessToken() {
296
+ const { appKey, appSecret, apiBaseUrl } = this.#options.config;
297
+ const params = new URLSearchParams({ appkey: appKey, appsecret: appSecret });
298
+ const url = `${apiBaseUrl}/gettoken?${params.toString()}`;
299
+ const response = await this.#fetch(url);
300
+ const data = await response.json();
301
+ if (data.errcode === 0 && data.access_token) {
302
+ this.#accessToken = {
303
+ token: data.access_token,
304
+ expires_in: data.expires_in ?? 7200,
305
+ timestamp: Date.now(),
306
+ };
307
+ logger.debug('Access token refreshed successfully');
308
+ return;
309
+ }
310
+ throw new Error(`Failed to get access token: ${data.errmsg} (${data.errcode})`);
311
+ }
546
312
  }
547
- //# sourceMappingURL=endpoint.js.map