@openclaw/whatsapp 2026.5.2-beta.2 → 2026.5.3-beta.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 (301) hide show
  1. package/dist/access-control-BQ3tCkH8.js +288 -0
  2. package/dist/account-config-CNQ33oav.js +47 -0
  3. package/dist/account-ids-DG18xddX.js +5 -0
  4. package/dist/accounts-BDqgpFEB.js +116 -0
  5. package/dist/action-runtime-DTGNgG-i.js +74 -0
  6. package/dist/action-runtime-api.js +2 -0
  7. package/dist/action-runtime.runtime.js +2 -0
  8. package/dist/active-listener-N7GFKsuN.js +11 -0
  9. package/dist/agent-tools-login-DpRxLNRB.js +89 -0
  10. package/dist/api.js +148 -0
  11. package/dist/audio-preflight.runtime-Bcdsl_ri.js +7 -0
  12. package/dist/auth-presence.js +32 -0
  13. package/dist/auth-store-BjCb50Ry.js +381 -0
  14. package/dist/channel-DQ-5Xoj9.js +542 -0
  15. package/dist/channel-config-api.js +2 -0
  16. package/dist/channel-plugin-api.js +2 -0
  17. package/dist/channel-react-action-Df20qAy9.js +42 -0
  18. package/dist/channel.runtime-DKXRnA8x.js +55 -0
  19. package/dist/channel.setup-DOHR2BGp.js +20 -0
  20. package/dist/command-policy-O8sRnqUV.js +8 -0
  21. package/dist/config-schema-Dmo66Ubb.js +27 -0
  22. package/dist/connection-controller-BiHAsTM8.js +596 -0
  23. package/dist/connection-controller-registry-D_iIGz5m.js +22 -0
  24. package/dist/constants-DLGCIbfl.js +4 -0
  25. package/dist/constants.js +2 -0
  26. package/dist/contract-api.js +19 -0
  27. package/dist/creds-files-_2V8jzQa.js +19 -0
  28. package/dist/directory-config-DVs4ffXu.js +37 -0
  29. package/dist/directory-contract-api.js +2 -0
  30. package/dist/doctor-A9tXm9Om.js +48 -0
  31. package/dist/doctor-contract-api.js +10 -0
  32. package/dist/group-session-contract-B8QBn0I2.js +15 -0
  33. package/dist/identity-QHAlV7Fu.js +75 -0
  34. package/dist/index.js +18 -0
  35. package/dist/legacy-session-surface-api.js +8 -0
  36. package/dist/legacy-state-migrations-api.js +2 -0
  37. package/dist/light-runtime-api.js +5 -0
  38. package/dist/login-DymdrKca.js +52 -0
  39. package/dist/login-qr-DIS0Iv-7.js +378 -0
  40. package/dist/login-qr-api.js +2 -0
  41. package/dist/login-qr-runtime.js +16 -0
  42. package/dist/monitor-C4Fhg_lD.js +3349 -0
  43. package/dist/normalize-CWYiGkXw.js +2 -0
  44. package/dist/normalize-target-BPVHNsM5.js +74 -0
  45. package/dist/outbound-adapter-BssVnZdz.js +29 -0
  46. package/dist/outbound-base-Bq1UYEK5.js +119 -0
  47. package/dist/outbound-media-contract-CGss_QHy.js +163 -0
  48. package/dist/outbound-payload-test-api.js +2 -0
  49. package/dist/quoted-message-TENQzyxV.js +108 -0
  50. package/dist/reaction-level-CRVEVAfi.js +16 -0
  51. package/dist/reply-resolver.runtime-BRCgoO4C.js +2 -0
  52. package/dist/resolve-outbound-target-CN-Wo6IY.js +39 -0
  53. package/dist/rolldown-runtime-RkAeH_Qm.js +27 -0
  54. package/dist/runtime-6fZ67abR.js +8 -0
  55. package/dist/runtime-api.js +15 -0
  56. package/dist/runtime-group-policy-Gp1lQ0at.js +11 -0
  57. package/dist/secret-contract-api.js +5 -0
  58. package/dist/security-contract-DqH8JDX6.js +26 -0
  59. package/dist/security-contract-api.js +2 -0
  60. package/dist/send-CIyLFxdZ.js +234 -0
  61. package/dist/session-contract-B0efQf9P.js +27 -0
  62. package/dist/session-errors-CSuKZPub.js +63 -0
  63. package/dist/session.runtime-C1eSE_KK.js +13 -0
  64. package/dist/setup-core-DZKvpzbH.js +46 -0
  65. package/dist/setup-entry.js +23 -0
  66. package/dist/setup-plugin-api.js +2 -0
  67. package/dist/setup-surface-ieFnIW4g.js +344 -0
  68. package/dist/shared-D_wzSVar.js +307 -0
  69. package/dist/state-migrations-DnCdvoYD.js +39 -0
  70. package/dist/targets.js +2 -0
  71. package/dist/test-api.js +3 -0
  72. package/dist/text-runtime-C1zNiZeO.js +116 -0
  73. package/package.json +14 -6
  74. package/action-runtime-api.ts +0 -1
  75. package/action-runtime.runtime.ts +0 -1
  76. package/api.ts +0 -213
  77. package/auth-presence.ts +0 -80
  78. package/channel-config-api.ts +0 -1
  79. package/channel-plugin-api.ts +0 -3
  80. package/config-api.ts +0 -4
  81. package/constants.ts +0 -1
  82. package/contract-api.ts +0 -29
  83. package/directory-contract-api.ts +0 -4
  84. package/doctor-contract-api.ts +0 -8
  85. package/index.test.ts +0 -13
  86. package/index.ts +0 -16
  87. package/legacy-session-surface-api.ts +0 -6
  88. package/legacy-state-migrations-api.ts +0 -1
  89. package/light-runtime-api.ts +0 -12
  90. package/login-qr-api.ts +0 -1
  91. package/login-qr-runtime.ts +0 -23
  92. package/outbound-payload-test-api.ts +0 -1
  93. package/runtime-api.ts +0 -84
  94. package/secret-contract-api.ts +0 -4
  95. package/security-contract-api.ts +0 -4
  96. package/setup-entry.test.ts +0 -21
  97. package/setup-entry.ts +0 -21
  98. package/setup-plugin-api.ts +0 -3
  99. package/src/account-config.ts +0 -77
  100. package/src/account-ids.ts +0 -13
  101. package/src/account-types.ts +0 -5
  102. package/src/accounts.test.ts +0 -182
  103. package/src/accounts.ts +0 -176
  104. package/src/accounts.whatsapp-auth.test.ts +0 -59
  105. package/src/action-runtime-target-auth.ts +0 -27
  106. package/src/action-runtime.test.ts +0 -330
  107. package/src/action-runtime.ts +0 -76
  108. package/src/active-listener.test.ts +0 -65
  109. package/src/active-listener.ts +0 -17
  110. package/src/agent-tools-login.test.ts +0 -81
  111. package/src/agent-tools-login.ts +0 -113
  112. package/src/approval-auth.test.ts +0 -24
  113. package/src/approval-auth.ts +0 -27
  114. package/src/auth-store.runtime.ts +0 -1
  115. package/src/auth-store.test.ts +0 -311
  116. package/src/auth-store.ts +0 -502
  117. package/src/auto-reply/config.runtime.ts +0 -16
  118. package/src/auto-reply/constants.ts +0 -1
  119. package/src/auto-reply/deliver-reply.test.ts +0 -843
  120. package/src/auto-reply/deliver-reply.ts +0 -279
  121. package/src/auto-reply/loggers.ts +0 -6
  122. package/src/auto-reply/mentions.ts +0 -131
  123. package/src/auto-reply/monitor/ack-reaction.test.ts +0 -170
  124. package/src/auto-reply/monitor/ack-reaction.ts +0 -99
  125. package/src/auto-reply/monitor/audio-preflight.runtime.ts +0 -9
  126. package/src/auto-reply/monitor/broadcast.ts +0 -153
  127. package/src/auto-reply/monitor/commands.ts +0 -19
  128. package/src/auto-reply/monitor/echo.ts +0 -64
  129. package/src/auto-reply/monitor/group-activation.runtime.ts +0 -1
  130. package/src/auto-reply/monitor/group-activation.test.ts +0 -189
  131. package/src/auto-reply/monitor/group-activation.ts +0 -73
  132. package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +0 -103
  133. package/src/auto-reply/monitor/group-gating.runtime.ts +0 -8
  134. package/src/auto-reply/monitor/group-gating.ts +0 -217
  135. package/src/auto-reply/monitor/group-members.test.ts +0 -56
  136. package/src/auto-reply/monitor/group-members.ts +0 -65
  137. package/src/auto-reply/monitor/inbound-context.test.ts +0 -97
  138. package/src/auto-reply/monitor/inbound-context.ts +0 -92
  139. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +0 -21
  140. package/src/auto-reply/monitor/inbound-dispatch.test.ts +0 -895
  141. package/src/auto-reply/monitor/inbound-dispatch.ts +0 -422
  142. package/src/auto-reply/monitor/last-route.test.ts +0 -37
  143. package/src/auto-reply/monitor/last-route.ts +0 -61
  144. package/src/auto-reply/monitor/message-line.runtime.ts +0 -38
  145. package/src/auto-reply/monitor/message-line.ts +0 -54
  146. package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +0 -354
  147. package/src/auto-reply/monitor/on-message.ts +0 -291
  148. package/src/auto-reply/monitor/peer.ts +0 -17
  149. package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +0 -420
  150. package/src/auto-reply/monitor/process-message.test.ts +0 -355
  151. package/src/auto-reply/monitor/process-message.ts +0 -531
  152. package/src/auto-reply/monitor/runtime-api.ts +0 -30
  153. package/src/auto-reply/monitor-state.test.ts +0 -64
  154. package/src/auto-reply/monitor-state.ts +0 -106
  155. package/src/auto-reply/monitor.ts +0 -600
  156. package/src/auto-reply/reply-resolver.runtime.ts +0 -1
  157. package/src/auto-reply/types.ts +0 -48
  158. package/src/auto-reply/util.ts +0 -63
  159. package/src/auto-reply/web-auto-reply-monitor.test.ts +0 -687
  160. package/src/auto-reply/web-auto-reply-utils.test.ts +0 -392
  161. package/src/auto-reply.broadcast-groups.combined.test.ts +0 -246
  162. package/src/auto-reply.broadcast-groups.test-harness.ts +0 -47
  163. package/src/auto-reply.impl.ts +0 -6
  164. package/src/auto-reply.test-harness.ts +0 -421
  165. package/src/auto-reply.ts +0 -1
  166. package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +0 -427
  167. package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +0 -908
  168. package/src/auto-reply.web-auto-reply.last-route.test.ts +0 -184
  169. package/src/channel-actions.runtime.ts +0 -7
  170. package/src/channel-actions.test.ts +0 -216
  171. package/src/channel-actions.ts +0 -84
  172. package/src/channel-outbound.test.ts +0 -131
  173. package/src/channel-outbound.ts +0 -36
  174. package/src/channel-react-action.runtime.ts +0 -7
  175. package/src/channel-react-action.test.ts +0 -294
  176. package/src/channel-react-action.ts +0 -84
  177. package/src/channel.runtime.ts +0 -117
  178. package/src/channel.setup.test.ts +0 -461
  179. package/src/channel.setup.ts +0 -28
  180. package/src/channel.ts +0 -334
  181. package/src/command-policy.ts +0 -7
  182. package/src/config-accessors.test.ts +0 -34
  183. package/src/config-accessors.ts +0 -22
  184. package/src/config-schema.test.ts +0 -121
  185. package/src/config-schema.ts +0 -6
  186. package/src/config-ui-hints.ts +0 -24
  187. package/src/connection-controller-registry.test.ts +0 -27
  188. package/src/connection-controller-registry.ts +0 -49
  189. package/src/connection-controller.test.ts +0 -282
  190. package/src/connection-controller.ts +0 -679
  191. package/src/creds-files.ts +0 -19
  192. package/src/creds-persistence.ts +0 -102
  193. package/src/directory-config.test.ts +0 -60
  194. package/src/directory-config.ts +0 -40
  195. package/src/directory-contract.test.ts +0 -56
  196. package/src/doctor-contract.ts +0 -11
  197. package/src/doctor.test.ts +0 -74
  198. package/src/doctor.ts +0 -56
  199. package/src/group-intro.ts +0 -15
  200. package/src/group-policy.test.ts +0 -36
  201. package/src/group-policy.ts +0 -40
  202. package/src/group-session-contract.ts +0 -20
  203. package/src/group-session-key.test.ts +0 -53
  204. package/src/group-session-key.ts +0 -41
  205. package/src/heartbeat.ts +0 -34
  206. package/src/identity.ts +0 -164
  207. package/src/inbound/access-control.test-harness.ts +0 -37
  208. package/src/inbound/access-control.test.ts +0 -375
  209. package/src/inbound/access-control.ts +0 -237
  210. package/src/inbound/dedupe.ts +0 -132
  211. package/src/inbound/extract.test.ts +0 -282
  212. package/src/inbound/extract.ts +0 -488
  213. package/src/inbound/lifecycle.ts +0 -39
  214. package/src/inbound/media.node.test.ts +0 -83
  215. package/src/inbound/media.ts +0 -102
  216. package/src/inbound/monitor.ts +0 -890
  217. package/src/inbound/runtime-api.ts +0 -7
  218. package/src/inbound/save-media.runtime.ts +0 -1
  219. package/src/inbound/send-api.test.ts +0 -307
  220. package/src/inbound/send-api.ts +0 -141
  221. package/src/inbound/send-result.ts +0 -61
  222. package/src/inbound/types.ts +0 -106
  223. package/src/inbound-context.contract.test.ts +0 -32
  224. package/src/inbound-policy.ts +0 -231
  225. package/src/inbound.media.test.ts +0 -320
  226. package/src/inbound.test.ts +0 -290
  227. package/src/inbound.ts +0 -9
  228. package/src/login-qr.test.ts +0 -483
  229. package/src/login-qr.ts +0 -542
  230. package/src/login.coverage.test.ts +0 -133
  231. package/src/login.test.ts +0 -89
  232. package/src/login.ts +0 -70
  233. package/src/logout.test.ts +0 -162
  234. package/src/media.test.ts +0 -480
  235. package/src/media.ts +0 -10
  236. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +0 -400
  237. package/src/monitor-inbox.append-upsert.test-support.ts +0 -133
  238. package/src/monitor-inbox.behavior.test.ts +0 -5
  239. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +0 -362
  240. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +0 -306
  241. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +0 -729
  242. package/src/monitor-inbox.test-harness.ts +0 -304
  243. package/src/normalize-target.ts +0 -112
  244. package/src/normalize.ts +0 -6
  245. package/src/outbound-adapter.poll.test.ts +0 -62
  246. package/src/outbound-adapter.sendpayload.test.ts +0 -195
  247. package/src/outbound-adapter.ts +0 -36
  248. package/src/outbound-base.test.ts +0 -560
  249. package/src/outbound-base.ts +0 -244
  250. package/src/outbound-media-contract.ts +0 -278
  251. package/src/outbound-media.runtime.ts +0 -36
  252. package/src/outbound-payload.contract.test.ts +0 -61
  253. package/src/outbound-send-deps.ts +0 -1
  254. package/src/outbound-test-support.ts +0 -16
  255. package/src/pairing-security.test-harness.ts +0 -59
  256. package/src/qr-image.ts +0 -1
  257. package/src/qr-terminal.ts +0 -1
  258. package/src/quoted-message.test.ts +0 -83
  259. package/src/quoted-message.ts +0 -184
  260. package/src/reaction-level.test.ts +0 -94
  261. package/src/reaction-level.ts +0 -21
  262. package/src/reconnect.test.ts +0 -51
  263. package/src/reconnect.ts +0 -55
  264. package/src/resolve-outbound-target.test.ts +0 -265
  265. package/src/resolve-outbound-target.ts +0 -54
  266. package/src/resolve-target.test.ts +0 -97
  267. package/src/runtime-api.ts +0 -59
  268. package/src/runtime-group-policy.ts +0 -16
  269. package/src/runtime.ts +0 -9
  270. package/src/security-contract.ts +0 -47
  271. package/src/security-fix.ts +0 -71
  272. package/src/send.test.ts +0 -515
  273. package/src/send.ts +0 -287
  274. package/src/session-contract.test.ts +0 -31
  275. package/src/session-contract.ts +0 -43
  276. package/src/session-errors.ts +0 -125
  277. package/src/session-route.ts +0 -26
  278. package/src/session.runtime.ts +0 -8
  279. package/src/session.test.ts +0 -559
  280. package/src/session.ts +0 -330
  281. package/src/setup-core.ts +0 -52
  282. package/src/setup-finalize.ts +0 -446
  283. package/src/setup-surface.test.ts +0 -367
  284. package/src/setup-surface.ts +0 -69
  285. package/src/setup-test-helpers.ts +0 -216
  286. package/src/shared.ts +0 -291
  287. package/src/socket-timing.test.ts +0 -49
  288. package/src/socket-timing.ts +0 -38
  289. package/src/state-migrations.ts +0 -54
  290. package/src/status-issues.test.ts +0 -137
  291. package/src/status-issues.ts +0 -185
  292. package/src/system-prompt.test.ts +0 -199
  293. package/src/system-prompt.ts +0 -31
  294. package/src/targets-runtime.ts +0 -180
  295. package/src/test-helpers.ts +0 -690
  296. package/src/text-runtime.test.ts +0 -162
  297. package/src/text-runtime.ts +0 -11
  298. package/src/vcard.ts +0 -84
  299. package/targets.ts +0 -5
  300. package/test-api.ts +0 -2
  301. package/tsconfig.json +0 -16
@@ -1,279 +0,0 @@
1
- import type { MarkdownTableMode } from "openclaw/plugin-sdk/config-types";
2
- import { chunkMarkdownTextWithMode, type ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
3
- import type { ReplyPayload } from "openclaw/plugin-sdk/reply-chunking";
4
- import {
5
- isReasoningReplyPayload,
6
- sendMediaWithLeadingCaption,
7
- } from "openclaw/plugin-sdk/reply-payload";
8
- import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
9
- import type { WhatsAppSendResult } from "../inbound/send-result.js";
10
- import { loadWebMedia } from "../media.js";
11
- import {
12
- type DeliverableWhatsAppOutboundPayload,
13
- normalizeWhatsAppOutboundPayload,
14
- normalizeWhatsAppPayloadTextPreservingIndentation,
15
- prepareWhatsAppOutboundMedia,
16
- sendWhatsAppOutboundWithRetry,
17
- } from "../outbound-media-contract.js";
18
- import { buildQuotedMessageOptions, lookupInboundMessageMeta } from "../quoted-message.js";
19
- import { newConnectionId } from "../reconnect.js";
20
- import { formatError } from "../session.js";
21
- import { convertMarkdownTables } from "../text-runtime.js";
22
- import { markdownToWhatsApp } from "../text-runtime.js";
23
- import { whatsappOutboundLog } from "./loggers.js";
24
- import type { WebInboundMsg } from "./types.js";
25
- import { elide } from "./util.js";
26
-
27
- export type WhatsAppReplyDeliveryResult = {
28
- results: WhatsAppSendResult[];
29
- messageIds: string[];
30
- providerAccepted: boolean;
31
- };
32
-
33
- export async function deliverWebReply(params: {
34
- replyResult: ReplyPayload;
35
- normalizedReplyResult?: DeliverableWhatsAppOutboundPayload<ReplyPayload>;
36
- msg: WebInboundMsg;
37
- mediaLocalRoots?: readonly string[];
38
- maxMediaBytes: number;
39
- textLimit: number;
40
- chunkMode?: ChunkMode;
41
- replyLogger: {
42
- info: (obj: unknown, msg: string) => void;
43
- warn: (obj: unknown, msg: string) => void;
44
- };
45
- connectionId?: string;
46
- skipLog?: boolean;
47
- tableMode?: MarkdownTableMode;
48
- }): Promise<WhatsAppReplyDeliveryResult> {
49
- const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
50
- const replyStarted = Date.now();
51
- const sendResults: WhatsAppSendResult[] = [];
52
- const rememberSendResult = (result: WhatsAppSendResult | undefined) => {
53
- if (result) {
54
- sendResults.push(result);
55
- }
56
- };
57
- const finishDelivery = (): WhatsAppReplyDeliveryResult => {
58
- const messageIds = [...new Set(sendResults.flatMap((result) => result.messageIds))];
59
- return {
60
- results: sendResults,
61
- messageIds,
62
- providerAccepted: sendResults.some((result) => result.providerAccepted),
63
- };
64
- };
65
- if (isReasoningReplyPayload(replyResult)) {
66
- whatsappOutboundLog.debug(`Suppressed reasoning payload to ${msg.from}`);
67
- return finishDelivery();
68
- }
69
- const tableMode = params.tableMode ?? "code";
70
- const chunkMode = params.chunkMode ?? "length";
71
- const normalizedReply =
72
- params.normalizedReplyResult ??
73
- normalizeWhatsAppOutboundPayload(replyResult, {
74
- normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation,
75
- });
76
- const convertedText = markdownToWhatsApp(
77
- convertMarkdownTables(normalizedReply.text ?? "", tableMode),
78
- );
79
- const textChunks = chunkMarkdownTextWithMode(convertedText, textLimit, chunkMode);
80
- const mediaList = normalizedReply.mediaUrls ?? [];
81
-
82
- const getQuote = () => {
83
- if (!replyResult.replyToId) {
84
- return undefined;
85
- }
86
- // Use replyToId (not msg.id) so batched payloads quote the correct
87
- // per-message target. Look up cached metadata for the specific
88
- // message being quoted — msg.body may be a combined batch body.
89
- const cached = lookupInboundMessageMeta(msg.accountId, msg.chatId, replyResult.replyToId);
90
- return buildQuotedMessageOptions({
91
- messageId: replyResult.replyToId,
92
- remoteJid: msg.chatId,
93
- fromMe: cached?.fromMe ?? false,
94
- participant: cached?.participant ?? (msg.chatType === "group" ? msg.senderJid : undefined),
95
- messageText: cached?.body ?? "",
96
- });
97
- };
98
-
99
- const sendWithRetry = async <T>(fn: () => Promise<T>, label: string, maxAttempts = 3) => {
100
- return await sendWhatsAppOutboundWithRetry({
101
- send: fn,
102
- maxAttempts,
103
- onRetry: ({ attempt, maxAttempts: retryMaxAttempts, backoffMs, errorText }) => {
104
- logVerbose(
105
- `Retrying ${label} to ${msg.from} after failure (${attempt}/${retryMaxAttempts - 1}) in ${backoffMs}ms: ${errorText}`,
106
- );
107
- },
108
- });
109
- };
110
-
111
- // Text-only replies
112
- if (mediaList.length === 0 && textChunks.length) {
113
- const totalChunks = textChunks.length;
114
- for (const [index, chunk] of textChunks.entries()) {
115
- const chunkStarted = Date.now();
116
- const quote = getQuote();
117
- rememberSendResult(await sendWithRetry(() => msg.reply(chunk, quote), "text"));
118
- if (!skipLog) {
119
- const durationMs = Date.now() - chunkStarted;
120
- whatsappOutboundLog.debug(
121
- `Sent chunk ${index + 1}/${totalChunks} to ${msg.from} (${durationMs.toFixed(0)}ms)`,
122
- );
123
- }
124
- }
125
- const delivery = finishDelivery();
126
- const logPayload = {
127
- correlationId: msg.id ?? newConnectionId(),
128
- connectionId: connectionId ?? null,
129
- to: msg.from,
130
- from: msg.to,
131
- text: elide(replyResult.text, 240),
132
- mediaUrl: null,
133
- mediaSizeBytes: null,
134
- mediaKind: null,
135
- durationMs: Date.now() - replyStarted,
136
- };
137
- if (delivery.providerAccepted) {
138
- replyLogger.info(logPayload, "auto-reply sent (text)");
139
- } else {
140
- replyLogger.warn(logPayload, "auto-reply text was not accepted by WhatsApp provider");
141
- }
142
- return delivery;
143
- }
144
-
145
- const remainingText = [...textChunks];
146
-
147
- // Media (with optional caption on first item)
148
- const leadingCaption = remainingText.shift() || "";
149
- await sendMediaWithLeadingCaption({
150
- mediaUrls: mediaList,
151
- caption: leadingCaption,
152
- send: async ({ mediaUrl, caption }) => {
153
- const media = await prepareWhatsAppOutboundMedia(
154
- await loadWebMedia(mediaUrl, {
155
- maxBytes: maxMediaBytes,
156
- localRoots: params.mediaLocalRoots,
157
- }),
158
- mediaUrl,
159
- );
160
- if (shouldLogVerbose()) {
161
- logVerbose(
162
- `Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`,
163
- );
164
- logVerbose(`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`);
165
- }
166
- if (media.kind === "image") {
167
- const quote = getQuote();
168
- rememberSendResult(
169
- await sendWithRetry(
170
- () =>
171
- msg.sendMedia(
172
- {
173
- image: media.buffer,
174
- caption,
175
- mimetype: media.mimetype,
176
- },
177
- quote,
178
- ),
179
- "media:image",
180
- ),
181
- );
182
- } else if (media.kind === "audio") {
183
- const quote = getQuote();
184
- rememberSendResult(
185
- await sendWithRetry(
186
- () =>
187
- msg.sendMedia(
188
- {
189
- audio: media.buffer,
190
- ptt: true,
191
- mimetype: media.mimetype,
192
- },
193
- quote,
194
- ),
195
- "media:audio",
196
- ),
197
- );
198
- if (caption) {
199
- rememberSendResult(
200
- await sendWithRetry(() => msg.reply(caption, quote), "media:audio-text"),
201
- );
202
- }
203
- } else if (media.kind === "video") {
204
- const quote = getQuote();
205
- rememberSendResult(
206
- await sendWithRetry(
207
- () =>
208
- msg.sendMedia(
209
- {
210
- video: media.buffer,
211
- caption,
212
- mimetype: media.mimetype,
213
- },
214
- quote,
215
- ),
216
- "media:video",
217
- ),
218
- );
219
- } else {
220
- const quote = getQuote();
221
- rememberSendResult(
222
- await sendWithRetry(
223
- () =>
224
- msg.sendMedia(
225
- {
226
- document: media.buffer,
227
- fileName: media.fileName,
228
- caption,
229
- mimetype: media.mimetype,
230
- },
231
- quote,
232
- ),
233
- "media:document",
234
- ),
235
- );
236
- }
237
- whatsappOutboundLog.info(
238
- `Sent media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`,
239
- );
240
- replyLogger.info(
241
- {
242
- correlationId: msg.id ?? newConnectionId(),
243
- connectionId: connectionId ?? null,
244
- to: msg.from,
245
- from: msg.to,
246
- text: caption ?? null,
247
- mediaUrl,
248
- mediaSizeBytes: media.buffer.length,
249
- mediaKind: media.kind,
250
- durationMs: Date.now() - replyStarted,
251
- },
252
- "auto-reply sent (media)",
253
- );
254
- },
255
- onError: async ({ error, mediaUrl, caption, isFirst }) => {
256
- whatsappOutboundLog.error(`Failed sending web media to ${msg.from}: ${formatError(error)}`);
257
- replyLogger.warn({ err: error, mediaUrl }, "failed to send web media reply");
258
- if (!isFirst) {
259
- return;
260
- }
261
- const warning = "⚠️ Media failed.";
262
- const fallbackTextParts = [remainingText.shift() ?? caption ?? "", warning].filter(Boolean);
263
- const fallbackText = fallbackTextParts.join("\n");
264
- if (!fallbackText) {
265
- return;
266
- }
267
- whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
268
- rememberSendResult(
269
- await sendWithRetry(() => msg.reply(fallbackText, getQuote()), "media:fallback-text"),
270
- );
271
- },
272
- });
273
-
274
- // Remaining text chunks after media
275
- for (const chunk of remainingText) {
276
- rememberSendResult(await sendWithRetry(() => msg.reply(chunk, getQuote()), "media:text"));
277
- }
278
- return finishDelivery();
279
- }
@@ -1,6 +0,0 @@
1
- import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
2
-
3
- export const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp");
4
- export const whatsappInboundLog = whatsappLog.child("inbound");
5
- export const whatsappOutboundLog = whatsappLog.child("outbound");
6
- export const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
@@ -1,131 +0,0 @@
1
- import {
2
- buildMentionRegexes,
3
- normalizeMentionText,
4
- } from "openclaw/plugin-sdk/channel-mention-gating";
5
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
6
- import {
7
- getComparableIdentityValues,
8
- getMentionIdentities,
9
- getSelfIdentity,
10
- identitiesOverlap,
11
- type WhatsAppIdentity,
12
- } from "../identity.js";
13
- import { isWhatsAppGroupJid } from "../normalize-target.js";
14
- import { isSelfChatMode, normalizeE164 } from "../text-runtime.js";
15
- import type { WebInboundMsg } from "./types.js";
16
-
17
- export type MentionConfig = {
18
- mentionRegexes: RegExp[];
19
- allowFrom?: Array<string | number>;
20
- isSelfChat?: boolean;
21
- };
22
-
23
- export type MentionTargets = {
24
- normalizedMentions: WhatsAppIdentity[];
25
- self: WhatsAppIdentity;
26
- };
27
-
28
- export function buildMentionConfig(cfg: OpenClawConfig, agentId?: string): MentionConfig {
29
- const mentionRegexes = buildMentionRegexes(cfg, agentId);
30
- return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
31
- }
32
-
33
- export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
34
- const normalizedMentions = getMentionIdentities(msg, authDir);
35
- const self = getSelfIdentity(msg, authDir);
36
- return { normalizedMentions, self };
37
- }
38
-
39
- export function isBotMentionedFromTargets(
40
- msg: WebInboundMsg,
41
- mentionCfg: MentionConfig,
42
- targets: MentionTargets,
43
- ): boolean {
44
- const clean = (text: string) =>
45
- // Remove zero-width and directionality markers WhatsApp injects around display names
46
- normalizeMentionText(text);
47
-
48
- const explicitSelfChatOverride = typeof mentionCfg.isSelfChat === "boolean";
49
- // `isSelfChatMode` is a config-shaped check ("is the bot's own E.164 in
50
- // allowFrom?"), not a conversation-shaped check, so it returns true even
51
- // for group conversations whenever the operator put their own number in
52
- // allowFrom — which is the common config. The original mention-skip path
53
- // was designed to prevent owner-mentioning-self in a true 1:1 self DM
54
- // from falsely triggering the bot, so when we derive the flag implicitly
55
- // from `allowFrom`, confine the suppression to non-group conversations
56
- // and let real group @mentions go through the identity-overlap check
57
- // (#49317). Explicit `mentionCfg.isSelfChat` overrides from the caller
58
- // are honored as-is so multi-account / precomputed paths keep working.
59
- const isGroupConversation = isWhatsAppGroupJid(msg.from);
60
- const isSelfChat = explicitSelfChatOverride
61
- ? Boolean(mentionCfg.isSelfChat)
62
- : isSelfChatMode(targets.self.e164, mentionCfg.allowFrom) && !isGroupConversation;
63
-
64
- const hasMentions = targets.normalizedMentions.length > 0;
65
- if (hasMentions && !isSelfChat) {
66
- for (const mention of targets.normalizedMentions) {
67
- if (identitiesOverlap(targets.self, mention)) {
68
- return true;
69
- }
70
- }
71
- // If the message explicitly mentions someone else, do not fall back to regex matches.
72
- return false;
73
- } else if (hasMentions && isSelfChat) {
74
- // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in self-chat triggers the bot.
75
- }
76
- const bodyClean = clean(msg.body);
77
- if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) {
78
- return true;
79
- }
80
-
81
- // Fallback: detect body containing our own number (with or without +, spacing)
82
- if (targets.self.e164) {
83
- const selfDigits = targets.self.e164.replace(/\D/g, "");
84
- if (selfDigits) {
85
- const bodyDigits = bodyClean.replace(/[^\d]/g, "");
86
- if (bodyDigits.includes(selfDigits)) {
87
- return true;
88
- }
89
- const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
90
- const pattern = new RegExp(`\\+?${selfDigits}`, "i");
91
- if (pattern.test(bodyNoSpace)) {
92
- return true;
93
- }
94
- }
95
- }
96
-
97
- return false;
98
- }
99
-
100
- export function debugMention(
101
- msg: WebInboundMsg,
102
- mentionCfg: MentionConfig,
103
- authDir?: string,
104
- ): { wasMentioned: boolean; details: Record<string, unknown> } {
105
- const mentionTargets = resolveMentionTargets(msg, authDir);
106
- const result = isBotMentionedFromTargets(msg, mentionCfg, mentionTargets);
107
- const details = {
108
- from: msg.from,
109
- body: msg.body,
110
- bodyClean: normalizeMentionText(msg.body),
111
- mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
112
- normalizedMentionedJids: mentionTargets.normalizedMentions.length
113
- ? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity))
114
- : null,
115
- selfJid: msg.self?.jid ?? msg.selfJid ?? null,
116
- selfLid: msg.self?.lid ?? msg.selfLid ?? null,
117
- selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
118
- resolvedSelf: mentionTargets.self,
119
- };
120
- return { wasMentioned: result, details };
121
- }
122
-
123
- export function resolveOwnerList(mentionCfg: MentionConfig, selfE164?: string | null) {
124
- const allowFrom = mentionCfg.allowFrom;
125
- const raw =
126
- Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : selfE164 ? [selfE164] : [];
127
- return raw
128
- .filter((entry): entry is string => Boolean(entry && entry !== "*"))
129
- .map((entry) => normalizeE164(entry))
130
- .filter((entry): entry is string => Boolean(entry));
131
- }
@@ -1,170 +0,0 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import type { WhatsAppSendResult } from "../../inbound/send-result.js";
4
- import type { WebInboundMessage } from "../../inbound/types.js";
5
- import { maybeSendAckReaction } from "./ack-reaction.js";
6
-
7
- const hoisted = vi.hoisted(() => ({
8
- sendReactionWhatsApp: vi.fn(async () => undefined),
9
- }));
10
-
11
- vi.mock("../../send.js", () => ({
12
- sendReactionWhatsApp: hoisted.sendReactionWhatsApp,
13
- }));
14
-
15
- function acceptedSendResult(kind: "media" | "text", id: string): WhatsAppSendResult {
16
- return {
17
- kind,
18
- messageId: id,
19
- messageIds: [id],
20
- keys: [{ id }],
21
- providerAccepted: true,
22
- };
23
- }
24
-
25
- function createMessage(overrides: Partial<WebInboundMessage> = {}): WebInboundMessage {
26
- return {
27
- id: "msg-1",
28
- from: "15551234567",
29
- conversationId: "15551234567",
30
- to: "15559876543",
31
- accountId: "default",
32
- body: "hello",
33
- chatType: "direct",
34
- chatId: "15551234567@s.whatsapp.net",
35
- sendComposing: async () => {},
36
- reply: async () => acceptedSendResult("text", "r1"),
37
- sendMedia: async () => acceptedSendResult("media", "m1"),
38
- ...overrides,
39
- };
40
- }
41
-
42
- function createConfig(
43
- reactionLevel: "off" | "ack" | "minimal" | "extensive",
44
- extras?: Partial<NonNullable<OpenClawConfig["channels"]>["whatsapp"]>,
45
- ): OpenClawConfig {
46
- return {
47
- channels: {
48
- whatsapp: {
49
- reactionLevel,
50
- ackReaction: {
51
- emoji: "👀",
52
- direct: true,
53
- group: "mentions",
54
- },
55
- ...extras,
56
- },
57
- },
58
- } as OpenClawConfig;
59
- }
60
-
61
- type AckReactionParams = Parameters<typeof maybeSendAckReaction>[0];
62
-
63
- const runAckReaction = (overrides: Partial<AckReactionParams> = {}) =>
64
- maybeSendAckReaction({
65
- cfg: createConfig("ack"),
66
- msg: createMessage(),
67
- agentId: "agent",
68
- sessionKey: "whatsapp:default:15551234567",
69
- conversationId: "15551234567",
70
- verbose: false,
71
- accountId: "default",
72
- info: vi.fn(),
73
- warn: vi.fn(),
74
- ...overrides,
75
- });
76
-
77
- const expectAckReactionSent = (accountId: string) => {
78
- expect(hoisted.sendReactionWhatsApp).toHaveBeenCalledWith(
79
- "15551234567@s.whatsapp.net",
80
- "msg-1",
81
- "👀",
82
- expect.objectContaining({
83
- verbose: false,
84
- fromMe: false,
85
- accountId,
86
- }),
87
- );
88
- };
89
-
90
- describe("maybeSendAckReaction", () => {
91
- beforeEach(() => {
92
- vi.clearAllMocks();
93
- });
94
-
95
- it.each(["ack", "minimal", "extensive"] as const)(
96
- "sends ack reactions when reactionLevel is %s",
97
- async (reactionLevel) => {
98
- const ackReaction = await runAckReaction({
99
- cfg: createConfig(reactionLevel),
100
- });
101
-
102
- expect(ackReaction?.ackReactionValue).toBe("👀");
103
- await expect(ackReaction?.ackReactionPromise).resolves.toBe(true);
104
- expectAckReactionSent("default");
105
- },
106
- );
107
-
108
- it("suppresses ack reactions when reactionLevel is off", async () => {
109
- const ackReaction = await runAckReaction({
110
- cfg: createConfig("off"),
111
- });
112
-
113
- expect(ackReaction).toBeNull();
114
- expect(hoisted.sendReactionWhatsApp).not.toHaveBeenCalled();
115
- });
116
-
117
- it("uses the active account reactionLevel override for ack gating", async () => {
118
- const ackReaction = await runAckReaction({
119
- cfg: createConfig("off", {
120
- accounts: {
121
- work: {
122
- reactionLevel: "ack",
123
- },
124
- },
125
- }),
126
- msg: createMessage({
127
- accountId: "work",
128
- }),
129
- sessionKey: "whatsapp:work:15551234567",
130
- accountId: "work",
131
- });
132
-
133
- expect(ackReaction?.ackReactionValue).toBe("👀");
134
- expectAckReactionSent("work");
135
- });
136
-
137
- it("returns a handle that removes the ack with an empty reaction", async () => {
138
- const ackReaction = await runAckReaction();
139
-
140
- await ackReaction?.remove();
141
-
142
- expect(hoisted.sendReactionWhatsApp).toHaveBeenLastCalledWith(
143
- "15551234567@s.whatsapp.net",
144
- "msg-1",
145
- "",
146
- expect.objectContaining({
147
- verbose: false,
148
- fromMe: false,
149
- accountId: "default",
150
- }),
151
- );
152
- });
153
-
154
- it("records ack send failures on the handle", async () => {
155
- const warn = vi.fn();
156
- hoisted.sendReactionWhatsApp.mockRejectedValueOnce(new Error("session down"));
157
-
158
- const ackReaction = await runAckReaction({ warn });
159
-
160
- await expect(ackReaction?.ackReactionPromise).resolves.toBe(false);
161
- expect(warn).toHaveBeenCalledWith(
162
- expect.objectContaining({
163
- error: "session down",
164
- chatId: "15551234567@s.whatsapp.net",
165
- messageId: "msg-1",
166
- }),
167
- "failed to send ack reaction",
168
- );
169
- });
170
- });
@@ -1,99 +0,0 @@
1
- import {
2
- createAckReactionHandle,
3
- shouldAckReactionForWhatsApp,
4
- type AckReactionHandle,
5
- } from "openclaw/plugin-sdk/channel-feedback";
6
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
7
- import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
8
- import { getSenderIdentity } from "../../identity.js";
9
- import { resolveWhatsAppReactionLevel } from "../../reaction-level.js";
10
- import { sendReactionWhatsApp } from "../../send.js";
11
- import { formatError } from "../../session.js";
12
- import type { WebInboundMsg } from "../types.js";
13
- import { resolveGroupActivationFor } from "./group-activation.js";
14
-
15
- export async function maybeSendAckReaction(params: {
16
- cfg: OpenClawConfig;
17
- msg: WebInboundMsg;
18
- agentId: string;
19
- sessionKey: string;
20
- conversationId: string;
21
- verbose: boolean;
22
- accountId?: string;
23
- info: (obj: unknown, msg: string) => void;
24
- warn: (obj: unknown, msg: string) => void;
25
- }): Promise<AckReactionHandle | null> {
26
- if (!params.msg.id) {
27
- return null;
28
- }
29
-
30
- // Keep ackReaction as the emoji/scope control, while letting reactionLevel
31
- // suppress all automatic reactions when it is explicitly set to "off".
32
- const reactionLevel = resolveWhatsAppReactionLevel({
33
- cfg: params.cfg,
34
- accountId: params.accountId,
35
- });
36
- if (reactionLevel.level === "off") {
37
- return null;
38
- }
39
-
40
- const ackConfig = params.cfg.channels?.whatsapp?.ackReaction;
41
- const emoji = (ackConfig?.emoji ?? "").trim();
42
- const directEnabled = ackConfig?.direct ?? true;
43
- const groupMode = ackConfig?.group ?? "mentions";
44
- const conversationIdForCheck = params.msg.conversationId ?? params.msg.from;
45
-
46
- const activation =
47
- params.msg.chatType === "group"
48
- ? await resolveGroupActivationFor({
49
- cfg: params.cfg,
50
- accountId: params.accountId,
51
- agentId: params.agentId,
52
- sessionKey: params.sessionKey,
53
- conversationId: conversationIdForCheck,
54
- })
55
- : null;
56
- const shouldSendReaction = () =>
57
- shouldAckReactionForWhatsApp({
58
- emoji,
59
- isDirect: params.msg.chatType === "direct",
60
- isGroup: params.msg.chatType === "group",
61
- directEnabled,
62
- groupMode,
63
- wasMentioned: params.msg.wasMentioned === true,
64
- groupActivated: activation === "always",
65
- });
66
-
67
- if (!shouldSendReaction()) {
68
- return null;
69
- }
70
-
71
- params.info(
72
- { chatId: params.msg.chatId, messageId: params.msg.id, emoji },
73
- "sending ack reaction",
74
- );
75
- const sender = getSenderIdentity(params.msg);
76
- const reactionOptions = {
77
- verbose: params.verbose,
78
- fromMe: false,
79
- ...(sender.jid ? { participant: sender.jid } : {}),
80
- ...(params.accountId ? { accountId: params.accountId } : {}),
81
- cfg: params.cfg,
82
- };
83
- return createAckReactionHandle({
84
- ackReactionValue: emoji,
85
- send: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, emoji, reactionOptions),
86
- remove: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id!, "", reactionOptions),
87
- onSendError: (err) => {
88
- params.warn(
89
- {
90
- error: formatError(err),
91
- chatId: params.msg.chatId,
92
- messageId: params.msg.id,
93
- },
94
- "failed to send ack reaction",
95
- );
96
- logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`);
97
- },
98
- });
99
- }
@@ -1,9 +0,0 @@
1
- import { transcribeFirstAudio as transcribeFirstAudioImpl } from "openclaw/plugin-sdk/media-runtime";
2
-
3
- type TranscribeFirstAudio = typeof import("openclaw/plugin-sdk/media-runtime").transcribeFirstAudio;
4
-
5
- export async function transcribeFirstAudio(
6
- ...args: Parameters<TranscribeFirstAudio>
7
- ): ReturnType<TranscribeFirstAudio> {
8
- return await transcribeFirstAudioImpl(...args);
9
- }