@openclaw/whatsapp 2026.5.1-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.
- package/action-runtime-api.ts +1 -0
- package/action-runtime.runtime.ts +1 -0
- package/api.ts +213 -0
- package/auth-presence.ts +80 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +3 -0
- package/config-api.ts +4 -0
- package/constants.ts +1 -0
- package/contract-api.ts +29 -0
- package/directory-contract-api.ts +4 -0
- package/dist/.boundary-tsc.stamp +1 -0
- package/dist/.boundary-tsc.tsbuildinfo +1 -0
- package/doctor-contract-api.ts +8 -0
- package/index.test.ts +13 -0
- package/index.ts +16 -0
- package/legacy-session-surface-api.ts +6 -0
- package/legacy-state-migrations-api.ts +1 -0
- package/light-runtime-api.ts +12 -0
- package/login-qr-api.ts +1 -0
- package/login-qr-runtime.ts +23 -0
- package/openclaw.plugin.json +742 -0
- package/outbound-payload-test-api.ts +1 -0
- package/package.json +74 -0
- package/runtime-api.ts +86 -0
- package/secret-contract-api.ts +4 -0
- package/security-contract-api.ts +4 -0
- package/setup-entry.test.ts +21 -0
- package/setup-entry.ts +21 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-config.ts +77 -0
- package/src/account-ids.ts +13 -0
- package/src/account-types.ts +5 -0
- package/src/accounts.test.ts +182 -0
- package/src/accounts.ts +176 -0
- package/src/accounts.whatsapp-auth.test.ts +59 -0
- package/src/action-runtime-target-auth.ts +27 -0
- package/src/action-runtime.test.ts +330 -0
- package/src/action-runtime.ts +76 -0
- package/src/active-listener.test.ts +65 -0
- package/src/active-listener.ts +17 -0
- package/src/agent-tools-login.test.ts +81 -0
- package/src/agent-tools-login.ts +113 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +27 -0
- package/src/auth-store.runtime.ts +1 -0
- package/src/auth-store.test.ts +311 -0
- package/src/auth-store.ts +502 -0
- package/src/auto-reply/config.runtime.ts +16 -0
- package/src/auto-reply/constants.ts +1 -0
- package/src/auto-reply/deliver-reply.test.ts +843 -0
- package/src/auto-reply/deliver-reply.ts +279 -0
- package/src/auto-reply/heartbeat-runner.runtime.ts +33 -0
- package/src/auto-reply/heartbeat-runner.test.ts +214 -0
- package/src/auto-reply/heartbeat-runner.ts +330 -0
- package/src/auto-reply/loggers.ts +6 -0
- package/src/auto-reply/mentions.ts +131 -0
- package/src/auto-reply/monitor/ack-reaction.test.ts +170 -0
- package/src/auto-reply/monitor/ack-reaction.ts +99 -0
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
- package/src/auto-reply/monitor/broadcast.ts +153 -0
- package/src/auto-reply/monitor/commands.ts +19 -0
- package/src/auto-reply/monitor/echo.ts +64 -0
- package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
- package/src/auto-reply/monitor/group-activation.test.ts +189 -0
- package/src/auto-reply/monitor/group-activation.ts +73 -0
- package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +103 -0
- package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
- package/src/auto-reply/monitor/group-gating.ts +217 -0
- package/src/auto-reply/monitor/group-members.test.ts +56 -0
- package/src/auto-reply/monitor/group-members.ts +65 -0
- package/src/auto-reply/monitor/inbound-context.test.ts +97 -0
- package/src/auto-reply/monitor/inbound-context.ts +92 -0
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +21 -0
- package/src/auto-reply/monitor/inbound-dispatch.test.ts +895 -0
- package/src/auto-reply/monitor/inbound-dispatch.ts +422 -0
- package/src/auto-reply/monitor/last-route.test.ts +37 -0
- package/src/auto-reply/monitor/last-route.ts +61 -0
- package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
- package/src/auto-reply/monitor/message-line.ts +54 -0
- package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +354 -0
- package/src/auto-reply/monitor/on-message.ts +291 -0
- package/src/auto-reply/monitor/peer.ts +17 -0
- package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +420 -0
- package/src/auto-reply/monitor/process-message.test.ts +355 -0
- package/src/auto-reply/monitor/process-message.ts +531 -0
- package/src/auto-reply/monitor/runtime-api.ts +30 -0
- package/src/auto-reply/monitor-state.test.ts +64 -0
- package/src/auto-reply/monitor-state.ts +106 -0
- package/src/auto-reply/monitor.ts +600 -0
- package/src/auto-reply/reply-resolver.runtime.ts +1 -0
- package/src/auto-reply/session-snapshot.ts +69 -0
- package/src/auto-reply/types.ts +48 -0
- package/src/auto-reply/util.ts +63 -0
- package/src/auto-reply/web-auto-reply-monitor.test.ts +687 -0
- package/src/auto-reply/web-auto-reply-utils.test.ts +327 -0
- package/src/auto-reply.broadcast-groups.combined.test.ts +246 -0
- package/src/auto-reply.broadcast-groups.test-harness.ts +47 -0
- package/src/auto-reply.impl.ts +7 -0
- package/src/auto-reply.test-harness.ts +421 -0
- package/src/auto-reply.ts +1 -0
- package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +427 -0
- package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +908 -0
- package/src/auto-reply.web-auto-reply.last-route.test.ts +184 -0
- package/src/channel-actions.runtime.ts +7 -0
- package/src/channel-actions.test.ts +216 -0
- package/src/channel-actions.ts +84 -0
- package/src/channel-outbound.test.ts +131 -0
- package/src/channel-outbound.ts +36 -0
- package/src/channel-react-action.runtime.ts +7 -0
- package/src/channel-react-action.test.ts +294 -0
- package/src/channel-react-action.ts +84 -0
- package/src/channel.runtime.ts +117 -0
- package/src/channel.setup.test.ts +461 -0
- package/src/channel.setup.ts +28 -0
- package/src/channel.ts +336 -0
- package/src/command-policy.ts +7 -0
- package/src/config-accessors.test.ts +34 -0
- package/src/config-accessors.ts +22 -0
- package/src/config-schema.test.ts +121 -0
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +24 -0
- package/src/connection-controller-registry.test.ts +27 -0
- package/src/connection-controller-registry.ts +49 -0
- package/src/connection-controller.test.ts +282 -0
- package/src/connection-controller.ts +679 -0
- package/src/creds-files.ts +19 -0
- package/src/creds-persistence.ts +102 -0
- package/src/directory-config.test.ts +60 -0
- package/src/directory-config.ts +40 -0
- package/src/directory-contract.test.ts +56 -0
- package/src/doctor-contract.ts +11 -0
- package/src/doctor.test.ts +74 -0
- package/src/doctor.ts +56 -0
- package/src/group-intro.ts +15 -0
- package/src/group-policy.test.ts +36 -0
- package/src/group-policy.ts +40 -0
- package/src/group-session-contract.ts +20 -0
- package/src/group-session-key.test.ts +53 -0
- package/src/group-session-key.ts +41 -0
- package/src/heartbeat-recipients.runtime.ts +6 -0
- package/src/heartbeat-recipients.test.ts +203 -0
- package/src/heartbeat-recipients.ts +104 -0
- package/src/heartbeat.ts +34 -0
- package/src/identity.ts +164 -0
- package/src/inbound/access-control.test-harness.ts +37 -0
- package/src/inbound/access-control.test.ts +375 -0
- package/src/inbound/access-control.ts +237 -0
- package/src/inbound/dedupe.ts +132 -0
- package/src/inbound/extract.test.ts +282 -0
- package/src/inbound/extract.ts +488 -0
- package/src/inbound/lifecycle.ts +39 -0
- package/src/inbound/media.node.test.ts +83 -0
- package/src/inbound/media.ts +102 -0
- package/src/inbound/monitor.ts +890 -0
- package/src/inbound/runtime-api.ts +7 -0
- package/src/inbound/save-media.runtime.ts +1 -0
- package/src/inbound/send-api.test.ts +307 -0
- package/src/inbound/send-api.ts +141 -0
- package/src/inbound/send-result.ts +61 -0
- package/src/inbound/types.ts +106 -0
- package/src/inbound-context.contract.test.ts +32 -0
- package/src/inbound-policy.ts +231 -0
- package/src/inbound.media.test.ts +320 -0
- package/src/inbound.test.ts +290 -0
- package/src/inbound.ts +9 -0
- package/src/login-qr.test.ts +483 -0
- package/src/login-qr.ts +542 -0
- package/src/login.coverage.test.ts +133 -0
- package/src/login.test.ts +89 -0
- package/src/login.ts +70 -0
- package/src/logout.test.ts +162 -0
- package/src/media.test.ts +480 -0
- package/src/media.ts +10 -0
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +400 -0
- package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
- package/src/monitor-inbox.behavior.test.ts +5 -0
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +362 -0
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +306 -0
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +729 -0
- package/src/monitor-inbox.test-harness.ts +304 -0
- package/src/normalize-target.ts +112 -0
- package/src/normalize.ts +6 -0
- package/src/outbound-adapter.poll.test.ts +62 -0
- package/src/outbound-adapter.sendpayload.test.ts +195 -0
- package/src/outbound-adapter.ts +36 -0
- package/src/outbound-base.test.ts +560 -0
- package/src/outbound-base.ts +244 -0
- package/src/outbound-media-contract.ts +278 -0
- package/src/outbound-media.runtime.ts +36 -0
- package/src/outbound-payload.contract.test.ts +61 -0
- package/src/outbound-send-deps.ts +1 -0
- package/src/outbound-test-support.ts +16 -0
- package/src/pairing-security.test-harness.ts +59 -0
- package/src/qr-image.ts +1 -0
- package/src/qr-terminal.ts +1 -0
- package/src/quoted-message.test.ts +83 -0
- package/src/quoted-message.ts +184 -0
- package/src/reaction-level.test.ts +94 -0
- package/src/reaction-level.ts +21 -0
- package/src/reconnect.test.ts +51 -0
- package/src/reconnect.ts +55 -0
- package/src/resolve-outbound-target.test.ts +265 -0
- package/src/resolve-outbound-target.ts +54 -0
- package/src/resolve-target.test.ts +97 -0
- package/src/runtime-api.ts +60 -0
- package/src/runtime-group-policy.ts +16 -0
- package/src/runtime.ts +9 -0
- package/src/security-contract.ts +47 -0
- package/src/security-fix.ts +71 -0
- package/src/send.test.ts +515 -0
- package/src/send.ts +287 -0
- package/src/session-contract.test.ts +31 -0
- package/src/session-contract.ts +43 -0
- package/src/session-errors.ts +125 -0
- package/src/session-route.ts +26 -0
- package/src/session.runtime.ts +8 -0
- package/src/session.test.ts +559 -0
- package/src/session.ts +330 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-finalize.ts +446 -0
- package/src/setup-surface.test.ts +367 -0
- package/src/setup-surface.ts +69 -0
- package/src/setup-test-helpers.ts +216 -0
- package/src/shared.ts +291 -0
- package/src/socket-timing.test.ts +49 -0
- package/src/socket-timing.ts +38 -0
- package/src/state-migrations.ts +54 -0
- package/src/status-issues.test.ts +137 -0
- package/src/status-issues.ts +185 -0
- package/src/system-prompt.test.ts +199 -0
- package/src/system-prompt.ts +31 -0
- package/src/targets-runtime.ts +180 -0
- package/src/test-helpers.ts +690 -0
- package/src/text-runtime.test.ts +162 -0
- package/src/text-runtime.ts +11 -0
- package/src/vcard.ts +84 -0
- package/targets.ts +5 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import "./test-helpers.js";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createAcceptedWhatsAppSendResult,
|
|
6
|
+
installWebAutoReplyUnitTestHooks,
|
|
7
|
+
makeSessionStore,
|
|
8
|
+
} from "./auto-reply.test-harness.js";
|
|
9
|
+
|
|
10
|
+
const updateLastRouteInBackgroundMock = vi.hoisted(() => vi.fn());
|
|
11
|
+
let awaitBackgroundTasks: typeof import("./auto-reply/monitor/last-route.js").awaitBackgroundTasks;
|
|
12
|
+
let buildMentionConfig: typeof import("./auto-reply/mentions.js").buildMentionConfig;
|
|
13
|
+
let createEchoTracker: typeof import("./auto-reply/monitor/echo.js").createEchoTracker;
|
|
14
|
+
let createWebOnMessageHandler: typeof import("./auto-reply/monitor/on-message.js").createWebOnMessageHandler;
|
|
15
|
+
|
|
16
|
+
vi.mock("./auto-reply/monitor/last-route.js", async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof import("./auto-reply/monitor/last-route.js")>(
|
|
18
|
+
"./auto-reply/monitor/last-route.js",
|
|
19
|
+
);
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
updateLastRouteInBackground: (...args: unknown[]) => updateLastRouteInBackgroundMock(...args),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function makeCfg(storePath: string): OpenClawConfig {
|
|
27
|
+
return {
|
|
28
|
+
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
29
|
+
session: { store: storePath },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeReplyLogger() {
|
|
34
|
+
return {
|
|
35
|
+
warn: vi.fn(),
|
|
36
|
+
info: vi.fn(),
|
|
37
|
+
debug: vi.fn(),
|
|
38
|
+
error: vi.fn(),
|
|
39
|
+
} as unknown as Parameters<typeof createWebOnMessageHandler>[0]["replyLogger"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createHandlerForTest(opts: { cfg: OpenClawConfig; replyResolver: unknown }) {
|
|
43
|
+
const backgroundTasks = new Set<Promise<unknown>>();
|
|
44
|
+
const handler = createWebOnMessageHandler({
|
|
45
|
+
cfg: opts.cfg,
|
|
46
|
+
verbose: false,
|
|
47
|
+
connectionId: "test",
|
|
48
|
+
maxMediaBytes: 1024,
|
|
49
|
+
groupHistoryLimit: 3,
|
|
50
|
+
groupHistories: new Map(),
|
|
51
|
+
groupMemberNames: new Map(),
|
|
52
|
+
echoTracker: createEchoTracker({ maxItems: 10 }),
|
|
53
|
+
backgroundTasks,
|
|
54
|
+
replyResolver: opts.replyResolver as Parameters<
|
|
55
|
+
typeof createWebOnMessageHandler
|
|
56
|
+
>[0]["replyResolver"],
|
|
57
|
+
replyLogger: makeReplyLogger(),
|
|
58
|
+
baseMentionConfig: buildMentionConfig(opts.cfg),
|
|
59
|
+
account: {},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { handler, backgroundTasks };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createLastRouteHarness(storePath: string) {
|
|
66
|
+
const replyResolver = vi.fn().mockResolvedValue(undefined);
|
|
67
|
+
const cfg = makeCfg(storePath);
|
|
68
|
+
return createHandlerForTest({ cfg, replyResolver });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildInboundMessage(params: {
|
|
72
|
+
id: string;
|
|
73
|
+
from: string;
|
|
74
|
+
conversationId: string;
|
|
75
|
+
chatType: "direct" | "group";
|
|
76
|
+
chatId: string;
|
|
77
|
+
timestamp: number;
|
|
78
|
+
body?: string;
|
|
79
|
+
to?: string;
|
|
80
|
+
accountId?: string;
|
|
81
|
+
senderE164?: string;
|
|
82
|
+
senderName?: string;
|
|
83
|
+
selfE164?: string;
|
|
84
|
+
}) {
|
|
85
|
+
return {
|
|
86
|
+
id: params.id,
|
|
87
|
+
from: params.from,
|
|
88
|
+
conversationId: params.conversationId,
|
|
89
|
+
to: params.to ?? "+2000",
|
|
90
|
+
body: params.body ?? "hello",
|
|
91
|
+
timestamp: params.timestamp,
|
|
92
|
+
chatType: params.chatType,
|
|
93
|
+
chatId: params.chatId,
|
|
94
|
+
accountId: params.accountId ?? "default",
|
|
95
|
+
senderE164: params.senderE164,
|
|
96
|
+
senderName: params.senderName,
|
|
97
|
+
selfE164: params.selfE164,
|
|
98
|
+
sendComposing: vi.fn().mockResolvedValue(undefined),
|
|
99
|
+
reply: vi.fn().mockResolvedValue(createAcceptedWhatsAppSendResult("text", "r1")),
|
|
100
|
+
sendMedia: vi.fn().mockResolvedValue(createAcceptedWhatsAppSendResult("media", "m1")),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("web auto-reply last-route", () => {
|
|
105
|
+
installWebAutoReplyUnitTestHooks();
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
vi.resetModules();
|
|
109
|
+
updateLastRouteInBackgroundMock.mockClear();
|
|
110
|
+
({ awaitBackgroundTasks } = await import("./auto-reply/monitor/last-route.js"));
|
|
111
|
+
({ buildMentionConfig } = await import("./auto-reply/mentions.js"));
|
|
112
|
+
({ createEchoTracker } = await import("./auto-reply/monitor/echo.js"));
|
|
113
|
+
({ createWebOnMessageHandler } = await import("./auto-reply/monitor/on-message.js"));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("updates last-route for direct chats without senderE164", async () => {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const mainSessionKey = "agent:main:main";
|
|
119
|
+
const store = await makeSessionStore({
|
|
120
|
+
[mainSessionKey]: { sessionId: "sid", updatedAt: now - 1 },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { handler, backgroundTasks } = createLastRouteHarness(store.storePath);
|
|
124
|
+
|
|
125
|
+
await handler(
|
|
126
|
+
buildInboundMessage({
|
|
127
|
+
id: "m1",
|
|
128
|
+
from: "+1000",
|
|
129
|
+
conversationId: "+1000",
|
|
130
|
+
chatType: "direct",
|
|
131
|
+
chatId: "direct:+1000",
|
|
132
|
+
timestamp: now,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await awaitBackgroundTasks(backgroundTasks);
|
|
137
|
+
|
|
138
|
+
expect(updateLastRouteInBackgroundMock).toHaveBeenCalledWith(
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
channel: "whatsapp",
|
|
141
|
+
to: "+1000",
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await store.cleanup();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("updates last-route for group chats with account id", async () => {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const groupSessionKey = "agent:main:whatsapp:group:123@g.us";
|
|
151
|
+
const store = await makeSessionStore({
|
|
152
|
+
[groupSessionKey]: { sessionId: "sid", updatedAt: now - 1 },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { handler, backgroundTasks } = createLastRouteHarness(store.storePath);
|
|
156
|
+
|
|
157
|
+
await handler(
|
|
158
|
+
buildInboundMessage({
|
|
159
|
+
id: "g1",
|
|
160
|
+
from: "123@g.us",
|
|
161
|
+
conversationId: "123@g.us",
|
|
162
|
+
chatType: "group",
|
|
163
|
+
chatId: "123@g.us",
|
|
164
|
+
timestamp: now,
|
|
165
|
+
accountId: "work",
|
|
166
|
+
senderE164: "+1000",
|
|
167
|
+
senderName: "Alice",
|
|
168
|
+
selfE164: "+2000",
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await awaitBackgroundTasks(backgroundTasks);
|
|
173
|
+
|
|
174
|
+
expect(updateLastRouteInBackgroundMock).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
channel: "whatsapp",
|
|
177
|
+
to: "123@g.us",
|
|
178
|
+
accountId: "work",
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await store.cleanup();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createActionGate } from "openclaw/plugin-sdk/channel-actions";
|
|
2
|
+
import type { ChannelMessageActionName } from "openclaw/plugin-sdk/channel-contract";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
4
|
+
|
|
5
|
+
export { listWhatsAppAccountIds, resolveWhatsAppAccount } from "./accounts.js";
|
|
6
|
+
export { resolveWhatsAppReactionLevel } from "./reaction-level.js";
|
|
7
|
+
export { createActionGate, type ChannelMessageActionName, type OpenClawConfig };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
describeWhatsAppMessageActions,
|
|
4
|
+
resolveWhatsAppAgentReactionGuidance,
|
|
5
|
+
} from "./channel-actions.js";
|
|
6
|
+
import type { OpenClawConfig } from "./runtime-api.js";
|
|
7
|
+
|
|
8
|
+
const hoisted = vi.hoisted(() => ({
|
|
9
|
+
listWhatsAppAccountIds: vi.fn((cfg: OpenClawConfig) => {
|
|
10
|
+
const accountIds = Object.keys(cfg.channels?.whatsapp?.accounts ?? {});
|
|
11
|
+
return accountIds.length > 0 ? accountIds : ["default"];
|
|
12
|
+
}),
|
|
13
|
+
resolveWhatsAppAccount: vi.fn(
|
|
14
|
+
({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => ({
|
|
15
|
+
enabled:
|
|
16
|
+
accountId == null ? true : cfg.channels?.whatsapp?.accounts?.[accountId]?.enabled !== false,
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("./channel-actions.runtime.js", async () => {
|
|
22
|
+
return {
|
|
23
|
+
listWhatsAppAccountIds: hoisted.listWhatsAppAccountIds,
|
|
24
|
+
resolveWhatsAppAccount: hoisted.resolveWhatsAppAccount,
|
|
25
|
+
createActionGate: (actions?: { reactions?: boolean; polls?: boolean }) => (name: string) => {
|
|
26
|
+
if (name === "reactions") {
|
|
27
|
+
return actions?.reactions !== false;
|
|
28
|
+
}
|
|
29
|
+
if (name === "polls") {
|
|
30
|
+
return actions?.polls !== false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
resolveWhatsAppReactionLevel: ({
|
|
35
|
+
cfg,
|
|
36
|
+
accountId,
|
|
37
|
+
}: {
|
|
38
|
+
cfg: OpenClawConfig;
|
|
39
|
+
accountId?: string;
|
|
40
|
+
}) => {
|
|
41
|
+
const accountLevel =
|
|
42
|
+
accountId == null
|
|
43
|
+
? undefined
|
|
44
|
+
: cfg.channels?.whatsapp?.accounts?.[accountId]?.reactionLevel;
|
|
45
|
+
const level = accountLevel ?? cfg.channels?.whatsapp?.reactionLevel ?? "minimal";
|
|
46
|
+
return {
|
|
47
|
+
level,
|
|
48
|
+
agentReactionsEnabled: level === "minimal" || level === "extensive",
|
|
49
|
+
agentReactionGuidance: level === "minimal" || level === "extensive" ? level : undefined,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("whatsapp channel action helpers", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
hoisted.listWhatsAppAccountIds.mockClear();
|
|
58
|
+
hoisted.resolveWhatsAppAccount.mockClear();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("defaults to minimal reaction guidance when reactions are available", () => {
|
|
62
|
+
const cfg = {
|
|
63
|
+
channels: {
|
|
64
|
+
whatsapp: {
|
|
65
|
+
allowFrom: ["*"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
} as OpenClawConfig;
|
|
69
|
+
|
|
70
|
+
expect(resolveWhatsAppAgentReactionGuidance({ cfg, accountId: "default" })).toBe("minimal");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("omits reaction guidance when WhatsApp is not configured", () => {
|
|
74
|
+
expect(
|
|
75
|
+
resolveWhatsAppAgentReactionGuidance({
|
|
76
|
+
cfg: {} as OpenClawConfig,
|
|
77
|
+
accountId: "default",
|
|
78
|
+
}),
|
|
79
|
+
).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns minimal reaction guidance when configured", () => {
|
|
83
|
+
const cfg = {
|
|
84
|
+
channels: {
|
|
85
|
+
whatsapp: {
|
|
86
|
+
reactionLevel: "minimal",
|
|
87
|
+
allowFrom: ["*"],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
} as OpenClawConfig;
|
|
91
|
+
|
|
92
|
+
expect(resolveWhatsAppAgentReactionGuidance({ cfg, accountId: "default" })).toBe("minimal");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("omits reaction guidance when WhatsApp reactions are disabled", () => {
|
|
96
|
+
const cfg = {
|
|
97
|
+
channels: {
|
|
98
|
+
whatsapp: {
|
|
99
|
+
actions: { reactions: false },
|
|
100
|
+
allowFrom: ["*"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} as OpenClawConfig;
|
|
104
|
+
|
|
105
|
+
expect(resolveWhatsAppAgentReactionGuidance({ cfg, accountId: "default" })).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("omits reaction guidance when reactionLevel disables agent reactions", () => {
|
|
109
|
+
const cfg = {
|
|
110
|
+
channels: {
|
|
111
|
+
whatsapp: {
|
|
112
|
+
reactionLevel: "ack",
|
|
113
|
+
allowFrom: ["*"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
} as OpenClawConfig;
|
|
117
|
+
|
|
118
|
+
expect(resolveWhatsAppAgentReactionGuidance({ cfg, accountId: "default" })).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("advertises react when agent reactions are enabled", () => {
|
|
122
|
+
const cfg = {
|
|
123
|
+
channels: {
|
|
124
|
+
whatsapp: {
|
|
125
|
+
allowFrom: ["*"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
} as OpenClawConfig;
|
|
129
|
+
|
|
130
|
+
expect(describeWhatsAppMessageActions({ cfg, accountId: "default" })?.actions).toEqual([
|
|
131
|
+
"react",
|
|
132
|
+
"poll",
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns null when WhatsApp is not configured", () => {
|
|
137
|
+
expect(
|
|
138
|
+
describeWhatsAppMessageActions({ cfg: {} as OpenClawConfig, accountId: "default" }),
|
|
139
|
+
).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("omits react when reactionLevel disables agent reactions", () => {
|
|
143
|
+
const cfg = {
|
|
144
|
+
channels: {
|
|
145
|
+
whatsapp: {
|
|
146
|
+
reactionLevel: "ack",
|
|
147
|
+
allowFrom: ["*"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
} as OpenClawConfig;
|
|
151
|
+
|
|
152
|
+
expect(describeWhatsAppMessageActions({ cfg, accountId: "default" })?.actions).toEqual([
|
|
153
|
+
"poll",
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("uses the active account reactionLevel for discovery", () => {
|
|
158
|
+
const cfg = {
|
|
159
|
+
channels: {
|
|
160
|
+
whatsapp: {
|
|
161
|
+
reactionLevel: "ack",
|
|
162
|
+
allowFrom: ["*"],
|
|
163
|
+
accounts: {
|
|
164
|
+
work: {
|
|
165
|
+
reactionLevel: "minimal",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
} as OpenClawConfig;
|
|
171
|
+
|
|
172
|
+
expect(describeWhatsAppMessageActions({ cfg, accountId: "work" })?.actions).toEqual([
|
|
173
|
+
"react",
|
|
174
|
+
"poll",
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("keeps react in global discovery when any account enables agent reactions", () => {
|
|
179
|
+
const cfg = {
|
|
180
|
+
channels: {
|
|
181
|
+
whatsapp: {
|
|
182
|
+
reactionLevel: "ack",
|
|
183
|
+
allowFrom: ["*"],
|
|
184
|
+
accounts: {
|
|
185
|
+
work: {
|
|
186
|
+
reactionLevel: "minimal",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
} as OpenClawConfig;
|
|
192
|
+
hoisted.listWhatsAppAccountIds.mockReturnValue(["default", "work"]);
|
|
193
|
+
|
|
194
|
+
expect(describeWhatsAppMessageActions({ cfg })?.actions).toEqual(["react", "poll"]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("omits react in global discovery when only disabled accounts enable agent reactions", () => {
|
|
198
|
+
const cfg = {
|
|
199
|
+
channels: {
|
|
200
|
+
whatsapp: {
|
|
201
|
+
reactionLevel: "ack",
|
|
202
|
+
allowFrom: ["*"],
|
|
203
|
+
accounts: {
|
|
204
|
+
work: {
|
|
205
|
+
enabled: false,
|
|
206
|
+
reactionLevel: "minimal",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
} as OpenClawConfig;
|
|
212
|
+
hoisted.listWhatsAppAccountIds.mockReturnValue(["default", "work"]);
|
|
213
|
+
|
|
214
|
+
expect(describeWhatsAppMessageActions({ cfg })?.actions).toEqual(["poll"]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listWhatsAppAccountIds,
|
|
3
|
+
resolveWhatsAppAccount,
|
|
4
|
+
createActionGate,
|
|
5
|
+
type ChannelMessageActionName,
|
|
6
|
+
type OpenClawConfig,
|
|
7
|
+
resolveWhatsAppReactionLevel,
|
|
8
|
+
} from "./channel-actions.runtime.js";
|
|
9
|
+
|
|
10
|
+
function areWhatsAppAgentReactionsEnabled(params: { cfg: OpenClawConfig; accountId?: string }) {
|
|
11
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
15
|
+
if (!gate("reactions")) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return resolveWhatsAppReactionLevel({
|
|
19
|
+
cfg: params.cfg,
|
|
20
|
+
accountId: params.accountId,
|
|
21
|
+
}).agentReactionsEnabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasAnyWhatsAppAccountWithAgentReactionsEnabled(cfg: OpenClawConfig) {
|
|
25
|
+
if (!cfg.channels?.whatsapp) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return listWhatsAppAccountIds(cfg).some((accountId) => {
|
|
29
|
+
const account = resolveWhatsAppAccount({ cfg, accountId });
|
|
30
|
+
if (!account.enabled) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return areWhatsAppAgentReactionsEnabled({
|
|
34
|
+
cfg,
|
|
35
|
+
accountId,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveWhatsAppAgentReactionGuidance(params: {
|
|
41
|
+
cfg: OpenClawConfig;
|
|
42
|
+
accountId?: string;
|
|
43
|
+
}) {
|
|
44
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
48
|
+
if (!gate("reactions")) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const resolved = resolveWhatsAppReactionLevel({
|
|
52
|
+
cfg: params.cfg,
|
|
53
|
+
accountId: params.accountId,
|
|
54
|
+
});
|
|
55
|
+
if (!resolved.agentReactionsEnabled) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return resolved.agentReactionGuidance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function describeWhatsAppMessageActions(params: {
|
|
62
|
+
cfg: OpenClawConfig;
|
|
63
|
+
accountId?: string | null;
|
|
64
|
+
}): { actions: ChannelMessageActionName[] } | null {
|
|
65
|
+
if (!params.cfg.channels?.whatsapp) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const gate = createActionGate(params.cfg.channels.whatsapp.actions);
|
|
69
|
+
const actions = new Set<ChannelMessageActionName>();
|
|
70
|
+
const canReact =
|
|
71
|
+
params.accountId != null
|
|
72
|
+
? areWhatsAppAgentReactionsEnabled({
|
|
73
|
+
cfg: params.cfg,
|
|
74
|
+
accountId: params.accountId ?? undefined,
|
|
75
|
+
})
|
|
76
|
+
: hasAnyWhatsAppAccountWithAgentReactionsEnabled(params.cfg);
|
|
77
|
+
if (canReact) {
|
|
78
|
+
actions.add("react");
|
|
79
|
+
}
|
|
80
|
+
if (gate("polls")) {
|
|
81
|
+
actions.add("poll");
|
|
82
|
+
}
|
|
83
|
+
return { actions: Array.from(actions) };
|
|
84
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const hoisted = vi.hoisted(() => ({
|
|
4
|
+
sendMessageWhatsApp: vi.fn(async () => ({ messageId: "wa-1", toJid: "jid" })),
|
|
5
|
+
sendPollWhatsApp: vi.fn(async () => ({ messageId: "poll-1", toJid: "jid" })),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./send.js", () => ({
|
|
9
|
+
sendMessageWhatsApp: hoisted.sendMessageWhatsApp,
|
|
10
|
+
sendPollWhatsApp: hoisted.sendPollWhatsApp,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("./runtime.js", () => ({
|
|
14
|
+
getWhatsAppRuntime: () => ({
|
|
15
|
+
logging: {
|
|
16
|
+
shouldLogVerbose: () => false,
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let whatsappChannelOutbound: typeof import("./channel-outbound.js").whatsappChannelOutbound;
|
|
22
|
+
|
|
23
|
+
describe("whatsappChannelOutbound", () => {
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
({ whatsappChannelOutbound } = await import("./channel-outbound.js"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("drops leading blank lines but preserves intentional indentation", () => {
|
|
33
|
+
expect(
|
|
34
|
+
whatsappChannelOutbound.normalizePayload?.({
|
|
35
|
+
payload: { text: "\n \n indented" },
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({
|
|
38
|
+
text: " indented",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("keeps XML sanitizer normalization idempotent", () => {
|
|
43
|
+
const raw = [
|
|
44
|
+
"<function_calls>",
|
|
45
|
+
' <invoke name="send_message">',
|
|
46
|
+
' <parameter name="text">hidden</parameter>',
|
|
47
|
+
" </invoke>",
|
|
48
|
+
"</function_calls>",
|
|
49
|
+
"After",
|
|
50
|
+
].join("\n");
|
|
51
|
+
const once = whatsappChannelOutbound.normalizePayload?.({ payload: { text: raw } });
|
|
52
|
+
const twice = whatsappChannelOutbound.normalizePayload?.({ payload: { text: once?.text } });
|
|
53
|
+
|
|
54
|
+
expect(once?.text).toBe("After");
|
|
55
|
+
expect(twice?.text).toBe("After");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("drops whitespace-only text after XML sanitizer removal", () => {
|
|
59
|
+
const raw = [
|
|
60
|
+
" <function_calls>",
|
|
61
|
+
' <invoke name="send_message">',
|
|
62
|
+
' <parameter name="text">hidden</parameter>',
|
|
63
|
+
" </invoke>",
|
|
64
|
+
" </function_calls>",
|
|
65
|
+
].join("\n");
|
|
66
|
+
|
|
67
|
+
expect(whatsappChannelOutbound.normalizePayload?.({ payload: { text: raw } })).toEqual({
|
|
68
|
+
text: "",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sanitizes XML tool payloads before plain HTML stripping", () => {
|
|
73
|
+
const raw = [
|
|
74
|
+
"Before",
|
|
75
|
+
"<function_calls>",
|
|
76
|
+
' <invoke name="send_message">',
|
|
77
|
+
' <parameter name="text">hidden</parameter>',
|
|
78
|
+
" </invoke>",
|
|
79
|
+
"</function_calls>",
|
|
80
|
+
"After",
|
|
81
|
+
].join("\n");
|
|
82
|
+
|
|
83
|
+
expect(whatsappChannelOutbound.sanitizeText?.({ text: raw, payload: { text: raw } })).toBe(
|
|
84
|
+
"Before\n\nAfter",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("preserves indentation for live text sends", async () => {
|
|
89
|
+
await whatsappChannelOutbound.sendText!({
|
|
90
|
+
cfg: {},
|
|
91
|
+
to: "5511999999999@c.us",
|
|
92
|
+
text: "\n \n indented",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(hoisted.sendMessageWhatsApp).toHaveBeenCalledWith("5511999999999@c.us", " indented", {
|
|
96
|
+
verbose: false,
|
|
97
|
+
cfg: {},
|
|
98
|
+
accountId: undefined,
|
|
99
|
+
gifPlayback: undefined,
|
|
100
|
+
preserveLeadingWhitespace: true,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("rejects non-WhatsApp provider-prefixed outbound targets", () => {
|
|
105
|
+
const result = whatsappChannelOutbound.resolveTarget?.({
|
|
106
|
+
to: "telegram:1234567890",
|
|
107
|
+
allowFrom: [],
|
|
108
|
+
mode: undefined,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result?.ok).toBe(false);
|
|
112
|
+
expect(hoisted.sendMessageWhatsApp).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("preserves indentation for payload delivery", async () => {
|
|
116
|
+
await whatsappChannelOutbound.sendPayload!({
|
|
117
|
+
cfg: {},
|
|
118
|
+
to: "5511999999999@c.us",
|
|
119
|
+
text: "",
|
|
120
|
+
payload: { text: "\n \n indented" },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(hoisted.sendMessageWhatsApp).toHaveBeenCalledWith("5511999999999@c.us", " indented", {
|
|
124
|
+
verbose: false,
|
|
125
|
+
cfg: {},
|
|
126
|
+
accountId: undefined,
|
|
127
|
+
gifPlayback: undefined,
|
|
128
|
+
preserveLeadingWhitespace: true,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { chunkText } from "openclaw/plugin-sdk/reply-chunking";
|
|
2
|
+
import { createWhatsAppOutboundBase } from "./outbound-base.js";
|
|
3
|
+
import { normalizeWhatsAppPayloadTextPreservingIndentation } from "./outbound-media-contract.js";
|
|
4
|
+
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
|
|
5
|
+
import { getWhatsAppRuntime } from "./runtime.js";
|
|
6
|
+
import { sendMessageWhatsApp, sendPollWhatsApp } from "./send.js";
|
|
7
|
+
|
|
8
|
+
export function normalizeWhatsAppChannelPayloadText(text: string | undefined): string {
|
|
9
|
+
return normalizeWhatsAppPayloadTextPreservingIndentation(text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeWhatsAppChannelSendText(text: string | undefined): string {
|
|
13
|
+
const normalized = normalizeWhatsAppChannelPayloadText(text);
|
|
14
|
+
return normalized.trim() ? normalized : "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const whatsappChannelOutbound = {
|
|
18
|
+
...createWhatsAppOutboundBase({
|
|
19
|
+
chunker: chunkText,
|
|
20
|
+
sendMessageWhatsApp: async (to, text, options) =>
|
|
21
|
+
await sendMessageWhatsApp(to, text, {
|
|
22
|
+
...options,
|
|
23
|
+
preserveLeadingWhitespace: true,
|
|
24
|
+
}),
|
|
25
|
+
sendPollWhatsApp,
|
|
26
|
+
shouldLogVerbose: () => getWhatsAppRuntime().logging.shouldLogVerbose(),
|
|
27
|
+
resolveTarget: ({ to, allowFrom, mode }) =>
|
|
28
|
+
resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
|
|
29
|
+
normalizeText: normalizeWhatsAppChannelSendText,
|
|
30
|
+
}),
|
|
31
|
+
sendTextOnlyErrorPayloads: true,
|
|
32
|
+
normalizePayload: ({ payload }: { payload: { text?: string } }) => ({
|
|
33
|
+
...payload,
|
|
34
|
+
text: normalizeWhatsAppChannelPayloadText(payload.text),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
3
|
+
|
|
4
|
+
export { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
|
|
5
|
+
export { handleWhatsAppAction } from "./action-runtime.js";
|
|
6
|
+
export { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
|
|
7
|
+
export { readStringOrNumberParam, readStringParam, type OpenClawConfig };
|