diff --git a/extensions/zalo/src/actions.runtime.ts b/extensions/zalo/src/actions.runtime.ts new file mode 100644 index 00000000000..a9616ce64a5 --- /dev/null +++ b/extensions/zalo/src/actions.runtime.ts @@ -0,0 +1 @@ +export { sendMessageZalo } from "./send.js"; diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index 4f6108fa892..6f8572b01cd 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -5,7 +5,13 @@ import type { } from "openclaw/plugin-sdk/zalo"; import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/zalo"; import { listEnabledZaloAccounts } from "./accounts.js"; -import { sendMessageZalo } from "./send.js"; + +let zaloActionsRuntimePromise: Promise | null = null; + +async function loadZaloActionsRuntime() { + zaloActionsRuntimePromise ??= import("./actions.runtime.js"); + return zaloActionsRuntimePromise; +} const providerId = "zalo"; @@ -35,6 +41,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = { }); const mediaUrl = readStringParam(params, "media", { trim: false }); + const { sendMessageZalo } = await loadZaloActionsRuntime(); const result = await sendMessageZalo(to ?? "", content ?? "", { accountId: accountId ?? undefined, mediaUrl: mediaUrl ?? undefined, diff --git a/extensions/zalo/src/channel.runtime.ts b/extensions/zalo/src/channel.runtime.ts new file mode 100644 index 00000000000..fc4488b5be8 --- /dev/null +++ b/extensions/zalo/src/channel.runtime.ts @@ -0,0 +1,91 @@ +import { createAccountStatusSink } from "openclaw/plugin-sdk/compat"; +import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/zalo"; +import { probeZalo } from "./probe.js"; +import { resolveZaloProxyFetch } from "./proxy.js"; +import { normalizeSecretInputString } from "./secret-input.js"; +import { sendMessageZalo } from "./send.js"; + +export async function notifyZaloPairingApproval(params: { + cfg: import("openclaw/plugin-sdk/zalo").OpenClawConfig; + id: string; +}) { + const { resolveZaloAccount } = await import("./accounts.js"); + const account = resolveZaloAccount({ cfg: params.cfg }); + if (!account.token) { + throw new Error("Zalo token not configured"); + } + await sendMessageZalo(params.id, PAIRING_APPROVED_MESSAGE, { + token: account.token, + }); +} + +export async function sendZaloText( + params: Parameters[2] & { + to: string; + text: string; + }, +) { + return await sendMessageZalo(params.to, params.text, params); +} + +export async function probeZaloAccount(params: { + account: import("./accounts.js").ResolvedZaloAccount; + timeoutMs?: number; +}) { + return await probeZalo( + params.account.token, + params.timeoutMs, + resolveZaloProxyFetch(params.account.config.proxy), + ); +} + +export async function startZaloGatewayAccount( + ctx: Parameters< + NonNullable["startAccount"] + >[0], +) { + const account = ctx.account; + const token = account.token.trim(); + const mode = account.config.webhookUrl ? "webhook" : "polling"; + let zaloBotLabel = ""; + const fetcher = resolveZaloProxyFetch(account.config.proxy); + try { + const probe = await probeZalo(token, 2500, fetcher); + const name = probe.ok ? probe.bot?.name?.trim() : null; + if (name) { + zaloBotLabel = ` (${name})`; + } + if (!probe.ok) { + ctx.log?.warn?.( + `[${account.accountId}] Zalo probe failed before provider start (${String(probe.elapsedMs)}ms): ${probe.error}`, + ); + } + ctx.setStatus({ + accountId: account.accountId, + bot: probe.bot, + }); + } catch (err) { + ctx.log?.warn?.( + `[${account.accountId}] Zalo probe threw before provider start: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`, + ); + } + const statusSink = createAccountStatusSink({ + accountId: ctx.accountId, + setStatus: ctx.setStatus, + }); + ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel} mode=${mode}`); + const { monitorZaloProvider } = await import("./monitor.js"); + return monitorZaloProvider({ + token, + account, + config: ctx.cfg, + runtime: ctx.runtime, + abortSignal: ctx.abortSignal, + useWebhook: Boolean(account.config.webhookUrl), + webhookUrl: account.config.webhookUrl, + webhookSecret: normalizeSecretInputString(account.config.webhookSecret), + webhookPath: account.config.webhookPath, + fetcher, + statusSink, + }); +} diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 32ceeeff110..ed735bbd1c7 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -3,7 +3,6 @@ import { buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, collectOpenProviderGroupPolicyWarnings, - createAccountStatusSink, mapAllowFromEntries, } from "openclaw/plugin-sdk/compat"; import type { @@ -22,8 +21,6 @@ import { formatAllowFromLowercase, listDirectoryUserEntriesFromAllowFrom, isNumericTargetId, - PAIRING_APPROVED_MESSAGE, - resolveOutboundMediaUrls, sendPayloadWithChunkedTextAndMedia, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk/zalo"; @@ -35,10 +32,6 @@ import { } from "./accounts.js"; import { zaloMessageActions } from "./actions.js"; import { ZaloConfigSchema } from "./config-schema.js"; -import { probeZalo } from "./probe.js"; -import { resolveZaloProxyFetch } from "./proxy.js"; -import { normalizeSecretInputString } from "./secret-input.js"; -import { sendMessageZalo } from "./send.js"; import { zaloSetupAdapter } from "./setup-core.js"; import { zaloSetupWizard } from "./setup-surface.js"; import { collectZaloStatusIssues } from "./status-issues.js"; @@ -63,6 +56,13 @@ function normalizeZaloMessagingTarget(raw: string): string | undefined { return trimmed.replace(/^(zalo|zl):/i, ""); } +let zaloChannelRuntimePromise: Promise | null = null; + +async function loadZaloChannelRuntime() { + zaloChannelRuntimePromise ??= import("./channel.runtime.js"); + return zaloChannelRuntimePromise; +} + export const zaloPlugin: ChannelPlugin = { id: "zalo", meta, @@ -190,13 +190,8 @@ export const zaloPlugin: ChannelPlugin = { pairing: { idLabel: "zaloUserId", normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""), - notifyApproval: async ({ cfg, id }) => { - const account = resolveZaloAccount({ cfg: cfg }); - if (!account.token) { - throw new Error("Zalo token not configured"); - } - await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token }); - }, + notifyApproval: async (params) => + await (await loadZaloChannelRuntime()).notifyZaloPairingApproval(params), }, outbound: { deliveryMode: "direct", @@ -213,14 +208,22 @@ export const zaloPlugin: ChannelPlugin = { emptyResult: { channel: "zalo", messageId: "" }, }), sendText: async ({ to, text, accountId, cfg }) => { - const result = await sendMessageZalo(to, text, { + const result = await ( + await loadZaloChannelRuntime() + ).sendZaloText({ + to, + text, accountId: accountId ?? undefined, cfg: cfg, }); return buildChannelSendResult("zalo", result); }, sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => { - const result = await sendMessageZalo(to, text, { + const result = await ( + await loadZaloChannelRuntime() + ).sendZaloText({ + to, + text, accountId: accountId ?? undefined, mediaUrl, cfg: cfg, @@ -239,7 +242,7 @@ export const zaloPlugin: ChannelPlugin = { collectStatusIssues: collectZaloStatusIssues, buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot), probeAccount: async ({ account, timeoutMs }) => - probeZalo(account.token, timeoutMs, resolveZaloProxyFetch(account.config.proxy)), + await (await loadZaloChannelRuntime()).probeZaloAccount({ account, timeoutMs }), buildAccountSnapshot: ({ account, runtime }) => { const configured = Boolean(account.token?.trim()); const base = buildBaseAccountStatusSnapshot({ @@ -260,51 +263,7 @@ export const zaloPlugin: ChannelPlugin = { }, }, gateway: { - startAccount: async (ctx) => { - const account = ctx.account; - const token = account.token.trim(); - const mode = account.config.webhookUrl ? "webhook" : "polling"; - let zaloBotLabel = ""; - const fetcher = resolveZaloProxyFetch(account.config.proxy); - try { - const probe = await probeZalo(token, 2500, fetcher); - const name = probe.ok ? probe.bot?.name?.trim() : null; - if (name) { - zaloBotLabel = ` (${name})`; - } - if (!probe.ok) { - ctx.log?.warn?.( - `[${account.accountId}] Zalo probe failed before provider start (${String(probe.elapsedMs)}ms): ${probe.error}`, - ); - } - ctx.setStatus({ - accountId: account.accountId, - bot: probe.bot, - }); - } catch (err) { - ctx.log?.warn?.( - `[${account.accountId}] Zalo probe threw before provider start: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`, - ); - } - const statusSink = createAccountStatusSink({ - accountId: ctx.accountId, - setStatus: ctx.setStatus, - }); - ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel} mode=${mode}`); - const { monitorZaloProvider } = await import("./monitor.js"); - return monitorZaloProvider({ - token, - account, - config: ctx.cfg, - runtime: ctx.runtime, - abortSignal: ctx.abortSignal, - useWebhook: Boolean(account.config.webhookUrl), - webhookUrl: account.config.webhookUrl, - webhookSecret: normalizeSecretInputString(account.config.webhookSecret), - webhookPath: account.config.webhookPath, - fetcher, - statusSink, - }); - }, + startAccount: async (ctx) => + await (await loadZaloChannelRuntime()).startZaloGatewayAccount(ctx), }, };