@openclaw-china/wecom 0.1.13 → 0.1.15
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/dist/index.js +418 -106
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/clawdbot.plugin.json +0 -52
- package/moltbot.plugin.json +0 -52
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fsPromises from 'fs/promises';
|
|
2
4
|
|
|
3
5
|
var __defProp = Object.defineProperty;
|
|
4
6
|
var __export = (target, all) => {
|
|
@@ -484,8 +486,8 @@ function getErrorMap() {
|
|
|
484
486
|
|
|
485
487
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
486
488
|
var makeIssue = (params) => {
|
|
487
|
-
const { data, path, errorMaps, issueData } = params;
|
|
488
|
-
const fullPath = [...
|
|
489
|
+
const { data, path: path2, errorMaps, issueData } = params;
|
|
490
|
+
const fullPath = [...path2, ...issueData.path || []];
|
|
489
491
|
const fullIssue = {
|
|
490
492
|
...issueData,
|
|
491
493
|
path: fullPath
|
|
@@ -601,11 +603,11 @@ var errorUtil;
|
|
|
601
603
|
|
|
602
604
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
603
605
|
var ParseInputLazyPath = class {
|
|
604
|
-
constructor(parent, value,
|
|
606
|
+
constructor(parent, value, path2, key) {
|
|
605
607
|
this._cachedPath = [];
|
|
606
608
|
this.parent = parent;
|
|
607
609
|
this.data = value;
|
|
608
|
-
this._path =
|
|
610
|
+
this._path = path2;
|
|
609
611
|
this._key = key;
|
|
610
612
|
}
|
|
611
613
|
get path() {
|
|
@@ -4229,6 +4231,70 @@ function checkGroupPolicy(params) {
|
|
|
4229
4231
|
return { allowed: true };
|
|
4230
4232
|
}
|
|
4231
4233
|
|
|
4234
|
+
// ../../packages/shared/src/file/file-utils.ts
|
|
4235
|
+
var MIME_TO_EXTENSION = {
|
|
4236
|
+
// Images
|
|
4237
|
+
"image/jpeg": ".jpg",
|
|
4238
|
+
"image/png": ".png",
|
|
4239
|
+
"image/gif": ".gif",
|
|
4240
|
+
"image/webp": ".webp",
|
|
4241
|
+
"image/bmp": ".bmp",
|
|
4242
|
+
// Audio
|
|
4243
|
+
"audio/mpeg": ".mp3",
|
|
4244
|
+
"audio/wav": ".wav",
|
|
4245
|
+
"audio/ogg": ".ogg",
|
|
4246
|
+
"audio/amr": ".amr",
|
|
4247
|
+
"audio/x-m4a": ".m4a",
|
|
4248
|
+
// Video
|
|
4249
|
+
"video/mp4": ".mp4",
|
|
4250
|
+
"video/quicktime": ".mov",
|
|
4251
|
+
"video/x-msvideo": ".avi",
|
|
4252
|
+
"video/webm": ".webm",
|
|
4253
|
+
// Documents
|
|
4254
|
+
"application/pdf": ".pdf",
|
|
4255
|
+
"application/msword": ".doc",
|
|
4256
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
4257
|
+
"application/vnd.ms-excel": ".xls",
|
|
4258
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
4259
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
4260
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
4261
|
+
"application/rtf": ".rtf",
|
|
4262
|
+
"application/vnd.oasis.opendocument.text": ".odt",
|
|
4263
|
+
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
4264
|
+
"text/plain": ".txt",
|
|
4265
|
+
"text/markdown": ".md",
|
|
4266
|
+
"text/csv": ".csv",
|
|
4267
|
+
// Archives
|
|
4268
|
+
"application/zip": ".zip",
|
|
4269
|
+
"application/x-rar-compressed": ".rar",
|
|
4270
|
+
"application/vnd.rar": ".rar",
|
|
4271
|
+
"application/x-7z-compressed": ".7z",
|
|
4272
|
+
"application/x-tar": ".tar",
|
|
4273
|
+
"application/gzip": ".gz",
|
|
4274
|
+
"application/x-gzip": ".gz",
|
|
4275
|
+
"application/x-bzip2": ".bz2",
|
|
4276
|
+
// Code
|
|
4277
|
+
"application/json": ".json",
|
|
4278
|
+
"application/xml": ".xml",
|
|
4279
|
+
"text/xml": ".xml",
|
|
4280
|
+
"text/html": ".html",
|
|
4281
|
+
"text/css": ".css",
|
|
4282
|
+
"text/javascript": ".js",
|
|
4283
|
+
"application/javascript": ".js",
|
|
4284
|
+
"text/x-python": ".py",
|
|
4285
|
+
"text/x-java-source": ".java",
|
|
4286
|
+
"text/x-c": ".c",
|
|
4287
|
+
"text/x-yaml": ".yaml",
|
|
4288
|
+
"application/x-yaml": ".yaml"
|
|
4289
|
+
};
|
|
4290
|
+
function resolveExtension(contentType, fileName) {
|
|
4291
|
+
const mimeType = contentType.split(";")[0].trim().toLowerCase();
|
|
4292
|
+
if (mimeType in MIME_TO_EXTENSION) {
|
|
4293
|
+
return MIME_TO_EXTENSION[mimeType];
|
|
4294
|
+
}
|
|
4295
|
+
return ".bin";
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4232
4298
|
// ../../packages/shared/src/media/media-parser.ts
|
|
4233
4299
|
var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4234
4300
|
"mp3",
|
|
@@ -4373,22 +4439,29 @@ function encryptWecomPlaintext(params) {
|
|
|
4373
4439
|
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
4374
4440
|
return encrypted.toString("base64");
|
|
4375
4441
|
}
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4442
|
+
function decryptWecomMedia(params) {
|
|
4443
|
+
const { encryptedBuffer, encodingAESKey } = params;
|
|
4444
|
+
if (!encryptedBuffer || encryptedBuffer.length === 0) {
|
|
4445
|
+
throw new Error("encryptedBuffer cannot be empty");
|
|
4446
|
+
}
|
|
4447
|
+
const aesKey = decodeEncodingAESKey(encodingAESKey);
|
|
4448
|
+
const iv = aesKey.subarray(0, 16);
|
|
4449
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
4450
|
+
decipher.setAutoPadding(false);
|
|
4451
|
+
try {
|
|
4452
|
+
const decryptedPadded = Buffer.concat([
|
|
4453
|
+
decipher.update(encryptedBuffer),
|
|
4454
|
+
decipher.final()
|
|
4455
|
+
]);
|
|
4456
|
+
const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
|
|
4457
|
+
return decrypted;
|
|
4458
|
+
} catch (err) {
|
|
4459
|
+
throw new Error(
|
|
4460
|
+
`Failed to decrypt media: ${err instanceof Error ? err.message : String(err)}`
|
|
4461
|
+
);
|
|
4388
4462
|
}
|
|
4389
|
-
return senderId;
|
|
4390
4463
|
}
|
|
4391
|
-
function
|
|
4464
|
+
function extractWecomContent(msg) {
|
|
4392
4465
|
const msgtype = String(msg.msgtype ?? "").toLowerCase();
|
|
4393
4466
|
if (msgtype === "text") {
|
|
4394
4467
|
const content = msg.text?.content;
|
|
@@ -4430,6 +4503,19 @@ function buildInboundBody(msg) {
|
|
|
4430
4503
|
}
|
|
4431
4504
|
return msgtype ? `[${msgtype}]` : "";
|
|
4432
4505
|
}
|
|
4506
|
+
function resolveSenderId(msg) {
|
|
4507
|
+
const userid = msg.from?.userid?.trim();
|
|
4508
|
+
return userid || "unknown";
|
|
4509
|
+
}
|
|
4510
|
+
function resolveChatType(msg) {
|
|
4511
|
+
return msg.chattype === "group" ? "group" : "direct";
|
|
4512
|
+
}
|
|
4513
|
+
function resolveChatId(msg, senderId, chatType) {
|
|
4514
|
+
if (chatType === "group") {
|
|
4515
|
+
return msg.chatid?.trim() || "unknown";
|
|
4516
|
+
}
|
|
4517
|
+
return senderId;
|
|
4518
|
+
}
|
|
4433
4519
|
async function dispatchWecomMessage(params) {
|
|
4434
4520
|
const { cfg, account, msg, core, hooks } = params;
|
|
4435
4521
|
const safeCfg = cfg ?? {};
|
|
@@ -4477,85 +4563,311 @@ async function dispatchWecomMessage(params) {
|
|
|
4477
4563
|
channel: "wecom",
|
|
4478
4564
|
peer: { kind: chatType === "group" ? "group" : "dm", id: chatId }
|
|
4479
4565
|
});
|
|
4480
|
-
const
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4566
|
+
const mediaResult = await processMediaInMessage({
|
|
4567
|
+
msg,
|
|
4568
|
+
encodingAESKey: account.encodingAESKey,
|
|
4569
|
+
log: logger
|
|
4484
4570
|
});
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
channel
|
|
4492
|
-
from: fromLabel,
|
|
4493
|
-
previousTimestamp,
|
|
4494
|
-
envelope: envelopeOptions,
|
|
4495
|
-
body: rawBody
|
|
4496
|
-
}) : rawBody;
|
|
4497
|
-
const ctxPayload = channel.reply?.finalizeInboundContext ? channel.reply.finalizeInboundContext({
|
|
4498
|
-
Body: body,
|
|
4499
|
-
RawBody: rawBody,
|
|
4500
|
-
CommandBody: rawBody,
|
|
4501
|
-
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
4502
|
-
To: `wecom:${chatId}`,
|
|
4503
|
-
SessionKey: route.sessionKey,
|
|
4504
|
-
AccountId: route.accountId,
|
|
4505
|
-
ChatType: chatType,
|
|
4506
|
-
ConversationLabel: fromLabel,
|
|
4507
|
-
SenderName: senderId,
|
|
4508
|
-
SenderId: senderId,
|
|
4509
|
-
Provider: "wecom",
|
|
4510
|
-
Surface: "wecom",
|
|
4511
|
-
MessageSid: msg.msgid,
|
|
4512
|
-
OriginatingChannel: "wecom",
|
|
4513
|
-
OriginatingTo: `wecom:${chatId}`
|
|
4514
|
-
}) : {
|
|
4515
|
-
Body: body,
|
|
4516
|
-
RawBody: rawBody,
|
|
4517
|
-
CommandBody: rawBody,
|
|
4518
|
-
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
4519
|
-
To: `wecom:${chatId}`,
|
|
4520
|
-
SessionKey: route.sessionKey,
|
|
4521
|
-
AccountId: route.accountId,
|
|
4522
|
-
ChatType: chatType,
|
|
4523
|
-
ConversationLabel: fromLabel,
|
|
4524
|
-
SenderName: senderId,
|
|
4525
|
-
SenderId: senderId,
|
|
4526
|
-
Provider: "wecom",
|
|
4527
|
-
Surface: "wecom",
|
|
4528
|
-
MessageSid: msg.msgid,
|
|
4529
|
-
OriginatingChannel: "wecom",
|
|
4530
|
-
OriginatingTo: `wecom:${chatId}`
|
|
4531
|
-
};
|
|
4532
|
-
if (channel.session?.recordInboundSession && storePath) {
|
|
4533
|
-
await channel.session.recordInboundSession({
|
|
4571
|
+
const rawBody = mediaResult.text;
|
|
4572
|
+
const fromLabel = chatType === "group" ? `group:${chatId}` : `user:${senderId}`;
|
|
4573
|
+
try {
|
|
4574
|
+
const storePath = channel.session?.resolveStorePath?.(safeCfg.session?.store, {
|
|
4575
|
+
agentId: route.agentId
|
|
4576
|
+
});
|
|
4577
|
+
const previousTimestamp = channel.session?.readSessionUpdatedAt ? channel.session.readSessionUpdatedAt({
|
|
4534
4578
|
storePath,
|
|
4535
|
-
sessionKey:
|
|
4579
|
+
sessionKey: route.sessionKey
|
|
4580
|
+
}) ?? void 0 : void 0;
|
|
4581
|
+
const envelopeOptions = channel.reply?.resolveEnvelopeFormatOptions ? channel.reply.resolveEnvelopeFormatOptions(safeCfg) : void 0;
|
|
4582
|
+
const body = channel.reply?.formatAgentEnvelope ? channel.reply.formatAgentEnvelope({
|
|
4583
|
+
channel: "WeCom",
|
|
4584
|
+
from: fromLabel,
|
|
4585
|
+
previousTimestamp,
|
|
4586
|
+
envelope: envelopeOptions,
|
|
4587
|
+
body: rawBody
|
|
4588
|
+
}) : rawBody;
|
|
4589
|
+
const ctxPayload = channel.reply?.finalizeInboundContext ? channel.reply.finalizeInboundContext({
|
|
4590
|
+
Body: body,
|
|
4591
|
+
RawBody: rawBody,
|
|
4592
|
+
CommandBody: rawBody,
|
|
4593
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
4594
|
+
To: `wecom:${chatId}`,
|
|
4595
|
+
SessionKey: route.sessionKey,
|
|
4596
|
+
AccountId: route.accountId,
|
|
4597
|
+
ChatType: chatType,
|
|
4598
|
+
ConversationLabel: fromLabel,
|
|
4599
|
+
SenderName: senderId,
|
|
4600
|
+
SenderId: senderId,
|
|
4601
|
+
Provider: "wecom",
|
|
4602
|
+
Surface: "wecom",
|
|
4603
|
+
MessageSid: msg.msgid,
|
|
4604
|
+
OriginatingChannel: "wecom",
|
|
4605
|
+
OriginatingTo: `wecom:${chatId}`
|
|
4606
|
+
}) : {
|
|
4607
|
+
Body: body,
|
|
4608
|
+
RawBody: rawBody,
|
|
4609
|
+
CommandBody: rawBody,
|
|
4610
|
+
From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${senderId}`,
|
|
4611
|
+
To: `wecom:${chatId}`,
|
|
4612
|
+
SessionKey: route.sessionKey,
|
|
4613
|
+
AccountId: route.accountId,
|
|
4614
|
+
ChatType: chatType,
|
|
4615
|
+
ConversationLabel: fromLabel,
|
|
4616
|
+
SenderName: senderId,
|
|
4617
|
+
SenderId: senderId,
|
|
4618
|
+
Provider: "wecom",
|
|
4619
|
+
Surface: "wecom",
|
|
4620
|
+
MessageSid: msg.msgid,
|
|
4621
|
+
OriginatingChannel: "wecom",
|
|
4622
|
+
OriginatingTo: `wecom:${chatId}`
|
|
4623
|
+
};
|
|
4624
|
+
if (channel.session?.recordInboundSession && storePath) {
|
|
4625
|
+
await channel.session.recordInboundSession({
|
|
4626
|
+
storePath,
|
|
4627
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
4628
|
+
ctx: ctxPayload,
|
|
4629
|
+
onRecordError: (err) => {
|
|
4630
|
+
logger.error(`wecom: failed updating session meta: ${String(err)}`);
|
|
4631
|
+
}
|
|
4632
|
+
});
|
|
4633
|
+
}
|
|
4634
|
+
const tableMode = channel.text?.resolveMarkdownTableMode ? channel.text.resolveMarkdownTableMode({ cfg: safeCfg, channel: "wecom", accountId: account.accountId }) : void 0;
|
|
4635
|
+
await channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
4536
4636
|
ctx: ctxPayload,
|
|
4537
|
-
|
|
4538
|
-
|
|
4637
|
+
cfg: safeCfg,
|
|
4638
|
+
dispatcherOptions: {
|
|
4639
|
+
deliver: async (payload) => {
|
|
4640
|
+
const rawText = payload.text ?? "";
|
|
4641
|
+
if (!rawText.trim()) return;
|
|
4642
|
+
const converted = channel.text?.convertMarkdownTables && tableMode ? channel.text.convertMarkdownTables(rawText, tableMode) : rawText;
|
|
4643
|
+
hooks.onChunk(converted);
|
|
4644
|
+
},
|
|
4645
|
+
onError: (err, info) => {
|
|
4646
|
+
hooks.onError?.(err);
|
|
4647
|
+
logger.error(`${info.kind} reply failed: ${String(err)}`);
|
|
4648
|
+
}
|
|
4539
4649
|
}
|
|
4540
4650
|
});
|
|
4651
|
+
} finally {
|
|
4541
4652
|
}
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4653
|
+
}
|
|
4654
|
+
var MEDIA_DOWNLOAD_TIMEOUT = 6e4;
|
|
4655
|
+
async function downloadAndDecryptMedia(params) {
|
|
4656
|
+
const { mediaUrl, encodingAESKey, fileName, log } = params;
|
|
4657
|
+
if (!mediaUrl) {
|
|
4658
|
+
throw new Error("mediaUrl is required");
|
|
4659
|
+
}
|
|
4660
|
+
if (!encodingAESKey) {
|
|
4661
|
+
throw new Error("encodingAESKey is required");
|
|
4662
|
+
}
|
|
4663
|
+
log?.debug?.(`[wecom] \u4E0B\u8F7D\u52A0\u5BC6\u5A92\u4F53\u6587\u4EF6: ${mediaUrl.slice(0, 100)}...`);
|
|
4664
|
+
const controller = new AbortController();
|
|
4665
|
+
const timeoutId = setTimeout(() => controller.abort(), MEDIA_DOWNLOAD_TIMEOUT);
|
|
4666
|
+
let encryptedBuffer;
|
|
4667
|
+
let contentType = "application/octet-stream";
|
|
4668
|
+
let contentDisposition = null;
|
|
4669
|
+
try {
|
|
4670
|
+
const response = await fetch(mediaUrl, { signal: controller.signal });
|
|
4671
|
+
if (!response.ok) {
|
|
4672
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4673
|
+
}
|
|
4674
|
+
contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
4675
|
+
contentDisposition = response.headers.get("content-disposition");
|
|
4676
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
4677
|
+
encryptedBuffer = Buffer.from(arrayBuffer);
|
|
4678
|
+
log?.debug?.(`[wecom] \u4E0B\u8F7D\u5B8C\u6210: ${encryptedBuffer.length} \u5B57\u8282`);
|
|
4679
|
+
} catch (err) {
|
|
4680
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
4681
|
+
throw new Error(`\u5A92\u4F53\u4E0B\u8F7D\u8D85\u65F6\uFF08${MEDIA_DOWNLOAD_TIMEOUT}ms\uFF09`);
|
|
4682
|
+
}
|
|
4683
|
+
throw err;
|
|
4684
|
+
} finally {
|
|
4685
|
+
clearTimeout(timeoutId);
|
|
4686
|
+
}
|
|
4687
|
+
log?.debug?.(`[wecom] \u89E3\u5BC6\u5A92\u4F53\u6587\u4EF6...`);
|
|
4688
|
+
let decryptedBuffer;
|
|
4689
|
+
try {
|
|
4690
|
+
decryptedBuffer = decryptWecomMedia({
|
|
4691
|
+
encryptedBuffer,
|
|
4692
|
+
encodingAESKey
|
|
4693
|
+
});
|
|
4694
|
+
log?.debug?.(`[wecom] \u89E3\u5BC6\u5B8C\u6210: ${decryptedBuffer.length} \u5B57\u8282`);
|
|
4695
|
+
} catch (err) {
|
|
4696
|
+
throw new Error(`\u89E3\u5BC6\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
4697
|
+
}
|
|
4698
|
+
const sanitizeFileName = (input) => {
|
|
4699
|
+
if (!input) return void 0;
|
|
4700
|
+
const base = path.basename(input);
|
|
4701
|
+
const cleaned = base.replace(/[\\\/]+/g, "_").replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
4702
|
+
if (!cleaned || cleaned === "." || cleaned === "..") return void 0;
|
|
4703
|
+
return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
|
|
4704
|
+
};
|
|
4705
|
+
let originalFileName = sanitizeFileName(fileName);
|
|
4706
|
+
if (contentDisposition && !originalFileName) {
|
|
4707
|
+
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
|
4708
|
+
if (filenameMatch && filenameMatch[1]) {
|
|
4709
|
+
let headerFileName = filenameMatch[1].replace(/['"]/g, "");
|
|
4710
|
+
try {
|
|
4711
|
+
headerFileName = decodeURIComponent(headerFileName);
|
|
4712
|
+
} catch {
|
|
4713
|
+
}
|
|
4714
|
+
originalFileName = sanitizeFileName(headerFileName);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
let extension = "";
|
|
4718
|
+
if (originalFileName) {
|
|
4719
|
+
const lastDotIndex = originalFileName.lastIndexOf(".");
|
|
4720
|
+
if (lastDotIndex > 0) {
|
|
4721
|
+
extension = originalFileName.slice(lastDotIndex);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (!extension) {
|
|
4725
|
+
extension = resolveExtension(contentType);
|
|
4726
|
+
}
|
|
4727
|
+
const now = /* @__PURE__ */ new Date();
|
|
4728
|
+
const yearMonth = now.toISOString().slice(0, 7).replace("-", "");
|
|
4729
|
+
const wecomDir = path.join("/tmp", "wecom", yearMonth);
|
|
4730
|
+
await fsPromises.mkdir(wecomDir, { recursive: true });
|
|
4731
|
+
const baseFileName = originalFileName || `wecom-media`;
|
|
4732
|
+
const baseNameWithoutExt = baseFileName.replace(/\.[-.\w]+$/, "");
|
|
4733
|
+
const timestamp = Date.now();
|
|
4734
|
+
const safeFileName = `${baseNameWithoutExt}-${timestamp}${extension}`;
|
|
4735
|
+
const resolvedDir = path.resolve(wecomDir);
|
|
4736
|
+
const resolvedPath = path.resolve(wecomDir, safeFileName);
|
|
4737
|
+
if (!resolvedPath.startsWith(`${resolvedDir}${path.sep}`) && resolvedPath !== resolvedDir) {
|
|
4738
|
+
throw new Error("Invalid media file path");
|
|
4739
|
+
}
|
|
4740
|
+
await fsPromises.writeFile(resolvedPath, decryptedBuffer);
|
|
4741
|
+
log?.debug?.(`[wecom] \u6587\u4EF6\u5DF2\u4FDD\u5B58: ${resolvedPath}`);
|
|
4742
|
+
return {
|
|
4743
|
+
path: resolvedPath,
|
|
4744
|
+
contentType,
|
|
4745
|
+
size: decryptedBuffer.length,
|
|
4746
|
+
fileName
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
async function processMediaInMessage(params) {
|
|
4750
|
+
const { msg, encodingAESKey, log } = params;
|
|
4751
|
+
if (!encodingAESKey) {
|
|
4752
|
+
log?.debug?.(`[wecom] \u672A\u914D\u7F6E encodingAESKey\uFF0C\u8DF3\u8FC7\u5A92\u4F53\u89E3\u5BC6`);
|
|
4753
|
+
return { text: extractWecomContent(msg) };
|
|
4754
|
+
}
|
|
4755
|
+
const msgtype = String(msg.msgtype ?? "").toLowerCase();
|
|
4756
|
+
if (msgtype === "mixed") {
|
|
4757
|
+
const items = msg.mixed?.msg_item;
|
|
4758
|
+
if (Array.isArray(items)) {
|
|
4759
|
+
const processedParts = [];
|
|
4760
|
+
for (const item of items) {
|
|
4761
|
+
if (!item || typeof item !== "object") continue;
|
|
4762
|
+
const typed = item;
|
|
4763
|
+
const t = String(typed.msgtype ?? "").toLowerCase();
|
|
4764
|
+
if (t === "text") {
|
|
4765
|
+
const content = String(typed.text?.content ?? "");
|
|
4766
|
+
processedParts.push(content);
|
|
4767
|
+
} else if (t === "image") {
|
|
4768
|
+
const url = String(typed.image?.url ?? "").trim();
|
|
4769
|
+
if (url) {
|
|
4770
|
+
try {
|
|
4771
|
+
const mediaFile = await downloadAndDecryptMedia({
|
|
4772
|
+
mediaUrl: url,
|
|
4773
|
+
encodingAESKey,
|
|
4774
|
+
fileName: "image.jpg",
|
|
4775
|
+
log
|
|
4776
|
+
});
|
|
4777
|
+
processedParts.push(`[image] ${mediaFile.path}`);
|
|
4778
|
+
} catch (err) {
|
|
4779
|
+
log?.error?.(`[wecom] mixed\u6D88\u606F\u4E2D\u56FE\u7247\u4E0B\u8F7D\u89E3\u5BC6\u5931\u8D25: ${err}`);
|
|
4780
|
+
processedParts.push(`[image] ${url}`);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
} else if (t === "file") {
|
|
4784
|
+
const url = String(typed.file?.url ?? "").trim();
|
|
4785
|
+
const fileName = String(typed.file?.filename ?? "file.bin").trim();
|
|
4786
|
+
if (url) {
|
|
4787
|
+
try {
|
|
4788
|
+
const mediaFile = await downloadAndDecryptMedia({
|
|
4789
|
+
mediaUrl: url,
|
|
4790
|
+
encodingAESKey,
|
|
4791
|
+
fileName,
|
|
4792
|
+
log
|
|
4793
|
+
});
|
|
4794
|
+
processedParts.push(`[file] ${mediaFile.path}`);
|
|
4795
|
+
} catch (err) {
|
|
4796
|
+
log?.error?.(`[wecom] mixed\u6D88\u606F\u4E2D\u6587\u4EF6\u4E0B\u8F7D\u89E3\u5BC6\u5931\u8D25: ${err}`);
|
|
4797
|
+
processedParts.push(`[file] ${url}`);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
} else {
|
|
4801
|
+
processedParts.push(t ? `[${t}]` : "");
|
|
4802
|
+
}
|
|
4556
4803
|
}
|
|
4804
|
+
return {
|
|
4805
|
+
text: processedParts.filter((p) => Boolean(p && p.trim())).join("\n")
|
|
4806
|
+
};
|
|
4557
4807
|
}
|
|
4558
|
-
|
|
4808
|
+
return { text: extractWecomContent(msg) };
|
|
4809
|
+
}
|
|
4810
|
+
if (msgtype === "image") {
|
|
4811
|
+
const url = String(msg.image?.url ?? "").trim();
|
|
4812
|
+
if (url) {
|
|
4813
|
+
try {
|
|
4814
|
+
const mediaFile = await downloadAndDecryptMedia({
|
|
4815
|
+
mediaUrl: url,
|
|
4816
|
+
encodingAESKey,
|
|
4817
|
+
fileName: "image.jpg",
|
|
4818
|
+
// 默认文件名
|
|
4819
|
+
log
|
|
4820
|
+
});
|
|
4821
|
+
return {
|
|
4822
|
+
text: `[image] ${mediaFile.path}`
|
|
4823
|
+
};
|
|
4824
|
+
} catch (err) {
|
|
4825
|
+
log?.error?.(`[wecom] \u56FE\u7247\u4E0B\u8F7D\u89E3\u5BC6\u5931\u8D25: ${err}`);
|
|
4826
|
+
return { text: extractWecomContent(msg) };
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
if (msgtype === "file") {
|
|
4831
|
+
const url = String(msg.file?.url ?? "").trim();
|
|
4832
|
+
const fileName = msg.file?.filename;
|
|
4833
|
+
if (url) {
|
|
4834
|
+
try {
|
|
4835
|
+
const mediaFile = await downloadAndDecryptMedia({
|
|
4836
|
+
mediaUrl: url,
|
|
4837
|
+
encodingAESKey,
|
|
4838
|
+
fileName,
|
|
4839
|
+
log
|
|
4840
|
+
});
|
|
4841
|
+
return {
|
|
4842
|
+
text: `[file] ${mediaFile.path}`
|
|
4843
|
+
};
|
|
4844
|
+
} catch (err) {
|
|
4845
|
+
log?.error?.(`[wecom] \u6587\u4EF6\u4E0B\u8F7D\u89E3\u5BC6\u5931\u8D25: ${err}`);
|
|
4846
|
+
return { text: extractWecomContent(msg) };
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
if (msgtype === "voice") {
|
|
4851
|
+
const url = String(msg.voice?.url ?? "").trim();
|
|
4852
|
+
if (url) {
|
|
4853
|
+
try {
|
|
4854
|
+
const mediaFile = await downloadAndDecryptMedia({
|
|
4855
|
+
mediaUrl: url,
|
|
4856
|
+
encodingAESKey,
|
|
4857
|
+
fileName: "voice.amr",
|
|
4858
|
+
// 默认文件名
|
|
4859
|
+
log
|
|
4860
|
+
});
|
|
4861
|
+
return {
|
|
4862
|
+
text: `[voice] ${mediaFile.path}`
|
|
4863
|
+
};
|
|
4864
|
+
} catch (err) {
|
|
4865
|
+
log?.error?.(`[wecom] \u8BED\u97F3\u4E0B\u8F7D\u89E3\u5BC6\u5931\u8D25: ${err}`);
|
|
4866
|
+
return { text: extractWecomContent(msg) };
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
return { text: extractWecomContent(msg) };
|
|
4559
4871
|
}
|
|
4560
4872
|
|
|
4561
4873
|
// src/runtime.ts
|
|
@@ -4614,11 +4926,11 @@ function jsonOk(res, body) {
|
|
|
4614
4926
|
async function readJsonBody(req, maxBytes) {
|
|
4615
4927
|
const chunks = [];
|
|
4616
4928
|
let total = 0;
|
|
4617
|
-
return await new Promise((
|
|
4929
|
+
return await new Promise((resolve2) => {
|
|
4618
4930
|
req.on("data", (chunk) => {
|
|
4619
4931
|
total += chunk.length;
|
|
4620
4932
|
if (total > maxBytes) {
|
|
4621
|
-
|
|
4933
|
+
resolve2({ ok: false, error: "payload too large" });
|
|
4622
4934
|
req.destroy();
|
|
4623
4935
|
return;
|
|
4624
4936
|
}
|
|
@@ -4628,16 +4940,16 @@ async function readJsonBody(req, maxBytes) {
|
|
|
4628
4940
|
try {
|
|
4629
4941
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
4630
4942
|
if (!raw.trim()) {
|
|
4631
|
-
|
|
4943
|
+
resolve2({ ok: false, error: "empty payload" });
|
|
4632
4944
|
return;
|
|
4633
4945
|
}
|
|
4634
|
-
|
|
4946
|
+
resolve2({ ok: true, value: JSON.parse(raw) });
|
|
4635
4947
|
} catch (err) {
|
|
4636
|
-
|
|
4948
|
+
resolve2({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
4637
4949
|
}
|
|
4638
4950
|
});
|
|
4639
4951
|
req.on("error", (err) => {
|
|
4640
|
-
|
|
4952
|
+
resolve2({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
4641
4953
|
});
|
|
4642
4954
|
});
|
|
4643
4955
|
}
|
|
@@ -4705,12 +5017,12 @@ function parseWecomPlainMessage(raw) {
|
|
|
4705
5017
|
}
|
|
4706
5018
|
async function waitForStreamContent(streamId, maxWaitMs) {
|
|
4707
5019
|
const startedAt = Date.now();
|
|
4708
|
-
await new Promise((
|
|
5020
|
+
await new Promise((resolve2) => {
|
|
4709
5021
|
const tick = () => {
|
|
4710
5022
|
const state = streams.get(streamId);
|
|
4711
|
-
if (!state) return
|
|
4712
|
-
if (state.error || state.finished || state.content.trim()) return
|
|
4713
|
-
if (Date.now() - startedAt >= maxWaitMs) return
|
|
5023
|
+
if (!state) return resolve2();
|
|
5024
|
+
if (state.error || state.finished || state.content.trim()) return resolve2();
|
|
5025
|
+
if (Date.now() - startedAt >= maxWaitMs) return resolve2();
|
|
4714
5026
|
setTimeout(tick, 25);
|
|
4715
5027
|
};
|
|
4716
5028
|
tick();
|
|
@@ -4743,8 +5055,8 @@ function registerWecomWebhookTarget(target) {
|
|
|
4743
5055
|
}
|
|
4744
5056
|
async function handleWecomWebhookRequest(req, res) {
|
|
4745
5057
|
pruneStreams();
|
|
4746
|
-
const
|
|
4747
|
-
const targets = webhookTargets.get(
|
|
5058
|
+
const path2 = resolvePath(req);
|
|
5059
|
+
const targets = webhookTargets.get(path2);
|
|
4748
5060
|
if (!targets || targets.length === 0) return false;
|
|
4749
5061
|
const query = resolveQueryParams(req);
|
|
4750
5062
|
const timestamp = query.get("timestamp") ?? "";
|
|
@@ -4752,7 +5064,7 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
4752
5064
|
const signature = resolveSignatureParam(query);
|
|
4753
5065
|
const primary = targets[0];
|
|
4754
5066
|
const logger = buildLogger(primary);
|
|
4755
|
-
logger.debug(`incoming ${req.method} request on ${
|
|
5067
|
+
logger.debug(`incoming ${req.method} request on ${path2} (timestamp=${timestamp}, nonce=${nonce})`);
|
|
4756
5068
|
if (req.method === "GET") {
|
|
4757
5069
|
const echostr = query.get("echostr") ?? "";
|
|
4758
5070
|
if (!timestamp || !nonce || !signature || !echostr) {
|
|
@@ -5126,7 +5438,7 @@ var wecomPlugin = {
|
|
|
5126
5438
|
ctx.setStatus?.({ accountId: ctx.accountId, running: false, configured: false });
|
|
5127
5439
|
return;
|
|
5128
5440
|
}
|
|
5129
|
-
const
|
|
5441
|
+
const path2 = (account.config.webhookPath ?? "/wecom").trim();
|
|
5130
5442
|
const unregister = registerWecomWebhookTarget({
|
|
5131
5443
|
account,
|
|
5132
5444
|
config: ctx.cfg ?? {},
|
|
@@ -5134,18 +5446,18 @@ var wecomPlugin = {
|
|
|
5134
5446
|
log: ctx.log?.info ?? console.log,
|
|
5135
5447
|
error: ctx.log?.error ?? console.error
|
|
5136
5448
|
},
|
|
5137
|
-
path,
|
|
5449
|
+
path: path2,
|
|
5138
5450
|
statusSink: (patch) => ctx.setStatus?.({ accountId: ctx.accountId, ...patch })
|
|
5139
5451
|
});
|
|
5140
5452
|
const existing = unregisterHooks.get(ctx.accountId);
|
|
5141
5453
|
if (existing) existing();
|
|
5142
5454
|
unregisterHooks.set(ctx.accountId, unregister);
|
|
5143
|
-
ctx.log?.info(`[wecom] webhook registered at ${
|
|
5455
|
+
ctx.log?.info(`[wecom] webhook registered at ${path2} for account ${ctx.accountId}`);
|
|
5144
5456
|
ctx.setStatus?.({
|
|
5145
5457
|
accountId: ctx.accountId,
|
|
5146
5458
|
running: true,
|
|
5147
5459
|
configured: true,
|
|
5148
|
-
webhookPath:
|
|
5460
|
+
webhookPath: path2,
|
|
5149
5461
|
lastStartAt: Date.now()
|
|
5150
5462
|
});
|
|
5151
5463
|
},
|