@zhin.js/adapter-dingtalk 1.0.38 → 1.0.40

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/lib/index.js CHANGED
@@ -1,874 +1,14 @@
1
- import { usePlugin, Adapter, Message, segment, createGroupManagementTools, GROUP_MANAGEMENT_SKILL_KEYWORDS, GROUP_MANAGEMENT_SKILL_TAGS, } from "zhin.js";
2
- import { createHmac } from 'crypto';
1
+ /**
2
+ * 钉钉适配器入口:类型扩展、导出、注册
3
+ */
4
+ import { usePlugin } from "zhin.js";
5
+ import { DingTalkAdapter } from "./adapter.js";
6
+ export * from "./types.js";
7
+ export { DingTalkBot } from "./bot.js";
8
+ export { DingTalkAdapter } from "./adapter.js";
3
9
  const plugin = usePlugin();
4
10
  const { provide, useContext } = plugin;
5
- // ================================================================================================
6
- // DingTalkBot 类
7
- // ================================================================================================
8
- export class DingTalkBot {
9
- adapter;
10
- $config;
11
- $connected;
12
- router;
13
- accessToken;
14
- baseURL;
15
- sessionWebhooks = new Map(); // conversationId -> webhook
16
- get $id() {
17
- return this.$config.name;
18
- }
19
- constructor(adapter, router, $config) {
20
- this.adapter = adapter;
21
- this.$config = $config;
22
- this.router = router;
23
- this.$connected = false;
24
- this.accessToken = { token: '', expires_in: 0, timestamp: 0 };
25
- // 设置 API 基础 URL
26
- this.baseURL = $config.apiBaseUrl || 'https://oapi.dingtalk.com';
27
- // 设置 webhook 路由
28
- this.setupWebhookRoute();
29
- }
30
- // 封装 fetch 请求方法
31
- async request(path, options = {}) {
32
- await this.ensureAccessToken();
33
- const { method = 'GET', params = {}, body } = options;
34
- // 添加 access_token 到查询参数
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
- this.router.post(this.$config.webhookPath, (ctx) => {
54
- this.handleWebhook(ctx);
55
- });
56
- }
57
- async handleWebhook(ctx) {
58
- try {
59
- const body = ctx.request.body;
60
- const headers = ctx.request.headers;
61
- // 钉钉签名验证
62
- const timestamp = headers['timestamp'];
63
- const sign = headers['sign'];
64
- if (timestamp && sign) {
65
- if (!this.verifySignature(timestamp, sign)) {
66
- plugin.logger.warn('Invalid signature in webhook');
67
- ctx.status = 403;
68
- ctx.body = { code: -1, msg: 'Forbidden' };
69
- return;
70
- }
71
- }
72
- const event = body;
73
- // 处理消息事件
74
- if (event.msgtype) {
75
- await this.handleEvent(event);
76
- }
77
- ctx.status = 200;
78
- ctx.body = { code: 0, msg: 'success' };
79
- }
80
- catch (error) {
81
- plugin.logger.error('Webhook error:', error);
82
- ctx.status = 500;
83
- ctx.body = { code: -1, msg: 'Internal Server Error' };
84
- }
85
- }
86
- verifySignature(timestamp, sign) {
87
- try {
88
- const stringToSign = `${timestamp}\n${this.$config.appSecret}`;
89
- const hmac = createHmac('sha256', this.$config.appSecret);
90
- hmac.update(stringToSign);
91
- const calculatedSign = hmac.digest('base64');
92
- return calculatedSign === sign;
93
- }
94
- catch (error) {
95
- plugin.logger.error('Signature verification error:', error);
96
- return false;
97
- }
98
- }
99
- async handleEvent(event) {
100
- // 存储会话 webhook(用于回复消息)
101
- if (event.sessionWebhook && event.conversationId) {
102
- this.sessionWebhooks.set(event.conversationId, event.sessionWebhook);
103
- }
104
- // 处理消息事件
105
- const message = this.$formatMessage(event);
106
- this.adapter.emit('message.receive', message);
107
- plugin.logger.info(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
108
- }
109
- // ================================================================================================
110
- // Token 管理
111
- // ================================================================================================
112
- async ensureAccessToken() {
113
- const now = Date.now();
114
- // 提前 5 分钟刷新 token
115
- if (this.accessToken.token && now < (this.accessToken.timestamp + (this.accessToken.expires_in - 300) * 1000)) {
116
- return;
117
- }
118
- await this.refreshAccessToken();
119
- }
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
126
- });
127
- const url = `${baseURL}/gettoken?${params.toString()}`;
128
- const response = await fetch(url);
129
- 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
- plugin.logger.debug('Access token refreshed successfully');
137
- }
138
- else {
139
- throw new Error(`Failed to get access token: ${data.errmsg}`);
140
- }
141
- }
142
- catch (error) {
143
- plugin.logger.error('Failed to refresh access token:', error);
144
- throw error;
145
- }
146
- }
147
- // ================================================================================================
148
- // 消息格式化
149
- // ================================================================================================
150
- $formatMessage(msg) {
151
- const content = this.parseMessageContent(msg);
152
- // 确定聊天类型: '1'为单聊, '2'为群聊
153
- const chatType = msg.conversationType === '2' ? 'group' : 'private';
154
- return Message.from(msg, {
155
- $id: msg.msgId || Date.now().toString(),
156
- $adapter: 'dingtalk',
157
- $bot: this.$config.name,
158
- $sender: {
159
- id: msg.senderId || msg.senderStaffId || 'unknown',
160
- name: msg.senderNick || msg.senderId || 'Unknown User'
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
- bot: this.$config.name,
176
- id: msg.conversationId || msg.senderId || 'unknown',
177
- type: chatType,
178
- content: content
179
- });
180
- }
181
- });
182
- }
183
- parseMessageContent(msg) {
184
- const content = [];
185
- if (!msg.msgtype) {
186
- return content;
187
- }
188
- try {
189
- switch (msg.msgtype) {
190
- case 'text':
191
- if (msg.text?.content) {
192
- content.push(segment('text', { content: msg.text.content }));
193
- // 处理 @提及
194
- if (msg.atUsers && msg.atUsers.length > 0) {
195
- for (const atUser of msg.atUsers) {
196
- content.push(segment('at', {
197
- id: atUser.dingtalkId || atUser.staffId,
198
- name: atUser.dingtalkId || atUser.staffId
199
- }));
200
- }
201
- }
202
- }
203
- break;
204
- case 'picture':
205
- if (msg.content) {
206
- content.push(segment('image', {
207
- url: msg.content.downloadCode || msg.content.pictureDownloadCode,
208
- file: msg.content.downloadCode || 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
- // 富文本消息处理(简化)
240
- if (msg.content?.richText) {
241
- for (const item of msg.content.richText) {
242
- if (item.text) {
243
- content.push(segment('text', { content: item.text }));
244
- }
245
- }
246
- }
247
- break;
248
- case 'markdown':
249
- if (msg.content?.text) {
250
- content.push(segment('markdown', {
251
- content: msg.content.text,
252
- title: msg.content.title
253
- }));
254
- }
255
- break;
256
- default:
257
- content.push(segment('text', { content: `[不支持的消息类型: ${msg.msgtype}]` }));
258
- break;
259
- }
260
- }
261
- catch (error) {
262
- plugin.logger.error('Failed to parse message content:', error);
263
- content.push(segment('text', { content: '[消息解析失败]' }));
264
- }
265
- return content;
266
- }
267
- // ================================================================================================
268
- // 消息发送
269
- // ================================================================================================
270
- async $sendMessage(options) {
271
- const conversationId = options.id;
272
- const content = this.formatSendContent(options.content);
273
- try {
274
- // 优先使用会话 webhook 发送消息(更快,更准确)
275
- const sessionWebhook = this.sessionWebhooks.get(conversationId);
276
- if (sessionWebhook) {
277
- const response = await fetch(sessionWebhook, {
278
- method: 'POST',
279
- headers: {
280
- 'Content-Type': 'application/json; charset=utf-8'
281
- },
282
- body: JSON.stringify(content)
283
- });
284
- const data = await response.json();
285
- if (data.errcode !== 0) {
286
- throw new Error(`Failed to send message via session webhook: ${data.errmsg}`);
287
- }
288
- plugin.logger.debug('Message sent via session webhook');
289
- return data.msgId || Date.now().toString();
290
- }
291
- // 否则使用普通机器人发送接口
292
- const data = await this.request('/robot/send', {
293
- method: 'POST',
294
- body: {
295
- ...content,
296
- robotCode: this.$config.robotCode
297
- }
298
- });
299
- if (data.errcode !== 0) {
300
- throw new Error(`Failed to send message: ${data.errmsg}`);
301
- }
302
- plugin.logger.debug('Message sent successfully');
303
- return data.msgId || Date.now().toString();
304
- }
305
- catch (error) {
306
- plugin.logger.error('Failed to send message:', error);
307
- throw error;
308
- }
309
- }
310
- async $recallMessage(id) {
311
- // 钉钉机器人不支持撤回消息
312
- plugin.logger.warn('DingTalk robot does not support message recall');
313
- }
314
- formatSendContent(content) {
315
- if (typeof content === 'string') {
316
- return {
317
- msgtype: 'text',
318
- text: { content }
319
- };
320
- }
321
- if (Array.isArray(content)) {
322
- const textParts = [];
323
- const atMobiles = [];
324
- const atUserIds = [];
325
- let hasMedia = false;
326
- let mediaContent = null;
327
- for (const item of content) {
328
- if (typeof item === 'string') {
329
- textParts.push(item);
330
- }
331
- else {
332
- const segment = item;
333
- switch (segment.type) {
334
- case 'text':
335
- textParts.push(segment.data.content || segment.data.text || '');
336
- break;
337
- case 'at':
338
- const userId = segment.data.id || segment.data.userId;
339
- if (userId) {
340
- atUserIds.push(userId);
341
- textParts.push(`@${segment.data.name || userId} `);
342
- }
343
- break;
344
- case 'image':
345
- if (!hasMedia) {
346
- hasMedia = true;
347
- mediaContent = {
348
- msgtype: 'picture',
349
- picture: {
350
- picURL: segment.data.url || segment.data.file
351
- }
352
- };
353
- }
354
- break;
355
- case 'markdown':
356
- if (!hasMedia) {
357
- hasMedia = true;
358
- mediaContent = {
359
- msgtype: 'markdown',
360
- markdown: {
361
- title: segment.data.title || '消息',
362
- text: segment.data.content || segment.data.text
363
- }
364
- };
365
- }
366
- break;
367
- case 'link':
368
- if (!hasMedia) {
369
- hasMedia = true;
370
- mediaContent = {
371
- msgtype: 'link',
372
- link: {
373
- title: segment.data.title || '链接',
374
- text: segment.data.text || segment.data.content || '',
375
- messageUrl: segment.data.url,
376
- picUrl: segment.data.picUrl
377
- }
378
- };
379
- }
380
- break;
381
- }
382
- }
383
- }
384
- // 优先发送媒体内容
385
- if (hasMedia && mediaContent) {
386
- return mediaContent;
387
- }
388
- // 否则发送文本内容
389
- const result = {
390
- msgtype: 'text',
391
- text: {
392
- content: textParts.join('')
393
- }
394
- };
395
- // 添加 @ 信息
396
- if (atUserIds.length > 0) {
397
- result.at = {
398
- atUserIds,
399
- isAtAll: false
400
- };
401
- }
402
- return result;
403
- }
404
- return {
405
- msgtype: 'text',
406
- text: {
407
- content: String(content)
408
- }
409
- };
410
- }
411
- // ================================================================================================
412
- // Bot 生命周期
413
- // ================================================================================================
414
- async $connect() {
415
- try {
416
- // 获取 access token
417
- await this.refreshAccessToken();
418
- this.$connected = true;
419
- plugin.logger.info(`DingTalk bot connected: ${this.$config.name}`);
420
- plugin.logger.info(`Webhook URL: ${this.$config.webhookPath}`);
421
- }
422
- catch (error) {
423
- plugin.logger.error('Failed to connect DingTalk bot:', error);
424
- throw error;
425
- }
426
- }
427
- async $disconnect() {
428
- try {
429
- this.$connected = false;
430
- plugin.logger.info('DingTalk bot disconnected');
431
- }
432
- catch (error) {
433
- plugin.logger.error('Error disconnecting DingTalk bot:', error);
434
- }
435
- }
436
- // ================================================================================================
437
- // 工具方法
438
- // ================================================================================================
439
- // 获取用户信息
440
- async getUserInfo(userId) {
441
- try {
442
- const data = await this.request('/topapi/v2/user/get', {
443
- method: 'POST',
444
- body: {
445
- userid: userId
446
- }
447
- });
448
- if (data.errcode === 0) {
449
- return data.result;
450
- }
451
- throw new Error(`Failed to get user info: ${data.errmsg}`);
452
- }
453
- catch (error) {
454
- plugin.logger.error('Failed to get user info:', error);
455
- return null;
456
- }
457
- }
458
- // 获取部门用户列表
459
- async getDepartmentUsers(deptId) {
460
- try {
461
- const data = await this.request('/topapi/user/listid', {
462
- method: 'POST',
463
- body: {
464
- dept_id: deptId
465
- }
466
- });
467
- if (data.errcode === 0) {
468
- return data.result.userid_list || [];
469
- }
470
- throw new Error(`Failed to get department users: ${data.errmsg}`);
471
- }
472
- catch (error) {
473
- plugin.logger.error('Failed to get department users:', error);
474
- return [];
475
- }
476
- }
477
- // 发送工作通知
478
- async sendWorkNotice(userIdList, content) {
479
- try {
480
- const data = await this.request('/topapi/message/corpconversation/asyncsend_v2', {
481
- method: 'POST',
482
- body: {
483
- agent_id: this.$config.robotCode,
484
- userid_list: userIdList.join(','),
485
- msg: content
486
- }
487
- });
488
- if (data.errcode === 0) {
489
- plugin.logger.debug('Work notice sent successfully');
490
- return true;
491
- }
492
- throw new Error(`Failed to send work notice: ${data.errmsg}`);
493
- }
494
- catch (error) {
495
- plugin.logger.error('Failed to send work notice:', error);
496
- return false;
497
- }
498
- }
499
- // ==================== 企业管理 API ====================
500
- /**
501
- * 获取部门列表
502
- * @param deptId 父部门 ID,默认为根部门 1
503
- */
504
- async getDepartmentList(deptId = 1) {
505
- try {
506
- const data = await this.request('/topapi/v2/department/listsub', {
507
- method: 'POST',
508
- body: { dept_id: deptId }
509
- });
510
- if (data.errcode === 0) {
511
- return data.result || [];
512
- }
513
- throw new Error(`Failed to get department list: ${data.errmsg}`);
514
- }
515
- catch (error) {
516
- plugin.logger.error('Failed to get department list:', error);
517
- return [];
518
- }
519
- }
520
- /**
521
- * 获取部门详情
522
- * @param deptId 部门 ID
523
- */
524
- async getDepartmentInfo(deptId) {
525
- try {
526
- const data = await this.request('/topapi/v2/department/get', {
527
- method: 'POST',
528
- body: { dept_id: deptId }
529
- });
530
- if (data.errcode === 0) {
531
- return data.result;
532
- }
533
- throw new Error(`Failed to get department info: ${data.errmsg}`);
534
- }
535
- catch (error) {
536
- plugin.logger.error('Failed to get department info:', error);
537
- return null;
538
- }
539
- }
540
- /**
541
- * 创建群聊
542
- * @param name 群名
543
- * @param ownerUserId 群主用户 ID
544
- * @param userIdList 成员用户 ID 列表
545
- */
546
- async createChat(name, ownerUserId, userIdList) {
547
- try {
548
- const data = await this.request('/topapi/chat/create', {
549
- method: 'POST',
550
- body: {
551
- name,
552
- owner: ownerUserId,
553
- useridlist: userIdList
554
- }
555
- });
556
- if (data.errcode === 0) {
557
- plugin.logger.info(`创建群聊成功: ${data.chatid}`);
558
- return data.chatid;
559
- }
560
- throw new Error(`Failed to create chat: ${data.errmsg}`);
561
- }
562
- catch (error) {
563
- plugin.logger.error('Failed to create chat:', error);
564
- return null;
565
- }
566
- }
567
- /**
568
- * 获取群聊信息
569
- * @param chatId 群聊 ID
570
- */
571
- async getChatInfo(chatId) {
572
- try {
573
- const data = await this.request('/topapi/chat/get', {
574
- method: 'POST',
575
- body: { chatid: chatId }
576
- });
577
- if (data.errcode === 0) {
578
- return data.chat_info;
579
- }
580
- throw new Error(`Failed to get chat info: ${data.errmsg}`);
581
- }
582
- catch (error) {
583
- plugin.logger.error('Failed to get chat info:', error);
584
- return null;
585
- }
586
- }
587
- /**
588
- * 更新群聊(添加/移除成员等)
589
- * @param chatId 群聊 ID
590
- * @param options 更新选项
591
- */
592
- async updateChat(chatId, options) {
593
- try {
594
- const data = await this.request('/topapi/chat/update', {
595
- method: 'POST',
596
- body: { chatid: chatId, ...options }
597
- });
598
- if (data.errcode === 0) {
599
- plugin.logger.info(`更新群聊成功: ${chatId}`);
600
- return true;
601
- }
602
- throw new Error(`Failed to update chat: ${data.errmsg}`);
603
- }
604
- catch (error) {
605
- plugin.logger.error('Failed to update chat:', error);
606
- return false;
607
- }
608
- }
609
- }
610
- // 定义 Adapter 类
611
- class DingTalkAdapter extends Adapter {
612
- #router;
613
- constructor(plugin, router) {
614
- super(plugin, 'dingtalk', []);
615
- this.#router = router;
616
- }
617
- createBot(config) {
618
- return new DingTalkBot(this, this.#router, config);
619
- }
620
- // ── IGroupManagement 标准群管方法 ──────────────────────────────────
621
- async kickMember(botId, sceneId, userId) {
622
- const bot = this.bots.get(botId);
623
- if (!bot)
624
- throw new Error(`Bot ${botId} 不存在`);
625
- return bot.updateChat(sceneId, { del_useridlist: [userId] });
626
- }
627
- async setGroupName(botId, sceneId, name) {
628
- const bot = this.bots.get(botId);
629
- if (!bot)
630
- throw new Error(`Bot ${botId} 不存在`);
631
- return bot.updateChat(sceneId, { name });
632
- }
633
- async getGroupInfo(botId, sceneId) {
634
- const bot = this.bots.get(botId);
635
- if (!bot)
636
- throw new Error(`Bot ${botId} 不存在`);
637
- return bot.getChatInfo(sceneId);
638
- }
639
- // ── 生命周期 ───────────────────────────────────────────────────────
640
- async start() {
641
- this.registerDingTalkPlatformTools();
642
- const groupTools = createGroupManagementTools(this, this.name);
643
- groupTools.forEach((t) => this.addTool(t));
644
- this.declareSkill({
645
- description: '钉钉群管理:踢人、禁言、设管理员、改群名、查成员等。仅有昵称时请先 list_members 获取 user_id 再操作。',
646
- keywords: GROUP_MANAGEMENT_SKILL_KEYWORDS,
647
- tags: GROUP_MANAGEMENT_SKILL_TAGS,
648
- });
649
- await super.start();
650
- }
651
- /**
652
- * 注册钉钉平台特有工具(获取用户信息等)
653
- */
654
- registerDingTalkPlatformTools() {
655
- // 获取用户信息工具
656
- this.addTool({
657
- name: 'dingtalk_get_user',
658
- description: '获取钉钉用户信息',
659
- parameters: {
660
- type: 'object',
661
- properties: {
662
- bot: { type: 'string', description: 'Bot 名称' },
663
- user_id: { type: 'string', description: '用户 ID' },
664
- },
665
- required: ['bot', 'user_id'],
666
- },
667
- platforms: ['dingtalk'],
668
- scopes: ['group', 'private'],
669
- permissionLevel: 'user',
670
- execute: async (args) => {
671
- const { bot: botId, user_id } = args;
672
- const bot = this.bots.get(botId);
673
- if (!bot)
674
- throw new Error(`Bot ${botId} 不存在`);
675
- return await bot.getUserInfo(user_id);
676
- },
677
- });
678
- // 获取部门用户列表工具
679
- this.addTool({
680
- name: 'dingtalk_get_dept_users',
681
- description: '获取钉钉部门用户列表',
682
- parameters: {
683
- type: 'object',
684
- properties: {
685
- bot: { type: 'string', description: 'Bot 名称' },
686
- dept_id: { type: 'number', description: '部门 ID' },
687
- },
688
- required: ['bot', 'dept_id'],
689
- },
690
- platforms: ['dingtalk'],
691
- scopes: ['group', 'private'],
692
- permissionLevel: 'user',
693
- execute: async (args) => {
694
- const { bot: botId, dept_id } = args;
695
- const bot = this.bots.get(botId);
696
- if (!bot)
697
- throw new Error(`Bot ${botId} 不存在`);
698
- const users = await bot.getDepartmentUsers(dept_id);
699
- return { users, count: users.length };
700
- },
701
- });
702
- // 获取部门列表工具
703
- this.addTool({
704
- name: 'dingtalk_list_departments',
705
- description: '获取钉钉部门列表',
706
- parameters: {
707
- type: 'object',
708
- properties: {
709
- bot: { type: 'string', description: 'Bot 名称' },
710
- dept_id: { type: 'number', description: '父部门 ID,默认 1(根部门)' },
711
- },
712
- required: ['bot'],
713
- },
714
- platforms: ['dingtalk'],
715
- scopes: ['group', 'private'],
716
- permissionLevel: 'user',
717
- execute: async (args) => {
718
- const { bot: botId, dept_id = 1 } = args;
719
- const bot = this.bots.get(botId);
720
- if (!bot)
721
- throw new Error(`Bot ${botId} 不存在`);
722
- const departments = await bot.getDepartmentList(dept_id);
723
- return { departments, count: departments.length };
724
- },
725
- });
726
- // 发送工作通知工具
727
- this.addTool({
728
- name: 'dingtalk_send_work_notice',
729
- description: '向指定用户发送钉钉工作通知',
730
- parameters: {
731
- type: 'object',
732
- properties: {
733
- bot: { type: 'string', description: 'Bot 名称' },
734
- user_ids: { type: 'array', items: { type: 'string' }, description: '用户 ID 列表' },
735
- content: { type: 'string', description: '通知内容' },
736
- },
737
- required: ['bot', 'user_ids', 'content'],
738
- },
739
- platforms: ['dingtalk'],
740
- scopes: ['group', 'private'],
741
- permissionLevel: 'group_admin',
742
- execute: async (args) => {
743
- const { bot: botId, user_ids, content } = args;
744
- const bot = this.bots.get(botId);
745
- if (!bot)
746
- throw new Error(`Bot ${botId} 不存在`);
747
- const msgContent = {
748
- msgtype: 'text',
749
- text: { content }
750
- };
751
- const success = await bot.sendWorkNotice(user_ids, msgContent);
752
- return { success, message: success ? '工作通知已发送' : '发送失败' };
753
- },
754
- });
755
- // 创建群聊工具
756
- this.addTool({
757
- name: 'dingtalk_create_chat',
758
- description: '创建钉钉群聊',
759
- parameters: {
760
- type: 'object',
761
- properties: {
762
- bot: { type: 'string', description: 'Bot 名称' },
763
- name: { type: 'string', description: '群名' },
764
- owner: { type: 'string', description: '群主用户 ID' },
765
- members: { type: 'array', items: { type: 'string' }, description: '成员用户 ID 列表' },
766
- },
767
- required: ['bot', 'name', 'owner', 'members'],
768
- },
769
- platforms: ['dingtalk'],
770
- scopes: ['group', 'private'],
771
- permissionLevel: 'group_admin',
772
- execute: async (args) => {
773
- const { bot: botId, name, owner, members } = args;
774
- const bot = this.bots.get(botId);
775
- if (!bot)
776
- throw new Error(`Bot ${botId} 不存在`);
777
- const chatId = await bot.createChat(name, owner, members);
778
- return { success: !!chatId, chat_id: chatId, message: chatId ? `群聊创建成功: ${chatId}` : '创建失败' };
779
- },
780
- });
781
- // 添加群成员工具
782
- this.addTool({
783
- name: 'dingtalk_add_chat_members',
784
- description: '向钉钉群聊添加成员',
785
- parameters: {
786
- type: 'object',
787
- properties: {
788
- bot: { type: 'string', description: 'Bot 名称' },
789
- chat_id: { type: 'string', description: '群聊 ID' },
790
- user_ids: { type: 'array', items: { type: 'string' }, description: '要添加的用户 ID 列表' },
791
- },
792
- required: ['bot', 'chat_id', 'user_ids'],
793
- },
794
- platforms: ['dingtalk'],
795
- scopes: ['group'],
796
- permissionLevel: 'group_admin',
797
- execute: async (args) => {
798
- const { bot: botId, chat_id, user_ids } = args;
799
- const bot = this.bots.get(botId);
800
- if (!bot)
801
- throw new Error(`Bot ${botId} 不存在`);
802
- const success = await bot.updateChat(chat_id, { add_useridlist: user_ids });
803
- return { success, message: success ? '成员添加成功' : '添加失败' };
804
- },
805
- });
806
- // 部门详情
807
- this.addTool({
808
- name: 'dingtalk_dept_info',
809
- description: '获取钉钉部门详细信息',
810
- parameters: {
811
- type: 'object',
812
- properties: {
813
- bot: { type: 'string', description: 'Bot 名称' },
814
- dept_id: { type: 'string', description: '部门 ID' },
815
- },
816
- required: ['bot', 'dept_id'],
817
- },
818
- platforms: ['dingtalk'],
819
- scopes: ['group', 'private'],
820
- permissionLevel: 'user',
821
- execute: async (args) => {
822
- const { bot: botId, dept_id } = args;
823
- const bot = this.bots.get(botId);
824
- if (!bot)
825
- throw new Error(`Bot ${botId} 不存在`);
826
- const info = await bot.getDepartmentInfo(dept_id);
827
- return info;
828
- },
829
- });
830
- // 更新群设置
831
- this.addTool({
832
- name: 'dingtalk_update_chat',
833
- description: '更新钉钉群聊设置(改名、换群主、增减成员)',
834
- parameters: {
835
- type: 'object',
836
- properties: {
837
- bot: { type: 'string', description: 'Bot 名称' },
838
- chat_id: { type: 'string', description: '群聊 ID' },
839
- name: { type: 'string', description: '新群名(可选)' },
840
- owner: { type: 'string', description: '新群主 userId(可选)' },
841
- add_members: { type: 'string', description: '要添加的成员 userId,逗号分隔(可选)' },
842
- remove_members: { type: 'string', description: '要移除的成员 userId,逗号分隔(可选)' },
843
- },
844
- required: ['bot', 'chat_id'],
845
- },
846
- platforms: ['dingtalk'],
847
- scopes: ['group'],
848
- permissionLevel: 'group_admin',
849
- execute: async (args) => {
850
- const { bot: botId, chat_id, name, owner, add_members, remove_members } = args;
851
- const bot = this.bots.get(botId);
852
- if (!bot)
853
- throw new Error(`Bot ${botId} 不存在`);
854
- const options = {};
855
- if (name)
856
- options.name = name;
857
- if (owner)
858
- options.owner = owner;
859
- if (add_members)
860
- options.add_useridlist = add_members.split(',').map((s) => s.trim());
861
- if (remove_members)
862
- options.del_useridlist = remove_members.split(',').map((s) => s.trim());
863
- await bot.updateChat(chat_id, options);
864
- return { success: true, message: '群聊设置已更新' };
865
- },
866
- });
867
- plugin.logger.debug('已注册钉钉平台管理工具');
868
- }
869
- }
870
- // 使用新的 provide() API 注册适配器
871
- useContext('router', (router) => {
11
+ useContext("router", (router) => {
872
12
  provide({
873
13
  name: "dingtalk",
874
14
  description: "DingTalk Bot Adapter",