refactor(zalo): share outbound chunker

This commit is contained in:
Peter Steinberger
2026-02-15 00:42:42 +00:00
parent 0d0ebd0e20
commit 56bc9b5058
6 changed files with 81 additions and 90 deletions

View File

@@ -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<ResolvedZaloAccount> = {
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<ResolvedZaloAccount> = {
},
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 }) => {

View File

@@ -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<ResolvedZalouserAccount> = {
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<ResolvedZalouserAccount> = {
},
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 }) => {

View File

@@ -0,0 +1,10 @@
export function formatAllowFromLowercase(params: {
allowFrom: Array<string | number>;
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());
}

View File

@@ -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<string, unknown> | undefined;
const channelSection = channels?.[params.channelKey] as Record<string, unknown> | undefined;
const accounts = channelSection?.accounts as Record<string, unknown> | undefined;
const useAccountPath = Boolean(accounts?.[params.accountId]);
return useAccountPath
? `channels.${params.channelKey}.accounts.${params.accountId}.`
: `channels.${params.channelKey}.`;
}

View File

@@ -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";

View File

@@ -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;
}