@huo15/dingtalk-connector-pro 1.0.5 → 1.0.7

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