mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:50:45 +00:00
fix: expose WhatsApp Baileys socket timing
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Memory-core/dreaming: retry managed dreaming cron registration after startup when the cron service is not reachable yet, so the scheduled Memory Dreaming Promotion sweep recovers without waiting for heartbeat traffic. Fixes #72841. Thanks @amknight.
|
- Memory-core/dreaming: retry managed dreaming cron registration after startup when the cron service is not reachable yet, so the scheduled Memory Dreaming Promotion sweep recovers without waiting for heartbeat traffic. Fixes #72841. Thanks @amknight.
|
||||||
- Acpx/runtime: validate the runtime session mode at the `AcpxRuntime.ensureSession` wrapper boundary so callers that pass anything other than `persistent` or `oneshot` get a clear `ACP_INVALID_RUNTIME_OPTION` error instead of silently round-tripping through the encoded handle as a default `persistent` mode and later throwing `SessionResumeRequiredError`. Investigation context: #73071. (#73548) Thanks @amknight.
|
- Acpx/runtime: validate the runtime session mode at the `AcpxRuntime.ensureSession` wrapper boundary so callers that pass anything other than `persistent` or `oneshot` get a clear `ACP_INVALID_RUNTIME_OPTION` error instead of silently round-tripping through the encoded handle as a default `persistent` mode and later throwing `SessionResumeRequiredError`. Investigation context: #73071. (#73548) Thanks @amknight.
|
||||||
- CLI/infer: keep web-search fallback on missing provider API keys, preserve structured validation errors from the selected provider, and let per-request image describe prompts override configured media-entry prompts. (#63263) Thanks @Spolen23.
|
- CLI/infer: keep web-search fallback on missing provider API keys, preserve structured validation errors from the selected provider, and let per-request image describe prompts override configured media-entry prompts. (#63263) Thanks @Spolen23.
|
||||||
|
- WhatsApp/Web: pass explicit Baileys socket timings into every WhatsApp Web socket and expose `web.whatsapp.*` keepalive, connect, and query timeout settings so unstable networks can avoid repeated 408 disconnect and opening-handshake timeout loops. Fixes #56365. Thanks @velvet-shark.
|
||||||
|
|
||||||
## 2026.4.27
|
## 2026.4.27
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
85842690af24b21a5e074d722930af95faaf6e91a918061bdc1b5c956860a7a0 config-baseline.json
|
39c5c0620611f355f20d5e9d2ddd74e198c344c63d5551a987e4b7538833ceac config-baseline.json
|
||||||
86ad0927d992bc873affb3e20a31c6e3c95b2185a91f46cc8e6262a723a78f7d config-baseline.core.json
|
805bd3f63ff7327da45c01b78dbc990ed53bd13b89e0cbf50f319aa99334ba92 config-baseline.core.json
|
||||||
323a9fd49a669951ca5b3442d95aad243bd1330083f9857e83a8dcfae2bbc9d0 config-baseline.channel.json
|
323a9fd49a669951ca5b3442d95aad243bd1330083f9857e83a8dcfae2bbc9d0 config-baseline.channel.json
|
||||||
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json
|
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
|
|||||||
|
|
||||||
- Gateway owns the WhatsApp socket and reconnect loop.
|
- Gateway owns the WhatsApp socket and reconnect loop.
|
||||||
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window.
|
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window.
|
||||||
|
- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` controls WhatsApp Web application pings, `connectTimeoutMs` controls the opening handshake timeout, and `defaultQueryTimeoutMs` controls Baileys query timeouts.
|
||||||
- Outbound sends require an active WhatsApp listener for the target account.
|
- Outbound sends require an active WhatsApp listener for the target account.
|
||||||
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
|
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
|
||||||
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
||||||
@@ -520,6 +521,23 @@ Behavior notes:
|
|||||||
restarts when WhatsApp Web transport activity stops, the socket closes, or
|
restarts when WhatsApp Web transport activity stops, the socket closes, or
|
||||||
application-level activity stays silent beyond the longer safety window.
|
application-level activity stays silent beyond the longer safety window.
|
||||||
|
|
||||||
|
If logs show repeated `status=408 Request Time-out Connection was lost`, tune
|
||||||
|
Baileys socket timings under `web.whatsapp`. Start by shortening
|
||||||
|
`keepAliveIntervalMs` below your network's idle timeout and increasing
|
||||||
|
`connectTimeoutMs` on slow or lossy links:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
web: {
|
||||||
|
whatsapp: {
|
||||||
|
keepAliveIntervalMs: 15000,
|
||||||
|
connectTimeoutMs: 60000,
|
||||||
|
defaultQueryTimeoutMs: 60000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Fix:
|
Fix:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -643,7 +661,7 @@ High-signal WhatsApp fields:
|
|||||||
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
|
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
|
||||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
|
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
|
||||||
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
|
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
|
||||||
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`
|
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
|
||||||
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
|
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
|
||||||
- prompts: `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt`
|
- prompts: `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt`
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
|||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
web: {
|
||||||
|
whatsapp: {
|
||||||
|
keepAliveIntervalMs: 25000,
|
||||||
|
connectTimeoutMs: 60000,
|
||||||
|
defaultQueryTimeoutMs: 60000,
|
||||||
|
},
|
||||||
|
},
|
||||||
channels: {
|
channels: {
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
sleepWithAbort,
|
sleepWithAbort,
|
||||||
} from "../reconnect.js";
|
} from "../reconnect.js";
|
||||||
import { formatError, getWebAuthAgeMs, logoutWeb, readWebSelfId } from "../session.js";
|
import { formatError, getWebAuthAgeMs, logoutWeb, readWebSelfId } from "../session.js";
|
||||||
|
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
|
||||||
import { getRuntimeConfig, getRuntimeConfigSourceSnapshot } from "./config.runtime.js";
|
import { getRuntimeConfig, getRuntimeConfigSourceSnapshot } from "./config.runtime.js";
|
||||||
import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
|
import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
|
||||||
import { buildMentionConfig } from "./mentions.js";
|
import { buildMentionConfig } from "./mentions.js";
|
||||||
@@ -181,6 +182,7 @@ export async function monitorWebChannel(
|
|||||||
const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
|
const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
|
||||||
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
|
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
|
||||||
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
||||||
|
const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming);
|
||||||
const baseMentionConfig = buildMentionConfig(cfg);
|
const baseMentionConfig = buildMentionConfig(cfg);
|
||||||
const groupHistoryLimit =
|
const groupHistoryLimit =
|
||||||
account.historyLimit ??
|
account.historyLimit ??
|
||||||
@@ -229,6 +231,7 @@ export async function monitorWebChannel(
|
|||||||
messageTimeoutMs,
|
messageTimeoutMs,
|
||||||
watchdogCheckMs,
|
watchdogCheckMs,
|
||||||
reconnectPolicy,
|
reconnectPolicy,
|
||||||
|
socketTiming,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
sleep,
|
sleep,
|
||||||
isNonRetryableStatus: isNonRetryableWebCloseStatus,
|
isNonRetryableStatus: isNonRetryableWebCloseStatus,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { WebInboundMessage } from "../inbound/types.js";
|
import type { WebInboundMessage } from "../inbound/types.js";
|
||||||
import type { ReconnectPolicy } from "../reconnect.js";
|
import type { ReconnectPolicy } from "../reconnect.js";
|
||||||
|
import type { WhatsAppSocketTimingOptions } from "../socket-timing.js";
|
||||||
|
|
||||||
export type WebChannelHealthState =
|
export type WebChannelHealthState =
|
||||||
| "starting"
|
| "starting"
|
||||||
@@ -32,6 +33,7 @@ export type WebChannelStatus = {
|
|||||||
|
|
||||||
export type WebMonitorTuning = {
|
export type WebMonitorTuning = {
|
||||||
reconnect?: Partial<ReconnectPolicy>;
|
reconnect?: Partial<ReconnectPolicy>;
|
||||||
|
socketTiming?: WhatsAppSocketTimingOptions;
|
||||||
heartbeatSeconds?: number;
|
heartbeatSeconds?: number;
|
||||||
messageTimeoutMs?: number;
|
messageTimeoutMs?: number;
|
||||||
watchdogCheckMs?: number;
|
watchdogCheckMs?: number;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
logoutWeb,
|
logoutWeb,
|
||||||
waitForWaConnection,
|
waitForWaConnection,
|
||||||
} from "./session.js";
|
} from "./session.js";
|
||||||
|
import type { WhatsAppSocketTimingOptions } from "./socket-timing.js";
|
||||||
|
|
||||||
const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401;
|
const LOGGED_OUT_STATUS = DisconnectReason?.loggedOut ?? 401;
|
||||||
const WHATSAPP_LOGIN_RESTART_MESSAGE =
|
const WHATSAPP_LOGIN_RESTART_MESSAGE =
|
||||||
@@ -171,6 +172,7 @@ export async function waitForWhatsAppLoginResult(params: {
|
|||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
waitForConnection?: typeof waitForWaConnection;
|
waitForConnection?: typeof waitForWaConnection;
|
||||||
createSocket?: typeof createWaSocket;
|
createSocket?: typeof createWaSocket;
|
||||||
|
socketTiming?: WhatsAppSocketTimingOptions;
|
||||||
onQr?: (qr: string) => void;
|
onQr?: (qr: string) => void;
|
||||||
onSocketReplaced?: (sock: WaSocket) => void;
|
onSocketReplaced?: (sock: WaSocket) => void;
|
||||||
}): Promise<WhatsAppLoginWaitResult> {
|
}): Promise<WhatsAppLoginWaitResult> {
|
||||||
@@ -196,6 +198,7 @@ export async function waitForWhatsAppLoginResult(params: {
|
|||||||
try {
|
try {
|
||||||
currentSock = await createSocket(false, params.verbose, {
|
currentSock = await createSocket(false, params.verbose, {
|
||||||
authDir: params.authDir,
|
authDir: params.authDir,
|
||||||
|
...params.socketTiming,
|
||||||
onQr: params.onQr,
|
onQr: params.onQr,
|
||||||
});
|
});
|
||||||
params.onSocketReplaced?.(currentSock);
|
params.onSocketReplaced?.(currentSock);
|
||||||
@@ -249,6 +252,7 @@ export class WhatsAppConnectionController {
|
|||||||
private readonly abortSignal?: AbortSignal;
|
private readonly abortSignal?: AbortSignal;
|
||||||
private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
||||||
private readonly isNonRetryableStatus: (statusCode: unknown) => boolean;
|
private readonly isNonRetryableStatus: (statusCode: unknown) => boolean;
|
||||||
|
private readonly socketTiming: WhatsAppSocketTimingOptions;
|
||||||
private readonly abortPromise?: Promise<"aborted">;
|
private readonly abortPromise?: Promise<"aborted">;
|
||||||
private readonly disconnectRetryController = new AbortController();
|
private readonly disconnectRetryController = new AbortController();
|
||||||
|
|
||||||
@@ -267,6 +271,7 @@ export class WhatsAppConnectionController {
|
|||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
||||||
isNonRetryableStatus?: (statusCode: unknown) => boolean;
|
isNonRetryableStatus?: (statusCode: unknown) => boolean;
|
||||||
|
socketTiming?: WhatsAppSocketTimingOptions;
|
||||||
}) {
|
}) {
|
||||||
this.accountId = params.accountId;
|
this.accountId = params.accountId;
|
||||||
this.authDir = params.authDir;
|
this.authDir = params.authDir;
|
||||||
@@ -280,6 +285,7 @@ export class WhatsAppConnectionController {
|
|||||||
this.abortSignal = params.abortSignal;
|
this.abortSignal = params.abortSignal;
|
||||||
this.sleep = params.sleep ?? ((ms: number, signal?: AbortSignal) => sleepWithAbort(ms, signal));
|
this.sleep = params.sleep ?? ((ms: number, signal?: AbortSignal) => sleepWithAbort(ms, signal));
|
||||||
this.isNonRetryableStatus = params.isNonRetryableStatus ?? (() => false);
|
this.isNonRetryableStatus = params.isNonRetryableStatus ?? (() => false);
|
||||||
|
this.socketTiming = params.socketTiming ?? {};
|
||||||
this.socketRef = { current: null };
|
this.socketRef = { current: null };
|
||||||
this.abortPromise =
|
this.abortPromise =
|
||||||
params.abortSignal &&
|
params.abortSignal &&
|
||||||
@@ -378,6 +384,7 @@ export class WhatsAppConnectionController {
|
|||||||
try {
|
try {
|
||||||
sock = await createWaSocket(false, this.verbose, {
|
sock = await createWaSocket(false, this.verbose, {
|
||||||
authDir: this.authDir,
|
authDir: this.authDir,
|
||||||
|
...this.socketTiming,
|
||||||
});
|
});
|
||||||
await waitForWaConnection(sock);
|
await waitForWaConnection(sock);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { cacheInboundMessageMeta } from "../quoted-message.js";
|
|||||||
import { DEFAULT_RECONNECT_POLICY, computeBackoff, sleepWithAbort } from "../reconnect.js";
|
import { DEFAULT_RECONNECT_POLICY, computeBackoff, sleepWithAbort } from "../reconnect.js";
|
||||||
import type { OpenClawConfig } from "../runtime-api.js";
|
import type { OpenClawConfig } from "../runtime-api.js";
|
||||||
import { createWaSocket, formatError, getStatusCode, waitForWaConnection } from "../session.js";
|
import { createWaSocket, formatError, getStatusCode, waitForWaConnection } from "../session.js";
|
||||||
|
import { resolveWhatsAppSocketTiming } from "../socket-timing.js";
|
||||||
import { resolveJidToE164 } from "../text-runtime.js";
|
import { resolveJidToE164 } from "../text-runtime.js";
|
||||||
import { checkInboundAccessControl } from "./access-control.js";
|
import { checkInboundAccessControl } from "./access-control.js";
|
||||||
import {
|
import {
|
||||||
@@ -774,6 +775,7 @@ export async function attachWebInboxToSocket(
|
|||||||
export async function monitorWebInbox(options: MonitorWebInboxOptions) {
|
export async function monitorWebInbox(options: MonitorWebInboxOptions) {
|
||||||
const sock = await createWaSocket(false, options.verbose, {
|
const sock = await createWaSocket(false, options.verbose, {
|
||||||
authDir: options.authDir,
|
authDir: options.authDir,
|
||||||
|
...resolveWhatsAppSocketTiming(options.cfg),
|
||||||
});
|
});
|
||||||
await waitForWaConnection(sock);
|
await waitForWaConnection(sock);
|
||||||
return attachWebInboxToSocket({
|
return attachWebInboxToSocket({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
readWebSelfId,
|
readWebSelfId,
|
||||||
WHATSAPP_AUTH_UNSTABLE_CODE,
|
WHATSAPP_AUTH_UNSTABLE_CODE,
|
||||||
} from "./session.js";
|
} from "./session.js";
|
||||||
|
import { resolveWhatsAppSocketTiming, type WhatsAppSocketTimingOptions } from "./socket-timing.js";
|
||||||
|
|
||||||
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
|
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
|
||||||
export type StartWebLoginWithQrResult = {
|
export type StartWebLoginWithQrResult = {
|
||||||
@@ -45,6 +46,7 @@ type ActiveLogin = {
|
|||||||
qrRenderPromise: Promise<string> | null;
|
qrRenderPromise: Promise<string> | null;
|
||||||
verbose: boolean;
|
verbose: boolean;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
|
socketTiming: WhatsAppSocketTimingOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoginQrRaceResult =
|
type LoginQrRaceResult =
|
||||||
@@ -178,6 +180,7 @@ function attachLoginWaiter(accountId: string, login: ActiveLogin) {
|
|||||||
isLegacyAuthDir: login.isLegacyAuthDir,
|
isLegacyAuthDir: login.isLegacyAuthDir,
|
||||||
verbose: login.verbose,
|
verbose: login.verbose,
|
||||||
runtime: login.runtime,
|
runtime: login.runtime,
|
||||||
|
socketTiming: login.socketTiming,
|
||||||
onQr: (qr) => {
|
onQr: (qr) => {
|
||||||
const current = activeLogins.get(accountId);
|
const current = activeLogins.get(accountId);
|
||||||
if (!current || current.id !== login.id) {
|
if (!current || current.id !== login.id) {
|
||||||
@@ -282,6 +285,7 @@ export async function startWebLoginWithQr(
|
|||||||
const runtime = opts.runtime ?? defaultRuntime;
|
const runtime = opts.runtime ?? defaultRuntime;
|
||||||
const cfg = getRuntimeConfig();
|
const cfg = getRuntimeConfig();
|
||||||
const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId });
|
const account = resolveWhatsAppAccount({ cfg, accountId: opts.accountId });
|
||||||
|
const socketTiming = resolveWhatsAppSocketTiming(cfg);
|
||||||
const authState = await readWebAuthExistsForDecision(account.authDir);
|
const authState = await readWebAuthExistsForDecision(account.authDir);
|
||||||
if (authState.outcome === "unstable") {
|
if (authState.outcome === "unstable") {
|
||||||
return {
|
return {
|
||||||
@@ -327,6 +331,7 @@ export async function startWebLoginWithQr(
|
|||||||
try {
|
try {
|
||||||
sock = await createWaSocket(false, Boolean(opts.verbose), {
|
sock = await createWaSocket(false, Boolean(opts.verbose), {
|
||||||
authDir: account.authDir,
|
authDir: account.authDir,
|
||||||
|
...socketTiming,
|
||||||
onQr: (qr: string) => {
|
onQr: (qr: string) => {
|
||||||
pendingQr = qr;
|
pendingQr = qr;
|
||||||
const current = activeLogins.get(account.accountId);
|
const current = activeLogins.get(account.accountId);
|
||||||
@@ -370,6 +375,7 @@ export async function startWebLoginWithQr(
|
|||||||
qrRenderPromise: null,
|
qrRenderPromise: null,
|
||||||
verbose: Boolean(opts.verbose),
|
verbose: Boolean(opts.verbose),
|
||||||
runtime,
|
runtime,
|
||||||
|
socketTiming,
|
||||||
};
|
};
|
||||||
resetQrUpdateSignal(login);
|
resetQrUpdateSignal(login);
|
||||||
activeLogins.set(account.accountId, login);
|
activeLogins.set(account.accountId, login);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { resolveWhatsAppAccount } from "./accounts.js";
|
|||||||
import { restoreCredsFromBackupIfNeeded } from "./auth-store.js";
|
import { restoreCredsFromBackupIfNeeded } from "./auth-store.js";
|
||||||
import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js";
|
import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js";
|
||||||
import { createWaSocket, waitForWaConnection } from "./session.js";
|
import { createWaSocket, waitForWaConnection } from "./session.js";
|
||||||
|
import { resolveWhatsAppSocketTiming } from "./socket-timing.js";
|
||||||
|
|
||||||
export async function loginWeb(
|
export async function loginWeb(
|
||||||
verbose: boolean,
|
verbose: boolean,
|
||||||
@@ -16,9 +17,11 @@ export async function loginWeb(
|
|||||||
) {
|
) {
|
||||||
const cfg = getRuntimeConfig();
|
const cfg = getRuntimeConfig();
|
||||||
const account = resolveWhatsAppAccount({ cfg, accountId });
|
const account = resolveWhatsAppAccount({ cfg, accountId });
|
||||||
|
const socketTiming = resolveWhatsAppSocketTiming(cfg);
|
||||||
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
|
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
|
||||||
let sock = await createWaSocket(true, verbose, {
|
let sock = await createWaSocket(true, verbose, {
|
||||||
authDir: account.authDir,
|
authDir: account.authDir,
|
||||||
|
...socketTiming,
|
||||||
});
|
});
|
||||||
logInfo("Waiting for WhatsApp connection...", runtime);
|
logInfo("Waiting for WhatsApp connection...", runtime);
|
||||||
try {
|
try {
|
||||||
@@ -29,6 +32,7 @@ export async function loginWeb(
|
|||||||
verbose,
|
verbose,
|
||||||
runtime,
|
runtime,
|
||||||
waitForConnection,
|
waitForConnection,
|
||||||
|
socketTiming,
|
||||||
onSocketReplaced: (replacementSock) => {
|
onSocketReplaced: (replacementSock) => {
|
||||||
sock = replacementSock;
|
sock = replacementSock;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let logWebSelfId: typeof import("./session.js").logWebSelfId;
|
|||||||
let waitForWaConnection: typeof import("./session.js").waitForWaConnection;
|
let waitForWaConnection: typeof import("./session.js").waitForWaConnection;
|
||||||
let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue;
|
let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue;
|
||||||
let writeCredsJsonAtomically: typeof import("./session.js").writeCredsJsonAtomically;
|
let writeCredsJsonAtomically: typeof import("./session.js").writeCredsJsonAtomically;
|
||||||
|
let DEFAULT_WHATSAPP_SOCKET_TIMING: typeof import("./socket-timing.js").DEFAULT_WHATSAPP_SOCKET_TIMING;
|
||||||
|
|
||||||
async function flushCredsUpdate() {
|
async function flushCredsUpdate() {
|
||||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
@@ -148,6 +149,7 @@ describe("web session", () => {
|
|||||||
waitForCredsSaveQueue,
|
waitForCredsSaveQueue,
|
||||||
writeCredsJsonAtomically,
|
writeCredsJsonAtomically,
|
||||||
} = await import("./session.js"));
|
} = await import("./session.js"));
|
||||||
|
({ DEFAULT_WHATSAPP_SOCKET_TIMING } = await import("./socket-timing.js"));
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -171,7 +173,10 @@ describe("web session", () => {
|
|||||||
await createWaSocket(true, false, { authDir });
|
await createWaSocket(true, false, { authDir });
|
||||||
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
|
const makeWASocket = baileys.makeWASocket as ReturnType<typeof vi.fn>;
|
||||||
expect(makeWASocket).toHaveBeenCalledWith(
|
expect(makeWASocket).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ printQRInTerminal: false }),
|
expect.objectContaining({
|
||||||
|
printQRInTerminal: false,
|
||||||
|
...DEFAULT_WHATSAPP_SOCKET_TIMING,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const passed = makeWASocket.mock.calls[0][0];
|
const passed = makeWASocket.mock.calls[0][0];
|
||||||
const passedLogger = (passed as { logger?: { level?: string; trace?: unknown } }).logger;
|
const passedLogger = (passed as { logger?: { level?: string; trace?: unknown } }).logger;
|
||||||
@@ -187,6 +192,22 @@ describe("web session", () => {
|
|||||||
openMock.restore();
|
openMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes explicit Baileys socket timing overrides", async () => {
|
||||||
|
await createWaSocket(false, false, {
|
||||||
|
keepAliveIntervalMs: 10_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(baileys.makeWASocket).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
keepAliveIntervalMs: 10_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
|
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import {
|
|||||||
makeWASocket,
|
makeWASocket,
|
||||||
useMultiFileAuthState,
|
useMultiFileAuthState,
|
||||||
} from "./session.runtime.js";
|
} from "./session.runtime.js";
|
||||||
|
import {
|
||||||
|
DEFAULT_WHATSAPP_SOCKET_TIMING,
|
||||||
|
type WhatsAppSocketTimingOptions,
|
||||||
|
} from "./socket-timing.js";
|
||||||
export { formatError, getStatusCode } from "./session-errors.js";
|
export { formatError, getStatusCode } from "./session-errors.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -126,7 +130,7 @@ async function printTerminalQr(qr: string): Promise<void> {
|
|||||||
export async function createWaSocket(
|
export async function createWaSocket(
|
||||||
printQr: boolean,
|
printQr: boolean,
|
||||||
verbose: boolean,
|
verbose: boolean,
|
||||||
opts: { authDir?: string; onQr?: (qr: string) => void } = {},
|
opts: { authDir?: string; onQr?: (qr: string) => void } & WhatsAppSocketTimingOptions = {},
|
||||||
): Promise<ReturnType<typeof makeWASocket>> {
|
): Promise<ReturnType<typeof makeWASocket>> {
|
||||||
const baseLogger = getChildLogger(
|
const baseLogger = getChildLogger(
|
||||||
{ module: "baileys" },
|
{ module: "baileys" },
|
||||||
@@ -151,6 +155,13 @@ export async function createWaSocket(
|
|||||||
const { version } = await fetchLatestBaileysVersion();
|
const { version } = await fetchLatestBaileysVersion();
|
||||||
const agent = await resolveEnvProxyAgent(sessionLogger);
|
const agent = await resolveEnvProxyAgent(sessionLogger);
|
||||||
const fetchAgent = await resolveEnvFetchDispatcher(sessionLogger, agent);
|
const fetchAgent = await resolveEnvFetchDispatcher(sessionLogger, agent);
|
||||||
|
const socketTiming = {
|
||||||
|
keepAliveIntervalMs:
|
||||||
|
opts.keepAliveIntervalMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
|
||||||
|
connectTimeoutMs: opts.connectTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
|
||||||
|
defaultQueryTimeoutMs:
|
||||||
|
opts.defaultQueryTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
|
||||||
|
};
|
||||||
const sock = makeWASocket({
|
const sock = makeWASocket({
|
||||||
auth: {
|
auth: {
|
||||||
creds: state.creds,
|
creds: state.creds,
|
||||||
@@ -162,6 +173,7 @@ export async function createWaSocket(
|
|||||||
browser: ["openclaw", "cli", VERSION],
|
browser: ["openclaw", "cli", VERSION],
|
||||||
syncFullHistory: false,
|
syncFullHistory: false,
|
||||||
markOnlineOnConnect: false,
|
markOnlineOnConnect: false,
|
||||||
|
...socketTiming,
|
||||||
agent,
|
agent,
|
||||||
// Baileys types still model `fetchAgent` as a Node agent even though the
|
// Baileys types still model `fetchAgent` as a Node agent even though the
|
||||||
// runtime path accepts an undici dispatcher for upload fetches.
|
// runtime path accepts an undici dispatcher for upload fetches.
|
||||||
|
|||||||
49
extensions/whatsapp/src/socket-timing.test.ts
Normal file
49
extensions/whatsapp/src/socket-timing.test.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DEFAULT_WHATSAPP_SOCKET_TIMING, resolveWhatsAppSocketTiming } from "./socket-timing.js";
|
||||||
|
|
||||||
|
describe("resolveWhatsAppSocketTiming", () => {
|
||||||
|
it("uses OpenClaw's explicit WhatsApp Web socket defaults", () => {
|
||||||
|
expect(resolveWhatsAppSocketTiming({})).toEqual(DEFAULT_WHATSAPP_SOCKET_TIMING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads Baileys timing values from web.whatsapp config", () => {
|
||||||
|
expect(
|
||||||
|
resolveWhatsAppSocketTiming({
|
||||||
|
web: {
|
||||||
|
whatsapp: {
|
||||||
|
keepAliveIntervalMs: 10_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
keepAliveIntervalMs: 10_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets call-site overrides take precedence over config", () => {
|
||||||
|
expect(
|
||||||
|
resolveWhatsAppSocketTiming(
|
||||||
|
{
|
||||||
|
web: {
|
||||||
|
whatsapp: {
|
||||||
|
keepAliveIntervalMs: 10_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepAliveIntervalMs: 20_000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
keepAliveIntervalMs: 20_000,
|
||||||
|
connectTimeoutMs: 90_000,
|
||||||
|
defaultQueryTimeoutMs: 120_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
38
extensions/whatsapp/src/socket-timing.ts
Normal file
38
extensions/whatsapp/src/socket-timing.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||||
|
|
||||||
|
export type WhatsAppSocketTimingOptions = {
|
||||||
|
keepAliveIntervalMs?: number;
|
||||||
|
connectTimeoutMs?: number;
|
||||||
|
defaultQueryTimeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_WHATSAPP_SOCKET_TIMING: Required<WhatsAppSocketTimingOptions> = {
|
||||||
|
keepAliveIntervalMs: 25_000,
|
||||||
|
connectTimeoutMs: 60_000,
|
||||||
|
defaultQueryTimeoutMs: 60_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
function positiveInteger(value: number | undefined): number | undefined {
|
||||||
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWhatsAppSocketTiming(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
overrides?: WhatsAppSocketTimingOptions,
|
||||||
|
): Required<WhatsAppSocketTimingOptions> {
|
||||||
|
const configured = cfg.web?.whatsapp;
|
||||||
|
return {
|
||||||
|
keepAliveIntervalMs:
|
||||||
|
positiveInteger(overrides?.keepAliveIntervalMs) ??
|
||||||
|
positiveInteger(configured?.keepAliveIntervalMs) ??
|
||||||
|
DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
|
||||||
|
connectTimeoutMs:
|
||||||
|
positiveInteger(overrides?.connectTimeoutMs) ??
|
||||||
|
positiveInteger(configured?.connectTimeoutMs) ??
|
||||||
|
DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
|
||||||
|
defaultQueryTimeoutMs:
|
||||||
|
positiveInteger(overrides?.defaultQueryTimeoutMs) ??
|
||||||
|
positiveInteger(configured?.defaultQueryTimeoutMs) ??
|
||||||
|
DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21678,6 +21678,39 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
description:
|
description:
|
||||||
"Reconnect backoff policy for web channel reconnect attempts after transport failure. Keep bounded retries and jitter tuned to avoid thundering-herd reconnect behavior.",
|
"Reconnect backoff policy for web channel reconnect attempts after transport failure. Keep bounded retries and jitter tuned to avoid thundering-herd reconnect behavior.",
|
||||||
},
|
},
|
||||||
|
whatsapp: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
keepAliveIntervalMs: {
|
||||||
|
type: "integer",
|
||||||
|
exclusiveMinimum: 0,
|
||||||
|
maximum: 9007199254740991,
|
||||||
|
title: "WhatsApp Web Keepalive Interval (ms)",
|
||||||
|
description:
|
||||||
|
"Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
|
||||||
|
},
|
||||||
|
connectTimeoutMs: {
|
||||||
|
type: "integer",
|
||||||
|
exclusiveMinimum: 0,
|
||||||
|
maximum: 9007199254740991,
|
||||||
|
title: "WhatsApp Web Connect Timeout (ms)",
|
||||||
|
description:
|
||||||
|
"Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
|
||||||
|
},
|
||||||
|
defaultQueryTimeoutMs: {
|
||||||
|
type: "integer",
|
||||||
|
exclusiveMinimum: 0,
|
||||||
|
maximum: 9007199254740991,
|
||||||
|
title: "WhatsApp Web Query Timeout (ms)",
|
||||||
|
description:
|
||||||
|
"Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
title: "WhatsApp Web Socket Timing",
|
||||||
|
description:
|
||||||
|
"WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
title: "Web Channel",
|
title: "Web Channel",
|
||||||
@@ -28020,6 +28053,26 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
|||||||
help: "Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
|
help: "Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
|
||||||
tags: ["performance"],
|
tags: ["performance"],
|
||||||
},
|
},
|
||||||
|
"web.whatsapp": {
|
||||||
|
label: "WhatsApp Web Socket Timing",
|
||||||
|
help: "WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
|
||||||
|
tags: ["advanced"],
|
||||||
|
},
|
||||||
|
"web.whatsapp.keepAliveIntervalMs": {
|
||||||
|
label: "WhatsApp Web Keepalive Interval (ms)",
|
||||||
|
help: "Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
|
||||||
|
tags: ["performance"],
|
||||||
|
},
|
||||||
|
"web.whatsapp.connectTimeoutMs": {
|
||||||
|
label: "WhatsApp Web Connect Timeout (ms)",
|
||||||
|
help: "Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
|
||||||
|
tags: ["performance"],
|
||||||
|
},
|
||||||
|
"web.whatsapp.defaultQueryTimeoutMs": {
|
||||||
|
label: "WhatsApp Web Query Timeout (ms)",
|
||||||
|
help: "Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
|
||||||
|
tags: ["performance"],
|
||||||
|
},
|
||||||
"discovery.wideArea": {
|
"discovery.wideArea": {
|
||||||
label: "Wide-area Discovery",
|
label: "Wide-area Discovery",
|
||||||
help: "Wide-area discovery configuration group for exposing discovery signals beyond local-link scopes. Enable only in deployments that intentionally aggregate gateway presence across sites.",
|
help: "Wide-area discovery configuration group for exposing discovery signals beyond local-link scopes. Enable only in deployments that intentionally aggregate gateway presence across sites.",
|
||||||
|
|||||||
@@ -308,6 +308,10 @@ const TARGET_KEYS = [
|
|||||||
"web.reconnect.factor",
|
"web.reconnect.factor",
|
||||||
"web.reconnect.jitter",
|
"web.reconnect.jitter",
|
||||||
"web.reconnect.maxAttempts",
|
"web.reconnect.maxAttempts",
|
||||||
|
"web.whatsapp",
|
||||||
|
"web.whatsapp.keepAliveIntervalMs",
|
||||||
|
"web.whatsapp.connectTimeoutMs",
|
||||||
|
"web.whatsapp.defaultQueryTimeoutMs",
|
||||||
"discovery",
|
"discovery",
|
||||||
"discovery.wideArea.domain",
|
"discovery.wideArea.domain",
|
||||||
"discovery.wideArea.enabled",
|
"discovery.wideArea.enabled",
|
||||||
|
|||||||
@@ -401,6 +401,14 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"Randomization factor (0-1) applied to reconnect delays to desynchronize clients after outage events. Keep non-zero jitter in multi-client deployments to reduce synchronized spikes.",
|
"Randomization factor (0-1) applied to reconnect delays to desynchronize clients after outage events. Keep non-zero jitter in multi-client deployments to reduce synchronized spikes.",
|
||||||
"web.reconnect.maxAttempts":
|
"web.reconnect.maxAttempts":
|
||||||
"Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
|
"Maximum reconnect attempts before giving up for the current failure sequence (0 means no retries). Use finite caps for controlled failure handling in automation-sensitive environments.",
|
||||||
|
"web.whatsapp":
|
||||||
|
"WhatsApp Web socket timing controls passed directly to Baileys. Tune these when network edges, proxies, or NATs are closing otherwise healthy WhatsApp Web sessions.",
|
||||||
|
"web.whatsapp.keepAliveIntervalMs":
|
||||||
|
"Baileys WhatsApp Web application ping interval in milliseconds. Lower values detect and refresh idle links sooner; keep this comfortably below your network's idle-flow timeout.",
|
||||||
|
"web.whatsapp.connectTimeoutMs":
|
||||||
|
"Maximum time in milliseconds Baileys waits for the WhatsApp WebSocket opening handshake. Use a higher value on slow or lossy networks that report opening handshake 408 timeouts.",
|
||||||
|
"web.whatsapp.defaultQueryTimeoutMs":
|
||||||
|
"Default Baileys query timeout in milliseconds for WhatsApp Web requests. Keep aligned with upstream unless a network-specific investigation shows queries need longer.",
|
||||||
canvasHost:
|
canvasHost:
|
||||||
"Canvas host settings for serving canvas assets and local live-reload behavior used by canvas-enabled workflows. Keep disabled unless canvas-hosted assets are actively used.",
|
"Canvas host settings for serving canvas assets and local live-reload behavior used by canvas-enabled workflows. Keep disabled unless canvas-hosted assets are actively used.",
|
||||||
"canvasHost.enabled":
|
"canvasHost.enabled":
|
||||||
|
|||||||
@@ -802,6 +802,10 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||||||
"web.reconnect.factor": "Web Reconnect Backoff Factor",
|
"web.reconnect.factor": "Web Reconnect Backoff Factor",
|
||||||
"web.reconnect.jitter": "Web Reconnect Jitter",
|
"web.reconnect.jitter": "Web Reconnect Jitter",
|
||||||
"web.reconnect.maxAttempts": "Web Reconnect Max Attempts",
|
"web.reconnect.maxAttempts": "Web Reconnect Max Attempts",
|
||||||
|
"web.whatsapp": "WhatsApp Web Socket Timing",
|
||||||
|
"web.whatsapp.keepAliveIntervalMs": "WhatsApp Web Keepalive Interval (ms)",
|
||||||
|
"web.whatsapp.connectTimeoutMs": "WhatsApp Web Connect Timeout (ms)",
|
||||||
|
"web.whatsapp.defaultQueryTimeoutMs": "WhatsApp Web Query Timeout (ms)",
|
||||||
discovery: "Discovery",
|
discovery: "Discovery",
|
||||||
"discovery.wideArea": "Wide-area Discovery",
|
"discovery.wideArea": "Wide-area Discovery",
|
||||||
"discovery.wideArea.enabled": "Wide-area Discovery Enabled",
|
"discovery.wideArea.enabled": "Wide-area Discovery Enabled",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { SENSITIVE_URL_HINT_TAG } from "../shared/net/redact-sensitive-url.js";
|
|||||||
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
|
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
|
||||||
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
|
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
|
||||||
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
|
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
|
||||||
|
import { OpenClawSchema } from "./zod-schema.js";
|
||||||
|
|
||||||
describe("config schema", () => {
|
describe("config schema", () => {
|
||||||
type SchemaInput = NonNullable<Parameters<typeof buildConfigSchema>[0]>;
|
type SchemaInput = NonNullable<Parameters<typeof buildConfigSchema>[0]>;
|
||||||
@@ -290,6 +291,24 @@ describe("config schema", () => {
|
|||||||
expect(parsed?.web?.fetch?.maxResponseBytes).toBe(2_000_000);
|
expect(parsed?.web?.fetch?.maxResponseBytes).toBe(2_000_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts WhatsApp Web Baileys socket timing in the runtime zod schema", () => {
|
||||||
|
const parsed = OpenClawSchema.parse({
|
||||||
|
web: {
|
||||||
|
whatsapp: {
|
||||||
|
keepAliveIntervalMs: 15_000,
|
||||||
|
connectTimeoutMs: 60_000,
|
||||||
|
defaultQueryTimeoutMs: 90_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed.web?.whatsapp).toEqual({
|
||||||
|
keepAliveIntervalMs: 15_000,
|
||||||
|
connectTimeoutMs: 60_000,
|
||||||
|
defaultQueryTimeoutMs: 90_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts web fetch ssrfPolicy in the runtime zod schema", () => {
|
it("accepts web fetch ssrfPolicy in the runtime zod schema", () => {
|
||||||
const parsed = ToolsSchema.parse({
|
const parsed = ToolsSchema.parse({
|
||||||
web: {
|
web: {
|
||||||
|
|||||||
@@ -290,11 +290,21 @@ export type WebReconnectConfig = {
|
|||||||
maxAttempts?: number; // 0 = unlimited
|
maxAttempts?: number; // 0 = unlimited
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WebWhatsAppConfig = {
|
||||||
|
/** Baileys application ping interval in milliseconds. Default: 25000. */
|
||||||
|
keepAliveIntervalMs?: number;
|
||||||
|
/** WebSocket opening handshake timeout in milliseconds. Default: 60000. */
|
||||||
|
connectTimeoutMs?: number;
|
||||||
|
/** Baileys query timeout in milliseconds. Default: 60000. */
|
||||||
|
defaultQueryTimeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type WebConfig = {
|
export type WebConfig = {
|
||||||
/** If false, do not start the WhatsApp web provider. Default: true. */
|
/** If false, do not start the WhatsApp web provider. Default: true. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
heartbeatSeconds?: number;
|
heartbeatSeconds?: number;
|
||||||
reconnect?: WebReconnectConfig;
|
reconnect?: WebReconnectConfig;
|
||||||
|
whatsapp?: WebWhatsAppConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Provider docking: allowlists keyed by provider id (and internal "webchat").
|
// Provider docking: allowlists keyed by provider id (and internal "webchat").
|
||||||
|
|||||||
@@ -678,6 +678,14 @@ export const OpenClawSchema = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
whatsapp: z
|
||||||
|
.object({
|
||||||
|
keepAliveIntervalMs: z.number().int().positive().optional(),
|
||||||
|
connectTimeoutMs: z.number().int().positive().optional(),
|
||||||
|
defaultQueryTimeoutMs: z.number().int().positive().optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user