mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
refactor(whatsapp): remove legacy heartbeat runners
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
7c25208c10ba075f76719883b7b2aefe4cf5e42328bad3acff1c5055350d344f plugin-sdk-api-baseline.json
|
||||
6cac90f85065bcbd447911a0c7c54e7d6992278fd1b95a3e78ae4be3f185848a plugin-sdk-api-baseline.jsonl
|
||||
84befa4ad71bee22d9ea91a6ff689532deb3783143af7488a98a7341d5ce5f25 plugin-sdk-api-baseline.json
|
||||
046bb0c9bc40bfb2f8a323bf658c45eeeb486571301757abc5472018db7d2189 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -37,8 +37,6 @@ export {
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
monitorWebChannel,
|
||||
resolveHeartbeatRecipients,
|
||||
runWebHeartbeatOnce,
|
||||
SILENT_REPLY_TOKEN,
|
||||
stripHeartbeatToken,
|
||||
type WebChannelStatus,
|
||||
|
||||
@@ -2,6 +2,5 @@ export { HEARTBEAT_PROMPT, stripHeartbeatToken } from "openclaw/plugin-sdk/reply
|
||||
export { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-runtime";
|
||||
|
||||
export { DEFAULT_WEB_MEDIA_BYTES } from "./auto-reply/constants.js";
|
||||
export { resolveHeartbeatRecipients, runWebHeartbeatOnce } from "./auto-reply/heartbeat-runner.js";
|
||||
export { monitorWebChannel } from "./auto-reply/monitor.js";
|
||||
export type { WebChannelStatus, WebMonitorTuning } from "./auto-reply/types.js";
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
export { appendCronStyleCurrentTimeLine } from "openclaw/plugin-sdk/agent-runtime";
|
||||
export {
|
||||
canonicalizeMainSessionAlias,
|
||||
loadSessionStore,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
export {
|
||||
emitHeartbeatEvent,
|
||||
resolveHeartbeatVisibility,
|
||||
resolveIndicatorType,
|
||||
} from "openclaw/plugin-sdk/heartbeat-runtime";
|
||||
export {
|
||||
hasOutboundReplyContent,
|
||||
resolveSendableOutboundReplyParts,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
export {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
HEARTBEAT_TOKEN,
|
||||
getReplyFromConfig,
|
||||
resolveHeartbeatPrompt,
|
||||
resolveHeartbeatReplyPayload,
|
||||
stripHeartbeatToken,
|
||||
} from "openclaw/plugin-sdk/reply-runtime";
|
||||
export { normalizeMainKey } from "openclaw/plugin-sdk/routing";
|
||||
export { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
export { redactIdentifier } from "openclaw/plugin-sdk/text-runtime";
|
||||
export { resolveWhatsAppHeartbeatRecipients } from "../runtime-api.js";
|
||||
export { sendMessageWhatsApp } from "../send.js";
|
||||
export { formatError } from "../session.js";
|
||||
export { whatsappHeartbeatLog } from "./loggers.js";
|
||||
@@ -1,214 +0,0 @@
|
||||
import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { sendMessageWhatsApp } from "../send.js";
|
||||
import type { getReplyFromConfig } from "./heartbeat-runner.runtime.js";
|
||||
|
||||
const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
visibility: { showAlerts: true, showOk: true, useIndicator: false },
|
||||
store: {} as Record<string, { updatedAt?: number; sessionId?: string }>,
|
||||
snapshot: {
|
||||
key: "k",
|
||||
entry: { sessionId: "s1", updatedAt: 123 },
|
||||
fresh: false,
|
||||
resetPolicy: { mode: "none", atHour: null, idleMinutes: null },
|
||||
dailyResetAt: null as number | null,
|
||||
idleExpiresAt: null as number | null,
|
||||
},
|
||||
events: [] as unknown[],
|
||||
loggerInfoCalls: [] as unknown[][],
|
||||
loggerWarnCalls: [] as unknown[][],
|
||||
heartbeatInfoLogs: [] as string[],
|
||||
heartbeatWarnLogs: [] as string[],
|
||||
}));
|
||||
|
||||
vi.mock("./heartbeat-runner.runtime.js", () => {
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: (...args: unknown[]) => state.loggerInfoCalls.push(args),
|
||||
warn: (...args: unknown[]) => state.loggerWarnCalls.push(args),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
return {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS: 32,
|
||||
HEARTBEAT_TOKEN,
|
||||
appendCronStyleCurrentTimeLine: (body: string) =>
|
||||
`${body}\nCurrent time: 2026-02-15T00:00:00Z (mock)`,
|
||||
canonicalizeMainSessionAlias: ({ sessionKey }: { sessionKey: string }) => sessionKey,
|
||||
emitHeartbeatEvent: (event: unknown) => state.events.push(event),
|
||||
formatError: (err: unknown) => `ERR:${String(err)}`,
|
||||
getChildLogger: () => logger,
|
||||
getReplyFromConfig: vi.fn(async () => undefined),
|
||||
hasOutboundReplyContent: (payload: { text?: string } | undefined) =>
|
||||
Boolean(payload?.text?.trim()),
|
||||
loadConfig: () => ({ agents: { defaults: {} }, session: {} }),
|
||||
loadSessionStore: () => state.store,
|
||||
normalizeMainKey: () => null,
|
||||
redactIdentifier,
|
||||
resolveHeartbeatPrompt: (prompt?: string) => prompt || "Heartbeat",
|
||||
resolveHeartbeatReplyPayload: (reply: unknown) => reply,
|
||||
resolveHeartbeatVisibility: () => state.visibility,
|
||||
resolveIndicatorType: (status: string) => `indicator:${status}`,
|
||||
resolveSendableOutboundReplyParts: (payload: { text?: string }) => ({
|
||||
text: payload.text ?? "",
|
||||
hasMedia: false,
|
||||
}),
|
||||
resolveSessionKey: () => "k",
|
||||
resolveStorePath: () => "/tmp/store.json",
|
||||
resolveWhatsAppHeartbeatRecipients: () => [],
|
||||
sendMessageWhatsApp: vi.fn(async () => ({ messageId: "m1" })),
|
||||
stripHeartbeatToken: (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed === HEARTBEAT_TOKEN) {
|
||||
return { shouldSkip: true, text: "" };
|
||||
}
|
||||
return { shouldSkip: false, text: trimmed };
|
||||
},
|
||||
updateSessionStore: async (_path: string, updater: (store: typeof state.store) => void) => {
|
||||
updater(state.store);
|
||||
},
|
||||
whatsappHeartbeatLog: {
|
||||
info: (msg: string) => state.heartbeatInfoLogs.push(msg),
|
||||
warn: (msg: string) => state.heartbeatWarnLogs.push(msg),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./session-snapshot.js", () => ({
|
||||
getSessionSnapshot: () => state.snapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../reconnect.js", () => ({
|
||||
newConnectionId: () => "run-1",
|
||||
}));
|
||||
|
||||
describe("runWebHeartbeatOnce", () => {
|
||||
let senderMock: ReturnType<typeof vi.fn>;
|
||||
let sender: typeof sendMessageWhatsApp;
|
||||
let replyResolverMock: ReturnType<typeof vi.fn>;
|
||||
let replyResolver: typeof getReplyFromConfig;
|
||||
let runWebHeartbeatOnce: typeof import("./heartbeat-runner.js").runWebHeartbeatOnce;
|
||||
|
||||
const buildRunArgs = (overrides: Record<string, unknown> = {}) => ({
|
||||
cfg: { agents: { defaults: {} }, session: {} } as never,
|
||||
to: "+123",
|
||||
sender,
|
||||
replyResolver,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
({ runWebHeartbeatOnce } = await import("./heartbeat-runner.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
state.visibility = { showAlerts: true, showOk: true, useIndicator: false };
|
||||
state.store = { k: { updatedAt: 999, sessionId: "s1" } };
|
||||
state.snapshot = {
|
||||
key: "k",
|
||||
entry: { sessionId: "s1", updatedAt: 123 },
|
||||
fresh: false,
|
||||
resetPolicy: { mode: "none", atHour: null, idleMinutes: null },
|
||||
dailyResetAt: null,
|
||||
idleExpiresAt: null,
|
||||
};
|
||||
state.events = [];
|
||||
state.loggerInfoCalls = [];
|
||||
state.loggerWarnCalls = [];
|
||||
state.heartbeatInfoLogs = [];
|
||||
state.heartbeatWarnLogs = [];
|
||||
|
||||
senderMock = vi.fn(async () => ({ messageId: "m1" }));
|
||||
sender = senderMock as unknown as typeof sendMessageWhatsApp;
|
||||
replyResolverMock = vi.fn(async () => undefined);
|
||||
replyResolver = replyResolverMock as unknown as typeof getReplyFromConfig;
|
||||
});
|
||||
|
||||
it("supports manual override body dry-run without sending", async () => {
|
||||
await runWebHeartbeatOnce(buildRunArgs({ overrideBody: "hello", dryRun: true }));
|
||||
expect(senderMock).not.toHaveBeenCalled();
|
||||
expect(state.events).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("sends HEARTBEAT_OK when reply is empty and showOk is enabled", async () => {
|
||||
await runWebHeartbeatOnce(buildRunArgs());
|
||||
expect(senderMock).toHaveBeenCalledWith(
|
||||
"+123",
|
||||
HEARTBEAT_TOKEN,
|
||||
expect.objectContaining({ verbose: false, cfg: expect.any(Object) }),
|
||||
);
|
||||
expect(state.events).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ status: "ok-empty", silent: false })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("injects a cron-style Current time line into the heartbeat prompt", async () => {
|
||||
await runWebHeartbeatOnce(
|
||||
buildRunArgs({
|
||||
cfg: { agents: { defaults: { heartbeat: { prompt: "Ops check" } } }, session: {} } as never,
|
||||
dryRun: true,
|
||||
}),
|
||||
);
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyResolverMock.mock.calls[0]?.[0];
|
||||
expect(ctx?.Body).toContain("Ops check");
|
||||
expect(ctx?.Body).toContain("Current time: 2026-02-15T00:00:00Z (mock)");
|
||||
});
|
||||
|
||||
it("treats heartbeat token-only replies as ok-token and preserves session updatedAt", async () => {
|
||||
replyResolverMock.mockResolvedValue({ text: HEARTBEAT_TOKEN });
|
||||
await runWebHeartbeatOnce(buildRunArgs());
|
||||
expect(state.store.k?.updatedAt).toBe(123);
|
||||
expect(senderMock).toHaveBeenCalledWith(
|
||||
"+123",
|
||||
HEARTBEAT_TOKEN,
|
||||
expect.objectContaining({ verbose: false, cfg: expect.any(Object) }),
|
||||
);
|
||||
expect(state.events).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ status: "ok-token", silent: false })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips sending alerts when showAlerts is disabled but still emits a skipped event", async () => {
|
||||
state.visibility = { showAlerts: false, showOk: true, useIndicator: true };
|
||||
replyResolverMock.mockResolvedValue({ text: "ALERT" });
|
||||
await runWebHeartbeatOnce(buildRunArgs());
|
||||
expect(senderMock).not.toHaveBeenCalled();
|
||||
expect(state.events).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ status: "skipped", reason: "alerts-disabled", preview: "ALERT" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("emits failed events when sending throws and rethrows the error", async () => {
|
||||
replyResolverMock.mockResolvedValue({ text: "ALERT" });
|
||||
senderMock.mockRejectedValueOnce(new Error("nope"));
|
||||
await expect(runWebHeartbeatOnce(buildRunArgs())).rejects.toThrow("nope");
|
||||
expect(state.events).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ status: "failed", reason: "ERR:Error: nope" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts recipient and omits body preview in heartbeat logs", async () => {
|
||||
replyResolverMock.mockResolvedValue({ text: "sensitive heartbeat body" });
|
||||
await runWebHeartbeatOnce(buildRunArgs({ dryRun: true }));
|
||||
|
||||
const expected = redactIdentifier("+123");
|
||||
const heartbeatLogs = state.heartbeatInfoLogs.join("\n");
|
||||
const childLoggerLogs = state.loggerInfoCalls.map((entry) => JSON.stringify(entry)).join("\n");
|
||||
|
||||
expect(heartbeatLogs).toContain(expected);
|
||||
expect(heartbeatLogs).not.toContain("+123");
|
||||
expect(heartbeatLogs).not.toContain("sensitive heartbeat body");
|
||||
|
||||
expect(childLoggerLogs).toContain(expected);
|
||||
expect(childLoggerLogs).not.toContain("+123");
|
||||
expect(childLoggerLogs).not.toContain("sensitive heartbeat body");
|
||||
expect(childLoggerLogs).not.toContain('"preview"');
|
||||
});
|
||||
});
|
||||
@@ -1,330 +0,0 @@
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { newConnectionId } from "../reconnect.js";
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
HEARTBEAT_TOKEN,
|
||||
appendCronStyleCurrentTimeLine,
|
||||
canonicalizeMainSessionAlias,
|
||||
emitHeartbeatEvent,
|
||||
formatError,
|
||||
getRuntimeConfig,
|
||||
getChildLogger,
|
||||
getReplyFromConfig,
|
||||
hasOutboundReplyContent,
|
||||
loadSessionStore,
|
||||
normalizeMainKey,
|
||||
redactIdentifier,
|
||||
resolveHeartbeatPrompt,
|
||||
resolveHeartbeatReplyPayload,
|
||||
resolveHeartbeatVisibility,
|
||||
resolveIndicatorType,
|
||||
resolveSendableOutboundReplyParts,
|
||||
resolveSessionKey,
|
||||
resolveStorePath,
|
||||
resolveWhatsAppHeartbeatRecipients,
|
||||
sendMessageWhatsApp,
|
||||
stripHeartbeatToken,
|
||||
updateSessionStore,
|
||||
whatsappHeartbeatLog,
|
||||
} from "./heartbeat-runner.runtime.js";
|
||||
import { getSessionSnapshot } from "./session-snapshot.js";
|
||||
|
||||
function resolveDefaultAgentIdFromConfig(cfg: ReturnType<typeof getRuntimeConfig>): string {
|
||||
const agents = cfg.agents?.list ?? [];
|
||||
const chosen = agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? "main";
|
||||
return normalizeOptionalLowercaseString(chosen) ?? "main";
|
||||
}
|
||||
|
||||
export async function runWebHeartbeatOnce(opts: {
|
||||
cfg?: ReturnType<typeof getRuntimeConfig>;
|
||||
to: string;
|
||||
verbose?: boolean;
|
||||
replyResolver?: typeof getReplyFromConfig;
|
||||
sender?: typeof sendMessageWhatsApp;
|
||||
sessionId?: string;
|
||||
overrideBody?: string;
|
||||
dryRun?: boolean;
|
||||
}) {
|
||||
const { cfg: cfgOverride, to, verbose = false, sessionId, overrideBody, dryRun = false } = opts;
|
||||
const replyResolver = opts.replyResolver ?? getReplyFromConfig;
|
||||
const sender = opts.sender ?? sendMessageWhatsApp;
|
||||
const runId = newConnectionId();
|
||||
const redactedTo = redactIdentifier(to);
|
||||
const heartbeatLogger = getChildLogger({
|
||||
module: "web-heartbeat",
|
||||
runId,
|
||||
to: redactedTo,
|
||||
});
|
||||
|
||||
const cfg = cfgOverride ?? getRuntimeConfig();
|
||||
|
||||
// Resolve heartbeat visibility settings for WhatsApp
|
||||
const visibility = resolveHeartbeatVisibility({ cfg, channel: "whatsapp" });
|
||||
const heartbeatOkText = HEARTBEAT_TOKEN;
|
||||
|
||||
const maybeSendHeartbeatOk = async (): Promise<boolean> => {
|
||||
if (!visibility.showOk) {
|
||||
return false;
|
||||
}
|
||||
if (dryRun) {
|
||||
whatsappHeartbeatLog.info(`[dry-run] heartbeat ok -> ${redactedTo}`);
|
||||
return false;
|
||||
}
|
||||
const sendResult = await sender(to, heartbeatOkText, { verbose, cfg });
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
to: redactedTo,
|
||||
messageId: sendResult.messageId,
|
||||
chars: heartbeatOkText.length,
|
||||
reason: "heartbeat-ok",
|
||||
},
|
||||
"heartbeat ok sent",
|
||||
);
|
||||
whatsappHeartbeatLog.info(`heartbeat ok sent to ${redactedTo} (id ${sendResult.messageId})`);
|
||||
return true;
|
||||
};
|
||||
|
||||
const sessionCfg = cfg.session;
|
||||
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
||||
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
|
||||
// Canonicalize so the written key matches what read paths produce (#29683).
|
||||
const rawSessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
|
||||
const sessionKey = canonicalizeMainSessionAlias({
|
||||
cfg,
|
||||
agentId: resolveDefaultAgentIdFromConfig(cfg),
|
||||
sessionKey: rawSessionKey,
|
||||
});
|
||||
if (sessionId) {
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const current = store[sessionKey] ?? {};
|
||||
store[sessionKey] = {
|
||||
...current,
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await updateSessionStore(storePath, (nextStore) => {
|
||||
const nextCurrent = nextStore[sessionKey] ?? current;
|
||||
nextStore[sessionKey] = {
|
||||
...nextCurrent,
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
});
|
||||
}
|
||||
const sessionSnapshot = getSessionSnapshot(cfg, to, true, { sessionKey });
|
||||
if (verbose) {
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
to: redactedTo,
|
||||
sessionKey: sessionSnapshot.key,
|
||||
sessionId: sessionId ?? sessionSnapshot.entry?.sessionId ?? null,
|
||||
sessionFresh: sessionSnapshot.fresh,
|
||||
resetMode: sessionSnapshot.resetPolicy.mode,
|
||||
resetAtHour: sessionSnapshot.resetPolicy.atHour,
|
||||
idleMinutes: sessionSnapshot.resetPolicy.idleMinutes ?? null,
|
||||
dailyResetAt: sessionSnapshot.dailyResetAt ?? null,
|
||||
idleExpiresAt: sessionSnapshot.idleExpiresAt ?? null,
|
||||
},
|
||||
"heartbeat session snapshot",
|
||||
);
|
||||
}
|
||||
|
||||
if (overrideBody && overrideBody.trim().length === 0) {
|
||||
throw new Error("Override body must be non-empty when provided.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (overrideBody) {
|
||||
if (dryRun) {
|
||||
whatsappHeartbeatLog.info(
|
||||
`[dry-run] web send -> ${redactedTo} (${overrideBody.trim().length} chars, manual message)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sendResult = await sender(to, overrideBody, { verbose, cfg });
|
||||
emitHeartbeatEvent({
|
||||
status: "sent",
|
||||
to,
|
||||
preview: overrideBody.slice(0, 160),
|
||||
hasMedia: false,
|
||||
channel: "whatsapp",
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
||||
});
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
to: redactedTo,
|
||||
messageId: sendResult.messageId,
|
||||
chars: overrideBody.length,
|
||||
reason: "manual-message",
|
||||
},
|
||||
"manual heartbeat message sent",
|
||||
);
|
||||
whatsappHeartbeatLog.info(
|
||||
`manual heartbeat sent to ${redactedTo} (id ${sendResult.messageId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
|
||||
heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped");
|
||||
emitHeartbeatEvent({
|
||||
status: "skipped",
|
||||
to,
|
||||
reason: "alerts-disabled",
|
||||
channel: "whatsapp",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const replyResult = await replyResolver(
|
||||
{
|
||||
Body: appendCronStyleCurrentTimeLine(
|
||||
resolveHeartbeatPrompt(cfg.agents?.defaults?.heartbeat?.prompt),
|
||||
cfg,
|
||||
Date.now(),
|
||||
),
|
||||
From: to,
|
||||
To: to,
|
||||
MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
|
||||
},
|
||||
{ isHeartbeat: true },
|
||||
cfg,
|
||||
);
|
||||
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
||||
|
||||
if (!replyPayload || !hasOutboundReplyContent(replyPayload)) {
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
to: redactedTo,
|
||||
reason: "empty-reply",
|
||||
sessionId: sessionSnapshot.entry?.sessionId ?? null,
|
||||
},
|
||||
"heartbeat skipped",
|
||||
);
|
||||
const okSent = await maybeSendHeartbeatOk();
|
||||
emitHeartbeatEvent({
|
||||
status: "ok-empty",
|
||||
to,
|
||||
channel: "whatsapp",
|
||||
silent: !okSent,
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-empty") : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = resolveSendableOutboundReplyParts(replyPayload);
|
||||
const hasMedia = reply.hasMedia;
|
||||
const ackMaxChars = Math.max(
|
||||
0,
|
||||
cfg.agents?.defaults?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
);
|
||||
const stripped = stripHeartbeatToken(replyPayload.text, {
|
||||
mode: "heartbeat",
|
||||
maxAckChars: ackMaxChars,
|
||||
});
|
||||
if (stripped.shouldSkip && !hasMedia) {
|
||||
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
|
||||
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
|
||||
await updateSessionStore(storePath, (nextStore) => {
|
||||
const nextEntry = nextStore[sessionSnapshot.key];
|
||||
if (!nextEntry) {
|
||||
return;
|
||||
}
|
||||
nextStore[sessionSnapshot.key] = {
|
||||
...nextEntry,
|
||||
updatedAt: sessionSnapshot.entry.updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
heartbeatLogger.info(
|
||||
{ to: redactedTo, reason: "heartbeat-token", rawLength: replyPayload.text?.length },
|
||||
"heartbeat skipped",
|
||||
);
|
||||
const okSent = await maybeSendHeartbeatOk();
|
||||
emitHeartbeatEvent({
|
||||
status: "ok-token",
|
||||
to,
|
||||
channel: "whatsapp",
|
||||
silent: !okSent,
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-token") : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasMedia) {
|
||||
heartbeatLogger.warn(
|
||||
{ to: redactedTo },
|
||||
"heartbeat reply contained media; sending text only",
|
||||
);
|
||||
}
|
||||
|
||||
const finalText = stripped.text || reply.text;
|
||||
|
||||
// Check if alerts are disabled for WhatsApp
|
||||
if (!visibility.showAlerts) {
|
||||
heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped");
|
||||
emitHeartbeatEvent({
|
||||
status: "skipped",
|
||||
to,
|
||||
reason: "alerts-disabled",
|
||||
preview: finalText.slice(0, 200),
|
||||
channel: "whatsapp",
|
||||
hasMedia,
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
heartbeatLogger.info(
|
||||
{ to: redactedTo, reason: "dry-run", chars: finalText.length },
|
||||
"heartbeat dry-run",
|
||||
);
|
||||
whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${redactedTo} (${finalText.length} chars)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sendResult = await sender(to, finalText, { verbose, cfg });
|
||||
emitHeartbeatEvent({
|
||||
status: "sent",
|
||||
to,
|
||||
preview: finalText.slice(0, 160),
|
||||
hasMedia,
|
||||
channel: "whatsapp",
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
||||
});
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
to: redactedTo,
|
||||
messageId: sendResult.messageId,
|
||||
chars: finalText.length,
|
||||
},
|
||||
"heartbeat sent",
|
||||
);
|
||||
whatsappHeartbeatLog.info(`heartbeat alert sent to ${redactedTo}`);
|
||||
} catch (err) {
|
||||
const reason = formatError(err);
|
||||
heartbeatLogger.warn({ to: redactedTo, error: reason }, "heartbeat failed");
|
||||
whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
|
||||
emitHeartbeatEvent({
|
||||
status: "failed",
|
||||
to,
|
||||
reason,
|
||||
channel: "whatsapp",
|
||||
indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : undefined,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveHeartbeatRecipients(
|
||||
cfg: ReturnType<typeof getRuntimeConfig>,
|
||||
opts: { to?: string; all?: boolean; accountId?: string } = {},
|
||||
) {
|
||||
return resolveWhatsAppHeartbeatRecipients(cfg, opts);
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
|
||||
import { checkWhatsAppHeartbeatReady } from "./heartbeat.js";
|
||||
import {
|
||||
isWhatsAppGroupJid,
|
||||
@@ -183,7 +182,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
|
||||
...(accountId ? { accountId } : {}),
|
||||
});
|
||||
},
|
||||
resolveRecipients: ({ cfg, opts }) => resolveWhatsAppHeartbeatRecipients(cfg, opts),
|
||||
},
|
||||
status: createAsyncComputedAccountStatusAdapter<ResolvedWhatsAppAccount>({
|
||||
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
export { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
export { normalizeChannelId } from "openclaw/plugin-sdk/channel-targets";
|
||||
export { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
@@ -1,197 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
const loadSessionStoreMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./heartbeat-recipients.runtime.js", () => ({
|
||||
DEFAULT_ACCOUNT_ID: "default",
|
||||
loadSessionStore: loadSessionStoreMock,
|
||||
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
|
||||
normalizeChannelId: (value?: string | null) => {
|
||||
const trimmed = value?.trim().toLowerCase();
|
||||
return trimmed ? (trimmed as "whatsapp") : null;
|
||||
},
|
||||
normalizeE164: (value?: string | null) => {
|
||||
const digits = (value ?? "").replace(/[^\d+]/g, "");
|
||||
if (!digits) {
|
||||
return "";
|
||||
}
|
||||
return digits.startsWith("+") ? digits : `+${digits}`;
|
||||
},
|
||||
}));
|
||||
|
||||
function makeCfg(overrides?: Partial<OpenClawConfig>): OpenClawConfig {
|
||||
return {
|
||||
bindings: [],
|
||||
channels: {},
|
||||
...overrides,
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
function setSessionStore(store: Record<string, unknown>) {
|
||||
loadSessionStoreMock.mockReturnValue(store);
|
||||
}
|
||||
|
||||
function resolveWith(
|
||||
cfgOverrides: Partial<OpenClawConfig> = {},
|
||||
opts?: Parameters<typeof resolveWhatsAppHeartbeatRecipients>[1],
|
||||
) {
|
||||
return resolveWhatsAppHeartbeatRecipients(makeCfg(cfgOverrides), opts);
|
||||
}
|
||||
|
||||
function setSingleUnauthorizedSessionWithAllowFrom() {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000099", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
loadSessionStoreMock.mockReturnValue({});
|
||||
});
|
||||
|
||||
it("uses configured allowFrom recipients when session recipients are ambiguous", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
|
||||
});
|
||||
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001"] } as never },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" });
|
||||
});
|
||||
|
||||
it("falls back to allowFrom when no session recipient is authorized", () => {
|
||||
setSingleUnauthorizedSessionWithAllowFrom();
|
||||
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001"] } as never },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ recipients: ["+15550000001"], source: "allowFrom" });
|
||||
});
|
||||
|
||||
it("includes both session and allowFrom recipients when --all is set", () => {
|
||||
setSingleUnauthorizedSessionWithAllowFrom();
|
||||
|
||||
const result = resolveWith(
|
||||
{ channels: { whatsapp: { allowFrom: ["+15550000001"] } as never } },
|
||||
{ all: true },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000099", "+15550000001"],
|
||||
source: "all",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns explicit --to recipient and source flag", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000099", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
const result = resolveWith({}, { to: " +1 555 000 7777 " });
|
||||
expect(result).toEqual({ recipients: ["+15550007777"], source: "flag" });
|
||||
});
|
||||
|
||||
it("returns ambiguous session recipients when no allowFrom list exists", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
|
||||
});
|
||||
const result = resolveWith();
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000001", "+15550000002"],
|
||||
source: "session-ambiguous",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns single session recipient when allowFrom is empty", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
const result = resolveWith();
|
||||
expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" });
|
||||
});
|
||||
|
||||
it("returns all authorized session recipients when allowFrom matches multiple", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
|
||||
c: { lastChannel: "whatsapp", lastTo: "+15550000003", updatedAt: 0, sessionId: "c" },
|
||||
});
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001", "+15550000002"] } as never },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000001", "+15550000002"],
|
||||
source: "session-ambiguous",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores session store when session scope is global", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
const result = resolveWith({
|
||||
session: { scope: "global" } as OpenClawConfig["session"],
|
||||
channels: { whatsapp: { allowFrom: ["*", "+15550000009"] } as never },
|
||||
});
|
||||
expect(result).toEqual({ recipients: ["+15550000009"], source: "allowFrom" });
|
||||
});
|
||||
|
||||
it("uses the requested account allowFrom config without pairing-store recipients", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
|
||||
const result = resolveWith(
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["+15550000001"],
|
||||
accounts: {
|
||||
work: {
|
||||
allowFrom: ["+15550000003"],
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
{ accountId: "work" },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000003"],
|
||||
source: "allowFrom",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount allowFrom config when accountId is omitted", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
|
||||
const result = resolveWith({
|
||||
channels: {
|
||||
whatsapp: {
|
||||
defaultAccount: "work",
|
||||
allowFrom: ["+15550000001"],
|
||||
accounts: {
|
||||
work: {
|
||||
allowFrom: ["+15550000003"],
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000003"],
|
||||
source: "allowFrom",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { resolveDefaultWhatsAppAccountId, resolveWhatsAppAccount } from "./accounts.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
loadSessionStore,
|
||||
normalizeChannelId,
|
||||
normalizeE164,
|
||||
resolveStorePath,
|
||||
type OpenClawConfig,
|
||||
} from "./heartbeat-recipients.runtime.js";
|
||||
|
||||
type HeartbeatRecipientsResult = { recipients: string[]; source: string };
|
||||
type HeartbeatRecipientsOpts = { to?: string; all?: boolean; accountId?: string };
|
||||
|
||||
function getSessionRecipients(cfg: OpenClawConfig) {
|
||||
const sessionCfg = cfg.session;
|
||||
const scope = sessionCfg?.scope ?? "per-sender";
|
||||
if (scope === "global") {
|
||||
return [];
|
||||
}
|
||||
const storePath = resolveStorePath(cfg.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const isGroupKey = (key: string) =>
|
||||
key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
|
||||
const isCronKey = (key: string) => key.startsWith("cron:");
|
||||
|
||||
const recipients = Object.entries(store)
|
||||
.filter(([key]) => key !== "global" && key !== "unknown")
|
||||
.filter(([key]) => !isGroupKey(key) && !isCronKey(key))
|
||||
.map(([_, entry]) => ({
|
||||
to:
|
||||
normalizeChannelId(entry?.lastChannel) === "whatsapp" && entry?.lastTo
|
||||
? normalizeE164(entry.lastTo)
|
||||
: "",
|
||||
updatedAt: entry?.updatedAt ?? 0,
|
||||
}))
|
||||
.filter(({ to }) => to.length > 1)
|
||||
.toSorted((a, b) => b.updatedAt - a.updatedAt);
|
||||
|
||||
const seen = new Set<string>();
|
||||
return recipients.filter((recipient) => {
|
||||
if (seen.has(recipient.to)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(recipient.to);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveWhatsAppHeartbeatRecipients(
|
||||
cfg: OpenClawConfig,
|
||||
opts: HeartbeatRecipientsOpts = {},
|
||||
): HeartbeatRecipientsResult {
|
||||
if (opts.to) {
|
||||
return { recipients: [normalizeE164(opts.to)], source: "flag" };
|
||||
}
|
||||
|
||||
const sessionRecipients = getSessionRecipients(cfg);
|
||||
const resolvedAccountId =
|
||||
opts.accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg) || DEFAULT_ACCOUNT_ID;
|
||||
const configuredAllowFrom = (
|
||||
resolveWhatsAppAccount({ cfg, accountId: resolvedAccountId }).allowFrom ?? []
|
||||
)
|
||||
.filter((value) => value !== "*")
|
||||
.map(normalizeE164);
|
||||
|
||||
const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
|
||||
const allowFrom = unique(configuredAllowFrom);
|
||||
|
||||
if (opts.all) {
|
||||
return {
|
||||
recipients: unique([...sessionRecipients.map((entry) => entry.to), ...allowFrom]),
|
||||
source: "all",
|
||||
};
|
||||
}
|
||||
|
||||
if (allowFrom.length > 0) {
|
||||
const allowSet = new Set(allowFrom);
|
||||
const authorizedSessionRecipients = sessionRecipients
|
||||
.map((entry) => entry.to)
|
||||
.filter((recipient) => allowSet.has(recipient));
|
||||
if (authorizedSessionRecipients.length === 1) {
|
||||
return { recipients: [authorizedSessionRecipients[0]], source: "session-single" };
|
||||
}
|
||||
if (authorizedSessionRecipients.length > 1) {
|
||||
return { recipients: authorizedSessionRecipients, source: "session-ambiguous" };
|
||||
}
|
||||
return { recipients: allowFrom, source: "allowFrom" };
|
||||
}
|
||||
|
||||
if (sessionRecipients.length === 1) {
|
||||
return { recipients: [sessionRecipients[0].to], source: "session-single" };
|
||||
}
|
||||
if (sessionRecipients.length > 1) {
|
||||
return { recipients: sessionRecipients.map((entry) => entry.to), source: "session-ambiguous" };
|
||||
}
|
||||
|
||||
return { recipients: allowFrom, source: "allowFrom" };
|
||||
}
|
||||
@@ -27,7 +27,6 @@ export {
|
||||
resolveWhatsAppGroupIntroHint,
|
||||
resolveWhatsAppMentionStripRegexes,
|
||||
} from "./group-intro.js";
|
||||
export { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
|
||||
export { createWhatsAppOutboundBase } from "./outbound-base.js";
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
|
||||
@@ -387,13 +387,6 @@ export type ChannelHeartbeatAdapter = {
|
||||
threadId?: string | number | null;
|
||||
deps?: ChannelHeartbeatDeps;
|
||||
}) => Promise<void> | void;
|
||||
resolveRecipients?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
opts?: { to?: string; all?: boolean; accountId?: string };
|
||||
}) => {
|
||||
recipients: string[];
|
||||
source: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ChannelDirectorySelfParams = {
|
||||
|
||||
@@ -224,7 +224,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { handleWhatsAppAction, whatsAppActionRuntime } from "./src/action-runtime.js";',
|
||||
'export { createWhatsAppLoginTool } from "./src/agent-tools-login.js";',
|
||||
'export { formatWhatsAppWebAuthStatusState, getWebAuthAgeMs, hasWebCredsSync, logWebSelfId, logoutWeb, pickWebChannel, readCredsJsonRaw, readWebAuthExistsBestEffort, readWebAuthExistsForDecision, readWebAuthSnapshot, readWebAuthSnapshotBestEffort, readWebAuthState, readWebSelfId, readWebSelfIdentity, readWebSelfIdentityForDecision, resolveDefaultWebAuthDir, resolveWebCredsBackupPath, resolveWebCredsPath, restoreCredsFromBackupIfNeeded, WA_WEB_AUTH_DIR, webAuthExists, WHATSAPP_AUTH_UNSTABLE_CODE, WhatsAppAuthUnstableError, type WhatsAppWebAuthState } from "./src/auth-store.js";',
|
||||
'export { DEFAULT_WEB_MEDIA_BYTES, HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, monitorWebChannel, resolveHeartbeatRecipients, runWebHeartbeatOnce, SILENT_REPLY_TOKEN, stripHeartbeatToken, type WebChannelStatus, type WebMonitorTuning } from "./src/auto-reply.js";',
|
||||
'export { DEFAULT_WEB_MEDIA_BYTES, HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, monitorWebChannel, SILENT_REPLY_TOKEN, stripHeartbeatToken, type WebChannelStatus, type WebMonitorTuning } from "./src/auto-reply.js";',
|
||||
'export { extractContactContext, extractLocationData, extractMediaPlaceholder, extractText, monitorWebInbox, resetWebInboundDedupe, type WebInboundMessage, type WebListenerCloseReason } from "./src/inbound.js";',
|
||||
'export { loginWeb } from "./src/login.js";',
|
||||
'export { getDefaultLocalRoots, loadWebMedia, loadWebMediaRaw, LocalMediaAccessError, optimizeImageToJpeg, optimizeImageToPng, type LocalMediaAccessErrorCode, type WebMediaResult } from "./src/media.js";',
|
||||
|
||||
@@ -98,13 +98,11 @@ type WebChannelHeavyRuntimeModule = {
|
||||
) => Promise<AgentToolResult<unknown>>;
|
||||
monitorWebChannel: (...args: unknown[]) => Promise<unknown>;
|
||||
monitorWebInbox: (...args: unknown[]) => Promise<unknown>;
|
||||
runWebHeartbeatOnce: (...args: unknown[]) => Promise<unknown>;
|
||||
startWebLoginWithQr: (...args: unknown[]) => Promise<unknown>;
|
||||
waitForWaConnection: (sock: unknown) => Promise<void>;
|
||||
waitForWebLogin: (...args: unknown[]) => Promise<unknown>;
|
||||
extractMediaPlaceholder: (...args: unknown[]) => unknown;
|
||||
extractText: (...args: unknown[]) => unknown;
|
||||
resolveHeartbeatRecipients: (...args: unknown[]) => unknown;
|
||||
};
|
||||
|
||||
type WebChannelRuntimeModuleKind = "heavy" | "light";
|
||||
@@ -335,12 +333,6 @@ export async function optimizeImageToJpeg(
|
||||
return await optimizeImageToJpegImpl(...args);
|
||||
}
|
||||
|
||||
export async function runWebHeartbeatOnce(
|
||||
...args: Parameters<WebChannelHeavyRuntimeModule["runWebHeartbeatOnce"]>
|
||||
): ReturnType<WebChannelHeavyRuntimeModule["runWebHeartbeatOnce"]> {
|
||||
return (await getHeavyExport("runWebHeartbeatOnce"))(...args);
|
||||
}
|
||||
|
||||
export async function startWebLoginWithQr(
|
||||
...args: Parameters<WebChannelHeavyRuntimeModule["startWebLoginWithQr"]>
|
||||
): ReturnType<WebChannelHeavyRuntimeModule["startWebLoginWithQr"]> {
|
||||
@@ -371,9 +363,3 @@ export function getDefaultLocalRoots(
|
||||
): ReturnType<typeof getDefaultLocalRootsImpl> {
|
||||
return getDefaultLocalRootsImpl(...args);
|
||||
}
|
||||
|
||||
export function resolveHeartbeatRecipients(
|
||||
...args: Parameters<WebChannelHeavyRuntimeModule["resolveHeartbeatRecipients"]>
|
||||
): ReturnType<WebChannelHeavyRuntimeModule["resolveHeartbeatRecipients"]> {
|
||||
return loadCurrentHeavyModuleSync().resolveHeartbeatRecipients(...args);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user