@openclaw/whatsapp 2026.5.2 → 2026.5.3-beta.2

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 (302) 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 -339
  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 -319
  220. package/src/inbound/send-api.ts +0 -145
  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 -123
  244. package/src/normalize.ts +0 -7
  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 -288
  265. package/src/resolve-outbound-target.ts +0 -58
  266. package/src/resolve-target.test.ts +0 -120
  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 -534
  273. package/src/send.ts +0 -292
  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.test.ts +0 -41
  278. package/src/session-route.ts +0 -32
  279. package/src/session.runtime.ts +0 -8
  280. package/src/session.test.ts +0 -559
  281. package/src/session.ts +0 -330
  282. package/src/setup-core.ts +0 -52
  283. package/src/setup-finalize.ts +0 -446
  284. package/src/setup-surface.test.ts +0 -367
  285. package/src/setup-surface.ts +0 -69
  286. package/src/setup-test-helpers.ts +0 -216
  287. package/src/shared.ts +0 -291
  288. package/src/socket-timing.test.ts +0 -49
  289. package/src/socket-timing.ts +0 -38
  290. package/src/state-migrations.ts +0 -54
  291. package/src/status-issues.test.ts +0 -137
  292. package/src/status-issues.ts +0 -185
  293. package/src/system-prompt.test.ts +0 -199
  294. package/src/system-prompt.ts +0 -31
  295. package/src/targets-runtime.ts +0 -180
  296. package/src/test-helpers.ts +0 -690
  297. package/src/text-runtime.test.ts +0 -162
  298. package/src/text-runtime.ts +0 -11
  299. package/src/vcard.ts +0 -84
  300. package/targets.ts +0 -5
  301. package/test-api.ts +0 -2
  302. package/tsconfig.json +0 -16
@@ -1,244 +0,0 @@
1
- import {
2
- DEFAULT_ACCOUNT_ID,
3
- listCombinedAccountIds,
4
- normalizeOptionalAccountId,
5
- resolveListedDefaultAccountId,
6
- } from "openclaw/plugin-sdk/account-core";
7
- import {
8
- createAttachedChannelResultAdapter,
9
- type ChannelOutboundAdapter,
10
- } from "openclaw/plugin-sdk/channel-send-result";
11
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
12
- import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-send-deps";
13
- import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
14
- import {
15
- normalizeWhatsAppOutboundPayload,
16
- normalizeWhatsAppPayloadText,
17
- } from "./outbound-media-contract.js";
18
- import { WHATSAPP_LEGACY_OUTBOUND_SEND_DEP_KEYS } from "./outbound-send-deps.js";
19
- import { lookupInboundMessageMetaForTarget } from "./quoted-message.js";
20
- import { toWhatsappJid } from "./text-runtime.js";
21
-
22
- type WhatsAppChunker = NonNullable<ChannelOutboundAdapter["chunker"]>;
23
- type WhatsAppSendTextOptions = {
24
- verbose: boolean;
25
- cfg: OpenClawConfig;
26
- mediaUrl?: string;
27
- mediaAccess?: {
28
- localRoots?: readonly string[];
29
- readFile?: (filePath: string) => Promise<Buffer>;
30
- };
31
- mediaLocalRoots?: readonly string[];
32
- mediaReadFile?: (filePath: string) => Promise<Buffer>;
33
- gifPlayback?: boolean;
34
- audioAsVoice?: boolean;
35
- accountId?: string;
36
- quotedMessageKey?: {
37
- id: string;
38
- remoteJid: string;
39
- fromMe: boolean;
40
- participant?: string;
41
- messageText?: string;
42
- };
43
- preserveLeadingWhitespace?: boolean;
44
- };
45
- type WhatsAppSendMessage = (
46
- to: string,
47
- body: string,
48
- options: WhatsAppSendTextOptions,
49
- ) => Promise<{ messageId: string; toJid: string }>;
50
- type WhatsAppSendPoll = (
51
- to: string,
52
- poll: Parameters<NonNullable<ChannelOutboundAdapter["sendPoll"]>>[0]["poll"],
53
- options: { verbose: boolean; accountId?: string; cfg: OpenClawConfig },
54
- ) => Promise<{ messageId: string; toJid: string }>;
55
-
56
- type CreateWhatsAppOutboundBaseParams = {
57
- chunker: WhatsAppChunker;
58
- sendMessageWhatsApp: WhatsAppSendMessage;
59
- sendPollWhatsApp: WhatsAppSendPoll;
60
- shouldLogVerbose: () => boolean;
61
- resolveTarget: ChannelOutboundAdapter["resolveTarget"];
62
- normalizeText?: (text: string | undefined) => string;
63
- skipEmptyText?: boolean;
64
- };
65
-
66
- function resolveQuoteLookupAccountId(cfg?: OpenClawConfig, accountId?: string | null): string {
67
- const explicitAccountId = normalizeOptionalAccountId(accountId);
68
- if (explicitAccountId) {
69
- return explicitAccountId;
70
- }
71
- const channelCfg = cfg?.channels?.whatsapp;
72
- const configuredIds = listCombinedAccountIds({
73
- configuredAccountIds:
74
- channelCfg?.accounts && typeof channelCfg.accounts === "object"
75
- ? Object.keys(channelCfg.accounts).filter(Boolean)
76
- : [],
77
- fallbackAccountIdWhenEmpty: DEFAULT_ACCOUNT_ID,
78
- });
79
- return resolveListedDefaultAccountId({
80
- accountIds: configuredIds,
81
- configuredDefaultAccountId: normalizeOptionalAccountId(channelCfg?.defaultAccount),
82
- });
83
- }
84
-
85
- type WhatsAppOutboundBaseCore = Pick<
86
- ChannelOutboundAdapter,
87
- | "deliveryMode"
88
- | "chunker"
89
- | "chunkerMode"
90
- | "textChunkLimit"
91
- | "sanitizeText"
92
- | "pollMaxOptions"
93
- | "resolveTarget"
94
- | "sendText"
95
- | "sendMedia"
96
- | "sendPoll"
97
- >;
98
-
99
- export function createWhatsAppOutboundBase({
100
- chunker,
101
- sendMessageWhatsApp,
102
- sendPollWhatsApp,
103
- shouldLogVerbose,
104
- resolveTarget,
105
- normalizeText = normalizeWhatsAppPayloadText,
106
- skipEmptyText = true,
107
- }: CreateWhatsAppOutboundBaseParams): Pick<
108
- ChannelOutboundAdapter,
109
- | "deliveryMode"
110
- | "chunker"
111
- | "chunkerMode"
112
- | "textChunkLimit"
113
- | "sanitizeText"
114
- | "pollMaxOptions"
115
- | "resolveTarget"
116
- | "sendPayload"
117
- | "sendText"
118
- | "sendMedia"
119
- | "sendPoll"
120
- > {
121
- const resolveQuotedMessageKey = (params: {
122
- accountId: string;
123
- to: string;
124
- replyToId?: string | null;
125
- }) => {
126
- const replyToId = params.replyToId?.trim();
127
- if (!replyToId) {
128
- return undefined;
129
- }
130
- const targetJid = toWhatsappJid(params.to);
131
- const cachedMeta = lookupInboundMessageMetaForTarget(params.accountId, targetJid, replyToId);
132
- return {
133
- id: replyToId,
134
- remoteJid: cachedMeta?.remoteJid ?? targetJid,
135
- fromMe: cachedMeta?.fromMe ?? false,
136
- participant: cachedMeta?.participant,
137
- messageText: cachedMeta?.body,
138
- };
139
- };
140
-
141
- const outbound: WhatsAppOutboundBaseCore = {
142
- deliveryMode: "gateway",
143
- chunker,
144
- chunkerMode: "text",
145
- textChunkLimit: 4000,
146
- sanitizeText: ({ text }) => normalizeText(text),
147
- pollMaxOptions: 12,
148
- resolveTarget,
149
- ...createAttachedChannelResultAdapter({
150
- channel: "whatsapp",
151
- sendText: async ({ cfg, to, text, accountId, deps, gifPlayback, replyToId }) => {
152
- const normalizedText = normalizeText(text);
153
- if (skipEmptyText && !normalizedText) {
154
- return { messageId: "" };
155
- }
156
- const send =
157
- resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp", {
158
- legacyKeys: WHATSAPP_LEGACY_OUTBOUND_SEND_DEP_KEYS,
159
- }) ?? sendMessageWhatsApp;
160
- const lookupAccountId = resolveQuoteLookupAccountId(cfg, accountId);
161
- const quotedMessageKey = resolveQuotedMessageKey({
162
- accountId: lookupAccountId,
163
- to,
164
- replyToId,
165
- });
166
- return await send(to, normalizedText, {
167
- verbose: false,
168
- cfg,
169
- accountId: accountId ?? undefined,
170
- gifPlayback,
171
- quotedMessageKey,
172
- });
173
- },
174
- sendMedia: async ({
175
- cfg,
176
- to,
177
- text,
178
- mediaUrl,
179
- mediaAccess,
180
- mediaLocalRoots,
181
- mediaReadFile,
182
- audioAsVoice,
183
- accountId,
184
- deps,
185
- gifPlayback,
186
- replyToId,
187
- }) => {
188
- const send =
189
- resolveOutboundSendDep<WhatsAppSendMessage>(deps, "whatsapp", {
190
- legacyKeys: WHATSAPP_LEGACY_OUTBOUND_SEND_DEP_KEYS,
191
- }) ?? sendMessageWhatsApp;
192
- const lookupAccountId = resolveQuoteLookupAccountId(cfg, accountId);
193
- const quotedMessageKey = resolveQuotedMessageKey({
194
- accountId: lookupAccountId,
195
- to,
196
- replyToId,
197
- });
198
- return await send(to, normalizeText(text), {
199
- verbose: false,
200
- cfg,
201
- mediaUrl,
202
- mediaAccess,
203
- mediaLocalRoots,
204
- mediaReadFile,
205
- ...(audioAsVoice === undefined ? {} : { audioAsVoice }),
206
- accountId: accountId ?? undefined,
207
- gifPlayback,
208
- quotedMessageKey,
209
- });
210
- },
211
- sendPoll: async ({ cfg, to, poll, accountId }) =>
212
- await sendPollWhatsApp(to, poll, {
213
- verbose: shouldLogVerbose(),
214
- accountId: accountId ?? undefined,
215
- cfg,
216
- }),
217
- }),
218
- };
219
- return {
220
- ...outbound,
221
- sendPayload: async (ctx) => {
222
- if (ctx.payload.isError === true) {
223
- return { channel: "whatsapp", messageId: "" };
224
- }
225
- const payload = normalizeWhatsAppOutboundPayload(ctx.payload, { normalizeText });
226
- if (!payload.text && !(payload.mediaUrl || payload.mediaUrls?.length)) {
227
- if (ctx.payload.interactive || ctx.payload.presentation || ctx.payload.channelData) {
228
- throw new Error(
229
- "WhatsApp sendPayload does not support structured-only payloads without text or media.",
230
- );
231
- }
232
- return { channel: "whatsapp", messageId: "" };
233
- }
234
- return await sendTextMediaPayload({
235
- channel: "whatsapp",
236
- ctx: {
237
- ...ctx,
238
- payload,
239
- },
240
- adapter: outbound,
241
- });
242
- },
243
- };
244
- }
@@ -1,278 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
4
- import { sanitizeForPlainText } from "openclaw/plugin-sdk/outbound-runtime";
5
- import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
6
- import { formatError } from "./session-errors.js";
7
- import {
8
- sanitizeAssistantVisibleText,
9
- sanitizeAssistantVisibleTextWithProfile,
10
- stripToolCallXmlTags,
11
- sleep,
12
- } from "./text-runtime.js";
13
-
14
- type WhatsAppOutboundPayloadLike = {
15
- text?: string;
16
- mediaUrl?: string;
17
- mediaUrls?: readonly string[];
18
- };
19
-
20
- type WhatsAppLoadedMediaLike = {
21
- buffer: Buffer;
22
- contentType?: string;
23
- kind?: string;
24
- fileName?: string;
25
- };
26
-
27
- type NormalizedWhatsAppOutboundPayload<T extends WhatsAppOutboundPayloadLike> = Omit<
28
- T,
29
- "text" | "mediaUrl" | "mediaUrls"
30
- > & {
31
- text: string;
32
- mediaUrl?: string;
33
- mediaUrls?: string[];
34
- };
35
-
36
- export type DeliverableWhatsAppOutboundPayload<T extends WhatsAppOutboundPayloadLike> = Omit<
37
- NormalizedWhatsAppOutboundPayload<T>,
38
- "text"
39
- > & {
40
- text?: string;
41
- };
42
-
43
- type CanonicalWhatsAppLoadedMedia = {
44
- buffer: Buffer;
45
- kind: "image" | "audio" | "video" | "document";
46
- mimetype: string;
47
- fileName?: string;
48
- };
49
-
50
- const WHATSAPP_VOICE_FILE_NAME = "voice.ogg";
51
- const WHATSAPP_VOICE_SAMPLE_RATE_HZ = 48_000;
52
- const WHATSAPP_VOICE_BITRATE = "64k";
53
- const WHATSAPP_VOICE_MIMETYPE = "audio/ogg; codecs=opus";
54
-
55
- function stripWhatsAppPluralToolXml(text: string): string {
56
- return stripToolCallXmlTags(text, { stripFunctionCallsXmlPayloads: true });
57
- }
58
-
59
- function finalizeWhatsAppVisibleText(text: string): string {
60
- return sanitizeForPlainText(stripWhatsAppPluralToolXml(text));
61
- }
62
-
63
- export function normalizeWhatsAppPayloadText(text: string | undefined): string {
64
- return finalizeWhatsAppVisibleText(sanitizeAssistantVisibleText(text ?? "")).trimStart();
65
- }
66
-
67
- function stripLeadingBlankLines(text: string): string {
68
- return text.replace(/^(?:[ \t]*\r?\n)+/, "");
69
- }
70
-
71
- export function normalizeWhatsAppPayloadTextPreservingIndentation(
72
- text: string | undefined,
73
- ): string {
74
- const sanitized = sanitizeAssistantVisibleTextWithProfile(
75
- stripLeadingBlankLines(text ?? ""),
76
- "history",
77
- );
78
- const normalized = stripLeadingBlankLines(finalizeWhatsAppVisibleText(sanitized));
79
- return normalized.trim() ? normalized : "";
80
- }
81
-
82
- export function resolveWhatsAppOutboundMediaUrls(
83
- payload: Pick<WhatsAppOutboundPayloadLike, "mediaUrl" | "mediaUrls">,
84
- ): string[] {
85
- const primaryMediaUrl = payload.mediaUrl?.trim();
86
- const mediaUrls = (payload.mediaUrls ? [...payload.mediaUrls] : [])
87
- .map((entry) => entry.trim())
88
- .filter((entry): entry is string => Boolean(entry));
89
- const orderedMediaUrls = [primaryMediaUrl, ...mediaUrls].filter((entry): entry is string =>
90
- Boolean(entry),
91
- );
92
- return Array.from(new Set(orderedMediaUrls));
93
- }
94
-
95
- // Keep new WhatsApp outbound-media behavior in this helper so payload, gateway, and auto-reply paths stay aligned.
96
- export function normalizeWhatsAppOutboundPayload<T extends WhatsAppOutboundPayloadLike>(
97
- payload: T,
98
- options?: {
99
- normalizeText?: (text: string | undefined) => string;
100
- },
101
- ): NormalizedWhatsAppOutboundPayload<T> {
102
- const mediaUrls = resolveWhatsAppOutboundMediaUrls(payload);
103
- const normalizeText = options?.normalizeText ?? normalizeWhatsAppPayloadText;
104
- return {
105
- ...payload,
106
- text: normalizeText(payload.text),
107
- mediaUrl: mediaUrls[0],
108
- mediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
109
- };
110
- }
111
-
112
- function normalizeWhatsAppLoadedMedia(
113
- media: WhatsAppLoadedMediaLike,
114
- mediaUrl?: string,
115
- ): CanonicalWhatsAppLoadedMedia {
116
- const kind =
117
- media.kind === "image" || media.kind === "audio" || media.kind === "video"
118
- ? media.kind
119
- : "document";
120
- const mimetype =
121
- kind === "audio" && isWhatsAppNativeVoiceAudio({ contentType: media.contentType, mediaUrl })
122
- ? WHATSAPP_VOICE_MIMETYPE
123
- : (media.contentType ?? "application/octet-stream");
124
- const fileName =
125
- kind === "document"
126
- ? (media.fileName ?? deriveWhatsAppDocumentFileName(mediaUrl) ?? "file")
127
- : undefined;
128
- return {
129
- buffer: media.buffer,
130
- kind,
131
- mimetype,
132
- ...(fileName ? { fileName } : {}),
133
- };
134
- }
135
-
136
- export async function prepareWhatsAppOutboundMedia(
137
- media: WhatsAppLoadedMediaLike,
138
- mediaUrl?: string,
139
- ): Promise<CanonicalWhatsAppLoadedMedia> {
140
- const normalized = normalizeWhatsAppLoadedMedia(media, mediaUrl);
141
- if (normalized.kind !== "audio") {
142
- return normalized;
143
- }
144
- if (
145
- isWhatsAppNativeVoiceAudio({
146
- contentType: media.contentType,
147
- fileName: media.fileName,
148
- mediaUrl,
149
- })
150
- ) {
151
- return normalized;
152
- }
153
-
154
- const buffer = await transcodeToWhatsAppVoiceOpus({
155
- buffer: media.buffer,
156
- fileName: media.fileName ?? deriveWhatsAppDocumentFileName(mediaUrl) ?? "audio",
157
- });
158
- return {
159
- buffer,
160
- kind: "audio",
161
- mimetype: WHATSAPP_VOICE_MIMETYPE,
162
- };
163
- }
164
-
165
- function normalizeContentType(value: string | undefined): string {
166
- return value?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
167
- }
168
-
169
- function isWhatsAppNativeVoiceAudio(params: {
170
- contentType?: string;
171
- fileName?: string;
172
- mediaUrl?: string;
173
- }): boolean {
174
- const contentType = normalizeContentType(params.contentType);
175
- if (contentType === "audio/ogg" || contentType === "audio/opus") {
176
- return true;
177
- }
178
- const fileName = params.fileName ?? deriveWhatsAppDocumentFileName(params.mediaUrl) ?? "";
179
- const ext = path.extname(fileName).toLowerCase();
180
- return ext === ".ogg" || ext === ".opus";
181
- }
182
-
183
- async function transcodeToWhatsAppVoiceOpus(params: {
184
- buffer: Buffer;
185
- fileName: string;
186
- }): Promise<Buffer> {
187
- const tempRoot = resolvePreferredOpenClawTmpDir();
188
- await fs.mkdir(tempRoot, { recursive: true, mode: 0o700 });
189
- const tempDir = await fs.mkdtemp(path.join(tempRoot, "whatsapp-voice-"));
190
- try {
191
- const ext = path.extname(params.fileName).toLowerCase();
192
- const inputExt = ext && ext.length <= 12 ? ext : ".audio";
193
- const inputPath = path.join(tempDir, `input${inputExt}`);
194
- const outputPath = path.join(tempDir, WHATSAPP_VOICE_FILE_NAME);
195
- await fs.writeFile(inputPath, params.buffer, { mode: 0o600 });
196
- await runFfmpeg([
197
- "-hide_banner",
198
- "-loglevel",
199
- "error",
200
- "-y",
201
- "-i",
202
- inputPath,
203
- "-vn",
204
- "-sn",
205
- "-dn",
206
- "-t",
207
- String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
208
- "-ar",
209
- String(WHATSAPP_VOICE_SAMPLE_RATE_HZ),
210
- "-ac",
211
- "1",
212
- "-c:a",
213
- "libopus",
214
- "-b:a",
215
- WHATSAPP_VOICE_BITRATE,
216
- outputPath,
217
- ]);
218
- return await fs.readFile(outputPath);
219
- } finally {
220
- await fs.rm(tempDir, { recursive: true, force: true });
221
- }
222
- }
223
-
224
- function deriveWhatsAppDocumentFileName(mediaUrl: string | undefined): string | undefined {
225
- if (!mediaUrl) {
226
- return undefined;
227
- }
228
- try {
229
- const parsed = new URL(mediaUrl);
230
- const fileName = path.posix.basename(parsed.pathname);
231
- return fileName ? decodeURIComponent(fileName) : undefined;
232
- } catch {
233
- const withoutQueryOrFragment = mediaUrl.split(/[?#]/, 1)[0] ?? "";
234
- const fileName = withoutQueryOrFragment.split(/[\\/]/).pop();
235
- return fileName || undefined;
236
- }
237
- }
238
-
239
- function isRetryableWhatsAppOutboundError(error: unknown): boolean {
240
- return /closed|reset|timed\s*out|disconnect/i.test(formatError(error));
241
- }
242
-
243
- export async function sendWhatsAppOutboundWithRetry<T>(params: {
244
- send: () => Promise<T>;
245
- onRetry?: (params: {
246
- attempt: number;
247
- maxAttempts: number;
248
- backoffMs: number;
249
- error: unknown;
250
- errorText: string;
251
- }) => Promise<void> | void;
252
- maxAttempts?: number;
253
- }): Promise<T> {
254
- const maxAttempts = params.maxAttempts ?? 3;
255
- let lastError: unknown;
256
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
257
- try {
258
- return await params.send();
259
- } catch (error) {
260
- lastError = error;
261
- const errorText = formatError(error);
262
- const isLastAttempt = attempt === maxAttempts;
263
- if (!isRetryableWhatsAppOutboundError(error) || isLastAttempt) {
264
- throw error;
265
- }
266
- const backoffMs = 500 * attempt;
267
- await params.onRetry?.({
268
- attempt,
269
- maxAttempts,
270
- backoffMs,
271
- error,
272
- errorText,
273
- });
274
- await sleep(backoffMs);
275
- }
276
- }
277
- throw lastError;
278
- }
@@ -1,36 +0,0 @@
1
- import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
2
-
3
- export async function loadOutboundMediaFromUrl(
4
- mediaUrl: string,
5
- options: {
6
- maxBytes?: number;
7
- mediaAccess?: {
8
- localRoots?: readonly string[];
9
- readFile?: (filePath: string) => Promise<Buffer>;
10
- };
11
- mediaLocalRoots?: readonly string[];
12
- mediaReadFile?: (filePath: string) => Promise<Buffer>;
13
- } = {},
14
- ) {
15
- const readFile = options.mediaAccess?.readFile ?? options.mediaReadFile;
16
- const localRoots =
17
- options.mediaAccess?.localRoots?.length && options.mediaAccess.localRoots.length > 0
18
- ? options.mediaAccess.localRoots
19
- : options.mediaLocalRoots && options.mediaLocalRoots.length > 0
20
- ? options.mediaLocalRoots
21
- : undefined;
22
- return await loadWebMedia(
23
- mediaUrl,
24
- readFile
25
- ? {
26
- ...(options.maxBytes !== undefined ? { maxBytes: options.maxBytes } : {}),
27
- localRoots: "any",
28
- readFile,
29
- hostReadCapability: true,
30
- }
31
- : {
32
- ...(options.maxBytes !== undefined ? { maxBytes: options.maxBytes } : {}),
33
- ...(localRoots ? { localRoots } : {}),
34
- },
35
- );
36
- }
@@ -1,61 +0,0 @@
1
- import {
2
- installChannelOutboundPayloadContractSuite,
3
- primeChannelOutboundSendMock,
4
- type OutboundPayloadHarnessParams,
5
- } from "openclaw/plugin-sdk/channel-contract-testing";
6
- import { describe, expect, it, vi } from "vitest";
7
- import { whatsappOutbound } from "./outbound-adapter.js";
8
-
9
- function createWhatsAppHarness(params: OutboundPayloadHarnessParams) {
10
- const sendWhatsApp = vi.fn();
11
- primeChannelOutboundSendMock(sendWhatsApp, { messageId: "wa-1" }, params.sendResults);
12
- const ctx = {
13
- cfg: {},
14
- to: "5511999999999@c.us",
15
- text: "",
16
- payload: params.payload,
17
- deps: {
18
- whatsapp: sendWhatsApp,
19
- },
20
- };
21
- return {
22
- run: async () => await whatsappOutbound.sendPayload!(ctx),
23
- sendMock: sendWhatsApp,
24
- to: ctx.to,
25
- };
26
- }
27
-
28
- describe("WhatsApp outbound payload contract", () => {
29
- installChannelOutboundPayloadContractSuite({
30
- channel: "whatsapp",
31
- chunking: { mode: "split", longTextLength: 5000, maxChunkLength: 4000 },
32
- createHarness: createWhatsAppHarness,
33
- });
34
-
35
- it("normalizes blank mediaUrls before contract delivery", async () => {
36
- const sendWhatsApp = vi.fn();
37
- primeChannelOutboundSendMock(sendWhatsApp, { messageId: "wa-1" });
38
-
39
- await whatsappOutbound.sendPayload!({
40
- cfg: {},
41
- to: "5511999999999@c.us",
42
- text: "",
43
- payload: {
44
- text: "\n\ncaption",
45
- mediaUrls: [" ", " /tmp/voice.ogg "],
46
- },
47
- deps: {
48
- whatsapp: sendWhatsApp,
49
- },
50
- });
51
-
52
- expect(sendWhatsApp).toHaveBeenCalledTimes(1);
53
- expect(sendWhatsApp).toHaveBeenCalledWith(
54
- "5511999999999@c.us",
55
- "caption",
56
- expect.objectContaining({
57
- mediaUrl: "/tmp/voice.ogg",
58
- }),
59
- );
60
- });
61
- });
@@ -1 +0,0 @@
1
- export const WHATSAPP_LEGACY_OUTBOUND_SEND_DEP_KEYS = ["sendWhatsApp"] as const;
@@ -1,16 +0,0 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
-
3
- export function createWhatsAppPollFixture() {
4
- const cfg = { marker: "resolved-cfg" } as OpenClawConfig;
5
- const poll = {
6
- question: "Lunch?",
7
- options: ["Pizza", "Sushi"],
8
- maxSelections: 1,
9
- };
10
- return {
11
- cfg,
12
- poll,
13
- to: "+1555",
14
- accountId: "work",
15
- };
16
- }
@@ -1,59 +0,0 @@
1
- import { vi, type Mock } from "vitest";
2
-
3
- export type AsyncMock<TArgs extends unknown[] = unknown[], TResult = unknown> = {
4
- (...args: TArgs): Promise<TResult>;
5
- mockReset: () => AsyncMock<TArgs, TResult>;
6
- mockResolvedValue: (value: TResult) => AsyncMock<TArgs, TResult>;
7
- mockResolvedValueOnce: (value: TResult) => AsyncMock<TArgs, TResult>;
8
- };
9
- type UnknownMock = Mock<(...args: unknown[]) => unknown>;
10
-
11
- export const loadConfigMock: UnknownMock = vi.fn();
12
- export const readAllowFromStoreMock = vi.fn() as AsyncMock;
13
- export const upsertPairingRequestMock = vi.fn() as AsyncMock;
14
-
15
- export function resetPairingSecurityMocks(config: Record<string, unknown>) {
16
- loadConfigMock.mockReset().mockReturnValue(config);
17
- readAllowFromStoreMock.mockReset().mockResolvedValue([]);
18
- upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
19
- }
20
-
21
- vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
22
- const actual = await vi.importActual<
23
- typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
24
- >("openclaw/plugin-sdk/runtime-config-snapshot");
25
- return {
26
- ...actual,
27
- getRuntimeConfig: (...args: unknown[]) => loadConfigMock(...args),
28
- };
29
- });
30
-
31
- vi.mock("openclaw/plugin-sdk/conversation-runtime", () => {
32
- return {
33
- upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
34
- };
35
- });
36
-
37
- vi.mock("openclaw/plugin-sdk/security-runtime", async () => {
38
- const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/security-runtime")>(
39
- "openclaw/plugin-sdk/security-runtime",
40
- );
41
- return {
42
- ...actual,
43
- readStoreAllowFromForDmPolicy: async (params: {
44
- provider: string;
45
- accountId: string;
46
- dmPolicy?: string | null;
47
- shouldRead?: boolean | null;
48
- }) => {
49
- if (params.shouldRead === false || params.dmPolicy === "allowlist") {
50
- return [];
51
- }
52
- try {
53
- return (await readAllowFromStoreMock(params.provider, params.accountId)) as string[];
54
- } catch {
55
- return [];
56
- }
57
- },
58
- };
59
- });
package/src/qr-image.ts DELETED
@@ -1 +0,0 @@
1
- export { renderQrPngBase64, renderQrPngDataUrl } from "openclaw/plugin-sdk/media-runtime";
@@ -1 +0,0 @@
1
- export { renderQrTerminal } from "openclaw/plugin-sdk/media-runtime";