Zalo: lazy-load channel runtime paths

This commit is contained in:
Vincent Koc
2026-03-16 20:56:42 -07:00
parent c64f6adc83
commit e064c1198e
4 changed files with 122 additions and 64 deletions

View File

@@ -0,0 +1 @@
export { sendMessageZalo } from "./send.js";

View File

@@ -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<typeof import("./actions.runtime.js")> | 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,

View File

@@ -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<typeof sendMessageZalo>[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<import("openclaw/plugin-sdk/zalo").ChannelPlugin["gateway"]>["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,
});
}

View File

@@ -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<typeof import("./channel.runtime.js")> | null = null;
async function loadZaloChannelRuntime() {
zaloChannelRuntimePromise ??= import("./channel.runtime.js");
return zaloChannelRuntimePromise;
}
export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
id: "zalo",
meta,
@@ -190,13 +190,8 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
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<ResolvedZaloAccount> = {
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<ResolvedZaloAccount> = {
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<ResolvedZaloAccount> = {
},
},
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),
},
};