@openclaw-china/wecom 0.1.29 → 2026.3.3
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 +273 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7335,7 +7335,156 @@ function jsonOk(res, body) {
|
|
|
7335
7335
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
7336
7336
|
res.end(JSON.stringify(body));
|
|
7337
7337
|
}
|
|
7338
|
-
|
|
7338
|
+
function extractXmlTag(xml, tag) {
|
|
7339
|
+
if (!xml || !tag) return void 0;
|
|
7340
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7341
|
+
const re = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>([\\s\\S]*?)</${escapedTag}>`, "i");
|
|
7342
|
+
const m = re.exec(xml);
|
|
7343
|
+
if (!m) return void 0;
|
|
7344
|
+
const body = m[1] ?? "";
|
|
7345
|
+
const cdata = /^<!\[CDATA\[([\s\S]*?)\]\]>$/i.exec(body.trim());
|
|
7346
|
+
if (cdata) return cdata[1] ?? "";
|
|
7347
|
+
return body;
|
|
7348
|
+
}
|
|
7349
|
+
function extractXmlTagAll(xml, tag) {
|
|
7350
|
+
if (!xml || !tag) return [];
|
|
7351
|
+
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7352
|
+
const re = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>([\\s\\S]*?)</${escapedTag}>`, "gi");
|
|
7353
|
+
const out = [];
|
|
7354
|
+
let m;
|
|
7355
|
+
while ((m = re.exec(xml)) !== null) {
|
|
7356
|
+
const body = m[1] ?? "";
|
|
7357
|
+
const cdata = /^<!\[CDATA\[([\s\S]*?)\]\]>$/i.exec(body.trim());
|
|
7358
|
+
out.push(cdata ? cdata[1] ?? "" : body);
|
|
7359
|
+
}
|
|
7360
|
+
return out;
|
|
7361
|
+
}
|
|
7362
|
+
function pickFirstNonEmpty(...values) {
|
|
7363
|
+
for (const value of values) {
|
|
7364
|
+
const trimmed = value?.trim();
|
|
7365
|
+
if (trimmed) return trimmed;
|
|
7366
|
+
}
|
|
7367
|
+
return "";
|
|
7368
|
+
}
|
|
7369
|
+
function parseXmlTextPayload(xml) {
|
|
7370
|
+
const textBlock = extractXmlTag(xml, "Text") ?? "";
|
|
7371
|
+
const content = pickFirstNonEmpty(
|
|
7372
|
+
extractXmlTag(textBlock, "Content"),
|
|
7373
|
+
extractXmlTag(xml, "Content")
|
|
7374
|
+
);
|
|
7375
|
+
if (!content) return void 0;
|
|
7376
|
+
return { content };
|
|
7377
|
+
}
|
|
7378
|
+
function parseXmlVoicePayload(xml) {
|
|
7379
|
+
const voiceBlock = extractXmlTag(xml, "Voice") ?? "";
|
|
7380
|
+
const content = pickFirstNonEmpty(
|
|
7381
|
+
extractXmlTag(voiceBlock, "Content"),
|
|
7382
|
+
extractXmlTag(voiceBlock, "Recognition"),
|
|
7383
|
+
extractXmlTag(xml, "Recognition"),
|
|
7384
|
+
extractXmlTag(xml, "Content")
|
|
7385
|
+
);
|
|
7386
|
+
const url = pickFirstNonEmpty(
|
|
7387
|
+
extractXmlTag(voiceBlock, "Url"),
|
|
7388
|
+
extractXmlTag(voiceBlock, "VoiceUrl"),
|
|
7389
|
+
extractXmlTag(xml, "VoiceUrl")
|
|
7390
|
+
);
|
|
7391
|
+
const mediaId = pickFirstNonEmpty(
|
|
7392
|
+
extractXmlTag(voiceBlock, "MediaId"),
|
|
7393
|
+
extractXmlTag(xml, "MediaId")
|
|
7394
|
+
);
|
|
7395
|
+
if (!content && !url && !mediaId) return void 0;
|
|
7396
|
+
const voice = {};
|
|
7397
|
+
if (content) voice.content = content;
|
|
7398
|
+
if (url) voice.url = url;
|
|
7399
|
+
if (mediaId) voice.media_id = mediaId;
|
|
7400
|
+
return voice;
|
|
7401
|
+
}
|
|
7402
|
+
function parseXmlImagePayload(xml) {
|
|
7403
|
+
const imageBlock = extractXmlTag(xml, "Image") ?? "";
|
|
7404
|
+
const url = pickFirstNonEmpty(
|
|
7405
|
+
extractXmlTag(imageBlock, "Url"),
|
|
7406
|
+
extractXmlTag(imageBlock, "PicUrl"),
|
|
7407
|
+
extractXmlTag(xml, "PicUrl"),
|
|
7408
|
+
extractXmlTag(xml, "Url")
|
|
7409
|
+
);
|
|
7410
|
+
const mediaId = pickFirstNonEmpty(
|
|
7411
|
+
extractXmlTag(imageBlock, "MediaId"),
|
|
7412
|
+
extractXmlTag(xml, "MediaId")
|
|
7413
|
+
);
|
|
7414
|
+
if (!url && !mediaId) return void 0;
|
|
7415
|
+
const image = {};
|
|
7416
|
+
if (url) image.url = url;
|
|
7417
|
+
if (mediaId) image.media_id = mediaId;
|
|
7418
|
+
return image;
|
|
7419
|
+
}
|
|
7420
|
+
function parseXmlFilePayload(xml) {
|
|
7421
|
+
const fileBlock = extractXmlTag(xml, "File") ?? "";
|
|
7422
|
+
const url = pickFirstNonEmpty(
|
|
7423
|
+
extractXmlTag(fileBlock, "Url"),
|
|
7424
|
+
extractXmlTag(fileBlock, "FileUrl"),
|
|
7425
|
+
extractXmlTag(xml, "FileUrl"),
|
|
7426
|
+
extractXmlTag(xml, "Url")
|
|
7427
|
+
);
|
|
7428
|
+
const fileName = pickFirstNonEmpty(
|
|
7429
|
+
extractXmlTag(fileBlock, "FileName"),
|
|
7430
|
+
extractXmlTag(fileBlock, "Name"),
|
|
7431
|
+
extractXmlTag(xml, "FileName")
|
|
7432
|
+
);
|
|
7433
|
+
const mediaId = pickFirstNonEmpty(
|
|
7434
|
+
extractXmlTag(fileBlock, "MediaId"),
|
|
7435
|
+
extractXmlTag(xml, "MediaId")
|
|
7436
|
+
);
|
|
7437
|
+
if (!url && !fileName && !mediaId) return void 0;
|
|
7438
|
+
const file = {};
|
|
7439
|
+
if (url) file.url = url;
|
|
7440
|
+
if (fileName) file.filename = fileName;
|
|
7441
|
+
if (mediaId) file.media_id = mediaId;
|
|
7442
|
+
return file;
|
|
7443
|
+
}
|
|
7444
|
+
function parseXmlMixedItems(xml) {
|
|
7445
|
+
const mixedBlock = extractXmlTag(xml, "Mixed");
|
|
7446
|
+
if (!mixedBlock) return [];
|
|
7447
|
+
const itemBlocks = [
|
|
7448
|
+
...extractXmlTagAll(mixedBlock, "MsgItem"),
|
|
7449
|
+
...extractXmlTagAll(mixedBlock, "msg_item")
|
|
7450
|
+
];
|
|
7451
|
+
if (itemBlocks.length === 0) return [];
|
|
7452
|
+
const items = [];
|
|
7453
|
+
for (const itemBlock of itemBlocks) {
|
|
7454
|
+
const itemType = pickFirstNonEmpty(
|
|
7455
|
+
extractXmlTag(itemBlock, "MsgType"),
|
|
7456
|
+
extractXmlTag(itemBlock, "msgtype"),
|
|
7457
|
+
extractXmlTag(itemBlock, "Type")
|
|
7458
|
+
).toLowerCase();
|
|
7459
|
+
if (!itemType) continue;
|
|
7460
|
+
if (itemType === "text") {
|
|
7461
|
+
const text = parseXmlTextPayload(itemBlock);
|
|
7462
|
+
items.push({ msgtype: "text", text: text ?? { content: "" } });
|
|
7463
|
+
continue;
|
|
7464
|
+
}
|
|
7465
|
+
if (itemType === "image") {
|
|
7466
|
+
const image = parseXmlImagePayload(itemBlock);
|
|
7467
|
+
if (image) items.push({ msgtype: "image", image });
|
|
7468
|
+
else items.push({ msgtype: "image" });
|
|
7469
|
+
continue;
|
|
7470
|
+
}
|
|
7471
|
+
if (itemType === "file") {
|
|
7472
|
+
const file = parseXmlFilePayload(itemBlock);
|
|
7473
|
+
if (file) items.push({ msgtype: "file", file });
|
|
7474
|
+
else items.push({ msgtype: "file" });
|
|
7475
|
+
continue;
|
|
7476
|
+
}
|
|
7477
|
+
if (itemType === "voice") {
|
|
7478
|
+
const voice = parseXmlVoicePayload(itemBlock);
|
|
7479
|
+
if (voice) items.push({ msgtype: "voice", voice });
|
|
7480
|
+
else items.push({ msgtype: "voice" });
|
|
7481
|
+
continue;
|
|
7482
|
+
}
|
|
7483
|
+
items.push({ msgtype: itemType });
|
|
7484
|
+
}
|
|
7485
|
+
return items;
|
|
7486
|
+
}
|
|
7487
|
+
async function readRequestBody(req, maxBytes) {
|
|
7339
7488
|
const chunks = [];
|
|
7340
7489
|
let total = 0;
|
|
7341
7490
|
return await new Promise((resolve3) => {
|
|
@@ -7355,7 +7504,17 @@ async function readJsonBody(req, maxBytes) {
|
|
|
7355
7504
|
resolve3({ ok: false, error: "empty payload" });
|
|
7356
7505
|
return;
|
|
7357
7506
|
}
|
|
7358
|
-
|
|
7507
|
+
const trimmed = raw.trim();
|
|
7508
|
+
if (trimmed.startsWith("<")) {
|
|
7509
|
+
const encrypt = pickFirstNonEmpty(extractXmlTag(trimmed, "Encrypt"));
|
|
7510
|
+
if (encrypt) {
|
|
7511
|
+
resolve3({ ok: true, value: { Encrypt: encrypt }, raw });
|
|
7512
|
+
} else {
|
|
7513
|
+
resolve3({ ok: false, raw, error: "xml body missing Encrypt tag" });
|
|
7514
|
+
}
|
|
7515
|
+
return;
|
|
7516
|
+
}
|
|
7517
|
+
resolve3({ ok: true, value: JSON.parse(raw), raw });
|
|
7359
7518
|
} catch (err) {
|
|
7360
7519
|
resolve3({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
7361
7520
|
}
|
|
@@ -7424,11 +7583,94 @@ function createStreamId() {
|
|
|
7424
7583
|
return crypto.randomBytes(16).toString("hex");
|
|
7425
7584
|
}
|
|
7426
7585
|
function parseWecomPlainMessage(raw) {
|
|
7427
|
-
const
|
|
7428
|
-
if (
|
|
7429
|
-
return
|
|
7586
|
+
const trimmed = raw.trim();
|
|
7587
|
+
if (trimmed.startsWith("<")) {
|
|
7588
|
+
return parseWecomXmlMessage(trimmed);
|
|
7589
|
+
}
|
|
7590
|
+
try {
|
|
7591
|
+
const parsed = JSON.parse(raw);
|
|
7592
|
+
if (!parsed || typeof parsed !== "object") {
|
|
7593
|
+
return {};
|
|
7594
|
+
}
|
|
7595
|
+
return parsed;
|
|
7596
|
+
} catch {
|
|
7597
|
+
return parseWecomXmlMessage(trimmed);
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
function parseWecomXmlMessage(xml) {
|
|
7601
|
+
const msgtype = pickFirstNonEmpty(
|
|
7602
|
+
extractXmlTag(xml, "MsgType"),
|
|
7603
|
+
extractXmlTag(xml, "msgtype")
|
|
7604
|
+
).toLowerCase();
|
|
7605
|
+
const chattype = pickFirstNonEmpty(
|
|
7606
|
+
extractXmlTag(xml, "ChatType"),
|
|
7607
|
+
extractXmlTag(xml, "chattype")
|
|
7608
|
+
).toLowerCase();
|
|
7609
|
+
const chatid = pickFirstNonEmpty(
|
|
7610
|
+
extractXmlTag(xml, "ChatId"),
|
|
7611
|
+
extractXmlTag(xml, "chatid")
|
|
7612
|
+
);
|
|
7613
|
+
const msgid = pickFirstNonEmpty(
|
|
7614
|
+
extractXmlTag(xml, "MsgId"),
|
|
7615
|
+
extractXmlTag(xml, "msgid")
|
|
7616
|
+
);
|
|
7617
|
+
const webhookUrl = pickFirstNonEmpty(
|
|
7618
|
+
extractXmlTag(xml, "WebhookUrl"),
|
|
7619
|
+
extractXmlTag(xml, "response_url")
|
|
7620
|
+
);
|
|
7621
|
+
const fromBlock = extractXmlTag(xml, "From") ?? "";
|
|
7622
|
+
const userid = pickFirstNonEmpty(
|
|
7623
|
+
extractXmlTag(fromBlock, "UserId"),
|
|
7624
|
+
extractXmlTag(xml, "FromUserName"),
|
|
7625
|
+
extractXmlTag(xml, "UserId")
|
|
7626
|
+
);
|
|
7627
|
+
const result = {
|
|
7628
|
+
msgtype,
|
|
7629
|
+
chattype: chattype === "group" ? "group" : "single",
|
|
7630
|
+
chatid,
|
|
7631
|
+
msgid,
|
|
7632
|
+
from: { userid }
|
|
7633
|
+
};
|
|
7634
|
+
if (webhookUrl) {
|
|
7635
|
+
result.response_url = webhookUrl;
|
|
7636
|
+
}
|
|
7637
|
+
if (msgtype === "text") {
|
|
7638
|
+
result.text = parseXmlTextPayload(xml) ?? { content: "" };
|
|
7639
|
+
} else if (msgtype === "voice") {
|
|
7640
|
+
result.voice = parseXmlVoicePayload(xml) ?? { content: "" };
|
|
7641
|
+
} else if (msgtype === "image") {
|
|
7642
|
+
const image = parseXmlImagePayload(xml);
|
|
7643
|
+
if (image) result.image = image;
|
|
7644
|
+
} else if (msgtype === "file") {
|
|
7645
|
+
const file = parseXmlFilePayload(xml);
|
|
7646
|
+
if (file) result.file = file;
|
|
7647
|
+
} else if (msgtype === "stream") {
|
|
7648
|
+
const streamBlock = extractXmlTag(xml, "Stream") ?? "";
|
|
7649
|
+
const id = pickFirstNonEmpty(
|
|
7650
|
+
extractXmlTag(streamBlock, "Id"),
|
|
7651
|
+
extractXmlTag(xml, "StreamId"),
|
|
7652
|
+
extractXmlTag(xml, "Id")
|
|
7653
|
+
);
|
|
7654
|
+
result.stream = { id };
|
|
7655
|
+
} else if (msgtype === "event") {
|
|
7656
|
+
const eventBlock = extractXmlTag(xml, "Event") ?? "";
|
|
7657
|
+
const eventtype = pickFirstNonEmpty(
|
|
7658
|
+
extractXmlTag(eventBlock, "EventType"),
|
|
7659
|
+
extractXmlTag(xml, "EventType"),
|
|
7660
|
+
extractXmlTag(xml, "Event")
|
|
7661
|
+
);
|
|
7662
|
+
result.event = { eventtype };
|
|
7663
|
+
} else if (msgtype === "mixed") {
|
|
7664
|
+
const mixedItems = parseXmlMixedItems(xml);
|
|
7665
|
+
if (mixedItems.length > 0) {
|
|
7666
|
+
result.mixed = { msg_item: mixedItems };
|
|
7667
|
+
}
|
|
7668
|
+
const text = parseXmlTextPayload(xml);
|
|
7669
|
+
if (text) {
|
|
7670
|
+
result.text = text;
|
|
7671
|
+
}
|
|
7430
7672
|
}
|
|
7431
|
-
return
|
|
7673
|
+
return result;
|
|
7432
7674
|
}
|
|
7433
7675
|
async function waitForStreamContent(streamId, maxWaitMs) {
|
|
7434
7676
|
const startedAt = Date.now();
|
|
@@ -7609,14 +7851,17 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7609
7851
|
return true;
|
|
7610
7852
|
}
|
|
7611
7853
|
const target2 = targets.find((candidate) => {
|
|
7612
|
-
if (!candidate.account.configured || !candidate.account.token)
|
|
7613
|
-
|
|
7854
|
+
if (!candidate.account.configured || !candidate.account.token) {
|
|
7855
|
+
return false;
|
|
7856
|
+
}
|
|
7857
|
+
const ok = verifyWecomSignature({
|
|
7614
7858
|
token: candidate.account.token,
|
|
7615
7859
|
timestamp,
|
|
7616
7860
|
nonce,
|
|
7617
7861
|
encrypt: echostr,
|
|
7618
7862
|
signature
|
|
7619
7863
|
});
|
|
7864
|
+
return ok;
|
|
7620
7865
|
});
|
|
7621
7866
|
if (!target2 || !target2.account.encodingAESKey) {
|
|
7622
7867
|
res.statusCode = 401;
|
|
@@ -7652,7 +7897,7 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7652
7897
|
res.end("missing query params");
|
|
7653
7898
|
return true;
|
|
7654
7899
|
}
|
|
7655
|
-
const body = await
|
|
7900
|
+
const body = await readRequestBody(req, 1024 * 1024);
|
|
7656
7901
|
if (!body.ok) {
|
|
7657
7902
|
res.statusCode = body.error === "payload too large" ? 413 : 400;
|
|
7658
7903
|
res.end(body.error ?? "invalid payload");
|
|
@@ -7666,14 +7911,17 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7666
7911
|
return true;
|
|
7667
7912
|
}
|
|
7668
7913
|
const target = targets.find((candidate) => {
|
|
7669
|
-
if (!candidate.account.token)
|
|
7670
|
-
|
|
7914
|
+
if (!candidate.account.token) {
|
|
7915
|
+
return false;
|
|
7916
|
+
}
|
|
7917
|
+
const ok = verifyWecomSignature({
|
|
7671
7918
|
token: candidate.account.token,
|
|
7672
7919
|
timestamp,
|
|
7673
7920
|
nonce,
|
|
7674
7921
|
encrypt,
|
|
7675
7922
|
signature
|
|
7676
7923
|
});
|
|
7924
|
+
return ok;
|
|
7677
7925
|
});
|
|
7678
7926
|
if (!target) {
|
|
7679
7927
|
res.statusCode = 401;
|
|
@@ -7711,15 +7959,13 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7711
7959
|
finished: true,
|
|
7712
7960
|
content: ""
|
|
7713
7961
|
});
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
})
|
|
7722
|
-
);
|
|
7962
|
+
const encReply2 = buildEncryptedJsonReply({
|
|
7963
|
+
account: target.account,
|
|
7964
|
+
plaintextJson: reply,
|
|
7965
|
+
nonce,
|
|
7966
|
+
timestamp
|
|
7967
|
+
});
|
|
7968
|
+
jsonOk(res, encReply2);
|
|
7723
7969
|
return true;
|
|
7724
7970
|
}
|
|
7725
7971
|
if (msgid && msgidToStreamId.has(msgid)) {
|
|
@@ -7867,15 +8113,13 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7867
8113
|
await waitForStreamContent(streamId, INITIAL_STREAM_WAIT_MS);
|
|
7868
8114
|
const state = streams.get(streamId);
|
|
7869
8115
|
const initialReply = state && (state.content.trim() || state.error) ? buildStreamReplyFromState(state) : buildStreamPlaceholderReply(streamId);
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
})
|
|
7878
|
-
);
|
|
8116
|
+
const encReply = buildEncryptedJsonReply({
|
|
8117
|
+
account: target.account,
|
|
8118
|
+
plaintextJson: initialReply,
|
|
8119
|
+
nonce,
|
|
8120
|
+
timestamp
|
|
8121
|
+
});
|
|
8122
|
+
jsonOk(res, encReply);
|
|
7879
8123
|
return true;
|
|
7880
8124
|
}
|
|
7881
8125
|
|