@xmanrui/dsh-im 4.5.0 → 4.7.0

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 (36) hide show
  1. package/README.en.md +2 -0
  2. package/README.md +2 -0
  3. package/lib/client.js +4 -1
  4. package/lib/index.js +249 -240
  5. package/package.json +1 -1
  6. package/src/channels/dingtalk/dingtalk-bridge.mjs +167 -20
  7. package/src/channels/dingtalk/dingtalk-card-stream.mjs +9 -2
  8. package/src/channels/dingtalk/state-store.mjs +98 -0
  9. package/src/channels/discord/discord-api.mjs +7 -0
  10. package/src/channels/discord/discord-runtime.mjs +97 -2
  11. package/src/channels/feishu/bridge.mjs +44 -13
  12. package/src/channels/feishu/feishu-cards.mjs +2 -0
  13. package/src/channels/feishu/message-utils.mjs +229 -0
  14. package/src/channels/qq/qq-bridge.mjs +49 -6
  15. package/src/channels/shared/batch-input.mjs +3 -3
  16. package/src/channels/shared/harness-client.mjs +82 -30
  17. package/src/channels/shared/i18n-en/feishu.mjs +2 -2
  18. package/src/channels/shared/i18n-en/shared-a.mjs +2 -0
  19. package/src/channels/shared/i18n-en/shared-b.mjs +2 -2
  20. package/src/channels/shared/i18n-en/shared-c.mjs +6 -4
  21. package/src/channels/shared/image-prompt.mjs +51 -0
  22. package/src/channels/shared/semantic/reply-reference.mjs +153 -0
  23. package/src/channels/shared/session-reply-recovery.mjs +104 -0
  24. package/src/channels/shared/session-title.mjs +1 -1
  25. package/src/channels/shared/text-harness-bridge.mjs +13 -6
  26. package/src/channels/shared/workspace-command.mjs +22 -3
  27. package/src/channels/slack/manifest.mjs +3 -0
  28. package/src/channels/slack/slack-api.mjs +18 -0
  29. package/src/channels/slack/slack-runtime.mjs +56 -0
  30. package/src/channels/telegram/telegram-runtime.mjs +117 -2
  31. package/src/channels/wecom/wecom-bridge.mjs +50 -7
  32. package/src/channels/weixin/state-store.mjs +110 -0
  33. package/src/channels/weixin/weixin-api.mjs +86 -2
  34. package/src/channels/weixin/weixin-bridge.mjs +97 -9
  35. package/src/channels/weixin/weixin-runtime.mjs +26 -6
  36. package/src/channels/whatsapp/whatsapp-runtime.mjs +56 -0
@@ -21,6 +21,9 @@ const ILINK_CLIENT_VERSION = (2 << 16) | (4 << 8) | 6;
21
21
  const DEFAULT_TIMEOUT_MS = 15_000;
22
22
  const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
23
23
  const WEIXIN_CDN_UPLOAD_RETRIES = 3;
24
+ const WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT = 22n;
25
+ const WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
26
+ const WEIXIN_MESSAGE_ID_MAX_FUTURE_MS = 24 * 60 * 60 * 1_000;
24
27
  const LOGIN_STATUSES = new Set([
25
28
  'wait',
26
29
  'scaned',
@@ -689,6 +692,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
689
692
  const recipient = nonEmptyString(toUserId);
690
693
  const content = nonEmptyString(text);
691
694
  if (!recipient || !content) throw new TypeError('toUserId and text are required');
695
+ const clientId = `dsh-weixin-${randomUUID()}`;
692
696
  const response = await requestJson(fetchImpl, {
693
697
  method: 'POST',
694
698
  baseUrl,
@@ -699,7 +703,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
699
703
  msg: {
700
704
  from_user_id: '',
701
705
  to_user_id: recipient,
702
- client_id: `dsh-weixin-${randomUUID()}`,
706
+ client_id: clientId,
703
707
  message_type: 2,
704
708
  message_state: 2,
705
709
  item_list: [{ type: 1, text_item: { text: content } }],
@@ -717,7 +721,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
717
721
  { providerCode: sendRejection },
718
722
  );
719
723
  }
720
- return true;
724
+ return {
725
+ ...(response && typeof response === 'object' ? response : {}),
726
+ providerMessageIds: [clientId],
727
+ };
721
728
  },
722
729
 
723
730
  async sendFile(request) {
@@ -791,6 +798,83 @@ export function extractWeixinText(message) {
791
798
  return null;
792
799
  }
793
800
 
801
+ function weixinReplyAttachment(item) {
802
+ if (item?.type === 2 || (item?.image_item && typeof item.image_item === 'object')) {
803
+ return { kind: 'image' };
804
+ }
805
+ if (item?.type === 3 || (item?.voice_item && typeof item.voice_item === 'object')) {
806
+ return { kind: 'audio' };
807
+ }
808
+ if (item?.type === 4 || (item?.file_item && typeof item.file_item === 'object')) {
809
+ const name = nonEmptyString(item?.file_item?.file_name);
810
+ return { kind: 'file', ...(name ? { name } : {}) };
811
+ }
812
+ if (item?.type === 5 || (item?.video_item && typeof item.video_item === 'object')) {
813
+ return { kind: 'video' };
814
+ }
815
+ return null;
816
+ }
817
+
818
+ function weixinQuotedText(item) {
819
+ const text = nonEmptyString(item?.text_item?.text);
820
+ if (text) return text;
821
+ return nonEmptyString(item?.voice_item?.text);
822
+ }
823
+
824
+ /** Extract the one-level reply snapshot embedded in an inbound iLink message item. */
825
+ export function extractWeixinReplyReference(message, { resolveContent, loadContent } = {}) {
826
+ const container = (message?.item_list ?? []).find((item) => (
827
+ item?.ref_msg && typeof item.ref_msg === 'object'
828
+ ));
829
+ if (!container) return null;
830
+ const ref = container.ref_msg;
831
+ const quotedItem = ref.message_item && typeof ref.message_item === 'object'
832
+ ? ref.message_item
833
+ : null;
834
+ const quotedText = weixinQuotedText(quotedItem);
835
+ let content = quotedText ?? nonEmptyString(ref.title);
836
+ const attachment = weixinReplyAttachment(quotedItem);
837
+ const messageId = quotedItem?.msg_id === undefined || quotedItem?.msg_id === null
838
+ ? null
839
+ : nonEmptyString(String(quotedItem.msg_id));
840
+ const referenceDetails = {
841
+ messageId,
842
+ createTimeMs: quotedItem?.create_time_ms ?? ref.create_time_ms,
843
+ updateTimeMs: quotedItem?.update_time_ms ?? ref.update_time_ms,
844
+ };
845
+ if (!content && !attachment && typeof resolveContent === 'function') {
846
+ content = nonEmptyString(resolveContent(referenceDetails));
847
+ }
848
+ const load = !content && !attachment && typeof loadContent === 'function'
849
+ ? (options) => loadContent(referenceDetails, options)
850
+ : null;
851
+ const knownType = quotedItem && [1, 2, 3, 4, 5, 8].includes(quotedItem.type);
852
+ return {
853
+ ...(messageId ? { messageId } : {}),
854
+ ...(content ? { content } : {}),
855
+ ...(attachment ? { attachments: [attachment] } : {}),
856
+ ...(load ? { load } : {}),
857
+ ...(!content && !attachment && !load
858
+ ? { unavailableReason: quotedItem && !knownType ? 'unsupported' : 'not-delivered' }
859
+ : {}),
860
+ };
861
+ }
862
+
863
+ /** Decode the millisecond timestamp carried by current 64-bit iLink message IDs. */
864
+ export function weixinMessageTimestampMs(messageId, { now = Date.now() } = {}) {
865
+ const value = messageId === undefined || messageId === null ? '' : String(messageId).trim();
866
+ if (!/^\d{16,20}$/u.test(value)) return null;
867
+ try {
868
+ const timestampMs = Number(BigInt(value) >> WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT);
869
+ if (!Number.isSafeInteger(timestampMs)
870
+ || timestampMs < WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS
871
+ || timestampMs > now + WEIXIN_MESSAGE_ID_MAX_FUTURE_MS) return null;
872
+ return timestampMs;
873
+ } catch {
874
+ return null;
875
+ }
876
+ }
877
+
794
878
  export function weixinMessageId(message) {
795
879
  if (message?.message_id !== undefined && message.message_id !== null) {
796
880
  return String(message.message_id);
@@ -2,9 +2,11 @@ import {
2
2
  DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
3
3
  extractWeixinFiles,
4
4
  extractWeixinImages,
5
+ extractWeixinReplyReference,
5
6
  extractWeixinText,
6
7
  splitWeixinText,
7
8
  weixinMessageId,
9
+ weixinMessageTimestampMs,
8
10
  } from './weixin-api.mjs';
9
11
  import {
10
12
  harnessAnswerForQuestion,
@@ -38,7 +40,6 @@ import {
38
40
  hasInboundImages,
39
41
  imagePromptDiagnostic,
40
42
  imagePromptUserMessage,
41
- promptContentForMessage,
42
43
  } from '../shared/image-prompt.mjs';
43
44
  import {
44
45
  hasInboundFiles,
@@ -47,10 +48,15 @@ import {
47
48
  } from '../shared/inbound-file.mjs';
48
49
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
49
50
  import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
51
+ import {
52
+ hasReplyReference,
53
+ promptContentForInboundMessage,
54
+ } from '../shared/semantic/reply-reference.mjs';
50
55
  import {
51
56
  createDeliveryReceipt,
52
57
  providerMessageIdsFor,
53
58
  } from '../shared/semantic/delivery.mjs';
59
+ import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
54
60
  import {
55
61
  channelDeliveryFailure,
56
62
  clearLastMessageFailure,
@@ -67,6 +73,10 @@ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处
67
73
  const DEFAULT_TYPING_KEEPALIVE_MS = 5_000;
68
74
  const TYPING_RETRY_DELAY_MS = 60_000;
69
75
  const WEIXIN_SEND_DIAGNOSTIC = Symbol('weixin-send-diagnostic');
76
+ const WEIXIN_REPLY_HISTORY_PAGE_SIZE = 100;
77
+ const WEIXIN_REPLY_HISTORY_MAX_PAGES = 3;
78
+ const WEIXIN_REPLY_HISTORY_TIMEOUT_MS = 5_000;
79
+ const WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS = 15_000;
70
80
 
71
81
  const HELP_TEXT = () => [
72
82
  t('微信已连接 DeepSeek Harness。'),
@@ -78,6 +88,7 @@ const HELP_TEXT = () => [
78
88
  t('/workspace 工作区序号或绝对路径 切换工作区'),
79
89
  t('/workspacelist 列出工作区绝对路径'),
80
90
  t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
91
+ t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
81
92
  t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
82
93
  t('/models 按序号列出所有可用模型'),
83
94
  t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
@@ -182,7 +193,20 @@ function weixinSendFailureOptions(error) {
182
193
  return undefined;
183
194
  }
184
195
 
185
- export function weixinInboundMessage(message, api) {
196
+ export function weixinInboundMessage(message, api, state, { loadReplyContent } = {}) {
197
+ const toUserId = nonEmptyString(message?.from_user_id);
198
+ const replyTo = extractWeixinReplyReference(message, {
199
+ resolveContent: (reference) => state?.recentOutboundTextFor?.({
200
+ toUserId,
201
+ ...reference,
202
+ }),
203
+ ...(typeof loadReplyContent === 'function' ? {
204
+ loadContent: (reference, options) => loadReplyContent({
205
+ toUserId,
206
+ ...reference,
207
+ }, options),
208
+ } : {}),
209
+ });
186
210
  return {
187
211
  content: extractWeixinText(message) ?? '',
188
212
  images: typeof api?.inboundImages === 'function'
@@ -191,6 +215,7 @@ export function weixinInboundMessage(message, api) {
191
215
  files: typeof api?.inboundFiles === 'function'
192
216
  ? api.inboundFiles(message)
193
217
  : extractWeixinFiles(message),
218
+ ...(replyTo ? { replyTo } : {}),
194
219
  };
195
220
  }
196
221
 
@@ -375,7 +400,8 @@ export class WeixinHarnessBridge {
375
400
  && (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
376
401
  ? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
377
402
  : this.#batchInputs.handle(key, commandText, {
378
- plainText: isNativeWeixinText(message),
403
+ plainText: isNativeWeixinText(message)
404
+ && !extractWeixinReplyReference(message),
379
405
  });
380
406
  if (result.handled) {
381
407
  if (result.kind === 'submit') {
@@ -489,7 +515,7 @@ export class WeixinHarnessBridge {
489
515
  batchSubmission = null,
490
516
  } = {}) {
491
517
  const preparedMessage = prefetchInboundFiles(
492
- weixinInboundMessage(message, this.#api),
518
+ this.#inboundMessage(message),
493
519
  { signal: this.#signal },
494
520
  );
495
521
  const previous = this.#queues.get(key) ?? Promise.resolve();
@@ -508,6 +534,54 @@ export class WeixinHarnessBridge {
508
534
  return current;
509
535
  }
510
536
 
537
+ #inboundMessage(message) {
538
+ return weixinInboundMessage(message, this.#api, this.#state, {
539
+ loadReplyContent: (reference, options) => this.#loadReplyContent(reference, options),
540
+ });
541
+ }
542
+
543
+ async #loadReplyContent(reference, { signal: callerSignal } = {}) {
544
+ const indexed = this.#state.recentOutboundTextFor?.(reference);
545
+ if (indexed) return { content: indexed };
546
+ const quotedAt = [
547
+ weixinMessageTimestampMs(reference?.messageId),
548
+ Number(reference?.createTimeMs),
549
+ Number(reference?.updateTimeMs),
550
+ ].find(Number.isSafeInteger);
551
+ if (quotedAt === undefined) return { unavailableReason: 'not-delivered' };
552
+ const sender = nonEmptyString(reference?.toUserId);
553
+ const sessionId = sender ? this.#state.sessionFor(conversationKey(sender)) : null;
554
+ const session = typeof sessionId === 'string' && sessionId
555
+ ? this.#harness.workspaceSession?.(sessionId)
556
+ : null;
557
+ if (typeof session?.readHistory !== 'function') {
558
+ return { unavailableReason: 'not-delivered' };
559
+ }
560
+ const text = await recoverAssistantTextByTimestamp({
561
+ session,
562
+ quotedAt,
563
+ signal: callerSignal,
564
+ pageSize: WEIXIN_REPLY_HISTORY_PAGE_SIZE,
565
+ maxPages: WEIXIN_REPLY_HISTORY_MAX_PAGES,
566
+ timeoutMs: WEIXIN_REPLY_HISTORY_TIMEOUT_MS,
567
+ toleranceMs: WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS,
568
+ });
569
+ if (!text) return { unavailableReason: 'not-delivered' };
570
+ const messageId = nonEmptyString(reference?.messageId);
571
+ try {
572
+ await this.#state.rememberOutboundMessage?.({
573
+ toUserId: sender,
574
+ text,
575
+ sentAt: quotedAt,
576
+ completedAt: quotedAt,
577
+ providerMessageIds: messageId ? [messageId] : [],
578
+ });
579
+ } catch (error) {
580
+ this.#logger.warn?.('[dsh-weixin] failed to remember a recovered quote:', error);
581
+ }
582
+ return { content: text };
583
+ }
584
+
511
585
  #finishBatchResult(message, messageId, key, sender, contextToken, runId, result) {
512
586
  let task;
513
587
  task = Promise.resolve().then(async () => {
@@ -644,11 +718,12 @@ export class WeixinHarnessBridge {
644
718
  let batchSettled = batchSubmission === null;
645
719
  let promptRecorded = false;
646
720
  try {
647
- const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
721
+ const promptMessage = preparedMessage ?? this.#inboundMessage(message);
648
722
  const text = promptMessage.content;
649
723
  const hasImages = hasInboundImages(promptMessage);
650
724
  const hasFiles = hasInboundFiles(promptMessage);
651
- if (!text && !hasImages && !hasFiles) {
725
+ const hasReply = hasReplyReference(promptMessage);
726
+ if (!text && !hasImages && !hasFiles && !hasReply) {
652
727
  await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
653
728
  await this.#state.markSeen(messageId);
654
729
  return;
@@ -701,8 +776,8 @@ export class WeixinHarnessBridge {
701
776
  let artifacts = [];
702
777
  await this.#startTyping(sender, contextToken);
703
778
  try {
704
- let content = hasImages
705
- ? await promptContentForMessage(promptMessage, { signal: this.#signal })
779
+ let content = hasImages || hasReply
780
+ ? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
706
781
  : undefined;
707
782
  const snapshot = this.#acceptedMessageIds.get(messageId);
708
783
  let contextEnhanced = false;
@@ -1154,6 +1229,7 @@ export class WeixinHarnessBridge {
1154
1229
  const chunks = splitWeixinText(text, this.#maxMessageChars);
1155
1230
  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
1156
1231
  const chunk = chunks[chunkIndex];
1232
+ const sentAt = Date.now();
1157
1233
  try {
1158
1234
  const result = await this.#api.sendText({
1159
1235
  baseUrl: this.#baseUrl,
@@ -1164,7 +1240,19 @@ export class WeixinHarnessBridge {
1164
1240
  runId,
1165
1241
  signal: this.#signal,
1166
1242
  });
1167
- providerMessageIds.push(...providerMessageIdsFor(result));
1243
+ const chunkMessageIds = providerMessageIdsFor(result);
1244
+ providerMessageIds.push(...chunkMessageIds);
1245
+ try {
1246
+ await this.#state.rememberOutboundMessage?.({
1247
+ toUserId,
1248
+ text: chunk,
1249
+ sentAt,
1250
+ completedAt: Date.now(),
1251
+ providerMessageIds: chunkMessageIds,
1252
+ });
1253
+ } catch (error) {
1254
+ this.#logger.warn?.('[dsh-weixin] failed to remember an outbound message:', error);
1255
+ }
1168
1256
  } catch (error) {
1169
1257
  throw weixinSendError(error, {
1170
1258
  baseUrl: this.#baseUrl,
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_WEIXIN_MAX_MESSAGE_CHARS, WeixinApiError } from './weixin-api.mjs';
2
2
  import { createWeixinBridgeStatus, WeixinHarnessBridge } from './weixin-bridge.mjs';
3
+ import { providerMessageIdsFor } from '../shared/semantic/delivery.mjs';
3
4
  import {
4
5
  connectionTestTarget,
5
6
  connectionTestTargetUnavailable,
@@ -324,9 +325,7 @@ export class WeixinRuntime {
324
325
  if (!this.#status.ready || !this.#abortController) {
325
326
  throw new Error('Weixin runtime is not connected');
326
327
  }
327
- await this.#api.sendText({
328
- baseUrl: this.#config.baseUrl,
329
- token: this.#token,
328
+ await this.#sendTrackedText({
330
329
  toUserId,
331
330
  text,
332
331
  signal: this.#abortController.signal,
@@ -334,6 +333,29 @@ export class WeixinRuntime {
334
333
  return { sent: true };
335
334
  }
336
335
 
336
+ async #sendTrackedText({ toUserId, text, signal }) {
337
+ const sentAt = Date.now();
338
+ const result = await this.#api.sendText({
339
+ baseUrl: this.#config.baseUrl,
340
+ token: this.#token,
341
+ toUserId,
342
+ text,
343
+ signal,
344
+ });
345
+ try {
346
+ await this.#state.rememberOutboundMessage?.({
347
+ toUserId,
348
+ text,
349
+ sentAt,
350
+ completedAt: Date.now(),
351
+ providerMessageIds: providerMessageIdsFor(result),
352
+ });
353
+ } catch (error) {
354
+ this.#logger.warn?.('[dsh-weixin] failed to remember an outbound message:', error);
355
+ }
356
+ return result;
357
+ }
358
+
337
359
  async sendProactiveText(target, text, { signal } = {}) {
338
360
  const toUserId = typeof target?.route?.toUserId === 'string'
339
361
  ? target.route.toUserId.trim() : '';
@@ -348,9 +370,7 @@ export class WeixinRuntime {
348
370
  throw error;
349
371
  }
350
372
  signal?.throwIfAborted();
351
- await this.#api.sendText({
352
- baseUrl: this.#config.baseUrl,
353
- token: this.#token,
373
+ await this.#sendTrackedText({
354
374
  toUserId,
355
375
  text,
356
376
  signal: signal ?? this.#abortController.signal,
@@ -96,6 +96,60 @@ function messageText(content) {
96
96
  ?? '';
97
97
  }
98
98
 
99
+ function whatsappReplyAttachment(kind, media, fallbackName) {
100
+ if (!media || typeof media !== 'object') return null;
101
+ const name = typeof media.fileName === 'string' && media.fileName
102
+ ? media.fileName : typeof fallbackName === 'string' && fallbackName
103
+ ? fallbackName : undefined;
104
+ return { kind, ...(name ? { name } : {}) };
105
+ }
106
+
107
+ function whatsappReplyAttachments(content) {
108
+ const attachments = [];
109
+ if (content?.imageMessage) {
110
+ attachments.push(whatsappReplyAttachment('image', content.imageMessage));
111
+ }
112
+ if (content?.documentMessage) {
113
+ const mediaType = typeof content.documentMessage.mimetype === 'string'
114
+ ? content.documentMessage.mimetype.toLowerCase() : '';
115
+ attachments.push(whatsappReplyAttachment(
116
+ mediaType.startsWith('image/') ? 'image' : 'file',
117
+ content.documentMessage,
118
+ ));
119
+ }
120
+ if (content?.audioMessage) {
121
+ attachments.push(whatsappReplyAttachment('audio', content.audioMessage));
122
+ }
123
+ if (content?.videoMessage) {
124
+ attachments.push(whatsappReplyAttachment('video', content.videoMessage));
125
+ }
126
+ if (content?.stickerMessage) {
127
+ const mediaType = typeof content.stickerMessage.mimetype === 'string'
128
+ ? content.stickerMessage.mimetype.toLowerCase() : '';
129
+ attachments.push(whatsappReplyAttachment(
130
+ mediaType.startsWith('video/') || content.stickerMessage.isAnimated === true
131
+ ? 'video' : 'image',
132
+ content.stickerMessage,
133
+ ));
134
+ }
135
+ return attachments.filter(Boolean);
136
+ }
137
+
138
+ function whatsappReplyReference(context) {
139
+ if (!context?.quotedMessage || typeof context.quotedMessage !== 'object') return undefined;
140
+ const content = normalizeMessageContent(context.quotedMessage);
141
+ const messageId = typeof context.stanzaId === 'string' && context.stanzaId
142
+ ? context.stanzaId : undefined;
143
+ const authorId = typeof context.participant === 'string' && context.participant
144
+ ? context.participant : undefined;
145
+ return {
146
+ ...(messageId ? { messageId } : {}),
147
+ ...(authorId ? { authorId } : {}),
148
+ content: messageText(content),
149
+ attachments: whatsappReplyAttachments(content),
150
+ };
151
+ }
152
+
99
153
  function mediaSize(value) {
100
154
  if (Number.isSafeInteger(value) && value >= 0) return value;
101
155
  let converted;
@@ -260,6 +314,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
260
314
  && areJidsSameUser(context.participant, accountJid);
261
315
  const image = whatsappImageSource(message, content, download, { viewOnce });
262
316
  const file = whatsappFileSource(message, content, download);
317
+ const replyTo = whatsappReplyReference(context);
263
318
  return {
264
319
  messageId: `${remoteJid}:${messageId}`,
265
320
  providerMessageId: messageId,
@@ -274,6 +329,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
274
329
  || typeof content?.extendedTextMessage?.text === 'string',
275
330
  images: image ? [image] : [],
276
331
  files: file ? [file] : [],
332
+ ...(replyTo ? { replyTo } : {}),
277
333
  addressed: !group || fromMe || mentioned || replyToSelf,
278
334
  selfChat,
279
335
  replyTarget: { jid: remoteJid, quoted: message, selfChat },