@soimy/dingtalk 2.6.5
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/README.md +482 -0
- package/clawbot.plugin.json +9 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +79 -0
- package/src/AGENTS.md +63 -0
- package/src/channel.ts +1807 -0
- package/src/config-schema.ts +92 -0
- package/src/connection-manager.ts +434 -0
- package/src/media-utils.ts +132 -0
- package/src/onboarding.ts +325 -0
- package/src/openclaw-channel-dingtalk.code-workspace +17 -0
- package/src/peer-id-registry.ts +35 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +543 -0
- package/src/utils.ts +106 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { OpenClawConfig, ChannelOnboardingAdapter, WizardPrompter } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, formatDocsLink } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { DingTalkConfig, DingTalkChannelConfig } from "./types.js";
|
|
4
|
+
import { listDingTalkAccountIds, resolveDingTalkAccount } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const channel = "dingtalk" as const;
|
|
7
|
+
|
|
8
|
+
function isConfigured(account: DingTalkConfig): boolean {
|
|
9
|
+
return Boolean(account.clientId && account.clientSecret);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseList(value: string): string[] {
|
|
13
|
+
return value
|
|
14
|
+
.split(/[\n,;]+/g)
|
|
15
|
+
.map((entry) => entry.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyAccountNameToChannelSection(params: {
|
|
20
|
+
cfg: OpenClawConfig;
|
|
21
|
+
channelKey: string;
|
|
22
|
+
accountId: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
}): OpenClawConfig {
|
|
25
|
+
const { cfg, channelKey, name } = params;
|
|
26
|
+
if (!name) return cfg;
|
|
27
|
+
const base = cfg.channels?.[channelKey] as DingTalkChannelConfig | undefined;
|
|
28
|
+
return {
|
|
29
|
+
...cfg,
|
|
30
|
+
channels: {
|
|
31
|
+
...cfg.channels,
|
|
32
|
+
[channelKey]: { ...base, name },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function promptDingTalkAccountId(options: {
|
|
38
|
+
cfg: OpenClawConfig;
|
|
39
|
+
prompter: WizardPrompter;
|
|
40
|
+
label: string;
|
|
41
|
+
currentId: string;
|
|
42
|
+
listAccountIds: (cfg: OpenClawConfig) => string[];
|
|
43
|
+
defaultAccountId: string;
|
|
44
|
+
}): Promise<string> {
|
|
45
|
+
const existingIds = options.listAccountIds(options.cfg);
|
|
46
|
+
if (existingIds.length === 0) {
|
|
47
|
+
return options.defaultAccountId;
|
|
48
|
+
}
|
|
49
|
+
const useExisting = await options.prompter.confirm({
|
|
50
|
+
message: `Use existing ${options.label} account?`,
|
|
51
|
+
initialValue: true,
|
|
52
|
+
});
|
|
53
|
+
if (useExisting && existingIds.includes(options.currentId)) {
|
|
54
|
+
return options.currentId;
|
|
55
|
+
}
|
|
56
|
+
const newId = await options.prompter.text({
|
|
57
|
+
message: `New ${options.label} account ID`,
|
|
58
|
+
placeholder: options.defaultAccountId,
|
|
59
|
+
initialValue: options.defaultAccountId,
|
|
60
|
+
});
|
|
61
|
+
return normalizeAccountId(String(newId));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function noteDingTalkHelp(prompter: WizardPrompter): Promise<void> {
|
|
65
|
+
await prompter.note(
|
|
66
|
+
[
|
|
67
|
+
"You need DingTalk application credentials.",
|
|
68
|
+
"1. Visit https://open-dev.dingtalk.com/",
|
|
69
|
+
"2. Create an enterprise internal application",
|
|
70
|
+
"3. Enable 'Robot' capability",
|
|
71
|
+
"4. Configure message receiving mode as 'Stream mode'",
|
|
72
|
+
"5. Copy Client ID (AppKey) and Client Secret (AppSecret)",
|
|
73
|
+
`Docs: ${formatDocsLink("/channels/dingtalk", "channels/dingtalk")}`,
|
|
74
|
+
].join("\n"),
|
|
75
|
+
"DingTalk setup",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyAccountConfig(params: {
|
|
80
|
+
cfg: OpenClawConfig;
|
|
81
|
+
accountId: string;
|
|
82
|
+
input: Partial<DingTalkConfig>;
|
|
83
|
+
}): OpenClawConfig {
|
|
84
|
+
const { cfg, accountId, input } = params;
|
|
85
|
+
const useDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
86
|
+
|
|
87
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
88
|
+
cfg,
|
|
89
|
+
channelKey: "dingtalk",
|
|
90
|
+
accountId,
|
|
91
|
+
name: input.name,
|
|
92
|
+
});
|
|
93
|
+
const base = namedConfig.channels?.dingtalk as DingTalkChannelConfig | undefined;
|
|
94
|
+
|
|
95
|
+
const payload: Partial<DingTalkConfig> = {
|
|
96
|
+
...(input.clientId ? { clientId: input.clientId } : {}),
|
|
97
|
+
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
98
|
+
...(input.robotCode ? { robotCode: input.robotCode } : {}),
|
|
99
|
+
...(input.corpId ? { corpId: input.corpId } : {}),
|
|
100
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
101
|
+
...(input.dmPolicy ? { dmPolicy: input.dmPolicy } : {}),
|
|
102
|
+
...(input.groupPolicy ? { groupPolicy: input.groupPolicy } : {}),
|
|
103
|
+
...(input.allowFrom && input.allowFrom.length > 0 ? { allowFrom: input.allowFrom } : {}),
|
|
104
|
+
...(input.messageType ? { messageType: input.messageType } : {}),
|
|
105
|
+
...(input.cardTemplateId ? { cardTemplateId: input.cardTemplateId } : {}),
|
|
106
|
+
...(input.cardTemplateKey ? { cardTemplateKey: input.cardTemplateKey } : {}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (useDefault) {
|
|
110
|
+
return {
|
|
111
|
+
...namedConfig,
|
|
112
|
+
channels: {
|
|
113
|
+
...namedConfig.channels,
|
|
114
|
+
dingtalk: {
|
|
115
|
+
...base,
|
|
116
|
+
enabled: true,
|
|
117
|
+
...payload,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const accounts = (base as { accounts?: Record<string, unknown> }).accounts ?? {};
|
|
124
|
+
const existingAccount =
|
|
125
|
+
(base as { accounts?: Record<string, Record<string, unknown>> }).accounts?.[accountId] ?? {};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...namedConfig,
|
|
129
|
+
channels: {
|
|
130
|
+
...namedConfig.channels,
|
|
131
|
+
dingtalk: {
|
|
132
|
+
...base,
|
|
133
|
+
enabled: base?.enabled ?? true,
|
|
134
|
+
accounts: {
|
|
135
|
+
...accounts,
|
|
136
|
+
[accountId]: {
|
|
137
|
+
...existingAccount,
|
|
138
|
+
enabled: true,
|
|
139
|
+
...payload,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const dingtalkOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
148
|
+
channel,
|
|
149
|
+
getStatus: ({ cfg }) => {
|
|
150
|
+
const accountIds = listDingTalkAccountIds(cfg);
|
|
151
|
+
const configured =
|
|
152
|
+
accountIds.length > 0
|
|
153
|
+
? accountIds.some((accountId) => isConfigured(resolveDingTalkAccount(cfg, accountId)))
|
|
154
|
+
: isConfigured(resolveDingTalkAccount(cfg, DEFAULT_ACCOUNT_ID));
|
|
155
|
+
|
|
156
|
+
return Promise.resolve({
|
|
157
|
+
channel,
|
|
158
|
+
configured,
|
|
159
|
+
statusLines: [`DingTalk: ${configured ? "configured" : "needs setup"}`],
|
|
160
|
+
selectionHint: configured ? "configured" : "钉钉企业机器人",
|
|
161
|
+
quickstartScore: configured ? 1 : 4,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
|
165
|
+
const override = accountOverrides[channel]?.trim();
|
|
166
|
+
let accountId = override ? normalizeAccountId(override) : DEFAULT_ACCOUNT_ID;
|
|
167
|
+
|
|
168
|
+
if (shouldPromptAccountIds && !override) {
|
|
169
|
+
accountId = await promptDingTalkAccountId({
|
|
170
|
+
cfg,
|
|
171
|
+
prompter,
|
|
172
|
+
label: "DingTalk",
|
|
173
|
+
currentId: accountId,
|
|
174
|
+
listAccountIds: listDingTalkAccountIds,
|
|
175
|
+
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const resolved = resolveDingTalkAccount(cfg, accountId);
|
|
180
|
+
await noteDingTalkHelp(prompter);
|
|
181
|
+
|
|
182
|
+
const clientId = await prompter.text({
|
|
183
|
+
message: "Client ID (AppKey)",
|
|
184
|
+
placeholder: "dingxxxxxxxx",
|
|
185
|
+
initialValue: resolved.clientId ?? undefined,
|
|
186
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const clientSecret = await prompter.text({
|
|
190
|
+
message: "Client Secret (AppSecret)",
|
|
191
|
+
placeholder: "xxx-xxx-xxx-xxx",
|
|
192
|
+
initialValue: resolved.clientSecret ?? undefined,
|
|
193
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const wantsFullConfig = await prompter.confirm({
|
|
197
|
+
message: "Configure robot code, corp ID, and agent ID? (recommended for full features)",
|
|
198
|
+
initialValue: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let robotCode: string | undefined;
|
|
202
|
+
let corpId: string | undefined;
|
|
203
|
+
let agentId: string | undefined;
|
|
204
|
+
|
|
205
|
+
if (wantsFullConfig) {
|
|
206
|
+
robotCode =
|
|
207
|
+
String(
|
|
208
|
+
await prompter.text({
|
|
209
|
+
message: "Robot Code",
|
|
210
|
+
placeholder: "dingxxxxxxxx",
|
|
211
|
+
initialValue: resolved.robotCode ?? undefined,
|
|
212
|
+
}),
|
|
213
|
+
).trim() || undefined;
|
|
214
|
+
|
|
215
|
+
corpId =
|
|
216
|
+
String(
|
|
217
|
+
await prompter.text({
|
|
218
|
+
message: "Corp ID",
|
|
219
|
+
placeholder: "dingxxxxxxxx",
|
|
220
|
+
initialValue: resolved.corpId ?? undefined,
|
|
221
|
+
}),
|
|
222
|
+
).trim() || undefined;
|
|
223
|
+
|
|
224
|
+
agentId =
|
|
225
|
+
String(
|
|
226
|
+
await prompter.text({
|
|
227
|
+
message: "Agent ID",
|
|
228
|
+
placeholder: "123456789",
|
|
229
|
+
initialValue: resolved.agentId ? String(resolved.agentId) : undefined,
|
|
230
|
+
}),
|
|
231
|
+
).trim() || undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const wantsCardMode = await prompter.confirm({
|
|
235
|
+
message: "Enable AI interactive card mode? (for streaming AI responses)",
|
|
236
|
+
initialValue: resolved.messageType === "card",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let cardTemplateId: string | undefined;
|
|
240
|
+
let cardTemplateKey: string | undefined;
|
|
241
|
+
let messageType: "markdown" | "card" = "markdown";
|
|
242
|
+
|
|
243
|
+
if (wantsCardMode) {
|
|
244
|
+
await prompter.note(
|
|
245
|
+
[
|
|
246
|
+
"Create an AI card template in DingTalk Developer Console:",
|
|
247
|
+
"https://open-dev.dingtalk.com/fe/card",
|
|
248
|
+
"1. Go to 'My Templates' > 'Create Template'",
|
|
249
|
+
"2. Select 'AI Card' scenario",
|
|
250
|
+
"3. Design your card and publish",
|
|
251
|
+
"4. Copy the Template ID (e.g., xxx.schema)",
|
|
252
|
+
].join("\n"),
|
|
253
|
+
"Card Template Setup",
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
cardTemplateId =
|
|
257
|
+
String(
|
|
258
|
+
await prompter.text({
|
|
259
|
+
message: "Card Template ID",
|
|
260
|
+
placeholder: "xxxxx-xxxxx-xxxxx.schema",
|
|
261
|
+
initialValue: resolved.cardTemplateId ?? undefined,
|
|
262
|
+
}),
|
|
263
|
+
).trim() || undefined;
|
|
264
|
+
|
|
265
|
+
cardTemplateKey =
|
|
266
|
+
String(
|
|
267
|
+
await prompter.text({
|
|
268
|
+
message: "Card Template Key (content field name)",
|
|
269
|
+
placeholder: "msgContent",
|
|
270
|
+
initialValue: resolved.cardTemplateKey ?? "msgContent",
|
|
271
|
+
}),
|
|
272
|
+
).trim() || "msgContent";
|
|
273
|
+
|
|
274
|
+
messageType = "card";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const dmPolicyValue = await prompter.select({
|
|
278
|
+
message: "Direct message policy",
|
|
279
|
+
options: [
|
|
280
|
+
{ label: "Open - anyone can DM", value: "open" },
|
|
281
|
+
{ label: "Allowlist - only allowed users", value: "allowlist" },
|
|
282
|
+
],
|
|
283
|
+
initialValue: resolved.dmPolicy ?? "open",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
let allowFrom: string[] | undefined;
|
|
287
|
+
if (dmPolicyValue === "allowlist") {
|
|
288
|
+
const entry = await prompter.text({
|
|
289
|
+
message: "Allowed user IDs (comma-separated)",
|
|
290
|
+
placeholder: "user1, user2",
|
|
291
|
+
});
|
|
292
|
+
const parsed = parseList(String(entry ?? ""));
|
|
293
|
+
allowFrom = parsed.length > 0 ? parsed : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const groupPolicyValue = await prompter.select({
|
|
297
|
+
message: "Group message policy",
|
|
298
|
+
options: [
|
|
299
|
+
{ label: "Open - any group can use bot", value: "open" },
|
|
300
|
+
{ label: "Allowlist - only allowed groups", value: "allowlist" },
|
|
301
|
+
],
|
|
302
|
+
initialValue: resolved.groupPolicy ?? "open",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const next = applyAccountConfig({
|
|
306
|
+
cfg,
|
|
307
|
+
accountId,
|
|
308
|
+
input: {
|
|
309
|
+
clientId: String(clientId).trim(),
|
|
310
|
+
clientSecret: String(clientSecret).trim(),
|
|
311
|
+
robotCode,
|
|
312
|
+
corpId,
|
|
313
|
+
agentId,
|
|
314
|
+
dmPolicy: dmPolicyValue as "open" | "allowlist",
|
|
315
|
+
groupPolicy: groupPolicyValue as "open" | "allowlist",
|
|
316
|
+
allowFrom,
|
|
317
|
+
messageType,
|
|
318
|
+
cardTemplateId,
|
|
319
|
+
cardTemplateKey,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return { cfg: next, accountId };
|
|
324
|
+
},
|
|
325
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{
|
|
4
|
+
"path": ".."
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"path": "../../.."
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"settings": {
|
|
11
|
+
"chat.tools.terminal.autoApprove": {
|
|
12
|
+
"npm --prefix /Users/sym/Repo/openclaw/extensions/openclaw-channel-dingtalk": true,
|
|
13
|
+
"pnpm": true,
|
|
14
|
+
"/usr/bin/git": true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peer ID Registry
|
|
3
|
+
*
|
|
4
|
+
* Maps lowercased peer/session keys back to their original case-sensitive
|
|
5
|
+
* DingTalk conversationId values. DingTalk conversationIds are base64-encoded
|
|
6
|
+
* and therefore case-sensitive, but the framework may lowercase session keys
|
|
7
|
+
* internally. This registry preserves the original casing so outbound messages
|
|
8
|
+
* can be delivered correctly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const peerIdMap = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register an original peer ID, keyed by its lowercased form.
|
|
15
|
+
*/
|
|
16
|
+
export function registerPeerId(originalId: string): void {
|
|
17
|
+
if (!originalId) return;
|
|
18
|
+
peerIdMap.set(originalId.toLowerCase(), originalId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a possibly-lowercased peer ID back to its original casing.
|
|
23
|
+
* Returns the original if found, otherwise returns the input as-is.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveOriginalPeerId(id: string): string {
|
|
26
|
+
if (!id) return id;
|
|
27
|
+
return peerIdMap.get(id.toLowerCase()) || id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Clear the registry (for testing or shutdown).
|
|
32
|
+
*/
|
|
33
|
+
export function clearPeerIdRegistry(): void {
|
|
34
|
+
peerIdMap.clear();
|
|
35
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setDingTalkRuntime(next: PluginRuntime): void {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getDingTalkRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error('DingTalk runtime not initialized');
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|