@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.
- package/CHANGELOG.md +56 -0
- package/README.md +58 -335
- package/adapters/dingtalk.ts +26 -0
- package/agent/tools/add_chat_members.ts +22 -0
- package/agent/tools/create_chat.ts +23 -0
- package/agent/tools/dept_info.ts +17 -0
- package/agent/tools/get_dept_users.ts +18 -0
- package/agent/tools/get_user.ts +17 -0
- package/agent/tools/list_departments.ts +18 -0
- package/agent/tools/send_work_notice.ts +20 -0
- package/agent/tools/update_chat.ts +27 -0
- package/lib/dingtalk-agent-deps.d.ts +26 -0
- package/lib/dingtalk-agent-deps.js +30 -0
- package/lib/endpoint.d.ts +45 -36
- package/lib/endpoint.js +199 -434
- package/lib/index.d.ts +5 -15
- package/lib/index.js +5 -219
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -2
- package/lib/protocol.d.ts +121 -0
- package/lib/protocol.js +221 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +48 -0
- package/package.json +51 -19
- package/plugin.ts +12 -0
- package/schema.json +23 -0
- package/src/dingtalk-agent-deps.ts +58 -0
- package/src/endpoint.ts +263 -479
- package/src/index.ts +46 -235
- package/src/platform-permit.ts +1 -2
- package/src/protocol.ts +338 -0
- package/src/webhook.ts +76 -0
- package/lib/adapter.d.ts +0 -19
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -40
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -58
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -46
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -56
- /package/{skills/dingtalk → agent}/PERMITS.md +0 -0
- /package/{skills/dingtalk/SKILL.md → agent/skills/dingtalk.md} +0 -0
package/lib/endpoint.js
CHANGED
|
@@ -1,418 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
21
|
+
/** Used by webhook handler. */
|
|
22
|
+
get isOpen() {
|
|
23
|
+
return this.#open;
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
this.
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
43
|
+
await this.stop();
|
|
44
|
+
logger.error('Failed to connect DingTalk endpoint:', error);
|
|
45
|
+
throw error;
|
|
81
46
|
}
|
|
82
47
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
415
|
-
method:
|
|
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
|
-
|
|
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
|
|
430
|
-
method:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
445
|
-
method:
|
|
169
|
+
const data = await this.#request('/topapi/message/corpconversation/asyncsend_v2', {
|
|
170
|
+
method: 'POST',
|
|
446
171
|
body: {
|
|
447
|
-
agent_id: this
|
|
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
|
-
|
|
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
|
|
466
|
-
method:
|
|
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
|
-
|
|
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
|
|
481
|
-
method:
|
|
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
|
-
|
|
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
|
|
496
|
-
method:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
517
|
-
method:
|
|
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
|
-
|
|
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
|
|
532
|
-
method:
|
|
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
|
-
|
|
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
|