mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:32:44 +00:00
feat: migrate zalo plugin to sdk
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Docs: document plugin slots and memory plugin behavior.
|
- Docs: document plugin slots and memory plugin behavior.
|
||||||
- Plugins: add the bundled BlueBubbles channel plugin (disabled by default).
|
- Plugins: add the bundled BlueBubbles channel plugin (disabled by default).
|
||||||
- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader.
|
- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader.
|
||||||
|
- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime.
|
||||||
|
|
||||||
## 2026.1.17-5
|
## 2026.1.17-5
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|||||||
|
|
||||||
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
||||||
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
||||||
|
import { setZaloRuntime } from "./src/runtime.js";
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
id: "zalo",
|
id: "zalo",
|
||||||
name: "Zalo",
|
name: "Zalo",
|
||||||
description: "Zalo channel plugin (Bot API)",
|
description: "Zalo channel plugin (Bot API)",
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
|
setZaloRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
||||||
api.registerHttpHandler(handleZaloWebhookRequest);
|
api.registerHttpHandler(handleZaloWebhookRequest);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import type {
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
CoreConfig,
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
|
||||||
ResolvedZaloAccount,
|
|
||||||
ZaloAccountConfig,
|
|
||||||
ZaloConfig,
|
|
||||||
} from "./types.js";
|
|
||||||
import { resolveZaloToken } from "./token.js";
|
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js";
|
|
||||||
|
|
||||||
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
||||||
|
import { resolveZaloToken } from "./token.js";
|
||||||
|
|
||||||
|
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
||||||
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
||||||
if (!accounts || typeof accounts !== "object") return [];
|
if (!accounts || typeof accounts !== "object") return [];
|
||||||
return Object.keys(accounts).filter(Boolean);
|
return Object.keys(accounts).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listZaloAccountIds(cfg: CoreConfig): string[] {
|
export function listZaloAccountIds(cfg: ClawdbotConfig): string[] {
|
||||||
const ids = listConfiguredAccountIds(cfg);
|
const ids = listConfiguredAccountIds(cfg);
|
||||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||||
return ids.sort((a, b) => a.localeCompare(b));
|
return ids.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDefaultZaloAccountId(cfg: CoreConfig): string {
|
export function resolveDefaultZaloAccountId(cfg: ClawdbotConfig): string {
|
||||||
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
||||||
if (zaloConfig?.defaultAccount?.trim()) return zaloConfig.defaultAccount.trim();
|
if (zaloConfig?.defaultAccount?.trim()) return zaloConfig.defaultAccount.trim();
|
||||||
const ids = listZaloAccountIds(cfg);
|
const ids = listZaloAccountIds(cfg);
|
||||||
@@ -28,7 +25,7 @@ export function resolveDefaultZaloAccountId(cfg: CoreConfig): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAccountConfig(
|
function resolveAccountConfig(
|
||||||
cfg: CoreConfig,
|
cfg: ClawdbotConfig,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
): ZaloAccountConfig | undefined {
|
): ZaloAccountConfig | undefined {
|
||||||
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
||||||
@@ -36,7 +33,7 @@ function resolveAccountConfig(
|
|||||||
return accounts[accountId] as ZaloAccountConfig | undefined;
|
return accounts[accountId] as ZaloAccountConfig | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeZaloAccountConfig(cfg: CoreConfig, accountId: string): ZaloAccountConfig {
|
function mergeZaloAccountConfig(cfg: ClawdbotConfig, accountId: string): ZaloAccountConfig {
|
||||||
const raw = (cfg.channels?.zalo ?? {}) as ZaloConfig;
|
const raw = (cfg.channels?.zalo ?? {}) as ZaloConfig;
|
||||||
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
|
||||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||||
@@ -44,7 +41,7 @@ function mergeZaloAccountConfig(cfg: CoreConfig, accountId: string): ZaloAccount
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveZaloAccount(params: {
|
export function resolveZaloAccount(params: {
|
||||||
cfg: CoreConfig;
|
cfg: ClawdbotConfig;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
}): ResolvedZaloAccount {
|
}): ResolvedZaloAccount {
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
@@ -67,7 +64,7 @@ export function resolveZaloAccount(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listEnabledZaloAccounts(cfg: CoreConfig): ResolvedZaloAccount[] {
|
export function listEnabledZaloAccounts(cfg: ClawdbotConfig): ResolvedZaloAccount[] {
|
||||||
return listZaloAccountIds(cfg)
|
return listZaloAccountIds(cfg)
|
||||||
.map((accountId) => resolveZaloAccount({ cfg, accountId }))
|
.map((accountId) => resolveZaloAccount({ cfg, accountId }))
|
||||||
.filter((account) => account.enabled);
|
.filter((account) => account.enabled);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type {
|
import type {
|
||||||
ChannelMessageActionAdapter,
|
ChannelMessageActionAdapter,
|
||||||
ChannelMessageActionName,
|
ChannelMessageActionName,
|
||||||
|
ClawdbotConfig,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
import { jsonResult, readStringParam } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import type { CoreConfig } from "./types.js";
|
|
||||||
import { listEnabledZaloAccounts } from "./accounts.js";
|
import { listEnabledZaloAccounts } from "./accounts.js";
|
||||||
import { sendMessageZalo } from "./send.js";
|
import { sendMessageZalo } from "./send.js";
|
||||||
import { jsonResult, readStringParam } from "./tool-helpers.js";
|
|
||||||
|
|
||||||
const providerId = "zalo";
|
const providerId = "zalo";
|
||||||
|
|
||||||
function listEnabledAccounts(cfg: CoreConfig) {
|
function listEnabledAccounts(cfg: ClawdbotConfig) {
|
||||||
return listEnabledZaloAccounts(cfg).filter(
|
return listEnabledZaloAccounts(cfg).filter(
|
||||||
(account) => account.enabled && account.tokenSource !== "none",
|
(account) => account.enabled && account.tokenSource !== "none",
|
||||||
);
|
);
|
||||||
@@ -18,7 +18,7 @@ function listEnabledAccounts(cfg: CoreConfig) {
|
|||||||
|
|
||||||
export const zaloMessageActions: ChannelMessageActionAdapter = {
|
export const zaloMessageActions: ChannelMessageActionAdapter = {
|
||||||
listActions: ({ cfg }) => {
|
listActions: ({ cfg }) => {
|
||||||
const accounts = listEnabledAccounts(cfg as CoreConfig);
|
const accounts = listEnabledAccounts(cfg as ClawdbotConfig);
|
||||||
if (accounts.length === 0) return [];
|
if (accounts.length === 0) return [];
|
||||||
const actions = new Set<ChannelMessageActionName>(["send"]);
|
const actions = new Set<ChannelMessageActionName>(["send"]);
|
||||||
return Array.from(actions);
|
return Array.from(actions);
|
||||||
@@ -44,7 +44,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = {
|
|||||||
const result = await sendMessageZalo(to ?? "", content ?? "", {
|
const result = await sendMessageZalo(to ?? "", content ?? "", {
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl: mediaUrl ?? undefined,
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { zaloPlugin } from "./channel.js";
|
import { zaloPlugin } from "./channel.js";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe("zalo directory", () => {
|
|||||||
allowFrom: ["zalo:123", "zl:234", "345"],
|
allowFrom: ["zalo:123", "zl:234", "345"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as CoreConfig;
|
} as unknown as ClawdbotConfig;
|
||||||
|
|
||||||
expect(zaloPlugin.directory).toBeTruthy();
|
expect(zaloPlugin.directory).toBeTruthy();
|
||||||
expect(zaloPlugin.directory?.listPeers).toBeTruthy();
|
expect(zaloPlugin.directory?.listPeers).toBeTruthy();
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import type { ChannelAccountSnapshot, ChannelDock, ChannelPlugin } from "clawdbot/plugin-sdk";
|
import type {
|
||||||
import { buildChannelConfigSchema } from "clawdbot/plugin-sdk";
|
ChannelAccountSnapshot,
|
||||||
|
ChannelDock,
|
||||||
|
ChannelPlugin,
|
||||||
|
ClawdbotConfig,
|
||||||
|
} from "clawdbot/plugin-sdk";
|
||||||
|
import {
|
||||||
|
applyAccountNameToChannelSection,
|
||||||
|
buildChannelConfigSchema,
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
deleteAccountFromConfigSection,
|
||||||
|
formatPairingApproveHint,
|
||||||
|
migrateBaseNameToDefaultAccount,
|
||||||
|
normalizeAccountId,
|
||||||
|
PAIRING_APPROVED_MESSAGE,
|
||||||
|
setAccountEnabledInConfigSection,
|
||||||
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount, type ResolvedZaloAccount } from "./accounts.js";
|
import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount, type ResolvedZaloAccount } from "./accounts.js";
|
||||||
import { zaloMessageActions } from "./actions.js";
|
import { zaloMessageActions } from "./actions.js";
|
||||||
import { ZaloConfigSchema } from "./config-schema.js";
|
import { ZaloConfigSchema } from "./config-schema.js";
|
||||||
import {
|
|
||||||
deleteAccountFromConfigSection,
|
|
||||||
setAccountEnabledInConfigSection,
|
|
||||||
} from "./shared/channel-config.js";
|
|
||||||
import { zaloOnboardingAdapter } from "./onboarding.js";
|
import { zaloOnboardingAdapter } from "./onboarding.js";
|
||||||
import { formatPairingApproveHint, PAIRING_APPROVED_MESSAGE } from "./shared/pairing.js";
|
|
||||||
import { resolveZaloProxyFetch } from "./proxy.js";
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
||||||
import { probeZalo } from "./probe.js";
|
import { probeZalo } from "./probe.js";
|
||||||
import { sendMessageZalo } from "./send.js";
|
import { sendMessageZalo } from "./send.js";
|
||||||
import {
|
|
||||||
applyAccountNameToChannelSection,
|
|
||||||
migrateBaseNameToDefaultAccount,
|
|
||||||
} from "./shared/channel-setup.js";
|
|
||||||
import { collectZaloStatusIssues } from "./status-issues.js";
|
import { collectZaloStatusIssues } from "./status-issues.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js";
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
id: "zalo",
|
id: "zalo",
|
||||||
@@ -33,7 +37,6 @@ const meta = {
|
|||||||
quickstartAllowFrom: true,
|
quickstartAllowFrom: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function normalizeZaloMessagingTarget(raw: string): string | undefined {
|
function normalizeZaloMessagingTarget(raw: string): string | undefined {
|
||||||
const trimmed = raw?.trim();
|
const trimmed = raw?.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
@@ -50,7 +53,7 @@ export const zaloDock: ChannelDock = {
|
|||||||
outbound: { textChunkLimit: 2000 },
|
outbound: { textChunkLimit: 2000 },
|
||||||
config: {
|
config: {
|
||||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||||
(resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
|
(resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
|
||||||
(entry) => String(entry),
|
(entry) => String(entry),
|
||||||
),
|
),
|
||||||
formatAllowFrom: ({ allowFrom }) =>
|
formatAllowFrom: ({ allowFrom }) =>
|
||||||
@@ -84,12 +87,12 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
reload: { configPrefixes: ["channels.zalo"] },
|
reload: { configPrefixes: ["channels.zalo"] },
|
||||||
configSchema: buildChannelConfigSchema(ZaloConfigSchema),
|
configSchema: buildChannelConfigSchema(ZaloConfigSchema),
|
||||||
config: {
|
config: {
|
||||||
listAccountIds: (cfg) => listZaloAccountIds(cfg as CoreConfig),
|
listAccountIds: (cfg) => listZaloAccountIds(cfg as ClawdbotConfig),
|
||||||
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }),
|
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }),
|
||||||
defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg as CoreConfig),
|
defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg as ClawdbotConfig),
|
||||||
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
||||||
setAccountEnabledInConfigSection({
|
setAccountEnabledInConfigSection({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
sectionKey: "zalo",
|
sectionKey: "zalo",
|
||||||
accountId,
|
accountId,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -97,7 +100,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
}),
|
}),
|
||||||
deleteAccount: ({ cfg, accountId }) =>
|
deleteAccount: ({ cfg, accountId }) =>
|
||||||
deleteAccountFromConfigSection({
|
deleteAccountFromConfigSection({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
sectionKey: "zalo",
|
sectionKey: "zalo",
|
||||||
accountId,
|
accountId,
|
||||||
clearBaseFields: ["botToken", "tokenFile", "name"],
|
clearBaseFields: ["botToken", "tokenFile", "name"],
|
||||||
@@ -111,7 +114,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
tokenSource: account.tokenSource,
|
tokenSource: account.tokenSource,
|
||||||
}),
|
}),
|
||||||
resolveAllowFrom: ({ cfg, accountId }) =>
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||||
(resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
|
(resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
|
||||||
(entry) => String(entry),
|
(entry) => String(entry),
|
||||||
),
|
),
|
||||||
formatAllowFrom: ({ allowFrom }) =>
|
formatAllowFrom: ({ allowFrom }) =>
|
||||||
@@ -125,7 +128,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||||
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||||
const useAccountPath = Boolean(
|
const useAccountPath = Boolean(
|
||||||
(cfg as CoreConfig).channels?.zalo?.accounts?.[resolvedAccountId],
|
(cfg as ClawdbotConfig).channels?.zalo?.accounts?.[resolvedAccountId],
|
||||||
);
|
);
|
||||||
const basePath = useAccountPath
|
const basePath = useAccountPath
|
||||||
? `channels.zalo.accounts.${resolvedAccountId}.`
|
? `channels.zalo.accounts.${resolvedAccountId}.`
|
||||||
@@ -161,7 +164,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
directory: {
|
directory: {
|
||||||
self: async () => null,
|
self: async () => null,
|
||||||
listPeers: async ({ cfg, accountId, query, limit }) => {
|
listPeers: async ({ cfg, accountId, query, limit }) => {
|
||||||
const account = resolveZaloAccount({ cfg: cfg as CoreConfig, accountId });
|
const account = resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId });
|
||||||
const q = query?.trim().toLowerCase() || "";
|
const q = query?.trim().toLowerCase() || "";
|
||||||
const peers = Array.from(
|
const peers = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -182,7 +185,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||||
applyAccountName: ({ cfg, accountId, name }) =>
|
applyAccountName: ({ cfg, accountId, name }) =>
|
||||||
applyAccountNameToChannelSection({
|
applyAccountNameToChannelSection({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
channelKey: "zalo",
|
channelKey: "zalo",
|
||||||
accountId,
|
accountId,
|
||||||
name,
|
name,
|
||||||
@@ -198,7 +201,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
},
|
},
|
||||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||||
const namedConfig = applyAccountNameToChannelSection({
|
const namedConfig = applyAccountNameToChannelSection({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
channelKey: "zalo",
|
channelKey: "zalo",
|
||||||
accountId,
|
accountId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -227,7 +230,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...next,
|
...next,
|
||||||
@@ -250,14 +253,14 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pairing: {
|
pairing: {
|
||||||
idLabel: "zaloUserId",
|
idLabel: "zaloUserId",
|
||||||
normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""),
|
normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""),
|
||||||
notifyApproval: async ({ cfg, id }) => {
|
notifyApproval: async ({ cfg, id }) => {
|
||||||
const account = resolveZaloAccount({ cfg: cfg as CoreConfig });
|
const account = resolveZaloAccount({ cfg: cfg as ClawdbotConfig });
|
||||||
if (!account.token) throw new Error("Zalo token not configured");
|
if (!account.token) throw new Error("Zalo token not configured");
|
||||||
await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token });
|
await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token });
|
||||||
},
|
},
|
||||||
@@ -289,7 +292,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
sendText: async ({ to, text, accountId, cfg }) => {
|
sendText: async ({ to, text, accountId, cfg }) => {
|
||||||
const result = await sendMessageZalo(to, text, {
|
const result = await sendMessageZalo(to, text, {
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
@@ -302,7 +305,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
const result = await sendMessageZalo(to, text, {
|
const result = await sendMessageZalo(to, text, {
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
@@ -375,7 +378,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|||||||
return monitorZaloProvider({
|
return monitorZaloProvider({
|
||||||
token,
|
token,
|
||||||
account,
|
account,
|
||||||
config: ctx.cfg as CoreConfig,
|
config: ctx.cfg as ClawdbotConfig,
|
||||||
runtime: ctx.runtime,
|
runtime: ctx.runtime,
|
||||||
abortSignal: ctx.abortSignal,
|
abortSignal: ctx.abortSignal,
|
||||||
useWebhook: Boolean(account.config.webhookUrl),
|
useWebhook: Boolean(account.config.webhookUrl),
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
||||||
|
|
||||||
export type CoreChannelDeps = {
|
|
||||||
chunkMarkdownText: (text: string, limit: number) => string[];
|
|
||||||
formatAgentEnvelope: (params: {
|
|
||||||
channel: string;
|
|
||||||
from: string;
|
|
||||||
timestamp?: number;
|
|
||||||
body: string;
|
|
||||||
}) => string;
|
|
||||||
dispatchReplyWithBufferedBlockDispatcher: (params: {
|
|
||||||
ctx: unknown;
|
|
||||||
cfg: unknown;
|
|
||||||
dispatcherOptions: {
|
|
||||||
deliver: (payload: unknown) => Promise<void>;
|
|
||||||
onError?: (err: unknown, info: { kind: string }) => void;
|
|
||||||
};
|
|
||||||
}) => Promise<void>;
|
|
||||||
resolveAgentRoute: (params: {
|
|
||||||
cfg: unknown;
|
|
||||||
channel: string;
|
|
||||||
accountId: string;
|
|
||||||
peer: { kind: "dm" | "group" | "channel"; id: string };
|
|
||||||
}) => { sessionKey: string; accountId: string };
|
|
||||||
buildPairingReply: (params: { channel: string; idLine: string; code: string }) => string;
|
|
||||||
readChannelAllowFromStore: (channel: string) => Promise<string[]>;
|
|
||||||
upsertChannelPairingRequest: (params: {
|
|
||||||
channel: string;
|
|
||||||
id: string;
|
|
||||||
meta?: { name?: string };
|
|
||||||
pairingAdapter?: {
|
|
||||||
idLabel: string;
|
|
||||||
normalizeAllowEntry?: (entry: string) => string;
|
|
||||||
notifyApproval?: (params: { cfg: unknown; id: string; runtime?: unknown }) => Promise<void>;
|
|
||||||
};
|
|
||||||
}) => Promise<{ code: string; created: boolean }>;
|
|
||||||
fetchRemoteMedia: (params: { url: string }) => Promise<{ buffer: Buffer; contentType?: string }>;
|
|
||||||
saveMediaBuffer: (
|
|
||||||
buffer: Buffer,
|
|
||||||
contentType: string | undefined,
|
|
||||||
type: "inbound" | "outbound",
|
|
||||||
maxBytes: number,
|
|
||||||
) => Promise<{ path: string; contentType: string }>;
|
|
||||||
shouldLogVerbose: () => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
let coreRootCache: string | null = null;
|
|
||||||
let coreDepsPromise: Promise<CoreChannelDeps> | null = null;
|
|
||||||
|
|
||||||
function findPackageRoot(startDir: string, name: string): string | null {
|
|
||||||
let dir = startDir;
|
|
||||||
for (;;) {
|
|
||||||
const pkgPath = path.join(dir, "package.json");
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(pkgPath)) {
|
|
||||||
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
||||||
const pkg = JSON.parse(raw) as { name?: string };
|
|
||||||
if (pkg.name === name) return dir;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore parse errors
|
|
||||||
}
|
|
||||||
const parent = path.dirname(dir);
|
|
||||||
if (parent === dir) return null;
|
|
||||||
dir = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveClawdbotRoot(): string {
|
|
||||||
if (coreRootCache) return coreRootCache;
|
|
||||||
const override = process.env.CLAWDBOT_ROOT?.trim();
|
|
||||||
if (override) {
|
|
||||||
coreRootCache = override;
|
|
||||||
return override;
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidates = new Set<string>();
|
|
||||||
if (process.argv[1]) {
|
|
||||||
candidates.add(path.dirname(process.argv[1]));
|
|
||||||
}
|
|
||||||
candidates.add(process.cwd());
|
|
||||||
try {
|
|
||||||
const urlPath = fileURLToPath(import.meta.url);
|
|
||||||
candidates.add(path.dirname(urlPath));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const start of candidates) {
|
|
||||||
const found = findPackageRoot(start, "clawdbot");
|
|
||||||
if (found) {
|
|
||||||
coreRootCache = found;
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
"Unable to resolve Clawdbot root. Set CLAWDBOT_ROOT to the package root.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importCoreModule<T>(relativePath: string): Promise<T> {
|
|
||||||
const root = resolveClawdbotRoot();
|
|
||||||
const distPath = path.join(root, "dist", relativePath);
|
|
||||||
if (!fs.existsSync(distPath)) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (await import(pathToFileURL(distPath).href)) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadCoreChannelDeps(): Promise<CoreChannelDeps> {
|
|
||||||
if (coreDepsPromise) return coreDepsPromise;
|
|
||||||
|
|
||||||
coreDepsPromise = (async () => {
|
|
||||||
const [
|
|
||||||
chunk,
|
|
||||||
envelope,
|
|
||||||
dispatcher,
|
|
||||||
routing,
|
|
||||||
pairingMessages,
|
|
||||||
pairingStore,
|
|
||||||
mediaFetch,
|
|
||||||
mediaStore,
|
|
||||||
globals,
|
|
||||||
] = await Promise.all([
|
|
||||||
importCoreModule<{ chunkMarkdownText: CoreChannelDeps["chunkMarkdownText"] }>(
|
|
||||||
"auto-reply/chunk.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{ formatAgentEnvelope: CoreChannelDeps["formatAgentEnvelope"] }>(
|
|
||||||
"auto-reply/envelope.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{
|
|
||||||
dispatchReplyWithBufferedBlockDispatcher: CoreChannelDeps["dispatchReplyWithBufferedBlockDispatcher"];
|
|
||||||
}>("auto-reply/reply/provider-dispatcher.js"),
|
|
||||||
importCoreModule<{ resolveAgentRoute: CoreChannelDeps["resolveAgentRoute"] }>(
|
|
||||||
"routing/resolve-route.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{ buildPairingReply: CoreChannelDeps["buildPairingReply"] }>(
|
|
||||||
"pairing/pairing-messages.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{
|
|
||||||
readChannelAllowFromStore: CoreChannelDeps["readChannelAllowFromStore"];
|
|
||||||
upsertChannelPairingRequest: CoreChannelDeps["upsertChannelPairingRequest"];
|
|
||||||
}>("pairing/pairing-store.js"),
|
|
||||||
importCoreModule<{ fetchRemoteMedia: CoreChannelDeps["fetchRemoteMedia"] }>(
|
|
||||||
"media/fetch.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{ saveMediaBuffer: CoreChannelDeps["saveMediaBuffer"] }>(
|
|
||||||
"media/store.js",
|
|
||||||
),
|
|
||||||
importCoreModule<{ shouldLogVerbose: CoreChannelDeps["shouldLogVerbose"] }>(
|
|
||||||
"globals.js",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
chunkMarkdownText: chunk.chunkMarkdownText,
|
|
||||||
formatAgentEnvelope: envelope.formatAgentEnvelope,
|
|
||||||
dispatchReplyWithBufferedBlockDispatcher:
|
|
||||||
dispatcher.dispatchReplyWithBufferedBlockDispatcher,
|
|
||||||
resolveAgentRoute: routing.resolveAgentRoute,
|
|
||||||
buildPairingReply: pairingMessages.buildPairingReply,
|
|
||||||
readChannelAllowFromStore: pairingStore.readChannelAllowFromStore,
|
|
||||||
upsertChannelPairingRequest: pairingStore.upsertChannelPairingRequest,
|
|
||||||
fetchRemoteMedia: mediaFetch.fetchRemoteMedia,
|
|
||||||
saveMediaBuffer: mediaStore.saveMediaBuffer,
|
|
||||||
shouldLogVerbose: globals.shouldLogVerbose,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
return coreDepsPromise;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
|
||||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
||||||
import {
|
import {
|
||||||
finalizeInboundContext,
|
finalizeInboundContext,
|
||||||
|
formatAgentEnvelope,
|
||||||
isControlCommandMessage,
|
isControlCommandMessage,
|
||||||
recordSessionMetaFromInbound,
|
recordSessionMetaFromInbound,
|
||||||
resolveCommandAuthorizedFromAuthorizers,
|
resolveCommandAuthorizedFromAuthorizers,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
shouldComputeCommandAuthorized,
|
shouldComputeCommandAuthorized,
|
||||||
|
type ClawdbotConfig,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||||
import {
|
import {
|
||||||
ZaloApiError,
|
ZaloApiError,
|
||||||
deleteWebhook,
|
deleteWebhook,
|
||||||
@@ -20,10 +23,8 @@ import {
|
|||||||
type ZaloMessage,
|
type ZaloMessage,
|
||||||
type ZaloUpdate,
|
type ZaloUpdate,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import { zaloPlugin } from "./channel.js";
|
|
||||||
import { loadCoreChannelDeps } from "./core-bridge.js";
|
|
||||||
import { resolveZaloProxyFetch } from "./proxy.js";
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import { getZaloRuntime } from "./runtime.js";
|
||||||
|
|
||||||
export type ZaloRuntimeEnv = {
|
export type ZaloRuntimeEnv = {
|
||||||
log?: (message: string) => void;
|
log?: (message: string) => void;
|
||||||
@@ -33,7 +34,7 @@ export type ZaloRuntimeEnv = {
|
|||||||
export type ZaloMonitorOptions = {
|
export type ZaloMonitorOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
account: ResolvedZaloAccount;
|
account: ResolvedZaloAccount;
|
||||||
config: CoreConfig;
|
config: ClawdbotConfig;
|
||||||
runtime: ZaloRuntimeEnv;
|
runtime: ZaloRuntimeEnv;
|
||||||
abortSignal: AbortSignal;
|
abortSignal: AbortSignal;
|
||||||
useWebhook?: boolean;
|
useWebhook?: boolean;
|
||||||
@@ -51,9 +52,11 @@ export type ZaloMonitorResult = {
|
|||||||
const ZALO_TEXT_LIMIT = 2000;
|
const ZALO_TEXT_LIMIT = 2000;
|
||||||
const DEFAULT_MEDIA_MAX_MB = 5;
|
const DEFAULT_MEDIA_MAX_MB = 5;
|
||||||
|
|
||||||
function logVerbose(deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>, message: string): void {
|
type ZaloCoreRuntime = ReturnType<typeof getZaloRuntime>;
|
||||||
if (deps.shouldLogVerbose()) {
|
|
||||||
console.log(`[zalo] ${message}`);
|
function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: string): void {
|
||||||
|
if (core.logging.shouldLogVerbose()) {
|
||||||
|
runtime.log?.(`[zalo] ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +103,9 @@ async function readJsonBody(req: IncomingMessage, maxBytes: number) {
|
|||||||
type WebhookTarget = {
|
type WebhookTarget = {
|
||||||
token: string;
|
token: string;
|
||||||
account: ResolvedZaloAccount;
|
account: ResolvedZaloAccount;
|
||||||
config: CoreConfig;
|
config: ClawdbotConfig;
|
||||||
runtime: ZaloRuntimeEnv;
|
runtime: ZaloRuntimeEnv;
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>;
|
core: ZaloCoreRuntime;
|
||||||
secret: string;
|
secret: string;
|
||||||
path: string;
|
path: string;
|
||||||
mediaMaxMb: number;
|
mediaMaxMb: number;
|
||||||
@@ -207,7 +210,7 @@ export async function handleZaloWebhookRequest(
|
|||||||
target.account,
|
target.account,
|
||||||
target.config,
|
target.config,
|
||||||
target.runtime,
|
target.runtime,
|
||||||
target.deps,
|
target.core,
|
||||||
target.mediaMaxMb,
|
target.mediaMaxMb,
|
||||||
target.statusSink,
|
target.statusSink,
|
||||||
target.fetcher,
|
target.fetcher,
|
||||||
@@ -223,9 +226,9 @@ export async function handleZaloWebhookRequest(
|
|||||||
function startPollingLoop(params: {
|
function startPollingLoop(params: {
|
||||||
token: string;
|
token: string;
|
||||||
account: ResolvedZaloAccount;
|
account: ResolvedZaloAccount;
|
||||||
config: CoreConfig;
|
config: ClawdbotConfig;
|
||||||
runtime: ZaloRuntimeEnv;
|
runtime: ZaloRuntimeEnv;
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>;
|
core: ZaloCoreRuntime;
|
||||||
abortSignal: AbortSignal;
|
abortSignal: AbortSignal;
|
||||||
isStopped: () => boolean;
|
isStopped: () => boolean;
|
||||||
mediaMaxMb: number;
|
mediaMaxMb: number;
|
||||||
@@ -237,7 +240,7 @@ function startPollingLoop(params: {
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
isStopped,
|
isStopped,
|
||||||
mediaMaxMb,
|
mediaMaxMb,
|
||||||
@@ -259,7 +262,7 @@ function startPollingLoop(params: {
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
mediaMaxMb,
|
mediaMaxMb,
|
||||||
statusSink,
|
statusSink,
|
||||||
fetcher,
|
fetcher,
|
||||||
@@ -286,9 +289,9 @@ async function processUpdate(
|
|||||||
update: ZaloUpdate,
|
update: ZaloUpdate,
|
||||||
token: string,
|
token: string,
|
||||||
account: ResolvedZaloAccount,
|
account: ResolvedZaloAccount,
|
||||||
config: CoreConfig,
|
config: ClawdbotConfig,
|
||||||
runtime: ZaloRuntimeEnv,
|
runtime: ZaloRuntimeEnv,
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>,
|
core: ZaloCoreRuntime,
|
||||||
mediaMaxMb: number,
|
mediaMaxMb: number,
|
||||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
||||||
fetcher?: ZaloFetch,
|
fetcher?: ZaloFetch,
|
||||||
@@ -304,7 +307,7 @@ async function processUpdate(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
statusSink,
|
statusSink,
|
||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
@@ -316,7 +319,7 @@ async function processUpdate(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
mediaMaxMb,
|
mediaMaxMb,
|
||||||
statusSink,
|
statusSink,
|
||||||
fetcher,
|
fetcher,
|
||||||
@@ -337,9 +340,9 @@ async function handleTextMessage(
|
|||||||
message: ZaloMessage,
|
message: ZaloMessage,
|
||||||
token: string,
|
token: string,
|
||||||
account: ResolvedZaloAccount,
|
account: ResolvedZaloAccount,
|
||||||
config: CoreConfig,
|
config: ClawdbotConfig,
|
||||||
runtime: ZaloRuntimeEnv,
|
runtime: ZaloRuntimeEnv,
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>,
|
core: ZaloCoreRuntime,
|
||||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
||||||
fetcher?: ZaloFetch,
|
fetcher?: ZaloFetch,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -352,7 +355,7 @@ async function handleTextMessage(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
text,
|
text,
|
||||||
mediaPath: undefined,
|
mediaPath: undefined,
|
||||||
mediaType: undefined,
|
mediaType: undefined,
|
||||||
@@ -365,9 +368,9 @@ async function handleImageMessage(
|
|||||||
message: ZaloMessage,
|
message: ZaloMessage,
|
||||||
token: string,
|
token: string,
|
||||||
account: ResolvedZaloAccount,
|
account: ResolvedZaloAccount,
|
||||||
config: CoreConfig,
|
config: ClawdbotConfig,
|
||||||
runtime: ZaloRuntimeEnv,
|
runtime: ZaloRuntimeEnv,
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>,
|
core: ZaloCoreRuntime,
|
||||||
mediaMaxMb: number,
|
mediaMaxMb: number,
|
||||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
||||||
fetcher?: ZaloFetch,
|
fetcher?: ZaloFetch,
|
||||||
@@ -380,8 +383,8 @@ async function handleImageMessage(
|
|||||||
if (photo) {
|
if (photo) {
|
||||||
try {
|
try {
|
||||||
const maxBytes = mediaMaxMb * 1024 * 1024;
|
const maxBytes = mediaMaxMb * 1024 * 1024;
|
||||||
const fetched = await deps.fetchRemoteMedia({ url: photo });
|
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo });
|
||||||
const saved = await deps.saveMediaBuffer(
|
const saved = await core.channel.media.saveMediaBuffer(
|
||||||
fetched.buffer,
|
fetched.buffer,
|
||||||
fetched.contentType,
|
fetched.contentType,
|
||||||
"inbound",
|
"inbound",
|
||||||
@@ -400,7 +403,7 @@ async function handleImageMessage(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
text: caption,
|
text: caption,
|
||||||
mediaPath,
|
mediaPath,
|
||||||
mediaType,
|
mediaType,
|
||||||
@@ -413,9 +416,9 @@ async function processMessageWithPipeline(params: {
|
|||||||
message: ZaloMessage;
|
message: ZaloMessage;
|
||||||
token: string;
|
token: string;
|
||||||
account: ResolvedZaloAccount;
|
account: ResolvedZaloAccount;
|
||||||
config: CoreConfig;
|
config: ClawdbotConfig;
|
||||||
runtime: ZaloRuntimeEnv;
|
runtime: ZaloRuntimeEnv;
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>;
|
core: ZaloCoreRuntime;
|
||||||
text?: string;
|
text?: string;
|
||||||
mediaPath?: string;
|
mediaPath?: string;
|
||||||
mediaType?: string;
|
mediaType?: string;
|
||||||
@@ -428,7 +431,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
text,
|
text,
|
||||||
mediaPath,
|
mediaPath,
|
||||||
mediaType,
|
mediaType,
|
||||||
@@ -448,7 +451,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config);
|
const shouldComputeAuth = shouldComputeCommandAuthorized(rawBody, config);
|
||||||
const storeAllowFrom =
|
const storeAllowFrom =
|
||||||
!isGroup && (dmPolicy !== "open" || shouldComputeAuth)
|
!isGroup && (dmPolicy !== "open" || shouldComputeAuth)
|
||||||
? await deps.readChannelAllowFromStore("zalo").catch(() => [])
|
? await core.channel.pairing.readAllowFromStore("zalo").catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
||||||
const useAccessGroups = config.commands?.useAccessGroups !== false;
|
const useAccessGroups = config.commands?.useAccessGroups !== false;
|
||||||
@@ -462,7 +465,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
|
|
||||||
if (!isGroup) {
|
if (!isGroup) {
|
||||||
if (dmPolicy === "disabled") {
|
if (dmPolicy === "disabled") {
|
||||||
logVerbose(deps, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
|
logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,21 +474,20 @@ async function processMessageWithPipeline(params: {
|
|||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
if (dmPolicy === "pairing") {
|
if (dmPolicy === "pairing") {
|
||||||
const { code, created } = await deps.upsertChannelPairingRequest({
|
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
id: senderId,
|
id: senderId,
|
||||||
meta: { name: senderName ?? undefined },
|
meta: { name: senderName ?? undefined },
|
||||||
pairingAdapter: zaloPlugin.pairing,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (created) {
|
if (created) {
|
||||||
logVerbose(deps, `zalo pairing request sender=${senderId}`);
|
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
||||||
try {
|
try {
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
token,
|
token,
|
||||||
{
|
{
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
text: deps.buildPairingReply({
|
text: core.channel.pairing.buildPairingReply({
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
idLine: `Your Zalo user id: ${senderId}`,
|
idLine: `Your Zalo user id: ${senderId}`,
|
||||||
code,
|
code,
|
||||||
@@ -495,18 +497,26 @@ async function processMessageWithPipeline(params: {
|
|||||||
);
|
);
|
||||||
statusSink?.({ lastOutboundAt: Date.now() });
|
statusSink?.({ lastOutboundAt: Date.now() });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(deps, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
|
logVerbose(
|
||||||
|
core,
|
||||||
|
runtime,
|
||||||
|
`zalo pairing reply failed for ${senderId}: ${String(err)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logVerbose(deps, `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`);
|
logVerbose(
|
||||||
|
core,
|
||||||
|
runtime,
|
||||||
|
`Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = deps.resolveAgentRoute({
|
const route = core.channel.routing.resolveAgentRoute({
|
||||||
cfg: config,
|
cfg: config,
|
||||||
channel: "zalo",
|
channel: "zalo",
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
@@ -517,16 +527,14 @@ async function processMessageWithPipeline(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
|
if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
|
||||||
logVerbose(deps, `zalo: drop control command from unauthorized sender ${senderId}`);
|
logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromLabel = isGroup
|
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
||||||
? `group:${chatId}`
|
const body = formatAgentEnvelope({
|
||||||
: senderName || `user:${senderId}`;
|
channel: "Zalo",
|
||||||
const body = deps.formatAgentEnvelope({
|
from: fromLabel,
|
||||||
channel: "Zalo",
|
|
||||||
from: fromLabel,
|
|
||||||
timestamp: date ? date * 1000 : undefined,
|
timestamp: date ? date * 1000 : undefined,
|
||||||
body: rawBody,
|
body: rawBody,
|
||||||
});
|
});
|
||||||
@@ -565,7 +573,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
await deps.dispatchReplyWithBufferedBlockDispatcher({
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||||
ctx: ctxPayload,
|
ctx: ctxPayload,
|
||||||
cfg: config,
|
cfg: config,
|
||||||
dispatcherOptions: {
|
dispatcherOptions: {
|
||||||
@@ -575,7 +583,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
token,
|
token,
|
||||||
chatId,
|
chatId,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
statusSink,
|
statusSink,
|
||||||
fetcher,
|
fetcher,
|
||||||
});
|
});
|
||||||
@@ -592,11 +600,11 @@ async function deliverZaloReply(params: {
|
|||||||
token: string;
|
token: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
runtime: ZaloRuntimeEnv;
|
runtime: ZaloRuntimeEnv;
|
||||||
deps: Awaited<ReturnType<typeof loadCoreChannelDeps>>;
|
core: ZaloCoreRuntime;
|
||||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||||
fetcher?: ZaloFetch;
|
fetcher?: ZaloFetch;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { payload, token, chatId, runtime, deps, statusSink, fetcher } = params;
|
const { payload, token, chatId, runtime, core, statusSink, fetcher } = params;
|
||||||
|
|
||||||
const mediaList = payload.mediaUrls?.length
|
const mediaList = payload.mediaUrls?.length
|
||||||
? payload.mediaUrls
|
? payload.mediaUrls
|
||||||
@@ -620,7 +628,7 @@ async function deliverZaloReply(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload.text) {
|
if (payload.text) {
|
||||||
const chunks = deps.chunkMarkdownText(payload.text, ZALO_TEXT_LIMIT);
|
const chunks = core.channel.text.chunkMarkdownText(payload.text, ZALO_TEXT_LIMIT);
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
try {
|
try {
|
||||||
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
||||||
@@ -649,7 +657,7 @@ export async function monitorZaloProvider(
|
|||||||
fetcher: fetcherOverride,
|
fetcher: fetcherOverride,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const deps = await loadCoreChannelDeps();
|
const core = getZaloRuntime();
|
||||||
const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
||||||
const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
|
const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
|
||||||
|
|
||||||
@@ -686,7 +694,7 @@ export async function monitorZaloProvider(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
path,
|
path,
|
||||||
secret: webhookSecret,
|
secret: webhookSecret,
|
||||||
statusSink: (patch) => statusSink?.(patch),
|
statusSink: (patch) => statusSink?.(patch),
|
||||||
@@ -715,7 +723,7 @@ export async function monitorZaloProvider(
|
|||||||
account,
|
account,
|
||||||
config,
|
config,
|
||||||
runtime,
|
runtime,
|
||||||
deps,
|
core,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
isStopped: () => stopped,
|
isStopped: () => stopped,
|
||||||
mediaMaxMb: effectiveMediaMaxMb,
|
mediaMaxMb: effectiveMediaMaxMb,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { AddressInfo } from "node:net";
|
|||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import type { CoreConfig, ResolvedZaloAccount } from "./types.js";
|
import type { ClawdbotConfig, PluginRuntime } from "clawdbot/plugin-sdk";
|
||||||
import type { loadCoreChannelDeps } from "./core-bridge.js";
|
import type { ResolvedZaloAccount } from "./types.js";
|
||||||
import { handleZaloWebhookRequest, registerZaloWebhookTarget } from "./monitor.js";
|
import { handleZaloWebhookRequest, registerZaloWebhookTarget } from "./monitor.js";
|
||||||
|
|
||||||
async function withServer(
|
async function withServer(
|
||||||
@@ -26,7 +26,7 @@ async function withServer(
|
|||||||
|
|
||||||
describe("handleZaloWebhookRequest", () => {
|
describe("handleZaloWebhookRequest", () => {
|
||||||
it("returns 400 for non-object payloads", async () => {
|
it("returns 400 for non-object payloads", async () => {
|
||||||
const deps = {} as Awaited<ReturnType<typeof loadCoreChannelDeps>>;
|
const core = {} as PluginRuntime;
|
||||||
const account: ResolvedZaloAccount = {
|
const account: ResolvedZaloAccount = {
|
||||||
accountId: "default",
|
accountId: "default",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -37,9 +37,9 @@ describe("handleZaloWebhookRequest", () => {
|
|||||||
const unregister = registerZaloWebhookTarget({
|
const unregister = registerZaloWebhookTarget({
|
||||||
token: "tok",
|
token: "tok",
|
||||||
account,
|
account,
|
||||||
config: {} as CoreConfig,
|
config: {} as ClawdbotConfig,
|
||||||
runtime: {},
|
runtime: {},
|
||||||
deps,
|
core,
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
path: "/hook",
|
path: "/hook",
|
||||||
mediaMaxMb: 5,
|
mediaMaxMb: 5,
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import type {
|
import type {
|
||||||
ChannelOnboardingAdapter,
|
ChannelOnboardingAdapter,
|
||||||
ChannelOnboardingDmPolicy,
|
ChannelOnboardingDmPolicy,
|
||||||
|
ClawdbotConfig,
|
||||||
WizardPrompter,
|
WizardPrompter,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
import {
|
||||||
|
addWildcardAllowFrom,
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
normalizeAccountId,
|
||||||
|
promptAccountId,
|
||||||
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { addWildcardAllowFrom, promptAccountId } from "./shared/onboarding.js";
|
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./shared/account-ids.js";
|
|
||||||
import {
|
import {
|
||||||
listZaloAccountIds,
|
listZaloAccountIds,
|
||||||
resolveDefaultZaloAccountId,
|
resolveDefaultZaloAccountId,
|
||||||
resolveZaloAccount,
|
resolveZaloAccount,
|
||||||
} from "./accounts.js";
|
} from "./accounts.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
|
||||||
|
|
||||||
const channel = "zalo" as const;
|
const channel = "zalo" as const;
|
||||||
|
|
||||||
type UpdateMode = "polling" | "webhook";
|
type UpdateMode = "polling" | "webhook";
|
||||||
|
|
||||||
function setZaloDmPolicy(cfg: CoreConfig, dmPolicy: "pairing" | "allowlist" | "open" | "disabled") {
|
function setZaloDmPolicy(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
||||||
|
) {
|
||||||
const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : undefined;
|
const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : undefined;
|
||||||
return {
|
return {
|
||||||
...cfg,
|
...cfg,
|
||||||
@@ -29,17 +36,17 @@ function setZaloDmPolicy(cfg: CoreConfig, dmPolicy: "pairing" | "allowlist" | "o
|
|||||||
...(allowFrom ? { allowFrom } : {}),
|
...(allowFrom ? { allowFrom } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setZaloUpdateMode(
|
function setZaloUpdateMode(
|
||||||
cfg: CoreConfig,
|
cfg: ClawdbotConfig,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
mode: UpdateMode,
|
mode: UpdateMode,
|
||||||
webhookUrl?: string,
|
webhookUrl?: string,
|
||||||
webhookSecret?: string,
|
webhookSecret?: string,
|
||||||
webhookPath?: string,
|
webhookPath?: string,
|
||||||
): CoreConfig {
|
): ClawdbotConfig {
|
||||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||||
if (mode === "polling") {
|
if (mode === "polling") {
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
@@ -55,7 +62,7 @@ function setZaloUpdateMode(
|
|||||||
...cfg.channels,
|
...cfg.channels,
|
||||||
zalo: rest,
|
zalo: rest,
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record<
|
const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record<
|
||||||
string,
|
string,
|
||||||
@@ -78,7 +85,7 @@ function setZaloUpdateMode(
|
|||||||
accounts,
|
accounts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefault) {
|
if (isDefault) {
|
||||||
@@ -93,7 +100,7 @@ function setZaloUpdateMode(
|
|||||||
webhookPath,
|
webhookPath,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record<
|
const accounts = { ...(cfg.channels?.zalo?.accounts ?? {}) } as Record<
|
||||||
@@ -115,7 +122,7 @@ function setZaloUpdateMode(
|
|||||||
accounts,
|
accounts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function noteZaloTokenHelp(prompter: WizardPrompter): Promise<void> {
|
async function noteZaloTokenHelp(prompter: WizardPrompter): Promise<void> {
|
||||||
@@ -132,10 +139,10 @@ async function noteZaloTokenHelp(prompter: WizardPrompter): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function promptZaloAllowFrom(params: {
|
async function promptZaloAllowFrom(params: {
|
||||||
cfg: CoreConfig;
|
cfg: ClawdbotConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
}): Promise<CoreConfig> {
|
}): Promise<ClawdbotConfig> {
|
||||||
const { cfg, prompter, accountId } = params;
|
const { cfg, prompter, accountId } = params;
|
||||||
const resolved = resolveZaloAccount({ cfg, accountId });
|
const resolved = resolveZaloAccount({ cfg, accountId });
|
||||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||||
@@ -169,7 +176,7 @@ async function promptZaloAllowFrom(params: {
|
|||||||
allowFrom: unique,
|
allowFrom: unique,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -190,7 +197,7 @@ async function promptZaloAllowFrom(params: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
@@ -199,15 +206,15 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
policyKey: "channels.zalo.dmPolicy",
|
policyKey: "channels.zalo.dmPolicy",
|
||||||
allowFromKey: "channels.zalo.allowFrom",
|
allowFromKey: "channels.zalo.allowFrom",
|
||||||
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
||||||
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as CoreConfig, policy),
|
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg as ClawdbotConfig, policy),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||||
channel,
|
channel,
|
||||||
dmPolicy,
|
dmPolicy,
|
||||||
getStatus: async ({ cfg }) => {
|
getStatus: async ({ cfg }) => {
|
||||||
const configured = listZaloAccountIds(cfg as CoreConfig).some((accountId) =>
|
const configured = listZaloAccountIds(cfg as ClawdbotConfig).some((accountId) =>
|
||||||
Boolean(resolveZaloAccount({ cfg: cfg as CoreConfig, accountId }).token),
|
Boolean(resolveZaloAccount({ cfg: cfg as ClawdbotConfig, accountId }).token),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
channel,
|
channel,
|
||||||
@@ -219,13 +226,13 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
},
|
},
|
||||||
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
|
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
|
||||||
const zaloOverride = accountOverrides.zalo?.trim();
|
const zaloOverride = accountOverrides.zalo?.trim();
|
||||||
const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg as CoreConfig);
|
const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg as ClawdbotConfig);
|
||||||
let zaloAccountId = zaloOverride
|
let zaloAccountId = zaloOverride
|
||||||
? normalizeAccountId(zaloOverride)
|
? normalizeAccountId(zaloOverride)
|
||||||
: defaultZaloAccountId;
|
: defaultZaloAccountId;
|
||||||
if (shouldPromptAccountIds && !zaloOverride) {
|
if (shouldPromptAccountIds && !zaloOverride) {
|
||||||
zaloAccountId = await promptAccountId({
|
zaloAccountId = await promptAccountId({
|
||||||
cfg: cfg as CoreConfig,
|
cfg: cfg as ClawdbotConfig,
|
||||||
prompter,
|
prompter,
|
||||||
label: "Zalo",
|
label: "Zalo",
|
||||||
currentId: zaloAccountId,
|
currentId: zaloAccountId,
|
||||||
@@ -234,7 +241,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let next = cfg as CoreConfig;
|
let next = cfg as ClawdbotConfig;
|
||||||
const resolvedAccount = resolveZaloAccount({ cfg: next, accountId: zaloAccountId });
|
const resolvedAccount = resolveZaloAccount({ cfg: next, accountId: zaloAccountId });
|
||||||
const accountConfigured = Boolean(resolvedAccount.token);
|
const accountConfigured = Boolean(resolvedAccount.token);
|
||||||
const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
|
const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
|
||||||
@@ -262,7 +269,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
} else {
|
} else {
|
||||||
token = String(
|
token = String(
|
||||||
await prompter.text({
|
await prompter.text({
|
||||||
@@ -305,7 +312,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
botToken: token,
|
botToken: token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
} else {
|
} else {
|
||||||
next = {
|
next = {
|
||||||
...next,
|
...next,
|
||||||
@@ -324,7 +331,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as CoreConfig;
|
} as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
extensions/zalo/src/runtime.ts
Normal file
14
extensions/zalo/src/runtime.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
let runtime: PluginRuntime | null = null;
|
||||||
|
|
||||||
|
export function setZaloRuntime(next: PluginRuntime): void {
|
||||||
|
runtime = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getZaloRuntime(): PluginRuntime {
|
||||||
|
if (!runtime) {
|
||||||
|
throw new Error("Zalo runtime not initialized");
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CoreConfig } from "./types.js";
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import type { ZaloFetch } from "./api.js";
|
import type { ZaloFetch } from "./api.js";
|
||||||
import { sendMessage, sendPhoto } from "./api.js";
|
import { sendMessage, sendPhoto } from "./api.js";
|
||||||
import { resolveZaloAccount } from "./accounts.js";
|
import { resolveZaloAccount } from "./accounts.js";
|
||||||
@@ -8,7 +9,7 @@ import { resolveZaloToken } from "./token.js";
|
|||||||
export type ZaloSendOptions = {
|
export type ZaloSendOptions = {
|
||||||
token?: string;
|
token?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
cfg?: CoreConfig;
|
cfg?: ClawdbotConfig;
|
||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export const DEFAULT_ACCOUNT_ID = "default";
|
|
||||||
|
|
||||||
export function normalizeAccountId(value: string | undefined | null): string {
|
|
||||||
const trimmed = (value ?? "").trim();
|
|
||||||
if (!trimmed) return DEFAULT_ACCOUNT_ID;
|
|
||||||
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed;
|
|
||||||
return (
|
|
||||||
trimmed
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9_-]+/g, "-")
|
|
||||||
.replace(/^-+/, "")
|
|
||||||
.replace(/-+$/, "")
|
|
||||||
.slice(0, 64) || DEFAULT_ACCOUNT_ID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { DEFAULT_ACCOUNT_ID } from "./account-ids.js";
|
|
||||||
|
|
||||||
type ChannelSection = {
|
|
||||||
accounts?: Record<string, Record<string, unknown>>;
|
|
||||||
enabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConfigWithChannels = {
|
|
||||||
channels?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setAccountEnabledInConfigSection<T extends ConfigWithChannels>(params: {
|
|
||||||
cfg: T;
|
|
||||||
sectionKey: string;
|
|
||||||
accountId: string;
|
|
||||||
enabled: boolean;
|
|
||||||
allowTopLevel?: boolean;
|
|
||||||
}): T {
|
|
||||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
|
||||||
const channels = params.cfg.channels;
|
|
||||||
const base = (channels?.[params.sectionKey] as ChannelSection | undefined) ?? undefined;
|
|
||||||
const hasAccounts = Boolean(base?.accounts);
|
|
||||||
if (params.allowTopLevel && accountKey === DEFAULT_ACCOUNT_ID && !hasAccounts) {
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.sectionKey]: {
|
|
||||||
...base,
|
|
||||||
enabled: params.enabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseAccounts = (base?.accounts ?? {}) as Record<string, Record<string, unknown>>;
|
|
||||||
const existing = baseAccounts[accountKey] ?? {};
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.sectionKey]: {
|
|
||||||
...base,
|
|
||||||
accounts: {
|
|
||||||
...baseAccounts,
|
|
||||||
[accountKey]: {
|
|
||||||
...existing,
|
|
||||||
enabled: params.enabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteAccountFromConfigSection<T extends ConfigWithChannels>(params: {
|
|
||||||
cfg: T;
|
|
||||||
sectionKey: string;
|
|
||||||
accountId: string;
|
|
||||||
clearBaseFields?: string[];
|
|
||||||
}): T {
|
|
||||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
|
||||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
|
||||||
const base = (channels?.[params.sectionKey] as ChannelSection | undefined) ?? undefined;
|
|
||||||
if (!base) return params.cfg;
|
|
||||||
|
|
||||||
const baseAccounts =
|
|
||||||
base.accounts && typeof base.accounts === "object" ? { ...base.accounts } : undefined;
|
|
||||||
|
|
||||||
if (accountKey !== DEFAULT_ACCOUNT_ID) {
|
|
||||||
const accounts = baseAccounts ? { ...baseAccounts } : {};
|
|
||||||
delete accounts[accountKey];
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.sectionKey]: {
|
|
||||||
...base,
|
|
||||||
accounts: Object.keys(accounts).length ? accounts : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseAccounts && Object.keys(baseAccounts).length > 0) {
|
|
||||||
delete baseAccounts[accountKey];
|
|
||||||
const baseRecord = { ...(base as Record<string, unknown>) };
|
|
||||||
for (const field of params.clearBaseFields ?? []) {
|
|
||||||
if (field in baseRecord) baseRecord[field] = undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.sectionKey]: {
|
|
||||||
...baseRecord,
|
|
||||||
accounts: Object.keys(baseAccounts).length ? baseAccounts : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextChannels = { ...channels } as Record<string, unknown>;
|
|
||||||
delete nextChannels[params.sectionKey];
|
|
||||||
const nextCfg = { ...params.cfg } as T;
|
|
||||||
if (Object.keys(nextChannels).length > 0) {
|
|
||||||
nextCfg.channels = nextChannels as T["channels"];
|
|
||||||
} else {
|
|
||||||
delete nextCfg.channels;
|
|
||||||
}
|
|
||||||
return nextCfg;
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./account-ids.js";
|
|
||||||
|
|
||||||
type ConfigWithChannels = {
|
|
||||||
channels?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChannelSectionBase = {
|
|
||||||
name?: string;
|
|
||||||
accounts?: Record<string, Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function channelHasAccounts(cfg: ConfigWithChannels, channelKey: string): boolean {
|
|
||||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
|
||||||
const base = channels?.[channelKey] as ChannelSectionBase | undefined;
|
|
||||||
return Boolean(base?.accounts && Object.keys(base.accounts).length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldStoreNameInAccounts(params: {
|
|
||||||
cfg: ConfigWithChannels;
|
|
||||||
channelKey: string;
|
|
||||||
accountId: string;
|
|
||||||
alwaysUseAccounts?: boolean;
|
|
||||||
}): boolean {
|
|
||||||
if (params.alwaysUseAccounts) return true;
|
|
||||||
if (params.accountId !== DEFAULT_ACCOUNT_ID) return true;
|
|
||||||
return channelHasAccounts(params.cfg, params.channelKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyAccountNameToChannelSection<T extends ConfigWithChannels>(params: {
|
|
||||||
cfg: T;
|
|
||||||
channelKey: string;
|
|
||||||
accountId: string;
|
|
||||||
name?: string;
|
|
||||||
alwaysUseAccounts?: boolean;
|
|
||||||
}): T {
|
|
||||||
const trimmed = params.name?.trim();
|
|
||||||
if (!trimmed) return params.cfg;
|
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
|
||||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
|
||||||
const baseConfig = channels?.[params.channelKey];
|
|
||||||
const base =
|
|
||||||
typeof baseConfig === "object" && baseConfig ? (baseConfig as ChannelSectionBase) : undefined;
|
|
||||||
const useAccounts = shouldStoreNameInAccounts({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channelKey: params.channelKey,
|
|
||||||
accountId,
|
|
||||||
alwaysUseAccounts: params.alwaysUseAccounts,
|
|
||||||
});
|
|
||||||
if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) {
|
|
||||||
const safeBase = base ?? {};
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.channelKey]: {
|
|
||||||
...safeBase,
|
|
||||||
name: trimmed,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
const baseAccounts: Record<string, Record<string, unknown>> = base?.accounts ?? {};
|
|
||||||
const existingAccount = baseAccounts[accountId] ?? {};
|
|
||||||
const baseWithoutName =
|
|
||||||
accountId === DEFAULT_ACCOUNT_ID
|
|
||||||
? (({ name: _ignored, ...rest }) => rest)(base ?? {})
|
|
||||||
: (base ?? {});
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.channelKey]: {
|
|
||||||
...baseWithoutName,
|
|
||||||
accounts: {
|
|
||||||
...baseAccounts,
|
|
||||||
[accountId]: {
|
|
||||||
...existingAccount,
|
|
||||||
name: trimmed,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateBaseNameToDefaultAccount<T extends ConfigWithChannels>(params: {
|
|
||||||
cfg: T;
|
|
||||||
channelKey: string;
|
|
||||||
alwaysUseAccounts?: boolean;
|
|
||||||
}): T {
|
|
||||||
if (params.alwaysUseAccounts) return params.cfg;
|
|
||||||
const channels = params.cfg.channels as Record<string, unknown> | undefined;
|
|
||||||
const base = channels?.[params.channelKey] as ChannelSectionBase | undefined;
|
|
||||||
const baseName = base?.name?.trim();
|
|
||||||
if (!baseName) return params.cfg;
|
|
||||||
const accounts: Record<string, Record<string, unknown>> = {
|
|
||||||
...base?.accounts,
|
|
||||||
};
|
|
||||||
const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {};
|
|
||||||
if (!defaultAccount.name) {
|
|
||||||
accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName };
|
|
||||||
}
|
|
||||||
const { name: _ignored, ...rest } = base ?? {};
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
channels: {
|
|
||||||
...channels,
|
|
||||||
[params.channelKey]: {
|
|
||||||
...rest,
|
|
||||||
accounts,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import type { WizardPrompter } from "clawdbot/plugin-sdk";
|
|
||||||
|
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./account-ids.js";
|
|
||||||
|
|
||||||
export type PromptAccountIdParams<TConfig> = {
|
|
||||||
cfg: TConfig;
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
label: string;
|
|
||||||
currentId?: string;
|
|
||||||
listAccountIds: (cfg: TConfig) => string[];
|
|
||||||
defaultAccountId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function promptAccountId<TConfig>(
|
|
||||||
params: PromptAccountIdParams<TConfig>,
|
|
||||||
): Promise<string> {
|
|
||||||
const existingIds = params.listAccountIds(params.cfg);
|
|
||||||
const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
|
|
||||||
const choice = (await params.prompter.select({
|
|
||||||
message: `${params.label} account`,
|
|
||||||
options: [
|
|
||||||
...existingIds.map((id) => ({
|
|
||||||
value: id,
|
|
||||||
label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id,
|
|
||||||
})),
|
|
||||||
{ value: "__new__", label: "Add a new account" },
|
|
||||||
],
|
|
||||||
initialValue: initial,
|
|
||||||
})) as string;
|
|
||||||
|
|
||||||
if (choice !== "__new__") return normalizeAccountId(choice);
|
|
||||||
|
|
||||||
const entered = await params.prompter.text({
|
|
||||||
message: `New ${params.label} account id`,
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
});
|
|
||||||
const normalized = normalizeAccountId(String(entered));
|
|
||||||
if (String(entered).trim() !== normalized) {
|
|
||||||
await params.prompter.note(
|
|
||||||
`Normalized account id to "${normalized}".`,
|
|
||||||
`${params.label} account`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addWildcardAllowFrom(
|
|
||||||
allowFrom?: Array<string | number> | null,
|
|
||||||
): Array<string | number> {
|
|
||||||
const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
||||||
if (!next.includes("*")) next.push("*");
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export const PAIRING_APPROVED_MESSAGE =
|
|
||||||
"\u2705 Clawdbot access approved. Send a message to start chatting.";
|
|
||||||
|
|
||||||
export function formatPairingApproveHint(channelId: string): string {
|
|
||||||
return `Approve via: clawdbot pairing list ${channelId} / clawdbot pairing approve ${channelId} <code>`;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
import { DEFAULT_ACCOUNT_ID } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import type { ZaloConfig } from "./types.js";
|
import type { ZaloConfig } from "./types.js";
|
||||||
import { DEFAULT_ACCOUNT_ID } from "./shared/account-ids.js";
|
|
||||||
|
|
||||||
export type ZaloTokenResolution = {
|
export type ZaloTokenResolution = {
|
||||||
token: string;
|
token: string;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
export function readStringParam(
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
opts?: { required?: boolean; allowEmpty?: boolean; trim?: boolean },
|
|
||||||
): string | undefined {
|
|
||||||
const raw = params[key];
|
|
||||||
if (raw === undefined || raw === null) {
|
|
||||||
if (opts?.required) throw new Error(`${key} is required`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const value = String(raw);
|
|
||||||
const trimmed = opts?.trim === false ? value : value.trim();
|
|
||||||
if (!opts?.allowEmpty && !trimmed) {
|
|
||||||
if (opts?.required) throw new Error(`${key} is required`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function jsonResult(payload: unknown) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(payload, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: payload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -40,10 +40,3 @@ export type ResolvedZaloAccount = {
|
|||||||
tokenSource: ZaloTokenSource;
|
tokenSource: ZaloTokenSource;
|
||||||
config: ZaloAccountConfig;
|
config: ZaloAccountConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CoreConfig = {
|
|
||||||
channels?: {
|
|
||||||
zalo?: ZaloConfig;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export type {
|
|||||||
ChannelOnboardingAdapter,
|
ChannelOnboardingAdapter,
|
||||||
ChannelOnboardingDmPolicy,
|
ChannelOnboardingDmPolicy,
|
||||||
} from "../channels/plugins/onboarding-types.js";
|
} from "../channels/plugins/onboarding-types.js";
|
||||||
export { addWildcardAllowFrom } from "../channels/plugins/onboarding/helpers.js";
|
export { addWildcardAllowFrom, promptAccountId } from "../channels/plugins/onboarding/helpers.js";
|
||||||
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
|
export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -41,7 +41,14 @@ export type PluginRuntime = {
|
|||||||
channel: string;
|
channel: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
peer: { kind: "dm" | "group" | "channel"; id: string };
|
peer: { kind: "dm" | "group" | "channel"; id: string };
|
||||||
}) => { sessionKey: string; accountId: string };
|
}) => {
|
||||||
|
agentId: string;
|
||||||
|
channel: string;
|
||||||
|
accountId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
mainSessionKey: string;
|
||||||
|
matchedBy: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
pairing: {
|
pairing: {
|
||||||
buildPairingReply: (params: { channel: string; idLine: string; code: string }) => string;
|
buildPairingReply: (params: { channel: string; idLine: string; code: string }) => string;
|
||||||
|
|||||||
Reference in New Issue
Block a user