@yanhaidao/wecom 2.2.5 → 2.2.28

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 (46) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.github/workflows/release.yml +56 -0
  3. package/CLAUDE.md +238 -0
  4. package/GOVERNANCE.md +26 -0
  5. package/LICENSE +7 -0
  6. package/README.md +279 -87
  7. package/assets/01.bot-add.png +0 -0
  8. package/assets/01.bot-setp2.png +0 -0
  9. package/assets/02.agent.add.png +0 -0
  10. package/assets/02.agent.api-set.png +0 -0
  11. package/changelog/v2.2.28.md +70 -0
  12. package/compat-single-account.md +118 -0
  13. package/package.json +11 -3
  14. package/src/accounts.ts +17 -55
  15. package/src/agent/api-client.ts +8 -3
  16. package/src/agent/handler.event-filter.test.ts +50 -0
  17. package/src/agent/handler.ts +162 -139
  18. package/src/channel.config.test.ts +147 -0
  19. package/src/channel.lifecycle.test.ts +234 -0
  20. package/src/channel.ts +90 -140
  21. package/src/config/accounts.resolve.test.ts +38 -0
  22. package/src/config/accounts.ts +257 -22
  23. package/src/config/index.ts +6 -0
  24. package/src/config/routing.test.ts +88 -0
  25. package/src/config/routing.ts +26 -0
  26. package/src/config/schema.ts +52 -4
  27. package/src/config-schema.ts +5 -41
  28. package/src/dynamic-agent.account-scope.test.ts +17 -0
  29. package/src/dynamic-agent.ts +178 -0
  30. package/src/gateway-monitor.ts +200 -0
  31. package/src/monitor/state.queue.test.ts +1 -1
  32. package/src/monitor/state.ts +1 -1
  33. package/src/monitor/types.ts +1 -1
  34. package/src/monitor.active.test.ts +6 -3
  35. package/src/monitor.inbound-filter.test.ts +63 -0
  36. package/src/monitor.ts +482 -53
  37. package/src/monitor.webhook.test.ts +288 -3
  38. package/src/outbound.test.ts +130 -0
  39. package/src/outbound.ts +38 -9
  40. package/src/shared/command-auth.ts +4 -2
  41. package/src/shared/xml-parser.test.ts +21 -1
  42. package/src/shared/xml-parser.ts +18 -0
  43. package/src/types/account.ts +43 -14
  44. package/src/types/config.ts +51 -2
  45. package/src/types/index.ts +3 -0
  46. package/src/types.ts +29 -147
@@ -0,0 +1,118 @@
1
+ # 附录 A:WeCom 单账号兼容模式配置指南
2
+
3
+ > 本文档用于历史部署或小规模场景。
4
+ > 新项目默认推荐多账号矩阵配置(`accounts + bindings.match.accountId`)。
5
+
6
+ ## A.1 适用场景
7
+
8
+ - 已在线运行单账号配置,短期内不希望迁移。
9
+ - 只有一个 Bot / Agent,不需要账号隔离。
10
+ - 本地 PoC 或临时验证链路。
11
+
12
+ ## A.2 快速配置
13
+
14
+ ### A.2.1 仅 Bot(单智能体)
15
+
16
+ ```bash
17
+ openclaw config set channels.wecom.enabled true
18
+ openclaw config set channels.wecom.bot.token "YOUR_BOT_TOKEN"
19
+ openclaw config set channels.wecom.bot.encodingAESKey "YOUR_BOT_AES_KEY"
20
+ openclaw config set channels.wecom.bot.receiveId ""
21
+ openclaw config set channels.wecom.bot.streamPlaceholderContent "正在思考..."
22
+ openclaw config set channels.wecom.bot.welcomeText "你好!我是 AI 助手"
23
+
24
+ # DM 门禁(推荐显式设置 policy)
25
+ openclaw config set channels.wecom.bot.dm.policy "open"
26
+ openclaw config set channels.wecom.bot.dm.allowFrom '["*"]'
27
+ ```
28
+
29
+ ### A.2.2 增加 Agent 兜底(可选)
30
+
31
+ ```bash
32
+ openclaw config set channels.wecom.agent.corpId "YOUR_CORP_ID"
33
+ openclaw config set channels.wecom.agent.corpSecret "YOUR_CORP_SECRET"
34
+ openclaw config set channels.wecom.agent.agentId 1000001
35
+ openclaw config set channels.wecom.agent.token "YOUR_CALLBACK_TOKEN"
36
+ openclaw config set channels.wecom.agent.encodingAESKey "YOUR_CALLBACK_AES_KEY"
37
+ openclaw config set channels.wecom.agent.welcomeText "欢迎使用智能助手"
38
+ openclaw config set channels.wecom.agent.dm.policy "open"
39
+ openclaw config set channels.wecom.agent.dm.allowFrom '["*"]'
40
+ ```
41
+
42
+ ### A.2.3 验证
43
+
44
+ ```bash
45
+ openclaw gateway restart
46
+ openclaw channels status
47
+ ```
48
+
49
+ ## A.3 完整单账号配置结构
50
+
51
+ ```jsonc
52
+ {
53
+ "channels": {
54
+ "wecom": {
55
+ "enabled": true,
56
+
57
+ "bot": {
58
+ "aibotid": "BOT_ID_OPTIONAL",
59
+ "token": "YOUR_BOT_TOKEN",
60
+ "encodingAESKey": "YOUR_BOT_AES_KEY",
61
+ "botIds": ["BOT_ID_OPTIONAL"],
62
+ "receiveId": "",
63
+ "streamPlaceholderContent": "正在思考...",
64
+ "welcomeText": "你好!我是 AI 助手",
65
+ "dm": {
66
+ "policy": "open",
67
+ "allowFrom": ["*"]
68
+ }
69
+ },
70
+
71
+ "agent": {
72
+ "corpId": "YOUR_CORP_ID",
73
+ "corpSecret": "YOUR_CORP_SECRET",
74
+ "agentId": 1000001,
75
+ "token": "YOUR_CALLBACK_TOKEN",
76
+ "encodingAESKey": "YOUR_CALLBACK_AES_KEY",
77
+ "welcomeText": "欢迎使用智能助手",
78
+ "dm": {
79
+ "policy": "open",
80
+ "allowFrom": ["*"]
81
+ }
82
+ },
83
+
84
+ "media": {
85
+ "tempDir": "/tmp/openclaw-wecom-media",
86
+ "retentionHours": 24,
87
+ "cleanupOnStart": true,
88
+ "maxBytes": 26214400
89
+ },
90
+
91
+ "network": {
92
+ "timeoutMs": 15000,
93
+ "retries": 2,
94
+ "retryDelayMs": 500,
95
+ "egressProxyUrl": "http://proxy.company.local:3128"
96
+ },
97
+
98
+ "dynamicAgents": {
99
+ "enabled": false,
100
+ "dmCreateAgent": false,
101
+ "groupEnabled": false,
102
+ "adminUsers": []
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## A.4 Webhook 路径
110
+
111
+ - Bot: `/wecom/bot`
112
+ - Agent: `/wecom/agent`
113
+
114
+ ## A.5 迁移建议
115
+
116
+ 如果后续需要多 Bot / 多 Agent 隔离,建议迁移到多账号矩阵模式:
117
+ - 在 `channels.wecom.accounts.<accountId>` 下拆分配置
118
+ - 通过 `bindings[].match.accountId` 映射到对应 OpenClaw agent
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@yanhaidao/wecom",
3
- "version": "2.2.5",
3
+ "version": "2.2.28",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom (WeChat Work) intelligent bot channel plugin",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/YanHaidao/wecom.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "license": "ISC",
6
14
  "author": "YanHaidao (VX: YanHaidao)",
7
15
  "openclaw": {
8
16
  "extensions": [
@@ -37,10 +45,10 @@
37
45
  "zod": "^4.3.6"
38
46
  },
39
47
  "peerDependencies": {
40
- "openclaw": ">=2026.1.26"
48
+ "openclaw": ">=2026.2.24"
41
49
  },
42
50
  "devDependencies": {
43
51
  "@types/node": "^25.2.0",
44
52
  "typescript": "^5.9.3"
45
53
  }
46
- }
54
+ }
package/src/accounts.ts CHANGED
@@ -1,72 +1,34 @@
1
1
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
3
-
4
- import type { ResolvedWecomAccount, WecomAccountConfig, WecomConfig } from "./types.js";
5
-
6
- function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
7
- const accounts = (cfg.channels?.wecom as WecomConfig | undefined)?.accounts;
8
- if (!accounts || typeof accounts !== "object") return [];
9
- return Object.keys(accounts).filter(Boolean);
10
- }
11
2
 
3
+ import type { ResolvedWecomAccount } from "./types/index.js";
4
+ import {
5
+ listWecomAccountIds as listWecomAccountIdsFromConfig,
6
+ resolveDefaultWecomAccountId as resolveDefaultWecomAccountIdFromConfig,
7
+ resolveWecomAccount as resolveWecomAccountFromConfig,
8
+ } from "./config/accounts.js";
9
+
10
+ /**
11
+ * Backward-compatible re-export layer.
12
+ * Keep this file as a thin wrapper so older imports continue to work,
13
+ * while all account logic stays single-sourced in `src/config/accounts.ts`.
14
+ */
12
15
  export function listWecomAccountIds(cfg: OpenClawConfig): string[] {
13
- const ids = listConfiguredAccountIds(cfg);
14
- if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
15
- return ids.sort((a, b) => a.localeCompare(b));
16
+ return listWecomAccountIdsFromConfig(cfg);
16
17
  }
17
18
 
18
19
  export function resolveDefaultWecomAccountId(cfg: OpenClawConfig): string {
19
- const wecomConfig = cfg.channels?.wecom as WecomConfig | undefined;
20
- if (wecomConfig?.defaultAccount?.trim()) return wecomConfig.defaultAccount.trim();
21
- const ids = listWecomAccountIds(cfg);
22
- if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
23
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
24
- }
25
-
26
- function resolveAccountConfig(
27
- cfg: OpenClawConfig,
28
- accountId: string,
29
- ): WecomAccountConfig | undefined {
30
- const accounts = (cfg.channels?.wecom as WecomConfig | undefined)?.accounts;
31
- if (!accounts || typeof accounts !== "object") return undefined;
32
- return accounts[accountId] as WecomAccountConfig | undefined;
33
- }
34
-
35
- function mergeWecomAccountConfig(cfg: OpenClawConfig, accountId: string): WecomAccountConfig {
36
- const raw = (cfg.channels?.wecom ?? {}) as WecomConfig;
37
- const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
38
- const account = resolveAccountConfig(cfg, accountId) ?? {};
39
- return { ...base, ...account };
20
+ return resolveDefaultWecomAccountIdFromConfig(cfg);
40
21
  }
41
22
 
42
23
  export function resolveWecomAccount(params: {
43
24
  cfg: OpenClawConfig;
44
25
  accountId?: string | null;
45
26
  }): ResolvedWecomAccount {
46
- const accountId = normalizeAccountId(params.accountId);
47
- const baseEnabled = (params.cfg.channels?.wecom as WecomConfig | undefined)?.enabled !== false;
48
- const merged = mergeWecomAccountConfig(params.cfg, accountId);
49
- const enabled = baseEnabled && merged.enabled !== false;
50
-
51
- const token = merged.token?.trim() || undefined;
52
- const encodingAESKey = merged.encodingAESKey?.trim() || undefined;
53
- const receiveId = merged.receiveId?.trim() ?? "";
54
- const configured = Boolean(token && encodingAESKey);
55
-
56
- return {
57
- accountId,
58
- name: merged.name?.trim() || undefined,
59
- enabled,
60
- configured,
61
- token,
62
- encodingAESKey,
63
- receiveId,
64
- config: merged,
65
- };
27
+ return resolveWecomAccountFromConfig(params);
66
28
  }
67
29
 
68
30
  export function listEnabledWecomAccounts(cfg: OpenClawConfig): ResolvedWecomAccount[] {
69
- return listWecomAccountIds(cfg)
70
- .map((accountId) => resolveWecomAccount({ cfg, accountId }))
31
+ return listWecomAccountIdsFromConfig(cfg)
32
+ .map((accountId) => resolveWecomAccountFromConfig({ cfg, accountId }))
71
33
  .filter((account) => account.enabled);
72
34
  }
@@ -25,6 +25,11 @@ type TokenCache = {
25
25
 
26
26
  const tokenCaches = new Map<string, TokenCache>();
27
27
 
28
+ function requireAgentId(agent: ResolvedAgentAccount): number {
29
+ if (typeof agent.agentId === "number" && Number.isFinite(agent.agentId)) return agent.agentId;
30
+ throw new Error(`wecom agent account=${agent.accountId} missing agentId; sending via cgi-bin/message/send requires agentId`);
31
+ }
32
+
28
33
  /**
29
34
  * **getAccessToken (获取 AccessToken)**
30
35
  *
@@ -35,7 +40,7 @@ const tokenCaches = new Map<string, TokenCache>();
35
40
  * @returns 有效的 AccessToken
36
41
  */
37
42
  export async function getAccessToken(agent: ResolvedAgentAccount): Promise<string> {
38
- const cacheKey = `${agent.corpId}:${agent.agentId}`;
43
+ const cacheKey = `${agent.corpId}:${String(agent.agentId ?? "na")}`;
39
44
  let cache = tokenCaches.get(cacheKey);
40
45
 
41
46
  if (!cache) {
@@ -109,7 +114,7 @@ export async function sendText(params: {
109
114
  toparty: toParty,
110
115
  totag: toTag,
111
116
  msgtype: "text",
112
- agentid: agent.agentId,
117
+ agentid: requireAgentId(agent),
113
118
  text: { content: text }
114
119
  };
115
120
 
@@ -252,7 +257,7 @@ export async function sendMedia(params: {
252
257
  toparty: toParty,
253
258
  totag: toTag,
254
259
  msgtype: mediaType,
255
- agentid: agent.agentId,
260
+ agentid: requireAgentId(agent),
256
261
  [mediaType]: mediaPayload
257
262
  };
258
263
 
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { shouldProcessAgentInboundMessage } from "./handler.js";
4
+
5
+ describe("shouldProcessAgentInboundMessage", () => {
6
+ it("skips event callbacks so they do not create sessions", () => {
7
+ const enterAgent = shouldProcessAgentInboundMessage({
8
+ msgType: "event",
9
+ eventType: "enter_agent",
10
+ fromUser: "zhangsan",
11
+ });
12
+ expect(enterAgent.shouldProcess).toBe(false);
13
+ expect(enterAgent.reason).toBe("event:enter_agent");
14
+
15
+ const subscribe = shouldProcessAgentInboundMessage({
16
+ msgType: "event",
17
+ eventType: "subscribe",
18
+ fromUser: "lisi",
19
+ });
20
+ expect(subscribe.shouldProcess).toBe(false);
21
+ expect(subscribe.reason).toBe("event:subscribe");
22
+ });
23
+
24
+ it("skips system sender callbacks", () => {
25
+ const systemSender = shouldProcessAgentInboundMessage({
26
+ msgType: "text",
27
+ fromUser: "sys",
28
+ });
29
+ expect(systemSender.shouldProcess).toBe(false);
30
+ expect(systemSender.reason).toBe("system_sender");
31
+ });
32
+
33
+ it("skips messages with missing sender id", () => {
34
+ const missingSender = shouldProcessAgentInboundMessage({
35
+ msgType: "text",
36
+ fromUser: " ",
37
+ });
38
+ expect(missingSender.shouldProcess).toBe(false);
39
+ expect(missingSender.reason).toBe("missing_sender");
40
+ });
41
+
42
+ it("allows normal user text message processing", () => {
43
+ const normalMessage = shouldProcessAgentInboundMessage({
44
+ msgType: "text",
45
+ fromUser: "wangwu",
46
+ });
47
+ expect(normalMessage.shouldProcess).toBe(true);
48
+ expect(normalMessage.reason).toBe("user_message");
49
+ });
50
+ });