@yanhaidao/wecom 2.3.3 → 2.3.9

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 (111) hide show
  1. package/.github/workflows/release.yml +69 -1
  2. package/README.md +213 -337
  3. package/assets/03.bot.page.png +0 -0
  4. package/changelog/v2.3.4.md +20 -0
  5. package/changelog/v2.3.9.md +22 -0
  6. package/compat-single-account.md +32 -2
  7. package/index.test.ts +34 -0
  8. package/index.ts +15 -7
  9. package/package.json +8 -7
  10. package/src/agent/api-client.upload.test.ts +1 -2
  11. package/src/agent/handler.ts +82 -9
  12. package/src/agent/index.ts +1 -1
  13. package/src/app/account-runtime.ts +245 -0
  14. package/src/app/bootstrap.ts +29 -0
  15. package/src/app/index.ts +31 -0
  16. package/src/capability/agent/delivery-service.ts +79 -0
  17. package/src/capability/agent/fallback-policy.ts +13 -0
  18. package/src/capability/agent/index.ts +3 -0
  19. package/src/capability/agent/ingress-service.ts +38 -0
  20. package/src/capability/bot/dispatch-config.ts +47 -0
  21. package/src/capability/bot/fallback-delivery.ts +178 -0
  22. package/src/capability/bot/index.ts +1 -0
  23. package/src/capability/bot/local-path-delivery.ts +215 -0
  24. package/src/capability/bot/service.ts +56 -0
  25. package/src/capability/bot/stream-delivery.ts +379 -0
  26. package/src/capability/bot/stream-finalizer.ts +120 -0
  27. package/src/capability/bot/stream-orchestrator.ts +352 -0
  28. package/src/capability/bot/types.ts +8 -0
  29. package/src/capability/index.ts +2 -0
  30. package/src/channel.lifecycle.test.ts +9 -6
  31. package/src/channel.meta.test.ts +12 -0
  32. package/src/channel.ts +48 -21
  33. package/src/config/accounts.ts +223 -283
  34. package/src/config/derived-paths.test.ts +111 -0
  35. package/src/config/derived-paths.ts +41 -0
  36. package/src/config/index.ts +10 -12
  37. package/src/config/runtime-config.ts +46 -0
  38. package/src/config/schema.ts +59 -102
  39. package/src/domain/models.ts +7 -0
  40. package/src/domain/policies.ts +36 -0
  41. package/src/dynamic-agent.ts +6 -0
  42. package/src/gateway-monitor.ts +43 -93
  43. package/src/http.ts +23 -2
  44. package/src/monitor/limits.ts +7 -0
  45. package/src/monitor/state.ts +28 -508
  46. package/src/monitor.active.test.ts +3 -3
  47. package/src/monitor.integration.test.ts +0 -1
  48. package/src/monitor.ts +64 -2603
  49. package/src/monitor.webhook.test.ts +127 -42
  50. package/src/observability/audit-log.ts +48 -0
  51. package/src/observability/legacy-operational-event-store.ts +36 -0
  52. package/src/observability/raw-envelope-log.ts +28 -0
  53. package/src/observability/status-registry.ts +13 -0
  54. package/src/observability/transport-session-view.ts +14 -0
  55. package/src/onboarding.test.ts +219 -0
  56. package/src/onboarding.ts +88 -71
  57. package/src/outbound.test.ts +5 -5
  58. package/src/outbound.ts +18 -66
  59. package/src/runtime/dispatcher.ts +52 -0
  60. package/src/runtime/index.ts +4 -0
  61. package/src/runtime/outbound-intent.ts +4 -0
  62. package/src/runtime/reply-orchestrator.test.ts +38 -0
  63. package/src/runtime/reply-orchestrator.ts +55 -0
  64. package/src/runtime/routing-bridge.ts +19 -0
  65. package/src/runtime/session-manager.ts +76 -0
  66. package/src/runtime.ts +7 -14
  67. package/src/shared/command-auth.ts +1 -17
  68. package/src/shared/media-service.ts +36 -0
  69. package/src/shared/media-types.ts +5 -0
  70. package/src/store/active-reply-store.ts +42 -0
  71. package/src/store/interfaces.ts +11 -0
  72. package/src/store/memory-store.ts +43 -0
  73. package/src/store/stream-batch-store.ts +350 -0
  74. package/src/target.ts +28 -0
  75. package/src/transport/agent-api/client.ts +44 -0
  76. package/src/transport/agent-api/core.ts +367 -0
  77. package/src/transport/agent-api/delivery.ts +41 -0
  78. package/src/transport/agent-api/media-upload.ts +11 -0
  79. package/src/transport/agent-api/reply.ts +39 -0
  80. package/src/transport/agent-callback/http-handler.ts +47 -0
  81. package/src/transport/agent-callback/inbound.ts +5 -0
  82. package/src/transport/agent-callback/reply.ts +13 -0
  83. package/src/transport/agent-callback/request-handler.ts +244 -0
  84. package/src/transport/agent-callback/session.ts +23 -0
  85. package/src/transport/bot-webhook/active-reply.ts +36 -0
  86. package/src/transport/bot-webhook/http-handler.ts +48 -0
  87. package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
  88. package/src/transport/bot-webhook/inbound.ts +5 -0
  89. package/src/transport/bot-webhook/message-shape.ts +89 -0
  90. package/src/transport/bot-webhook/protocol.ts +148 -0
  91. package/src/transport/bot-webhook/reply.ts +15 -0
  92. package/src/transport/bot-webhook/request-handler.ts +394 -0
  93. package/src/transport/bot-webhook/session.ts +23 -0
  94. package/src/transport/bot-ws/inbound.ts +109 -0
  95. package/src/transport/bot-ws/reply.ts +48 -0
  96. package/src/transport/bot-ws/sdk-adapter.ts +180 -0
  97. package/src/transport/bot-ws/session.ts +28 -0
  98. package/src/transport/http/common.ts +109 -0
  99. package/src/transport/http/registry.ts +92 -0
  100. package/src/transport/http/request-handler.ts +84 -0
  101. package/src/transport/index.ts +14 -0
  102. package/src/types/account.ts +56 -91
  103. package/src/types/config.ts +59 -112
  104. package/src/types/constants.ts +20 -35
  105. package/src/types/events.ts +21 -0
  106. package/src/types/index.ts +14 -38
  107. package/src/types/legacy-stream.ts +50 -0
  108. package/src/types/runtime-context.ts +28 -0
  109. package/src/types/runtime.ts +161 -0
  110. package/src/agent/api-client.ts +0 -383
  111. package/src/monitor/types.ts +0 -136
@@ -1,334 +1,274 @@
1
- /**
2
- * WeCom 账号解析与模式检测
3
- */
4
-
5
1
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+
6
3
  import type {
7
- WecomConfig,
8
- WecomAccountConfig,
9
- WecomBotConfig,
10
- WecomAgentConfig,
11
- WecomNetworkConfig,
12
- ResolvedWecomAccount,
13
- ResolvedBotAccount,
14
- ResolvedAgentAccount,
15
- ResolvedMode,
16
- ResolvedWecomAccounts,
4
+ ResolvedAgentAccount,
5
+ ResolvedBotAccount,
6
+ ResolvedMode,
7
+ ResolvedWecomAccount,
8
+ ResolvedWecomAccounts,
9
+ WecomAccountConfig,
10
+ WecomAgentConfig,
11
+ WecomBotConfig,
12
+ WecomConfig,
13
+ WecomNetworkConfig,
17
14
  } from "../types/index.js";
18
15
 
19
16
  export const DEFAULT_ACCOUNT_ID = "default";
20
17
 
21
18
  export type WecomAccountConflict = {
22
- type: "duplicate_bot_token" | "duplicate_bot_aibotid" | "duplicate_agent_id";
23
- accountId: string;
24
- ownerAccountId: string;
25
- message: string;
19
+ type: "duplicate_bot_id" | "duplicate_agent_id";
20
+ accountId: string;
21
+ ownerAccountId: string;
22
+ message: string;
26
23
  };
27
24
 
28
- /**
29
- * 检测配置中启用的模式
30
- */
31
- export function detectMode(config: WecomConfig | undefined): ResolvedMode {
32
- if (!config || config.enabled === false) return "disabled";
33
-
34
- const accounts = config.accounts;
35
- if (accounts && typeof accounts === "object") {
36
- const enabledEntries = Object.values(accounts).filter(
37
- (entry) => entry && entry.enabled !== false,
38
- );
39
- if (enabledEntries.length > 0) return "matrix";
40
- }
41
-
42
- return "legacy";
25
+ function toNumber(value: number | string | undefined): number | undefined {
26
+ if (value == null) return undefined;
27
+ const parsed = typeof value === "number" ? value : Number(value);
28
+ return Number.isFinite(parsed) ? parsed : undefined;
43
29
  }
44
30
 
45
- /**
46
- * 解析 Bot 模式账号
47
- */
48
- function resolveBotAccount(accountId: string, config: WecomBotConfig, network?: WecomNetworkConfig): ResolvedBotAccount {
49
- return {
50
- accountId,
51
- enabled: true,
52
- configured: Boolean(config.token && config.encodingAESKey),
53
- token: config.token,
54
- encodingAESKey: config.encodingAESKey,
55
- receiveId: config.receiveId?.trim() ?? "",
56
- config,
57
- network,
58
- };
31
+ function resolveBotAccount(
32
+ accountId: string,
33
+ config: WecomBotConfig,
34
+ network?: WecomNetworkConfig,
35
+ ): ResolvedBotAccount {
36
+ const primaryTransport = config.primaryTransport ?? (config.ws ? "ws" : "webhook");
37
+ const wsConfigured = Boolean(config.ws?.botId && config.ws?.secret);
38
+ const webhookConfigured = Boolean(config.webhook?.token && config.webhook?.encodingAESKey);
39
+ const configured = primaryTransport === "ws" ? wsConfigured : webhookConfigured;
40
+ return {
41
+ accountId,
42
+ configured,
43
+ primaryTransport,
44
+ wsConfigured,
45
+ webhookConfigured,
46
+ config,
47
+ network,
48
+ ws: config.ws
49
+ ? {
50
+ botId: config.ws.botId,
51
+ secret: config.ws.secret,
52
+ }
53
+ : undefined,
54
+ webhook: config.webhook
55
+ ? {
56
+ token: config.webhook.token,
57
+ encodingAESKey: config.webhook.encodingAESKey,
58
+ receiveId: config.webhook.receiveId?.trim() ?? "",
59
+ }
60
+ : undefined,
61
+ token: config.webhook?.token ?? "",
62
+ encodingAESKey: config.webhook?.encodingAESKey ?? "",
63
+ receiveId: config.webhook?.receiveId?.trim() ?? "",
64
+ botId: config.ws?.botId ?? "",
65
+ secret: config.ws?.secret ?? "",
66
+ };
59
67
  }
60
68
 
61
- /**
62
- * 解析 Agent 模式账号
63
- */
64
- function resolveAgentAccount(accountId: string, config: WecomAgentConfig, network?: WecomNetworkConfig): ResolvedAgentAccount {
65
- const agentIdRaw = config.agentId;
66
- const agentId = agentIdRaw == null
67
- ? undefined
68
- : (typeof agentIdRaw === "number" ? agentIdRaw : Number(agentIdRaw));
69
- const normalizedAgentId = Number.isFinite(agentId) ? agentId : undefined;
70
-
71
- return {
72
- accountId,
73
- enabled: true,
74
- configured: Boolean(
75
- config.corpId && config.corpSecret &&
76
- config.token && config.encodingAESKey
77
- ),
78
- corpId: config.corpId,
79
- corpSecret: config.corpSecret,
80
- agentId: normalizedAgentId,
81
- token: config.token,
82
- encodingAESKey: config.encodingAESKey,
83
- config,
84
- network,
85
- };
69
+ function resolveAgentAccount(
70
+ accountId: string,
71
+ config: WecomAgentConfig,
72
+ network?: WecomNetworkConfig,
73
+ ): ResolvedAgentAccount {
74
+ const agentId = toNumber(config.agentId);
75
+ const callbackConfigured = Boolean(config.token && config.encodingAESKey);
76
+ const apiConfigured = Boolean(config.corpId && config.corpSecret && agentId);
77
+ return {
78
+ accountId,
79
+ configured: callbackConfigured || apiConfigured,
80
+ callbackConfigured,
81
+ apiConfigured,
82
+ corpId: config.corpId,
83
+ corpSecret: config.corpSecret,
84
+ agentId,
85
+ token: config.token,
86
+ encodingAESKey: config.encodingAESKey,
87
+ config,
88
+ network,
89
+ };
86
90
  }
87
91
 
88
92
  function toResolvedAccount(params: {
89
- accountId: string;
90
- enabled: boolean;
91
- name?: string;
92
- config: WecomAccountConfig;
93
- network?: WecomNetworkConfig;
93
+ accountId: string;
94
+ enabled: boolean;
95
+ name?: string;
96
+ config: WecomAccountConfig;
97
+ network?: WecomNetworkConfig;
94
98
  }): ResolvedWecomAccount {
95
- const bot = params.config.bot
96
- ? resolveBotAccount(params.accountId, params.config.bot, params.network)
97
- : undefined;
98
- const agent = params.config.agent
99
- ? resolveAgentAccount(params.accountId, params.config.agent, params.network)
100
- : undefined;
101
- const configured = Boolean(bot?.configured || agent?.configured);
102
- return {
103
- accountId: params.accountId,
104
- name: params.name,
105
- enabled: params.enabled,
106
- configured,
107
- config: params.config,
108
- bot,
109
- agent,
110
- };
99
+ const bot = params.config.bot
100
+ ? resolveBotAccount(params.accountId, params.config.bot, params.network)
101
+ : undefined;
102
+ const agent = params.config.agent
103
+ ? resolveAgentAccount(params.accountId, params.config.agent, params.network)
104
+ : undefined;
105
+ return {
106
+ accountId: params.accountId,
107
+ name: params.name,
108
+ enabled: params.enabled,
109
+ configured: Boolean(bot?.configured || agent?.configured),
110
+ config: params.config,
111
+ bot,
112
+ agent,
113
+ };
111
114
  }
112
115
 
113
- function resolveMatrixAccounts(wecom: WecomConfig): Record<string, ResolvedWecomAccount> {
114
- const accounts = wecom.accounts;
115
- if (!accounts || typeof accounts !== "object") return {};
116
-
117
- const resolved: Record<string, ResolvedWecomAccount> = {};
118
- for (const [rawId, entry] of Object.entries(accounts)) {
119
- const accountId = rawId.trim();
120
- if (!accountId || !entry) continue;
121
- const enabled = wecom.enabled !== false && entry.enabled !== false;
122
- const config: WecomAccountConfig = {
123
- enabled: entry.enabled,
124
- name: entry.name,
125
- bot: entry.bot,
126
- agent: entry.agent,
127
- };
128
- resolved[accountId] = toResolvedAccount({
129
- accountId,
130
- enabled,
131
- name: entry.name,
132
- config,
133
- network: wecom.network,
134
- });
135
- }
136
- return resolved;
116
+ export function detectMode(config: WecomConfig | undefined): ResolvedMode {
117
+ if (!config || config.enabled === false) return "disabled";
118
+ if (config.accounts && Object.keys(config.accounts).length > 0) {
119
+ return "matrix";
120
+ }
121
+ if (config.bot || config.agent) {
122
+ return "legacy";
123
+ }
124
+ return "disabled";
137
125
  }
138
126
 
139
- function resolveLegacyAccounts(wecom: WecomConfig): Record<string, ResolvedWecomAccount> {
140
- const config: WecomAccountConfig = {
141
- bot: wecom.bot,
142
- agent: wecom.agent,
143
- };
144
- const account = toResolvedAccount({
145
- accountId: DEFAULT_ACCOUNT_ID,
146
- enabled: wecom.enabled !== false,
147
- config,
148
- network: wecom.network,
127
+ function resolveMatrixAccounts(wecom: WecomConfig): Record<string, ResolvedWecomAccount> {
128
+ const resolved: Record<string, ResolvedWecomAccount> = {};
129
+ for (const [rawId, entry] of Object.entries(wecom.accounts ?? {})) {
130
+ const accountId = rawId.trim();
131
+ if (!accountId || !entry) continue;
132
+ resolved[accountId] = toResolvedAccount({
133
+ accountId,
134
+ enabled: wecom.enabled !== false && entry.enabled !== false,
135
+ name: entry.name,
136
+ config: entry,
137
+ network: wecom.network,
149
138
  });
150
- return { [DEFAULT_ACCOUNT_ID]: account };
139
+ }
140
+ return resolved;
151
141
  }
152
142
 
153
- function normalizeDuplicateKey(value: string): string {
154
- return value.trim().toLowerCase();
155
- }
156
-
157
- function formatBotTokenConflict(params: { accountId: string; ownerAccountId: string }): WecomAccountConflict {
158
- return {
159
- type: "duplicate_bot_token",
160
- accountId: params.accountId,
161
- ownerAccountId: params.ownerAccountId,
162
- message:
163
- `Duplicate WeCom bot token: account "${params.accountId}" shares a token with account "${params.ownerAccountId}". ` +
164
- "Keep one owner account per bot token.",
165
- };
166
- }
167
-
168
- function formatBotAibotidConflict(params: { accountId: string; ownerAccountId: string }): WecomAccountConflict {
169
- return {
170
- type: "duplicate_bot_aibotid",
171
- accountId: params.accountId,
172
- ownerAccountId: params.ownerAccountId,
173
- message:
174
- `Duplicate WeCom bot aibotid: account "${params.accountId}" shares aibotid with account "${params.ownerAccountId}". ` +
175
- "Keep one owner account per aibotid.",
176
- };
143
+ function resolveLegacyAccounts(wecom: WecomConfig): Record<string, ResolvedWecomAccount> {
144
+ const config: WecomAccountConfig = {
145
+ bot: wecom.bot,
146
+ agent: wecom.agent,
147
+ };
148
+ return {
149
+ [DEFAULT_ACCOUNT_ID]: toResolvedAccount({
150
+ accountId: DEFAULT_ACCOUNT_ID,
151
+ enabled: wecom.enabled !== false,
152
+ config,
153
+ network: wecom.network,
154
+ }),
155
+ };
177
156
  }
178
157
 
179
- function formatAgentIdConflict(params: { accountId: string; ownerAccountId: string; corpId: string; agentId: number }): WecomAccountConflict {
180
- return {
181
- type: "duplicate_agent_id",
182
- accountId: params.accountId,
183
- ownerAccountId: params.ownerAccountId,
184
- message:
185
- `Duplicate WeCom agent identity: account "${params.accountId}" shares corpId/agentId (${params.corpId}/${params.agentId}) with account "${params.ownerAccountId}". ` +
186
- "Keep one owner account per corpId/agentId pair.",
187
- };
158
+ function normalizeKey(value: string): string {
159
+ return value.trim().toLowerCase();
188
160
  }
189
161
 
190
162
  function collectWecomAccountConflicts(cfg: OpenClawConfig): Map<string, WecomAccountConflict> {
191
- const resolved = resolveWecomAccounts(cfg);
192
- const conflicts = new Map<string, WecomAccountConflict>();
193
- const botTokenOwners = new Map<string, string>();
194
- const botAibotidOwners = new Map<string, string>();
195
- const agentOwners = new Map<string, string>();
196
-
197
- const accountIds = Object.keys(resolved.accounts).sort((a, b) => a.localeCompare(b));
198
- for (const accountId of accountIds) {
199
- const account = resolved.accounts[accountId];
200
- if (!account || account.enabled === false) {
201
- continue;
202
- }
203
- const bot = account.bot;
204
- const agent = account.agent;
205
-
206
- const botToken = bot?.token?.trim();
207
- if (botToken) {
208
- const key = normalizeDuplicateKey(botToken);
209
- const owner = botTokenOwners.get(key);
210
- if (owner && owner !== accountId) {
211
- conflicts.set(accountId, formatBotTokenConflict({ accountId, ownerAccountId: owner }));
212
- } else {
213
- botTokenOwners.set(key, accountId);
214
- }
215
- }
216
-
217
- const botAibotid = bot?.config.aibotid?.trim();
218
- if (botAibotid) {
219
- const key = normalizeDuplicateKey(botAibotid);
220
- const owner = botAibotidOwners.get(key);
221
- if (owner && owner !== accountId) {
222
- conflicts.set(accountId, formatBotAibotidConflict({ accountId, ownerAccountId: owner }));
223
- } else {
224
- botAibotidOwners.set(key, accountId);
225
- }
226
- }
163
+ const resolved = resolveWecomAccounts(cfg);
164
+ const conflicts = new Map<string, WecomAccountConflict>();
165
+ const botOwners = new Map<string, string>();
166
+ const agentOwners = new Map<string, string>();
167
+
168
+ for (const accountId of Object.keys(resolved.accounts).sort((a, b) => a.localeCompare(b))) {
169
+ const account = resolved.accounts[accountId];
170
+ if (!account || account.enabled === false) continue;
171
+
172
+ const botId = account.bot?.botId?.trim();
173
+ if (botId) {
174
+ const key = normalizeKey(botId);
175
+ const owner = botOwners.get(key);
176
+ if (owner && owner !== accountId) {
177
+ conflicts.set(accountId, {
178
+ type: "duplicate_bot_id",
179
+ accountId,
180
+ ownerAccountId: owner,
181
+ message:
182
+ `Duplicate WeCom botId: account "${accountId}" shares botId with account "${owner}". ` +
183
+ "Keep one owner account per botId.",
184
+ });
185
+ } else {
186
+ botOwners.set(key, accountId);
187
+ }
188
+ }
227
189
 
228
- const corpId = agent?.corpId?.trim();
229
- const agentId = agent?.agentId;
230
- if (corpId && typeof agentId === "number" && Number.isFinite(agentId)) {
231
- const key = `${normalizeDuplicateKey(corpId)}:${agentId}`;
232
- const owner = agentOwners.get(key);
233
- if (owner && owner !== accountId) {
234
- conflicts.set(accountId, formatAgentIdConflict({ accountId, ownerAccountId: owner, corpId, agentId }));
235
- } else {
236
- agentOwners.set(key, accountId);
237
- }
238
- }
190
+ const corpId = account.agent?.corpId?.trim();
191
+ const agentId = account.agent?.agentId;
192
+ if (corpId && typeof agentId === "number") {
193
+ const key = `${normalizeKey(corpId)}:${agentId}`;
194
+ const owner = agentOwners.get(key);
195
+ if (owner && owner !== accountId) {
196
+ conflicts.set(accountId, {
197
+ type: "duplicate_agent_id",
198
+ accountId,
199
+ ownerAccountId: owner,
200
+ message:
201
+ `Duplicate WeCom agent identity: account "${accountId}" shares corpId/agentId (${corpId}/${agentId}) with account "${owner}". ` +
202
+ "Keep one owner account per corpId/agentId pair.",
203
+ });
204
+ } else {
205
+ agentOwners.set(key, accountId);
206
+ }
239
207
  }
208
+ }
240
209
 
241
- return conflicts;
210
+ return conflicts;
242
211
  }
243
212
 
244
213
  export function resolveWecomAccountConflict(params: {
245
- cfg: OpenClawConfig;
246
- accountId: string;
214
+ cfg: OpenClawConfig;
215
+ accountId: string;
247
216
  }): WecomAccountConflict | undefined {
248
- return collectWecomAccountConflicts(params.cfg).get(params.accountId);
217
+ return collectWecomAccountConflicts(params.cfg).get(params.accountId);
249
218
  }
250
219
 
251
220
  export function listWecomAccountIds(cfg: OpenClawConfig): string[] {
252
- const wecom = cfg.channels?.wecom as WecomConfig | undefined;
253
- const mode = detectMode(wecom);
254
- if (mode === "matrix" && wecom?.accounts) {
255
- const ids = Object.keys(wecom.accounts)
256
- .map((id) => id.trim())
257
- .filter(Boolean)
258
- .sort((a, b) => a.localeCompare(b));
259
- if (ids.length > 0) return ids;
260
- }
221
+ const wecom = cfg.channels?.wecom as WecomConfig | undefined;
222
+ const mode = detectMode(wecom);
223
+ if (mode === "matrix") {
224
+ return Object.keys(wecom?.accounts ?? {})
225
+ .map((value) => value.trim())
226
+ .filter(Boolean)
227
+ .sort((a, b) => a.localeCompare(b));
228
+ }
229
+ if (mode === "legacy") {
261
230
  return [DEFAULT_ACCOUNT_ID];
231
+ }
232
+ return [];
262
233
  }
263
234
 
264
235
  export function resolveDefaultWecomAccountId(cfg: OpenClawConfig): string {
265
- const wecom = cfg.channels?.wecom as WecomConfig | undefined;
266
- const ids = listWecomAccountIds(cfg);
267
- const preferred = wecom?.defaultAccount?.trim();
268
- if (preferred && ids.includes(preferred)) return preferred;
269
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
236
+ const wecom = cfg.channels?.wecom as WecomConfig | undefined;
237
+ const ids = listWecomAccountIds(cfg);
238
+ if (wecom?.defaultAccount && ids.includes(wecom.defaultAccount)) {
239
+ return wecom.defaultAccount;
240
+ }
241
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
270
242
  }
271
243
 
272
- export function resolveWecomAccount(params: {
273
- cfg: OpenClawConfig;
274
- accountId?: string | null;
275
- }): ResolvedWecomAccount {
276
- const resolved = resolveWecomAccounts(params.cfg);
277
- const fallbackId = resolved.defaultAccountId;
278
- const requestedId = params.accountId?.trim();
279
- if (requestedId) {
280
- return (
281
- resolved.accounts[requestedId] ??
282
- toResolvedAccount({
283
- accountId: requestedId,
284
- enabled: false,
285
- config: {},
286
- })
287
- );
288
- }
289
- return (
290
- resolved.accounts[fallbackId] ??
291
- resolved.accounts[DEFAULT_ACCOUNT_ID] ??
292
- toResolvedAccount({
293
- accountId: fallbackId,
294
- enabled: false,
295
- config: {},
296
- })
297
- );
298
- }
299
-
300
- /**
301
- * 解析 WeCom 账号 (双模式)
302
- */
303
244
  export function resolveWecomAccounts(cfg: OpenClawConfig): ResolvedWecomAccounts {
304
- const wecom = cfg.channels?.wecom as WecomConfig | undefined;
305
-
306
- if (!wecom || wecom.enabled === false) {
307
- return {
308
- mode: "disabled",
309
- defaultAccountId: DEFAULT_ACCOUNT_ID,
310
- accounts: {},
311
- };
312
- }
313
-
314
- const mode = detectMode(wecom);
315
- const accounts = mode === "matrix" ? resolveMatrixAccounts(wecom) : resolveLegacyAccounts(wecom);
316
- const defaultAccountId = resolveDefaultWecomAccountId(cfg);
317
- const defaultAccount = accounts[defaultAccountId] ?? accounts[DEFAULT_ACCOUNT_ID];
245
+ const wecom = (cfg.channels?.wecom as WecomConfig | undefined) ?? {};
246
+ const mode = detectMode(wecom);
247
+ const accounts = mode === "matrix" ? resolveMatrixAccounts(wecom) : mode === "legacy" ? resolveLegacyAccounts(wecom) : {};
248
+ const defaultAccountId = resolveDefaultWecomAccountId(cfg);
249
+ return {
250
+ mode,
251
+ defaultAccountId,
252
+ accounts,
253
+ bot: accounts[defaultAccountId]?.bot,
254
+ agent: accounts[defaultAccountId]?.agent,
255
+ };
256
+ }
318
257
 
319
- return {
320
- mode,
321
- defaultAccountId,
322
- accounts,
323
- bot: defaultAccount?.bot,
324
- agent: defaultAccount?.agent,
325
- };
258
+ export function resolveWecomAccount(params: {
259
+ cfg: OpenClawConfig;
260
+ accountId?: string | null;
261
+ }): ResolvedWecomAccount {
262
+ const resolved = resolveWecomAccounts(params.cfg);
263
+ const accountId = params.accountId?.trim() || resolved.defaultAccountId;
264
+ const account = resolved.accounts[accountId];
265
+ if (!account) {
266
+ throw new Error(`WeCom account "${accountId}" not found.`);
267
+ }
268
+ return account;
326
269
  }
327
270
 
328
- /**
329
- * 检查是否有任何模式启用
330
- */
331
271
  export function isWecomEnabled(cfg: OpenClawConfig): boolean {
332
- const resolved = resolveWecomAccounts(cfg);
333
- return Object.values(resolved.accounts).some((account) => account.configured && account.enabled);
272
+ const resolved = resolveWecomAccounts(cfg);
273
+ return Object.values(resolved.accounts).some((account) => account.enabled && account.configured);
334
274
  }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
4
+
5
+ import { resolveDerivedPathSummary } from "./derived-paths.js";
6
+ import {
7
+ hasMatrixExplicitRoutesRegistered,
8
+ registerAgentWebhookTarget,
9
+ registerWecomWebhookTarget,
10
+ } from "../transport/http/registry.js";
11
+ import type { ResolvedAgentAccount, ResolvedBotAccount } from "../types/index.js";
12
+
13
+ function createBotAccount(accountId: string): ResolvedBotAccount {
14
+ return {
15
+ accountId,
16
+ configured: true,
17
+ primaryTransport: "webhook",
18
+ wsConfigured: false,
19
+ webhookConfigured: true,
20
+ config: {} as ResolvedBotAccount["config"],
21
+ token: "token",
22
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
23
+ receiveId: "",
24
+ botId: "",
25
+ secret: "",
26
+ webhook: {
27
+ token: "token",
28
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
29
+ receiveId: "",
30
+ },
31
+ };
32
+ }
33
+
34
+ function createAgentAccount(accountId: string): ResolvedAgentAccount {
35
+ return {
36
+ accountId,
37
+ configured: true,
38
+ callbackConfigured: true,
39
+ apiConfigured: true,
40
+ corpId: `corp-${accountId}`,
41
+ corpSecret: `secret-${accountId}`,
42
+ agentId: 1001,
43
+ token: "token",
44
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
45
+ config: {} as ResolvedAgentAccount["config"],
46
+ };
47
+ }
48
+
49
+ const emptyConfig = {} as OpenClawConfig;
50
+ const emptyCore = {} as PluginRuntime;
51
+
52
+ describe("resolveDerivedPathSummary", () => {
53
+ it("registers scoped aliases first for the default account", () => {
54
+ expect(resolveDerivedPathSummary("default")).toEqual({
55
+ botWebhook: [
56
+ "/plugins/wecom/bot/default",
57
+ "/wecom/bot/default",
58
+ "/plugins/wecom/bot",
59
+ "/wecom",
60
+ "/wecom/bot",
61
+ ],
62
+ agentCallback: [
63
+ "/plugins/wecom/agent/default",
64
+ "/wecom/agent/default",
65
+ "/plugins/wecom/agent",
66
+ "/wecom/agent",
67
+ ],
68
+ });
69
+ });
70
+ });
71
+
72
+ describe("hasMatrixExplicitRoutesRegistered", () => {
73
+ it("ignores default-account scoped aliases", () => {
74
+ const unregisterBot = registerWecomWebhookTarget({
75
+ account: createBotAccount("default"),
76
+ config: emptyConfig,
77
+ runtime: {},
78
+ core: emptyCore,
79
+ path: "/plugins/wecom/bot/default",
80
+ });
81
+ const unregisterAgent = registerAgentWebhookTarget({
82
+ agent: createAgentAccount("default"),
83
+ config: emptyConfig,
84
+ runtimeEnv: {},
85
+ path: "/plugins/wecom/agent/default",
86
+ });
87
+
88
+ try {
89
+ expect(hasMatrixExplicitRoutesRegistered()).toBe(false);
90
+ } finally {
91
+ unregisterAgent();
92
+ unregisterBot();
93
+ }
94
+ });
95
+
96
+ it("detects non-default explicit account routes", () => {
97
+ const unregister = registerWecomWebhookTarget({
98
+ account: createBotAccount("acct-a"),
99
+ config: emptyConfig,
100
+ runtime: {},
101
+ core: emptyCore,
102
+ path: "/plugins/wecom/bot/acct-a",
103
+ });
104
+
105
+ try {
106
+ expect(hasMatrixExplicitRoutesRegistered()).toBe(true);
107
+ } finally {
108
+ unregister();
109
+ }
110
+ });
111
+ });