@xmanrui/dsh-im 1.2.0 → 1.3.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 (30) hide show
  1. package/lib/index.js +166 -163
  2. package/package.json +1 -1
  3. package/plugin-src/host/channels/dingtalk/production.mjs +3 -1
  4. package/plugin-src/host/channels/feishu/production.mjs +3 -1
  5. package/plugin-src/host/channels/qq/production.mjs +3 -1
  6. package/plugin-src/host/channels/shared/production.mjs +3 -1
  7. package/plugin-src/host/channels/slack/production.mjs +3 -1
  8. package/plugin-src/host/channels/wecom/production.mjs +3 -1
  9. package/plugin-src/host/channels/weixin/production.mjs +3 -1
  10. package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
  11. package/plugin-src/host/harness-session-coordinator.mjs +32 -5
  12. package/src/channels/dingtalk/dingtalk-api.mjs +93 -27
  13. package/src/channels/dingtalk/dingtalk-bridge.mjs +60 -13
  14. package/src/channels/discord/discord-runtime.mjs +23 -0
  15. package/src/channels/feishu/bridge.mjs +18 -10
  16. package/src/channels/feishu/message-utils.mjs +47 -0
  17. package/src/channels/qq/qq-bridge.mjs +80 -28
  18. package/src/channels/shared/file-download.mjs +64 -0
  19. package/src/channels/shared/harness-client.mjs +45 -0
  20. package/src/channels/shared/inbound-file.mjs +206 -0
  21. package/src/channels/shared/text-harness-bridge.mjs +31 -11
  22. package/src/channels/slack/slack-api.mjs +27 -4
  23. package/src/channels/slack/slack-runtime.mjs +55 -5
  24. package/src/channels/telegram/telegram-api.mjs +21 -6
  25. package/src/channels/telegram/telegram-runtime.mjs +24 -3
  26. package/src/channels/wecom/wecom-bridge.mjs +73 -10
  27. package/src/channels/weixin/weixin-api.mjs +45 -0
  28. package/src/channels/weixin/weixin-bridge.mjs +52 -18
  29. package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -2
  30. package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
@@ -26,6 +26,10 @@ import {
26
26
  imagePromptUserMessage,
27
27
  promptContentForMessage,
28
28
  } from '../shared/image-prompt.mjs';
29
+ import {
30
+ hasInboundFiles,
31
+ inboundFileUserMessage,
32
+ } from '../shared/inbound-file.mjs';
29
33
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
30
34
  import {
31
35
  materializeOutboundArtifact,
@@ -43,7 +47,7 @@ const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
43
47
  const HELP_TEXT = [
44
48
  '企业微信机器人已连接 DeepSeek Harness。',
45
49
  '',
46
- '直接发送文字或图片即可继续当前会话。',
50
+ '直接发送文字、图片或文件即可继续当前会话。',
47
51
  '/new 开启一个全新会话',
48
52
  '/compact 压缩当前会话的较早上下文',
49
53
  '/workspace 工作区绝对路径 切换工作区',
@@ -110,6 +114,13 @@ function imageContents(frame) {
110
114
  .map((item) => item.image);
111
115
  }
112
116
 
117
+ function fileContents(frame) {
118
+ const body = bodyOf(frame);
119
+ return body.msgtype === 'file' && body.file && typeof body.file === 'object'
120
+ ? [body.file]
121
+ : [];
122
+ }
123
+
113
124
  function imageSource(client, image) {
114
125
  const url = nonEmptyString(image?.url);
115
126
  if (!url) return null;
@@ -139,10 +150,56 @@ function imageSource(client, image) {
139
150
  };
140
151
  }
141
152
 
153
+ function fileSource(client, file) {
154
+ const url = nonEmptyString(file?.url);
155
+ if (!url) return null;
156
+ const aeskey = nonEmptyString(file?.aeskey) ?? undefined;
157
+ return {
158
+ name: nonEmptyString(file?.filename ?? file?.file_name ?? file?.name) ?? 'file',
159
+ async load({ signal } = {}) {
160
+ signal?.throwIfAborted();
161
+ if (typeof client?.downloadFile !== 'function') {
162
+ throw new Error('Enterprise WeChat file download is unavailable');
163
+ }
164
+ const result = await client.downloadFile(url, aeskey);
165
+ signal?.throwIfAborted();
166
+ const raw = result?.buffer ?? result?.data;
167
+ if (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)) {
168
+ throw new Error('Enterprise WeChat file download returned no data');
169
+ }
170
+ return {
171
+ data: Buffer.from(raw),
172
+ ...(nonEmptyString(result?.filename) ? { name: result.filename.trim() } : {}),
173
+ };
174
+ },
175
+ };
176
+ }
177
+
142
178
  export function wecomInboundMessage(frame, client) {
143
179
  return {
144
180
  content: messageText(frame),
145
181
  images: imageContents(frame).map((image) => imageSource(client, image)).filter(Boolean),
182
+ files: fileContents(frame).map((file) => fileSource(client, file)).filter(Boolean),
183
+ };
184
+ }
185
+
186
+ function prefetchInboundFiles(message, signal) {
187
+ if (!Array.isArray(message?.files) || message.files.length === 0) return message;
188
+ return {
189
+ ...message,
190
+ files: message.files.map((source) => {
191
+ const download = source.load({ signal });
192
+ download.catch(() => undefined);
193
+ return {
194
+ ...source,
195
+ async load({ signal: loadSignal } = {}) {
196
+ loadSignal?.throwIfAborted();
197
+ const result = await download;
198
+ loadSignal?.throwIfAborted();
199
+ return result;
200
+ },
201
+ };
202
+ }),
146
203
  };
147
204
  }
148
205
 
@@ -400,7 +457,7 @@ export class WecomHarnessBridge {
400
457
  const pending = this.#pendingInteractions.get(key);
401
458
  const commandMessage = wecomInboundMessage(frame, this.#client);
402
459
  const commandText = nonEmptyString(commandMessage.content) ?? '';
403
- const commandRunner = isControlCommand(commandText)
460
+ const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
404
461
  ? runControlCommand
405
462
  : (isModelCommand(commandText)
406
463
  ? runModelCommand
@@ -510,6 +567,7 @@ export class WecomHarnessBridge {
510
567
  preparedMessage = imageQueueFullMessage(inboundMessage);
511
568
  }
512
569
  }
570
+ preparedMessage = prefetchInboundFiles(preparedMessage, this.#signal);
513
571
  const previous = this.#queues.get(key) ?? Promise.resolve();
514
572
  const current = previous
515
573
  .catch(() => undefined)
@@ -543,6 +601,7 @@ export class WecomHarnessBridge {
543
601
  const result = await runner(message.content, this.#harness, this.#state, key, {
544
602
  signal: this.#signal,
545
603
  hasImages: hasInboundImages(message),
604
+ hasFiles: hasInboundFiles(message),
546
605
  pendingInteraction: this.#pendingInteractions.has(key)
547
606
  || this.#approvals.hasPending(key),
548
607
  control: { owner: this, key },
@@ -701,34 +760,35 @@ export class WecomHarnessBridge {
701
760
  const message = preparedMessage ?? wecomInboundMessage(frame, this.#client);
702
761
  const text = message.content;
703
762
  const hasImages = hasInboundImages(message);
763
+ const hasFiles = hasInboundFiles(message);
704
764
  const key = conversationKey(frame);
705
765
  let streamId = null;
706
766
  let streamStarted = false;
707
767
  try {
708
- if (!text && !hasImages) {
709
- await this.#sendImmediate(frame, chatId, '目前支持文字、图片和语音转写消息。');
768
+ if (!text && !hasImages && !hasFiles) {
769
+ await this.#sendImmediate(frame, chatId, '目前支持文字、图片、文件和语音转写消息。');
710
770
  await this.#state.markSeen(messageId);
711
771
  return;
712
772
  }
713
773
  const command = text.toLowerCase();
714
- if (!hasImages && command === '/help') {
774
+ if (!hasImages && !hasFiles && command === '/help') {
715
775
  await this.#sendImmediate(frame, chatId, HELP_TEXT);
716
776
  await this.#state.markSeen(messageId);
717
777
  return;
718
778
  }
719
- if (!hasImages && command === '/status') {
779
+ if (!hasImages && !hasFiles && command === '/status') {
720
780
  await this.#harness.ensureRunning({ signal: this.#signal });
721
781
  await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
722
782
  await this.#state.markSeen(messageId);
723
783
  return;
724
784
  }
725
- if (!hasImages && command === '/new') {
785
+ if (!hasImages && !hasFiles && command === '/new') {
726
786
  await this.#state.clearSession(key);
727
787
  await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
728
788
  await this.#state.markSeen(messageId);
729
789
  return;
730
790
  }
731
- const workspaceCommand = hasImages
791
+ const workspaceCommand = hasImages || hasFiles
732
792
  ? null
733
793
  : await runWorkspaceCommand(text, this.#harness, key);
734
794
  if (workspaceCommand) {
@@ -738,7 +798,7 @@ export class WecomHarnessBridge {
738
798
  await this.#state.markSeen(messageId);
739
799
  return;
740
800
  }
741
- const compactCommand = hasImages
801
+ const compactCommand = hasImages || hasFiles
742
802
  ? null
743
803
  : await runCompactCommand(
744
804
  text,
@@ -789,6 +849,7 @@ export class WecomHarnessBridge {
789
849
  requiresMention: body.chattype === 'group',
790
850
  }),
791
851
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
852
+ files: message.files,
792
853
  },
793
854
  });
794
855
 
@@ -862,7 +923,9 @@ export class WecomHarnessBridge {
862
923
  if (this.#signal?.aborted) return;
863
924
  this.#status.lastError = error?.message ?? String(error);
864
925
  this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
865
- const errorText = imagePromptUserMessage(error) ?? '消息处理失败,请稍后重试。';
926
+ const errorText = inboundFileUserMessage(error)
927
+ ?? imagePromptUserMessage(error)
928
+ ?? '消息处理失败,请稍后重试。';
866
929
  try {
867
930
  if (streamStarted && streamId) {
868
931
  await this.#client.replyStream(frame, streamId, errorText, true);
@@ -196,6 +196,47 @@ export function extractWeixinImages(message, { fetchImpl = fetch } = {}) {
196
196
  return images;
197
197
  }
198
198
 
199
+ async function fetchWeixinFileCiphertext(url, { fetchImpl, signal }) {
200
+ const response = await fetchImpl(new URL(url), {
201
+ method: 'GET',
202
+ signal,
203
+ redirect: 'manual',
204
+ });
205
+ if (!response?.ok) {
206
+ await response?.body?.cancel?.().catch?.(() => undefined);
207
+ throw new WeixinApiError(
208
+ 'file-download-failed',
209
+ `微信文件下载失败(HTTP ${response?.status ?? 'unknown'})。`,
210
+ { status: response?.status },
211
+ );
212
+ }
213
+ return Buffer.from(await response.arrayBuffer());
214
+ }
215
+
216
+ /** Convert native iLink file items into lazily downloaded, decrypted file references. */
217
+ export function extractWeixinFiles(message, { fetchImpl = fetch } = {}) {
218
+ if (typeof fetchImpl !== 'function') throw new TypeError('fetchImpl must be a function');
219
+ const files = [];
220
+ for (const item of message?.item_list ?? []) {
221
+ const fileItem = item?.file_item;
222
+ if (!fileItem || typeof fileItem !== 'object') continue;
223
+ const declaredSize = Number(fileItem.len);
224
+ files.push({
225
+ name: nonEmptyString(fileItem.file_name) ?? (files.length === 0 ? 'file' : `file-${files.length + 1}`),
226
+ ...(Number.isFinite(declaredSize) && declaredSize >= 0 ? { size: declaredSize } : {}),
227
+ load: async ({ signal } = {}) => {
228
+ signal?.throwIfAborted();
229
+ const key = parseWeixinImageAesKey(fileItem);
230
+ const url = weixinImageDownloadUrl(fileItem.media);
231
+ const ciphertext = await fetchWeixinFileCiphertext(url, { fetchImpl, signal });
232
+ signal?.throwIfAborted();
233
+ return decryptWeixinImage(ciphertext, key);
234
+ },
235
+ });
236
+ }
237
+ return files;
238
+ }
239
+
199
240
  function isWeixinHost(hostname) {
200
241
  const normalized = hostname.toLowerCase().replace(/\.$/, '');
201
242
  return normalized === 'weixin.qq.com' || normalized.endsWith('.weixin.qq.com');
@@ -421,6 +462,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
421
462
  return extractWeixinImages(message, { fetchImpl });
422
463
  },
423
464
 
465
+ inboundFiles(message) {
466
+ return extractWeixinFiles(message, { fetchImpl });
467
+ },
468
+
424
469
  async beginLogin({ localTokens = [], botType = DEFAULT_BOT_TYPE, signal } = {}) {
425
470
  const tokens = [...new Set(localTokens.map(nonEmptyString).filter(Boolean))].slice(-10);
426
471
  const response = await requestJson(fetchImpl, {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ extractWeixinFiles,
2
3
  extractWeixinImages,
3
4
  extractWeixinText,
4
5
  splitWeixinText,
@@ -31,6 +32,11 @@ import {
31
32
  imagePromptUserMessage,
32
33
  promptContentForMessage,
33
34
  } from '../shared/image-prompt.mjs';
35
+ import {
36
+ hasInboundFiles,
37
+ inboundFileUserMessage,
38
+ prefetchInboundFiles,
39
+ } from '../shared/inbound-file.mjs';
34
40
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
35
41
  import {
36
42
  materializeOutboundArtifact,
@@ -49,7 +55,7 @@ const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
49
55
  const HELP_TEXT = [
50
56
  '微信已连接 DeepSeek Harness。',
51
57
  '',
52
- '直接发送文字、图片或带文字识别结果的语音即可继续当前会话。',
58
+ '直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。',
53
59
  '/new 开启一个全新会话',
54
60
  '/compact 压缩当前会话的较早上下文',
55
61
  '/workspace 工作区绝对路径 切换工作区',
@@ -77,15 +83,33 @@ function nonEmptyString(value) {
77
83
  return typeof value === 'string' && value.trim() ? value.trim() : null;
78
84
  }
79
85
 
86
+ export function weixinInboundMessage(message, api) {
87
+ return {
88
+ content: extractWeixinText(message) ?? '',
89
+ images: typeof api?.inboundImages === 'function'
90
+ ? api.inboundImages(message)
91
+ : extractWeixinImages(message),
92
+ files: typeof api?.inboundFiles === 'function'
93
+ ? api.inboundFiles(message)
94
+ : extractWeixinFiles(message),
95
+ };
96
+ }
97
+
80
98
  function hasWeixinImageItems(message) {
81
99
  return Array.isArray(message?.item_list)
82
100
  && message.item_list.some((item) => item?.image_item && typeof item.image_item === 'object');
83
101
  }
84
102
 
103
+ function hasWeixinFileItems(message) {
104
+ return Array.isArray(message?.item_list)
105
+ && message.item_list.some((item) => item?.file_item && typeof item.file_item === 'object');
106
+ }
107
+
85
108
  function canClaimInteractionReply(message, pending) {
86
109
  return pending.questions[pending.index]
87
110
  && nonEmptyString(message?.from_user_id) === pending.actor
88
111
  && !hasWeixinImageItems(message)
112
+ && !hasWeixinFileItems(message)
89
113
  && nonEmptyString(extractWeixinText(message));
90
114
  }
91
115
 
@@ -204,7 +228,7 @@ export class WeixinHarnessBridge {
204
228
  const runId = nonEmptyString(message?.run_id) ?? undefined;
205
229
  const pending = this.#pendingInteractions.get(key);
206
230
  const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
207
- const commandRunner = isControlCommand(commandText)
231
+ const commandRunner = hasWeixinFileItems(message) ? null : isControlCommand(commandText)
208
232
  ? runControlCommand
209
233
  : (isModelCommand(commandText)
210
234
  ? runModelCommand
@@ -238,7 +262,9 @@ export class WeixinHarnessBridge {
238
262
  key,
239
263
  actor: sender,
240
264
  messageId,
241
- text: hasWeixinImageItems(message) ? '' : extractWeixinText(message),
265
+ text: hasWeixinImageItems(message) || hasWeixinFileItems(message)
266
+ ? ''
267
+ : extractWeixinText(message),
242
268
  addressed: true,
243
269
  hasPendingQuestion: Boolean(pending),
244
270
  questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
@@ -290,10 +316,16 @@ export class WeixinHarnessBridge {
290
316
  releaseMessageId = true,
291
317
  alreadyRecorded = false,
292
318
  } = {}) {
319
+ const preparedMessage = message.from_user_id === this.#ownerUserId
320
+ ? prefetchInboundFiles(
321
+ weixinInboundMessage(message, this.#api),
322
+ { signal: this.#signal },
323
+ )
324
+ : undefined;
293
325
  const previous = this.#queues.get(key) ?? Promise.resolve();
294
326
  const current = previous
295
327
  .catch(() => undefined)
296
- .then(() => this.#process(message, key, { alreadyRecorded }))
328
+ .then(() => this.#process(message, key, { alreadyRecorded, preparedMessage }))
297
329
  .finally(() => {
298
330
  if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
299
331
  if (this.#queues.get(key) === current) this.#queues.delete(key);
@@ -331,6 +363,7 @@ export class WeixinHarnessBridge {
331
363
  const result = await runner(text, this.#harness, this.#state, key, {
332
364
  signal: this.#signal,
333
365
  hasImages: hasWeixinImageItems(message),
366
+ hasFiles: hasWeixinFileItems(message),
334
367
  pendingInteraction: this.#pendingInteractions.has(key)
335
368
  || this.#approvals.hasPending(key),
336
369
  control: { owner: this, key },
@@ -348,7 +381,7 @@ export class WeixinHarnessBridge {
348
381
  this.#status.lastMessageError = null;
349
382
  }
350
383
 
351
- async #process(message, key, { alreadyRecorded = false } = {}) {
384
+ async #process(message, key, { alreadyRecorded = false, preparedMessage } = {}) {
352
385
  this.#signal?.throwIfAborted();
353
386
  const messageId = weixinMessageId(message);
354
387
  const sender = nonEmptyString(message?.from_user_id);
@@ -366,38 +399,36 @@ export class WeixinHarnessBridge {
366
399
 
367
400
  const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
368
401
  const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
369
- const text = extractWeixinText(message) ?? '';
370
402
  try {
371
- const images = typeof this.#api.inboundImages === 'function'
372
- ? this.#api.inboundImages(message)
373
- : extractWeixinImages(message);
374
- const promptMessage = { content: text, images };
403
+ const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
404
+ const text = promptMessage.content;
375
405
  const hasImages = hasInboundImages(promptMessage);
376
- if (!text && !hasImages) {
377
- await this.#send(sender, '目前支持文字、图片,以及微信已转成文字的语音消息。', contextToken, runId);
406
+ const hasFiles = hasInboundFiles(promptMessage);
407
+ if (!text && !hasImages && !hasFiles) {
408
+ await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
378
409
  await this.#state.markSeen(messageId);
379
410
  return;
380
411
  }
381
412
 
382
413
  const command = text.trim().toLowerCase();
383
- if (!hasImages && command === '/help') {
414
+ if (!hasImages && !hasFiles && command === '/help') {
384
415
  await this.#send(sender, HELP_TEXT, contextToken, runId);
385
416
  await this.#state.markSeen(messageId);
386
417
  return;
387
418
  }
388
- if (!hasImages && command === '/status') {
419
+ if (!hasImages && !hasFiles && command === '/status') {
389
420
  await this.#harness.ensureRunning({ signal: this.#signal });
390
421
  await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
391
422
  await this.#state.markSeen(messageId);
392
423
  return;
393
424
  }
394
- if (!hasImages && command === '/new') {
425
+ if (!hasImages && !hasFiles && command === '/new') {
395
426
  await this.#state.clearSession(key);
396
427
  await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
397
428
  await this.#state.markSeen(messageId);
398
429
  return;
399
430
  }
400
- const workspaceCommand = hasImages
431
+ const workspaceCommand = hasImages || hasFiles
401
432
  ? null
402
433
  : await runWorkspaceCommand(text, this.#harness, key);
403
434
  if (workspaceCommand) {
@@ -407,7 +438,7 @@ export class WeixinHarnessBridge {
407
438
  await this.#state.markSeen(messageId);
408
439
  return;
409
440
  }
410
- const compactCommand = hasImages
441
+ const compactCommand = hasImages || hasFiles
411
442
  ? null
412
443
  : await runCompactCommand(
413
444
  text,
@@ -446,6 +477,7 @@ export class WeixinHarnessBridge {
446
477
  runId,
447
478
  }),
448
479
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
480
+ files: promptMessage.files,
449
481
  },
450
482
  }));
451
483
  } finally {
@@ -490,7 +522,9 @@ export class WeixinHarnessBridge {
490
522
  }
491
523
  if (this.#signal?.aborted) return;
492
524
  this.#status.lastError = error?.message ?? String(error);
493
- const userMessage = imagePromptUserMessage(error) ?? GENERIC_PROCESSING_ERROR;
525
+ const userMessage = inboundFileUserMessage(error)
526
+ ?? imagePromptUserMessage(error)
527
+ ?? GENERIC_PROCESSING_ERROR;
494
528
  this.#status.lastMessageError = safeMessageError(error, userMessage);
495
529
  this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
496
530
  try {
@@ -170,6 +170,39 @@ function whatsappImageSource(message, content, download, { viewOnce = false } =
170
170
  };
171
171
  }
172
172
 
173
+ function whatsappFileSource(message, content, download) {
174
+ const media = content?.documentMessage;
175
+ if (!media) return null;
176
+ const mediaType = typeof media.mimetype === 'string' && media.mimetype
177
+ ? media.mimetype.toLowerCase() : undefined;
178
+ if (IMAGE_MEDIA_TYPES.has(mediaType)) return null;
179
+ return {
180
+ name: typeof media.fileName === 'string' && media.fileName
181
+ ? media.fileName : 'whatsapp-file',
182
+ ...(mediaType ? { mediaType } : {}),
183
+ size: mediaSize(media.fileLength),
184
+ async load({ signal } = {}) {
185
+ signal?.throwIfAborted();
186
+ const stream = await download(message, 'stream', { options: { signal } });
187
+ signal?.throwIfAborted();
188
+ return { stream };
189
+ },
190
+ };
191
+ }
192
+
193
+ export function createWhatsappMediaDownloader({
194
+ socket,
195
+ logger = console,
196
+ download = downloadMediaMessage,
197
+ } = {}) {
198
+ if (typeof download !== 'function') throw new TypeError('WhatsApp media downloader is required');
199
+ if (typeof socket?.updateMediaMessage !== 'function') return download;
200
+ return (message, type, options) => download(message, type, options, {
201
+ logger,
202
+ reuploadRequest: (candidate) => socket.updateMediaMessage(candidate),
203
+ });
204
+ }
205
+
173
206
  export function normalizeWhatsappMessage(message, accountJid, {
174
207
  download = downloadMediaMessage,
175
208
  } = {}) {
@@ -195,6 +228,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
195
228
  const replyToSelf = typeof context?.participant === 'string'
196
229
  && areJidsSameUser(context.participant, accountJid);
197
230
  const image = whatsappImageSource(message, content, download, { viewOnce });
231
+ const file = whatsappFileSource(message, content, download);
198
232
  return {
199
233
  messageId: `${remoteJid}:${messageId}`,
200
234
  providerMessageId: messageId,
@@ -205,6 +239,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
205
239
  conversationId: remoteJid,
206
240
  content: messageText(content),
207
241
  images: image ? [image] : [],
242
+ files: file ? [file] : [],
208
243
  addressed: !group || mentioned || replyToSelf,
209
244
  selfChat,
210
245
  replyTarget: { jid: remoteJid, quoted: message, selfChat },
@@ -495,8 +530,13 @@ export class WhatsappRuntime {
495
530
  new Error('WhatsApp linked-device session must be scanned again'),
496
531
  { code: 'relink-required' },
497
532
  )),
498
- onMessage: async (raw) => {
499
- const message = normalizeWhatsappMessage(raw, this.#config.accountJid);
533
+ onMessage: async (raw, context) => {
534
+ const message = normalizeWhatsappMessage(raw, this.#config.accountJid, {
535
+ download: createWhatsappMediaDownloader({
536
+ socket: context?.socket,
537
+ logger: this.#logger,
538
+ }),
539
+ });
500
540
  if (!message || outboundIds.has(message.providerMessageId) || !this.#bridge) return;
501
541
  this.#status.lastCheckedAt = Date.now();
502
542
  if (!whatsappInboundAllowed(message, {
@@ -190,7 +190,7 @@ export async function createWhatsappWebSession({
190
190
  const timestamp = messageTimestampMs(message?.messageTimestamp);
191
191
  if (timestamp === null || timestamp < sessionStartedAt - APPEND_RECENT_GRACE_MS) continue;
192
192
  }
193
- Promise.resolve(onMessage(message)).catch(() => {
193
+ Promise.resolve(onMessage(message, { socket: nextSocket })).catch(() => {
194
194
  logger.error?.('[dsh-im:whatsapp] failed to process an inbound WhatsApp message');
195
195
  });
196
196
  }