@huo15/dingtalk-connector-pro 1.0.0 → 1.0.2
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/dist/hooks/init.js +16 -0
- package/hooks/init.js +18 -0
- package/package.json +25 -11
- package/CHANGELOG.md +0 -485
- package/docs/AGENT_ROUTING.md +0 -335
- package/docs/DEAP_AGENT_GUIDE.en.md +0 -115
- package/docs/DEAP_AGENT_GUIDE.md +0 -115
- package/docs/images/dingtalk.svg +0 -1
- package/docs/images/image-1.png +0 -0
- package/docs/images/image-2.png +0 -0
- package/docs/images/image-3.png +0 -0
- package/docs/images/image-4.png +0 -0
- package/docs/images/image-5.png +0 -0
- package/docs/images/image-6.png +0 -0
- package/docs/images/image-7.png +0 -0
- package/install-beta.sh +0 -438
- package/install-npm.sh +0 -167
- package/openclaw.plugin.json +0 -498
- package/src/channel.ts +0 -463
- package/src/config/accounts.ts +0 -242
- package/src/config/schema.ts +0 -148
- package/src/core/connection.ts +0 -722
- package/src/core/message-handler.ts +0 -1700
- package/src/core/provider.ts +0 -111
- package/src/core/state.ts +0 -54
- package/src/directory.ts +0 -95
- package/src/docs.ts +0 -293
- package/src/gateway-methods.ts +0 -404
- package/src/onboarding.ts +0 -413
- package/src/policy.ts +0 -32
- package/src/probe.ts +0 -212
- package/src/reply-dispatcher.ts +0 -630
- package/src/runtime.ts +0 -32
- package/src/sdk/helpers.ts +0 -322
- package/src/sdk/types.ts +0 -513
- package/src/secret-input.ts +0 -19
- package/src/services/media/audio.ts +0 -54
- package/src/services/media/chunk-upload.ts +0 -296
- package/src/services/media/common.ts +0 -155
- package/src/services/media/file.ts +0 -70
- package/src/services/media/image.ts +0 -81
- package/src/services/media/index.ts +0 -10
- package/src/services/media/video.ts +0 -162
- package/src/services/media.ts +0 -1136
- package/src/services/messaging/card.ts +0 -342
- package/src/services/messaging/index.ts +0 -17
- package/src/services/messaging/send.ts +0 -141
- package/src/services/messaging.ts +0 -1013
- package/src/targets.ts +0 -45
- package/src/types/index.ts +0 -59
- package/src/utils/agent.ts +0 -63
- package/src/utils/async.ts +0 -51
- package/src/utils/constants.ts +0 -27
- package/src/utils/http-client.ts +0 -37
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -78
- package/src/utils/session.ts +0 -147
- package/src/utils/token.ts +0 -93
- package/src/utils/utils-legacy.ts +0 -454
- package/tsconfig.json +0 -20
package/src/onboarding.ts
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
OpenClawConfig,
|
|
3
|
-
SecretInput,
|
|
4
|
-
WizardPrompter,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import type {
|
|
7
|
-
ChannelSetupWizardAdapter,
|
|
8
|
-
ChannelSetupDmPolicy,
|
|
9
|
-
DmPolicy,
|
|
10
|
-
} from "openclaw/plugin-sdk/setup";
|
|
11
|
-
import {
|
|
12
|
-
addWildcardAllowFrom,
|
|
13
|
-
DEFAULT_ACCOUNT_ID,
|
|
14
|
-
formatDocsLink,
|
|
15
|
-
hasConfiguredSecretInput,
|
|
16
|
-
} from "./sdk/helpers.ts";
|
|
17
|
-
import { promptSingleChannelSecretInput } from "openclaw/plugin-sdk/setup";
|
|
18
|
-
import { resolveDingtalkAccount, resolveDingtalkCredentials } from "./config/accounts.ts";
|
|
19
|
-
import { probeDingtalk } from "./probe.ts";
|
|
20
|
-
import type { DingtalkConfig } from "./types/index.ts";
|
|
21
|
-
|
|
22
|
-
const channel = "dingtalk-connector" as const;
|
|
23
|
-
|
|
24
|
-
function normalizeString(value: unknown): string | undefined {
|
|
25
|
-
if (typeof value === "number") {
|
|
26
|
-
return String(value);
|
|
27
|
-
}
|
|
28
|
-
if (typeof value !== "string") {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
const trimmed = value.trim();
|
|
32
|
-
return trimmed || undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function setDingtalkDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
|
36
|
-
const allowFrom =
|
|
37
|
-
dmPolicy === "open"
|
|
38
|
-
? addWildcardAllowFrom(cfg.channels?.["dingtalk-connector"]?.allowFrom)?.map((entry) => String(entry))
|
|
39
|
-
: undefined;
|
|
40
|
-
return {
|
|
41
|
-
...cfg,
|
|
42
|
-
channels: {
|
|
43
|
-
...cfg.channels,
|
|
44
|
-
"dingtalk-connector": {
|
|
45
|
-
...cfg.channels?.["dingtalk-connector"],
|
|
46
|
-
dmPolicy,
|
|
47
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function setDingtalkAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
|
54
|
-
return {
|
|
55
|
-
...cfg,
|
|
56
|
-
channels: {
|
|
57
|
-
...cfg.channels,
|
|
58
|
-
"dingtalk-connector": {
|
|
59
|
-
...cfg.channels?.["dingtalk-connector"],
|
|
60
|
-
allowFrom,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseAllowFromInput(raw: string): string[] {
|
|
67
|
-
return raw
|
|
68
|
-
.split(/[\n,;]+/g)
|
|
69
|
-
.map((entry) => entry.trim())
|
|
70
|
-
.filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function promptDingtalkAllowFrom(params: {
|
|
74
|
-
cfg: OpenClawConfig;
|
|
75
|
-
prompter: WizardPrompter;
|
|
76
|
-
}): Promise<OpenClawConfig> {
|
|
77
|
-
const existing = params.cfg.channels?.["dingtalk-connector"]?.allowFrom ?? [];
|
|
78
|
-
await params.prompter.note(
|
|
79
|
-
[
|
|
80
|
-
"Allowlist DingTalk DMs by user ID.",
|
|
81
|
-
"You can find user ID in DingTalk admin console or via API.",
|
|
82
|
-
"Examples:",
|
|
83
|
-
"- user123456",
|
|
84
|
-
"- user789012",
|
|
85
|
-
].join("\n"),
|
|
86
|
-
"DingTalk allowlist",
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
while (true) {
|
|
90
|
-
const entry = await params.prompter.text({
|
|
91
|
-
message: "DingTalk allowFrom (user IDs)",
|
|
92
|
-
placeholder: "user123456, user789012",
|
|
93
|
-
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
94
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
95
|
-
});
|
|
96
|
-
const parts = parseAllowFromInput(String(entry));
|
|
97
|
-
if (parts.length === 0) {
|
|
98
|
-
await params.prompter.note("Enter at least one user.", "DingTalk allowlist");
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const unique = [
|
|
103
|
-
...new Set([
|
|
104
|
-
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
|
|
105
|
-
...parts,
|
|
106
|
-
]),
|
|
107
|
-
];
|
|
108
|
-
return setDingtalkAllowFrom(params.cfg, unique);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function noteDingtalkCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
113
|
-
await prompter.note(
|
|
114
|
-
[
|
|
115
|
-
"1) Go to DingTalk Open Platform (open-dev.dingtalk.com)",
|
|
116
|
-
"2) Create an enterprise internal app",
|
|
117
|
-
"3) Get Client ID and Client Secret from Credentials page",
|
|
118
|
-
"4) Enable required permissions: im:message, im:chat",
|
|
119
|
-
"5) Publish the app or add it to a test group",
|
|
120
|
-
"Tip: you can also set DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET env vars.",
|
|
121
|
-
`Docs: ${formatDocsLink("/channels/dingtalk-connector", "dingtalk-connector")}`,
|
|
122
|
-
].join("\n"),
|
|
123
|
-
"DingTalk credentials",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function promptDingtalkClientId(params: {
|
|
128
|
-
prompter: WizardPrompter;
|
|
129
|
-
initialValue?: string;
|
|
130
|
-
}): Promise<string> {
|
|
131
|
-
const clientId = String(
|
|
132
|
-
await params.prompter.text({
|
|
133
|
-
message: "Enter DingTalk Client ID",
|
|
134
|
-
initialValue: params.initialValue,
|
|
135
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
136
|
-
}),
|
|
137
|
-
).trim();
|
|
138
|
-
return clientId;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function setDingtalkGroupPolicy(
|
|
142
|
-
cfg: OpenClawConfig,
|
|
143
|
-
groupPolicy: "open" | "allowlist" | "disabled",
|
|
144
|
-
): OpenClawConfig {
|
|
145
|
-
return {
|
|
146
|
-
...cfg,
|
|
147
|
-
channels: {
|
|
148
|
-
...cfg.channels,
|
|
149
|
-
"dingtalk-connector": {
|
|
150
|
-
...cfg.channels?.["dingtalk-connector"],
|
|
151
|
-
enabled: true,
|
|
152
|
-
groupPolicy,
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function setDingtalkGroupAllowFrom(cfg: OpenClawConfig, groupAllowFrom: string[]): OpenClawConfig {
|
|
159
|
-
return {
|
|
160
|
-
...cfg,
|
|
161
|
-
channels: {
|
|
162
|
-
...cfg.channels,
|
|
163
|
-
"dingtalk-connector": {
|
|
164
|
-
...cfg.channels?.["dingtalk-connector"],
|
|
165
|
-
groupAllowFrom,
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const dmPolicy: ChannelSetupDmPolicy = {
|
|
172
|
-
label: "DingTalk",
|
|
173
|
-
channel,
|
|
174
|
-
policyKey: "channels.dingtalk-connector.dmPolicy",
|
|
175
|
-
allowFromKey: "channels.dingtalk-connector.allowFrom",
|
|
176
|
-
getCurrent: (cfg) => (cfg.channels?.["dingtalk-connector"] as DingtalkConfig | undefined)?.dmPolicy ?? "open",
|
|
177
|
-
setPolicy: (cfg, policy) => setDingtalkDmPolicy(cfg, policy),
|
|
178
|
-
promptAllowFrom: promptDingtalkAllowFrom,
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
export const dingtalkOnboardingAdapter: ChannelSetupWizardAdapter = {
|
|
182
|
-
channel,
|
|
183
|
-
getStatus: async ({ cfg }) => {
|
|
184
|
-
// Use resolveDingtalkAccount to correctly support pure multi-account configs
|
|
185
|
-
// where credentials are only under accounts.<id>, not at the top level.
|
|
186
|
-
const defaultAccount = resolveDingtalkAccount({ cfg });
|
|
187
|
-
const configured = defaultAccount.configured;
|
|
188
|
-
|
|
189
|
-
let probeResult = null;
|
|
190
|
-
if (configured && defaultAccount.clientId && defaultAccount.clientSecret) {
|
|
191
|
-
try {
|
|
192
|
-
probeResult = await probeDingtalk({
|
|
193
|
-
clientId: defaultAccount.clientId,
|
|
194
|
-
clientSecret: defaultAccount.clientSecret,
|
|
195
|
-
});
|
|
196
|
-
} catch {
|
|
197
|
-
// Ignore probe errors
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const statusLines: string[] = [];
|
|
202
|
-
if (!configured) {
|
|
203
|
-
statusLines.push("DingTalk: needs app credentials");
|
|
204
|
-
} else if (probeResult?.ok) {
|
|
205
|
-
statusLines.push(
|
|
206
|
-
`DingTalk: connected as ${probeResult.botName ?? "bot"}`,
|
|
207
|
-
);
|
|
208
|
-
} else {
|
|
209
|
-
statusLines.push("DingTalk: configured (connection not verified)");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
channel,
|
|
214
|
-
configured,
|
|
215
|
-
statusLines,
|
|
216
|
-
selectionHint: configured ? "configured" : "needs app creds",
|
|
217
|
-
quickstartScore: configured ? 2 : 0,
|
|
218
|
-
};
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
configure: async ({ cfg, prompter }) => {
|
|
222
|
-
const dingtalkCfg = cfg.channels?.["dingtalk-connector"] as DingtalkConfig | undefined;
|
|
223
|
-
const resolved = resolveDingtalkCredentials(dingtalkCfg, {
|
|
224
|
-
allowUnresolvedSecretRef: true,
|
|
225
|
-
});
|
|
226
|
-
const hasConfigSecret = hasConfiguredSecretInput(dingtalkCfg?.clientSecret);
|
|
227
|
-
const hasConfigCreds = Boolean(
|
|
228
|
-
typeof dingtalkCfg?.clientId === "string" && dingtalkCfg.clientId.trim() && hasConfigSecret,
|
|
229
|
-
);
|
|
230
|
-
let canUseEnv = Boolean(
|
|
231
|
-
!hasConfigCreds && process.env.DINGTALK_CLIENT_ID?.trim() && process.env.DINGTALK_CLIENT_SECRET?.trim(),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
let next = cfg;
|
|
235
|
-
let clientId: string | null = null;
|
|
236
|
-
let clientSecret: SecretInput | null = null;
|
|
237
|
-
let clientSecretProbeValue: string | null = null;
|
|
238
|
-
|
|
239
|
-
if (!resolved) {
|
|
240
|
-
await noteDingtalkCredentialHelp(prompter);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Check if we can use environment variables
|
|
244
|
-
if (canUseEnv) {
|
|
245
|
-
const useEnv = await prompter.confirm({
|
|
246
|
-
message: "DINGTALK_CLIENT_ID + DINGTALK_CLIENT_SECRET detected. Use env vars?",
|
|
247
|
-
initialValue: true,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
if (useEnv) {
|
|
251
|
-
next = {
|
|
252
|
-
...next,
|
|
253
|
-
channels: {
|
|
254
|
-
...next.channels,
|
|
255
|
-
"dingtalk-connector": { ...next.channels?.["dingtalk-connector"], enabled: true },
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
// Environment variables will be used, skip manual input
|
|
259
|
-
} else {
|
|
260
|
-
// User chose not to use env vars, proceed to manual input
|
|
261
|
-
canUseEnv = false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// If not using env vars, prompt for credentials
|
|
266
|
-
if (!canUseEnv) {
|
|
267
|
-
// Check if we should keep existing configuration
|
|
268
|
-
if (resolved && hasConfigSecret) {
|
|
269
|
-
const keepExisting = await prompter.confirm({
|
|
270
|
-
message: "DingTalk credentials already configured. Keep them?",
|
|
271
|
-
initialValue: true,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
if (!keepExisting) {
|
|
275
|
-
// User wants to reconfigure, proceed to input
|
|
276
|
-
// Step 1: Prompt for Client ID first
|
|
277
|
-
clientId = await promptDingtalkClientId({
|
|
278
|
-
prompter,
|
|
279
|
-
initialValue:
|
|
280
|
-
normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Step 2: Then prompt for Client Secret
|
|
284
|
-
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
285
|
-
cfg: next,
|
|
286
|
-
prompter,
|
|
287
|
-
providerHint: "dingtalk",
|
|
288
|
-
credentialLabel: "Client Secret",
|
|
289
|
-
accountConfigured: false, // Force new input
|
|
290
|
-
canUseEnv: false, // Already handled above
|
|
291
|
-
hasConfigToken: false, // Force new input
|
|
292
|
-
envPrompt: "", // Not used
|
|
293
|
-
keepPrompt: "", // Not used
|
|
294
|
-
inputPrompt: "Enter DingTalk Client Secret",
|
|
295
|
-
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
if (clientSecretResult.action === "set") {
|
|
299
|
-
clientSecret = clientSecretResult.value;
|
|
300
|
-
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// If keepExisting is true, we don't modify anything
|
|
304
|
-
} else {
|
|
305
|
-
// No existing config, prompt for new credentials
|
|
306
|
-
// Step 1: Prompt for Client ID first
|
|
307
|
-
clientId = await promptDingtalkClientId({
|
|
308
|
-
prompter,
|
|
309
|
-
initialValue:
|
|
310
|
-
normalizeString(dingtalkCfg?.clientId) ?? normalizeString(process.env.DINGTALK_CLIENT_ID),
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Step 2: Then prompt for Client Secret
|
|
314
|
-
const clientSecretResult = await promptSingleChannelSecretInput({
|
|
315
|
-
cfg: next,
|
|
316
|
-
prompter,
|
|
317
|
-
providerHint: "dingtalk",
|
|
318
|
-
credentialLabel: "Client Secret",
|
|
319
|
-
accountConfigured: false,
|
|
320
|
-
canUseEnv: false,
|
|
321
|
-
hasConfigToken: false,
|
|
322
|
-
envPrompt: "",
|
|
323
|
-
keepPrompt: "",
|
|
324
|
-
inputPrompt: "Enter DingTalk Client Secret",
|
|
325
|
-
preferredEnvVar: "DINGTALK_CLIENT_SECRET",
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (clientSecretResult.action === "set") {
|
|
329
|
-
clientSecret = clientSecretResult.value;
|
|
330
|
-
clientSecretProbeValue = clientSecretResult.resolvedValue;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (clientId && clientSecret) {
|
|
336
|
-
next = {
|
|
337
|
-
...next,
|
|
338
|
-
channels: {
|
|
339
|
-
...next.channels,
|
|
340
|
-
"dingtalk-connector": {
|
|
341
|
-
...next.channels?.["dingtalk-connector"],
|
|
342
|
-
enabled: true,
|
|
343
|
-
clientId,
|
|
344
|
-
clientSecret,
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// Test connection
|
|
350
|
-
try {
|
|
351
|
-
const probe = await probeDingtalk({
|
|
352
|
-
clientId,
|
|
353
|
-
clientSecret: clientSecretProbeValue ?? undefined,
|
|
354
|
-
});
|
|
355
|
-
if (probe.ok) {
|
|
356
|
-
await prompter.note(
|
|
357
|
-
`Connected as ${probe.botName ?? "bot"}`,
|
|
358
|
-
"DingTalk connection test",
|
|
359
|
-
);
|
|
360
|
-
} else {
|
|
361
|
-
await prompter.note(
|
|
362
|
-
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
363
|
-
"DingTalk connection test",
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
} catch (err) {
|
|
367
|
-
await prompter.note(`Connection test failed: ${String(err)}`, "DingTalk connection test");
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Group policy
|
|
372
|
-
const groupPolicy = await prompter.select({
|
|
373
|
-
message: "Group chat policy",
|
|
374
|
-
options: [
|
|
375
|
-
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
376
|
-
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
377
|
-
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
378
|
-
],
|
|
379
|
-
initialValue: (next.channels?.["dingtalk-connector"] as DingtalkConfig | undefined)?.groupPolicy ?? "open",
|
|
380
|
-
});
|
|
381
|
-
if (groupPolicy) {
|
|
382
|
-
next = setDingtalkGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Group allowlist if needed
|
|
386
|
-
if (groupPolicy === "allowlist") {
|
|
387
|
-
const existing = (next.channels?.["dingtalk-connector"] as DingtalkConfig | undefined)?.groupAllowFrom ?? [];
|
|
388
|
-
const entry = await prompter.text({
|
|
389
|
-
message: "Group chat allowlist (conversation IDs)",
|
|
390
|
-
placeholder: "cidxxxx, cidyyyy",
|
|
391
|
-
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
392
|
-
});
|
|
393
|
-
if (entry) {
|
|
394
|
-
const parts = parseAllowFromInput(String(entry));
|
|
395
|
-
if (parts.length > 0) {
|
|
396
|
-
next = setDingtalkGroupAllowFrom(next, parts);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
dmPolicy,
|
|
405
|
-
|
|
406
|
-
disable: (cfg) => ({
|
|
407
|
-
...cfg,
|
|
408
|
-
channels: {
|
|
409
|
-
...cfg.channels,
|
|
410
|
-
"dingtalk-connector": { ...cfg.channels?.["dingtalk-connector"], enabled: false },
|
|
411
|
-
},
|
|
412
|
-
}),
|
|
413
|
-
};
|
package/src/policy.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// 类型定义
|
|
2
|
-
interface ClawdbotConfig {
|
|
3
|
-
[key: string]: any;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
interface ToolPolicy {
|
|
7
|
-
allow?: string[];
|
|
8
|
-
deny?: string[];
|
|
9
|
-
}
|
|
10
|
-
import { resolveDingtalkAccount } from "./config/accounts.ts";
|
|
11
|
-
|
|
12
|
-
export function resolveDingtalkGroupToolPolicy(params: {
|
|
13
|
-
cfg: ClawdbotConfig;
|
|
14
|
-
groupId?: string | null;
|
|
15
|
-
accountId?: string | null;
|
|
16
|
-
}): ToolPolicy | undefined {
|
|
17
|
-
const { cfg, groupId, accountId } = params;
|
|
18
|
-
|
|
19
|
-
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
20
|
-
const dingtalkCfg = account.config;
|
|
21
|
-
|
|
22
|
-
// Check group-specific policy first
|
|
23
|
-
if (groupId) {
|
|
24
|
-
const groupConfig = dingtalkCfg?.groups?.[groupId];
|
|
25
|
-
if (groupConfig?.tools) {
|
|
26
|
-
return groupConfig.tools;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fall back to account-level default (allow all)
|
|
31
|
-
return { allow: ["*"] };
|
|
32
|
-
}
|
package/src/probe.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { raceWithTimeoutAndAbort } from "./utils/async.ts";
|
|
2
|
-
import type { DingtalkProbeResult } from "./types/index.ts";
|
|
3
|
-
|
|
4
|
-
/** LRU Cache for probe results to reduce repeated health-check calls. */
|
|
5
|
-
class LRUCache<K, V> {
|
|
6
|
-
private cache = new Map<K, V>();
|
|
7
|
-
private maxSize: number;
|
|
8
|
-
|
|
9
|
-
constructor(maxSize: number) {
|
|
10
|
-
this.maxSize = maxSize;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
get(key: K): V | undefined {
|
|
14
|
-
const value = this.cache.get(key);
|
|
15
|
-
if (value !== undefined) {
|
|
16
|
-
// 重新插入以更新访问顺序
|
|
17
|
-
this.cache.delete(key);
|
|
18
|
-
this.cache.set(key, value);
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
set(key: K, value: V): void {
|
|
24
|
-
// 如果已存在,先删除(更新顺序)
|
|
25
|
-
if (this.cache.has(key)) {
|
|
26
|
-
this.cache.delete(key);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
this.cache.set(key, value);
|
|
30
|
-
|
|
31
|
-
// 超过大小限制时删除最旧的(最少使用的)
|
|
32
|
-
if (this.cache.size > this.maxSize) {
|
|
33
|
-
const oldest = this.cache.keys().next().value;
|
|
34
|
-
if (oldest !== undefined) {
|
|
35
|
-
this.cache.delete(oldest);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
clear(): void {
|
|
41
|
-
this.cache.clear();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const probeCache = new LRUCache<string, { result: DingtalkProbeResult; expiresAt: number }>(64);
|
|
46
|
-
const PROBE_SUCCESS_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
47
|
-
const PROBE_ERROR_TTL_MS = 60 * 1000; // 1 minute
|
|
48
|
-
export const DINGTALK_PROBE_REQUEST_TIMEOUT_MS = 10_000;
|
|
49
|
-
export type ProbeDingtalkOptions = {
|
|
50
|
-
timeoutMs?: number;
|
|
51
|
-
abortSignal?: AbortSignal;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type DingtalkBotInfoResponse = {
|
|
55
|
-
errcode?: number;
|
|
56
|
-
errmsg?: string;
|
|
57
|
-
nick?: string;
|
|
58
|
-
unionid?: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function setCachedProbeResult(
|
|
62
|
-
cacheKey: string,
|
|
63
|
-
result: DingtalkProbeResult,
|
|
64
|
-
ttlMs: number,
|
|
65
|
-
): DingtalkProbeResult {
|
|
66
|
-
probeCache.set(cacheKey, { result, expiresAt: Date.now() + ttlMs });
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function probeDingtalk(
|
|
71
|
-
creds?: { clientId: string; clientSecret: string; accountId?: string },
|
|
72
|
-
options: ProbeDingtalkOptions = {},
|
|
73
|
-
): Promise<DingtalkProbeResult> {
|
|
74
|
-
if (!creds?.clientId || !creds?.clientSecret) {
|
|
75
|
-
return {
|
|
76
|
-
ok: false,
|
|
77
|
-
error: "missing credentials (clientId, clientSecret)",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (options.abortSignal?.aborted) {
|
|
81
|
-
return {
|
|
82
|
-
ok: false,
|
|
83
|
-
clientId: creds.clientId,
|
|
84
|
-
error: "probe aborted",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const timeoutMs = options.timeoutMs ?? DINGTALK_PROBE_REQUEST_TIMEOUT_MS;
|
|
89
|
-
|
|
90
|
-
// Return cached result if still valid.
|
|
91
|
-
const cacheKey = creds.accountId ?? `${creds.clientId}:${creds.clientSecret.slice(0, 8)}`;
|
|
92
|
-
const cached = probeCache.get(cacheKey);
|
|
93
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
94
|
-
return cached.result;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
// Get access token
|
|
99
|
-
const tokenResponse = await raceWithTimeoutAndAbort(
|
|
100
|
-
fetch("https://api.dingtalk.com/v1.0/oauth2/accessToken", {
|
|
101
|
-
method: "POST",
|
|
102
|
-
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify({
|
|
104
|
-
appKey: creds.clientId,
|
|
105
|
-
appSecret: creds.clientSecret,
|
|
106
|
-
}),
|
|
107
|
-
}),
|
|
108
|
-
{ timeoutMs, abortSignal: options.abortSignal },
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
if (tokenResponse.status === "aborted") {
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
clientId: creds.clientId,
|
|
115
|
-
error: "probe aborted",
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (tokenResponse.status === "timeout") {
|
|
119
|
-
return setCachedProbeResult(
|
|
120
|
-
cacheKey,
|
|
121
|
-
{
|
|
122
|
-
ok: false,
|
|
123
|
-
clientId: creds.clientId,
|
|
124
|
-
error: `probe timed out after ${timeoutMs}ms`,
|
|
125
|
-
},
|
|
126
|
-
PROBE_ERROR_TTL_MS,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const tokenData = await tokenResponse.value.json() as { accessToken?: string };
|
|
131
|
-
if (!tokenData.accessToken) {
|
|
132
|
-
return setCachedProbeResult(
|
|
133
|
-
cacheKey,
|
|
134
|
-
{
|
|
135
|
-
ok: false,
|
|
136
|
-
clientId: creds.clientId,
|
|
137
|
-
error: "failed to get access token",
|
|
138
|
-
},
|
|
139
|
-
PROBE_ERROR_TTL_MS,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Get bot info
|
|
144
|
-
const botResponse = await raceWithTimeoutAndAbort(
|
|
145
|
-
fetch(`https://api.dingtalk.com/v1.0/contact/users/me`, {
|
|
146
|
-
method: "GET",
|
|
147
|
-
headers: {
|
|
148
|
-
"x-acs-dingtalk-access-token": tokenData.accessToken,
|
|
149
|
-
"Content-Type": "application/json",
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
{ timeoutMs, abortSignal: options.abortSignal },
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
if (botResponse.status === "aborted") {
|
|
156
|
-
return {
|
|
157
|
-
ok: false,
|
|
158
|
-
clientId: creds.clientId,
|
|
159
|
-
error: "probe aborted",
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
if (botResponse.status === "timeout") {
|
|
163
|
-
return setCachedProbeResult(
|
|
164
|
-
cacheKey,
|
|
165
|
-
{
|
|
166
|
-
ok: false,
|
|
167
|
-
clientId: creds.clientId,
|
|
168
|
-
error: `probe timed out after ${timeoutMs}ms`,
|
|
169
|
-
},
|
|
170
|
-
PROBE_ERROR_TTL_MS,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const botData = await botResponse.value.json() as DingtalkBotInfoResponse;
|
|
175
|
-
if (botData.errcode && botData.errcode !== 0) {
|
|
176
|
-
return setCachedProbeResult(
|
|
177
|
-
cacheKey,
|
|
178
|
-
{
|
|
179
|
-
ok: false,
|
|
180
|
-
clientId: creds.clientId,
|
|
181
|
-
error: `API error: ${botData.errmsg || `code ${botData.errcode}`}`,
|
|
182
|
-
},
|
|
183
|
-
PROBE_ERROR_TTL_MS,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return setCachedProbeResult(
|
|
188
|
-
cacheKey,
|
|
189
|
-
{
|
|
190
|
-
ok: true,
|
|
191
|
-
clientId: creds.clientId,
|
|
192
|
-
botName: botData.nick,
|
|
193
|
-
},
|
|
194
|
-
PROBE_SUCCESS_TTL_MS,
|
|
195
|
-
);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
return setCachedProbeResult(
|
|
198
|
-
cacheKey,
|
|
199
|
-
{
|
|
200
|
-
ok: false,
|
|
201
|
-
clientId: creds.clientId,
|
|
202
|
-
error: err instanceof Error ? err.message : String(err),
|
|
203
|
-
},
|
|
204
|
-
PROBE_ERROR_TTL_MS,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Clear the probe cache (for testing). */
|
|
210
|
-
export function clearProbeCache(): void {
|
|
211
|
-
probeCache.clear();
|
|
212
|
-
}
|