refactor: route channel runtime via plugin api

This commit is contained in:
Peter Steinberger
2026-01-18 11:00:19 +00:00
parent 676d41d415
commit ee6e534ccb
82 changed files with 1253 additions and 3167 deletions

View File

@@ -1,23 +1,16 @@
import {
applyAccountNameToChannelSection,
buildChannelConfigSchema,
chunkText,
collectWhatsAppStatusIssues,
createActionGate,
createWhatsAppLoginTool,
DEFAULT_ACCOUNT_ID,
formatPairingApproveHint,
getActiveWebListener,
getChatChannelMeta,
getWebAuthAgeMs,
handleWhatsAppAction,
isWhatsAppGroupJid,
listWhatsAppAccountIds,
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
logWebSelfId,
looksLikeWhatsAppTargetId,
logoutWeb,
migrateBaseNameToDefaultAccount,
missingTargetError,
normalizeAccountId,
@@ -25,22 +18,19 @@ import {
normalizeWhatsAppMessagingTarget,
normalizeWhatsAppTarget,
readStringParam,
readWebSelfId,
resolveDefaultWhatsAppAccountId,
resolveWhatsAppAccount,
resolveWhatsAppGroupRequireMention,
resolveWhatsAppHeartbeatRecipients,
sendMessageWhatsApp,
sendPollWhatsApp,
shouldLogVerbose,
whatsappOnboardingAdapter,
WhatsAppConfigSchema,
type ChannelMessageActionName,
type ChannelPlugin,
type ResolvedWhatsAppAccount,
webAuthExists,
} from "clawdbot/plugin-sdk";
import { getWhatsAppRuntime } from "./runtime.js";
const meta = getChatChannelMeta("whatsapp");
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -55,7 +45,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
preferSessionLookupForAnnounceTarget: true,
},
onboarding: whatsappOnboardingAdapter,
agentTools: () => [createWhatsAppLoginTool()],
agentTools: () => [getWhatsAppRuntime().channel.whatsapp.createLoginTool()],
pairing: {
idLabel: "whatsappSenderId",
},
@@ -110,7 +100,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
isEnabled: (account, cfg) => account.enabled !== false && cfg.web?.enabled !== false,
disabledReason: () => "disabled",
isConfigured: async (account) => await webAuthExists(account.authDir),
isConfigured: async (account) =>
await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir),
unconfiguredReason: () => "not linked",
describeAccount: (account) => ({
accountId: account.accountId,
@@ -232,7 +223,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
directory: {
self: async ({ cfg, accountId }) => {
const account = resolveWhatsAppAccount({ cfg, accountId });
const { e164, jid } = readWebSelfId(account.authDir);
const { e164, jid } = getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir);
const id = e164 ?? jid;
if (!id) return null;
return {
@@ -264,7 +255,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
});
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
return await handleWhatsAppAction(
return await getWhatsAppRuntime().channel.whatsapp.handleWhatsAppAction(
{
action: "react",
chatJid:
@@ -282,7 +273,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
outbound: {
deliveryMode: "gateway",
chunker: chunkText,
chunker: (text, limit) => getWhatsAppRuntime().channel.text.chunkText(text, limit),
textChunkLimit: 4000,
pollMaxOptions: 12,
resolveTarget: ({ to, allowFrom, mode }) => {
@@ -335,7 +326,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
};
},
sendText: async ({ to, text, accountId, deps, gifPlayback }) => {
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
const send =
deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp;
const result = await send(to, text, {
verbose: false,
accountId: accountId ?? undefined,
@@ -344,7 +336,8 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
return { channel: "whatsapp", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, deps, gifPlayback }) => {
const send = deps?.sendWhatsApp ?? sendMessageWhatsApp;
const send =
deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp;
const result = await send(to, text, {
verbose: false,
mediaUrl,
@@ -354,16 +347,20 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
return { channel: "whatsapp", ...result };
},
sendPoll: async ({ to, poll, accountId }) =>
await sendPollWhatsApp(to, poll, {
verbose: shouldLogVerbose(),
await getWhatsAppRuntime().channel.whatsapp.sendPollWhatsApp(to, poll, {
verbose: getWhatsAppRuntime().logging.shouldLogVerbose(),
accountId: accountId ?? undefined,
}),
},
auth: {
login: async ({ cfg, accountId, runtime, verbose }) => {
const resolvedAccountId = accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg);
const { loginWeb } = await import("clawdbot/plugin-sdk");
await loginWeb(Boolean(verbose), undefined, runtime, resolvedAccountId);
await getWhatsAppRuntime().channel.whatsapp.loginWeb(
Boolean(verbose),
undefined,
runtime,
resolvedAccountId,
);
},
},
heartbeat: {
@@ -372,13 +369,14 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
return { ok: false, reason: "whatsapp-disabled" };
}
const account = resolveWhatsAppAccount({ cfg, accountId });
const authExists = await (deps?.webAuthExists ?? webAuthExists)(account.authDir);
const authExists = await (deps?.webAuthExists ??
getWhatsAppRuntime().channel.whatsapp.webAuthExists)(account.authDir);
if (!authExists) {
return { ok: false, reason: "whatsapp-not-linked" };
}
const listenerActive = deps?.hasActiveWebListener
? deps.hasActiveWebListener()
: Boolean(getActiveWebListener());
: Boolean(getWhatsAppRuntime().channel.whatsapp.getActiveWebListener());
if (!listenerActive) {
return { ok: false, reason: "whatsapp-not-running" };
}
@@ -405,10 +403,16 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
typeof snapshot.linked === "boolean"
? snapshot.linked
: authDir
? await webAuthExists(authDir)
? await getWhatsAppRuntime().channel.whatsapp.webAuthExists(authDir)
: false;
const authAgeMs = linked && authDir ? getWebAuthAgeMs(authDir) : null;
const self = linked && authDir ? readWebSelfId(authDir) : { e164: null, jid: null };
const authAgeMs =
linked && authDir
? getWhatsAppRuntime().channel.whatsapp.getWebAuthAgeMs(authDir)
: null;
const self =
linked && authDir
? getWhatsAppRuntime().channel.whatsapp.readWebSelfId(authDir)
: { e164: null, jid: null };
return {
configured: linked,
linked,
@@ -425,7 +429,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
};
},
buildAccountSnapshot: async ({ account, runtime }) => {
const linked = await webAuthExists(account.authDir);
const linked = await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir);
return {
accountId: account.accountId,
name: account.name,
@@ -446,19 +450,21 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
resolveAccountState: ({ configured }) => (configured ? "linked" : "not linked"),
logSelfId: ({ account, runtime, includeChannelPrefix }) => {
logWebSelfId(account.authDir, runtime, includeChannelPrefix);
getWhatsAppRuntime().channel.whatsapp.logWebSelfId(
account.authDir,
runtime,
includeChannelPrefix,
);
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
const { e164, jid } = readWebSelfId(account.authDir);
const { e164, jid } = getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir);
const identity = e164 ? e164 : jid ? `jid ${jid}` : "unknown";
ctx.log?.info(`[${account.accountId}] starting provider (${identity})`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorWebChannel } = await import("clawdbot/plugin-sdk");
return monitorWebChannel(
shouldLogVerbose(),
return getWhatsAppRuntime().channel.whatsapp.monitorWebChannel(
getWhatsAppRuntime().logging.shouldLogVerbose(),
undefined,
true,
undefined,
@@ -471,22 +477,16 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
);
},
loginWithQrStart: async ({ accountId, force, timeoutMs, verbose }) =>
await (async () => {
const { startWebLoginWithQr } = await import("clawdbot/plugin-sdk");
return await startWebLoginWithQr({
accountId,
force,
timeoutMs,
verbose,
});
})(),
await getWhatsAppRuntime().channel.whatsapp.startWebLoginWithQr({
accountId,
force,
timeoutMs,
verbose,
}),
loginWithQrWait: async ({ accountId, timeoutMs }) =>
await (async () => {
const { waitForWebLogin } = await import("clawdbot/plugin-sdk");
return await waitForWebLogin({ accountId, timeoutMs });
})(),
await getWhatsAppRuntime().channel.whatsapp.waitForWebLogin({ accountId, timeoutMs }),
logoutAccount: async ({ account, runtime }) => {
const cleared = await logoutWeb({
const cleared = await getWhatsAppRuntime().channel.whatsapp.logoutWeb({
authDir: account.authDir,
isLegacyAuthDir: account.isLegacyAuthDir,
runtime,

View File

@@ -0,0 +1,14 @@
import type { PluginRuntime } from "clawdbot/plugin-sdk";
let runtime: PluginRuntime | null = null;
export function setWhatsAppRuntime(next: PluginRuntime) {
runtime = next;
}
export function getWhatsAppRuntime(): PluginRuntime {
if (!runtime) {
throw new Error("WhatsApp runtime not initialized");
}
return runtime;
}