@sunnoy/wecom 1.2.0 → 1.4.0
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/README.md +831 -147
- package/dynamic-agent.js +18 -4
- package/index.js +16 -1602
- package/package.json +8 -2
- package/wecom/accounts.js +258 -0
- package/wecom/agent-api.js +251 -0
- package/wecom/agent-inbound.js +441 -0
- package/wecom/allow-from.js +45 -0
- package/wecom/channel-plugin.js +732 -0
- package/wecom/commands.js +90 -0
- package/wecom/constants.js +58 -0
- package/wecom/http-handler.js +315 -0
- package/wecom/inbound-processor.js +531 -0
- package/wecom/media.js +118 -0
- package/wecom/outbound-delivery.js +484 -0
- package/wecom/response-url.js +33 -0
- package/wecom/state.js +84 -0
- package/wecom/stream-utils.js +124 -0
- package/wecom/target.js +57 -0
- package/wecom/webhook-bot.js +155 -0
- package/wecom/webhook-targets.js +28 -0
- package/wecom/workspace-template.js +165 -0
- package/wecom/xml-parser.js +126 -0
- package/README_ZH.md +0 -303
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeCom Agent Inbound Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles XML-format callbacks from WeCom self-built applications (自建应用).
|
|
5
|
+
* - GET /webhooks/app → URL verification (echostr decrypt)
|
|
6
|
+
* - POST /webhooks/app → Message callback (decrypt → parse → dispatch to LLM)
|
|
7
|
+
*
|
|
8
|
+
* Replies are sent asynchronously via Agent API (not passive stream response).
|
|
9
|
+
* Uses the same sessionKey format as Bot mode for unified session management.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from "../logger.js";
|
|
13
|
+
import { WecomCrypto } from "../crypto.js";
|
|
14
|
+
import {
|
|
15
|
+
generateAgentId,
|
|
16
|
+
getDynamicAgentConfig,
|
|
17
|
+
shouldUseDynamicAgent,
|
|
18
|
+
} from "../dynamic-agent.js";
|
|
19
|
+
import { agentSendText, agentDownloadMedia } from "./agent-api.js";
|
|
20
|
+
import { resolveAccount } from "./accounts.js";
|
|
21
|
+
import { resolveWecomCommandAuthorized } from "./allow-from.js";
|
|
22
|
+
import { checkCommandAllowlist, getCommandConfig, isWecomAdmin } from "./commands.js";
|
|
23
|
+
import { MAX_REQUEST_BODY_SIZE } from "./constants.js";
|
|
24
|
+
import { getRuntime, resolveAgentConfig } from "./state.js";
|
|
25
|
+
import { ensureDynamicAgentListed } from "./workspace-template.js";
|
|
26
|
+
import {
|
|
27
|
+
extractEncryptFromXml,
|
|
28
|
+
parseXml,
|
|
29
|
+
extractMsgType,
|
|
30
|
+
extractFromUser,
|
|
31
|
+
extractChatId,
|
|
32
|
+
extractContent,
|
|
33
|
+
extractMediaId,
|
|
34
|
+
extractMsgId,
|
|
35
|
+
extractFileName,
|
|
36
|
+
} from "./xml-parser.js";
|
|
37
|
+
|
|
38
|
+
// ── Message deduplication ──────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const RECENT_MSGID_TTL_MS = 10 * 60 * 1000;
|
|
41
|
+
const recentAgentMsgIds = new Map();
|
|
42
|
+
|
|
43
|
+
function rememberAgentMsgId(msgId) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const existing = recentAgentMsgIds.get(msgId);
|
|
46
|
+
if (existing && now - existing < RECENT_MSGID_TTL_MS) return false;
|
|
47
|
+
recentAgentMsgIds.set(msgId, now);
|
|
48
|
+
// Prune expired entries on write
|
|
49
|
+
for (const [k, ts] of recentAgentMsgIds) {
|
|
50
|
+
if (now - ts >= RECENT_MSGID_TTL_MS) recentAgentMsgIds.delete(k);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── HTTP body reader ───────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function readRawBody(req, maxSize = MAX_REQUEST_BODY_SIZE) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
let size = 0;
|
|
61
|
+
|
|
62
|
+
req.on("data", (chunk) => {
|
|
63
|
+
size += chunk.length;
|
|
64
|
+
if (size > maxSize) {
|
|
65
|
+
reject(new Error("Request body too large"));
|
|
66
|
+
req.destroy();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
req.on("end", () => {
|
|
73
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
req.on("error", reject);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── URL Verification (GET) ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle WeCom URL verification during callback configuration.
|
|
84
|
+
* Verify signature → decrypt echostr → return plaintext.
|
|
85
|
+
*/
|
|
86
|
+
function handleUrlVerification(req, res, crypto) {
|
|
87
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
88
|
+
const timestamp = url.searchParams.get("timestamp") || "";
|
|
89
|
+
const nonce = url.searchParams.get("nonce") || "";
|
|
90
|
+
const echostr = url.searchParams.get("echostr") || "";
|
|
91
|
+
const msgSignature = url.searchParams.get("msg_signature") || "";
|
|
92
|
+
|
|
93
|
+
// Verify signature
|
|
94
|
+
const expectedSig = crypto.getSignature(timestamp, nonce, echostr);
|
|
95
|
+
if (expectedSig !== msgSignature) {
|
|
96
|
+
logger.warn("[agent-inbound] URL verification: signature mismatch");
|
|
97
|
+
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8" });
|
|
98
|
+
res.end("unauthorized - 签名验证失败,请检查 Token 配置");
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Decrypt echostr
|
|
103
|
+
try {
|
|
104
|
+
const { message: plainEchostr } = crypto.decrypt(echostr);
|
|
105
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
106
|
+
res.end(plainEchostr);
|
|
107
|
+
logger.info("[agent-inbound] URL verification successful");
|
|
108
|
+
return true;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.error("[agent-inbound] URL verification: decrypt failed", { error: err.message });
|
|
111
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
112
|
+
res.end("decrypt failed - 解密失败,请检查 EncodingAESKey 配置");
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Message Callback (POST) ────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle WeCom message callback.
|
|
121
|
+
* Read XML → extract Encrypt → verify → decrypt → parse → dedup → respond 200 → async process.
|
|
122
|
+
*/
|
|
123
|
+
async function handleMessageCallback(req, res, crypto, agentConfig, config, accountId) {
|
|
124
|
+
try {
|
|
125
|
+
const rawXml = await readRawBody(req);
|
|
126
|
+
logger.debug("[agent-inbound] received callback", { bodyBytes: Buffer.byteLength(rawXml, "utf8") });
|
|
127
|
+
|
|
128
|
+
const encrypted = extractEncryptFromXml(rawXml);
|
|
129
|
+
|
|
130
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
131
|
+
const timestamp = url.searchParams.get("timestamp") || "";
|
|
132
|
+
const nonce = url.searchParams.get("nonce") || "";
|
|
133
|
+
const msgSignature = url.searchParams.get("msg_signature") || "";
|
|
134
|
+
|
|
135
|
+
// Verify signature
|
|
136
|
+
const expectedSig = crypto.getSignature(timestamp, nonce, encrypted);
|
|
137
|
+
if (expectedSig !== msgSignature) {
|
|
138
|
+
logger.warn("[agent-inbound] message callback: signature mismatch");
|
|
139
|
+
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8" });
|
|
140
|
+
res.end("unauthorized - 签名验证失败");
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Decrypt
|
|
145
|
+
const { message: decryptedXml } = crypto.decrypt(encrypted);
|
|
146
|
+
logger.debug("[agent-inbound] decrypted", { bytes: Buffer.byteLength(decryptedXml, "utf8") });
|
|
147
|
+
|
|
148
|
+
// Parse XML
|
|
149
|
+
const msg = parseXml(decryptedXml);
|
|
150
|
+
const msgType = extractMsgType(msg);
|
|
151
|
+
const fromUser = extractFromUser(msg);
|
|
152
|
+
const chatId = extractChatId(msg);
|
|
153
|
+
const msgId = extractMsgId(msg);
|
|
154
|
+
const content = extractContent(msg);
|
|
155
|
+
|
|
156
|
+
// Deduplication
|
|
157
|
+
if (msgId) {
|
|
158
|
+
if (!rememberAgentMsgId(msgId)) {
|
|
159
|
+
logger.debug("[agent-inbound] duplicate msgId, skipping", { msgId, fromUser });
|
|
160
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
161
|
+
res.end("success");
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
logger.info("[agent-inbound] message received", {
|
|
167
|
+
msgType,
|
|
168
|
+
fromUser,
|
|
169
|
+
chatId: chatId || "N/A",
|
|
170
|
+
msgId: msgId || "N/A",
|
|
171
|
+
contentPreview: content.substring(0, 100),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Respond immediately (Agent mode uses API for replies, not passive response)
|
|
175
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
176
|
+
res.end("success");
|
|
177
|
+
|
|
178
|
+
// Async message processing
|
|
179
|
+
processAgentMessage({
|
|
180
|
+
agentConfig,
|
|
181
|
+
config,
|
|
182
|
+
accountId,
|
|
183
|
+
fromUser,
|
|
184
|
+
chatId,
|
|
185
|
+
msgType,
|
|
186
|
+
content,
|
|
187
|
+
msg,
|
|
188
|
+
}).catch((err) => {
|
|
189
|
+
logger.error("[agent-inbound] async processing failed", { error: err.message });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return true;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.error("[agent-inbound] callback failed", { error: err.message });
|
|
195
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
196
|
+
res.end("error - 回调处理失败");
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Async Message Processing ───────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process a decrypted Agent message and dispatch to the LLM.
|
|
205
|
+
* Uses the same dynamic agent routing and sessionKey format as Bot mode
|
|
206
|
+
* to ensure unified session management.
|
|
207
|
+
*/
|
|
208
|
+
async function processAgentMessage({
|
|
209
|
+
agentConfig,
|
|
210
|
+
config,
|
|
211
|
+
accountId,
|
|
212
|
+
fromUser,
|
|
213
|
+
chatId,
|
|
214
|
+
msgType,
|
|
215
|
+
content,
|
|
216
|
+
msg,
|
|
217
|
+
}) {
|
|
218
|
+
const runtime = getRuntime();
|
|
219
|
+
const core = runtime.channel;
|
|
220
|
+
|
|
221
|
+
// Resolve per-account config for utility functions.
|
|
222
|
+
const resolvedAccount = resolveAccount(config, accountId);
|
|
223
|
+
const accountCfg = resolvedAccount?.config || {};
|
|
224
|
+
|
|
225
|
+
const isGroup = Boolean(chatId);
|
|
226
|
+
const peerId = isGroup ? chatId : fromUser;
|
|
227
|
+
const peerKind = isGroup ? "group" : "dm";
|
|
228
|
+
|
|
229
|
+
let finalContent = content;
|
|
230
|
+
const mediaPaths = [];
|
|
231
|
+
const mediaTypes = [];
|
|
232
|
+
|
|
233
|
+
// ── Media processing ──────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
if (["image", "voice", "video", "file"].includes(msgType)) {
|
|
236
|
+
const mediaId = extractMediaId(msg);
|
|
237
|
+
if (mediaId) {
|
|
238
|
+
try {
|
|
239
|
+
logger.debug("[agent-inbound] downloading media", { mediaId, msgType });
|
|
240
|
+
const { buffer, contentType } = await agentDownloadMedia({
|
|
241
|
+
agent: agentConfig,
|
|
242
|
+
mediaId,
|
|
243
|
+
});
|
|
244
|
+
const originalFileName = extractFileName(msg) || `${mediaId}.bin`;
|
|
245
|
+
|
|
246
|
+
// Save media via core SDK
|
|
247
|
+
const saved = await core.media.saveMediaBuffer(
|
|
248
|
+
buffer,
|
|
249
|
+
contentType,
|
|
250
|
+
"inbound",
|
|
251
|
+
25 * 1024 * 1024,
|
|
252
|
+
originalFileName,
|
|
253
|
+
);
|
|
254
|
+
logger.info("[agent-inbound] media saved", { path: saved.path, size: buffer.length });
|
|
255
|
+
|
|
256
|
+
mediaPaths.push(saved.path);
|
|
257
|
+
mediaTypes.push(contentType);
|
|
258
|
+
finalContent = `${content} (已下载 ${buffer.length} 字节)`;
|
|
259
|
+
|
|
260
|
+
// For image-only messages, set a placeholder body.
|
|
261
|
+
if (!content.trim() || content.startsWith("[图片]")) {
|
|
262
|
+
finalContent = "[用户发送了一张图片]";
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
logger.error("[agent-inbound] media download failed", { error: err.message });
|
|
266
|
+
finalContent = `${content}\n\n媒体处理失败:${err.message}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Command allowlist ─────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
const senderIsAdmin = isWecomAdmin(fromUser, accountCfg);
|
|
274
|
+
const commandCheck = checkCommandAllowlist(finalContent, accountCfg);
|
|
275
|
+
|
|
276
|
+
if (commandCheck.isCommand && !commandCheck.allowed && !senderIsAdmin) {
|
|
277
|
+
const cmdConfig = getCommandConfig(accountCfg);
|
|
278
|
+
logger.warn("[agent-inbound] blocked command", { command: commandCheck.command, from: fromUser });
|
|
279
|
+
try {
|
|
280
|
+
await agentSendText({ agent: agentConfig, toUser: fromUser, text: cmdConfig.blockMessage });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logger.error("[agent-inbound] failed to send block message", { error: err.message });
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Dynamic agent routing ─────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
const dynamicConfig = getDynamicAgentConfig(accountCfg);
|
|
290
|
+
const targetAgentId =
|
|
291
|
+
dynamicConfig.enabled && shouldUseDynamicAgent({ chatType: peerKind, config: accountCfg })
|
|
292
|
+
? generateAgentId(peerKind, peerId, accountId)
|
|
293
|
+
: null;
|
|
294
|
+
|
|
295
|
+
if (targetAgentId) {
|
|
296
|
+
await ensureDynamicAgentListed(targetAgentId);
|
|
297
|
+
logger.debug("[agent-inbound] dynamic agent", { agentId: targetAgentId, peerId });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Route resolution ──────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
const route = core.routing.resolveAgentRoute({
|
|
303
|
+
cfg: config,
|
|
304
|
+
channel: "wecom",
|
|
305
|
+
accountId: accountId || "default",
|
|
306
|
+
peer: { kind: peerKind, id: peerId },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (targetAgentId) {
|
|
310
|
+
route.agentId = targetAgentId;
|
|
311
|
+
route.sessionKey = `agent:${targetAgentId}:${peerKind}:${peerId}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Build inbound context ─────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
const fromLabel = isGroup ? `[${fromUser}]` : fromUser;
|
|
317
|
+
const storePath = core.session.resolveStorePath(config.session?.store, {
|
|
318
|
+
agentId: route.agentId,
|
|
319
|
+
});
|
|
320
|
+
const envelopeOptions = core.reply.resolveEnvelopeFormatOptions(config);
|
|
321
|
+
const previousTimestamp = core.session.readSessionUpdatedAt({
|
|
322
|
+
storePath,
|
|
323
|
+
sessionKey: route.sessionKey,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const body = core.reply.formatAgentEnvelope({
|
|
327
|
+
channel: isGroup ? "Enterprise WeChat Group" : "Enterprise WeChat",
|
|
328
|
+
from: fromLabel,
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
previousTimestamp,
|
|
331
|
+
envelope: envelopeOptions,
|
|
332
|
+
body: finalContent,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const commandAuthorized = resolveWecomCommandAuthorized({
|
|
336
|
+
cfg: config,
|
|
337
|
+
accountId: accountId || "default",
|
|
338
|
+
senderId: fromUser,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const conversationId = isGroup ? `wecom:group:${chatId}` : `wecom:${fromUser}`;
|
|
342
|
+
|
|
343
|
+
const ctxPayload = core.reply.finalizeInboundContext({
|
|
344
|
+
Body: body,
|
|
345
|
+
RawBody: finalContent,
|
|
346
|
+
CommandBody: finalContent,
|
|
347
|
+
From: isGroup ? `wecom:group:${peerId}` : `wecom:${fromUser}`,
|
|
348
|
+
To: conversationId,
|
|
349
|
+
SessionKey: route.sessionKey,
|
|
350
|
+
AccountId: route.accountId,
|
|
351
|
+
ChatType: isGroup ? "group" : "direct",
|
|
352
|
+
ConversationLabel: isGroup ? `Group ${chatId}` : fromUser,
|
|
353
|
+
SenderName: fromUser,
|
|
354
|
+
SenderId: fromUser,
|
|
355
|
+
Provider: "wecom",
|
|
356
|
+
Surface: "wecom",
|
|
357
|
+
OriginatingChannel: "wecom",
|
|
358
|
+
OriginatingTo: `wecom-agent:${fromUser}`,
|
|
359
|
+
CommandAuthorized: commandAuthorized,
|
|
360
|
+
...(mediaPaths.length > 0 && { MediaPaths: mediaPaths }),
|
|
361
|
+
...(mediaTypes.length > 0 && { MediaTypes: mediaTypes }),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ── Record session ────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
void core.session
|
|
367
|
+
.recordSessionMetaFromInbound({
|
|
368
|
+
storePath,
|
|
369
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
370
|
+
ctx: ctxPayload,
|
|
371
|
+
})
|
|
372
|
+
.catch((err) => {
|
|
373
|
+
logger.error("[agent-inbound] session record failed", { error: err.message });
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ── Dispatch to LLM ──────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
await core.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
379
|
+
ctx: ctxPayload,
|
|
380
|
+
cfg: config,
|
|
381
|
+
replyOptions: {
|
|
382
|
+
disableBlockStreaming: true,
|
|
383
|
+
},
|
|
384
|
+
dispatcherOptions: {
|
|
385
|
+
deliver: async (payload, info) => {
|
|
386
|
+
const text = payload.text ?? "";
|
|
387
|
+
if (!text.trim()) return;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
// Agent mode: reply via API to the sender (DM, even for group messages)
|
|
391
|
+
await agentSendText({ agent: agentConfig, toUser: fromUser, text });
|
|
392
|
+
logger.info("[agent-inbound] reply delivered", {
|
|
393
|
+
kind: info.kind,
|
|
394
|
+
to: fromUser,
|
|
395
|
+
contentPreview: text.substring(0, 50),
|
|
396
|
+
});
|
|
397
|
+
} catch (err) {
|
|
398
|
+
logger.error("[agent-inbound] reply delivery failed", { error: err.message });
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
onError: (err, info) => {
|
|
402
|
+
logger.error("[agent-inbound] dispatch error", { kind: info.kind, error: err.message });
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Public Entry Point ─────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Handle Agent inbound webhook request.
|
|
412
|
+
* Routes GET → URL verification, POST → message callback.
|
|
413
|
+
*
|
|
414
|
+
* @param {object} params
|
|
415
|
+
* @param {import("http").IncomingMessage} params.req
|
|
416
|
+
* @param {import("http").ServerResponse} params.res
|
|
417
|
+
* @param {object} params.agentAccount - { token, encodingAesKey, corpId, corpSecret, agentId }
|
|
418
|
+
* @param {object} params.config - Full openclaw config
|
|
419
|
+
* @returns {Promise<boolean>} Whether the request was handled
|
|
420
|
+
*/
|
|
421
|
+
export async function handleAgentInbound({ req, res, agentAccount, config }) {
|
|
422
|
+
const crypto = new WecomCrypto(agentAccount.token, agentAccount.encodingAesKey);
|
|
423
|
+
const agentConfig = {
|
|
424
|
+
corpId: agentAccount.corpId,
|
|
425
|
+
corpSecret: agentAccount.corpSecret,
|
|
426
|
+
agentId: agentAccount.agentId,
|
|
427
|
+
};
|
|
428
|
+
const accountId = agentAccount.accountId || "default";
|
|
429
|
+
|
|
430
|
+
if (req.method === "GET") {
|
|
431
|
+
return handleUrlVerification(req, res, crypto);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (req.method === "POST") {
|
|
435
|
+
return handleMessageCallback(req, res, crypto, agentConfig, config, accountId);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
439
|
+
res.end("Method Not Allowed");
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resolveAccount } from "./accounts.js";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
export function normalizeWecomAllowFromEntry(raw) {
|
|
5
|
+
const trimmed = String(raw ?? "").trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if (trimmed === "*") {
|
|
10
|
+
return "*";
|
|
11
|
+
}
|
|
12
|
+
return trimmed
|
|
13
|
+
.replace(/^(wecom|wework):/i, "")
|
|
14
|
+
.replace(/^user:/i, "")
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveWecomAllowFrom(cfg, accountId) {
|
|
19
|
+
const account = resolveAccount(cfg, accountId);
|
|
20
|
+
if (!account) return [];
|
|
21
|
+
|
|
22
|
+
const accountCfg = account.config;
|
|
23
|
+
const allowFromRaw = accountCfg?.dm?.allowFrom ?? accountCfg?.allowFrom ?? [];
|
|
24
|
+
|
|
25
|
+
if (!Array.isArray(allowFromRaw)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return allowFromRaw.map(normalizeWecomAllowFromEntry).filter((entry) => Boolean(entry));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveWecomCommandAuthorized({ cfg, accountId, senderId }) {
|
|
33
|
+
const sender = String(senderId ?? "")
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
if (!sender) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const allowFrom = resolveWecomAllowFrom(cfg, accountId);
|
|
41
|
+
if (allowFrom.includes("*") || allowFrom.length === 0) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return allowFrom.includes(sender);
|
|
45
|
+
}
|