@openclaw-china/wecom 0.1.28 → 2026.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +273 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6112,14 +6112,6 @@ async function configureQQBot(prompter, cfg) {
|
|
|
6112
6112
|
existingValue: toTrimmedString(existing.clientSecret),
|
|
6113
6113
|
required: true
|
|
6114
6114
|
});
|
|
6115
|
-
Ve(
|
|
6116
|
-
"QQ \u7684 Markdown \u4F53\u9A8C\u5F88\u597D\uFF0C\u4F46\u9700\u8981\u5148\u7533\u8BF7\u5F00\u901A\uFF0C\u8BE6\u60C5\u8BF7\u67E5\u770B\u914D\u7F6E\u6587\u6863\u3002",
|
|
6117
|
-
"\u63D0\u793A"
|
|
6118
|
-
);
|
|
6119
|
-
const markdownSupport = await prompter.askConfirm(
|
|
6120
|
-
"\u542F\u7528 Markdown \u652F\u6301",
|
|
6121
|
-
toBoolean(existing.markdownSupport, false)
|
|
6122
|
-
);
|
|
6123
6115
|
const asrEnabled = await prompter.askConfirm(
|
|
6124
6116
|
"\u542F\u7528 ASR\uFF08\u652F\u6301\u5165\u7AD9\u8BED\u97F3\u81EA\u52A8\u8F6C\u6587\u5B57\uFF09",
|
|
6125
6117
|
toBoolean(existingAsr.enabled, false)
|
|
@@ -6148,7 +6140,6 @@ async function configureQQBot(prompter, cfg) {
|
|
|
6148
6140
|
return mergeChannelConfig(cfg, "qqbot", {
|
|
6149
6141
|
appId,
|
|
6150
6142
|
clientSecret,
|
|
6151
|
-
markdownSupport,
|
|
6152
6143
|
asr
|
|
6153
6144
|
});
|
|
6154
6145
|
}
|
|
@@ -7344,7 +7335,156 @@ function jsonOk(res, body) {
|
|
|
7344
7335
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
7345
7336
|
res.end(JSON.stringify(body));
|
|
7346
7337
|
}
|
|
7347
|
-
|
|
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) {
|
|
7348
7488
|
const chunks = [];
|
|
7349
7489
|
let total = 0;
|
|
7350
7490
|
return await new Promise((resolve3) => {
|
|
@@ -7364,7 +7504,17 @@ async function readJsonBody(req, maxBytes) {
|
|
|
7364
7504
|
resolve3({ ok: false, error: "empty payload" });
|
|
7365
7505
|
return;
|
|
7366
7506
|
}
|
|
7367
|
-
|
|
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 });
|
|
7368
7518
|
} catch (err) {
|
|
7369
7519
|
resolve3({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
7370
7520
|
}
|
|
@@ -7433,11 +7583,94 @@ function createStreamId() {
|
|
|
7433
7583
|
return crypto.randomBytes(16).toString("hex");
|
|
7434
7584
|
}
|
|
7435
7585
|
function parseWecomPlainMessage(raw) {
|
|
7436
|
-
const
|
|
7437
|
-
if (
|
|
7438
|
-
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;
|
|
7439
7636
|
}
|
|
7440
|
-
|
|
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
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
return result;
|
|
7441
7674
|
}
|
|
7442
7675
|
async function waitForStreamContent(streamId, maxWaitMs) {
|
|
7443
7676
|
const startedAt = Date.now();
|
|
@@ -7618,14 +7851,17 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7618
7851
|
return true;
|
|
7619
7852
|
}
|
|
7620
7853
|
const target2 = targets.find((candidate) => {
|
|
7621
|
-
if (!candidate.account.configured || !candidate.account.token)
|
|
7622
|
-
|
|
7854
|
+
if (!candidate.account.configured || !candidate.account.token) {
|
|
7855
|
+
return false;
|
|
7856
|
+
}
|
|
7857
|
+
const ok = verifyWecomSignature({
|
|
7623
7858
|
token: candidate.account.token,
|
|
7624
7859
|
timestamp,
|
|
7625
7860
|
nonce,
|
|
7626
7861
|
encrypt: echostr,
|
|
7627
7862
|
signature
|
|
7628
7863
|
});
|
|
7864
|
+
return ok;
|
|
7629
7865
|
});
|
|
7630
7866
|
if (!target2 || !target2.account.encodingAESKey) {
|
|
7631
7867
|
res.statusCode = 401;
|
|
@@ -7661,7 +7897,7 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7661
7897
|
res.end("missing query params");
|
|
7662
7898
|
return true;
|
|
7663
7899
|
}
|
|
7664
|
-
const body = await
|
|
7900
|
+
const body = await readRequestBody(req, 1024 * 1024);
|
|
7665
7901
|
if (!body.ok) {
|
|
7666
7902
|
res.statusCode = body.error === "payload too large" ? 413 : 400;
|
|
7667
7903
|
res.end(body.error ?? "invalid payload");
|
|
@@ -7675,14 +7911,17 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7675
7911
|
return true;
|
|
7676
7912
|
}
|
|
7677
7913
|
const target = targets.find((candidate) => {
|
|
7678
|
-
if (!candidate.account.token)
|
|
7679
|
-
|
|
7914
|
+
if (!candidate.account.token) {
|
|
7915
|
+
return false;
|
|
7916
|
+
}
|
|
7917
|
+
const ok = verifyWecomSignature({
|
|
7680
7918
|
token: candidate.account.token,
|
|
7681
7919
|
timestamp,
|
|
7682
7920
|
nonce,
|
|
7683
7921
|
encrypt,
|
|
7684
7922
|
signature
|
|
7685
7923
|
});
|
|
7924
|
+
return ok;
|
|
7686
7925
|
});
|
|
7687
7926
|
if (!target) {
|
|
7688
7927
|
res.statusCode = 401;
|
|
@@ -7720,15 +7959,13 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7720
7959
|
finished: true,
|
|
7721
7960
|
content: ""
|
|
7722
7961
|
});
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
})
|
|
7731
|
-
);
|
|
7962
|
+
const encReply2 = buildEncryptedJsonReply({
|
|
7963
|
+
account: target.account,
|
|
7964
|
+
plaintextJson: reply,
|
|
7965
|
+
nonce,
|
|
7966
|
+
timestamp
|
|
7967
|
+
});
|
|
7968
|
+
jsonOk(res, encReply2);
|
|
7732
7969
|
return true;
|
|
7733
7970
|
}
|
|
7734
7971
|
if (msgid && msgidToStreamId.has(msgid)) {
|
|
@@ -7876,15 +8113,13 @@ async function handleWecomWebhookRequest(req, res) {
|
|
|
7876
8113
|
await waitForStreamContent(streamId, INITIAL_STREAM_WAIT_MS);
|
|
7877
8114
|
const state = streams.get(streamId);
|
|
7878
8115
|
const initialReply = state && (state.content.trim() || state.error) ? buildStreamReplyFromState(state) : buildStreamPlaceholderReply(streamId);
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
})
|
|
7887
|
-
);
|
|
8116
|
+
const encReply = buildEncryptedJsonReply({
|
|
8117
|
+
account: target.account,
|
|
8118
|
+
plaintextJson: initialReply,
|
|
8119
|
+
nonce,
|
|
8120
|
+
timestamp
|
|
8121
|
+
});
|
|
8122
|
+
jsonOk(res, encReply);
|
|
7888
8123
|
return true;
|
|
7889
8124
|
}
|
|
7890
8125
|
|