@mocrane/wecom 2026.2.5 → 2026.3.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.
- package/LICENSE +4 -18
- package/README.md +572 -0
- package/assets/01.bot-add.png +0 -0
- package/assets/01.bot-setp2.png +0 -0
- package/assets/01.image.jpg +0 -0
- package/assets/02.agent.add.png +0 -0
- package/assets/02.agent.api-set.png +0 -0
- package/assets/02.image.jpg +0 -0
- package/assets/03.agent.page.png +0 -0
- package/assets/03.bot.page.png +0 -0
- package/assets/link-me.jpg +0 -0
- package/assets/register.png +0 -0
- package/changelog/v2.2.28.md +70 -0
- package/changelog/v2.3.2.md +28 -0
- package/changelog/v2.3.4.md +20 -0
- package/index.ts +11 -3
- package/package.json +4 -2
- package/src/accounts.ts +17 -55
- package/src/agent/api-client.ts +84 -37
- package/src/agent/api-client.upload.test.ts +110 -0
- package/src/agent/handler.event-filter.test.ts +50 -0
- package/src/agent/handler.ts +166 -143
- package/src/channel.config.test.ts +147 -0
- package/src/channel.lifecycle.test.ts +252 -0
- package/src/channel.ts +95 -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/network.ts +9 -5
- package/src/config/routing.test.ts +88 -0
- package/src/config/routing.ts +26 -0
- package/src/config/schema.ts +52 -4
- package/src/config-schema.ts +5 -41
- package/src/dynamic-agent.account-scope.test.ts +17 -0
- package/src/dynamic-agent.ts +178 -0
- package/src/gateway-monitor.ts +238 -0
- package/src/http.ts +16 -2
- package/src/media.test.ts +28 -1
- package/src/media.ts +59 -1
- 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 +15 -9
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.integration.test.ts +4 -2
- package/src/monitor.ts +988 -125
- package/src/monitor.webhook.test.ts +381 -3
- package/src/onboarding.ts +229 -53
- package/src/outbound.test.ts +130 -0
- package/src/outbound.ts +44 -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 +51 -2
- package/src/types/constants.ts +7 -3
- package/src/types/index.ts +3 -0
- package/src/types.ts +29 -147
package/src/agent/handler.ts
CHANGED
|
@@ -8,15 +8,22 @@ import path from "node:path";
|
|
|
8
8
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
9
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
10
10
|
import type { ResolvedAgentAccount } from "../types/index.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
extractMsgType,
|
|
13
|
+
extractFromUser,
|
|
14
|
+
extractContent,
|
|
15
|
+
extractChatId,
|
|
16
|
+
extractMediaId,
|
|
17
|
+
extractMsgId,
|
|
18
|
+
extractFileName,
|
|
19
|
+
extractAgentId,
|
|
20
|
+
} from "../shared/xml-parser.js";
|
|
15
21
|
import { sendText, downloadMedia } from "./api-client.js";
|
|
16
22
|
import { getWecomRuntime } from "../runtime.js";
|
|
17
23
|
import type { WecomAgentInboundMessage } from "../types/index.js";
|
|
18
24
|
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
|
|
19
|
-
import { resolveWecomMediaMaxBytes } from "../config/index.js";
|
|
25
|
+
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
|
|
26
|
+
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
20
27
|
|
|
21
28
|
/** 错误提示信息 */
|
|
22
29
|
const ERROR_HELP = "";
|
|
@@ -98,6 +105,18 @@ function buildTextFilePreview(buffer: Buffer, maxChars: number): string | undefi
|
|
|
98
105
|
export type AgentWebhookParams = {
|
|
99
106
|
req: IncomingMessage;
|
|
100
107
|
res: ServerResponse;
|
|
108
|
+
/**
|
|
109
|
+
* 上游已完成验签/解密时传入,避免重复协议处理。
|
|
110
|
+
* 仅用于 POST 消息回调流程。
|
|
111
|
+
*/
|
|
112
|
+
verifiedPost?: {
|
|
113
|
+
timestamp: string;
|
|
114
|
+
nonce: string;
|
|
115
|
+
signature: string;
|
|
116
|
+
encrypted: string;
|
|
117
|
+
decrypted: string;
|
|
118
|
+
parsed: WecomAgentInboundMessage;
|
|
119
|
+
};
|
|
101
120
|
agent: ResolvedAgentAccount;
|
|
102
121
|
config: OpenClawConfig;
|
|
103
122
|
core: PluginRuntime;
|
|
@@ -105,157 +124,119 @@ export type AgentWebhookParams = {
|
|
|
105
124
|
error?: (msg: string) => void;
|
|
106
125
|
};
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*/
|
|
113
|
-
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
114
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
115
|
-
return url.searchParams;
|
|
116
|
-
}
|
|
127
|
+
export type AgentInboundProcessDecision = {
|
|
128
|
+
shouldProcess: boolean;
|
|
129
|
+
reason: string;
|
|
130
|
+
};
|
|
117
131
|
|
|
118
132
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
133
|
+
* 仅允许“用户意图消息”进入 AI 会话。
|
|
134
|
+
* - event 回调(如 enter_agent/subscribe)不应触发会话与自动回复
|
|
135
|
+
* - 系统发送者(sys)不应触发会话与自动回复
|
|
136
|
+
* - 缺失发送者时默认丢弃,避免写入异常会话
|
|
123
137
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
export function shouldProcessAgentInboundMessage(params: {
|
|
139
|
+
msgType: string;
|
|
140
|
+
fromUser: string;
|
|
141
|
+
eventType?: string;
|
|
142
|
+
}): AgentInboundProcessDecision {
|
|
143
|
+
const msgType = String(params.msgType ?? "").trim().toLowerCase();
|
|
144
|
+
const fromUser = String(params.fromUser ?? "").trim();
|
|
145
|
+
const normalizedFromUser = fromUser.toLowerCase();
|
|
146
|
+
const eventType = String(params.eventType ?? "").trim().toLowerCase();
|
|
147
|
+
|
|
148
|
+
if (msgType === "event") {
|
|
149
|
+
return {
|
|
150
|
+
shouldProcess: false,
|
|
151
|
+
reason: `event:${eventType || "unknown"}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
if (!fromUser) {
|
|
156
|
+
return {
|
|
157
|
+
shouldProcess: false,
|
|
158
|
+
reason: "missing_sender",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
if (normalizedFromUser === "sys") {
|
|
163
|
+
return {
|
|
164
|
+
shouldProcess: false,
|
|
165
|
+
reason: "system_sender",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
shouldProcess: true,
|
|
171
|
+
reason: "user_message",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeAgentId(value: unknown): number | undefined {
|
|
176
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
177
|
+
const raw = String(value ?? "").trim();
|
|
178
|
+
if (!raw) return undefined;
|
|
179
|
+
const parsed = Number(raw);
|
|
180
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
/**
|
|
148
|
-
* **
|
|
184
|
+
* **resolveQueryParams (解析查询参数)**
|
|
149
185
|
*
|
|
150
|
-
*
|
|
151
|
-
* 流程:
|
|
152
|
-
* 1. 验证 msg_signature 签名。
|
|
153
|
-
* 2. 解密 echostr 参数。
|
|
154
|
-
* 3. 返回解密后的明文 echostr。
|
|
186
|
+
* 辅助函数:从 IncomingMessage 中解析 URL 查询字符串,用于获取签名、时间戳等参数。
|
|
155
187
|
*/
|
|
156
|
-
|
|
157
|
-
req
|
|
158
|
-
|
|
159
|
-
agent: ResolvedAgentAccount,
|
|
160
|
-
): Promise<boolean> {
|
|
161
|
-
const query = resolveQueryParams(req);
|
|
162
|
-
const timestamp = query.get("timestamp") ?? "";
|
|
163
|
-
const nonce = query.get("nonce") ?? "";
|
|
164
|
-
const echostr = query.get("echostr") ?? "";
|
|
165
|
-
const signature = query.get("msg_signature") ?? "";
|
|
166
|
-
const remote = req.socket?.remoteAddress ?? "unknown";
|
|
167
|
-
|
|
168
|
-
// 不输出敏感参数内容,仅输出存在性
|
|
169
|
-
// 用于排查:是否有请求打到 /wecom/agent
|
|
170
|
-
// 以及是否带齐 timestamp/nonce/msg_signature/echostr
|
|
171
|
-
// eslint-disable-next-line no-unused-vars
|
|
172
|
-
const _debug = { remote, hasTimestamp: Boolean(timestamp), hasNonce: Boolean(nonce), hasSig: Boolean(signature), hasEchostr: Boolean(echostr) };
|
|
173
|
-
|
|
174
|
-
const valid = verifyWecomSignature({
|
|
175
|
-
token: agent.token,
|
|
176
|
-
timestamp,
|
|
177
|
-
nonce,
|
|
178
|
-
encrypt: echostr,
|
|
179
|
-
signature,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (!valid) {
|
|
183
|
-
res.statusCode = 401;
|
|
184
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
185
|
-
res.end(`unauthorized - 签名验证失败,请检查 Token 配置${ERROR_HELP}`);
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const plain = decryptWecomEncrypted({
|
|
191
|
-
encodingAESKey: agent.encodingAESKey,
|
|
192
|
-
receiveId: agent.corpId,
|
|
193
|
-
encrypt: echostr,
|
|
194
|
-
});
|
|
195
|
-
res.statusCode = 200;
|
|
196
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
197
|
-
res.end(plain);
|
|
198
|
-
return true;
|
|
199
|
-
} catch {
|
|
200
|
-
res.statusCode = 400;
|
|
201
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
202
|
-
res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey 配置${ERROR_HELP}`);
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
188
|
+
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
189
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
190
|
+
return url.searchParams;
|
|
205
191
|
}
|
|
206
192
|
|
|
207
193
|
/**
|
|
208
194
|
* 处理消息回调 (POST)
|
|
209
195
|
*/
|
|
210
196
|
async function handleMessageCallback(params: AgentWebhookParams): Promise<boolean> {
|
|
211
|
-
const { req, res, agent, config, core, log, error } = params;
|
|
197
|
+
const { req, res, verifiedPost, agent, config, core, log, error } = params;
|
|
212
198
|
|
|
213
199
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
200
|
+
if (!verifiedPost) {
|
|
201
|
+
error?.("[wecom-agent] inbound: missing preverified envelope");
|
|
202
|
+
res.statusCode = 400;
|
|
203
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
204
|
+
res.end(`invalid request - 缺少上游验签结果${ERROR_HELP}`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
219
207
|
|
|
208
|
+
log?.(`[wecom-agent] inbound: method=${req.method ?? "UNKNOWN"} remote=${req.socket?.remoteAddress ?? "unknown"}`);
|
|
220
209
|
const query = resolveQueryParams(req);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
const
|
|
210
|
+
const querySignature = query.get("msg_signature") ?? "";
|
|
211
|
+
|
|
212
|
+
const encrypted = verifiedPost.encrypted;
|
|
213
|
+
const decrypted = verifiedPost.decrypted;
|
|
214
|
+
const msg = verifiedPost.parsed;
|
|
215
|
+
const timestamp = verifiedPost.timestamp;
|
|
216
|
+
const nonce = verifiedPost.nonce;
|
|
217
|
+
const signature = verifiedPost.signature || querySignature;
|
|
224
218
|
log?.(
|
|
225
|
-
`[wecom-agent] inbound:
|
|
219
|
+
`[wecom-agent] inbound: using preverified envelope timestamp=${timestamp ? "yes" : "no"} nonce=${nonce ? "yes" : "no"} msg_signature=${signature ? "yes" : "no"} encryptLen=${encrypted.length}`,
|
|
226
220
|
);
|
|
227
221
|
|
|
228
|
-
// 验证签名
|
|
229
|
-
const valid = verifyWecomSignature({
|
|
230
|
-
token: agent.token,
|
|
231
|
-
timestamp,
|
|
232
|
-
nonce,
|
|
233
|
-
encrypt: encrypted,
|
|
234
|
-
signature,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
if (!valid) {
|
|
238
|
-
error?.(`[wecom-agent] inbound: signature invalid`);
|
|
239
|
-
res.statusCode = 401;
|
|
240
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
241
|
-
res.end(`unauthorized - 签名验证失败${ERROR_HELP}`);
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 解密
|
|
246
|
-
const decrypted = decryptWecomEncrypted({
|
|
247
|
-
encodingAESKey: agent.encodingAESKey,
|
|
248
|
-
receiveId: agent.corpId,
|
|
249
|
-
encrypt: encrypted,
|
|
250
|
-
});
|
|
251
222
|
log?.(`[wecom-agent] inbound: decryptedBytes=${Buffer.byteLength(decrypted, "utf8")}`);
|
|
252
223
|
|
|
253
|
-
|
|
254
|
-
|
|
224
|
+
const inboundAgentId = normalizeAgentId(extractAgentId(msg));
|
|
225
|
+
if (
|
|
226
|
+
inboundAgentId !== undefined &&
|
|
227
|
+
typeof agent.agentId === "number" &&
|
|
228
|
+
Number.isFinite(agent.agentId) &&
|
|
229
|
+
inboundAgentId !== agent.agentId
|
|
230
|
+
) {
|
|
231
|
+
error?.(
|
|
232
|
+
`[wecom-agent] inbound: agentId mismatch ignored expectedAgentId=${agent.agentId} actualAgentId=${String(extractAgentId(msg) ?? "")}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
255
235
|
const msgType = extractMsgType(msg);
|
|
256
236
|
const fromUser = extractFromUser(msg);
|
|
257
237
|
const chatId = extractChatId(msg);
|
|
258
238
|
const msgId = extractMsgId(msg);
|
|
239
|
+
const eventType = String((msg as Record<string, unknown>).Event ?? "").trim().toLowerCase();
|
|
259
240
|
if (msgId) {
|
|
260
241
|
const ok = rememberAgentMsgId(msgId);
|
|
261
242
|
if (!ok) {
|
|
@@ -276,6 +257,18 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
276
257
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
277
258
|
res.end("success");
|
|
278
259
|
|
|
260
|
+
const decision = shouldProcessAgentInboundMessage({
|
|
261
|
+
msgType,
|
|
262
|
+
fromUser,
|
|
263
|
+
eventType,
|
|
264
|
+
});
|
|
265
|
+
if (!decision.shouldProcess) {
|
|
266
|
+
log?.(
|
|
267
|
+
`[wecom-agent] skip processing: type=${msgType || "unknown"} event=${eventType || "N/A"} from=${fromUser || "N/A"} reason=${decision.reason}`,
|
|
268
|
+
);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
279
272
|
// 异步处理消息
|
|
280
273
|
processAgentMessage({
|
|
281
274
|
agent,
|
|
@@ -439,9 +432,46 @@ async function processAgentMessage(params: {
|
|
|
439
432
|
cfg: config,
|
|
440
433
|
channel: "wecom",
|
|
441
434
|
accountId: agent.accountId,
|
|
442
|
-
peer: { kind: isGroup ? "group" : "
|
|
435
|
+
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
|
443
436
|
});
|
|
444
437
|
|
|
438
|
+
// ===== 动态 Agent 路由注入 =====
|
|
439
|
+
const useDynamicAgent = shouldUseDynamicAgent({
|
|
440
|
+
chatType: isGroup ? "group" : "dm",
|
|
441
|
+
senderId: fromUser,
|
|
442
|
+
config,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
|
|
446
|
+
const prompt =
|
|
447
|
+
`当前账号(${agent.accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。` +
|
|
448
|
+
`请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${agent.accountId}"}}`;
|
|
449
|
+
error?.(
|
|
450
|
+
`[wecom-agent] routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`,
|
|
451
|
+
);
|
|
452
|
+
try {
|
|
453
|
+
await sendText({ agent, toUser: fromUser, chatId: undefined, text: prompt });
|
|
454
|
+
log?.(`[wecom-agent] routing guard prompt delivered to ${fromUser}`);
|
|
455
|
+
} catch (err: unknown) {
|
|
456
|
+
error?.(`[wecom-agent] routing guard prompt failed: ${String(err)}`);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (useDynamicAgent) {
|
|
462
|
+
const targetAgentId = generateAgentId(
|
|
463
|
+
isGroup ? "group" : "dm",
|
|
464
|
+
peerId,
|
|
465
|
+
agent.accountId,
|
|
466
|
+
);
|
|
467
|
+
route.agentId = targetAgentId;
|
|
468
|
+
route.sessionKey = `agent:${targetAgentId}:wecom:${agent.accountId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
469
|
+
// 异步添加到 agents.list(不阻塞)
|
|
470
|
+
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
471
|
+
log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
472
|
+
}
|
|
473
|
+
// ===== 动态 Agent 路由注入结束 =====
|
|
474
|
+
|
|
445
475
|
// 构建上下文
|
|
446
476
|
const fromLabel = isGroup ? `group:${peerId}` : `user:${fromUser}`;
|
|
447
477
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
@@ -464,7 +494,7 @@ async function processAgentMessage(params: {
|
|
|
464
494
|
core,
|
|
465
495
|
cfg: config,
|
|
466
496
|
// Agent 门禁应读取 channels.wecom.agent.dm(即 agent.config.dm),而不是 channels.wecom.dm(不存在)
|
|
467
|
-
accountConfig: agent.config
|
|
497
|
+
accountConfig: agent.config,
|
|
468
498
|
rawBody: finalContent,
|
|
469
499
|
senderUserId: fromUser,
|
|
470
500
|
});
|
|
@@ -496,7 +526,7 @@ async function processAgentMessage(params: {
|
|
|
496
526
|
SenderName: fromUser,
|
|
497
527
|
SenderId: fromUser,
|
|
498
528
|
Provider: "wecom",
|
|
499
|
-
Surface: "
|
|
529
|
+
Surface: "webchat",
|
|
500
530
|
OriginatingChannel: "wecom",
|
|
501
531
|
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
502
532
|
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
@@ -532,32 +562,25 @@ async function processAgentMessage(params: {
|
|
|
532
562
|
await sendText({ agent, toUser: fromUser, chatId: undefined, text });
|
|
533
563
|
log?.(`[wecom-agent] reply delivered (${info.kind}) to ${fromUser}`);
|
|
534
564
|
} catch (err: unknown) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
},
|
|
565
|
+
const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
|
|
566
|
+
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
567
|
+
} },
|
|
538
568
|
onError: (err: unknown, info: { kind: string }) => {
|
|
539
569
|
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
|
540
570
|
},
|
|
541
|
-
}
|
|
542
|
-
replyOptions: {
|
|
543
|
-
disableBlockStreaming: true,
|
|
544
|
-
},
|
|
571
|
+
}
|
|
545
572
|
});
|
|
546
573
|
}
|
|
547
574
|
|
|
548
575
|
/**
|
|
549
576
|
* **handleAgentWebhook (Agent Webhook 入口)**
|
|
550
577
|
*
|
|
551
|
-
* 统一处理 Agent 模式的
|
|
552
|
-
*
|
|
578
|
+
* 统一处理 Agent 模式的 POST 消息回调请求。
|
|
579
|
+
* URL 验证与验签/解密由 monitor 层统一处理后再调用本函数。
|
|
553
580
|
*/
|
|
554
581
|
export async function handleAgentWebhook(params: AgentWebhookParams): Promise<boolean> {
|
|
555
582
|
const { req } = params;
|
|
556
583
|
|
|
557
|
-
if (req.method === "GET") {
|
|
558
|
-
return handleUrlVerification(req, params.res, params.agent);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
584
|
if (req.method === "POST") {
|
|
562
585
|
return handleMessageCallback(params);
|
|
563
586
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { wecomPlugin } from "./channel.js";
|
|
5
|
+
|
|
6
|
+
describe("wecomPlugin config.deleteAccount", () => {
|
|
7
|
+
it("removes only the target matrix account", () => {
|
|
8
|
+
const cfg: OpenClawConfig = {
|
|
9
|
+
channels: {
|
|
10
|
+
wecom: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
accounts: {
|
|
13
|
+
"acct-a": {
|
|
14
|
+
enabled: true,
|
|
15
|
+
bot: {
|
|
16
|
+
token: "token-a",
|
|
17
|
+
encodingAESKey: "aes-a",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"acct-b": {
|
|
21
|
+
enabled: true,
|
|
22
|
+
bot: {
|
|
23
|
+
token: "token-b",
|
|
24
|
+
encodingAESKey: "aes-b",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
} as OpenClawConfig;
|
|
31
|
+
|
|
32
|
+
const next = wecomPlugin.config.deleteAccount!({ cfg, accountId: "acct-a" });
|
|
33
|
+
const accounts = (next.channels?.wecom as { accounts?: Record<string, unknown> } | undefined)
|
|
34
|
+
?.accounts;
|
|
35
|
+
|
|
36
|
+
expect(accounts?.["acct-a"]).toBeUndefined();
|
|
37
|
+
expect(accounts?.["acct-b"]).toBeDefined();
|
|
38
|
+
expect(next.channels?.wecom).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("removes legacy wecom section when deleting default account", () => {
|
|
42
|
+
const cfg: OpenClawConfig = {
|
|
43
|
+
channels: {
|
|
44
|
+
wecom: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
bot: {
|
|
47
|
+
token: "token",
|
|
48
|
+
encodingAESKey: "aes",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
} as OpenClawConfig;
|
|
53
|
+
|
|
54
|
+
const next = wecomPlugin.config.deleteAccount!({ cfg, accountId: "default" });
|
|
55
|
+
expect(next.channels?.wecom).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("wecomPlugin account conflict guards", () => {
|
|
60
|
+
it("marks duplicate bot token account as unconfigured", async () => {
|
|
61
|
+
const cfg: OpenClawConfig = {
|
|
62
|
+
channels: {
|
|
63
|
+
wecom: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
accounts: {
|
|
66
|
+
"acct-a": {
|
|
67
|
+
enabled: true,
|
|
68
|
+
bot: { token: "token-shared", encodingAESKey: "aes-a" },
|
|
69
|
+
},
|
|
70
|
+
"acct-b": {
|
|
71
|
+
enabled: true,
|
|
72
|
+
bot: { token: "token-shared", encodingAESKey: "aes-b" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
} as OpenClawConfig;
|
|
78
|
+
|
|
79
|
+
const accountA = wecomPlugin.config.resolveAccount(cfg, "acct-a");
|
|
80
|
+
const accountB = wecomPlugin.config.resolveAccount(cfg, "acct-b");
|
|
81
|
+
expect(await wecomPlugin.config.isConfigured!(accountA, cfg)).toBe(true);
|
|
82
|
+
expect(await wecomPlugin.config.isConfigured!(accountB, cfg)).toBe(false);
|
|
83
|
+
expect(wecomPlugin.config.unconfiguredReason?.(accountB, cfg)).toContain("Duplicate WeCom bot token");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("marks duplicate bot aibotid account as unconfigured", async () => {
|
|
87
|
+
const cfg: OpenClawConfig = {
|
|
88
|
+
channels: {
|
|
89
|
+
wecom: {
|
|
90
|
+
enabled: true,
|
|
91
|
+
accounts: {
|
|
92
|
+
"acct-a": {
|
|
93
|
+
enabled: true,
|
|
94
|
+
bot: { token: "token-a", encodingAESKey: "aes-a", aibotid: "BOT_001" },
|
|
95
|
+
},
|
|
96
|
+
"acct-b": {
|
|
97
|
+
enabled: true,
|
|
98
|
+
bot: { token: "token-b", encodingAESKey: "aes-b", aibotid: "BOT_001" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} as OpenClawConfig;
|
|
104
|
+
|
|
105
|
+
const accountB = wecomPlugin.config.resolveAccount(cfg, "acct-b");
|
|
106
|
+
expect(await wecomPlugin.config.isConfigured!(accountB, cfg)).toBe(false);
|
|
107
|
+
expect(wecomPlugin.config.unconfiguredReason?.(accountB, cfg)).toContain("Duplicate WeCom bot aibotid");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("marks duplicate corpId/agentId account as unconfigured", async () => {
|
|
111
|
+
const cfg: OpenClawConfig = {
|
|
112
|
+
channels: {
|
|
113
|
+
wecom: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
accounts: {
|
|
116
|
+
"acct-a": {
|
|
117
|
+
enabled: true,
|
|
118
|
+
agent: {
|
|
119
|
+
corpId: "corp-1",
|
|
120
|
+
corpSecret: "secret-a",
|
|
121
|
+
agentId: 1001,
|
|
122
|
+
token: "token-a",
|
|
123
|
+
encodingAESKey: "aes-a",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"acct-b": {
|
|
127
|
+
enabled: true,
|
|
128
|
+
agent: {
|
|
129
|
+
corpId: "corp-1",
|
|
130
|
+
corpSecret: "secret-b",
|
|
131
|
+
agentId: 1001,
|
|
132
|
+
token: "token-b",
|
|
133
|
+
encodingAESKey: "aes-b",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
} as OpenClawConfig;
|
|
140
|
+
|
|
141
|
+
const accountB = wecomPlugin.config.resolveAccount(cfg, "acct-b");
|
|
142
|
+
expect(await wecomPlugin.config.isConfigured!(accountB, cfg)).toBe(false);
|
|
143
|
+
expect(wecomPlugin.config.unconfiguredReason?.(accountB, cfg)).toContain(
|
|
144
|
+
"Duplicate WeCom agent identity",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|