@yanhaidao/wecom 2.2.7 → 2.3.2
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 +275 -91
- 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/assets/register.png +0 -0
- package/changelog/v2.2.28.md +70 -0
- package/changelog/v2.3.2.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 +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 +147 -145
- 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/network.ts +9 -5
- 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/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 +13 -7
- package/src/monitor.inbound-filter.test.ts +63 -0
- package/src/monitor.ts +948 -128
- package/src/monitor.webhook.test.ts +288 -3
- 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 +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
package/src/agent/handler.ts
CHANGED
|
@@ -8,15 +8,21 @@ 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";
|
|
20
26
|
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";
|
|
21
27
|
|
|
22
28
|
/** 错误提示信息 */
|
|
@@ -99,6 +105,18 @@ function buildTextFilePreview(buffer: Buffer, maxChars: number): string | undefi
|
|
|
99
105
|
export type AgentWebhookParams = {
|
|
100
106
|
req: IncomingMessage;
|
|
101
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
|
+
};
|
|
102
120
|
agent: ResolvedAgentAccount;
|
|
103
121
|
config: OpenClawConfig;
|
|
104
122
|
core: PluginRuntime;
|
|
@@ -106,157 +124,119 @@ export type AgentWebhookParams = {
|
|
|
106
124
|
error?: (msg: string) => void;
|
|
107
125
|
};
|
|
108
126
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*/
|
|
114
|
-
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
115
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
116
|
-
return url.searchParams;
|
|
117
|
-
}
|
|
127
|
+
export type AgentInboundProcessDecision = {
|
|
128
|
+
shouldProcess: boolean;
|
|
129
|
+
reason: string;
|
|
130
|
+
};
|
|
118
131
|
|
|
119
132
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
133
|
+
* 仅允许“用户意图消息”进入 AI 会话。
|
|
134
|
+
* - event 回调(如 enter_agent/subscribe)不应触发会话与自动回复
|
|
135
|
+
* - 系统发送者(sys)不应触发会话与自动回复
|
|
136
|
+
* - 缺失发送者时默认丢弃,避免写入异常会话
|
|
124
137
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
154
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
if (!fromUser) {
|
|
156
|
+
return {
|
|
157
|
+
shouldProcess: false,
|
|
158
|
+
reason: "missing_sender",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
143
161
|
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
/**
|
|
149
|
-
* **
|
|
184
|
+
* **resolveQueryParams (解析查询参数)**
|
|
150
185
|
*
|
|
151
|
-
*
|
|
152
|
-
* 流程:
|
|
153
|
-
* 1. 验证 msg_signature 签名。
|
|
154
|
-
* 2. 解密 echostr 参数。
|
|
155
|
-
* 3. 返回解密后的明文 echostr。
|
|
186
|
+
* 辅助函数:从 IncomingMessage 中解析 URL 查询字符串,用于获取签名、时间戳等参数。
|
|
156
187
|
*/
|
|
157
|
-
|
|
158
|
-
req
|
|
159
|
-
|
|
160
|
-
agent: ResolvedAgentAccount,
|
|
161
|
-
): Promise<boolean> {
|
|
162
|
-
const query = resolveQueryParams(req);
|
|
163
|
-
const timestamp = query.get("timestamp") ?? "";
|
|
164
|
-
const nonce = query.get("nonce") ?? "";
|
|
165
|
-
const echostr = query.get("echostr") ?? "";
|
|
166
|
-
const signature = query.get("msg_signature") ?? "";
|
|
167
|
-
const remote = req.socket?.remoteAddress ?? "unknown";
|
|
168
|
-
|
|
169
|
-
// 不输出敏感参数内容,仅输出存在性
|
|
170
|
-
// 用于排查:是否有请求打到 /wecom/agent
|
|
171
|
-
// 以及是否带齐 timestamp/nonce/msg_signature/echostr
|
|
172
|
-
// eslint-disable-next-line no-unused-vars
|
|
173
|
-
const _debug = { remote, hasTimestamp: Boolean(timestamp), hasNonce: Boolean(nonce), hasSig: Boolean(signature), hasEchostr: Boolean(echostr) };
|
|
174
|
-
|
|
175
|
-
const valid = verifyWecomSignature({
|
|
176
|
-
token: agent.token,
|
|
177
|
-
timestamp,
|
|
178
|
-
nonce,
|
|
179
|
-
encrypt: echostr,
|
|
180
|
-
signature,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (!valid) {
|
|
184
|
-
res.statusCode = 401;
|
|
185
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
186
|
-
res.end(`unauthorized - 签名验证失败,请检查 Token 配置${ERROR_HELP}`);
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const plain = decryptWecomEncrypted({
|
|
192
|
-
encodingAESKey: agent.encodingAESKey,
|
|
193
|
-
receiveId: agent.corpId,
|
|
194
|
-
encrypt: echostr,
|
|
195
|
-
});
|
|
196
|
-
res.statusCode = 200;
|
|
197
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
198
|
-
res.end(plain);
|
|
199
|
-
return true;
|
|
200
|
-
} catch {
|
|
201
|
-
res.statusCode = 400;
|
|
202
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
203
|
-
res.end(`decrypt failed - 解密失败,请检查 EncodingAESKey 配置${ERROR_HELP}`);
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
188
|
+
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
|
|
189
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
190
|
+
return url.searchParams;
|
|
206
191
|
}
|
|
207
192
|
|
|
208
193
|
/**
|
|
209
194
|
* 处理消息回调 (POST)
|
|
210
195
|
*/
|
|
211
196
|
async function handleMessageCallback(params: AgentWebhookParams): Promise<boolean> {
|
|
212
|
-
const { req, res, agent, config, core, log, error } = params;
|
|
197
|
+
const { req, res, verifiedPost, agent, config, core, log, error } = params;
|
|
213
198
|
|
|
214
199
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
220
207
|
|
|
208
|
+
log?.(`[wecom-agent] inbound: method=${req.method ?? "UNKNOWN"} remote=${req.socket?.remoteAddress ?? "unknown"}`);
|
|
221
209
|
const query = resolveQueryParams(req);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
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;
|
|
225
218
|
log?.(
|
|
226
|
-
`[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}`,
|
|
227
220
|
);
|
|
228
221
|
|
|
229
|
-
// 验证签名
|
|
230
|
-
const valid = verifyWecomSignature({
|
|
231
|
-
token: agent.token,
|
|
232
|
-
timestamp,
|
|
233
|
-
nonce,
|
|
234
|
-
encrypt: encrypted,
|
|
235
|
-
signature,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
if (!valid) {
|
|
239
|
-
error?.(`[wecom-agent] inbound: signature invalid`);
|
|
240
|
-
res.statusCode = 401;
|
|
241
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
242
|
-
res.end(`unauthorized - 签名验证失败${ERROR_HELP}`);
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// 解密
|
|
247
|
-
const decrypted = decryptWecomEncrypted({
|
|
248
|
-
encodingAESKey: agent.encodingAESKey,
|
|
249
|
-
receiveId: agent.corpId,
|
|
250
|
-
encrypt: encrypted,
|
|
251
|
-
});
|
|
252
222
|
log?.(`[wecom-agent] inbound: decryptedBytes=${Buffer.byteLength(decrypted, "utf8")}`);
|
|
253
223
|
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
}
|
|
256
235
|
const msgType = extractMsgType(msg);
|
|
257
236
|
const fromUser = extractFromUser(msg);
|
|
258
237
|
const chatId = extractChatId(msg);
|
|
259
238
|
const msgId = extractMsgId(msg);
|
|
239
|
+
const eventType = String((msg as Record<string, unknown>).Event ?? "").trim().toLowerCase();
|
|
260
240
|
if (msgId) {
|
|
261
241
|
const ok = rememberAgentMsgId(msgId);
|
|
262
242
|
if (!ok) {
|
|
@@ -277,6 +257,18 @@ async function handleMessageCallback(params: AgentWebhookParams): Promise<boolea
|
|
|
277
257
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
278
258
|
res.end("success");
|
|
279
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
|
+
|
|
280
272
|
// 异步处理消息
|
|
281
273
|
processAgentMessage({
|
|
282
274
|
agent,
|
|
@@ -440,7 +432,7 @@ async function processAgentMessage(params: {
|
|
|
440
432
|
cfg: config,
|
|
441
433
|
channel: "wecom",
|
|
442
434
|
accountId: agent.accountId,
|
|
443
|
-
peer: { kind: isGroup ? "group" : "
|
|
435
|
+
peer: { kind: isGroup ? "group" : "direct", id: peerId },
|
|
444
436
|
});
|
|
445
437
|
|
|
446
438
|
// ===== 动态 Agent 路由注入 =====
|
|
@@ -450,13 +442,30 @@ async function processAgentMessage(params: {
|
|
|
450
442
|
config,
|
|
451
443
|
});
|
|
452
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
|
+
|
|
453
461
|
if (useDynamicAgent) {
|
|
454
462
|
const targetAgentId = generateAgentId(
|
|
455
463
|
isGroup ? "group" : "dm",
|
|
456
|
-
peerId
|
|
464
|
+
peerId,
|
|
465
|
+
agent.accountId,
|
|
457
466
|
);
|
|
458
467
|
route.agentId = targetAgentId;
|
|
459
|
-
route.sessionKey = `agent:${targetAgentId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
468
|
+
route.sessionKey = `agent:${targetAgentId}:wecom:${agent.accountId}:${isGroup ? "group" : "dm"}:${peerId}`;
|
|
460
469
|
// 异步添加到 agents.list(不阻塞)
|
|
461
470
|
ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
|
|
462
471
|
log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
|
|
@@ -485,7 +494,7 @@ async function processAgentMessage(params: {
|
|
|
485
494
|
core,
|
|
486
495
|
cfg: config,
|
|
487
496
|
// Agent 门禁应读取 channels.wecom.agent.dm(即 agent.config.dm),而不是 channels.wecom.dm(不存在)
|
|
488
|
-
accountConfig: agent.config
|
|
497
|
+
accountConfig: agent.config,
|
|
489
498
|
rawBody: finalContent,
|
|
490
499
|
senderUserId: fromUser,
|
|
491
500
|
});
|
|
@@ -517,7 +526,7 @@ async function processAgentMessage(params: {
|
|
|
517
526
|
SenderName: fromUser,
|
|
518
527
|
SenderId: fromUser,
|
|
519
528
|
Provider: "wecom",
|
|
520
|
-
Surface: "
|
|
529
|
+
Surface: "webchat",
|
|
521
530
|
OriginatingChannel: "wecom",
|
|
522
531
|
// 标记为 Agent 会话的回复路由目标,避免与 Bot 会话混淆:
|
|
523
532
|
// - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
|
|
@@ -553,32 +562,25 @@ async function processAgentMessage(params: {
|
|
|
553
562
|
await sendText({ agent, toUser: fromUser, chatId: undefined, text });
|
|
554
563
|
log?.(`[wecom-agent] reply delivered (${info.kind}) to ${fromUser}`);
|
|
555
564
|
} catch (err: unknown) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
},
|
|
565
|
+
const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
|
|
566
|
+
error?.(`[wecom-agent] reply failed: ${message}`);
|
|
567
|
+
} },
|
|
559
568
|
onError: (err: unknown, info: { kind: string }) => {
|
|
560
569
|
error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
|
|
561
570
|
},
|
|
562
|
-
}
|
|
563
|
-
replyOptions: {
|
|
564
|
-
disableBlockStreaming: true,
|
|
565
|
-
},
|
|
571
|
+
}
|
|
566
572
|
});
|
|
567
573
|
}
|
|
568
574
|
|
|
569
575
|
/**
|
|
570
576
|
* **handleAgentWebhook (Agent Webhook 入口)**
|
|
571
577
|
*
|
|
572
|
-
* 统一处理 Agent 模式的
|
|
573
|
-
*
|
|
578
|
+
* 统一处理 Agent 模式的 POST 消息回调请求。
|
|
579
|
+
* URL 验证与验签/解密由 monitor 层统一处理后再调用本函数。
|
|
574
580
|
*/
|
|
575
581
|
export async function handleAgentWebhook(params: AgentWebhookParams): Promise<boolean> {
|
|
576
582
|
const { req } = params;
|
|
577
583
|
|
|
578
|
-
if (req.method === "GET") {
|
|
579
|
-
return handleUrlVerification(req, params.res, params.agent);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
584
|
if (req.method === "POST") {
|
|
583
585
|
return handleMessageCallback(params);
|
|
584
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
|
+
});
|