@zhin.js/adapter-dingtalk 4.0.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +59 -345
- package/adapters/dingtalk.ts +26 -0
- package/agent/tools/add_chat_members.ts +2 -2
- package/agent/tools/create_chat.ts +2 -2
- package/agent/tools/dept_info.ts +2 -2
- package/agent/tools/get_dept_users.ts +2 -2
- package/agent/tools/get_user.ts +2 -2
- package/agent/tools/list_departments.ts +2 -2
- package/agent/tools/send_work_notice.ts +2 -2
- package/agent/tools/update_chat.ts +2 -2
- package/lib/dingtalk-agent-deps.d.ts +26 -0
- package/lib/dingtalk-agent-deps.js +30 -0
- package/lib/endpoint.d.ts +55 -0
- package/lib/endpoint.js +312 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -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 +121 -0
- package/lib/protocol.js +221 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +48 -0
- package/package.json +45 -23
- package/plugin.ts +12 -0
- package/schema.json +56 -0
- package/src/dingtalk-agent-deps.ts +49 -9
- package/src/endpoint.ts +263 -479
- package/src/index.ts +45 -59
- package/src/platform-permit.ts +1 -1
- package/src/protocol.ts +338 -0
- package/src/webhook.ts +76 -0
- package/lib/agent/tools/add_chat_members.js +0 -21
- package/lib/agent/tools/add_chat_members.js.map +0 -1
- package/lib/agent/tools/create_chat.js +0 -22
- package/lib/agent/tools/create_chat.js.map +0 -1
- package/lib/agent/tools/dept_info.js +0 -17
- package/lib/agent/tools/dept_info.js.map +0 -1
- package/lib/agent/tools/get_dept_users.js +0 -18
- package/lib/agent/tools/get_dept_users.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/list_departments.js +0 -18
- package/lib/agent/tools/list_departments.js.map +0 -1
- package/lib/agent/tools/send_work_notice.js +0 -20
- package/lib/agent/tools/send_work_notice.js.map +0 -1
- package/lib/agent/tools/update_chat.js +0 -31
- package/lib/agent/tools/update_chat.js.map +0 -1
- package/lib/src/adapter.js +0 -40
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/dingtalk-agent-deps.js +0 -10
- package/lib/src/dingtalk-agent-deps.js.map +0 -1
- package/lib/src/endpoint.js +0 -547
- 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/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 -5
- package/lib/src/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/lib/protocol.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DingTalk protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
/**
|
|
7
|
+
* 钉钉回调消息 @ 机器人判定:机器人被 @ 时回调带 `isInAtList: true`;
|
|
8
|
+
* 部分回调形态的 `atUserIds` / `atUsers[].dingtalkId` 会包含机器人 robotCode(来自配置)。
|
|
9
|
+
* 两者都不满足则不标注。
|
|
10
|
+
*/
|
|
11
|
+
export function isDingtalkBotMentioned(event, robotCode) {
|
|
12
|
+
const extra = event;
|
|
13
|
+
if (extra.isInAtList === true)
|
|
14
|
+
return true;
|
|
15
|
+
if (!robotCode)
|
|
16
|
+
return false;
|
|
17
|
+
if (Array.isArray(extra.atUserIds) && extra.atUserIds.some((id) => String(id) === robotCode)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return (event.atUsers ?? []).some((user) => user.dingtalkId === robotCode);
|
|
21
|
+
}
|
|
22
|
+
export function resolveDingTalkConfig(config = {}) {
|
|
23
|
+
const entry = config.endpoints?.find((item) => item.context === 'dingtalk');
|
|
24
|
+
const appKey = config.appKey ?? entry?.appKey ?? process.env.DINGTALK_APP_KEY;
|
|
25
|
+
const appSecret = config.appSecret ?? entry?.appSecret ?? process.env.DINGTALK_APP_SECRET;
|
|
26
|
+
if (!appKey || !appSecret) {
|
|
27
|
+
throw new TypeError('DingTalk adapter requires appKey + appSecret (plugins.<key> or endpoints with context: dingtalk)');
|
|
28
|
+
}
|
|
29
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
30
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
31
|
+
|| process.env.DINGTALK_BOT_NAME
|
|
32
|
+
|| 'dingtalk-bot';
|
|
33
|
+
const webhookPath = normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? '/dingtalk/webhook');
|
|
34
|
+
const apiBaseUrl = (config.apiBaseUrl ?? entry?.apiBaseUrl ?? 'https://oapi.dingtalk.com').replace(/\/$/, '');
|
|
35
|
+
const robotCode = config.robotCode ?? entry?.robotCode;
|
|
36
|
+
return {
|
|
37
|
+
context: 'dingtalk',
|
|
38
|
+
name,
|
|
39
|
+
appKey,
|
|
40
|
+
appSecret,
|
|
41
|
+
webhookPath,
|
|
42
|
+
...(robotCode ? { robotCode } : {}),
|
|
43
|
+
apiBaseUrl,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function normalizeWebhookPath(path) {
|
|
47
|
+
const trimmed = path.trim() || '/dingtalk/webhook';
|
|
48
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
49
|
+
}
|
|
50
|
+
export function resolveChatType(conversationType) {
|
|
51
|
+
return conversationType === '2' ? 'group' : 'private';
|
|
52
|
+
}
|
|
53
|
+
export function resolveTarget(msg) {
|
|
54
|
+
return msg.conversationId || msg.senderId || 'unknown';
|
|
55
|
+
}
|
|
56
|
+
export function resolveSender(msg) {
|
|
57
|
+
return msg.senderId || msg.senderStaffId || 'unknown';
|
|
58
|
+
}
|
|
59
|
+
export function generateMessageId(msg) {
|
|
60
|
+
return msg.msgId || `${msg.createAt ?? Date.now()}`;
|
|
61
|
+
}
|
|
62
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
63
|
+
export function formatInboundContent(msg) {
|
|
64
|
+
if (!msg.msgtype)
|
|
65
|
+
return '';
|
|
66
|
+
switch (msg.msgtype) {
|
|
67
|
+
case 'text':
|
|
68
|
+
return msg.text?.content || '';
|
|
69
|
+
case 'picture':
|
|
70
|
+
return '[image]';
|
|
71
|
+
case 'file': {
|
|
72
|
+
const name = typeof msg.content?.fileName === 'string' ? msg.content.fileName : '';
|
|
73
|
+
return name ? `[file: ${name}]` : '[file]';
|
|
74
|
+
}
|
|
75
|
+
case 'audio':
|
|
76
|
+
return '[audio]';
|
|
77
|
+
case 'video':
|
|
78
|
+
return '[video]';
|
|
79
|
+
case 'richText': {
|
|
80
|
+
const rich = msg.content?.richText;
|
|
81
|
+
if (Array.isArray(rich)) {
|
|
82
|
+
return rich
|
|
83
|
+
.map((item) => (item && typeof item === 'object' && 'text' in item
|
|
84
|
+
? String(item.text || '')
|
|
85
|
+
: ''))
|
|
86
|
+
.join('');
|
|
87
|
+
}
|
|
88
|
+
return '[richText]';
|
|
89
|
+
}
|
|
90
|
+
case 'markdown':
|
|
91
|
+
return typeof msg.content?.text === 'string' ? msg.content.text : '[markdown]';
|
|
92
|
+
default:
|
|
93
|
+
return `[${msg.msgtype}]`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function verifySignature(appSecret, timestamp, sign) {
|
|
97
|
+
try {
|
|
98
|
+
const stringToSign = `${timestamp}\n${appSecret}`;
|
|
99
|
+
const hmac = createHmac('sha256', appSecret);
|
|
100
|
+
hmac.update(stringToSign);
|
|
101
|
+
const calculated = hmac.digest('base64');
|
|
102
|
+
const a = Buffer.from(calculated);
|
|
103
|
+
const b = Buffer.from(sign);
|
|
104
|
+
if (a.length !== b.length)
|
|
105
|
+
return false;
|
|
106
|
+
return timingSafeEqual(a, b);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Wire-encode an already-rendered outbound payload into DingTalk robot body.
|
|
114
|
+
* Segment canonicalization is intentionally not done here.
|
|
115
|
+
*/
|
|
116
|
+
export function formatOutboundBody(payload) {
|
|
117
|
+
if (typeof payload === 'string') {
|
|
118
|
+
return { msgtype: 'text', text: { content: payload } };
|
|
119
|
+
}
|
|
120
|
+
const items = Array.isArray(payload)
|
|
121
|
+
? payload
|
|
122
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
123
|
+
? [payload]
|
|
124
|
+
: [];
|
|
125
|
+
if (items.length === 0) {
|
|
126
|
+
const text = payload == null
|
|
127
|
+
? ''
|
|
128
|
+
: typeof payload === 'object'
|
|
129
|
+
? JSON.stringify(payload)
|
|
130
|
+
: String(payload);
|
|
131
|
+
return { msgtype: 'text', text: { content: text } };
|
|
132
|
+
}
|
|
133
|
+
const textParts = [];
|
|
134
|
+
const atUserIds = [];
|
|
135
|
+
let media = null;
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
if (typeof item === 'string') {
|
|
138
|
+
textParts.push(item);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const data = item.data ?? {};
|
|
142
|
+
switch (item.type) {
|
|
143
|
+
case 'text':
|
|
144
|
+
textParts.push(String(data.content ?? data.text ?? ''));
|
|
145
|
+
break;
|
|
146
|
+
case 'at': {
|
|
147
|
+
const userId = data.id ?? data.userId;
|
|
148
|
+
if (userId) {
|
|
149
|
+
atUserIds.push(String(userId));
|
|
150
|
+
textParts.push(`@${String(data.name || userId)} `);
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'image':
|
|
155
|
+
if (!media) {
|
|
156
|
+
media = {
|
|
157
|
+
msgtype: 'picture',
|
|
158
|
+
picture: { picURL: String(data.url ?? data.file ?? '') },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'markdown':
|
|
163
|
+
if (!media) {
|
|
164
|
+
media = {
|
|
165
|
+
msgtype: 'markdown',
|
|
166
|
+
markdown: {
|
|
167
|
+
title: String(data.title || '消息'),
|
|
168
|
+
text: String(data.content ?? data.text ?? ''),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'link':
|
|
174
|
+
if (!media) {
|
|
175
|
+
media = {
|
|
176
|
+
msgtype: 'link',
|
|
177
|
+
link: {
|
|
178
|
+
title: String(data.title || '链接'),
|
|
179
|
+
text: String(data.text ?? data.content ?? ''),
|
|
180
|
+
messageUrl: typeof data.url === 'string' ? data.url : undefined,
|
|
181
|
+
picUrl: typeof data.picUrl === 'string' ? data.picUrl : undefined,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
textParts.push(`[${item.type}]`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (media)
|
|
191
|
+
return media;
|
|
192
|
+
const result = {
|
|
193
|
+
msgtype: 'text',
|
|
194
|
+
text: { content: textParts.join('') },
|
|
195
|
+
};
|
|
196
|
+
if (atUserIds.length > 0) {
|
|
197
|
+
return { ...result, at: { atUserIds, isAtAll: false } };
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
export function headerValue(headers, name) {
|
|
202
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
203
|
+
if (Array.isArray(value))
|
|
204
|
+
return value[0] ?? '';
|
|
205
|
+
return value ?? '';
|
|
206
|
+
}
|
|
207
|
+
export async function readTextBody(request, options = {}) {
|
|
208
|
+
const limit = options.limit ?? 1_048_576;
|
|
209
|
+
const chunks = [];
|
|
210
|
+
let size = 0;
|
|
211
|
+
for await (const chunk of request) {
|
|
212
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
213
|
+
size += buffer.length;
|
|
214
|
+
if (size > limit) {
|
|
215
|
+
request.destroy();
|
|
216
|
+
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
217
|
+
}
|
|
218
|
+
chunks.push(buffer);
|
|
219
|
+
}
|
|
220
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
221
|
+
}
|
package/lib/webhook.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DingTalk webhook HTTP: signature → parse → admit.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { type DingTalkEvent, type ResolvedDingTalkConfig } from './protocol.js';
|
|
7
|
+
export interface DingTalkWebhookHandler {
|
|
8
|
+
readonly config: ResolvedDingTalkConfig;
|
|
9
|
+
readonly isOpen: boolean;
|
|
10
|
+
admit(event: DingTalkEvent): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerDingTalkWebhookRoutes(http: HttpHost, handler: DingTalkWebhookHandler): HttpRouteRegistration[];
|
|
13
|
+
export declare function handleDingTalkWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: DingTalkWebhookHandler): Promise<void>;
|
package/lib/webhook.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { headerValue, readTextBody, verifySignature, } from './protocol.js';
|
|
3
|
+
const logger = getLogger('dingtalk');
|
|
4
|
+
export function registerDingTalkWebhookRoutes(http, handler) {
|
|
5
|
+
const path = handler.config.webhookPath;
|
|
6
|
+
return [
|
|
7
|
+
http.route('POST', path, async (request, response) => {
|
|
8
|
+
await handleDingTalkWebhookRequest(request, response, handler);
|
|
9
|
+
}, { summary: 'DingTalk robot webhook', tags: ['dingtalk'] }),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
export async function handleDingTalkWebhookRequest(request, response, handler) {
|
|
13
|
+
try {
|
|
14
|
+
// DingTalk outgoing callbacks put timestamp/sign on the URL query;
|
|
15
|
+
// headers are accepted as a fallback for legacy senders.
|
|
16
|
+
const query = new URL(request.url ?? '/', 'http://localhost').searchParams;
|
|
17
|
+
const timestamp = query.get('timestamp') || headerValue(request.headers, 'timestamp');
|
|
18
|
+
const sign = query.get('sign') || headerValue(request.headers, 'sign');
|
|
19
|
+
if (timestamp && sign) {
|
|
20
|
+
if (!verifySignature(handler.config.appSecret, timestamp, sign)) {
|
|
21
|
+
logger.warn(formatCompact({ op: 'webhook', ok: false, error: 'invalid signature' }));
|
|
22
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
23
|
+
response.end(JSON.stringify({ code: -1, msg: 'Forbidden' }));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const rawBody = await readTextBody(request);
|
|
28
|
+
let event;
|
|
29
|
+
try {
|
|
30
|
+
event = JSON.parse(rawBody);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
34
|
+
response.end(JSON.stringify({ code: 0, msg: 'success' }));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.msgtype && handler.isOpen) {
|
|
38
|
+
handler.admit(event);
|
|
39
|
+
}
|
|
40
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
41
|
+
response.end(JSON.stringify({ code: 0, msg: 'success' }));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error('Webhook error:', error);
|
|
45
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
46
|
+
response.end(JSON.stringify({ code: -1, msg: 'Internal Server Error' }));
|
|
47
|
+
}
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-dingtalk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Zhin.js DingTalk (钉钉) adapter for Plugin Runtime (HTTP webhook)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -32,24 +32,25 @@
|
|
|
32
32
|
"type": "git",
|
|
33
33
|
"directory": "plugins/adapters/dingtalk"
|
|
34
34
|
},
|
|
35
|
-
"
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"@zhin.js/
|
|
41
|
-
"zhin.js": "4.1.2",
|
|
42
|
-
"@zhin.js/agent": "1.0.3"
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@zhin.js/adapter": "1.1.0",
|
|
37
|
+
"@zhin.js/core": "1.4.0",
|
|
38
|
+
"@zhin.js/host-http": "1.0.2",
|
|
39
|
+
"@zhin.js/logger": "1.0.75",
|
|
40
|
+
"@zhin.js/plugin-runtime": "1.1.0"
|
|
43
41
|
},
|
|
44
42
|
"peerDependencies": {
|
|
45
43
|
"zod": "^4.0.0",
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"zhin.js": "
|
|
48
|
-
"@zhin.js/
|
|
44
|
+
"@zhin.js/adapter": "1.1.0",
|
|
45
|
+
"@zhin.js/agent": "1.0.5",
|
|
46
|
+
"@zhin.js/core": "1.4.0",
|
|
47
|
+
"@zhin.js/host-http": "1.0.2",
|
|
48
|
+
"@zhin.js/plugin-runtime": "1.1.0",
|
|
49
|
+
"zhin.js": "5.0.0"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
51
|
-
"
|
|
52
|
-
"optional":
|
|
52
|
+
"zhin.js": {
|
|
53
|
+
"optional": true
|
|
53
54
|
},
|
|
54
55
|
"@zhin.js/agent": {
|
|
55
56
|
"optional": true
|
|
@@ -58,16 +59,23 @@
|
|
|
58
59
|
"optional": true
|
|
59
60
|
}
|
|
60
61
|
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^26.1.0",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"vitest": "^4.1.10",
|
|
66
|
+
"zod": "^4.4.3",
|
|
67
|
+
"@zhin.js/agent": "1.0.5",
|
|
68
|
+
"zhin.js": "5.0.0"
|
|
69
|
+
},
|
|
61
70
|
"files": [
|
|
71
|
+
"adapters",
|
|
72
|
+
"plugin.ts",
|
|
73
|
+
"schema.json",
|
|
62
74
|
"src",
|
|
63
75
|
"lib",
|
|
64
|
-
"
|
|
65
|
-
"dist",
|
|
66
|
-
"plugin.yml",
|
|
76
|
+
"agent",
|
|
67
77
|
"README.md",
|
|
68
|
-
"
|
|
69
|
-
"CHANGELOG.md",
|
|
70
|
-
"agent"
|
|
78
|
+
"CHANGELOG.md"
|
|
71
79
|
],
|
|
72
80
|
"publishConfig": {
|
|
73
81
|
"access": "public",
|
|
@@ -76,9 +84,23 @@
|
|
|
76
84
|
"engines": {
|
|
77
85
|
"node": "^20.19.0 || >=22.12.0"
|
|
78
86
|
},
|
|
87
|
+
"zhin": {
|
|
88
|
+
"protocol": 1,
|
|
89
|
+
"type": "plugin",
|
|
90
|
+
"entry": "./plugin.ts",
|
|
91
|
+
"engine": "^1.0.0",
|
|
92
|
+
"runtime": "trusted",
|
|
93
|
+
"features": [
|
|
94
|
+
{
|
|
95
|
+
"package": "@zhin.js/adapter",
|
|
96
|
+
"api": "^1.0.0"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"plugins": []
|
|
100
|
+
},
|
|
79
101
|
"scripts": {
|
|
80
|
-
"build": "
|
|
102
|
+
"build": "tsc",
|
|
81
103
|
"clean": "rimraf lib",
|
|
82
|
-
"
|
|
104
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/dingtalk/tests"
|
|
83
105
|
}
|
|
84
106
|
}
|
package/plugin.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { definePlugin } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { registerDingtalkPlatformPermitChecker } from './src/platform-permit.js';
|
|
3
|
+
|
|
4
|
+
export default definePlugin({
|
|
5
|
+
name: 'dingtalk',
|
|
6
|
+
metadata: {
|
|
7
|
+
displayName: 'DingTalk (钉钉) Adapter',
|
|
8
|
+
},
|
|
9
|
+
setup() {
|
|
10
|
+
return registerDingtalkPlatformPermitChecker();
|
|
11
|
+
},
|
|
12
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"apiBaseUrl": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"default": "https://oapi.dingtalk.com"
|
|
9
|
+
},
|
|
10
|
+
"endpoints": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(name 必填,其余覆盖顶层)",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": true,
|
|
16
|
+
"properties": {
|
|
17
|
+
"name": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Dingtalk bot name"
|
|
20
|
+
},
|
|
21
|
+
"appKey": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Dingtalk app key"
|
|
24
|
+
},
|
|
25
|
+
"appSecret": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Dingtalk app secret"
|
|
28
|
+
},
|
|
29
|
+
"webhookPath": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Dingtalk webhook path"
|
|
32
|
+
},
|
|
33
|
+
"robotCode": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Dingtalk robot code"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": [
|
|
39
|
+
"name",
|
|
40
|
+
"appKey",
|
|
41
|
+
"appSecret",
|
|
42
|
+
"webhookPath",
|
|
43
|
+
"robotCode"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"commandPrefix": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"default": "",
|
|
50
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": [
|
|
54
|
+
"endpoints"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for dingtalk (user / dept / chat / work notice).
|
|
3
|
+
* Endpoints register themselves on start; tools look up by endpoint id.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DingtalkAgentEndpoint {
|
|
7
|
+
getUserInfo(userId: string): Promise<unknown>;
|
|
8
|
+
getDepartmentUsers(deptId: number): Promise<unknown[]>;
|
|
9
|
+
sendWorkNotice(userIdList: string[], content: unknown): Promise<boolean>;
|
|
10
|
+
getDepartmentList(deptId?: number): Promise<unknown[]>;
|
|
11
|
+
getDepartmentInfo(deptId: number): Promise<unknown>;
|
|
12
|
+
createChat(name: string, ownerUserId: string, userIdList: string[]): Promise<string | null>;
|
|
13
|
+
getChatInfo(chatId: string): Promise<unknown>;
|
|
14
|
+
updateChat(
|
|
15
|
+
chatId: string,
|
|
16
|
+
options: {
|
|
17
|
+
name?: string;
|
|
18
|
+
owner?: string;
|
|
19
|
+
add_useridlist?: string[];
|
|
20
|
+
del_useridlist?: string[];
|
|
21
|
+
},
|
|
22
|
+
): Promise<boolean>;
|
|
23
|
+
}
|
|
3
24
|
|
|
4
25
|
export interface DingtalkAgentDeps {
|
|
5
|
-
getEndpoint: (endpointId: string) =>
|
|
6
|
-
getAdapter: () => DingTalkAdapter;
|
|
26
|
+
getEndpoint: (endpointId: string) => DingtalkAgentEndpoint;
|
|
7
27
|
}
|
|
8
28
|
|
|
9
|
-
|
|
29
|
+
const endpoints = new Map<string, DingtalkAgentEndpoint>();
|
|
30
|
+
let override: DingtalkAgentDeps | null = null;
|
|
31
|
+
|
|
32
|
+
export function registerDingtalkAgentEndpoint(
|
|
33
|
+
endpointId: string,
|
|
34
|
+
endpoint: DingtalkAgentEndpoint,
|
|
35
|
+
): () => void {
|
|
36
|
+
endpoints.set(endpointId, endpoint);
|
|
37
|
+
return () => {
|
|
38
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
39
|
+
endpoints.delete(endpointId);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
10
43
|
|
|
11
|
-
|
|
12
|
-
|
|
44
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
45
|
+
export function setDingtalkAgentDeps(deps: DingtalkAgentDeps | null): void {
|
|
46
|
+
override = deps;
|
|
13
47
|
}
|
|
14
48
|
|
|
15
49
|
export function getDingtalkAgentDeps(): DingtalkAgentDeps {
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
50
|
+
if (override) return override;
|
|
51
|
+
return {
|
|
52
|
+
getEndpoint(endpointId) {
|
|
53
|
+
const endpoint = endpoints.get(endpointId);
|
|
54
|
+
if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
55
|
+
return endpoint;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
18
58
|
}
|