@openclaw-china/wecom 0.1.1
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/clawdbot.plugin.json +52 -0
- package/dist/index.d.ts +414 -0
- package/dist/index.js +1086 -0
- package/dist/index.js.map +1 -0
- package/moltbot.plugin.json +52 -0
- package/package.json +96 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
var DEFAULT_ACCOUNT_ID = "default";
|
|
6
|
+
var WecomAccountSchema = z.object({
|
|
7
|
+
name: z.string().optional(),
|
|
8
|
+
enabled: z.boolean().optional(),
|
|
9
|
+
webhookPath: z.string().optional(),
|
|
10
|
+
token: z.string().optional(),
|
|
11
|
+
encodingAESKey: z.string().optional(),
|
|
12
|
+
receiveId: z.string().optional(),
|
|
13
|
+
welcomeText: z.string().optional(),
|
|
14
|
+
dmPolicy: z.enum(["open", "pairing", "allowlist", "disabled"]).optional(),
|
|
15
|
+
allowFrom: z.array(z.string()).optional(),
|
|
16
|
+
groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
|
|
17
|
+
groupAllowFrom: z.array(z.string()).optional(),
|
|
18
|
+
requireMention: z.boolean().optional()
|
|
19
|
+
});
|
|
20
|
+
WecomAccountSchema.extend({
|
|
21
|
+
defaultAccount: z.string().optional(),
|
|
22
|
+
accounts: z.record(WecomAccountSchema).optional()
|
|
23
|
+
});
|
|
24
|
+
var WecomConfigJsonSchema = {
|
|
25
|
+
schema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
name: { type: "string" },
|
|
30
|
+
enabled: { type: "boolean" },
|
|
31
|
+
webhookPath: { type: "string" },
|
|
32
|
+
token: { type: "string" },
|
|
33
|
+
encodingAESKey: { type: "string" },
|
|
34
|
+
receiveId: { type: "string" },
|
|
35
|
+
welcomeText: { type: "string" },
|
|
36
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist", "disabled"] },
|
|
37
|
+
allowFrom: { type: "array", items: { type: "string" } },
|
|
38
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
39
|
+
groupAllowFrom: { type: "array", items: { type: "string" } },
|
|
40
|
+
requireMention: { type: "boolean" },
|
|
41
|
+
defaultAccount: { type: "string" },
|
|
42
|
+
accounts: {
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
name: { type: "string" },
|
|
49
|
+
enabled: { type: "boolean" },
|
|
50
|
+
webhookPath: { type: "string" },
|
|
51
|
+
token: { type: "string" },
|
|
52
|
+
encodingAESKey: { type: "string" },
|
|
53
|
+
receiveId: { type: "string" },
|
|
54
|
+
welcomeText: { type: "string" },
|
|
55
|
+
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist", "disabled"] },
|
|
56
|
+
allowFrom: { type: "array", items: { type: "string" } },
|
|
57
|
+
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
58
|
+
groupAllowFrom: { type: "array", items: { type: "string" } },
|
|
59
|
+
requireMention: { type: "boolean" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function normalizeAccountId(raw) {
|
|
67
|
+
const trimmed = String(raw ?? "").trim();
|
|
68
|
+
return trimmed || DEFAULT_ACCOUNT_ID;
|
|
69
|
+
}
|
|
70
|
+
function listConfiguredAccountIds(cfg) {
|
|
71
|
+
const accounts = cfg.channels?.wecom?.accounts;
|
|
72
|
+
if (!accounts || typeof accounts !== "object") return [];
|
|
73
|
+
return Object.keys(accounts).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
function listWecomAccountIds(cfg) {
|
|
76
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
77
|
+
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
|
78
|
+
return ids.sort((a, b) => a.localeCompare(b));
|
|
79
|
+
}
|
|
80
|
+
function resolveDefaultWecomAccountId(cfg) {
|
|
81
|
+
const wecomConfig = cfg.channels?.wecom;
|
|
82
|
+
if (wecomConfig?.defaultAccount?.trim()) return wecomConfig.defaultAccount.trim();
|
|
83
|
+
const ids = listWecomAccountIds(cfg);
|
|
84
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
|
85
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
86
|
+
}
|
|
87
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
88
|
+
const accounts = cfg.channels?.wecom?.accounts;
|
|
89
|
+
if (!accounts || typeof accounts !== "object") return void 0;
|
|
90
|
+
return accounts[accountId];
|
|
91
|
+
}
|
|
92
|
+
function mergeWecomAccountConfig(cfg, accountId) {
|
|
93
|
+
const base = cfg.channels?.wecom ?? {};
|
|
94
|
+
const { accounts: _ignored, defaultAccount: _ignored2, ...baseConfig } = base;
|
|
95
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
96
|
+
return { ...baseConfig, ...account };
|
|
97
|
+
}
|
|
98
|
+
function resolveWecomAccount(params) {
|
|
99
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
100
|
+
const baseEnabled = params.cfg.channels?.wecom?.enabled !== false;
|
|
101
|
+
const merged = mergeWecomAccountConfig(params.cfg, accountId);
|
|
102
|
+
const enabled = baseEnabled && merged.enabled !== false;
|
|
103
|
+
const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
|
|
104
|
+
const token = merged.token?.trim() || (isDefaultAccount ? process.env.WECOM_TOKEN?.trim() : void 0) || void 0;
|
|
105
|
+
const encodingAESKey = merged.encodingAESKey?.trim() || (isDefaultAccount ? process.env.WECOM_ENCODING_AES_KEY?.trim() : void 0) || void 0;
|
|
106
|
+
const receiveId = merged.receiveId?.trim() ?? "";
|
|
107
|
+
const configured = Boolean(token && encodingAESKey);
|
|
108
|
+
return {
|
|
109
|
+
accountId,
|
|
110
|
+
name: merged.name?.trim() || void 0,
|
|
111
|
+
enabled,
|
|
112
|
+
configured,
|
|
113
|
+
token,
|
|
114
|
+
encodingAESKey,
|
|
115
|
+
receiveId,
|
|
116
|
+
config: merged
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveGroupPolicy(config) {
|
|
120
|
+
return config.groupPolicy ?? "open";
|
|
121
|
+
}
|
|
122
|
+
function resolveRequireMention(config) {
|
|
123
|
+
if (typeof config.requireMention === "boolean") return config.requireMention;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function resolveAllowFrom(config) {
|
|
127
|
+
return config.allowFrom ?? [];
|
|
128
|
+
}
|
|
129
|
+
function resolveGroupAllowFrom(config) {
|
|
130
|
+
return config.groupAllowFrom ?? [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ../../node_modules/.pnpm/@openclaw-china+shared@0.1.5/node_modules/@openclaw-china/shared/src/logger/logger.ts
|
|
134
|
+
function createLogger(prefix, opts) {
|
|
135
|
+
const logFn = opts?.log ?? console.log;
|
|
136
|
+
const errorFn = opts?.error ?? console.error;
|
|
137
|
+
return {
|
|
138
|
+
debug: (msg) => logFn(`[${prefix}] [DEBUG] ${msg}`),
|
|
139
|
+
info: (msg) => logFn(`[${prefix}] ${msg}`),
|
|
140
|
+
warn: (msg) => logFn(`[${prefix}] [WARN] ${msg}`),
|
|
141
|
+
error: (msg) => errorFn(`[${prefix}] [ERROR] ${msg}`)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ../../node_modules/.pnpm/@openclaw-china+shared@0.1.5/node_modules/@openclaw-china/shared/src/policy/dm-policy.ts
|
|
146
|
+
function checkDmPolicy(params) {
|
|
147
|
+
const { dmPolicy, senderId, allowFrom = [] } = params;
|
|
148
|
+
switch (dmPolicy) {
|
|
149
|
+
case "open":
|
|
150
|
+
return { allowed: true };
|
|
151
|
+
case "pairing":
|
|
152
|
+
return { allowed: true };
|
|
153
|
+
case "allowlist":
|
|
154
|
+
if (allowFrom.includes(senderId)) {
|
|
155
|
+
return { allowed: true };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
allowed: false,
|
|
159
|
+
reason: `sender ${senderId} not in DM allowlist`
|
|
160
|
+
};
|
|
161
|
+
default:
|
|
162
|
+
return { allowed: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ../../node_modules/.pnpm/@openclaw-china+shared@0.1.5/node_modules/@openclaw-china/shared/src/policy/group-policy.ts
|
|
167
|
+
function checkGroupPolicy(params) {
|
|
168
|
+
const { groupPolicy, conversationId, groupAllowFrom = []} = params;
|
|
169
|
+
switch (groupPolicy) {
|
|
170
|
+
case "disabled":
|
|
171
|
+
return {
|
|
172
|
+
allowed: false,
|
|
173
|
+
reason: "group messages disabled"
|
|
174
|
+
};
|
|
175
|
+
case "allowlist":
|
|
176
|
+
if (!groupAllowFrom.includes(conversationId)) {
|
|
177
|
+
return {
|
|
178
|
+
allowed: false,
|
|
179
|
+
reason: `group ${conversationId} not in allowlist`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
return { allowed: true };
|
|
185
|
+
}
|
|
186
|
+
function decodeEncodingAESKey(encodingAESKey) {
|
|
187
|
+
const trimmed = encodingAESKey.trim();
|
|
188
|
+
if (!trimmed) throw new Error("encodingAESKey missing");
|
|
189
|
+
const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
|
|
190
|
+
const key = Buffer.from(withPadding, "base64");
|
|
191
|
+
if (key.length !== 32) {
|
|
192
|
+
throw new Error(`invalid encodingAESKey (expected 32 bytes after base64 decode, got ${key.length})`);
|
|
193
|
+
}
|
|
194
|
+
return key;
|
|
195
|
+
}
|
|
196
|
+
var WECOM_PKCS7_BLOCK_SIZE = 32;
|
|
197
|
+
function pkcs7Pad(buf, blockSize) {
|
|
198
|
+
const mod = buf.length % blockSize;
|
|
199
|
+
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
200
|
+
return Buffer.concat([buf, Buffer.alloc(pad, pad)]);
|
|
201
|
+
}
|
|
202
|
+
function pkcs7Unpad(buf, blockSize) {
|
|
203
|
+
if (buf.length === 0) throw new Error("invalid pkcs7 payload");
|
|
204
|
+
const pad = buf[buf.length - 1];
|
|
205
|
+
if (!pad || pad < 1 || pad > blockSize || pad > buf.length) {
|
|
206
|
+
throw new Error("invalid pkcs7 padding");
|
|
207
|
+
}
|
|
208
|
+
for (let i = 1; i <= pad; i += 1) {
|
|
209
|
+
if (buf[buf.length - i] !== pad) {
|
|
210
|
+
throw new Error("invalid pkcs7 padding");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return buf.subarray(0, buf.length - pad);
|
|
214
|
+
}
|
|
215
|
+
function sha1Hex(input) {
|
|
216
|
+
return crypto.createHash("sha1").update(input).digest("hex");
|
|
217
|
+
}
|
|
218
|
+
function computeWecomMsgSignature(params) {
|
|
219
|
+
const parts = [params.token, params.timestamp, params.nonce, params.encrypt].map((value) => String(value ?? "")).sort();
|
|
220
|
+
return sha1Hex(parts.join(""));
|
|
221
|
+
}
|
|
222
|
+
function verifyWecomSignature(params) {
|
|
223
|
+
const expected = computeWecomMsgSignature({
|
|
224
|
+
token: params.token,
|
|
225
|
+
timestamp: params.timestamp,
|
|
226
|
+
nonce: params.nonce,
|
|
227
|
+
encrypt: params.encrypt
|
|
228
|
+
});
|
|
229
|
+
return expected === params.signature;
|
|
230
|
+
}
|
|
231
|
+
function decryptWecomEncrypted(params) {
|
|
232
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
233
|
+
const iv = aesKey.subarray(0, 16);
|
|
234
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
235
|
+
decipher.setAutoPadding(false);
|
|
236
|
+
const decryptedPadded = Buffer.concat([
|
|
237
|
+
decipher.update(Buffer.from(params.encrypt, "base64")),
|
|
238
|
+
decipher.final()
|
|
239
|
+
]);
|
|
240
|
+
const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
|
|
241
|
+
if (decrypted.length < 20) {
|
|
242
|
+
throw new Error(`invalid decrypted payload (expected at least 20 bytes, got ${decrypted.length})`);
|
|
243
|
+
}
|
|
244
|
+
const msgLen = decrypted.readUInt32BE(16);
|
|
245
|
+
const msgStart = 20;
|
|
246
|
+
const msgEnd = msgStart + msgLen;
|
|
247
|
+
if (msgEnd > decrypted.length) {
|
|
248
|
+
throw new Error(`invalid decrypted msg length (msgEnd=${msgEnd}, payloadLength=${decrypted.length})`);
|
|
249
|
+
}
|
|
250
|
+
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
251
|
+
const receiveId = params.receiveId ?? "";
|
|
252
|
+
if (receiveId) {
|
|
253
|
+
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
254
|
+
if (trailing !== receiveId) {
|
|
255
|
+
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return msg;
|
|
259
|
+
}
|
|
260
|
+
function encryptWecomPlaintext(params) {
|
|
261
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
262
|
+
const iv = aesKey.subarray(0, 16);
|
|
263
|
+
const random16 = crypto.randomBytes(16);
|
|
264
|
+
const msg = Buffer.from(params.plaintext ?? "", "utf8");
|
|
265
|
+
const msgLen = Buffer.alloc(4);
|
|
266
|
+
msgLen.writeUInt32BE(msg.length, 0);
|
|
267
|
+
const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
|
|
268
|
+
const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
|
|
269
|
+
const padded = pkcs7Pad(raw, WECOM_PKCS7_BLOCK_SIZE);
|
|
270
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
|
|
271
|
+
cipher.setAutoPadding(false);
|
|
272
|
+
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
273
|
+
return encrypted.toString("base64");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/bot.ts
|
|
277
|
+
function resolveSenderId(msg) {
|
|
278
|
+
const userid = msg.from?.userid?.trim();
|
|
279
|
+
return userid || "unknown";
|
|
280
|
+
}
|
|
281
|
+
function resolveChatType(msg) {
|
|
282
|
+
return msg.chattype === "group" ? "group" : "direct";
|
|
283
|
+
}
|
|
284
|
+
function resolveChatId(msg, senderId, chatType) {
|
|
285
|
+
if (chatType === "group") {
|
|
286
|
+
return msg.chatid?.trim() || "unknown";
|
|
287
|
+
}
|
|
288
|
+
return senderId;
|
|
289
|
+
}
|
|
290
|
+
function buildInboundBody(msg) {
|
|
291
|
+
const msgtype = String(msg.msgtype ?? "").toLowerCase();
|
|
292
|
+
if (msgtype === "text") {
|
|
293
|
+
const content = msg.text?.content;
|
|
294
|
+
return typeof content === "string" ? content : "";
|
|
295
|
+
}
|
|
296
|
+
if (msgtype === "voice") {
|
|
297
|
+
const content = msg.voice?.content;
|
|
298
|
+
return typeof content === "string" ? content : "[voice]";
|
|
299
|
+
}
|
|
300
|
+
if (msgtype === "mixed") {
|
|
301
|
+
const items = msg.mixed?.msg_item;
|
|
302
|
+
if (Array.isArray(items)) {
|
|
303
|
+
return items.map((item) => {
|
|
304
|
+
if (!item || typeof item !== "object") return "";
|
|
305
|
+
const typed = item;
|
|
306
|
+
const t = String(typed.msgtype ?? "").toLowerCase();
|
|
307
|
+
if (t === "text") return String(typed.text?.content ?? "");
|
|
308
|
+
if (t === "image") return `[image] ${String(typed.image?.url ?? "").trim()}`.trim();
|
|
309
|
+
return t ? `[${t}]` : "";
|
|
310
|
+
}).filter((part) => Boolean(part && part.trim())).join("\n");
|
|
311
|
+
}
|
|
312
|
+
return "[mixed]";
|
|
313
|
+
}
|
|
314
|
+
if (msgtype === "image") {
|
|
315
|
+
const url = String(msg.image?.url ?? "").trim();
|
|
316
|
+
return url ? `[image] ${url}` : "[image]";
|
|
317
|
+
}
|
|
318
|
+
if (msgtype === "file") {
|
|
319
|
+
const url = String(msg.file?.url ?? "").trim();
|
|
320
|
+
return url ? `[file] ${url}` : "[file]";
|
|
321
|
+
}
|
|
322
|
+
if (msgtype === "event") {
|
|
323
|
+
const eventtype = String(msg.event?.eventtype ?? "").trim();
|
|
324
|
+
return eventtype ? `[event] ${eventtype}` : "[event]";
|
|
325
|
+
}
|
|
326
|
+
if (msgtype === "stream") {
|
|
327
|
+
const id = String(msg.stream?.id ?? "").trim();
|
|
328
|
+
return id ? `[stream_refresh] ${id}` : "[stream_refresh]";
|
|
329
|
+
}
|
|
330
|
+
return msgtype ? `[${msgtype}]` : "";
|
|
331
|
+
}
|
|
332
|
+
async function dispatchWecomMessage(params) {
|
|
333
|
+
const { cfg, account, msg, core, hooks } = params;
|
|
334
|
+
const safeCfg = cfg ?? {};
|
|
335
|
+
const logger = createLogger("wecom", { log: params.log, error: params.error });
|
|
336
|
+
const chatType = resolveChatType(msg);
|
|
337
|
+
const senderId = resolveSenderId(msg);
|
|
338
|
+
const chatId = resolveChatId(msg, senderId, chatType);
|
|
339
|
+
const accountConfig = account?.config ?? {};
|
|
340
|
+
if (chatType === "group") {
|
|
341
|
+
const groupPolicy = resolveGroupPolicy(accountConfig);
|
|
342
|
+
const groupAllowFrom = resolveGroupAllowFrom(accountConfig);
|
|
343
|
+
resolveRequireMention(accountConfig);
|
|
344
|
+
const policyResult = checkGroupPolicy({
|
|
345
|
+
groupPolicy,
|
|
346
|
+
conversationId: chatId,
|
|
347
|
+
groupAllowFrom});
|
|
348
|
+
if (!policyResult.allowed) {
|
|
349
|
+
logger.debug(`policy rejected: ${policyResult.reason}`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
const dmPolicyRaw = accountConfig.dmPolicy ?? "pairing";
|
|
354
|
+
if (dmPolicyRaw === "disabled") {
|
|
355
|
+
logger.debug("dmPolicy=disabled, skipping dispatch");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const allowFrom = resolveAllowFrom(accountConfig);
|
|
359
|
+
const policyResult = checkDmPolicy({
|
|
360
|
+
dmPolicy: dmPolicyRaw,
|
|
361
|
+
senderId,
|
|
362
|
+
allowFrom
|
|
363
|
+
});
|
|
364
|
+
if (!policyResult.allowed) {
|
|
365
|
+
logger.debug(`policy rejected: ${policyResult.reason}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const channel = core.channel;
|
|
370
|
+
if (!channel?.routing?.resolveAgentRoute || !channel.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
371
|
+
logger.debug("core routing or buffered dispatcher missing, skipping dispatch");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const route = channel.routing.resolveAgentRoute({
|
|
375
|
+
cfg: safeCfg,
|
|
376
|
+
channel: "wecom",
|
|
377
|
+
peer: { kind: chatType === "group" ? "group" : "dm", id: chatId }
|
|
378
|
+
});
|
|
379
|
+
const rawBody = buildInboundBody(msg);
|
|
380
|
+
const fromLabel = chatType === "group" ? `group:${chatId}` : `user:${senderId}`;
|
|
381
|
+
const storePath = channel.session?.resolveStorePath?.(safeCfg.session?.store, {
|
|
382
|
+
agentId: route.agentId
|
|
383
|
+
});
|
|
384
|
+
const previousTimestamp = channel.session?.readSessionUpdatedAt ? channel.session.readSessionUpdatedAt({
|
|
385
|
+
storePath,
|
|
386
|
+
sessionKey: route.sessionKey
|
|
387
|
+
}) ?? void 0 : void 0;
|
|
388
|
+
const envelopeOptions = channel.reply?.resolveEnvelopeFormatOptions ? channel.reply.resolveEnvelopeFormatOptions(safeCfg) : void 0;
|
|
389
|
+
const body = channel.reply?.formatAgentEnvelope ? channel.reply.formatAgentEnvelope({
|
|
390
|
+
channel: "WeCom",
|
|
391
|
+
from: fromLabel,
|
|
392
|
+
previousTimestamp,
|
|
393
|
+
envelope: envelopeOptions,
|
|
394
|
+
body: rawBody
|
|
395
|
+
}) : rawBody;
|
|
396
|
+
const ctxPayload = channel.reply?.finalizeInboundContext ? channel.reply.finalizeInboundContext({
|
|
397
|
+
Body: body,
|
|
398
|
+
RawBody: rawBody,
|
|
399
|
+
CommandBody: rawBody,
|
|
400
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
401
|
+
To: `wecom:${chatId}`,
|
|
402
|
+
SessionKey: route.sessionKey,
|
|
403
|
+
AccountId: route.accountId,
|
|
404
|
+
ChatType: chatType,
|
|
405
|
+
ConversationLabel: fromLabel,
|
|
406
|
+
SenderName: senderId,
|
|
407
|
+
SenderId: senderId,
|
|
408
|
+
Provider: "wecom",
|
|
409
|
+
Surface: "wecom",
|
|
410
|
+
MessageSid: msg.msgid,
|
|
411
|
+
OriginatingChannel: "wecom",
|
|
412
|
+
OriginatingTo: `wecom:${chatId}`
|
|
413
|
+
}) : {
|
|
414
|
+
Body: body,
|
|
415
|
+
RawBody: rawBody,
|
|
416
|
+
CommandBody: rawBody,
|
|
417
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
418
|
+
To: `wecom:${chatId}`,
|
|
419
|
+
SessionKey: route.sessionKey,
|
|
420
|
+
AccountId: route.accountId,
|
|
421
|
+
ChatType: chatType,
|
|
422
|
+
ConversationLabel: fromLabel,
|
|
423
|
+
SenderName: senderId,
|
|
424
|
+
SenderId: senderId,
|
|
425
|
+
Provider: "wecom",
|
|
426
|
+
Surface: "wecom",
|
|
427
|
+
MessageSid: msg.msgid,
|
|
428
|
+
OriginatingChannel: "wecom",
|
|
429
|
+
OriginatingTo: `wecom:${chatId}`
|
|
430
|
+
};
|
|
431
|
+
if (channel.session?.recordInboundSession && storePath) {
|
|
432
|
+
await channel.session.recordInboundSession({
|
|
433
|
+
storePath,
|
|
434
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
435
|
+
ctx: ctxPayload,
|
|
436
|
+
onRecordError: (err) => {
|
|
437
|
+
logger.error(`wecom: failed updating session meta: ${String(err)}`);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const tableMode = channel.text?.resolveMarkdownTableMode ? channel.text.resolveMarkdownTableMode({ cfg: safeCfg, channel: "wecom", accountId: account.accountId }) : void 0;
|
|
442
|
+
await channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
443
|
+
ctx: ctxPayload,
|
|
444
|
+
cfg: safeCfg,
|
|
445
|
+
dispatcherOptions: {
|
|
446
|
+
deliver: async (payload) => {
|
|
447
|
+
const rawText = payload.text ?? "";
|
|
448
|
+
if (!rawText.trim()) return;
|
|
449
|
+
const converted = channel.text?.convertMarkdownTables && tableMode ? channel.text.convertMarkdownTables(rawText, tableMode) : rawText;
|
|
450
|
+
hooks.onChunk(converted);
|
|
451
|
+
},
|
|
452
|
+
onError: (err, info) => {
|
|
453
|
+
hooks.onError?.(err);
|
|
454
|
+
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/runtime.ts
|
|
461
|
+
var runtime = null;
|
|
462
|
+
function setWecomRuntime(next) {
|
|
463
|
+
runtime = next;
|
|
464
|
+
}
|
|
465
|
+
function getWecomRuntime() {
|
|
466
|
+
if (!runtime) {
|
|
467
|
+
throw new Error("WeCom runtime not initialized. Make sure the plugin is properly registered with Moltbot.");
|
|
468
|
+
}
|
|
469
|
+
return runtime;
|
|
470
|
+
}
|
|
471
|
+
function tryGetWecomRuntime() {
|
|
472
|
+
return runtime;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/monitor.ts
|
|
476
|
+
var webhookTargets = /* @__PURE__ */ new Map();
|
|
477
|
+
var streams = /* @__PURE__ */ new Map();
|
|
478
|
+
var msgidToStreamId = /* @__PURE__ */ new Map();
|
|
479
|
+
var STREAM_TTL_MS = 10 * 60 * 1e3;
|
|
480
|
+
var STREAM_MAX_BYTES = 20480;
|
|
481
|
+
var INITIAL_STREAM_WAIT_MS = 800;
|
|
482
|
+
function normalizeWebhookPath(raw) {
|
|
483
|
+
const trimmed = raw.trim();
|
|
484
|
+
if (!trimmed) return "/";
|
|
485
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
486
|
+
if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
|
|
487
|
+
return withSlash;
|
|
488
|
+
}
|
|
489
|
+
function pruneStreams() {
|
|
490
|
+
const cutoff = Date.now() - STREAM_TTL_MS;
|
|
491
|
+
for (const [id, state] of streams.entries()) {
|
|
492
|
+
if (state.updatedAt < cutoff) {
|
|
493
|
+
streams.delete(id);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const [msgid, id] of msgidToStreamId.entries()) {
|
|
497
|
+
if (!streams.has(id)) {
|
|
498
|
+
msgidToStreamId.delete(msgid);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function truncateUtf8Bytes(text, maxBytes) {
|
|
503
|
+
const buf = Buffer.from(text, "utf8");
|
|
504
|
+
if (buf.length <= maxBytes) return text;
|
|
505
|
+
const slice = buf.subarray(buf.length - maxBytes);
|
|
506
|
+
return slice.toString("utf8");
|
|
507
|
+
}
|
|
508
|
+
function jsonOk(res, body) {
|
|
509
|
+
res.statusCode = 200;
|
|
510
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
511
|
+
res.end(JSON.stringify(body));
|
|
512
|
+
}
|
|
513
|
+
async function readJsonBody(req, maxBytes) {
|
|
514
|
+
const chunks = [];
|
|
515
|
+
let total = 0;
|
|
516
|
+
return await new Promise((resolve) => {
|
|
517
|
+
req.on("data", (chunk) => {
|
|
518
|
+
total += chunk.length;
|
|
519
|
+
if (total > maxBytes) {
|
|
520
|
+
resolve({ ok: false, error: "payload too large" });
|
|
521
|
+
req.destroy();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
chunks.push(chunk);
|
|
525
|
+
});
|
|
526
|
+
req.on("end", () => {
|
|
527
|
+
try {
|
|
528
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
529
|
+
if (!raw.trim()) {
|
|
530
|
+
resolve({ ok: false, error: "empty payload" });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
resolve({ ok: true, value: JSON.parse(raw) });
|
|
534
|
+
} catch (err) {
|
|
535
|
+
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
req.on("error", (err) => {
|
|
539
|
+
resolve({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function buildEncryptedJsonReply(params) {
|
|
544
|
+
const plaintext = JSON.stringify(params.plaintextJson ?? {});
|
|
545
|
+
const encrypt = encryptWecomPlaintext({
|
|
546
|
+
encodingAESKey: params.account.encodingAESKey ?? "",
|
|
547
|
+
receiveId: params.account.receiveId ?? "",
|
|
548
|
+
plaintext
|
|
549
|
+
});
|
|
550
|
+
const msgsignature = computeWecomMsgSignature({
|
|
551
|
+
token: params.account.token ?? "",
|
|
552
|
+
timestamp: params.timestamp,
|
|
553
|
+
nonce: params.nonce,
|
|
554
|
+
encrypt
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
encrypt,
|
|
558
|
+
msgsignature,
|
|
559
|
+
timestamp: params.timestamp,
|
|
560
|
+
nonce: params.nonce
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function resolveQueryParams(req) {
|
|
564
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
565
|
+
return url.searchParams;
|
|
566
|
+
}
|
|
567
|
+
function resolvePath(req) {
|
|
568
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
569
|
+
return normalizeWebhookPath(url.pathname || "/");
|
|
570
|
+
}
|
|
571
|
+
function resolveSignatureParam(params) {
|
|
572
|
+
return params.get("msg_signature") ?? params.get("msgsignature") ?? params.get("signature") ?? "";
|
|
573
|
+
}
|
|
574
|
+
function buildStreamPlaceholderReply(streamId) {
|
|
575
|
+
return {
|
|
576
|
+
msgtype: "stream",
|
|
577
|
+
stream: {
|
|
578
|
+
id: streamId,
|
|
579
|
+
finish: false,
|
|
580
|
+
content: "\u7A0D\u7B49~"
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function buildStreamReplyFromState(state) {
|
|
585
|
+
const content = truncateUtf8Bytes(state.content, STREAM_MAX_BYTES);
|
|
586
|
+
return {
|
|
587
|
+
msgtype: "stream",
|
|
588
|
+
stream: {
|
|
589
|
+
id: state.streamId,
|
|
590
|
+
finish: state.finished,
|
|
591
|
+
content
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function createStreamId() {
|
|
596
|
+
return crypto.randomBytes(16).toString("hex");
|
|
597
|
+
}
|
|
598
|
+
function parseWecomPlainMessage(raw) {
|
|
599
|
+
const parsed = JSON.parse(raw);
|
|
600
|
+
if (!parsed || typeof parsed !== "object") {
|
|
601
|
+
return {};
|
|
602
|
+
}
|
|
603
|
+
return parsed;
|
|
604
|
+
}
|
|
605
|
+
async function waitForStreamContent(streamId, maxWaitMs) {
|
|
606
|
+
const startedAt = Date.now();
|
|
607
|
+
await new Promise((resolve) => {
|
|
608
|
+
const tick = () => {
|
|
609
|
+
const state = streams.get(streamId);
|
|
610
|
+
if (!state) return resolve();
|
|
611
|
+
if (state.error || state.finished || state.content.trim()) return resolve();
|
|
612
|
+
if (Date.now() - startedAt >= maxWaitMs) return resolve();
|
|
613
|
+
setTimeout(tick, 25);
|
|
614
|
+
};
|
|
615
|
+
tick();
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function appendStreamContent(state, nextText) {
|
|
619
|
+
const content = state.content ? `${state.content}
|
|
620
|
+
|
|
621
|
+
${nextText}`.trim() : nextText.trim();
|
|
622
|
+
state.content = truncateUtf8Bytes(content, STREAM_MAX_BYTES);
|
|
623
|
+
state.updatedAt = Date.now();
|
|
624
|
+
}
|
|
625
|
+
function buildLogger(target) {
|
|
626
|
+
return createLogger("wecom", {
|
|
627
|
+
log: target.runtime.log,
|
|
628
|
+
error: target.runtime.error
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function registerWecomWebhookTarget(target) {
|
|
632
|
+
const key = normalizeWebhookPath(target.path);
|
|
633
|
+
const normalizedTarget = { ...target, path: key };
|
|
634
|
+
const existing = webhookTargets.get(key) ?? [];
|
|
635
|
+
const next = [...existing, normalizedTarget];
|
|
636
|
+
webhookTargets.set(key, next);
|
|
637
|
+
return () => {
|
|
638
|
+
const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
|
|
639
|
+
if (updated.length > 0) webhookTargets.set(key, updated);
|
|
640
|
+
else webhookTargets.delete(key);
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function handleWecomWebhookRequest(req, res) {
|
|
644
|
+
pruneStreams();
|
|
645
|
+
const path = resolvePath(req);
|
|
646
|
+
const targets = webhookTargets.get(path);
|
|
647
|
+
if (!targets || targets.length === 0) return false;
|
|
648
|
+
const query = resolveQueryParams(req);
|
|
649
|
+
const timestamp = query.get("timestamp") ?? "";
|
|
650
|
+
const nonce = query.get("nonce") ?? "";
|
|
651
|
+
const signature = resolveSignatureParam(query);
|
|
652
|
+
const primary = targets[0];
|
|
653
|
+
const logger = buildLogger(primary);
|
|
654
|
+
logger.debug(`incoming ${req.method} request on ${path} (timestamp=${timestamp}, nonce=${nonce})`);
|
|
655
|
+
if (req.method === "GET") {
|
|
656
|
+
const echostr = query.get("echostr") ?? "";
|
|
657
|
+
if (!timestamp || !nonce || !signature || !echostr) {
|
|
658
|
+
res.statusCode = 400;
|
|
659
|
+
res.end("missing query params");
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
const target2 = targets.find((candidate) => {
|
|
663
|
+
if (!candidate.account.configured || !candidate.account.token) return false;
|
|
664
|
+
return verifyWecomSignature({
|
|
665
|
+
token: candidate.account.token,
|
|
666
|
+
timestamp,
|
|
667
|
+
nonce,
|
|
668
|
+
encrypt: echostr,
|
|
669
|
+
signature
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
if (!target2 || !target2.account.encodingAESKey) {
|
|
673
|
+
res.statusCode = 401;
|
|
674
|
+
res.end("unauthorized");
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const plain2 = decryptWecomEncrypted({
|
|
679
|
+
encodingAESKey: target2.account.encodingAESKey,
|
|
680
|
+
receiveId: target2.account.receiveId,
|
|
681
|
+
encrypt: echostr
|
|
682
|
+
});
|
|
683
|
+
res.statusCode = 200;
|
|
684
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
685
|
+
res.end(plain2);
|
|
686
|
+
return true;
|
|
687
|
+
} catch (err) {
|
|
688
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
689
|
+
res.statusCode = 400;
|
|
690
|
+
res.end(msg2 || "decrypt failed");
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (req.method !== "POST") {
|
|
695
|
+
res.statusCode = 405;
|
|
696
|
+
res.setHeader("Allow", "GET, POST");
|
|
697
|
+
res.end("Method Not Allowed");
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
if (!timestamp || !nonce || !signature) {
|
|
701
|
+
res.statusCode = 400;
|
|
702
|
+
res.end("missing query params");
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
const body = await readJsonBody(req, 1024 * 1024);
|
|
706
|
+
if (!body.ok) {
|
|
707
|
+
res.statusCode = body.error === "payload too large" ? 413 : 400;
|
|
708
|
+
res.end(body.error ?? "invalid payload");
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
const record = body.value && typeof body.value === "object" ? body.value : null;
|
|
712
|
+
const encrypt = record ? String(record.encrypt ?? record.Encrypt ?? "") : "";
|
|
713
|
+
if (!encrypt) {
|
|
714
|
+
res.statusCode = 400;
|
|
715
|
+
res.end("missing encrypt");
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
const target = targets.find((candidate) => {
|
|
719
|
+
if (!candidate.account.token) return false;
|
|
720
|
+
return verifyWecomSignature({
|
|
721
|
+
token: candidate.account.token,
|
|
722
|
+
timestamp,
|
|
723
|
+
nonce,
|
|
724
|
+
encrypt,
|
|
725
|
+
signature
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
if (!target) {
|
|
729
|
+
res.statusCode = 401;
|
|
730
|
+
res.end("unauthorized");
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
if (!target.account.configured || !target.account.token || !target.account.encodingAESKey) {
|
|
734
|
+
res.statusCode = 500;
|
|
735
|
+
res.end("wecom not configured");
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
let plain;
|
|
739
|
+
try {
|
|
740
|
+
plain = decryptWecomEncrypted({
|
|
741
|
+
encodingAESKey: target.account.encodingAESKey,
|
|
742
|
+
receiveId: target.account.receiveId,
|
|
743
|
+
encrypt
|
|
744
|
+
});
|
|
745
|
+
} catch (err) {
|
|
746
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
747
|
+
res.statusCode = 400;
|
|
748
|
+
res.end(msg2 || "decrypt failed");
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
const msg = parseWecomPlainMessage(plain);
|
|
752
|
+
target.statusSink?.({ lastInboundAt: Date.now() });
|
|
753
|
+
const msgtype = String(msg.msgtype ?? "").toLowerCase();
|
|
754
|
+
const msgid = msg.msgid ? String(msg.msgid) : void 0;
|
|
755
|
+
if (msgtype === "stream") {
|
|
756
|
+
const streamId2 = String(msg.stream?.id ?? "").trim();
|
|
757
|
+
const state2 = streamId2 ? streams.get(streamId2) : void 0;
|
|
758
|
+
const reply = state2 ? buildStreamReplyFromState(state2) : buildStreamReplyFromState({
|
|
759
|
+
streamId: streamId2 || "unknown",
|
|
760
|
+
finished: true,
|
|
761
|
+
content: ""
|
|
762
|
+
});
|
|
763
|
+
jsonOk(
|
|
764
|
+
res,
|
|
765
|
+
buildEncryptedJsonReply({
|
|
766
|
+
account: target.account,
|
|
767
|
+
plaintextJson: reply,
|
|
768
|
+
nonce,
|
|
769
|
+
timestamp
|
|
770
|
+
})
|
|
771
|
+
);
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
if (msgid && msgidToStreamId.has(msgid)) {
|
|
775
|
+
const streamId2 = msgidToStreamId.get(msgid) ?? "";
|
|
776
|
+
const reply = buildStreamPlaceholderReply(streamId2);
|
|
777
|
+
jsonOk(
|
|
778
|
+
res,
|
|
779
|
+
buildEncryptedJsonReply({
|
|
780
|
+
account: target.account,
|
|
781
|
+
plaintextJson: reply,
|
|
782
|
+
nonce,
|
|
783
|
+
timestamp
|
|
784
|
+
})
|
|
785
|
+
);
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
if (msgtype === "event") {
|
|
789
|
+
const eventtype = String(msg.event?.eventtype ?? "").toLowerCase();
|
|
790
|
+
if (eventtype === "enter_chat") {
|
|
791
|
+
const welcome = target.account.config.welcomeText?.trim();
|
|
792
|
+
const reply = welcome ? { msgtype: "text", text: { content: welcome } } : {};
|
|
793
|
+
jsonOk(
|
|
794
|
+
res,
|
|
795
|
+
buildEncryptedJsonReply({
|
|
796
|
+
account: target.account,
|
|
797
|
+
plaintextJson: reply,
|
|
798
|
+
nonce,
|
|
799
|
+
timestamp
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
jsonOk(
|
|
805
|
+
res,
|
|
806
|
+
buildEncryptedJsonReply({
|
|
807
|
+
account: target.account,
|
|
808
|
+
plaintextJson: {},
|
|
809
|
+
nonce,
|
|
810
|
+
timestamp
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
const streamId = createStreamId();
|
|
816
|
+
if (msgid) msgidToStreamId.set(msgid, streamId);
|
|
817
|
+
streams.set(streamId, {
|
|
818
|
+
streamId,
|
|
819
|
+
msgid,
|
|
820
|
+
createdAt: Date.now(),
|
|
821
|
+
updatedAt: Date.now(),
|
|
822
|
+
started: false,
|
|
823
|
+
finished: false,
|
|
824
|
+
content: ""
|
|
825
|
+
});
|
|
826
|
+
const core = tryGetWecomRuntime();
|
|
827
|
+
if (core) {
|
|
828
|
+
const state2 = streams.get(streamId);
|
|
829
|
+
if (state2) state2.started = true;
|
|
830
|
+
const hooks = {
|
|
831
|
+
onChunk: (text) => {
|
|
832
|
+
const current = streams.get(streamId);
|
|
833
|
+
if (!current) return;
|
|
834
|
+
appendStreamContent(current, text);
|
|
835
|
+
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
836
|
+
},
|
|
837
|
+
onError: (err) => {
|
|
838
|
+
const current = streams.get(streamId);
|
|
839
|
+
if (current) {
|
|
840
|
+
current.error = err instanceof Error ? err.message : String(err);
|
|
841
|
+
current.content = current.content || `Error: ${current.error}`;
|
|
842
|
+
current.finished = true;
|
|
843
|
+
current.updatedAt = Date.now();
|
|
844
|
+
}
|
|
845
|
+
logger.error(`wecom agent failed: ${String(err)}`);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
dispatchWecomMessage({
|
|
849
|
+
cfg: target.config,
|
|
850
|
+
account: target.account,
|
|
851
|
+
msg,
|
|
852
|
+
core,
|
|
853
|
+
hooks,
|
|
854
|
+
log: target.runtime.log,
|
|
855
|
+
error: target.runtime.error
|
|
856
|
+
}).then(() => {
|
|
857
|
+
const current = streams.get(streamId);
|
|
858
|
+
if (current) {
|
|
859
|
+
current.finished = true;
|
|
860
|
+
current.updatedAt = Date.now();
|
|
861
|
+
}
|
|
862
|
+
}).catch((err) => {
|
|
863
|
+
const current = streams.get(streamId);
|
|
864
|
+
if (current) {
|
|
865
|
+
current.error = err instanceof Error ? err.message : String(err);
|
|
866
|
+
current.content = current.content || `Error: ${current.error}`;
|
|
867
|
+
current.finished = true;
|
|
868
|
+
current.updatedAt = Date.now();
|
|
869
|
+
}
|
|
870
|
+
logger.error(`wecom agent failed: ${String(err)}`);
|
|
871
|
+
});
|
|
872
|
+
} else {
|
|
873
|
+
const state2 = streams.get(streamId);
|
|
874
|
+
if (state2) {
|
|
875
|
+
state2.finished = true;
|
|
876
|
+
state2.updatedAt = Date.now();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
await waitForStreamContent(streamId, INITIAL_STREAM_WAIT_MS);
|
|
880
|
+
const state = streams.get(streamId);
|
|
881
|
+
const initialReply = state && (state.content.trim() || state.error) ? buildStreamReplyFromState(state) : buildStreamPlaceholderReply(streamId);
|
|
882
|
+
jsonOk(
|
|
883
|
+
res,
|
|
884
|
+
buildEncryptedJsonReply({
|
|
885
|
+
account: target.account,
|
|
886
|
+
plaintextJson: initialReply,
|
|
887
|
+
nonce,
|
|
888
|
+
timestamp
|
|
889
|
+
})
|
|
890
|
+
);
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/channel.ts
|
|
895
|
+
var meta = {
|
|
896
|
+
id: "wecom",
|
|
897
|
+
label: "WeCom",
|
|
898
|
+
selectionLabel: "WeCom (\u4F01\u4E1A\u5FAE\u4FE1)",
|
|
899
|
+
docsPath: "/channels/wecom",
|
|
900
|
+
docsLabel: "wecom",
|
|
901
|
+
blurb: "\u4F01\u4E1A\u5FAE\u4FE1\u667A\u80FD\u673A\u5668\u4EBA\u56DE\u8C03",
|
|
902
|
+
aliases: ["wechatwork", "wework", "qywx", "\u4F01\u5FAE", "\u4F01\u4E1A\u5FAE\u4FE1"],
|
|
903
|
+
order: 85
|
|
904
|
+
};
|
|
905
|
+
var unregisterHooks = /* @__PURE__ */ new Map();
|
|
906
|
+
var wecomPlugin = {
|
|
907
|
+
id: "wecom",
|
|
908
|
+
meta: {
|
|
909
|
+
...meta
|
|
910
|
+
},
|
|
911
|
+
capabilities: {
|
|
912
|
+
chatTypes: ["direct", "group"],
|
|
913
|
+
media: false,
|
|
914
|
+
reactions: false,
|
|
915
|
+
threads: false,
|
|
916
|
+
edit: false,
|
|
917
|
+
reply: true,
|
|
918
|
+
polls: false
|
|
919
|
+
},
|
|
920
|
+
configSchema: WecomConfigJsonSchema,
|
|
921
|
+
reload: { configPrefixes: ["channels.wecom"] },
|
|
922
|
+
config: {
|
|
923
|
+
listAccountIds: (cfg) => listWecomAccountIds(cfg),
|
|
924
|
+
resolveAccount: (cfg, accountId) => resolveWecomAccount({ cfg, accountId }),
|
|
925
|
+
defaultAccountId: (cfg) => resolveDefaultWecomAccountId(cfg),
|
|
926
|
+
setAccountEnabled: (params) => {
|
|
927
|
+
const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
928
|
+
const useAccount = Boolean(params.cfg.channels?.wecom?.accounts?.[accountId]);
|
|
929
|
+
if (!useAccount) {
|
|
930
|
+
return {
|
|
931
|
+
...params.cfg,
|
|
932
|
+
channels: {
|
|
933
|
+
...params.cfg.channels,
|
|
934
|
+
wecom: {
|
|
935
|
+
...params.cfg.channels?.wecom ?? {},
|
|
936
|
+
enabled: params.enabled
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
...params.cfg,
|
|
943
|
+
channels: {
|
|
944
|
+
...params.cfg.channels,
|
|
945
|
+
wecom: {
|
|
946
|
+
...params.cfg.channels?.wecom ?? {},
|
|
947
|
+
accounts: {
|
|
948
|
+
...params.cfg.channels?.wecom?.accounts ?? {},
|
|
949
|
+
[accountId]: {
|
|
950
|
+
...params.cfg.channels?.wecom?.accounts?.[accountId] ?? {},
|
|
951
|
+
enabled: params.enabled
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
},
|
|
958
|
+
deleteAccount: (params) => {
|
|
959
|
+
const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
960
|
+
const next = { ...params.cfg };
|
|
961
|
+
const current = next.channels?.wecom;
|
|
962
|
+
if (!current) return next;
|
|
963
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
964
|
+
const { accounts: _ignored, defaultAccount: _ignored2, ...rest } = current;
|
|
965
|
+
next.channels = {
|
|
966
|
+
...next.channels,
|
|
967
|
+
wecom: { ...rest, enabled: false }
|
|
968
|
+
};
|
|
969
|
+
return next;
|
|
970
|
+
}
|
|
971
|
+
const accounts = { ...current.accounts ?? {} };
|
|
972
|
+
delete accounts[accountId];
|
|
973
|
+
next.channels = {
|
|
974
|
+
...next.channels,
|
|
975
|
+
wecom: {
|
|
976
|
+
...current,
|
|
977
|
+
accounts: Object.keys(accounts).length > 0 ? accounts : void 0
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
return next;
|
|
981
|
+
},
|
|
982
|
+
isConfigured: (account) => account.configured,
|
|
983
|
+
describeAccount: (account) => ({
|
|
984
|
+
accountId: account.accountId,
|
|
985
|
+
name: account.name,
|
|
986
|
+
enabled: account.enabled,
|
|
987
|
+
configured: account.configured,
|
|
988
|
+
webhookPath: account.config.webhookPath ?? "/wecom"
|
|
989
|
+
}),
|
|
990
|
+
resolveAllowFrom: (params) => {
|
|
991
|
+
const account = resolveWecomAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
992
|
+
return resolveAllowFrom(account.config);
|
|
993
|
+
},
|
|
994
|
+
formatAllowFrom: (params) => params.allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry.toLowerCase())
|
|
995
|
+
},
|
|
996
|
+
groups: {
|
|
997
|
+
resolveRequireMention: (params) => {
|
|
998
|
+
const account = params.account ?? resolveWecomAccount({ cfg: params.cfg ?? {}, accountId: params.accountId });
|
|
999
|
+
return resolveRequireMention(account.config);
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
outbound: {
|
|
1003
|
+
deliveryMode: "direct",
|
|
1004
|
+
sendText: async () => {
|
|
1005
|
+
return {
|
|
1006
|
+
channel: "wecom",
|
|
1007
|
+
ok: false,
|
|
1008
|
+
messageId: "",
|
|
1009
|
+
error: new Error("WeCom intelligent bot only supports replying within callbacks (no standalone sendText).")
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
gateway: {
|
|
1014
|
+
startAccount: async (ctx) => {
|
|
1015
|
+
ctx.setStatus?.({ accountId: ctx.accountId });
|
|
1016
|
+
if (ctx.runtime) {
|
|
1017
|
+
const candidate = ctx.runtime;
|
|
1018
|
+
if (candidate.channel?.routing?.resolveAgentRoute && candidate.channel?.reply?.dispatchReplyFromConfig) {
|
|
1019
|
+
setWecomRuntime(ctx.runtime);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const account = resolveWecomAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
1023
|
+
if (!account.configured) {
|
|
1024
|
+
ctx.log?.info(`[wecom] account ${ctx.accountId} not configured; webhook not registered`);
|
|
1025
|
+
ctx.setStatus?.({ accountId: ctx.accountId, running: false, configured: false });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const path = (account.config.webhookPath ?? "/wecom").trim();
|
|
1029
|
+
const unregister = registerWecomWebhookTarget({
|
|
1030
|
+
account,
|
|
1031
|
+
config: ctx.cfg ?? {},
|
|
1032
|
+
runtime: {
|
|
1033
|
+
log: ctx.log?.info ?? console.log,
|
|
1034
|
+
error: ctx.log?.error ?? console.error
|
|
1035
|
+
},
|
|
1036
|
+
path,
|
|
1037
|
+
statusSink: (patch) => ctx.setStatus?.({ accountId: ctx.accountId, ...patch })
|
|
1038
|
+
});
|
|
1039
|
+
const existing = unregisterHooks.get(ctx.accountId);
|
|
1040
|
+
if (existing) existing();
|
|
1041
|
+
unregisterHooks.set(ctx.accountId, unregister);
|
|
1042
|
+
ctx.log?.info(`[wecom] webhook registered at ${path} for account ${ctx.accountId}`);
|
|
1043
|
+
ctx.setStatus?.({
|
|
1044
|
+
accountId: ctx.accountId,
|
|
1045
|
+
running: true,
|
|
1046
|
+
configured: true,
|
|
1047
|
+
webhookPath: path,
|
|
1048
|
+
lastStartAt: Date.now()
|
|
1049
|
+
});
|
|
1050
|
+
},
|
|
1051
|
+
stopAccount: async (ctx) => {
|
|
1052
|
+
const unregister = unregisterHooks.get(ctx.accountId);
|
|
1053
|
+
if (unregister) {
|
|
1054
|
+
unregister();
|
|
1055
|
+
unregisterHooks.delete(ctx.accountId);
|
|
1056
|
+
}
|
|
1057
|
+
ctx.setStatus?.({ accountId: ctx.accountId, running: false, lastStopAt: Date.now() });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// index.ts
|
|
1063
|
+
var plugin = {
|
|
1064
|
+
id: "wecom",
|
|
1065
|
+
name: "WeCom",
|
|
1066
|
+
description: "\u4F01\u4E1A\u5FAE\u4FE1\u667A\u80FD\u673A\u5668\u4EBA\u56DE\u8C03\u63D2\u4EF6",
|
|
1067
|
+
configSchema: {
|
|
1068
|
+
type: "object",
|
|
1069
|
+
additionalProperties: false,
|
|
1070
|
+
properties: {}
|
|
1071
|
+
},
|
|
1072
|
+
register(api) {
|
|
1073
|
+
if (api.runtime) {
|
|
1074
|
+
setWecomRuntime(api.runtime);
|
|
1075
|
+
}
|
|
1076
|
+
api.registerChannel({ plugin: wecomPlugin });
|
|
1077
|
+
if (api.registerHttpHandler) {
|
|
1078
|
+
api.registerHttpHandler(handleWecomWebhookRequest);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
var index_default = plugin;
|
|
1083
|
+
|
|
1084
|
+
export { DEFAULT_ACCOUNT_ID, index_default as default, getWecomRuntime, setWecomRuntime, wecomPlugin };
|
|
1085
|
+
//# sourceMappingURL=index.js.map
|
|
1086
|
+
//# sourceMappingURL=index.js.map
|