@yanhaidao/wecom 2.2.7 → 2.3.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 (54) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/CLAUDE.md +1 -1
  3. package/GOVERNANCE.md +26 -0
  4. package/LICENSE +7 -0
  5. package/README.md +275 -91
  6. package/assets/01.bot-add.png +0 -0
  7. package/assets/01.bot-setp2.png +0 -0
  8. package/assets/02.agent.add.png +0 -0
  9. package/assets/02.agent.api-set.png +0 -0
  10. package/assets/register.png +0 -0
  11. package/changelog/v2.2.28.md +70 -0
  12. package/changelog/v2.3.2.md +70 -0
  13. package/compat-single-account.md +118 -0
  14. package/package.json +10 -2
  15. package/src/accounts.ts +17 -55
  16. package/src/agent/api-client.ts +84 -37
  17. package/src/agent/api-client.upload.test.ts +110 -0
  18. package/src/agent/handler.event-filter.test.ts +50 -0
  19. package/src/agent/handler.ts +147 -145
  20. package/src/channel.config.test.ts +147 -0
  21. package/src/channel.lifecycle.test.ts +234 -0
  22. package/src/channel.ts +90 -140
  23. package/src/config/accounts.resolve.test.ts +38 -0
  24. package/src/config/accounts.ts +257 -22
  25. package/src/config/index.ts +6 -0
  26. package/src/config/network.ts +9 -5
  27. package/src/config/routing.test.ts +88 -0
  28. package/src/config/routing.ts +26 -0
  29. package/src/config/schema.ts +35 -4
  30. package/src/config-schema.ts +5 -41
  31. package/src/dynamic-agent.account-scope.test.ts +17 -0
  32. package/src/dynamic-agent.ts +13 -13
  33. package/src/gateway-monitor.ts +200 -0
  34. package/src/http.ts +16 -2
  35. package/src/media.test.ts +28 -1
  36. package/src/media.ts +59 -1
  37. package/src/monitor/state.queue.test.ts +1 -1
  38. package/src/monitor/state.ts +1 -1
  39. package/src/monitor/types.ts +1 -1
  40. package/src/monitor.active.test.ts +13 -7
  41. package/src/monitor.inbound-filter.test.ts +63 -0
  42. package/src/monitor.ts +948 -128
  43. package/src/monitor.webhook.test.ts +288 -3
  44. package/src/outbound.test.ts +130 -0
  45. package/src/outbound.ts +44 -9
  46. package/src/shared/command-auth.ts +4 -2
  47. package/src/shared/xml-parser.test.ts +21 -1
  48. package/src/shared/xml-parser.ts +18 -0
  49. package/src/types/account.ts +43 -14
  50. package/src/types/config.ts +37 -2
  51. package/src/types/index.ts +3 -0
  52. package/src/types.ts +29 -147
  53. package/GEMINI.md +0 -76
  54. package//345/212/250/346/200/201Agent/350/267/257/347/224/261.md +0 -360
@@ -5,66 +5,79 @@
5
5
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
6
6
  import type {
7
7
  WecomConfig,
8
+ WecomAccountConfig,
8
9
  WecomBotConfig,
9
10
  WecomAgentConfig,
10
11
  WecomNetworkConfig,
12
+ ResolvedWecomAccount,
11
13
  ResolvedBotAccount,
12
14
  ResolvedAgentAccount,
13
15
  ResolvedMode,
14
16
  ResolvedWecomAccounts,
15
17
  } from "../types/index.js";
16
18
 
17
- const DEFAULT_ACCOUNT_ID = "default";
19
+ export const DEFAULT_ACCOUNT_ID = "default";
20
+
21
+ export type WecomAccountConflict = {
22
+ type: "duplicate_bot_token" | "duplicate_bot_aibotid" | "duplicate_agent_id";
23
+ accountId: string;
24
+ ownerAccountId: string;
25
+ message: string;
26
+ };
18
27
 
19
28
  /**
20
29
  * 检测配置中启用的模式
21
30
  */
22
31
  export function detectMode(config: WecomConfig | undefined): ResolvedMode {
23
- if (!config) return { bot: false, agent: false };
32
+ if (!config || config.enabled === false) return "disabled";
24
33
 
25
- const botConfigured = Boolean(
26
- config.bot?.token && config.bot?.encodingAESKey
27
- );
28
- const agentConfigured = Boolean(
29
- config.agent?.corpId && config.agent?.corpSecret && config.agent?.agentId &&
30
- config.agent?.token && config.agent?.encodingAESKey
31
- );
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
+ }
32
41
 
33
- return { bot: botConfigured, agent: agentConfigured };
42
+ return "legacy";
34
43
  }
35
44
 
36
45
  /**
37
46
  * 解析 Bot 模式账号
38
47
  */
39
- function resolveBotAccount(config: WecomBotConfig): ResolvedBotAccount {
48
+ function resolveBotAccount(accountId: string, config: WecomBotConfig, network?: WecomNetworkConfig): ResolvedBotAccount {
40
49
  return {
41
- accountId: DEFAULT_ACCOUNT_ID,
50
+ accountId,
42
51
  enabled: true,
43
52
  configured: Boolean(config.token && config.encodingAESKey),
44
53
  token: config.token,
45
54
  encodingAESKey: config.encodingAESKey,
46
55
  receiveId: config.receiveId?.trim() ?? "",
47
56
  config,
57
+ network,
48
58
  };
49
59
  }
50
60
 
51
61
  /**
52
62
  * 解析 Agent 模式账号
53
63
  */
54
- function resolveAgentAccount(config: WecomAgentConfig, network?: WecomNetworkConfig): ResolvedAgentAccount {
64
+ function resolveAgentAccount(accountId: string, config: WecomAgentConfig, network?: WecomNetworkConfig): ResolvedAgentAccount {
55
65
  const agentIdRaw = config.agentId;
56
- const agentId = typeof agentIdRaw === "number" ? agentIdRaw : Number(agentIdRaw);
66
+ const agentId = agentIdRaw == null
67
+ ? undefined
68
+ : (typeof agentIdRaw === "number" ? agentIdRaw : Number(agentIdRaw));
69
+ const normalizedAgentId = Number.isFinite(agentId) ? agentId : undefined;
57
70
 
58
71
  return {
59
- accountId: DEFAULT_ACCOUNT_ID,
72
+ accountId,
60
73
  enabled: true,
61
74
  configured: Boolean(
62
- config.corpId && config.corpSecret && agentId &&
75
+ config.corpId && config.corpSecret &&
63
76
  config.token && config.encodingAESKey
64
77
  ),
65
78
  corpId: config.corpId,
66
79
  corpSecret: config.corpSecret,
67
- agentId,
80
+ agentId: normalizedAgentId,
68
81
  token: config.token,
69
82
  encodingAESKey: config.encodingAESKey,
70
83
  config,
@@ -72,6 +85,218 @@ function resolveAgentAccount(config: WecomAgentConfig, network?: WecomNetworkCon
72
85
  };
73
86
  }
74
87
 
88
+ function toResolvedAccount(params: {
89
+ accountId: string;
90
+ enabled: boolean;
91
+ name?: string;
92
+ config: WecomAccountConfig;
93
+ network?: WecomNetworkConfig;
94
+ }): 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
+ };
111
+ }
112
+
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;
137
+ }
138
+
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,
149
+ });
150
+ return { [DEFAULT_ACCOUNT_ID]: account };
151
+ }
152
+
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
+ };
177
+ }
178
+
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
+ };
188
+ }
189
+
190
+ 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
+ }
227
+
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
+ }
239
+ }
240
+
241
+ return conflicts;
242
+ }
243
+
244
+ export function resolveWecomAccountConflict(params: {
245
+ cfg: OpenClawConfig;
246
+ accountId: string;
247
+ }): WecomAccountConflict | undefined {
248
+ return collectWecomAccountConflicts(params.cfg).get(params.accountId);
249
+ }
250
+
251
+ 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
+ }
261
+ return [DEFAULT_ACCOUNT_ID];
262
+ }
263
+
264
+ 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;
270
+ }
271
+
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
+
75
300
  /**
76
301
  * 解析 WeCom 账号 (双模式)
77
302
  */
@@ -79,14 +304,24 @@ export function resolveWecomAccounts(cfg: OpenClawConfig): ResolvedWecomAccounts
79
304
  const wecom = cfg.channels?.wecom as WecomConfig | undefined;
80
305
 
81
306
  if (!wecom || wecom.enabled === false) {
82
- return {};
307
+ return {
308
+ mode: "disabled",
309
+ defaultAccountId: DEFAULT_ACCOUNT_ID,
310
+ accounts: {},
311
+ };
83
312
  }
84
313
 
85
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];
86
318
 
87
319
  return {
88
- bot: mode.bot && wecom.bot ? { ...resolveBotAccount(wecom.bot), network: wecom.network } : undefined,
89
- agent: mode.agent && wecom.agent ? resolveAgentAccount(wecom.agent, wecom.network) : undefined,
320
+ mode,
321
+ defaultAccountId,
322
+ accounts,
323
+ bot: defaultAccount?.bot,
324
+ agent: defaultAccount?.agent,
90
325
  };
91
326
  }
92
327
 
@@ -94,6 +329,6 @@ export function resolveWecomAccounts(cfg: OpenClawConfig): ResolvedWecomAccounts
94
329
  * 检查是否有任何模式启用
95
330
  */
96
331
  export function isWecomEnabled(cfg: OpenClawConfig): boolean {
97
- const accounts = resolveWecomAccounts(cfg);
98
- return Boolean(accounts.bot?.configured || accounts.agent?.configured);
332
+ const resolved = resolveWecomAccounts(cfg);
333
+ return Object.values(resolved.accounts).some((account) => account.configured && account.enabled);
99
334
  }
@@ -4,9 +4,15 @@
4
4
 
5
5
  export { WecomConfigSchema, type WecomConfigInput } from "./schema.js";
6
6
  export {
7
+ DEFAULT_ACCOUNT_ID,
7
8
  detectMode,
9
+ listWecomAccountIds,
10
+ resolveDefaultWecomAccountId,
11
+ resolveWecomAccount,
12
+ resolveWecomAccountConflict,
8
13
  resolveWecomAccounts,
9
14
  isWecomEnabled,
10
15
  } from "./accounts.js";
11
16
  export { resolveWecomEgressProxyUrl, resolveWecomEgressProxyUrlFromNetwork } from "./network.js";
12
17
  export { DEFAULT_WECOM_MEDIA_MAX_BYTES, resolveWecomMediaMaxBytes } from "./media.js";
18
+ export { resolveWecomFailClosedOnDefaultRoute, shouldRejectWecomDefaultRoute } from "./routing.js";
@@ -3,11 +3,15 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
3
  import type { WecomConfig, WecomNetworkConfig } from "../types/index.js";
4
4
 
5
5
  export function resolveWecomEgressProxyUrlFromNetwork(network?: WecomNetworkConfig): string | undefined {
6
- const env = (process.env.OPENCLAW_WECOM_EGRESS_PROXY_URL ?? process.env.WECOM_EGRESS_PROXY_URL ?? "").trim();
7
- if (env) return env;
8
-
9
- const fromCfg = network?.egressProxyUrl?.trim() ?? "";
10
- return fromCfg || undefined;
6
+ const proxyUrl = network?.egressProxyUrl ??
7
+ process.env.OPENCLAW_WECOM_EGRESS_PROXY_URL ??
8
+ process.env.WECOM_EGRESS_PROXY_URL ??
9
+ process.env.HTTPS_PROXY ??
10
+ process.env.ALL_PROXY ??
11
+ process.env.HTTP_PROXY ??
12
+ "";
13
+
14
+ return proxyUrl.trim() || undefined;
11
15
  }
12
16
 
13
17
  export function resolveWecomEgressProxyUrl(cfg: OpenClawConfig): string | undefined {
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
+
4
+ import { resolveWecomFailClosedOnDefaultRoute, shouldRejectWecomDefaultRoute } from "./routing.js";
5
+
6
+ describe("resolveWecomFailClosedOnDefaultRoute", () => {
7
+ it("defaults to true in matrix mode", () => {
8
+ const cfg: OpenClawConfig = {
9
+ channels: {
10
+ wecom: {
11
+ enabled: true,
12
+ accounts: {
13
+ a: { enabled: true, bot: { token: "t1", encodingAESKey: "k1" } },
14
+ },
15
+ },
16
+ },
17
+ } as OpenClawConfig;
18
+ expect(resolveWecomFailClosedOnDefaultRoute(cfg)).toBe(true);
19
+ });
20
+
21
+ it("defaults to false in legacy mode", () => {
22
+ const cfg: OpenClawConfig = {
23
+ channels: {
24
+ wecom: {
25
+ enabled: true,
26
+ bot: { token: "t1", encodingAESKey: "k1" },
27
+ },
28
+ },
29
+ } as OpenClawConfig;
30
+ expect(resolveWecomFailClosedOnDefaultRoute(cfg)).toBe(false);
31
+ });
32
+
33
+ it("respects explicit override", () => {
34
+ const cfg: OpenClawConfig = {
35
+ channels: {
36
+ wecom: {
37
+ enabled: true,
38
+ bot: { token: "t1", encodingAESKey: "k1" },
39
+ routing: { failClosedOnDefaultRoute: true },
40
+ },
41
+ },
42
+ } as OpenClawConfig;
43
+ expect(resolveWecomFailClosedOnDefaultRoute(cfg)).toBe(true);
44
+ });
45
+ });
46
+
47
+ describe("shouldRejectWecomDefaultRoute", () => {
48
+ const matrixCfg = {
49
+ channels: {
50
+ wecom: {
51
+ enabled: true,
52
+ accounts: {
53
+ a: { enabled: true, bot: { token: "t1", encodingAESKey: "k1" } },
54
+ },
55
+ },
56
+ },
57
+ } as OpenClawConfig;
58
+
59
+ it("rejects default route in matrix mode when dynamic agent is disabled", () => {
60
+ expect(
61
+ shouldRejectWecomDefaultRoute({
62
+ cfg: matrixCfg,
63
+ matchedBy: "default",
64
+ useDynamicAgent: false,
65
+ }),
66
+ ).toBe(true);
67
+ });
68
+
69
+ it("does not reject when route already matched a binding", () => {
70
+ expect(
71
+ shouldRejectWecomDefaultRoute({
72
+ cfg: matrixCfg,
73
+ matchedBy: "binding.account",
74
+ useDynamicAgent: false,
75
+ }),
76
+ ).toBe(false);
77
+ });
78
+
79
+ it("does not reject when dynamic agent routing is enabled", () => {
80
+ expect(
81
+ shouldRejectWecomDefaultRoute({
82
+ cfg: matrixCfg,
83
+ matchedBy: "default",
84
+ useDynamicAgent: true,
85
+ }),
86
+ ).toBe(false);
87
+ });
88
+ });
@@ -0,0 +1,26 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+
3
+ import type { WecomConfig } from "../types/index.js";
4
+ import { detectMode } from "./accounts.js";
5
+
6
+ /**
7
+ * 默认策略:
8
+ * - matrix(多账号): 开启 fail-closed,防止未绑定账号回退到 main
9
+ * - legacy(单账号兼容): 维持历史行为,不强制拦截
10
+ */
11
+ export function resolveWecomFailClosedOnDefaultRoute(cfg: OpenClawConfig): boolean {
12
+ const wecom = cfg.channels?.wecom as WecomConfig | undefined;
13
+ const explicit = wecom?.routing?.failClosedOnDefaultRoute;
14
+ if (typeof explicit === "boolean") return explicit;
15
+ return detectMode(wecom) === "matrix";
16
+ }
17
+
18
+ export function shouldRejectWecomDefaultRoute(params: {
19
+ cfg: OpenClawConfig;
20
+ matchedBy: string;
21
+ useDynamicAgent: boolean;
22
+ }): boolean {
23
+ if (params.matchedBy !== "default") return false;
24
+ if (params.useDynamicAgent) return false;
25
+ return resolveWecomFailClosedOnDefaultRoute(params.cfg);
26
+ }
@@ -4,6 +4,14 @@
4
4
 
5
5
  import { z } from "zod";
6
6
 
7
+ function bindToJsonSchema<T extends z.ZodTypeAny>(schema: T): T {
8
+ const anySchema = schema as unknown as { toJSONSchema?: (...args: any[]) => unknown };
9
+ if (typeof anySchema.toJSONSchema === "function") {
10
+ anySchema.toJSONSchema = anySchema.toJSONSchema.bind(schema) as any;
11
+ }
12
+ return schema;
13
+ }
14
+
7
15
  /**
8
16
  * **dmSchema (单聊配置)**
9
17
  *
@@ -50,6 +58,16 @@ const networkSchema = z.object({
50
58
  egressProxyUrl: z.string().optional(),
51
59
  }).optional();
52
60
 
61
+ /**
62
+ * **routingSchema (路由策略配置)**
63
+ *
64
+ * 控制未命中 bindings 时的回退行为。
65
+ * @property failClosedOnDefaultRoute - true=拒绝 default 回退,false=允许回退默认 agent
66
+ */
67
+ const routingSchema = z.object({
68
+ failClosedOnDefaultRoute: z.boolean().optional(),
69
+ }).optional();
70
+
53
71
  /**
54
72
  * **botSchema (Bot 模式配置)**
55
73
  *
@@ -62,8 +80,10 @@ const networkSchema = z.object({
62
80
  * @property dm - 单聊策略覆盖配置
63
81
  */
64
82
  const botSchema = z.object({
83
+ aibotid: z.string().optional(),
65
84
  token: z.string(),
66
85
  encodingAESKey: z.string(),
86
+ botIds: z.array(z.string()).optional(),
67
87
  receiveId: z.string().optional(),
68
88
  streamPlaceholderContent: z.string().optional(),
69
89
  welcomeText: z.string().optional(),
@@ -76,7 +96,7 @@ const botSchema = z.object({
76
96
  * 用于配置企业微信自建应用 (Agent)。
77
97
  * @property corpId - 企业 ID (CorpID)
78
98
  * @property corpSecret - 应用 Secret
79
- * @property agentId - 应用 AgentId (数字)
99
+ * @property agentId - 应用 AgentId (数字,可选)
80
100
  * @property token - 回调配置 Token
81
101
  * @property encodingAESKey - 回调配置 EncodingAESKey
82
102
  * @property welcomeText - (可选) 欢迎语
@@ -85,7 +105,7 @@ const botSchema = z.object({
85
105
  const agentSchema = z.object({
86
106
  corpId: z.string(),
87
107
  corpSecret: z.string(),
88
- agentId: z.union([z.string(), z.number()]),
108
+ agentId: z.union([z.string(), z.number()]).optional(),
89
109
  token: z.string(),
90
110
  encodingAESKey: z.string(),
91
111
  welcomeText: z.string().optional(),
@@ -108,14 +128,25 @@ const dynamicAgentsSchema = z.object({
108
128
  adminUsers: z.array(z.string()).optional(),
109
129
  }).optional();
110
130
 
131
+ /** Matrix 账号条目 */
132
+ const accountSchema = z.object({
133
+ enabled: z.boolean().optional(),
134
+ name: z.string().optional(),
135
+ bot: botSchema,
136
+ agent: agentSchema,
137
+ });
138
+
111
139
  /** 顶层 WeCom 配置 Schema */
112
- export const WecomConfigSchema = z.object({
140
+ export const WecomConfigSchema = bindToJsonSchema(z.object({
113
141
  enabled: z.boolean().optional(),
114
142
  bot: botSchema,
115
143
  agent: agentSchema,
144
+ accounts: z.record(z.string(), accountSchema).optional(),
145
+ defaultAccount: z.string().optional(),
116
146
  media: mediaSchema,
117
147
  network: networkSchema,
148
+ routing: routingSchema,
118
149
  dynamicAgents: dynamicAgentsSchema,
119
- });
150
+ }));
120
151
 
121
152
  export type WecomConfigInput = z.infer<typeof WecomConfigSchema>;
@@ -1,41 +1,5 @@
1
- import { z } from "zod";
2
-
3
- const allowFromEntry = z.union([z.string(), z.number()]);
4
-
5
- const dmSchema = z
6
- .object({
7
- enabled: z.boolean().optional(),
8
- policy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
9
- allowFrom: z.array(allowFromEntry).optional(),
10
- })
11
- .optional();
12
-
13
- export const WecomConfigSchema = z.object({
14
- name: z.string().optional(),
15
- enabled: z.boolean().optional(),
16
-
17
- webhookPath: z.string().optional(),
18
- token: z.string().optional(),
19
- encodingAESKey: z.string().optional(),
20
- receiveId: z.string().optional(),
21
-
22
- streamPlaceholderContent: z.string().optional(),
23
- debounceMs: z.number().optional(),
24
-
25
- welcomeText: z.string().optional(),
26
- dm: dmSchema,
27
-
28
- defaultAccount: z.string().optional(),
29
- accounts: z.object({}).catchall(z.object({
30
- name: z.string().optional(),
31
- enabled: z.boolean().optional(),
32
- webhookPath: z.string().optional(),
33
- token: z.string().optional(),
34
- encodingAESKey: z.string().optional(),
35
- receiveId: z.string().optional(),
36
- streamPlaceholderContent: z.string().optional(),
37
- debounceMs: z.number().optional(),
38
- welcomeText: z.string().optional(),
39
- dm: dmSchema,
40
- })).optional(),
41
- });
1
+ /**
2
+ * Backward-compatible schema export.
3
+ * Canonical schema lives in `src/config/schema.ts`.
4
+ */
5
+ export { WecomConfigSchema, type WecomConfigInput } from "./config/schema.js";
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { generateAgentId } from "./dynamic-agent.js";
4
+
5
+ describe("generateAgentId account scoping", () => {
6
+ it("generates different ids for same peer across accounts", () => {
7
+ const a = generateAgentId("dm", "zhangsan", "acct-a");
8
+ const b = generateAgentId("dm", "zhangsan", "acct-b");
9
+ expect(a).toBe("wecom-acct-a-dm-zhangsan");
10
+ expect(b).toBe("wecom-acct-b-dm-zhangsan");
11
+ expect(a).not.toBe(b);
12
+ });
13
+
14
+ it("falls back to default account scope when accountId is omitted", () => {
15
+ expect(generateAgentId("group", "wr123456")).toBe("wecom-default-group-wr123456");
16
+ });
17
+ });