@dingxiang-me/openclaw-wechat 1.4.1 → 1.7.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 (47) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.en.md +89 -12
  3. package/README.md +103 -15
  4. package/docs/channels/wecom.md +28 -3
  5. package/openclaw.plugin.json +467 -10
  6. package/package.json +13 -2
  7. package/src/core/agent-routing.js +6 -0
  8. package/src/core.js +564 -35
  9. package/src/wecom/account-config-core.js +28 -8
  10. package/src/wecom/account-config.js +55 -0
  11. package/src/wecom/account-diagnostics.js +121 -0
  12. package/src/wecom/account-paths.js +39 -0
  13. package/src/wecom/agent-inbound-dispatch.js +9 -0
  14. package/src/wecom/agent-inbound-guards.js +24 -4
  15. package/src/wecom/agent-inbound-processor.js +27 -0
  16. package/src/wecom/agent-webhook-handler.js +11 -0
  17. package/src/wecom/bot-context.js +2 -1
  18. package/src/wecom/bot-dispatch-fallback.js +2 -1
  19. package/src/wecom/bot-inbound-content.js +73 -3
  20. package/src/wecom/bot-inbound-dispatch-runtime.js +2 -1
  21. package/src/wecom/bot-inbound-executor-helpers.js +56 -5
  22. package/src/wecom/bot-inbound-executor.js +19 -0
  23. package/src/wecom/bot-inbound-guards.js +36 -4
  24. package/src/wecom/bot-runtime-context.js +5 -3
  25. package/src/wecom/bot-webhook-dispatch.js +45 -12
  26. package/src/wecom/bot-webhook-handler.js +45 -13
  27. package/src/wecom/command-handlers.js +26 -0
  28. package/src/wecom/command-status-text.js +76 -7
  29. package/src/wecom/observability-metrics.js +133 -0
  30. package/src/wecom/outbound-agent-push.js +2 -1
  31. package/src/wecom/outbound-bot-card.js +103 -0
  32. package/src/wecom/outbound-delivery.js +92 -7
  33. package/src/wecom/outbound-response-delivery.js +10 -6
  34. package/src/wecom/outbound-webhook-delivery.js +42 -1
  35. package/src/wecom/plugin-account-policy-services.js +19 -0
  36. package/src/wecom/plugin-base-services.js +13 -0
  37. package/src/wecom/plugin-constants.js +1 -1
  38. package/src/wecom/plugin-delivery-inbound-services.js +8 -0
  39. package/src/wecom/plugin-processing-deps.js +4 -0
  40. package/src/wecom/plugin-route-runtime-deps.js +5 -0
  41. package/src/wecom/plugin-services.js +7 -0
  42. package/src/wecom/policy-resolvers.js +82 -5
  43. package/src/wecom/register-runtime.js +31 -2
  44. package/src/wecom/route-registration.js +173 -41
  45. package/src/wecom/runtime-utils.js +7 -2
  46. package/src/wecom/webhook-adapter.js +61 -0
  47. package/src/wecom/webhook-bot.js +26 -0
package/src/core.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { buildDefaultBotWebhookPath } from "./wecom/account-paths.js";
2
3
 
3
4
  export const WECOM_TEXT_BYTE_LIMIT = 2000;
4
5
  export const INBOUND_DEDUPE_TTL_MS = 5 * 60 * 1000;
@@ -41,17 +42,59 @@ const DEFAULT_COMMAND_ALLOWLIST = Object.freeze([
41
42
  "/compact",
42
43
  ]);
43
44
  const DEFAULT_ALLOW_FROM_REJECT_MESSAGE = "当前账号未授权,请联系管理员。";
45
+ const DEFAULT_EVENT_ENTER_AGENT_WELCOME_TEXT = "你好,我是 AI 助手,直接发消息即可开始对话。";
44
46
  const DEFAULT_DELIVERY_FALLBACK_ORDER = Object.freeze([
45
47
  "active_stream",
46
48
  "response_url",
47
49
  "webhook_bot",
48
50
  "agent_push",
49
51
  ]);
52
+ const DEFAULT_BOT_CARD_MODE = "markdown";
53
+ const BOT_CARD_MODE_SET = new Set(["markdown", "template_card"]);
50
54
  const DELIVERY_FALLBACK_LAYER_SET = new Set(DEFAULT_DELIVERY_FALLBACK_ORDER);
51
55
  const DYNAMIC_AGENT_MAP_SPLITTER = /[,\n]/;
52
56
  const GROUP_CHAT_TRIGGER_MODE_SET = new Set(["direct", "mention", "keyword"]);
57
+ const DM_POLICY_MODE_SET = new Set(["open", "allowlist", "deny"]);
53
58
  const DYNAMIC_AGENT_MODE_SET = new Set(["mapping", "deterministic", "hybrid"]);
54
59
  const DYNAMIC_AGENT_ID_STRATEGY_SET = new Set(["readable-hash"]);
60
+ const LEGACY_INLINE_ACCOUNT_RESERVED_KEYS = new Set([
61
+ "name",
62
+ "enabled",
63
+ "corpId",
64
+ "corpSecret",
65
+ "agentId",
66
+ "callbackToken",
67
+ "token",
68
+ "callbackAesKey",
69
+ "encodingAesKey",
70
+ "webhookPath",
71
+ "outboundProxy",
72
+ "proxyUrl",
73
+ "proxy",
74
+ "webhooks",
75
+ "allowFrom",
76
+ "allowFromRejectMessage",
77
+ "rejectUnauthorizedMessage",
78
+ "adminUsers",
79
+ "commandAllowlist",
80
+ "commandBlockMessage",
81
+ "commands",
82
+ "workspaceTemplate",
83
+ "groupChat",
84
+ "dynamicAgent",
85
+ "dynamicAgents",
86
+ "dm",
87
+ "debounce",
88
+ "streaming",
89
+ "bot",
90
+ "delivery",
91
+ "webhookBot",
92
+ "stream",
93
+ "observability",
94
+ "voiceTranscription",
95
+ "accounts",
96
+ "agent",
97
+ ]);
55
98
 
56
99
  const inboundMessageDedupe = new Map();
57
100
 
@@ -188,6 +231,32 @@ function normalizeAccountIdForEnv(accountId) {
188
231
  return normalized || "default";
189
232
  }
190
233
 
234
+ function resolveLegacyInlineAccountConfig(channelConfig, accountId) {
235
+ if (!channelConfig || typeof channelConfig !== "object") return null;
236
+ const normalizedAccountId = normalizeAccountIdForEnv(accountId);
237
+ for (const [key, value] of Object.entries(channelConfig)) {
238
+ const normalizedKey = normalizeAccountIdForEnv(key);
239
+ if (LEGACY_INLINE_ACCOUNT_RESERVED_KEYS.has(normalizedKey)) continue;
240
+ if (normalizedKey !== normalizedAccountId) continue;
241
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
242
+ return value;
243
+ }
244
+ return null;
245
+ }
246
+
247
+ function collectLegacyInlineAccountIds(channelConfig) {
248
+ if (!channelConfig || typeof channelConfig !== "object") return [];
249
+ const ids = [];
250
+ for (const [key, value] of Object.entries(channelConfig)) {
251
+ const normalizedKey = normalizeAccountIdForEnv(key);
252
+ if (!normalizedKey) continue;
253
+ if (LEGACY_INLINE_ACCOUNT_RESERVED_KEYS.has(normalizedKey)) continue;
254
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
255
+ ids.push(normalizedKey);
256
+ }
257
+ return Array.from(new Set(ids));
258
+ }
259
+
191
260
  function readAllowFromEnv(envVars, processEnv, accountId = "default") {
192
261
  const normalizedId = normalizeAccountIdForEnv(accountId);
193
262
  const scopedAllowFromKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_ALLOW_FROM`;
@@ -211,6 +280,98 @@ function readAllowFromRejectMessageEnv(envVars, processEnv, accountId = "default
211
280
  );
212
281
  }
213
282
 
283
+ function readDmPolicyModeEnv(envVars, processEnv, accountId = "default") {
284
+ const normalizedId = normalizeAccountIdForEnv(accountId);
285
+ const scopedDmPolicyKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_DM_POLICY`;
286
+ const scopedDmModeKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_DM_MODE`;
287
+ return pickFirstNonEmptyString(
288
+ scopedDmPolicyKey ? envVars?.[scopedDmPolicyKey] : undefined,
289
+ scopedDmPolicyKey ? processEnv?.[scopedDmPolicyKey] : undefined,
290
+ scopedDmModeKey ? envVars?.[scopedDmModeKey] : undefined,
291
+ scopedDmModeKey ? processEnv?.[scopedDmModeKey] : undefined,
292
+ envVars?.WECOM_DM_POLICY,
293
+ processEnv?.WECOM_DM_POLICY,
294
+ envVars?.WECOM_DM_MODE,
295
+ processEnv?.WECOM_DM_MODE,
296
+ );
297
+ }
298
+
299
+ function readDmAllowFromEnv(envVars, processEnv, accountId = "default") {
300
+ const normalizedId = normalizeAccountIdForEnv(accountId);
301
+ const scopedAllowFromKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_DM_ALLOW_FROM`;
302
+ const scoped = parseStringList(
303
+ scopedAllowFromKey ? envVars?.[scopedAllowFromKey] : undefined,
304
+ scopedAllowFromKey ? processEnv?.[scopedAllowFromKey] : undefined,
305
+ );
306
+ if (scoped.length > 0) return scoped;
307
+ return parseStringList(envVars?.WECOM_DM_ALLOW_FROM, processEnv?.WECOM_DM_ALLOW_FROM);
308
+ }
309
+
310
+ function readDmRejectMessageEnv(envVars, processEnv, accountId = "default") {
311
+ const normalizedId = normalizeAccountIdForEnv(accountId);
312
+ const scopedRejectMessageKey =
313
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_DM_REJECT_MESSAGE`;
314
+ return pickFirstNonEmptyString(
315
+ scopedRejectMessageKey ? envVars?.[scopedRejectMessageKey] : undefined,
316
+ scopedRejectMessageKey ? processEnv?.[scopedRejectMessageKey] : undefined,
317
+ envVars?.WECOM_DM_REJECT_MESSAGE,
318
+ processEnv?.WECOM_DM_REJECT_MESSAGE,
319
+ );
320
+ }
321
+
322
+ function readEventEnabledEnv(envVars, processEnv, accountId = "default") {
323
+ const normalizedId = normalizeAccountIdForEnv(accountId);
324
+ const scopedEnabledKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENT_ENABLED`;
325
+ const scopedEventsEnabledKey =
326
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENTS_ENABLED`;
327
+ return pickFirstNonEmptyString(
328
+ scopedEnabledKey ? envVars?.[scopedEnabledKey] : undefined,
329
+ scopedEnabledKey ? processEnv?.[scopedEnabledKey] : undefined,
330
+ scopedEventsEnabledKey ? envVars?.[scopedEventsEnabledKey] : undefined,
331
+ scopedEventsEnabledKey ? processEnv?.[scopedEventsEnabledKey] : undefined,
332
+ envVars?.WECOM_EVENT_ENABLED,
333
+ processEnv?.WECOM_EVENT_ENABLED,
334
+ envVars?.WECOM_EVENTS_ENABLED,
335
+ processEnv?.WECOM_EVENTS_ENABLED,
336
+ );
337
+ }
338
+
339
+ function readEventEnterAgentWelcomeEnabledEnv(envVars, processEnv, accountId = "default") {
340
+ const normalizedId = normalizeAccountIdForEnv(accountId);
341
+ const scopedEnabledKey =
342
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENT_ENTER_AGENT_WELCOME_ENABLED`;
343
+ const scopedEventsEnabledKey =
344
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENTS_ENTER_AGENT_WELCOME_ENABLED`;
345
+ return pickFirstNonEmptyString(
346
+ scopedEnabledKey ? envVars?.[scopedEnabledKey] : undefined,
347
+ scopedEnabledKey ? processEnv?.[scopedEnabledKey] : undefined,
348
+ scopedEventsEnabledKey ? envVars?.[scopedEventsEnabledKey] : undefined,
349
+ scopedEventsEnabledKey ? processEnv?.[scopedEventsEnabledKey] : undefined,
350
+ envVars?.WECOM_EVENT_ENTER_AGENT_WELCOME_ENABLED,
351
+ processEnv?.WECOM_EVENT_ENTER_AGENT_WELCOME_ENABLED,
352
+ envVars?.WECOM_EVENTS_ENTER_AGENT_WELCOME_ENABLED,
353
+ processEnv?.WECOM_EVENTS_ENTER_AGENT_WELCOME_ENABLED,
354
+ );
355
+ }
356
+
357
+ function readEventEnterAgentWelcomeTextEnv(envVars, processEnv, accountId = "default") {
358
+ const normalizedId = normalizeAccountIdForEnv(accountId);
359
+ const scopedTextKey =
360
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENT_ENTER_AGENT_WELCOME_TEXT`;
361
+ const scopedEventsTextKey =
362
+ normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_EVENTS_ENTER_AGENT_WELCOME_TEXT`;
363
+ return pickFirstNonEmptyString(
364
+ scopedTextKey ? envVars?.[scopedTextKey] : undefined,
365
+ scopedTextKey ? processEnv?.[scopedTextKey] : undefined,
366
+ scopedEventsTextKey ? envVars?.[scopedEventsTextKey] : undefined,
367
+ scopedEventsTextKey ? processEnv?.[scopedEventsTextKey] : undefined,
368
+ envVars?.WECOM_EVENT_ENTER_AGENT_WELCOME_TEXT,
369
+ processEnv?.WECOM_EVENT_ENTER_AGENT_WELCOME_TEXT,
370
+ envVars?.WECOM_EVENTS_ENTER_AGENT_WELCOME_TEXT,
371
+ processEnv?.WECOM_EVENTS_ENTER_AGENT_WELCOME_TEXT,
372
+ );
373
+ }
374
+
214
375
  function readProxyEnv(envVars, processEnv, accountId = "default") {
215
376
  const normalizedId = normalizeAccountIdForEnv(accountId);
216
377
  const scopedProxyKey = normalizedId === "default" ? null : `WECOM_${normalizedId.toUpperCase()}_PROXY`;
@@ -389,6 +550,27 @@ function uniqueDeliveryFallbackOrder(values) {
389
550
  return deduped;
390
551
  }
391
552
 
553
+ function normalizeWecomBotCardMode(value, fallback = DEFAULT_BOT_CARD_MODE) {
554
+ const normalized = String(value ?? "")
555
+ .trim()
556
+ .toLowerCase();
557
+ if (!normalized) return fallback;
558
+ if (normalized === "template-card" || normalized === "templatecard") return "template_card";
559
+ if (BOT_CARD_MODE_SET.has(normalized)) return normalized;
560
+ return fallback;
561
+ }
562
+
563
+ function normalizeWecomDmPolicyMode(value, fallback = "open") {
564
+ const normalized = String(value ?? "")
565
+ .trim()
566
+ .toLowerCase();
567
+ if (!normalized) return fallback;
568
+ if (normalized === "closed" || normalized === "close") return "deny";
569
+ if (normalized === "whitelist") return "allowlist";
570
+ if (DM_POLICY_MODE_SET.has(normalized)) return normalized;
571
+ return fallback;
572
+ }
573
+
392
574
  export function normalizeWecomAllowFromEntry(raw) {
393
575
  const trimmed = String(raw ?? "").trim();
394
576
  if (!trimmed) return "";
@@ -541,23 +723,31 @@ export function resolveWecomCommandPolicyConfig({
541
723
  } = {}) {
542
724
  const commandConfig =
543
725
  channelConfig?.commands && typeof channelConfig.commands === "object" ? channelConfig.commands : {};
544
- const enabled = parseBooleanLike(
545
- commandConfig.enabled,
546
- parseBooleanLike(envVars?.WECOM_COMMANDS_ENABLED, parseBooleanLike(processEnv?.WECOM_COMMANDS_ENABLED, false)),
547
- );
726
+ const legacyAllowlist = uniqueCommandList(parseStringList(channelConfig?.commandAllowlist));
548
727
  const configuredAllowlist = uniqueCommandList(
549
728
  parseStringList(
550
729
  commandConfig.allowlist,
730
+ legacyAllowlist,
551
731
  envVars?.WECOM_COMMANDS_ALLOWLIST,
552
732
  processEnv?.WECOM_COMMANDS_ALLOWLIST,
553
733
  ),
554
734
  );
735
+ const allowlistEnabledByConfig = configuredAllowlist.length > 0;
736
+ const enabled = parseBooleanLike(
737
+ commandConfig.enabled,
738
+ parseBooleanLike(
739
+ envVars?.WECOM_COMMANDS_ENABLED,
740
+ parseBooleanLike(processEnv?.WECOM_COMMANDS_ENABLED, allowlistEnabledByConfig),
741
+ ),
742
+ );
555
743
  const allowlist = configuredAllowlist.length > 0 ? configuredAllowlist : Array.from(DEFAULT_COMMAND_ALLOWLIST);
556
744
  const adminUsers = uniqueLowerCaseList(
557
745
  parseStringList(channelConfig?.adminUsers, envVars?.WECOM_ADMIN_USERS, processEnv?.WECOM_ADMIN_USERS),
558
746
  );
559
747
  const rejectMessage = pickFirstNonEmptyString(
560
748
  commandConfig.rejectMessage,
749
+ commandConfig.blockMessage,
750
+ channelConfig?.commandBlockMessage,
561
751
  envVars?.WECOM_COMMANDS_REJECT_MESSAGE,
562
752
  processEnv?.WECOM_COMMANDS_REJECT_MESSAGE,
563
753
  "该指令未开放,请联系管理员。",
@@ -578,8 +768,12 @@ export function resolveWecomAllowFromPolicyConfig({
578
768
  processEnv = process.env,
579
769
  accountId = "default",
580
770
  } = {}) {
581
- const accountAllowFrom = uniqueAllowFromList(parseStringList(accountConfig?.allowFrom));
582
- const channelAllowFrom = uniqueAllowFromList(parseStringList(channelConfig?.allowFrom));
771
+ const accountAllowFrom = uniqueAllowFromList(
772
+ parseStringList(accountConfig?.allowFrom, accountConfig?.dm?.allowFrom),
773
+ );
774
+ const channelAllowFrom = uniqueAllowFromList(
775
+ parseStringList(channelConfig?.allowFrom, channelConfig?.dm?.allowFrom),
776
+ );
583
777
  const envAllowFrom = uniqueAllowFromList(readAllowFromEnv(envVars, processEnv, accountId));
584
778
  const allowFrom = accountAllowFrom.length > 0 ? accountAllowFrom : channelAllowFrom.length > 0 ? channelAllowFrom : envAllowFrom;
585
779
  const rejectMessage = pickFirstNonEmptyString(
@@ -596,6 +790,85 @@ export function resolveWecomAllowFromPolicyConfig({
596
790
  };
597
791
  }
598
792
 
793
+ export function resolveWecomDmPolicyConfig({
794
+ channelConfig = {},
795
+ accountConfig = {},
796
+ envVars = {},
797
+ processEnv = process.env,
798
+ accountId = "default",
799
+ } = {}) {
800
+ const channelDmConfig = channelConfig?.dm && typeof channelConfig.dm === "object" ? channelConfig.dm : {};
801
+ const accountDmConfig = accountConfig?.dm && typeof accountConfig.dm === "object" ? accountConfig.dm : {};
802
+ const mode = normalizeWecomDmPolicyMode(
803
+ pickFirstNonEmptyString(
804
+ accountDmConfig.mode,
805
+ channelDmConfig.mode,
806
+ readDmPolicyModeEnv(envVars, processEnv, accountId),
807
+ "open",
808
+ ),
809
+ );
810
+ const allowFrom = uniqueAllowFromList(
811
+ parseStringList(
812
+ accountDmConfig.allowFrom,
813
+ channelDmConfig.allowFrom,
814
+ readDmAllowFromEnv(envVars, processEnv, accountId),
815
+ ),
816
+ );
817
+ const rejectMessage = pickFirstNonEmptyString(
818
+ accountDmConfig.rejectMessage,
819
+ accountDmConfig.blockMessage,
820
+ channelDmConfig.rejectMessage,
821
+ channelDmConfig.blockMessage,
822
+ readDmRejectMessageEnv(envVars, processEnv, accountId),
823
+ mode === "deny" ? "当前渠道私聊已关闭,请联系管理员。" : "当前私聊账号未授权,请联系管理员。",
824
+ );
825
+ const effectiveMode = mode === "allowlist" && allowFrom.length === 0 ? "deny" : mode;
826
+ return {
827
+ mode: effectiveMode,
828
+ allowFrom,
829
+ rejectMessage,
830
+ enabled: effectiveMode !== "open" || allowFrom.length > 0,
831
+ };
832
+ }
833
+
834
+ export function resolveWecomEventPolicyConfig({
835
+ channelConfig = {},
836
+ accountConfig = {},
837
+ envVars = {},
838
+ processEnv = process.env,
839
+ accountId = "default",
840
+ } = {}) {
841
+ const channelEventConfig = channelConfig?.events && typeof channelConfig.events === "object" ? channelConfig.events : {};
842
+ const accountEventConfig = accountConfig?.events && typeof accountConfig.events === "object" ? accountConfig.events : {};
843
+ const enabled = parseBooleanLike(
844
+ accountEventConfig.enabled,
845
+ parseBooleanLike(
846
+ channelEventConfig.enabled,
847
+ parseBooleanLike(readEventEnabledEnv(envVars, processEnv, accountId), true),
848
+ ),
849
+ );
850
+ const enterAgentWelcomeEnabled = enabled
851
+ ? parseBooleanLike(
852
+ accountEventConfig.enterAgentWelcomeEnabled,
853
+ parseBooleanLike(
854
+ channelEventConfig.enterAgentWelcomeEnabled,
855
+ parseBooleanLike(readEventEnterAgentWelcomeEnabledEnv(envVars, processEnv, accountId), false),
856
+ ),
857
+ )
858
+ : false;
859
+ const enterAgentWelcomeText = pickFirstNonEmptyString(
860
+ accountEventConfig.enterAgentWelcomeText,
861
+ channelEventConfig.enterAgentWelcomeText,
862
+ readEventEnterAgentWelcomeTextEnv(envVars, processEnv, accountId),
863
+ DEFAULT_EVENT_ENTER_AGENT_WELCOME_TEXT,
864
+ );
865
+ return {
866
+ enabled,
867
+ enterAgentWelcomeEnabled,
868
+ enterAgentWelcomeText,
869
+ };
870
+ }
871
+
599
872
  export function isWecomSenderAllowed({ senderId, allowFrom = [] } = {}) {
600
873
  const sender = normalizeWecomAllowFromEntry(senderId);
601
874
  if (!sender) return false;
@@ -817,8 +1090,16 @@ export function resolveWecomDynamicAgentConfig({
817
1090
  envVars = {},
818
1091
  processEnv = process.env,
819
1092
  } = {}) {
820
- const dynamicConfig =
821
- channelConfig?.dynamicAgent && typeof channelConfig.dynamicAgent === "object" ? channelConfig.dynamicAgent : {};
1093
+ const dynamicAgentConfig =
1094
+ channelConfig?.dynamicAgent && typeof channelConfig.dynamicAgent === "object"
1095
+ ? channelConfig.dynamicAgent
1096
+ : {};
1097
+ const dynamicAgentsCompatConfig =
1098
+ channelConfig?.dynamicAgents && typeof channelConfig.dynamicAgents === "object"
1099
+ ? channelConfig.dynamicAgents
1100
+ : {};
1101
+ const dynamicConfig = Object.keys(dynamicAgentConfig).length > 0 ? dynamicAgentConfig : dynamicAgentsCompatConfig;
1102
+ const dmCompatConfig = channelConfig?.dm && typeof channelConfig.dm === "object" ? channelConfig.dm : {};
822
1103
  const enabled = parseBooleanLike(
823
1104
  dynamicConfig.enabled,
824
1105
  parseBooleanLike(
@@ -866,6 +1147,7 @@ export function resolveWecomDynamicAgentConfig({
866
1147
  );
867
1148
  const workspaceTemplate = pickFirstNonEmptyString(
868
1149
  dynamicConfig.workspaceTemplate,
1150
+ channelConfig?.workspaceTemplate,
869
1151
  envVars?.WECOM_DYNAMIC_AGENT_WORKSPACE_TEMPLATE,
870
1152
  processEnv?.WECOM_DYNAMIC_AGENT_WORKSPACE_TEMPLATE,
871
1153
  );
@@ -911,6 +1193,23 @@ export function resolveWecomDynamicAgentConfig({
911
1193
  parseBooleanLike(processEnv?.WECOM_DYNAMIC_AGENT_ALLOW_FALLBACK, true),
912
1194
  ),
913
1195
  );
1196
+ const dmCreateAgent = parseBooleanLike(
1197
+ dynamicConfig.dmCreateAgentOnFirstMessage,
1198
+ parseBooleanLike(
1199
+ dmCompatConfig.createAgentOnFirstMessage,
1200
+ parseBooleanLike(
1201
+ envVars?.WECOM_DM_CREATE_AGENT_ON_FIRST_MESSAGE,
1202
+ parseBooleanLike(processEnv?.WECOM_DM_CREATE_AGENT_ON_FIRST_MESSAGE, true),
1203
+ ),
1204
+ ),
1205
+ );
1206
+ const groupEnabled = parseBooleanLike(
1207
+ dynamicConfig.groupEnabled,
1208
+ parseBooleanLike(
1209
+ channelConfig?.groupChat?.enabled,
1210
+ parseBooleanLike(envVars?.WECOM_GROUP_CHAT_ENABLED, parseBooleanLike(processEnv?.WECOM_GROUP_CHAT_ENABLED, true)),
1211
+ ),
1212
+ );
914
1213
  const userMap = parseDynamicAgentMap(
915
1214
  dynamicConfig.userMap,
916
1215
  envVars?.WECOM_DYNAMIC_AGENT_USER_MAP,
@@ -941,6 +1240,8 @@ export function resolveWecomDynamicAgentConfig({
941
1240
  forceAgentSessionKey,
942
1241
  preferMentionMap,
943
1242
  allowFallbackToDefaultRoute,
1243
+ dmCreateAgent,
1244
+ groupEnabled,
944
1245
  userMap,
945
1246
  groupMap,
946
1247
  mentionMap,
@@ -1077,90 +1378,318 @@ export function resolveWecomObservabilityConfig({
1077
1378
  };
1078
1379
  }
1079
1380
 
1381
+ export function resolveWecomBotCardConfig({
1382
+ botConfig = {},
1383
+ envVars = {},
1384
+ processEnv = process.env,
1385
+ } = {}) {
1386
+ const cardConfig = botConfig?.card && typeof botConfig.card === "object" ? botConfig.card : {};
1387
+ const enabled = parseBooleanLike(
1388
+ cardConfig.enabled,
1389
+ parseBooleanLike(
1390
+ envVars?.WECOM_BOT_CARD_ENABLED,
1391
+ parseBooleanLike(processEnv?.WECOM_BOT_CARD_ENABLED, false),
1392
+ ),
1393
+ );
1394
+ const mode = normalizeWecomBotCardMode(
1395
+ pickFirstNonEmptyString(
1396
+ cardConfig.mode,
1397
+ envVars?.WECOM_BOT_CARD_MODE,
1398
+ processEnv?.WECOM_BOT_CARD_MODE,
1399
+ DEFAULT_BOT_CARD_MODE,
1400
+ ),
1401
+ );
1402
+ const title = pickFirstNonEmptyString(
1403
+ cardConfig.title,
1404
+ envVars?.WECOM_BOT_CARD_TITLE,
1405
+ processEnv?.WECOM_BOT_CARD_TITLE,
1406
+ "OpenClaw-Wechat",
1407
+ );
1408
+ const subtitle = pickFirstNonEmptyString(
1409
+ cardConfig.subtitle,
1410
+ cardConfig.subTitle,
1411
+ envVars?.WECOM_BOT_CARD_SUBTITLE,
1412
+ processEnv?.WECOM_BOT_CARD_SUBTITLE,
1413
+ );
1414
+ const footer = pickFirstNonEmptyString(
1415
+ cardConfig.footer,
1416
+ envVars?.WECOM_BOT_CARD_FOOTER,
1417
+ processEnv?.WECOM_BOT_CARD_FOOTER,
1418
+ );
1419
+ const maxContentLength = asBoundedPositiveInteger(
1420
+ cardConfig.maxContentLength ??
1421
+ cardConfig.maxBodyChars ??
1422
+ envVars?.WECOM_BOT_CARD_MAX_CONTENT_LENGTH ??
1423
+ processEnv?.WECOM_BOT_CARD_MAX_CONTENT_LENGTH,
1424
+ 1400,
1425
+ 200,
1426
+ 4000,
1427
+ );
1428
+ const responseUrlEnabled = parseBooleanLike(
1429
+ cardConfig.responseUrlEnabled,
1430
+ parseBooleanLike(
1431
+ envVars?.WECOM_BOT_CARD_RESPONSE_URL_ENABLED,
1432
+ parseBooleanLike(processEnv?.WECOM_BOT_CARD_RESPONSE_URL_ENABLED, true),
1433
+ ),
1434
+ );
1435
+ const webhookBotEnabled = parseBooleanLike(
1436
+ cardConfig.webhookBotEnabled,
1437
+ parseBooleanLike(
1438
+ envVars?.WECOM_BOT_CARD_WEBHOOK_BOT_ENABLED,
1439
+ parseBooleanLike(processEnv?.WECOM_BOT_CARD_WEBHOOK_BOT_ENABLED, true),
1440
+ ),
1441
+ );
1442
+ return {
1443
+ enabled,
1444
+ mode,
1445
+ title,
1446
+ subtitle: subtitle || undefined,
1447
+ footer: footer || undefined,
1448
+ maxContentLength,
1449
+ responseUrlEnabled,
1450
+ webhookBotEnabled,
1451
+ };
1452
+ }
1453
+
1080
1454
  export function resolveWecomBotModeConfig({
1081
1455
  channelConfig = {},
1082
1456
  envVars = {},
1083
1457
  processEnv = process.env,
1458
+ accountId = "default",
1459
+ botConfigOverride,
1084
1460
  } = {}) {
1085
- const botConfig = channelConfig?.bot && typeof channelConfig.bot === "object" ? channelConfig.bot : {};
1461
+ const normalizedAccountId = normalizeAccountIdForEnv(accountId);
1462
+ const legacyInlineAccountConfig = resolveLegacyInlineAccountConfig(channelConfig, normalizedAccountId);
1463
+ const defaultInlineAccountConfig = resolveLegacyInlineAccountConfig(channelConfig, "default");
1464
+ const accountConfig =
1465
+ normalizedAccountId === "default"
1466
+ ? (defaultInlineAccountConfig ?? channelConfig)
1467
+ : channelConfig?.accounts && typeof channelConfig.accounts === "object"
1468
+ ? (channelConfig.accounts[normalizedAccountId] ?? legacyInlineAccountConfig)
1469
+ : legacyInlineAccountConfig;
1470
+ const scopedBotConfig =
1471
+ normalizedAccountId === "default"
1472
+ ? channelConfig?.bot
1473
+ : accountConfig && typeof accountConfig === "object"
1474
+ ? accountConfig.bot
1475
+ : null;
1476
+ const botConfig =
1477
+ botConfigOverride && typeof botConfigOverride === "object"
1478
+ ? botConfigOverride
1479
+ : scopedBotConfig && typeof scopedBotConfig === "object"
1480
+ ? scopedBotConfig
1481
+ : {};
1482
+
1483
+ const scopedEnvVars = { ...(envVars && typeof envVars === "object" ? envVars : {}) };
1484
+ const scopedProcessEnv = { ...(processEnv && typeof processEnv === "object" ? processEnv : {}) };
1485
+ const botEnvSuffixes = [
1486
+ "ENABLED",
1487
+ "TOKEN",
1488
+ "ENCODING_AES_KEY",
1489
+ "WEBHOOK_PATH",
1490
+ "PLACEHOLDER_TEXT",
1491
+ "STREAM_EXPIRE_MS",
1492
+ "REPLY_TIMEOUT_MS",
1493
+ "LATE_REPLY_WATCH_MS",
1494
+ "LATE_REPLY_POLL_MS",
1495
+ "CARD_ENABLED",
1496
+ "CARD_MODE",
1497
+ "CARD_TITLE",
1498
+ "CARD_SUBTITLE",
1499
+ "CARD_FOOTER",
1500
+ "CARD_MAX_CONTENT_LENGTH",
1501
+ "CARD_RESPONSE_URL_ENABLED",
1502
+ "CARD_WEBHOOK_BOT_ENABLED",
1503
+ ];
1504
+ if (normalizedAccountId !== "default") {
1505
+ const accountPrefix = `WECOM_${normalizedAccountId.toUpperCase()}_BOT_`;
1506
+ for (const suffix of botEnvSuffixes) {
1507
+ const scopedKey = `${accountPrefix}${suffix}`;
1508
+ const mappedKey = `WECOM_BOT_${suffix}`;
1509
+ if (Object.prototype.hasOwnProperty.call(scopedEnvVars, scopedKey)) {
1510
+ scopedEnvVars[mappedKey] = scopedEnvVars[scopedKey];
1511
+ }
1512
+ if (Object.prototype.hasOwnProperty.call(scopedProcessEnv, scopedKey)) {
1513
+ scopedProcessEnv[mappedKey] = scopedProcessEnv[scopedKey];
1514
+ }
1515
+ }
1516
+ }
1086
1517
  const enabled = parseBooleanLike(
1087
1518
  botConfig.enabled,
1088
- parseBooleanLike(envVars?.WECOM_BOT_ENABLED, parseBooleanLike(processEnv?.WECOM_BOT_ENABLED, false)),
1519
+ parseBooleanLike(scopedEnvVars?.WECOM_BOT_ENABLED, parseBooleanLike(scopedProcessEnv?.WECOM_BOT_ENABLED, false)),
1089
1520
  );
1521
+ const legacyAgentCompat =
1522
+ accountConfig?.agent && typeof accountConfig.agent === "object" ? accountConfig.agent : null;
1523
+ const legacyTopLevelBotToken = legacyAgentCompat ? pickFirstNonEmptyString(accountConfig?.token) : "";
1524
+ const legacyTopLevelBotAesKey = legacyAgentCompat ? pickFirstNonEmptyString(accountConfig?.encodingAesKey) : "";
1525
+ const legacyTopLevelBotWebhookPath = legacyAgentCompat ? pickFirstNonEmptyString(accountConfig?.webhookPath) : "";
1090
1526
  const token = pickFirstNonEmptyString(
1091
1527
  botConfig.token,
1092
- envVars?.WECOM_BOT_TOKEN,
1093
- processEnv?.WECOM_BOT_TOKEN,
1528
+ botConfig.callbackToken,
1529
+ legacyTopLevelBotToken,
1530
+ scopedEnvVars?.WECOM_BOT_TOKEN,
1531
+ scopedProcessEnv?.WECOM_BOT_TOKEN,
1094
1532
  );
1095
1533
  const encodingAesKey = pickFirstNonEmptyString(
1096
1534
  botConfig.encodingAesKey,
1097
- envVars?.WECOM_BOT_ENCODING_AES_KEY,
1098
- processEnv?.WECOM_BOT_ENCODING_AES_KEY,
1535
+ botConfig.callbackAesKey,
1536
+ legacyTopLevelBotAesKey,
1537
+ scopedEnvVars?.WECOM_BOT_ENCODING_AES_KEY,
1538
+ scopedProcessEnv?.WECOM_BOT_ENCODING_AES_KEY,
1099
1539
  );
1100
1540
  const webhookPath = pickFirstNonEmptyString(
1101
1541
  botConfig.webhookPath,
1102
- envVars?.WECOM_BOT_WEBHOOK_PATH,
1103
- processEnv?.WECOM_BOT_WEBHOOK_PATH,
1104
- "/wecom/bot/callback",
1542
+ legacyTopLevelBotWebhookPath,
1543
+ scopedEnvVars?.WECOM_BOT_WEBHOOK_PATH,
1544
+ scopedProcessEnv?.WECOM_BOT_WEBHOOK_PATH,
1545
+ buildDefaultBotWebhookPath(normalizedAccountId),
1105
1546
  );
1106
1547
  const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key);
1107
1548
  const placeholderText = (() => {
1108
1549
  if (hasOwn(botConfig, "placeholderText")) return String(botConfig.placeholderText ?? "");
1109
- if (hasOwn(envVars, "WECOM_BOT_PLACEHOLDER_TEXT")) return String(envVars.WECOM_BOT_PLACEHOLDER_TEXT ?? "");
1110
- if (hasOwn(processEnv, "WECOM_BOT_PLACEHOLDER_TEXT"))
1111
- return String(processEnv.WECOM_BOT_PLACEHOLDER_TEXT ?? "");
1550
+ if (hasOwn(scopedEnvVars, "WECOM_BOT_PLACEHOLDER_TEXT")) return String(scopedEnvVars.WECOM_BOT_PLACEHOLDER_TEXT ?? "");
1551
+ if (hasOwn(scopedProcessEnv, "WECOM_BOT_PLACEHOLDER_TEXT"))
1552
+ return String(scopedProcessEnv.WECOM_BOT_PLACEHOLDER_TEXT ?? "");
1112
1553
  return "消息已收到,正在处理中,请稍等片刻。";
1113
1554
  })();
1114
1555
  const streamExpireMs = asBoundedPositiveInteger(
1115
1556
  botConfig.streamExpireMs ??
1116
- envVars?.WECOM_BOT_STREAM_EXPIRE_MS ??
1117
- processEnv?.WECOM_BOT_STREAM_EXPIRE_MS,
1557
+ scopedEnvVars?.WECOM_BOT_STREAM_EXPIRE_MS ??
1558
+ scopedProcessEnv?.WECOM_BOT_STREAM_EXPIRE_MS,
1118
1559
  10 * 60 * 1000,
1119
1560
  30 * 1000,
1120
1561
  60 * 60 * 1000,
1121
1562
  );
1122
1563
  const replyTimeoutMs = asBoundedPositiveInteger(
1123
1564
  botConfig.replyTimeoutMs ??
1124
- envVars?.WECOM_BOT_REPLY_TIMEOUT_MS ??
1125
- processEnv?.WECOM_BOT_REPLY_TIMEOUT_MS ??
1126
- envVars?.WECOM_REPLY_TIMEOUT_MS ??
1127
- processEnv?.WECOM_REPLY_TIMEOUT_MS,
1565
+ scopedEnvVars?.WECOM_BOT_REPLY_TIMEOUT_MS ??
1566
+ scopedProcessEnv?.WECOM_BOT_REPLY_TIMEOUT_MS ??
1567
+ scopedEnvVars?.WECOM_REPLY_TIMEOUT_MS ??
1568
+ scopedProcessEnv?.WECOM_REPLY_TIMEOUT_MS,
1128
1569
  90000,
1129
1570
  15000,
1130
1571
  10 * 60 * 1000,
1131
1572
  );
1132
1573
  const lateReplyWatchMs = asBoundedPositiveInteger(
1133
1574
  botConfig.lateReplyWatchMs ??
1134
- envVars?.WECOM_BOT_LATE_REPLY_WATCH_MS ??
1135
- processEnv?.WECOM_BOT_LATE_REPLY_WATCH_MS ??
1136
- envVars?.WECOM_LATE_REPLY_WATCH_MS ??
1137
- processEnv?.WECOM_LATE_REPLY_WATCH_MS,
1575
+ scopedEnvVars?.WECOM_BOT_LATE_REPLY_WATCH_MS ??
1576
+ scopedProcessEnv?.WECOM_BOT_LATE_REPLY_WATCH_MS ??
1577
+ scopedEnvVars?.WECOM_LATE_REPLY_WATCH_MS ??
1578
+ scopedProcessEnv?.WECOM_LATE_REPLY_WATCH_MS,
1138
1579
  180000,
1139
1580
  30000,
1140
1581
  10 * 60 * 1000,
1141
1582
  );
1142
1583
  const lateReplyPollMs = asBoundedPositiveInteger(
1143
1584
  botConfig.lateReplyPollMs ??
1144
- envVars?.WECOM_BOT_LATE_REPLY_POLL_MS ??
1145
- processEnv?.WECOM_BOT_LATE_REPLY_POLL_MS ??
1146
- envVars?.WECOM_LATE_REPLY_POLL_MS ??
1147
- processEnv?.WECOM_LATE_REPLY_POLL_MS,
1585
+ scopedEnvVars?.WECOM_BOT_LATE_REPLY_POLL_MS ??
1586
+ scopedProcessEnv?.WECOM_BOT_LATE_REPLY_POLL_MS ??
1587
+ scopedEnvVars?.WECOM_LATE_REPLY_POLL_MS ??
1588
+ scopedProcessEnv?.WECOM_LATE_REPLY_POLL_MS,
1148
1589
  2000,
1149
1590
  500,
1150
1591
  10000,
1151
1592
  );
1593
+ const card = resolveWecomBotCardConfig({
1594
+ botConfig,
1595
+ envVars: scopedEnvVars,
1596
+ processEnv: scopedProcessEnv,
1597
+ });
1152
1598
 
1153
1599
  return {
1600
+ accountId: normalizedAccountId,
1154
1601
  enabled,
1155
1602
  token: token || undefined,
1156
1603
  encodingAesKey: encodingAesKey || undefined,
1157
- webhookPath: webhookPath || "/wecom/bot/callback",
1604
+ webhookPath: webhookPath || buildDefaultBotWebhookPath(normalizedAccountId),
1158
1605
  placeholderText,
1159
1606
  streamExpireMs,
1160
1607
  replyTimeoutMs,
1161
1608
  lateReplyWatchMs,
1162
1609
  lateReplyPollMs,
1610
+ card,
1611
+ };
1612
+ }
1613
+
1614
+ export function resolveWecomBotModeAccountsConfig({
1615
+ channelConfig = {},
1616
+ envVars = {},
1617
+ processEnv = process.env,
1618
+ } = {}) {
1619
+ const accountIds = new Set(["default"]);
1620
+ const channelAccounts = channelConfig?.accounts;
1621
+ if (channelAccounts && typeof channelAccounts === "object") {
1622
+ for (const accountId of Object.keys(channelAccounts)) {
1623
+ accountIds.add(normalizeAccountIdForEnv(accountId));
1624
+ }
1625
+ }
1626
+ for (const accountId of collectLegacyInlineAccountIds(channelConfig)) {
1627
+ accountIds.add(normalizeAccountIdForEnv(accountId));
1628
+ }
1629
+
1630
+ const scopedBotIdRegex =
1631
+ /^WECOM_([A-Z0-9]+)_BOT_(ENABLED|TOKEN|ENCODING_AES_KEY|WEBHOOK_PATH|PLACEHOLDER_TEXT|STREAM_EXPIRE_MS|REPLY_TIMEOUT_MS|LATE_REPLY_WATCH_MS|LATE_REPLY_POLL_MS|PROXY|CARD_ENABLED|CARD_MODE|CARD_TITLE|CARD_SUBTITLE|CARD_FOOTER|CARD_MAX_CONTENT_LENGTH|CARD_RESPONSE_URL_ENABLED|CARD_WEBHOOK_BOT_ENABLED)$/;
1632
+ const collectScopedIds = (obj) => {
1633
+ if (!obj || typeof obj !== "object") return;
1634
+ for (const key of Object.keys(obj)) {
1635
+ const match = key.match(scopedBotIdRegex);
1636
+ if (!match) continue;
1637
+ const candidate = String(match[1] ?? "").trim().toLowerCase();
1638
+ if (candidate) accountIds.add(candidate);
1639
+ }
1640
+ };
1641
+ collectScopedIds(envVars);
1642
+ collectScopedIds(processEnv);
1643
+
1644
+ const hasScopedBotEnv = (accountId) => {
1645
+ const normalizedAccountId = normalizeAccountIdForEnv(accountId);
1646
+ if (normalizedAccountId === "default") return false;
1647
+ const prefix = `WECOM_${normalizedAccountId.toUpperCase()}_BOT_`;
1648
+ const hasIn = (obj) =>
1649
+ Boolean(
1650
+ obj &&
1651
+ typeof obj === "object" &&
1652
+ Object.keys(obj).some((key) => String(key ?? "").startsWith(prefix)),
1653
+ );
1654
+ return hasIn(envVars) || hasIn(processEnv);
1163
1655
  };
1656
+
1657
+ const ordered = Array.from(accountIds).sort((a, b) => {
1658
+ if (a === "default" && b !== "default") return -1;
1659
+ if (a !== "default" && b === "default") return 1;
1660
+ return a.localeCompare(b);
1661
+ });
1662
+
1663
+ const botConfigs = [];
1664
+ for (const accountId of ordered) {
1665
+ const resolved = resolveWecomBotModeConfig({
1666
+ channelConfig,
1667
+ envVars,
1668
+ processEnv,
1669
+ accountId,
1670
+ });
1671
+ const normalizedAccountId = normalizeAccountIdForEnv(accountId);
1672
+ const accountCfg =
1673
+ normalizedAccountId === "default"
1674
+ ? channelConfig
1675
+ : channelConfig?.accounts && typeof channelConfig.accounts === "object"
1676
+ ? (channelConfig.accounts[normalizedAccountId] ??
1677
+ resolveLegacyInlineAccountConfig(channelConfig, normalizedAccountId))
1678
+ : resolveLegacyInlineAccountConfig(channelConfig, normalizedAccountId);
1679
+ const hasBotConfigObject = Boolean(accountCfg && typeof accountCfg === "object" && accountCfg.bot && typeof accountCfg.bot === "object");
1680
+ if (
1681
+ normalizedAccountId !== "default" &&
1682
+ !hasBotConfigObject &&
1683
+ !hasScopedBotEnv(normalizedAccountId) &&
1684
+ resolved.enabled !== true &&
1685
+ !resolved.token &&
1686
+ !resolved.encodingAesKey
1687
+ ) {
1688
+ continue;
1689
+ }
1690
+ botConfigs.push(resolved);
1691
+ }
1692
+ return botConfigs;
1164
1693
  }
1165
1694
 
1166
1695
  function readVoiceEnv(envVars, processEnv, suffix) {