@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 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
- async function readJsonBody(req, maxBytes) {
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
- resolve3({ ok: true, value: JSON.parse(raw) });
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 parsed = JSON.parse(raw);
7428
- if (!parsed || typeof parsed !== "object") {
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 parsed;
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) return false;
7613
- return verifyWecomSignature({
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 readJsonBody(req, 1024 * 1024);
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) return false;
7670
- return verifyWecomSignature({
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
- jsonOk(
7715
- res,
7716
- buildEncryptedJsonReply({
7717
- account: target.account,
7718
- plaintextJson: reply,
7719
- nonce,
7720
- timestamp
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
- jsonOk(
7871
- res,
7872
- buildEncryptedJsonReply({
7873
- account: target.account,
7874
- plaintextJson: initialReply,
7875
- nonce,
7876
- timestamp
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