@daolmedo/openclaw-twilio-whatsapp 1.1.22 → 1.1.24

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 (2) hide show
  1. package/dist/index.js +356 -176
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -63,30 +63,63 @@ async function sendTwilioWhatsappMessage(opts) {
63
63
  const to = opts.to.startsWith("whatsapp:") ? opts.to : `whatsapp:${opts.to}`;
64
64
  const chunks = opts.body ? splitMessage(opts.body) : [void 0];
65
65
  let lastSid = "";
66
- for (const chunk of chunks) {
67
- const msg = await client.messages.create({
68
- from,
69
- to,
70
- ...chunk ? { body: chunk } : {},
71
- ...opts.mediaUrl ? { mediaUrl: [opts.mediaUrl] } : {}
72
- });
73
- lastSid = msg.sid;
66
+ for (let i = 0; i < chunks.length; i += 1) {
67
+ const chunk = chunks[i];
68
+ console.log(
69
+ `[twilio-whatsapp] Twilio create start from=${from} to=${to} chunk=${i + 1}/${chunks.length} bodyChars=${chunk?.length ?? 0} media=${opts.mediaUrl ? "yes" : "no"}`
70
+ );
71
+ try {
72
+ const msg = await client.messages.create({
73
+ from,
74
+ to,
75
+ ...chunk ? { body: chunk } : {},
76
+ ...opts.mediaUrl ? { mediaUrl: [opts.mediaUrl] } : {}
77
+ });
78
+ lastSid = msg.sid;
79
+ console.log(
80
+ `[twilio-whatsapp] Twilio create ok sid=${msg.sid} status=${msg.status} chunk=${i + 1}/${chunks.length}`
81
+ );
82
+ } catch (err) {
83
+ console.error(
84
+ `[twilio-whatsapp] Twilio create failed chunk=${i + 1}/${chunks.length}:`,
85
+ err
86
+ );
87
+ throw err;
88
+ }
74
89
  }
75
90
  return lastSid;
76
91
  }
77
92
 
78
93
  // src/outbound-adapter.ts
94
+ function summarizeText(text) {
95
+ if (!text) return "chars=0";
96
+ return `chars=${text.length} preview=${JSON.stringify(text.slice(0, 120))}`;
97
+ }
79
98
  async function deliver(ctx) {
80
99
  const account = resolveTwilioAccount(ctx.cfg, ctx.accountId);
81
- const sid = await sendTwilioWhatsappMessage({
82
- accountSid: account.accountSid,
83
- authToken: account.authToken,
84
- from: account.phoneNumber,
85
- to: ctx.to,
86
- body: ctx.text || void 0,
87
- mediaUrl: ctx.mediaUrl
88
- });
89
- return sid;
100
+ console.log(
101
+ `[twilio-whatsapp] Durable adapter deliver start accountId=${ctx.accountId ?? "default"} from=${account.phoneNumber} to=${ctx.to} ${summarizeText(ctx.text)} media=${ctx.mediaUrl ? "yes" : "no"}`
102
+ );
103
+ try {
104
+ const sid = await sendTwilioWhatsappMessage({
105
+ accountSid: account.accountSid,
106
+ authToken: account.authToken,
107
+ from: account.phoneNumber,
108
+ to: ctx.to,
109
+ body: ctx.text || void 0,
110
+ mediaUrl: ctx.mediaUrl
111
+ });
112
+ console.log(
113
+ `[twilio-whatsapp] Durable adapter deliver ok accountId=${ctx.accountId ?? "default"} outboundSid=${sid}`
114
+ );
115
+ return sid;
116
+ } catch (err) {
117
+ console.error(
118
+ `[twilio-whatsapp] Durable adapter deliver failed accountId=${ctx.accountId ?? "default"}:`,
119
+ err
120
+ );
121
+ throw err;
122
+ }
90
123
  }
91
124
  var twilioWhatsappOutbound = {
92
125
  deliveryMode: "direct",
@@ -211,7 +244,7 @@ var twilioWhatsappPlugin = {
211
244
  // src/http-routes.ts
212
245
  import { readFile } from "node:fs/promises";
213
246
  import path from "node:path";
214
- import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
247
+ import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/direct-dm";
215
248
  import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
216
249
 
217
250
  // src/webhook.ts
@@ -278,6 +311,20 @@ function startTypingKeepalive(opts) {
278
311
 
279
312
  // src/http-routes.ts
280
313
  var MEDIA_MAX_BYTES = 16 * 1024 * 1024;
314
+ function resolveMediaBatchFlushMs() {
315
+ const raw = Number(process.env.TWILIO_WHATSAPP_MEDIA_BATCH_FLUSH_MS);
316
+ if (Number.isFinite(raw) && raw >= 0) return Math.min(raw, 6e4);
317
+ return 3e3;
318
+ }
319
+ function resolveMediaBatchMaxItems() {
320
+ const raw = Number(process.env.TWILIO_WHATSAPP_MEDIA_BATCH_MAX_ITEMS);
321
+ if (Number.isFinite(raw) && raw > 0) return Math.floor(Math.min(raw, 50));
322
+ return 10;
323
+ }
324
+ function summarizeText2(text) {
325
+ if (!text) return "chars=0";
326
+ return `chars=${text.length} preview=${JSON.stringify(text.slice(0, 120))}`;
327
+ }
281
328
  async function readBody(req) {
282
329
  return new Promise((resolve, reject) => {
283
330
  const chunks = [];
@@ -296,6 +343,11 @@ function twimlError(res, status, message) {
296
343
  `<?xml version="1.0" encoding="UTF-8"?><Response><Message>${message}</Message></Response>`
297
344
  );
298
345
  }
346
+ var pendingMediaByConversation = /* @__PURE__ */ new Map();
347
+ var dispatchQueueByConversation = /* @__PURE__ */ new Map();
348
+ function resolvePendingMediaKey(accountId, fromPhone) {
349
+ return `${accountId}:${fromPhone}`;
350
+ }
299
351
  async function downloadTwilioMedia(url, account) {
300
352
  try {
301
353
  const authHeader = "Basic " + Buffer.from(`${account.accountSid}:${account.authToken}`).toString("base64");
@@ -322,6 +374,243 @@ async function downloadTwilioMedia(url, account) {
322
374
  return null;
323
375
  }
324
376
  }
377
+ function enqueueConversationDispatch(key, task) {
378
+ const previous = dispatchQueueByConversation.get(key) ?? Promise.resolve();
379
+ const next = previous.catch(() => void 0).then(task).catch((err) => {
380
+ console.error(`[twilio-whatsapp] Conversation dispatch failed key=${key}:`, err);
381
+ });
382
+ dispatchQueueByConversation.set(key, next);
383
+ void next.finally(() => {
384
+ if (dispatchQueueByConversation.get(key) === next) {
385
+ dispatchQueueByConversation.delete(key);
386
+ }
387
+ });
388
+ }
389
+ function stopBatchTimer(batch) {
390
+ if (batch.timer) {
391
+ clearTimeout(batch.timer);
392
+ batch.timer = void 0;
393
+ }
394
+ }
395
+ function armBatchTimer(api, key, batch) {
396
+ stopBatchTimer(batch);
397
+ const flushMs = resolveMediaBatchFlushMs();
398
+ batch.timer = setTimeout(() => {
399
+ flushPendingMediaBatch(api, key, "timer");
400
+ }, flushMs);
401
+ const maybeTimer = batch.timer;
402
+ maybeTimer.unref?.();
403
+ }
404
+ function combineBatchMessageText(items) {
405
+ return items.map((item) => item.messageText.trim()).filter(Boolean).join("\n\n");
406
+ }
407
+ function flushPendingMediaBatch(api, key, reason) {
408
+ const batch = pendingMediaByConversation.get(key);
409
+ if (!batch) return;
410
+ stopBatchTimer(batch);
411
+ pendingMediaByConversation.delete(key);
412
+ const items = [...batch.items].sort((a, b) => a.receivedAt - b.receivedAt);
413
+ const last = items[items.length - 1];
414
+ if (!last) return;
415
+ const resolvedMedia = items.flatMap((item) => item.resolvedMedia);
416
+ if (!resolvedMedia.length && !combineBatchMessageText(items)) {
417
+ return;
418
+ }
419
+ const messageSids = items.map((item) => item.fields.messageSid || "unknown");
420
+ console.log(
421
+ `[twilio-whatsapp] Flushing media batch key=${key} reason=${reason} messages=${items.length} media=${resolvedMedia.length} sids=${messageSids.join(",")}`
422
+ );
423
+ enqueueConversationDispatch(key, async () => {
424
+ await dispatchTwilioInbound(api, {
425
+ ...last,
426
+ messageText: combineBatchMessageText(items),
427
+ resolvedMedia
428
+ });
429
+ });
430
+ }
431
+ function queueMediaBatch(api, key, item) {
432
+ let batch = pendingMediaByConversation.get(key);
433
+ if (!batch) {
434
+ batch = { items: [] };
435
+ pendingMediaByConversation.set(key, batch);
436
+ }
437
+ batch.items.push(item);
438
+ console.log(
439
+ `[twilio-whatsapp] Buffered media inbound sid=${item.fields.messageSid || "unknown"} key=${key} pendingMessages=${batch.items.length} pendingMedia=${batch.items.reduce(
440
+ (count, pending) => count + pending.resolvedMedia.length,
441
+ 0
442
+ )}`
443
+ );
444
+ if (batch.items.length >= resolveMediaBatchMaxItems()) {
445
+ flushPendingMediaBatch(api, key, "max-items");
446
+ return;
447
+ }
448
+ if (resolveMediaBatchFlushMs() === 0) {
449
+ flushPendingMediaBatch(api, key, "disabled");
450
+ return;
451
+ }
452
+ armBatchTimer(api, key, batch);
453
+ }
454
+ async function dispatchTwilioInbound(api, params) {
455
+ const {
456
+ fields,
457
+ stored,
458
+ account,
459
+ toPhone,
460
+ fromPhone,
461
+ senderAddress,
462
+ recipientAddress,
463
+ conversationLabel,
464
+ resolvedMedia
465
+ } = params;
466
+ let messageText = params.messageText;
467
+ const mediaContext = resolvedMedia.length > 0 ? {
468
+ MediaPath: resolvedMedia[0].path,
469
+ MediaUrl: resolvedMedia[0].path,
470
+ MediaType: resolvedMedia[0].contentType,
471
+ MediaPaths: resolvedMedia.map((m) => m.path),
472
+ MediaUrls: resolvedMedia.map((m) => m.path),
473
+ MediaTypes: resolvedMedia.map((m) => m.contentType)
474
+ } : {};
475
+ let bodyForAgent = messageText || void 0;
476
+ const firstAudio = resolvedMedia.find((m) => m.contentType.startsWith("audio/"));
477
+ if (firstAudio) {
478
+ try {
479
+ const cfgAny = stored.cfg;
480
+ const agentId = cfgAny.bindings?.find(
481
+ (b) => b.match?.channel === "twilio-whatsapp" && b.match?.accountId === stored.accountId
482
+ )?.agentId;
483
+ const agentDir = cfgAny.agents?.list?.find(
484
+ (a) => a.id === agentId
485
+ )?.agentDir;
486
+ const auth = await api.runtime.modelAuth.resolveApiKeyForProvider({
487
+ provider: "openai",
488
+ cfg: api.config,
489
+ agentDir
490
+ });
491
+ if (!auth.apiKey) throw new Error("No OpenAI API key available for audio transcription");
492
+ const audioBuffer = await readFile(firstAudio.path);
493
+ const form = new FormData();
494
+ form.append(
495
+ "file",
496
+ new Blob([audioBuffer], { type: firstAudio.contentType }),
497
+ path.basename(firstAudio.path)
498
+ );
499
+ form.append("model", "gpt-4o-mini-transcribe");
500
+ form.append("response_format", "json");
501
+ const transcribeRes = await fetch("https://api.openai.com/v1/audio/transcriptions", {
502
+ method: "POST",
503
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
504
+ body: form,
505
+ signal: AbortSignal.timeout(12e4)
506
+ });
507
+ if (!transcribeRes.ok) {
508
+ const errBody = await transcribeRes.text();
509
+ throw new Error(`OpenAI transcription HTTP ${transcribeRes.status}: ${errBody}`);
510
+ }
511
+ const transcribeJson = await transcribeRes.json();
512
+ const text = transcribeJson.text;
513
+ console.log("[twilio-whatsapp] Transcription result:", text);
514
+ if (text) {
515
+ bodyForAgent = messageText ? `${messageText}
516
+
517
+ [Audio]
518
+ Transcript:
519
+ ${text}` : `[Audio]
520
+ Transcript:
521
+ ${text}`;
522
+ messageText = messageText ? `${messageText}
523
+
524
+ ${text}` : text;
525
+ mediaContext.Transcript = text;
526
+ } else {
527
+ bodyForAgent = messageText || "<media:audio>";
528
+ }
529
+ } catch (err) {
530
+ console.error("[twilio-whatsapp] Audio transcription failed:", err);
531
+ bodyForAgent = messageText || "<media:audio>";
532
+ }
533
+ }
534
+ console.log(
535
+ `[twilio-whatsapp] Dispatching inbound sid=${fields.messageSid || "unknown"} bodyForAgent=${summarizeText2(bodyForAgent)} media=${resolvedMedia.length}`
536
+ );
537
+ const stopTypingKeepalive = fields.messageSid ? (() => {
538
+ console.log(`[twilio-whatsapp] Typing keepalive start sid=${fields.messageSid}`);
539
+ return startTypingKeepalive({
540
+ accountSid: account.accountSid,
541
+ authToken: account.authToken,
542
+ messageId: fields.messageSid
543
+ });
544
+ })() : () => {
545
+ };
546
+ let typingStopped = false;
547
+ const stopTyping = () => {
548
+ if (typingStopped) return;
549
+ typingStopped = true;
550
+ stopTypingKeepalive();
551
+ console.log(`[twilio-whatsapp] Typing keepalive stop sid=${fields.messageSid || "unknown"}`);
552
+ };
553
+ try {
554
+ const timestamp = Date.now();
555
+ await dispatchInboundDirectDmWithRuntime({
556
+ cfg: stored.cfg,
557
+ runtime: { channel: stored.channelRuntime },
558
+ channel: "twilio-whatsapp",
559
+ channelLabel: "WhatsApp",
560
+ accountId: account.accountId,
561
+ peer: { kind: "direct", id: fromPhone },
562
+ senderId: fromPhone,
563
+ senderAddress,
564
+ recipientAddress,
565
+ conversationLabel,
566
+ rawBody: messageText,
567
+ bodyForAgent,
568
+ messageId: fields.messageSid,
569
+ timestamp,
570
+ commandAuthorized: true,
571
+ provider: "twilio-whatsapp",
572
+ surface: "twilio-whatsapp",
573
+ originatingChannel: "twilio-whatsapp",
574
+ originatingTo: senderAddress,
575
+ extraContext: mediaContext,
576
+ deliver: async (payload) => {
577
+ const text = payload.text ?? "";
578
+ const mediaUrl = payload.mediaUrl ?? payload.mediaUrls?.[0];
579
+ console.log(
580
+ `[twilio-whatsapp] Direct deliver invoked sid=${fields.messageSid || "unknown"} ${summarizeText2(text)} media=${mediaUrl ? "yes" : "no"}`
581
+ );
582
+ if (!text && !mediaUrl) {
583
+ console.log(
584
+ `[twilio-whatsapp] Direct deliver skipped empty payload sid=${fields.messageSid || "unknown"}`
585
+ );
586
+ return;
587
+ }
588
+ console.log(
589
+ `[twilio-whatsapp] Twilio direct send start sid=${fields.messageSid || "unknown"} from=${recipientAddress} to=${senderAddress} ${summarizeText2(text)} media=${mediaUrl ? "yes" : "no"}`
590
+ );
591
+ const messageId = await sendTwilioWhatsappMessage({
592
+ accountSid: account.accountSid,
593
+ authToken: account.authToken,
594
+ from: toPhone,
595
+ to: fromPhone,
596
+ body: text || void 0,
597
+ mediaUrl
598
+ });
599
+ console.log(
600
+ `[twilio-whatsapp] Twilio direct send ok sid=${fields.messageSid || "unknown"} outboundSid=${messageId}`
601
+ );
602
+ },
603
+ onRecordError: (err) => {
604
+ console.error("[twilio-whatsapp] Session record error:", err);
605
+ },
606
+ onDispatchError: (err, info) => {
607
+ console.error(`[twilio-whatsapp] Dispatch error (${info.kind}):`, err);
608
+ }
609
+ });
610
+ } finally {
611
+ stopTyping();
612
+ }
613
+ }
325
614
  function registerTwilioWhatsappHttpRoutes(api) {
326
615
  api.registerHttpRoute({
327
616
  path: "/twilio/whatsapp/events",
@@ -333,6 +622,9 @@ function registerTwilioWhatsappHttpRoutes(api) {
333
622
  const webhookUrl = String(headers["x-twilio-webhook-url"] ?? "");
334
623
  const fields = parseTwilioWebhook(rawBody);
335
624
  const toPhone = normalizePhoneNumber(fields.to);
625
+ console.log(
626
+ `[twilio-whatsapp] Webhook received sid=${fields.messageSid || "unknown"} from=${fields.from} to=${fields.to} bodyChars=${fields.body.length} media=${fields.media.length}`
627
+ );
336
628
  const stored = getRuntimeForPhoneNumber(toPhone);
337
629
  if (!stored) {
338
630
  console.error(`[twilio-whatsapp] No runtime found for To=${fields.to}`);
@@ -358,7 +650,7 @@ function registerTwilioWhatsappHttpRoutes(api) {
358
650
  return true;
359
651
  }
360
652
  } else {
361
- console.warn("[twilio-whatsapp] Skipping sig verification \u2014 headers missing");
653
+ console.warn("[twilio-whatsapp] Skipping sig verification - headers missing");
362
654
  }
363
655
  const fromPhone = normalizePhoneNumber(fields.from);
364
656
  const senderAddress = `whatsapp:${fromPhone}`;
@@ -366,11 +658,15 @@ function registerTwilioWhatsappHttpRoutes(api) {
366
658
  const conversationLabel = fields.profileName || fromPhone;
367
659
  const messageText = fields.body.trim();
368
660
  const hasMedia = fields.media.length > 0;
661
+ const receivedAt = Date.now();
369
662
  if (!messageText && !hasMedia) {
370
663
  twimlOk(res);
371
664
  return true;
372
665
  }
373
666
  twimlOk(res);
667
+ console.log(
668
+ `[twilio-whatsapp] Webhook acknowledged sid=${fields.messageSid || "unknown"} accountId=${account.accountId}`
669
+ );
374
670
  const resolvedMedia = [];
375
671
  if (hasMedia) {
376
672
  const results = await Promise.all(
@@ -379,165 +675,49 @@ function registerTwilioWhatsappHttpRoutes(api) {
379
675
  for (const r of results) {
380
676
  if (r) resolvedMedia.push(r);
381
677
  }
678
+ console.log(
679
+ `[twilio-whatsapp] Media resolved sid=${fields.messageSid || "unknown"} requested=${fields.media.length} saved=${resolvedMedia.length}`
680
+ );
382
681
  }
383
- const mediaContext = resolvedMedia.length > 0 ? {
384
- MediaPath: resolvedMedia[0].path,
385
- MediaUrl: resolvedMedia[0].path,
386
- MediaType: resolvedMedia[0].contentType,
387
- MediaPaths: resolvedMedia.map((m) => m.path),
388
- MediaUrls: resolvedMedia.map((m) => m.path),
389
- MediaTypes: resolvedMedia.map((m) => m.contentType)
390
- } : {};
391
- let bodyForAgent = messageText || void 0;
392
- const firstAudio = resolvedMedia.find((m) => m.contentType.startsWith("audio/"));
393
- if (firstAudio) {
394
- try {
395
- const cfgAny = stored.cfg;
396
- const agentId = cfgAny.bindings?.find(
397
- (b) => b.match?.channel === "twilio-whatsapp" && b.match?.accountId === stored.accountId
398
- )?.agentId;
399
- const agentDir = cfgAny.agents?.list?.find(
400
- (a) => a.id === agentId
401
- )?.agentDir;
402
- const auth = await api.runtime.modelAuth.resolveApiKeyForProvider({
403
- provider: "openai",
404
- cfg: api.config,
405
- agentDir
406
- });
407
- if (!auth.apiKey) throw new Error("No OpenAI API key available for audio transcription");
408
- const audioBuffer = await readFile(firstAudio.path);
409
- const form = new FormData();
410
- form.append(
411
- "file",
412
- new Blob([audioBuffer], { type: firstAudio.contentType }),
413
- path.basename(firstAudio.path)
414
- );
415
- form.append("model", "gpt-4o-mini-transcribe");
416
- form.append("response_format", "json");
417
- const transcribeRes = await fetch("https://api.openai.com/v1/audio/transcriptions", {
418
- method: "POST",
419
- headers: { Authorization: `Bearer ${auth.apiKey}` },
420
- body: form,
421
- signal: AbortSignal.timeout(12e4)
422
- });
423
- if (!transcribeRes.ok) {
424
- const errBody = await transcribeRes.text();
425
- throw new Error(`OpenAI transcription HTTP ${transcribeRes.status}: ${errBody}`);
426
- }
427
- const transcribeJson = await transcribeRes.json();
428
- const text = transcribeJson.text;
429
- console.log("[twilio-whatsapp] Transcription result:", text);
430
- if (text) {
431
- bodyForAgent = `[Audio]
432
- Transcript:
433
- ${text}`;
434
- mediaContext.Transcript = text;
435
- } else {
436
- bodyForAgent = messageText || "<media:audio>";
437
- }
438
- } catch (err) {
439
- console.error("[twilio-whatsapp] Audio transcription failed:", err);
440
- bodyForAgent = messageText || "<media:audio>";
441
- }
682
+ const pendingMediaKey = resolvePendingMediaKey(account.accountId, fromPhone);
683
+ if (hasMedia && resolvedMedia.length > 0) {
684
+ queueMediaBatch(api, pendingMediaKey, {
685
+ fields,
686
+ stored,
687
+ account,
688
+ toPhone,
689
+ fromPhone,
690
+ senderAddress,
691
+ recipientAddress,
692
+ conversationLabel,
693
+ messageText,
694
+ resolvedMedia,
695
+ receivedAt
696
+ });
697
+ return true;
442
698
  }
443
- console.log("[twilio-whatsapp] Dispatching bodyForAgent:", bodyForAgent);
444
- const stopTypingKeepalive = fields.messageSid ? startTypingKeepalive({
445
- accountSid: account.accountSid,
446
- authToken: account.authToken,
447
- messageId: fields.messageSid
448
- }) : () => {
449
- };
450
- let typingStopped = false;
451
- const stopTyping = () => {
452
- if (typingStopped) return;
453
- typingStopped = true;
454
- stopTypingKeepalive();
455
- };
456
- const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
457
- cfg: stored.cfg,
458
- channel: "twilio-whatsapp",
459
- accountId: account.accountId,
460
- peer: { kind: "direct", id: fromPhone },
461
- runtime: stored.channelRuntime,
462
- sessionStore: stored.cfg.session?.store
463
- });
464
- const timestamp = Date.now();
465
- const rawBodyForContext = mediaContext.Transcript ? String(mediaContext.Transcript) : messageText;
466
- const { storePath, body } = buildEnvelope({
467
- channel: "WhatsApp",
468
- from: conversationLabel,
469
- body: rawBodyForContext,
470
- timestamp
471
- });
472
- const ctxPayload = stored.channelRuntime.reply.finalizeInboundContext({
473
- Body: body,
474
- BodyForAgent: bodyForAgent ?? rawBodyForContext,
475
- RawBody: rawBodyForContext,
476
- CommandBody: rawBodyForContext,
477
- From: senderAddress,
478
- To: recipientAddress,
479
- SessionKey: route.sessionKey,
480
- AccountId: route.accountId ?? account.accountId,
481
- ChatType: "direct",
482
- ConversationLabel: conversationLabel,
483
- SenderId: fromPhone,
484
- Provider: "twilio-whatsapp",
485
- Surface: "twilio-whatsapp",
486
- MessageSid: fields.messageSid,
487
- MessageSidFull: fields.messageSid,
488
- Timestamp: timestamp,
489
- CommandAuthorized: true,
490
- OriginatingChannel: "twilio-whatsapp",
491
- OriginatingTo: senderAddress,
492
- ...mediaContext
493
- });
494
- stored.channelRuntime.inbound.dispatchReply({
495
- cfg: stored.cfg,
496
- channel: "twilio-whatsapp",
497
- accountId: route.accountId ?? account.accountId,
498
- agentId: route.agentId,
499
- routeSessionKey: route.sessionKey,
500
- storePath,
501
- ctxPayload,
502
- recordInboundSession: stored.channelRuntime.session.recordInboundSession,
503
- dispatchReplyWithBufferedBlockDispatcher: stored.channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher,
504
- delivery: {
505
- deliver: async (payload) => {
506
- const outbound = payload;
507
- const text = outbound.text ?? "";
508
- const mediaUrl = outbound.mediaUrl ?? outbound.mediaUrls?.[0];
509
- if (!text && !mediaUrl) {
510
- return { visibleReplySent: false };
511
- }
512
- const messageId = await sendTwilioWhatsappMessage({
513
- accountSid: account.accountSid,
514
- authToken: account.authToken,
515
- from: toPhone,
516
- to: fromPhone,
517
- body: text || void 0,
518
- mediaUrl
519
- });
520
- return {
521
- messageIds: [messageId],
522
- visibleReplySent: true
523
- };
524
- },
525
- onError: (err, info) => {
526
- stopTyping();
527
- console.error(`[twilio-whatsapp] Dispatch error (${info.kind}):`, err);
528
- }
529
- },
530
- replyPipeline: {},
531
- record: {
532
- onRecordError: (err) => {
533
- console.error("[twilio-whatsapp] Session record error:", err);
534
- }
535
- },
536
- messageId: fields.messageSid
537
- }).catch((err) => {
538
- console.error("[twilio-whatsapp] Unhandled dispatch error:", err);
539
- }).finally(() => {
540
- stopTyping();
699
+ if (hasMedia && resolvedMedia.length === 0 && !messageText) {
700
+ console.warn(
701
+ `[twilio-whatsapp] Dropping media-only inbound with no resolved media sid=${fields.messageSid || "unknown"}`
702
+ );
703
+ return true;
704
+ }
705
+ if (!hasMedia) {
706
+ flushPendingMediaBatch(api, pendingMediaKey, "before-text");
707
+ }
708
+ enqueueConversationDispatch(pendingMediaKey, async () => {
709
+ await dispatchTwilioInbound(api, {
710
+ fields,
711
+ stored,
712
+ account,
713
+ toPhone,
714
+ fromPhone,
715
+ senderAddress,
716
+ recipientAddress,
717
+ conversationLabel,
718
+ messageText,
719
+ resolvedMedia
720
+ });
541
721
  });
542
722
  return true;
543
723
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daolmedo/openclaw-twilio-whatsapp",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "Twilio WhatsApp channel plugin for OpenClaw (Cody ISV)",
5
5
  "type": "module",
6
6
  "exports": {