@yanhaidao/wecom 2.2.7 → 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 (47) 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 +271 -87
  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/changelog/v2.2.28.md +70 -0
  11. package/compat-single-account.md +118 -0
  12. package/package.json +10 -2
  13. package/src/accounts.ts +17 -55
  14. package/src/agent/api-client.ts +8 -3
  15. package/src/agent/handler.event-filter.test.ts +50 -0
  16. package/src/agent/handler.ts +143 -141
  17. package/src/channel.config.test.ts +147 -0
  18. package/src/channel.lifecycle.test.ts +234 -0
  19. package/src/channel.ts +90 -140
  20. package/src/config/accounts.resolve.test.ts +38 -0
  21. package/src/config/accounts.ts +257 -22
  22. package/src/config/index.ts +6 -0
  23. package/src/config/routing.test.ts +88 -0
  24. package/src/config/routing.ts +26 -0
  25. package/src/config/schema.ts +35 -4
  26. package/src/config-schema.ts +5 -41
  27. package/src/dynamic-agent.account-scope.test.ts +17 -0
  28. package/src/dynamic-agent.ts +13 -13
  29. package/src/gateway-monitor.ts +200 -0
  30. package/src/monitor/state.queue.test.ts +1 -1
  31. package/src/monitor/state.ts +1 -1
  32. package/src/monitor/types.ts +1 -1
  33. package/src/monitor.active.test.ts +6 -3
  34. package/src/monitor.inbound-filter.test.ts +63 -0
  35. package/src/monitor.ts +464 -56
  36. package/src/monitor.webhook.test.ts +288 -3
  37. package/src/outbound.test.ts +130 -0
  38. package/src/outbound.ts +38 -9
  39. package/src/shared/command-auth.ts +4 -2
  40. package/src/shared/xml-parser.test.ts +21 -1
  41. package/src/shared/xml-parser.ts +18 -0
  42. package/src/types/account.ts +43 -14
  43. package/src/types/config.ts +37 -2
  44. package/src/types/index.ts +3 -0
  45. package/src/types.ts +29 -147
  46. package/GEMINI.md +0 -76
  47. package//345/212/250/346/200/201Agent/350/267/257/347/224/261.md +0 -360
@@ -0,0 +1,200 @@
1
+ import type {
2
+ ChannelGatewayContext,
3
+ OpenClawConfig,
4
+ PluginRuntime,
5
+ } from "openclaw/plugin-sdk";
6
+
7
+ import {
8
+ detectMode,
9
+ listWecomAccountIds,
10
+ resolveWecomAccount,
11
+ resolveWecomAccountConflict,
12
+ } from "./config/index.js";
13
+ import { registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
14
+ import type { ResolvedWecomAccount, WecomConfig } from "./types/index.js";
15
+
16
+ type AccountRouteRegistryItem = {
17
+ botPaths: string[];
18
+ agentPath?: string;
19
+ };
20
+
21
+ const accountRouteRegistry = new Map<string, AccountRouteRegistryItem>();
22
+
23
+ function logRegisteredRouteSummary(
24
+ ctx: ChannelGatewayContext<ResolvedWecomAccount>,
25
+ preferredOrder: string[],
26
+ ): void {
27
+ const seen = new Set<string>();
28
+ const orderedAccountIds = [
29
+ ...preferredOrder.filter((accountId) => accountRouteRegistry.has(accountId)),
30
+ ...Array.from(accountRouteRegistry.keys())
31
+ .filter((accountId) => !seen.has(accountId))
32
+ .sort((a, b) => a.localeCompare(b)),
33
+ ].filter((accountId) => {
34
+ if (seen.has(accountId)) return false;
35
+ seen.add(accountId);
36
+ return true;
37
+ });
38
+
39
+ const entries = orderedAccountIds
40
+ .map((accountId) => {
41
+ const routes = accountRouteRegistry.get(accountId);
42
+ if (!routes) return undefined;
43
+ const botText = routes.botPaths.length > 0 ? routes.botPaths.join(", ") : "未启用";
44
+ const agentText = routes.agentPath ?? "未启用";
45
+ return `accountId=${accountId}(Bot: ${botText};Agent: ${agentText})`;
46
+ })
47
+ .filter((entry): entry is string => Boolean(entry));
48
+ const summary = entries.length > 0 ? entries.join("; ") : "无";
49
+ ctx.log?.info(`[${ctx.account.accountId}] 已注册账号路由汇总:${summary}`);
50
+ }
51
+
52
+ function resolveExpectedRouteSummaryAccountIds(cfg: OpenClawConfig): string[] {
53
+ return listWecomAccountIds(cfg)
54
+ .filter((accountId) => {
55
+ const conflict = resolveWecomAccountConflict({ cfg, accountId });
56
+ if (conflict) return false;
57
+ const account = resolveWecomAccount({ cfg, accountId });
58
+ if (!account.enabled || !account.configured) return false;
59
+ return Boolean(account.bot?.configured || account.agent?.configured);
60
+ })
61
+ .sort((a, b) => a.localeCompare(b));
62
+ }
63
+
64
+ function waitForAbortSignal(abortSignal: AbortSignal): Promise<void> {
65
+ if (abortSignal.aborted) {
66
+ return Promise.resolve();
67
+ }
68
+ return new Promise<void>((resolve) => {
69
+ const onAbort = () => {
70
+ abortSignal.removeEventListener("abort", onAbort);
71
+ resolve();
72
+ };
73
+ abortSignal.addEventListener("abort", onAbort, { once: true });
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Keeps WeCom webhook targets registered for the account lifecycle.
79
+ * The promise only settles after gateway abort/reload signals shutdown.
80
+ */
81
+ export async function monitorWecomProvider(
82
+ ctx: ChannelGatewayContext<ResolvedWecomAccount>,
83
+ ): Promise<void> {
84
+ const account = ctx.account;
85
+ const cfg = ctx.cfg as OpenClawConfig;
86
+ const expectedRouteSummaryAccountIds = resolveExpectedRouteSummaryAccountIds(cfg);
87
+ const conflict = resolveWecomAccountConflict({
88
+ cfg,
89
+ accountId: account.accountId,
90
+ });
91
+ if (conflict) {
92
+ ctx.setStatus({
93
+ accountId: account.accountId,
94
+ running: false,
95
+ configured: false,
96
+ lastError: conflict.message,
97
+ });
98
+ throw new Error(conflict.message);
99
+ }
100
+ const mode = detectMode(cfg.channels?.wecom as WecomConfig | undefined);
101
+ const matrixMode = mode === "matrix";
102
+ const bot = account.bot;
103
+ const agent = account.agent;
104
+ const botConfigured = Boolean(bot?.configured);
105
+ const agentConfigured = Boolean(agent?.configured);
106
+
107
+ if (mode === "legacy" && (botConfigured || agentConfigured)) {
108
+ if (agentConfigured && !botConfigured) {
109
+ ctx.log?.warn(
110
+ `[${account.accountId}] 检测到仍在使用单 Agent 兼容模式。建议尽快升级为多账号模式:` +
111
+ `将 channels.wecom.agent 迁移到 channels.wecom.accounts.<accountId>.agent,` +
112
+ `并设置 channels.wecom.defaultAccount。`,
113
+ );
114
+ } else {
115
+ ctx.log?.warn(
116
+ `[${account.accountId}] 检测到仍在使用单账号兼容模式。建议尽快升级为多账号模式:` +
117
+ `将 channels.wecom.bot/agent 迁移到 channels.wecom.accounts.<accountId>.bot/agent,` +
118
+ `并设置 channels.wecom.defaultAccount。`,
119
+ );
120
+ }
121
+ }
122
+
123
+ if (!botConfigured && !agentConfigured) {
124
+ ctx.log?.warn(`[${account.accountId}] wecom not configured; channel is idle`);
125
+ ctx.setStatus({ accountId: account.accountId, running: false, configured: false });
126
+ await waitForAbortSignal(ctx.abortSignal);
127
+ return;
128
+ }
129
+
130
+ const unregisters: Array<() => void> = [];
131
+ const botPaths: string[] = [];
132
+ let agentPath: string | undefined;
133
+ try {
134
+ if (bot && botConfigured) {
135
+ const paths = matrixMode
136
+ ? [`/wecom/bot/${account.accountId}`]
137
+ : ["/wecom", "/wecom/bot"];
138
+ for (const path of paths) {
139
+ unregisters.push(
140
+ registerWecomWebhookTarget({
141
+ account: bot,
142
+ config: cfg,
143
+ runtime: ctx.runtime,
144
+ // The HTTP handler resolves the active PluginRuntime via getWecomRuntime().
145
+ // The stored target only needs to be decrypt/verify-capable.
146
+ core: {} as PluginRuntime,
147
+ path,
148
+ statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
149
+ }),
150
+ );
151
+ }
152
+ botPaths.push(...paths);
153
+ ctx.log?.info(`[${account.accountId}] wecom bot webhook registered at ${paths.join(", ")}`);
154
+ }
155
+
156
+ if (agent && agentConfigured) {
157
+ const path = matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent";
158
+ unregisters.push(
159
+ registerAgentWebhookTarget({
160
+ agent,
161
+ config: cfg,
162
+ runtime: ctx.runtime,
163
+ path,
164
+ }),
165
+ );
166
+ agentPath = path;
167
+ ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at ${path}`);
168
+ }
169
+
170
+ accountRouteRegistry.set(account.accountId, { botPaths, agentPath });
171
+ const shouldLogSummary =
172
+ expectedRouteSummaryAccountIds.length <= 1 ||
173
+ expectedRouteSummaryAccountIds.every((accountId) => accountRouteRegistry.has(accountId));
174
+ if (shouldLogSummary) {
175
+ logRegisteredRouteSummary(ctx, expectedRouteSummaryAccountIds);
176
+ }
177
+
178
+ ctx.setStatus({
179
+ accountId: account.accountId,
180
+ running: true,
181
+ configured: true,
182
+ webhookPath: botConfigured
183
+ ? (matrixMode ? `/wecom/bot/${account.accountId}` : "/wecom/bot")
184
+ : (matrixMode ? `/wecom/agent/${account.accountId}` : "/wecom/agent"),
185
+ lastStartAt: Date.now(),
186
+ });
187
+
188
+ await waitForAbortSignal(ctx.abortSignal);
189
+ } finally {
190
+ for (const unregister of unregisters) {
191
+ unregister();
192
+ }
193
+ accountRouteRegistry.delete(account.accountId);
194
+ ctx.setStatus({
195
+ accountId: account.accountId,
196
+ running: false,
197
+ lastStopAt: Date.now(),
198
+ });
199
+ }
200
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test, vi } from "vitest";
2
2
 
3
- import type { WecomInboundMessage } from "../types.js";
3
+ import type { WecomBotInboundMessage as WecomInboundMessage } from "../types/index.js";
4
4
  import type { WecomWebhookTarget } from "./types.js";
5
5
  import { StreamStore } from "./state.js";
6
6
 
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { StreamState, PendingInbound, ActiveReplyState, WecomWebhookTarget } from "./types.js";
3
- import type { WecomInboundMessage } from "../types.js";
3
+ import type { WecomBotInboundMessage as WecomInboundMessage } from "../types/index.js";
4
4
 
5
5
  // Constants
6
6
  export const LIMITS = {
@@ -1,7 +1,7 @@
1
1
 
2
2
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
3
3
  import type { ResolvedBotAccount } from "../types/index.js";
4
- import type { WecomInboundMessage } from "../types.js";
4
+ import type { WecomBotInboundMessage as WecomInboundMessage } from "../types/index.js";
5
5
 
6
6
  /**
7
7
  * **WecomRuntimeEnv (运行时环境)**
@@ -45,6 +45,7 @@ function createMockResponse(): ServerResponse {
45
45
 
46
46
  describe("Monitor Active Features", () => {
47
47
  let capturedDeliver: ((payload: { text: string }) => Promise<void>) | undefined;
48
+ let unregisterTarget: (() => void) | undefined;
48
49
  let mockCore: any;
49
50
  let msgSeq = 0;
50
51
  let senderUserId = "";
@@ -109,7 +110,7 @@ describe("Monitor Active Features", () => {
109
110
  return;
110
111
  }
111
112
  },
112
- routing: { resolveAgentRoute: () => ({ agentId: "1", sessionKey: "1", accountId: "1" }) },
113
+ routing: { resolveAgentRoute: () => ({ agentId: "1", sessionKey: "1", accountId: "default" }) },
113
114
  session: {
114
115
  resolveStorePath: () => "",
115
116
  readSessionUpdatedAt: () => 0,
@@ -121,8 +122,8 @@ describe("Monitor Active Features", () => {
121
122
 
122
123
  vi.spyOn(runtime, "getWecomRuntime").mockReturnValue(mockCore);
123
124
 
124
- registerWecomWebhookTarget({
125
- account: { accountId: "1", enabled: true, configured: true, token: "T", encodingAESKey: validKey, receiveId: "R", config: {} as any },
125
+ unregisterTarget = registerWecomWebhookTarget({
126
+ account: { accountId: "default", enabled: true, configured: true, token: "T", encodingAESKey: validKey, receiveId: "R", config: {} as any },
126
127
  config: {
127
128
  channels: {
128
129
  wecom: {
@@ -144,6 +145,8 @@ describe("Monitor Active Features", () => {
144
145
  });
145
146
 
146
147
  afterEach(() => {
148
+ unregisterTarget?.();
149
+ unregisterTarget = undefined;
147
150
  vi.useRealTimers();
148
151
  });
149
152
 
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { shouldProcessBotInboundMessage } from "./monitor.js";
4
+
5
+ describe("shouldProcessBotInboundMessage", () => {
6
+ it("skips payloads without sender id", () => {
7
+ const result = shouldProcessBotInboundMessage({
8
+ msgtype: "text",
9
+ from: {},
10
+ text: { content: "hello" },
11
+ });
12
+ expect(result.shouldProcess).toBe(false);
13
+ expect(result.reason).toBe("missing_sender");
14
+ });
15
+
16
+ it("skips system sender payloads", () => {
17
+ const result = shouldProcessBotInboundMessage({
18
+ msgtype: "text",
19
+ from: { userid: "sys" },
20
+ text: { content: "hello" },
21
+ });
22
+ expect(result.shouldProcess).toBe(false);
23
+ expect(result.reason).toBe("system_sender");
24
+ });
25
+
26
+ it("skips group payloads without chatid", () => {
27
+ const result = shouldProcessBotInboundMessage({
28
+ msgtype: "text",
29
+ chattype: "group",
30
+ from: { userid: "zhangsan" },
31
+ text: { content: "hello" },
32
+ });
33
+ expect(result.shouldProcess).toBe(false);
34
+ expect(result.reason).toBe("missing_chatid");
35
+ });
36
+
37
+ it("accepts normal direct-user messages", () => {
38
+ const result = shouldProcessBotInboundMessage({
39
+ msgtype: "text",
40
+ chattype: "single",
41
+ from: { userid: "zhangsan" },
42
+ text: { content: "hello" },
43
+ });
44
+ expect(result.shouldProcess).toBe(true);
45
+ expect(result.reason).toBe("user_message");
46
+ expect(result.senderUserId).toBe("zhangsan");
47
+ expect(result.chatId).toBe("zhangsan");
48
+ });
49
+
50
+ it("accepts normal group messages with chatid", () => {
51
+ const result = shouldProcessBotInboundMessage({
52
+ msgtype: "text",
53
+ chattype: "group",
54
+ chatid: "wr123",
55
+ from: { userid: "zhangsan" },
56
+ text: { content: "hello" },
57
+ });
58
+ expect(result.shouldProcess).toBe(true);
59
+ expect(result.reason).toBe("user_message");
60
+ expect(result.senderUserId).toBe("zhangsan");
61
+ expect(result.chatId).toBe("wr123");
62
+ });
63
+ });