@mocrane/wecom 2026.2.27 → 2026.3.8-4

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/LICENSE +4 -18
  2. package/README.md +572 -0
  3. package/assets/01.bot-add.png +0 -0
  4. package/assets/01.bot-setp2.png +0 -0
  5. package/assets/02.agent.add.png +0 -0
  6. package/assets/02.agent.api-set.png +0 -0
  7. package/assets/register.png +0 -0
  8. package/changelog/v2.2.28.md +70 -0
  9. package/changelog/v2.3.2.md +28 -0
  10. package/changelog/v2.3.4.md +20 -0
  11. package/index.ts +11 -3
  12. package/package.json +4 -2
  13. package/src/accounts.ts +17 -55
  14. package/src/agent/api-client.ts +84 -37
  15. package/src/agent/api-client.upload.test.ts +110 -0
  16. package/src/agent/handler.event-filter.test.ts +50 -0
  17. package/src/agent/handler.ts +147 -145
  18. package/src/channel.config.test.ts +147 -0
  19. package/src/channel.lifecycle.test.ts +252 -0
  20. package/src/channel.ts +132 -141
  21. package/src/config/accounts.resolve.test.ts +38 -0
  22. package/src/config/accounts.ts +267 -25
  23. package/src/config/index.ts +6 -0
  24. package/src/config/network.ts +9 -5
  25. package/src/config/routing.test.ts +88 -0
  26. package/src/config/routing.ts +26 -0
  27. package/src/config/schema.ts +41 -6
  28. package/src/config-schema.ts +5 -41
  29. package/src/dynamic-agent.account-scope.test.ts +17 -0
  30. package/src/dynamic-agent.ts +13 -13
  31. package/src/gateway-monitor.ts +260 -0
  32. package/src/http.ts +16 -2
  33. package/src/media.test.ts +28 -1
  34. package/src/media.ts +59 -1
  35. package/src/monitor/state.queue.test.ts +1 -1
  36. package/src/monitor/state.ts +1 -1
  37. package/src/monitor/types.ts +5 -1
  38. package/src/monitor.active.test.ts +15 -9
  39. package/src/monitor.inbound-filter.test.ts +63 -0
  40. package/src/monitor.integration.test.ts +4 -2
  41. package/src/monitor.ts +982 -134
  42. package/src/monitor.webhook.test.ts +381 -3
  43. package/src/onboarding.ts +379 -54
  44. package/src/outbound.test.ts +130 -0
  45. package/src/outbound.ts +82 -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 +54 -16
  50. package/src/types/config.ts +50 -6
  51. package/src/types/constants.ts +7 -3
  52. package/src/types/index.ts +3 -0
  53. package/src/types.ts +29 -147
  54. package/src/ws-adapter.ts +481 -0
@@ -19,6 +19,38 @@ describe("wecomOutbound", () => {
19
19
  ).rejects.toThrow(/Agent mode/i);
20
20
  });
21
21
 
22
+ it("throws explicit error when outbound accountId does not exist", async () => {
23
+ const { wecomOutbound } = await import("./outbound.js");
24
+ const cfg = {
25
+ channels: {
26
+ wecom: {
27
+ enabled: true,
28
+ defaultAccount: "acct-a",
29
+ accounts: {
30
+ "acct-a": {
31
+ enabled: true,
32
+ agent: {
33
+ corpId: "corp-a",
34
+ corpSecret: "secret-a",
35
+ agentId: 10001,
36
+ token: "token-a",
37
+ encodingAESKey: "aes-a",
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ };
44
+ await expect(
45
+ wecomOutbound.sendText({
46
+ cfg,
47
+ accountId: "acct-missing",
48
+ to: "user:zhangsan",
49
+ text: "hello",
50
+ } as any),
51
+ ).rejects.toThrow(/account "acct-missing" not found/i);
52
+ });
53
+
22
54
  it("routes sendText to agent chatId/userid", async () => {
23
55
  const { wecomOutbound } = await import("./outbound.js");
24
56
  const api = await import("./agent/api-client.js");
@@ -140,4 +172,102 @@ describe("wecomOutbound", () => {
140
172
 
141
173
  now.mockRestore();
142
174
  });
175
+
176
+ it("uses account-scoped agent config in matrix mode", async () => {
177
+ const { wecomOutbound } = await import("./outbound.js");
178
+ const api = await import("./agent/api-client.js");
179
+ (api.sendText as any).mockResolvedValue(undefined);
180
+ (api.sendText as any).mockClear();
181
+
182
+ const cfg = {
183
+ channels: {
184
+ wecom: {
185
+ enabled: true,
186
+ defaultAccount: "acct-a",
187
+ accounts: {
188
+ "acct-a": {
189
+ enabled: true,
190
+ agent: {
191
+ corpId: "corp-a",
192
+ corpSecret: "secret-a",
193
+ agentId: 10001,
194
+ token: "token-a",
195
+ encodingAESKey: "aes-a",
196
+ },
197
+ },
198
+ "acct-b": {
199
+ enabled: true,
200
+ agent: {
201
+ corpId: "corp-b",
202
+ corpSecret: "secret-b",
203
+ agentId: 10002,
204
+ token: "token-b",
205
+ encodingAESKey: "aes-b",
206
+ },
207
+ },
208
+ },
209
+ },
210
+ },
211
+ };
212
+
213
+ await wecomOutbound.sendText({
214
+ cfg,
215
+ accountId: "acct-b",
216
+ to: "user:lisi",
217
+ text: "hello b",
218
+ } as any);
219
+ expect(api.sendText).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ toUser: "lisi",
222
+ agent: expect.objectContaining({
223
+ accountId: "acct-b",
224
+ agentId: 10002,
225
+ corpId: "corp-b",
226
+ }),
227
+ }),
228
+ );
229
+ });
230
+
231
+ it("rejects outbound when target account has matrix conflict", async () => {
232
+ const { wecomOutbound } = await import("./outbound.js");
233
+ const cfg = {
234
+ channels: {
235
+ wecom: {
236
+ enabled: true,
237
+ defaultAccount: "acct-a",
238
+ accounts: {
239
+ "acct-a": {
240
+ enabled: true,
241
+ agent: {
242
+ corpId: "corp-shared",
243
+ corpSecret: "secret-a",
244
+ agentId: 10001,
245
+ token: "token-a",
246
+ encodingAESKey: "aes-a",
247
+ },
248
+ },
249
+ "acct-b": {
250
+ enabled: true,
251
+ agent: {
252
+ corpId: "corp-shared",
253
+ corpSecret: "secret-b",
254
+ agentId: 10001,
255
+ token: "token-b",
256
+ encodingAESKey: "aes-b",
257
+ },
258
+ },
259
+ },
260
+ },
261
+ },
262
+ };
263
+
264
+ await expect(
265
+ wecomOutbound.sendText({
266
+ cfg,
267
+ accountId: "acct-b",
268
+ to: "user:lisi",
269
+ text: "hello",
270
+ } as any),
271
+ ).rejects.toThrow(/duplicate wecom agent identity/i);
272
+ });
143
273
  });
package/src/outbound.ts CHANGED
@@ -1,20 +1,50 @@
1
1
  import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
2
2
 
3
3
  import { sendText as sendAgentText, sendMedia as sendAgentMedia, uploadMedia } from "./agent/api-client.js";
4
- import { resolveWecomAccounts } from "./config/index.js";
4
+ import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
5
5
  import { getWecomRuntime } from "./runtime.js";
6
+ import { getWsClient, waitForWsConnection } from "./ws-adapter.js";
6
7
 
7
8
  import { resolveWecomTarget } from "./target.js";
8
9
 
9
- function resolveAgentConfigOrThrow(cfg: ChannelOutboundContext["cfg"]) {
10
- const account = resolveWecomAccounts(cfg).agent;
10
+ function resolveAgentConfigOrThrow(params: {
11
+ cfg: ChannelOutboundContext["cfg"];
12
+ accountId?: string | null;
13
+ }) {
14
+ const resolvedAccounts = resolveWecomAccounts(params.cfg);
15
+ const conflictAccountId = params.accountId?.trim() || resolvedAccounts.defaultAccountId;
16
+ const conflict = resolveWecomAccountConflict({
17
+ cfg: params.cfg,
18
+ accountId: conflictAccountId,
19
+ });
20
+ if (conflict) {
21
+ throw new Error(conflict.message);
22
+ }
23
+
24
+ const requestedAccountId = params.accountId?.trim();
25
+ if (requestedAccountId) {
26
+ if (!resolvedAccounts.accounts[requestedAccountId]) {
27
+ throw new Error(
28
+ `WeCom outbound account "${requestedAccountId}" not found. Configure channels.wecom.accounts.${requestedAccountId} or use an existing accountId.`,
29
+ );
30
+ }
31
+ }
32
+ const account = resolveWecomAccount({
33
+ cfg: params.cfg,
34
+ accountId: params.accountId,
35
+ }).agent;
11
36
  if (!account?.configured) {
12
37
  throw new Error(
13
- "WeCom outbound requires Agent mode. Configure channels.wecom.agent (corpId/corpSecret/agentId/token/encodingAESKey).",
38
+ `WeCom outbound requires Agent mode for account=${params.accountId ?? "default"}. Configure channels.wecom.accounts.<accountId>.agent (or legacy channels.wecom.agent).`,
39
+ );
40
+ }
41
+ if (typeof account.agentId !== "number" || !Number.isFinite(account.agentId)) {
42
+ throw new Error(
43
+ `WeCom outbound requires channels.wecom.accounts.<accountId>.agent.agentId (or legacy channels.wecom.agent.agentId) for account=${params.accountId ?? account.accountId}.`,
14
44
  );
15
45
  }
16
46
  // 注意:不要在日志里输出 corpSecret 等敏感信息
17
- console.log(`[wecom-outbound] Using agent config: corpId=${account.corpId}, agentId=${account.agentId}`);
47
+ console.log(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
18
48
  return account;
19
49
  }
20
50
 
@@ -29,10 +59,47 @@ export const wecomOutbound: ChannelOutboundAdapter = {
29
59
  return [text];
30
60
  }
31
61
  },
32
- sendText: async ({ cfg, to, text }: ChannelOutboundContext) => {
62
+ sendText: async ({ cfg, to, text, accountId }: ChannelOutboundContext) => {
33
63
  // signal removed - not supported in current SDK
34
64
 
35
- const agent = resolveAgentConfigOrThrow(cfg);
65
+ // ── Bot WebSocket outbound 独立路径 ──
66
+ // WS Bot 完全独立收发,不与 Agent 组成双模,失败时直接抛错而非 fallthrough
67
+ const resolvedAccount = resolveWecomAccount({ cfg, accountId });
68
+ const botAccount = resolvedAccount.bot;
69
+ if (botAccount?.connectionMode === 'websocket' && botAccount.configured) {
70
+ const wsClient = getWsClient(botAccount.accountId);
71
+ const wsTarget = resolveWecomTarget(to);
72
+ const chatid = wsTarget?.touser || wsTarget?.chatid;
73
+
74
+ // 如果目标是 Agent 会话(wecom-agent:),跳过 WS Bot,走 Agent outbound
75
+ const rawTo = typeof to === "string" ? to.trim().toLowerCase() : "";
76
+ if (!rawTo.startsWith("wecom-agent:")) {
77
+ if (!wsClient?.isConnected) {
78
+ console.log(`[wecom-outbound] Bot WS 未连接,等待重连... (accountId=${botAccount.accountId})`);
79
+ const reconnected = await waitForWsConnection(botAccount.accountId, 10_000);
80
+ if (!reconnected) {
81
+ throw new Error(`[wecom-outbound] Bot WS 等待重连超时,无法发送消息 (accountId=${botAccount.accountId})`);
82
+ }
83
+ }
84
+ if (!chatid) {
85
+ throw new Error(`[wecom-outbound] Bot WS 无法解析目标 chatid (to=${String(to)})`);
86
+ }
87
+ // 重连后重新获取 client(可能是新实例)
88
+ const activeClient = getWsClient(botAccount.accountId);
89
+ if (!activeClient?.isConnected) {
90
+ throw new Error(`[wecom-outbound] Bot WS 重连后仍不可用 (accountId=${botAccount.accountId})`);
91
+ }
92
+ await activeClient.sendMessage(chatid, {
93
+ msgtype: 'markdown',
94
+ markdown: { content: text },
95
+ });
96
+ console.log(`[wecom-outbound] Sent text via Bot WS to chatid=${chatid} (len=${text.length})`);
97
+ return { channel: "wecom", messageId: `ws-bot-${Date.now()}`, timestamp: Date.now() };
98
+ }
99
+ }
100
+
101
+ // ── Agent outbound(Webhook Bot 双模 / Agent 独立)──
102
+ const agent = resolveAgentConfigOrThrow({ cfg, accountId });
36
103
  const target = resolveWecomTarget(to);
37
104
  if (!target) {
38
105
  throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
@@ -98,10 +165,10 @@ export const wecomOutbound: ChannelOutboundAdapter = {
98
165
  timestamp: Date.now(),
99
166
  };
100
167
  },
101
- sendMedia: async ({ cfg, to, text, mediaUrl }: ChannelOutboundContext) => {
168
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }: ChannelOutboundContext) => {
102
169
  // signal removed - not supported in current SDK
103
170
 
104
- const agent = resolveAgentConfigOrThrow(cfg);
171
+ const agent = resolveAgentConfigOrThrow({ cfg, accountId });
105
172
  const target = resolveWecomTarget(to);
106
173
  if (!target) {
107
174
  throw new Error("WeCom outbound requires a target (userid, partyid, tagid or chatid).");
@@ -149,6 +216,12 @@ export const wecomOutbound: ChannelOutboundAdapter = {
149
216
  amr: "audio/amr", mp4: "video/mp4", pdf: "application/pdf", doc: "application/msword",
150
217
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
151
218
  xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
219
+ ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
220
+ txt: "text/plain", csv: "text/csv", tsv: "text/tab-separated-values", md: "text/markdown", json: "application/json",
221
+ xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
222
+ zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
223
+ tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip",
224
+ rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
152
225
  };
153
226
  contentType = mimeTypes[ext] || "application/octet-stream";
154
227
  console.log(`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`);
@@ -1,6 +1,8 @@
1
1
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
- import type { WecomAccountConfig } from "../types.js";
3
+ import type { WecomAgentConfig, WecomBotConfig } from "../types/index.js";
4
+
5
+ type WecomCommandAuthAccountConfig = Pick<WecomBotConfig, "dm"> | Pick<WecomAgentConfig, "dm">;
4
6
 
5
7
  function normalizeWecomAllowFromEntry(raw: string): string {
6
8
  return raw
@@ -22,7 +24,7 @@ function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolea
22
24
  export async function resolveWecomCommandAuthorization(params: {
23
25
  core: PluginRuntime;
24
26
  cfg: OpenClawConfig;
25
- accountConfig: WecomAccountConfig;
27
+ accountConfig: WecomCommandAuthAccountConfig;
26
28
  rawBody: string;
27
29
  senderUserId: string;
28
30
  }): Promise<{
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
 
3
- import { extractContent, extractMediaId, extractMsgId } from "./xml-parser.js";
3
+ import { extractContent, extractFromUser, extractMediaId, extractMsgId, parseXml } from "./xml-parser.js";
4
4
 
5
5
  describe("wecom xml-parser", () => {
6
6
  test("extractContent is robust to non-string Content", () => {
@@ -27,4 +27,24 @@ describe("wecom xml-parser", () => {
27
27
  const msg: any = { MsgId: 123456789 };
28
28
  expect(extractMsgId(msg)).toBe("123456789");
29
29
  });
30
+
31
+ test("parseXml preserves leading zero userid in FromUserName", () => {
32
+ const xml = `
33
+ <xml>
34
+ <FromUserName><![CDATA[0254571]]></FromUserName>
35
+ </xml>
36
+ `;
37
+ const msg = parseXml(xml);
38
+ expect(extractFromUser(msg)).toBe("0254571");
39
+ });
40
+
41
+ test("parseXml preserves 64-bit MsgId as string", () => {
42
+ const xml = `
43
+ <xml>
44
+ <MsgId>1234567890123456</MsgId>
45
+ </xml>
46
+ `;
47
+ const msg = parseXml(xml);
48
+ expect(extractMsgId(msg)).toBe("1234567890123456");
49
+ });
30
50
  });
@@ -10,6 +10,8 @@ const xmlParser = new XMLParser({
10
10
  ignoreAttributes: false,
11
11
  trimValues: true,
12
12
  processEntities: false,
13
+ parseTagValue: false,
14
+ parseAttributeValue: false,
13
15
  });
14
16
 
15
17
  /**
@@ -72,6 +74,22 @@ export function extractChatId(msg: WecomAgentInboundMessage): string | undefined
72
74
  return msg.ChatId ? String(msg.ChatId) : undefined;
73
75
  }
74
76
 
77
+ /**
78
+ * 从 XML 中提取 AgentID(兼容 AgentID/agentid 等大小写)
79
+ */
80
+ export function extractAgentId(msg: WecomAgentInboundMessage): string | number | undefined {
81
+ const raw =
82
+ (msg as any).AgentID ??
83
+ (msg as any).AgentId ??
84
+ (msg as any).agentid ??
85
+ (msg as any).agentId;
86
+ if (raw == null) return undefined;
87
+ if (typeof raw === "string") return raw.trim() || undefined;
88
+ if (typeof raw === "number") return raw;
89
+ const asString = String(raw).trim();
90
+ return asString || undefined;
91
+ }
92
+
75
93
  /**
76
94
  * 从 XML 中提取消息内容
77
95
  */
@@ -2,7 +2,13 @@
2
2
  * WeCom 账号类型定义
3
3
  */
4
4
 
5
- import type { WecomBotConfig, WecomAgentConfig, WecomDmConfig, WecomNetworkConfig } from "./config.js";
5
+ import type {
6
+ WecomBotConfig,
7
+ WecomAgentConfig,
8
+ WecomDmConfig,
9
+ WecomNetworkConfig,
10
+ WecomAccountConfig,
11
+ } from "./config.js";
6
12
 
7
13
  /**
8
14
  * 解析后的 Bot 账号
@@ -14,9 +20,9 @@ export type ResolvedBotAccount = {
14
20
  enabled: boolean;
15
21
  /** 是否配置完整 */
16
22
  configured: boolean;
17
- /** 回调 Token */
23
+ /** 回调 Token (webhook 模式) */
18
24
  token: string;
19
- /** 回调加密密钥 */
25
+ /** 回调加密密钥 (webhook 模式) */
20
26
  encodingAESKey: string;
21
27
  /** 接收者 ID */
22
28
  receiveId: string;
@@ -24,6 +30,15 @@ export type ResolvedBotAccount = {
24
30
  config: WecomBotConfig;
25
31
  /** 网络配置(来自 channels.wecom.network) */
26
32
  network?: WecomNetworkConfig;
33
+
34
+ // --- 长链接模式 (WebSocket) ---
35
+
36
+ /** 连接模式 */
37
+ connectionMode: 'webhook' | 'websocket';
38
+ /** 机器人 BotID(websocket 模式下有值) */
39
+ botId?: string;
40
+ /** 机器人 Secret(websocket 模式下有值) */
41
+ secret?: string;
27
42
  };
28
43
 
29
44
  /**
@@ -40,8 +55,8 @@ export type ResolvedAgentAccount = {
40
55
  corpId: string;
41
56
  /** 应用 Secret */
42
57
  corpSecret: string;
43
- /** 应用 ID (数字) */
44
- agentId: number;
58
+ /** 应用 ID (数字,可选) */
59
+ agentId?: number;
45
60
  /** 回调 Token */
46
61
  token: string;
47
62
  /** 回调加密密钥 */
@@ -52,23 +67,46 @@ export type ResolvedAgentAccount = {
52
67
  network?: WecomNetworkConfig;
53
68
  };
54
69
 
55
- /**
56
- * 已解析的模式状态
57
- */
58
- export type ResolvedMode = {
59
- /** Bot 模式是否已配置 */
60
- bot: boolean;
61
- /** Agent 模式是否已配置 */
62
- agent: boolean;
70
+ /** Matrix/Legacy 的统一账号解析结果 */
71
+ export type ResolvedWecomAccount = {
72
+ /** 账号 ID(用于 bindings.match.accountId) */
73
+ accountId: string;
74
+ /** 展示名称 */
75
+ name?: string;
76
+ /** 是否启用 */
77
+ enabled: boolean;
78
+ /** 是否具备至少一种可用能力(bot/agent) */
79
+ configured: boolean;
80
+ /** 原始账号配置(Matrix 条目或 Legacy 聚合) */
81
+ config: WecomAccountConfig;
82
+ /** Bot 能力 */
83
+ bot?: ResolvedBotAccount;
84
+ /** Agent 能力 */
85
+ agent?: ResolvedAgentAccount;
63
86
  };
64
87
 
88
+ /** 解析模式 */
89
+ export type ResolvedMode = "disabled" | "legacy" | "matrix";
90
+
65
91
  /**
66
- * 解析后的 WeCom 账号集合
92
+ * 已解析的模式状态
67
93
  */
68
94
  export type ResolvedWecomAccounts = {
69
- /** Bot 模式账号 */
95
+ /** 当前模式 */
96
+ mode: ResolvedMode;
97
+ /** 默认账号 ID */
98
+ defaultAccountId: string;
99
+ /** 账号集合(Legacy 下仅 default) */
100
+ accounts: Record<string, ResolvedWecomAccount>;
101
+ /**
102
+ * 向后兼容:默认账号的 bot(历史调用点仍可读取)。
103
+ * Matrix 下等价于 defaultAccountId 对应账号的 bot。
104
+ */
70
105
  bot?: ResolvedBotAccount;
71
- /** Agent 模式账号 */
106
+ /**
107
+ * 向后兼容:默认账号的 agent(历史调用点仍可读取)。
108
+ * Matrix 下等价于 defaultAccountId 对应账号的 agent。
109
+ */
72
110
  agent?: ResolvedAgentAccount;
73
111
  };
74
112
 
@@ -30,15 +30,33 @@ export type WecomNetworkConfig = {
30
30
  egressProxyUrl?: string;
31
31
  };
32
32
 
33
+ /** 路由行为配置 */
34
+ export type WecomRoutingConfig = {
35
+ /**
36
+ * 当路由未命中 bindings(matchedBy=default)时是否拒绝继续处理。
37
+ * - true: fail-closed(推荐于多账号)
38
+ * - false: 允许回退默认 agent(历史兼容)
39
+ */
40
+ failClosedOnDefaultRoute?: boolean;
41
+ };
42
+
33
43
  /**
34
44
  * Bot 模式配置 (智能体)
35
45
  * 用于接收 JSON 格式回调 + 流式回复
36
46
  */
37
47
  export type WecomBotConfig = {
38
- /** 回调 Token (企微后台生成) */
39
- token: string;
40
- /** 回调加密密钥 (企微后台生成) */
41
- encodingAESKey: string;
48
+ /** 智能机器人 ID(用于 Matrix 模式二次身份确认,webhook 模式) */
49
+ aibotid?: string;
50
+ /** 回调 Token (企微后台生成,webhook 模式必填) */
51
+ token?: string;
52
+ /** 回调加密密钥 (企微后台生成,webhook 模式必填) */
53
+ encodingAESKey?: string;
54
+ /**
55
+ * BotId 列表(可选,用于审计与告警)。
56
+ * - 回调路由优先由 URL + 签名决定;botIds 不参与强制拦截。
57
+ * - 当解密后的 aibotid 不在 botIds 中时,仅记录告警日志。
58
+ */
59
+ botIds?: string[];
42
60
  /** 接收者 ID (可选,用于解密校验) */
43
61
  receiveId?: string;
44
62
  /** 流式消息占位符 */
@@ -47,6 +65,15 @@ export type WecomBotConfig = {
47
65
  welcomeText?: string;
48
66
  /** DM 策略 */
49
67
  dm?: WecomDmConfig;
68
+
69
+ // --- 长链接模式 (WebSocket) ---
70
+
71
+ /** 连接模式:webhook(默认)或 websocket */
72
+ connectionMode?: 'webhook' | 'websocket';
73
+ /** 机器人 BotID(websocket 模式必填,企微后台获取) */
74
+ botId?: string;
75
+ /** 机器人 Secret(websocket 模式必填,企微后台获取) */
76
+ secret?: string;
50
77
  };
51
78
 
52
79
  /**
@@ -58,8 +85,8 @@ export type WecomAgentConfig = {
58
85
  corpId: string;
59
86
  /** 应用 Secret */
60
87
  corpSecret: string;
61
- /** 应用 ID */
62
- agentId: number | string;
88
+ /** 应用 ID(可选;不填时可接收回调,但主动发送需具备该字段) */
89
+ agentId?: number | string;
63
90
  /** 回调 Token (企微后台「设置API接收」) */
64
91
  token: string;
65
92
  /** 回调加密密钥 (企微后台「设置API接收」) */
@@ -93,10 +120,27 @@ export type WecomConfig = {
93
120
  bot?: WecomBotConfig;
94
121
  /** Agent 模式配置 (自建应用) */
95
122
  agent?: WecomAgentConfig;
123
+ /**
124
+ * 多账号配置(每个账号可包含 bot + agent,作为一组)。
125
+ * accountId 用于与 OpenClaw `bindings[].match.accountId` 对齐,从而把不同 WeCom 账号路由到不同 OpenClaw agent。
126
+ */
127
+ accounts?: Record<string, WecomAccountConfig>;
128
+ /** 默认账号(可选) */
129
+ defaultAccount?: string;
96
130
  /** 媒体处理配置 */
97
131
  media?: WecomMediaConfig;
98
132
  /** 网络配置 */
99
133
  network?: WecomNetworkConfig;
134
+ /** 路由配置 */
135
+ routing?: WecomRoutingConfig;
100
136
  /** 动态 Agent 配置 */
101
137
  dynamicAgents?: WecomDynamicAgentsConfig;
102
138
  };
139
+
140
+ /** Matrix 账号条目 */
141
+ export type WecomAccountConfig = {
142
+ enabled?: boolean;
143
+ name?: string;
144
+ bot?: WecomBotConfig;
145
+ agent?: WecomAgentConfig;
146
+ };
@@ -4,12 +4,16 @@
4
4
 
5
5
  /** 固定 Webhook 路径 */
6
6
  export const WEBHOOK_PATHS = {
7
- /** Bot 模式 (智能体) - 兼容原有路径 */
7
+ /** Bot 模式历史兼容路径(不再维护) */
8
8
  BOT: "/wecom",
9
- /** Bot 模式备用路径 */
9
+ /** Bot 模式历史备用兼容路径(不再维护) */
10
10
  BOT_ALT: "/wecom/bot",
11
- /** Agent 模式 (自建应用) */
11
+ /** Agent 模式历史兼容路径(不再维护) */
12
12
  AGENT: "/wecom/agent",
13
+ /** Bot 模式推荐路径前缀 */
14
+ BOT_PLUGIN: "/plugins/wecom/bot",
15
+ /** Agent 模式推荐路径前缀 */
16
+ AGENT_PLUGIN: "/plugins/wecom/agent",
13
17
  } as const;
14
18
 
15
19
  /** 企业微信 API 端点 */
@@ -7,9 +7,11 @@ export * from "./constants.js";
7
7
 
8
8
  // 配置类型
9
9
  export type {
10
+ WecomAccountConfig,
10
11
  WecomDmConfig,
11
12
  WecomMediaConfig,
12
13
  WecomNetworkConfig,
14
+ WecomRoutingConfig,
13
15
  WecomBotConfig,
14
16
  WecomAgentConfig,
15
17
  WecomConfig,
@@ -17,6 +19,7 @@ export type {
17
19
 
18
20
  // 账号类型
19
21
  export type {
22
+ ResolvedWecomAccount,
20
23
  ResolvedBotAccount,
21
24
  ResolvedAgentAccount,
22
25
  ResolvedMode,