@huo15/dingtalk-connector-pro 1.0.4 → 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 (142) 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/src/hooks/init.ts +0 -16
  142. package/tsconfig.json +0 -20
@@ -0,0 +1,415 @@
1
+ import { buildChannelConfigSchema, } from "openclaw/plugin-sdk/core";
2
+ import { createDefaultChannelRuntimeState, DEFAULT_ACCOUNT_ID, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, } from "./sdk/helpers.js";
3
+ import { DingtalkConfigBaseSchema } from "./config/schema.js";
4
+ import { createLogger } from "./utils/logger.js";
5
+ import { resolveDingtalkAccount, listDingtalkAccountIds, resolveDefaultDingtalkAccountId, } from "./config/accounts.js";
6
+ import { listDingtalkDirectoryPeers, listDingtalkDirectoryGroups, listDingtalkDirectoryPeersLive, listDingtalkDirectoryGroupsLive, } from "./directory.js";
7
+ import { resolveDingtalkGroupToolPolicy } from "./policy.js";
8
+ import { probeDingtalk } from "./probe.js";
9
+ import { normalizeDingtalkTarget, looksLikeDingtalkId } from "./targets.js";
10
+ import { dingtalkOnboardingAdapter } from "./onboarding.js";
11
+ import { monitorDingtalkProvider } from "./core/provider.js";
12
+ import { sendTextToDingTalk, sendMediaToDingTalk } from "./services/messaging/index.js";
13
+ const meta = {
14
+ id: "dingtalk-connector",
15
+ label: "DingTalk",
16
+ selectionLabel: "DingTalk (钉钉)",
17
+ docsPath: "/channels/dingtalk-connector",
18
+ docsLabel: "dingtalk-connector",
19
+ blurb: "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP,支持 AI Card 流式响应。",
20
+ aliases: ["dd", "ding"],
21
+ order: 70,
22
+ };
23
+ export const dingtalkPlugin = {
24
+ id: "dingtalk-connector",
25
+ meta: {
26
+ ...meta,
27
+ },
28
+ pairing: {
29
+ idLabel: "dingtalkUserId",
30
+ normalizeAllowEntry: (entry) => entry.replace(/^(dingtalk|user|dd):/i, ""),
31
+ notifyApproval: async ({ cfg, id }) => {
32
+ // TODO: Implement notification when pairing is approved
33
+ const logger = createLogger(false, 'DingTalk:Pairing');
34
+ logger.info(`Pairing approved for user: ${id}`);
35
+ },
36
+ },
37
+ capabilities: {
38
+ chatTypes: ["direct", "group"],
39
+ polls: false,
40
+ threads: false,
41
+ media: true, // ✅ 启用媒体支持
42
+ reactions: false,
43
+ edit: false,
44
+ reply: false,
45
+ },
46
+ agentPrompt: {
47
+ messageToolHints: () => [
48
+ "- DingTalk targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:userId` or `group:conversationId`.",
49
+ "- DingTalk supports interactive cards for rich messages.",
50
+ ],
51
+ },
52
+ groups: {
53
+ resolveToolPolicy: resolveDingtalkGroupToolPolicy,
54
+ },
55
+ mentions: {
56
+ stripPatterns: () => ['@[^\\s]+'], // Strip @mentions
57
+ },
58
+ reload: { configPrefixes: ["channels.dingtalk-connector"] },
59
+ configSchema: buildChannelConfigSchema(DingtalkConfigBaseSchema),
60
+ config: {
61
+ listAccountIds: (cfg) => listDingtalkAccountIds(cfg),
62
+ resolveAccount: (cfg, accountId) => resolveDingtalkAccount({ cfg, accountId }),
63
+ defaultAccountId: (cfg) => resolveDefaultDingtalkAccountId(cfg),
64
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
65
+ const account = resolveDingtalkAccount({ cfg, accountId });
66
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
67
+ if (isDefault) {
68
+ // For default account, set top-level enabled
69
+ return {
70
+ ...cfg,
71
+ channels: {
72
+ ...cfg.channels,
73
+ "dingtalk-connector": {
74
+ ...cfg.channels?.["dingtalk-connector"],
75
+ enabled,
76
+ },
77
+ },
78
+ };
79
+ }
80
+ // For named accounts, set enabled in accounts[accountId]
81
+ const dingtalkCfg = cfg.channels?.["dingtalk-connector"];
82
+ return {
83
+ ...cfg,
84
+ channels: {
85
+ ...cfg.channels,
86
+ "dingtalk-connector": {
87
+ ...dingtalkCfg,
88
+ accounts: {
89
+ ...dingtalkCfg?.accounts,
90
+ [accountId]: {
91
+ ...dingtalkCfg?.accounts?.[accountId],
92
+ enabled,
93
+ },
94
+ },
95
+ },
96
+ },
97
+ };
98
+ },
99
+ deleteAccount: ({ cfg, accountId }) => {
100
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
101
+ if (isDefault) {
102
+ // Delete entire dingtalk-connector config
103
+ const next = { ...cfg };
104
+ const nextChannels = { ...cfg.channels };
105
+ delete nextChannels["dingtalk-connector"];
106
+ if (Object.keys(nextChannels).length > 0) {
107
+ next.channels = nextChannels;
108
+ }
109
+ else {
110
+ delete next.channels;
111
+ }
112
+ return next;
113
+ }
114
+ // Delete specific account from accounts
115
+ const dingtalkCfg = cfg.channels?.["dingtalk-connector"];
116
+ const accounts = { ...dingtalkCfg?.accounts };
117
+ delete accounts[accountId];
118
+ return {
119
+ ...cfg,
120
+ channels: {
121
+ ...cfg.channels,
122
+ "dingtalk-connector": {
123
+ ...dingtalkCfg,
124
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
125
+ },
126
+ },
127
+ };
128
+ },
129
+ isConfigured: (account) => account.configured,
130
+ describeAccount: (account) => ({
131
+ accountId: account.accountId,
132
+ enabled: account.enabled,
133
+ configured: account.configured,
134
+ name: account.name,
135
+ clientId: account.clientId,
136
+ }),
137
+ resolveAllowFrom: ({ cfg, accountId }) => {
138
+ const account = resolveDingtalkAccount({ cfg, accountId });
139
+ return (account.config?.allowFrom ?? []).map((entry) => String(entry));
140
+ },
141
+ formatAllowFrom: ({ allowFrom }) => allowFrom
142
+ .map((entry) => String(entry).trim())
143
+ .filter(Boolean)
144
+ .map((entry) => entry.toLowerCase()),
145
+ },
146
+ security: {
147
+ collectWarnings: ({ cfg, accountId }) => {
148
+ const account = resolveDingtalkAccount({ cfg, accountId });
149
+ const dingtalkCfg = account.config;
150
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
151
+ const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
152
+ providerConfigPresent: cfg.channels?.["dingtalk-connector"] !== undefined,
153
+ groupPolicy: dingtalkCfg?.groupPolicy,
154
+ defaultGroupPolicy,
155
+ });
156
+ if (groupPolicy !== "open")
157
+ return [];
158
+ return [
159
+ `- DingTalk[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.dingtalk-connector.groupPolicy="allowlist" + channels.dingtalk-connector.groupAllowFrom to restrict senders.`,
160
+ ];
161
+ },
162
+ },
163
+ setup: {
164
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
165
+ applyAccountConfig: ({ cfg, accountId }) => {
166
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
167
+ if (isDefault) {
168
+ return {
169
+ ...cfg,
170
+ channels: {
171
+ ...cfg.channels,
172
+ "dingtalk-connector": {
173
+ ...cfg.channels?.["dingtalk-connector"],
174
+ enabled: true,
175
+ },
176
+ },
177
+ };
178
+ }
179
+ const dingtalkCfg = cfg.channels?.["dingtalk-connector"];
180
+ return {
181
+ ...cfg,
182
+ channels: {
183
+ ...cfg.channels,
184
+ "dingtalk-connector": {
185
+ ...dingtalkCfg,
186
+ accounts: {
187
+ ...dingtalkCfg?.accounts,
188
+ [accountId]: {
189
+ ...dingtalkCfg?.accounts?.[accountId],
190
+ enabled: true,
191
+ },
192
+ },
193
+ },
194
+ },
195
+ };
196
+ },
197
+ },
198
+ setupWizard: dingtalkOnboardingAdapter,
199
+ messaging: {
200
+ normalizeTarget: (raw) => normalizeDingtalkTarget(raw) ?? undefined,
201
+ targetResolver: {
202
+ looksLikeId: looksLikeDingtalkId,
203
+ hint: "<userId|user:userId|group:conversationId>",
204
+ },
205
+ },
206
+ directory: {
207
+ self: async () => null,
208
+ listPeers: async ({ cfg, query, limit, accountId }) => listDingtalkDirectoryPeers({
209
+ cfg,
210
+ query: query ?? undefined,
211
+ limit: limit ?? undefined,
212
+ accountId: accountId ?? undefined,
213
+ }),
214
+ listGroups: async ({ cfg, query, limit, accountId }) => listDingtalkDirectoryGroups({
215
+ cfg,
216
+ query: query ?? undefined,
217
+ limit: limit ?? undefined,
218
+ accountId: accountId ?? undefined,
219
+ }),
220
+ listPeersLive: async ({ cfg, query, limit, accountId }) => listDingtalkDirectoryPeersLive({
221
+ cfg,
222
+ query: query ?? undefined,
223
+ limit: limit ?? undefined,
224
+ accountId: accountId ?? undefined,
225
+ }),
226
+ listGroupsLive: async ({ cfg, query, limit, accountId }) => listDingtalkDirectoryGroupsLive({
227
+ cfg,
228
+ query: query ?? undefined,
229
+ limit: limit ?? undefined,
230
+ accountId: accountId ?? undefined,
231
+ }),
232
+ },
233
+ outbound: {
234
+ deliveryMode: "direct",
235
+ chunker: (text, limit) => {
236
+ // Simple markdown chunking - split by newlines
237
+ const chunks = [];
238
+ const lines = text.split("\n");
239
+ let currentChunk = "";
240
+ for (const line of lines) {
241
+ const testChunk = currentChunk + (currentChunk ? "\n" : "") + line;
242
+ if (testChunk.length <= limit) {
243
+ currentChunk = testChunk;
244
+ }
245
+ else {
246
+ if (currentChunk)
247
+ chunks.push(currentChunk);
248
+ currentChunk = line;
249
+ }
250
+ }
251
+ if (currentChunk)
252
+ chunks.push(currentChunk);
253
+ return chunks;
254
+ },
255
+ chunkerMode: "markdown",
256
+ textChunkLimit: 2000,
257
+ sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
258
+ const account = resolveDingtalkAccount({ cfg, accountId });
259
+ const result = await sendTextToDingTalk({
260
+ config: account.config,
261
+ target: to,
262
+ text,
263
+ replyToId,
264
+ });
265
+ return {
266
+ channel: "dingtalk-connector",
267
+ messageId: result.processQueryKey ?? result.cardInstanceId ?? "unknown",
268
+ conversationId: to,
269
+ };
270
+ },
271
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots, replyToId, threadId }) => {
272
+ const account = resolveDingtalkAccount({ cfg, accountId });
273
+ const logger = createLogger(account.config?.debug ?? false, 'DingTalk:SendMedia');
274
+ logger.info('开始处理,参数:', JSON.stringify({
275
+ to,
276
+ text,
277
+ mediaUrl,
278
+ accountId,
279
+ replyToId,
280
+ threadId,
281
+ toType: typeof to,
282
+ mediaUrlType: typeof mediaUrl,
283
+ }));
284
+ // 参数校验
285
+ if (!to || typeof to !== 'string') {
286
+ throw new Error(`Invalid 'to' parameter: ${to}`);
287
+ }
288
+ if (!mediaUrl || typeof mediaUrl !== 'string') {
289
+ throw new Error(`Invalid 'mediaUrl' parameter: ${mediaUrl}`);
290
+ }
291
+ const result = await sendMediaToDingTalk({
292
+ config: account.config,
293
+ target: to,
294
+ text,
295
+ mediaUrl,
296
+ replyToId,
297
+ });
298
+ logger.info('sendMediaToDingTalk 返回结果:', JSON.stringify({
299
+ ok: result.ok,
300
+ error: result.error,
301
+ hasProcessQueryKey: !!result.processQueryKey,
302
+ hasCardInstanceId: !!result.cardInstanceId,
303
+ }));
304
+ return {
305
+ channel: "dingtalk-connector",
306
+ messageId: result.processQueryKey ?? result.cardInstanceId ?? "unknown",
307
+ conversationId: to,
308
+ };
309
+ },
310
+ },
311
+ status: {
312
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
313
+ buildChannelSummary: ({ snapshot }) => ({
314
+ // 只返回 probe 相关字段,不透传运行时字段(running/lastStartAt 等)。
315
+ // 运行时状态由框架从 store.runtimes 自动维护,buildChannelSummary 在 probe
316
+ // 流程中被调用时 runtime 为 undefined,透传会导致 lastStartAt 永远是 null。
317
+ configured: snapshot.configured ?? false,
318
+ port: snapshot.port ?? null,
319
+ probe: snapshot.probe,
320
+ lastProbeAt: snapshot.lastProbeAt ?? null,
321
+ }),
322
+ probeAccount: async ({ account }) => await probeDingtalk({
323
+ clientId: account.clientId,
324
+ clientSecret: account.clientSecret,
325
+ accountId: account.accountId,
326
+ }),
327
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
328
+ accountId: account.accountId,
329
+ enabled: account.enabled,
330
+ configured: account.configured,
331
+ name: account.name,
332
+ clientId: account.clientId,
333
+ running: runtime?.running ?? false,
334
+ lastStartAt: runtime?.lastStartAt ?? null,
335
+ lastStopAt: runtime?.lastStopAt ?? null,
336
+ lastError: runtime?.lastError ?? null,
337
+ port: runtime?.port ?? null,
338
+ // 连接状态和消息时间戳:由 startAccount 里的 onStatusChange 回调写入 runtime,
339
+ // 必须在此处透传,否则 UI 的 Connected 和 Last inbound 字段永远显示 n/a。
340
+ connected: runtime?.connected ?? null,
341
+ lastConnectedAt: runtime?.lastConnectedAt ?? null,
342
+ lastInboundAt: runtime?.lastInboundAt ?? null,
343
+ probe,
344
+ }),
345
+ },
346
+ gateway: {
347
+ startAccount: async (ctx) => {
348
+ const account = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
349
+ // 检查账号是否启用和配置
350
+ if (!account.enabled) {
351
+ ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] is disabled, skipping startup`);
352
+ // 返回一个永不 resolve 的 Promise,保持 pending 状态直到 abort
353
+ return new Promise((resolve) => {
354
+ if (ctx.abortSignal?.aborted) {
355
+ resolve();
356
+ return;
357
+ }
358
+ ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
359
+ });
360
+ }
361
+ if (!account.configured) {
362
+ throw new Error(`DingTalk account "${ctx.accountId}" is not properly configured`);
363
+ }
364
+ // 去重检查:如果列表中排在当前账号之前的账号已使用相同 clientId,则跳过当前账号
365
+ // 使用静态配置分析(而非运行时状态),避免并发竞态条件
366
+ // 规则:同一 clientId 只有列表中第一个启用且已配置的账号才会建立连接
367
+ if (account.clientId) {
368
+ const clientId = String(account.clientId);
369
+ const allAccountIds = listDingtalkAccountIds(ctx.cfg);
370
+ const currentIndex = allAccountIds.indexOf(ctx.accountId);
371
+ const priorAccountWithSameClientId = allAccountIds.slice(0, currentIndex).find((otherId) => {
372
+ const other = resolveDingtalkAccount({ cfg: ctx.cfg, accountId: otherId });
373
+ return other.enabled && other.configured && other.clientId && String(other.clientId) === clientId;
374
+ });
375
+ if (priorAccountWithSameClientId) {
376
+ ctx.log?.info?.(`dingtalk-connector[${ctx.accountId}] skipped: clientId "${clientId.substring(0, 8)}..." is already used by account "${priorAccountWithSameClientId}"`);
377
+ return new Promise((resolve) => {
378
+ if (ctx.abortSignal?.aborted) {
379
+ resolve();
380
+ return;
381
+ }
382
+ ctx.abortSignal?.addEventListener('abort', () => resolve(), { once: true });
383
+ });
384
+ }
385
+ }
386
+ ctx.setStatus({ accountId: ctx.accountId, port: null });
387
+ ctx.log?.info(`starting dingtalk-connector[${ctx.accountId}] (mode: stream)`);
388
+ // 把 ctx.setStatus 包装成 onStatusChange 回调,传入连接层,
389
+ // 使连接层能在 WebSocket 连接/断开/收到消息时更新 UI 显示的
390
+ // Connected 和 Last inbound 字段。
391
+ // 注意:ctx.setStatus 是完全替换而非 merge patch,必须先 getStatus()
392
+ // 获取当前快照再合并,否则会清空 configured/running 等已有字段。
393
+ const onStatusChange = (patch) => {
394
+ const currentSnapshot = ctx.getStatus?.() ?? { accountId: ctx.accountId };
395
+ const nextSnapshot = { ...currentSnapshot, ...patch, accountId: ctx.accountId };
396
+ process.stderr.write(`[dingtalk-connector][${ctx.accountId}] onStatusChange patch=${JSON.stringify(patch)} current=${JSON.stringify(currentSnapshot)} next=${JSON.stringify(nextSnapshot)}\n`);
397
+ ctx.setStatus(nextSnapshot);
398
+ };
399
+ try {
400
+ return await monitorDingtalkProvider({
401
+ config: ctx.cfg,
402
+ runtime: ctx.runtime,
403
+ abortSignal: ctx.abortSignal,
404
+ accountId: ctx.accountId,
405
+ onStatusChange,
406
+ });
407
+ }
408
+ catch (err) {
409
+ // 打印真实错误到 stderr,绕过框架 log 系统(框架的 runtime.log 可能未初始化)
410
+ ctx.log?.error(`[dingtalk-connector][${ctx.accountId}] startAccount error: ${err?.message ?? err}\n${err?.stack ?? ''}`);
411
+ throw err;
412
+ }
413
+ },
414
+ },
415
+ };
@@ -0,0 +1,182 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeResolvedSecretInputString, normalizeSecretInputString } from "../sdk/helpers.js";
2
+ /**
3
+ * List all configured account IDs from the accounts field.
4
+ */
5
+ function listConfiguredAccountIds(cfg) {
6
+ const accounts = cfg.channels?.["dingtalk-connector"]?.accounts;
7
+ if (!accounts || typeof accounts !== "object") {
8
+ return [];
9
+ }
10
+ return Object.keys(accounts).filter(Boolean);
11
+ }
12
+ /**
13
+ * List all DingTalk account IDs.
14
+ * If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
15
+ */
16
+ export function listDingtalkAccountIds(cfg) {
17
+ const ids = listConfiguredAccountIds(cfg);
18
+ if (ids.length === 0) {
19
+ // Backward compatibility: no accounts configured, use default
20
+ return [DEFAULT_ACCOUNT_ID];
21
+ }
22
+ return [...ids].toSorted((a, b) => a.localeCompare(b));
23
+ }
24
+ /**
25
+ * Resolve the default account selection and its source.
26
+ */
27
+ export function resolveDefaultDingtalkAccountSelection(cfg) {
28
+ const preferredRaw = cfg.channels?.["dingtalk-connector"]?.defaultAccount?.trim();
29
+ const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
30
+ if (preferred) {
31
+ return {
32
+ accountId: preferred,
33
+ source: "explicit-default",
34
+ };
35
+ }
36
+ const ids = listDingtalkAccountIds(cfg);
37
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
38
+ return {
39
+ accountId: DEFAULT_ACCOUNT_ID,
40
+ source: "mapped-default",
41
+ };
42
+ }
43
+ return {
44
+ accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
45
+ source: "fallback",
46
+ };
47
+ }
48
+ /**
49
+ * Resolve the default account ID.
50
+ */
51
+ export function resolveDefaultDingtalkAccountId(cfg) {
52
+ return resolveDefaultDingtalkAccountSelection(cfg).accountId;
53
+ }
54
+ /**
55
+ * Get the raw account-specific config.
56
+ */
57
+ function resolveAccountConfig(cfg, accountId) {
58
+ const accounts = cfg.channels?.["dingtalk-connector"]?.accounts;
59
+ if (!accounts || typeof accounts !== "object") {
60
+ return undefined;
61
+ }
62
+ return accounts[accountId];
63
+ }
64
+ /**
65
+ * Merge top-level config with account-specific config.
66
+ * Account-specific fields override top-level fields.
67
+ */
68
+ function mergeDingtalkAccountConfig(cfg, accountId) {
69
+ const dingtalkCfg = cfg.channels?.["dingtalk-connector"];
70
+ // Extract base config (exclude accounts field to avoid recursion)
71
+ const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = dingtalkCfg ?? {};
72
+ // Get account-specific overrides
73
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
74
+ // Merge: account config overrides base config
75
+ return { ...base, ...account };
76
+ }
77
+ export function resolveDingtalkCredentials(cfg, options) {
78
+ const normalizeString = (value) => {
79
+ if (typeof value === "number") {
80
+ return String(value);
81
+ }
82
+ if (typeof value !== "string") {
83
+ return undefined;
84
+ }
85
+ const trimmed = value.trim();
86
+ return trimmed ? trimmed : undefined;
87
+ };
88
+ const resolveSecretLike = (value, path) => {
89
+ // Missing credential: treat as not configured (no exception).
90
+ // This path is used in non-onboarding contexts (e.g. channel listing/status),
91
+ // so we must not throw when credentials are absent.
92
+ if (value === undefined || value === null) {
93
+ return undefined;
94
+ }
95
+ const asString = normalizeString(value);
96
+ if (asString) {
97
+ return asString;
98
+ }
99
+ // In relaxed/onboarding paths only: allow direct env SecretRef reads for UX.
100
+ // Default resolution path must preserve unresolved-ref diagnostics/policy semantics.
101
+ if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
102
+ const rec = value;
103
+ const source = normalizeString(rec.source)?.toLowerCase();
104
+ const id = normalizeString(rec.id);
105
+ if (source === "env" && id) {
106
+ const envValue = normalizeString(process.env[id]);
107
+ if (envValue) {
108
+ return envValue;
109
+ }
110
+ }
111
+ }
112
+ if (options?.allowUnresolvedSecretRef) {
113
+ return normalizeSecretInputString(value);
114
+ }
115
+ return normalizeResolvedSecretInputString({ value, path });
116
+ };
117
+ const clientId = resolveSecretLike(cfg?.clientId, "channels.dingtalk-connector.clientId");
118
+ const clientSecret = resolveSecretLike(cfg?.clientSecret, "channels.dingtalk-connector.clientSecret");
119
+ if (!clientId || !clientSecret) {
120
+ return null;
121
+ }
122
+ return {
123
+ clientId,
124
+ clientSecret,
125
+ };
126
+ }
127
+ /**
128
+ * Resolve a complete DingTalk account with merged config.
129
+ */
130
+ export function resolveDingtalkAccount(params) {
131
+ const hasExplicitAccountId = typeof params.accountId === "string" && params.accountId.trim() !== "";
132
+ const defaultSelection = hasExplicitAccountId
133
+ ? null
134
+ : resolveDefaultDingtalkAccountSelection(params.cfg);
135
+ const accountId = hasExplicitAccountId
136
+ ? normalizeAccountId(params.accountId ?? "")
137
+ : (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
138
+ const selectionSource = hasExplicitAccountId
139
+ ? "explicit"
140
+ : (defaultSelection?.source ?? "fallback");
141
+ const dingtalkCfg = params.cfg.channels?.["dingtalk-connector"];
142
+ // Base enabled state (top-level)
143
+ const baseEnabled = dingtalkCfg?.enabled !== false;
144
+ // Merge configs
145
+ const merged = mergeDingtalkAccountConfig(params.cfg, accountId);
146
+ // Account-level enabled state
147
+ const accountEnabled = merged.enabled !== false;
148
+ const enabled = baseEnabled && accountEnabled;
149
+ // Resolve credentials from merged config
150
+ const creds = resolveDingtalkCredentials(merged);
151
+ const accountName = merged.name;
152
+ return {
153
+ accountId,
154
+ selectionSource,
155
+ enabled,
156
+ configured: Boolean(creds),
157
+ name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
158
+ clientId: creds?.clientId,
159
+ clientSecret: creds?.clientSecret,
160
+ config: merged,
161
+ };
162
+ }
163
+ /**
164
+ * List all enabled and configured accounts.
165
+ * Deduplicates by clientId to avoid creating multiple connections with the same credentials.
166
+ */
167
+ export function listEnabledDingtalkAccounts(cfg) {
168
+ const accounts = listDingtalkAccountIds(cfg)
169
+ .map((accountId) => resolveDingtalkAccount({ cfg, accountId }))
170
+ .filter((account) => account.enabled && account.configured);
171
+ // Deduplicate by clientId to avoid multiple connections with same credentials
172
+ const seen = new Set();
173
+ return accounts.filter((account) => {
174
+ if (!account.clientId)
175
+ return true;
176
+ if (seen.has(account.clientId)) {
177
+ return false;
178
+ }
179
+ seen.add(account.clientId);
180
+ return true;
181
+ });
182
+ }