@xmanrui/dsh-im 4.38.0 → 4.38.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.
Files changed (32) hide show
  1. package/PROACTIVE_DELIVERY.en.md +18 -2
  2. package/PROACTIVE_DELIVERY.md +18 -2
  3. package/README.en.md +2 -0
  4. package/README.md +2 -0
  5. package/lib/client.js +1 -1
  6. package/lib/index.js +321 -316
  7. package/locale/en.json +1 -1
  8. package/locale/zh.json +1 -1
  9. package/package.json +1 -1
  10. package/plugin-src/host/delivery-adapter.mjs +9 -1
  11. package/plugin-src/host/delivery-service.mjs +4 -3
  12. package/plugin-src/host/session-sync-coordinator.mjs +9 -5
  13. package/src/channels/dingtalk/dingtalk-api.mjs +41 -24
  14. package/src/channels/dingtalk/dingtalk-runtime.mjs +3 -2
  15. package/src/channels/discord/discord-runtime.mjs +13 -2
  16. package/src/channels/feishu/bridge.mjs +8 -1
  17. package/src/channels/matrix/matrix-runtime.mjs +16 -6
  18. package/src/channels/qq/markdown-reply.mjs +7 -340
  19. package/src/channels/qq/qq-runtime.mjs +11 -3
  20. package/src/channels/shared/harness-client.mjs +15 -3
  21. package/src/channels/shared/i18n-en/wecom.mjs +3 -0
  22. package/src/channels/shared/markdown-chunks.mjs +338 -0
  23. package/src/channels/shared/message-failure.mjs +1 -1
  24. package/src/channels/shared/text-harness-bridge.mjs +4 -1
  25. package/src/channels/slack/slack-api.mjs +2 -1
  26. package/src/channels/slack/slack-runtime.mjs +30 -0
  27. package/src/channels/telegram/telegram-runtime.mjs +1 -0
  28. package/src/channels/wecom/wecom-bridge.mjs +45 -10
  29. package/src/channels/wecom/wecom-runtime.mjs +11 -5
  30. package/src/channels/weixin/weixin-runtime.mjs +17 -10
  31. package/src/channels/whatsapp/whatsapp-markdown.mjs +59 -0
  32. package/src/channels/whatsapp/whatsapp-runtime.mjs +26 -1
@@ -0,0 +1,59 @@
1
+ // A conservative projection of common Markdown into WhatsApp's text dialect.
2
+ // Unsupported structures stay literal; code and link destinations are protected.
3
+ function inline(text) {
4
+ const saved = [];
5
+ let marker = '\u0000';
6
+ while (text.includes(marker)) marker += '\u0000';
7
+ const stash = value => `${marker}${saved.push(value) - 1}${marker}`;
8
+ let value = text.replace(/(`+)([^\n]*?)\1/g, (_match, ticks, code) => (
9
+ // WhatsApp cannot represent embedded backticks in inline code safely.
10
+ stash(code.includes('`') ? `${ticks}${code}${ticks}` : `\`${code}\``)
11
+ ));
12
+ value = value.replace(/\\([\\`*_{}[\]()#+.!~>-])/g, match => stash(match));
13
+ value = value.replace(/!?\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"\n]*")?\)/g,
14
+ (_match, label, url) => stash(`${label} (${url})`));
15
+ value = value.replace(/https?:\/\/[^\s<>]+/g, url => stash(url));
16
+ // Leave nested delimiters untouched instead of guessing their meaning.
17
+ value = value.replace(/(?<![*_])(\*\*|__)([^*_\n]+)\1(?![*_])/g,
18
+ (_match, _marker, body) => stash(`*${body}*`));
19
+ value = value.replace(/(?<![\w*_])\*([^*\n]+)\*(?![*_])/g, (_match, body) => `_${body}_`);
20
+ value = value.replace(/(?<!~)~~([^~\n]+)~~(?!~)/g, '~$1~');
21
+ const token = new RegExp(`${marker}(\\d+)${marker}`, 'g');
22
+ // A later protected URL can contain an earlier token; restore both passes.
23
+ for (let pass = 0; pass <= saved.length && value.includes(marker); pass++) {
24
+ value = value.replace(token, (match, index) => saved[Number(index)] ?? match);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ export function markdownToWhatsapp(text) {
30
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
31
+ const result = [];
32
+ let table = false;
33
+ for (let index = 0; index < lines.length; index++) {
34
+ const line = lines[index];
35
+ const open = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
36
+ if (open && !(open[1][0] === '`' && open[2].includes('`'))) {
37
+ let end = index + 1;
38
+ for (; end < lines.length; end++) {
39
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(lines[end]);
40
+ if (close && close[1][0] === open[1][0] && close[1].length >= open[1].length) break;
41
+ }
42
+ const body = lines.slice(index + 1, end);
43
+ // Embedded triple backticks cannot be represented by WhatsApp's fence.
44
+ // Preserve the entire original block instead of changing code boundaries.
45
+ if (body.some(value => value.includes('```'))) result.push(...lines.slice(index, end + 1));
46
+ else result.push('```', ...body, '```');
47
+ index = end;
48
+ table = false;
49
+ continue;
50
+ }
51
+ if (line.includes('|') && /^\s*\|?\s*:?-+:?\s*\|[\s|:-]*$/.test(lines[index + 1] ?? '')) table = true;
52
+ if (table && line.includes('|')) { result.push(line); continue; }
53
+ table = false;
54
+ const heading = /^ {0,3}#{1,6}\s+(.+?)(?:\s+#+)?\s*$/.exec(line);
55
+ // A heading with its own inline delimiters stays literal rather than nested.
56
+ result.push(heading && !/[*_`]/.test(heading[1]) ? `*${heading[1]}*` : inline(line));
57
+ }
58
+ return result.join('\n');
59
+ }
@@ -14,6 +14,9 @@ import { t } from '../shared/i18n.mjs';
14
14
  import { ImagePromptError } from '../shared/image-prompt.mjs';
15
15
  import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
16
16
  import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
17
+ import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
18
+ import { chunkMarkdownText } from '../shared/markdown-chunks.mjs';
19
+ import { markdownToWhatsapp } from './whatsapp-markdown.mjs';
17
20
  import {
18
21
  WHATSAPP_ACCESS_MODES,
19
22
  normalizeWhatsappAccountJid,
@@ -490,6 +493,25 @@ export class WhatsappBotClient {
490
493
  return { providerMessageIds };
491
494
  }
492
495
 
496
+ async sendDelivery(target, value) {
497
+ const block = createTextDeliveryBlock(value);
498
+ if (block.format === 'plain') return this.sendText(target, block.text);
499
+ let chunks;
500
+ try {
501
+ chunks = chunkMarkdownText(markdownToWhatsapp(block.text), WHATSAPP_TEXT_LIMIT);
502
+ } catch {
503
+ this.#logger.warn?.('[dsh-im:whatsapp] Markdown could not be prepared; sending plain text');
504
+ return this.sendText(target, block.text);
505
+ }
506
+ await this.#stopTyping(target.jid);
507
+ const providerMessageIds = [];
508
+ for (const [index, chunk] of chunks.entries()) {
509
+ const result = await this.#sendTextMessage(target, chunk, { quote: index === 0 });
510
+ providerMessageIds.push(result.key.id);
511
+ }
512
+ return { providerMessageIds };
513
+ }
514
+
493
515
  async #sendTextMessage(target, text, { edit, quote = true } = {}) {
494
516
  this.#signal?.throwIfAborted();
495
517
  const messageId = randomBytes(10).toString('hex').toUpperCase();
@@ -886,7 +908,7 @@ export class WhatsappRuntime {
886
908
  }, options);
887
909
  }
888
910
 
889
- async sendProactiveText(target, text, { signal } = {}) {
911
+ async sendProactiveText(target, text, { signal, format = 'plain' } = {}) {
890
912
  const jid = typeof target?.route?.jid === 'string' ? target.route.jid.trim() : '';
891
913
  const validUser = target?.kind === 'user'
892
914
  && /^[^@\s]+@(s\.whatsapp\.net|lid)$/.test(jid);
@@ -902,6 +924,9 @@ export class WhatsappRuntime {
902
924
  throw error;
903
925
  }
904
926
  signal?.throwIfAborted();
927
+ if (format === 'markdown') {
928
+ return this.#client.sendDelivery({ jid }, createTextDeliveryBlock(text, format));
929
+ }
905
930
  return this.#client.sendText({ jid }, text);
906
931
  }
907
932