@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.
- package/.github/workflows/release.yml +56 -0
- package/CLAUDE.md +1 -1
- package/GOVERNANCE.md +26 -0
- package/LICENSE +7 -0
- package/README.md +271 -87
- package/assets/01.bot-add.png +0 -0
- package/assets/01.bot-setp2.png +0 -0
- package/assets/02.agent.add.png +0 -0
- package/assets/02.agent.api-set.png +0 -0
- package/changelog/v2.2.28.md +70 -0
- package/compat-single-account.md +118 -0
- package/package.json +10 -2
- package/src/accounts.ts +17 -55
- package/src/agent/api-client.ts +8 -3
- package/src/agent/handler.event-filter.test.ts +50 -0
- package/src/agent/handler.ts +143 -141
- package/src/channel.config.test.ts +147 -0
- package/src/channel.lifecycle.test.ts +234 -0
- package/src/channel.ts +90 -140
- package/src/config/accounts.resolve.test.ts +38 -0
- package/src/config/accounts.ts +257 -22
- package/src/config/index.ts +6 -0
- package/src/config/routing.test.ts +88 -0
- package/src/config/routing.ts +26 -0
- package/src/config/schema.ts +35 -4
- package/src/config-schema.ts +5 -41
- package/src/dynamic-agent.account-scope.test.ts +17 -0
- package/src/dynamic-agent.ts +13 -13
- package/src/gateway-monitor.ts +200 -0
- package/src/monitor/state.queue.test.ts +1 -1
- package/src/monitor/state.ts +1 -1
- package/src/monitor/types.ts +1 -1
- package/src/monitor.active.test.ts +6 -3
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.ts +464 -56
- package/src/monitor.webhook.test.ts +288 -3
- package/src/outbound.test.ts +130 -0
- package/src/outbound.ts +38 -9
- package/src/shared/command-auth.ts +4 -2
- package/src/shared/xml-parser.test.ts +21 -1
- package/src/shared/xml-parser.ts +18 -0
- package/src/types/account.ts +43 -14
- package/src/types/config.ts +37 -2
- package/src/types/index.ts +3 -0
- package/src/types.ts +29 -147
- package/GEMINI.md +0 -76
- 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
|
|
package/src/monitor/state.ts
CHANGED
|
@@ -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 = {
|
package/src/monitor/types.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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
|
+
});
|