From 56bc9b5058bd7af4ecbc97292025a11a02082270 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 00:42:42 +0000 Subject: [PATCH] refactor(zalo): share outbound chunker --- extensions/zalo/src/channel.ts | 56 ++++++------------------------ extensions/zalouser/src/channel.ts | 56 ++++++------------------------ src/plugin-sdk/allow-from.ts | 10 ++++++ src/plugin-sdk/config-paths.ts | 15 ++++++++ src/plugin-sdk/index.ts | 3 ++ src/plugin-sdk/text-chunking.ts | 31 +++++++++++++++++ 6 files changed, 81 insertions(+), 90 deletions(-) create mode 100644 src/plugin-sdk/allow-from.ts create mode 100644 src/plugin-sdk/config-paths.ts create mode 100644 src/plugin-sdk/text-chunking.ts diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 6bf61bf68ec..b7f9fce996d 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -9,10 +9,13 @@ import { buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, + chunkTextForOutbound, + formatAllowFromLowercase, formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, PAIRING_APPROVED_MESSAGE, + resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk"; import { @@ -63,11 +66,7 @@ export const zaloDock: ChannelDock = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(zalo|zl):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }), }, groups: { resolveRequireMention: () => true, @@ -124,19 +123,16 @@ export const zaloPlugin: ChannelPlugin = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(zalo|zl):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalo|zl):/i }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.zalo?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.zalo.accounts.${resolvedAccountId}.` - : "channels.zalo."; + const basePath = resolveChannelAccountConfigBasePath({ + cfg, + channelKey: "zalo", + accountId: resolvedAccountId, + }); return { policy: account.config.dmPolicy ?? "pairing", allowFrom: account.config.allowFrom ?? [], @@ -275,37 +271,7 @@ export const zaloPlugin: ChannelPlugin = { }, outbound: { deliveryMode: "direct", - chunker: (text, limit) => { - if (!text) { - return []; - } - if (limit <= 0 || text.length <= limit) { - return [text]; - } - const chunks: string[] = []; - let remaining = text; - while (remaining.length > limit) { - const window = remaining.slice(0, limit); - const lastNewline = window.lastIndexOf("\n"); - const lastSpace = window.lastIndexOf(" "); - let breakIdx = lastNewline > 0 ? lastNewline : lastSpace; - if (breakIdx <= 0) { - breakIdx = limit; - } - const rawChunk = remaining.slice(0, breakIdx); - const chunk = rawChunk.trimEnd(); - if (chunk.length > 0) { - chunks.push(chunk); - } - const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); - const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); - remaining = remaining.slice(nextStart).trimStart(); - } - if (remaining.length) { - chunks.push(remaining); - } - return chunks; - }, + chunker: chunkTextForOutbound, chunkerMode: "text", textChunkLimit: 2000, sendText: async ({ to, text, accountId, cfg }) => { diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 41cec8c561c..fcbc0140715 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -11,10 +11,13 @@ import { applyAccountNameToChannelSection, buildChannelConfigSchema, DEFAULT_ACCOUNT_ID, + chunkTextForOutbound, deleteAccountFromConfigSection, + formatAllowFromLowercase, formatPairingApproveHint, migrateBaseNameToDefaultAccount, normalizeAccountId, + resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk"; import type { ZcaFriend, ZcaGroup, ZcaUserInfo } from "./types.js"; @@ -117,11 +120,7 @@ export const zalouserDock: ChannelDock = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(zalouser|zlu):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }), }, groups: { resolveRequireMention: () => true, @@ -193,19 +192,16 @@ export const zalouserPlugin: ChannelPlugin = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(zalouser|zlu):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID; - const useAccountPath = Boolean(cfg.channels?.zalouser?.accounts?.[resolvedAccountId]); - const basePath = useAccountPath - ? `channels.zalouser.accounts.${resolvedAccountId}.` - : "channels.zalouser."; + const basePath = resolveChannelAccountConfigBasePath({ + cfg, + channelKey: "zalouser", + accountId: resolvedAccountId, + }); return { policy: account.config.dmPolicy ?? "pairing", allowFrom: account.config.allowFrom ?? [], @@ -519,37 +515,7 @@ export const zalouserPlugin: ChannelPlugin = { }, outbound: { deliveryMode: "direct", - chunker: (text, limit) => { - if (!text) { - return []; - } - if (limit <= 0 || text.length <= limit) { - return [text]; - } - const chunks: string[] = []; - let remaining = text; - while (remaining.length > limit) { - const window = remaining.slice(0, limit); - const lastNewline = window.lastIndexOf("\n"); - const lastSpace = window.lastIndexOf(" "); - let breakIdx = lastNewline > 0 ? lastNewline : lastSpace; - if (breakIdx <= 0) { - breakIdx = limit; - } - const rawChunk = remaining.slice(0, breakIdx); - const chunk = rawChunk.trimEnd(); - if (chunk.length > 0) { - chunks.push(chunk); - } - const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); - const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); - remaining = remaining.slice(nextStart).trimStart(); - } - if (remaining.length) { - chunks.push(remaining); - } - return chunks; - }, + chunker: chunkTextForOutbound, chunkerMode: "text", textChunkLimit: 2000, sendText: async ({ to, text, accountId, cfg }) => { diff --git a/src/plugin-sdk/allow-from.ts b/src/plugin-sdk/allow-from.ts new file mode 100644 index 00000000000..17188bc7d11 --- /dev/null +++ b/src/plugin-sdk/allow-from.ts @@ -0,0 +1,10 @@ +export function formatAllowFromLowercase(params: { + allowFrom: Array; + stripPrefixRe?: RegExp; +}): string[] { + return params.allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map((entry) => (params.stripPrefixRe ? entry.replace(params.stripPrefixRe, "") : entry)) + .map((entry) => entry.toLowerCase()); +} diff --git a/src/plugin-sdk/config-paths.ts b/src/plugin-sdk/config-paths.ts new file mode 100644 index 00000000000..06940f1842a --- /dev/null +++ b/src/plugin-sdk/config-paths.ts @@ -0,0 +1,15 @@ +import type { OpenClawConfig } from "../config/config.js"; + +export function resolveChannelAccountConfigBasePath(params: { + cfg: OpenClawConfig; + channelKey: string; + accountId: string; +}): string { + const channels = params.cfg.channels as unknown as Record | undefined; + const channelSection = channels?.[params.channelKey] as Record | undefined; + const accounts = channelSection?.accounts as Record | undefined; + const useAccountPath = Boolean(accounts?.[params.accountId]); + return useAccountPath + ? `channels.${params.channelKey}.accounts.${params.accountId}.` + : `channels.${params.channelKey}.`; +} diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 709ba3fb4c1..4b60ac0473f 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -126,6 +126,9 @@ export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; export type { RuntimeEnv } from "../runtime.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export { formatAllowFromLowercase } from "./allow-from.js"; +export { resolveChannelAccountConfigBasePath } from "./config-paths.js"; +export { chunkTextForOutbound } from "./text-chunking.js"; export type { ChatType } from "../channels/chat-type.js"; /** @deprecated Use ChatType instead */ export type { RoutePeerKind } from "../routing/resolve-route.js"; diff --git a/src/plugin-sdk/text-chunking.ts b/src/plugin-sdk/text-chunking.ts new file mode 100644 index 00000000000..3c86e43f6fd --- /dev/null +++ b/src/plugin-sdk/text-chunking.ts @@ -0,0 +1,31 @@ +export function chunkTextForOutbound(text: string, limit: number): string[] { + if (!text) { + return []; + } + if (limit <= 0 || text.length <= limit) { + return [text]; + } + const chunks: string[] = []; + let remaining = text; + while (remaining.length > limit) { + const window = remaining.slice(0, limit); + const lastNewline = window.lastIndexOf("\n"); + const lastSpace = window.lastIndexOf(" "); + let breakIdx = lastNewline > 0 ? lastNewline : lastSpace; + if (breakIdx <= 0) { + breakIdx = limit; + } + const rawChunk = remaining.slice(0, breakIdx); + const chunk = rawChunk.trimEnd(); + if (chunk.length > 0) { + chunks.push(chunk); + } + const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); + const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); + remaining = remaining.slice(nextStart).trimStart(); + } + if (remaining.length) { + chunks.push(remaining); + } + return chunks; +}