@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.
Files changed (60) hide show
  1. package/dist/hooks/init.js +16 -0
  2. package/hooks/init.js +18 -0
  3. package/package.json +25 -11
  4. package/CHANGELOG.md +0 -485
  5. package/docs/AGENT_ROUTING.md +0 -335
  6. package/docs/DEAP_AGENT_GUIDE.en.md +0 -115
  7. package/docs/DEAP_AGENT_GUIDE.md +0 -115
  8. package/docs/images/dingtalk.svg +0 -1
  9. package/docs/images/image-1.png +0 -0
  10. package/docs/images/image-2.png +0 -0
  11. package/docs/images/image-3.png +0 -0
  12. package/docs/images/image-4.png +0 -0
  13. package/docs/images/image-5.png +0 -0
  14. package/docs/images/image-6.png +0 -0
  15. package/docs/images/image-7.png +0 -0
  16. package/install-beta.sh +0 -438
  17. package/install-npm.sh +0 -167
  18. package/openclaw.plugin.json +0 -498
  19. package/src/channel.ts +0 -463
  20. package/src/config/accounts.ts +0 -242
  21. package/src/config/schema.ts +0 -148
  22. package/src/core/connection.ts +0 -722
  23. package/src/core/message-handler.ts +0 -1700
  24. package/src/core/provider.ts +0 -111
  25. package/src/core/state.ts +0 -54
  26. package/src/directory.ts +0 -95
  27. package/src/docs.ts +0 -293
  28. package/src/gateway-methods.ts +0 -404
  29. package/src/onboarding.ts +0 -413
  30. package/src/policy.ts +0 -32
  31. package/src/probe.ts +0 -212
  32. package/src/reply-dispatcher.ts +0 -630
  33. package/src/runtime.ts +0 -32
  34. package/src/sdk/helpers.ts +0 -322
  35. package/src/sdk/types.ts +0 -513
  36. package/src/secret-input.ts +0 -19
  37. package/src/services/media/audio.ts +0 -54
  38. package/src/services/media/chunk-upload.ts +0 -296
  39. package/src/services/media/common.ts +0 -155
  40. package/src/services/media/file.ts +0 -70
  41. package/src/services/media/image.ts +0 -81
  42. package/src/services/media/index.ts +0 -10
  43. package/src/services/media/video.ts +0 -162
  44. package/src/services/media.ts +0 -1136
  45. package/src/services/messaging/card.ts +0 -342
  46. package/src/services/messaging/index.ts +0 -17
  47. package/src/services/messaging/send.ts +0 -141
  48. package/src/services/messaging.ts +0 -1013
  49. package/src/targets.ts +0 -45
  50. package/src/types/index.ts +0 -59
  51. package/src/utils/agent.ts +0 -63
  52. package/src/utils/async.ts +0 -51
  53. package/src/utils/constants.ts +0 -27
  54. package/src/utils/http-client.ts +0 -37
  55. package/src/utils/index.ts +0 -8
  56. package/src/utils/logger.ts +0 -78
  57. package/src/utils/session.ts +0 -147
  58. package/src/utils/token.ts +0 -93
  59. package/src/utils/utils-legacy.ts +0 -454
  60. 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
- }