From ef903d881e57aba15f2587430bcb3c080e6d9a35 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 00:03:28 +0100 Subject: [PATCH] refactor: dedupe channel trimmed readers --- src/channels/allow-from.ts | 8 ++++---- src/channels/allowlists/resolve-utils.ts | 11 ++++++---- src/channels/plugins/account-helpers.ts | 6 ++---- src/channels/plugins/bootstrap-registry.ts | 5 +++-- src/channels/plugins/chat-target-prefixes.ts | 12 +++++++---- .../plugins/directory-config-helpers.ts | 13 +++++++----- src/channels/plugins/package-state-probes.ts | 5 +++-- src/channels/plugins/registry.ts | 7 ++++--- src/channels/plugins/setup-helpers.ts | 6 ++++-- src/channels/plugins/setup-registry.ts | 5 +++-- src/channels/plugins/setup-wizard-helpers.ts | 20 +++++++++---------- src/channels/plugins/setup-wizard.ts | 2 +- src/channels/plugins/status.ts | 3 ++- src/shared/node-resolve.ts | 3 ++- src/shared/string-normalization.ts | 4 ++-- 15 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/channels/allow-from.ts b/src/channels/allow-from.ts index 3e7591f2347..6a517145755 100644 --- a/src/channels/allow-from.ts +++ b/src/channels/allow-from.ts @@ -1,12 +1,12 @@ +import { normalizeStringEntries } from "../shared/string-normalization.js"; + export function mergeDmAllowFromSources(params: { allowFrom?: Array; storeAllowFrom?: Array; dmPolicy?: string; }): string[] { const storeEntries = params.dmPolicy === "allowlist" ? [] : (params.storeAllowFrom ?? []); - return [...(params.allowFrom ?? []), ...storeEntries] - .map((value) => String(value).trim()) - .filter(Boolean); + return normalizeStringEntries([...(params.allowFrom ?? []), ...storeEntries]); } export function resolveGroupAllowFromSources(params: { @@ -23,7 +23,7 @@ export function resolveGroupAllowFromSources(params: { : params.fallbackToAllowFrom === false ? [] : (params.allowFrom ?? []); - return scoped.map((value) => String(value).trim()).filter(Boolean); + return normalizeStringEntries(scoped); } export function firstDefined(...values: Array) { diff --git a/src/channels/allowlists/resolve-utils.ts b/src/channels/allowlists/resolve-utils.ts index ded157d20fd..e95c97c2660 100644 --- a/src/channels/allowlists/resolve-utils.ts +++ b/src/channels/allowlists/resolve-utils.ts @@ -1,6 +1,9 @@ import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; import type { RuntimeEnv } from "../../runtime.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { summarizeStringEntries } from "../../shared/string-sample.js"; export type AllowlistUserResolutionLike = { @@ -62,7 +65,7 @@ export function resolveAllowlistIdAdditions(params: { existing?: Array; resolvedMap: Map }): string[] { const canonicalized: string[] = []; for (const entry of params.existing ?? []) { - const trimmed = String(entry).trim(); + const trimmed = normalizeOptionalString(entry) ?? ""; if (!trimmed) { continue; } @@ -137,7 +140,7 @@ export function addAllowlistUserEntriesFromConfigEntry(target: Set, entr return; } for (const value of users) { - const trimmed = String(value).trim(); + const trimmed = normalizeOptionalString(value) ?? ""; if (trimmed && trimmed !== "*") { target.add(trimmed); } diff --git a/src/channels/plugins/account-helpers.ts b/src/channels/plugins/account-helpers.ts index d7b4e6e627c..f9fcbdc756d 100644 --- a/src/channels/plugins/account-helpers.ts +++ b/src/channels/plugins/account-helpers.ts @@ -8,6 +8,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { ChannelAccountSnapshot } from "./types.core.js"; export function createAccountListHelpers( @@ -188,10 +189,7 @@ export function describeAccountSnapshot< }): ChannelAccountSnapshot { return { accountId: String(params.account.accountId ?? DEFAULT_ACCOUNT_ID), - name: - typeof params.account.name === "string" && params.account.name.trim() - ? params.account.name - : undefined, + name: normalizeOptionalString(params.account.name), enabled: params.account.enabled !== false, configured: params.configured, ...params.extra, diff --git a/src/channels/plugins/bootstrap-registry.ts b/src/channels/plugins/bootstrap-registry.ts index faca55fa768..faf40b242c5 100644 --- a/src/channels/plugins/bootstrap-registry.ts +++ b/src/channels/plugins/bootstrap-registry.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { listBundledChannelPluginIds } from "./bundled-ids.js"; import { getBundledChannelPlugin, @@ -93,7 +94,7 @@ export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] { } export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefined { - const resolvedId = String(id).trim(); + const resolvedId = normalizeOptionalString(id) ?? ""; if (!resolvedId) { return undefined; } @@ -120,7 +121,7 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi } export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined { - const resolvedId = String(id).trim(); + const resolvedId = normalizeOptionalString(id) ?? ""; if (!resolvedId) { return undefined; } diff --git a/src/channels/plugins/chat-target-prefixes.ts b/src/channels/plugins/chat-target-prefixes.ts index fc6bc03bcd2..ea95a65feff 100644 --- a/src/channels/plugins/chat-target-prefixes.ts +++ b/src/channels/plugins/chat-target-prefixes.ts @@ -1,4 +1,8 @@ -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; +import { normalizeStringEntries } from "../../shared/string-normalization.js"; export type ServicePrefix = { prefix: string; service: TService }; @@ -34,7 +38,7 @@ function isAllowedParsedChatSender(params normalizeSender: (sender: string) => string; parseAllowTarget: (entry: string) => TParsed; }): boolean { - const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); + const allowFrom = normalizeStringEntries(params.allowFrom); if (allowFrom.length === 0) { return false; } @@ -44,8 +48,8 @@ function isAllowedParsedChatSender(params const senderNormalized = params.normalizeSender(params.sender); const chatId = params.chatId ?? undefined; - const chatGuid = params.chatGuid?.trim(); - const chatIdentifier = params.chatIdentifier?.trim(); + const chatGuid = normalizeOptionalString(params.chatGuid); + const chatIdentifier = normalizeOptionalString(params.chatIdentifier); for (const entry of allowFrom) { if (!entry) { diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts index a7498302089..d7f352603e6 100644 --- a/src/channels/plugins/directory-config-helpers.ts +++ b/src/channels/plugins/directory-config-helpers.ts @@ -1,5 +1,8 @@ import type { OpenClawConfig } from "../../config/types.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import type { DirectoryConfigParams } from "./directory-types.js"; import type { ChannelDirectoryEntry } from "./types.js"; @@ -30,11 +33,11 @@ function normalizeDirectoryIds(params: { normalizeId?: (entry: string) => string | null | undefined; }): string[] { return params.rawIds - .map((entry) => entry.trim()) + .map((entry) => normalizeOptionalString(entry) ?? "") .filter((entry) => Boolean(entry) && entry !== "*") .map((entry) => { const normalized = params.normalizeId ? params.normalizeId(entry) : entry; - return typeof normalized === "string" ? normalized.trim() : ""; + return normalizeOptionalString(normalized) ?? ""; }) .filter(Boolean); } @@ -70,12 +73,12 @@ export function collectNormalizedDirectoryIds(params: { const ids = new Set(); for (const source of params.sources) { for (const value of source) { - const raw = String(value).trim(); + const raw = normalizeOptionalString(value) ?? ""; if (!raw || raw === "*") { continue; } const normalized = params.normalizeId(raw); - const trimmed = typeof normalized === "string" ? normalized.trim() : ""; + const trimmed = normalizeOptionalString(normalized) ?? ""; if (trimmed) { ids.add(trimmed); } diff --git a/src/channels/plugins/package-state-probes.ts b/src/channels/plugins/package-state-probes.ts index 6d37eb82773..8e3ac06b60b 100644 --- a/src/channels/plugins/package-state-probes.ts +++ b/src/channels/plugins/package-state-probes.ts @@ -5,6 +5,7 @@ import { listChannelCatalogEntries, type PluginChannelCatalogEntry, } from "../../plugins/channel-catalog-registry.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { isJavaScriptModulePath, loadChannelPluginModule, @@ -40,8 +41,8 @@ function resolveChannelPackageStateMetadata( if (!metadata || typeof metadata !== "object") { return null; } - const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : ""; - const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : ""; + const specifier = normalizeOptionalString(metadata.specifier) ?? ""; + const exportName = normalizeOptionalString(metadata.exportName) ?? ""; if (!specifier || !exportName) { return null; } diff --git a/src/channels/plugins/registry.ts b/src/channels/plugins/registry.ts index bf75af9a519..565ed85658f 100644 --- a/src/channels/plugins/registry.ts +++ b/src/channels/plugins/registry.ts @@ -2,6 +2,7 @@ import { getActivePluginChannelRegistryVersion, requireActivePluginChannelRegistry, } from "../../plugins/runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js"; import { getBundledChannelPlugin } from "./bundled.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; @@ -10,7 +11,7 @@ function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] { const seen = new Set(); const resolved: ChannelPlugin[] = []; for (const plugin of channels) { - const id = String(plugin.id).trim(); + const id = normalizeOptionalString(plugin.id) ?? ""; if (!id || seen.has(id)) { continue; } @@ -83,7 +84,7 @@ export function listChannelPlugins(): ChannelPlugin[] { } export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined { - const resolvedId = String(id).trim(); + const resolvedId = normalizeOptionalString(id) ?? ""; if (!resolvedId) { return undefined; } @@ -91,7 +92,7 @@ export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined } export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined { - const resolvedId = String(id).trim(); + const resolvedId = normalizeOptionalString(id) ?? ""; if (!resolvedId) { return undefined; } diff --git a/src/channels/plugins/setup-helpers.ts b/src/channels/plugins/setup-helpers.ts index 016f645c4cd..a4e9235555f 100644 --- a/src/channels/plugins/setup-helpers.ts +++ b/src/channels/plugins/setup-helpers.ts @@ -1,6 +1,7 @@ import { z, type ZodType } from "zod"; import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { getBundledChannelPlugin } from "./bundled.js"; import { getChannelPlugin } from "./registry.js"; import type { ChannelSetupAdapter } from "./types.adapters.js"; @@ -499,8 +500,9 @@ export function resolveSingleAccountPromotionTarget(params: { const resolved = surface?.resolveSingleAccountPromotionTarget?.({ channel: params.channel, }); - if (typeof resolved === "string" && resolved.trim()) { - return resolveExistingAccountId(resolved); + const normalizedResolved = normalizeOptionalString(resolved); + if (normalizedResolved) { + return resolveExistingAccountId(normalizedResolved); } return resolveExistingAccountId(DEFAULT_ACCOUNT_ID); } diff --git a/src/channels/plugins/setup-registry.ts b/src/channels/plugins/setup-registry.ts index 1867f0db377..15f0b480259 100644 --- a/src/channels/plugins/setup-registry.ts +++ b/src/channels/plugins/setup-registry.ts @@ -2,6 +2,7 @@ import { getActivePluginRegistryVersion, requireActivePluginRegistry, } from "../../plugins/runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js"; import { listBundledChannelSetupPlugins } from "./bundled.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; @@ -26,7 +27,7 @@ function dedupeSetupPlugins(plugins: readonly ChannelPlugin[]): ChannelPlugin[] const seen = new Set(); const resolved: ChannelPlugin[] = []; for (const plugin of plugins) { - const id = String(plugin.id).trim(); + const id = normalizeOptionalString(plugin.id) ?? ""; if (!id || seen.has(id)) { continue; } @@ -81,7 +82,7 @@ export function listChannelSetupPlugins(): ChannelPlugin[] { } export function getChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined { - const resolvedId = String(id).trim(); + const resolvedId = normalizeOptionalString(id) ?? ""; if (!resolvedId) { return undefined; } diff --git a/src/channels/plugins/setup-wizard-helpers.ts b/src/channels/plugins/setup-wizard-helpers.ts index 2c038a2622a..f6e825def9d 100644 --- a/src/channels/plugins/setup-wizard-helpers.ts +++ b/src/channels/plugins/setup-wizard-helpers.ts @@ -4,6 +4,7 @@ import type { SecretInput } from "../../config/types.secrets.js"; import { resolveSecretInputModeForEnvSelection } from "../../plugins/provider-auth-mode.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { normalizeStringEntries } from "../../shared/string-normalization.js"; import type { WizardPrompter } from "../../wizard/prompts.js"; import { moveSingleAccountChannelSectionToDefaultAccount, @@ -50,10 +51,10 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa const entered = await params.prompter.text({ message: `New ${params.label} account id`, - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), }); const normalized = normalizeAccountId(String(entered)); - if (String(entered).trim() !== normalized) { + if ((normalizeOptionalString(entered) ?? "") !== normalized) { await params.prompter.note( `Normalized account id to "${normalized}".`, `${params.label} account`, @@ -63,7 +64,7 @@ export const promptAccountId: PromptAccountId = async (params: PromptAccountIdPa }; export function addWildcardAllowFrom(allowFrom?: ReadonlyArray | null): string[] { - const next = (allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean); + const next = normalizeStringEntries(allowFrom ?? []); if (!next.includes("*")) { next.push("*"); } @@ -74,7 +75,7 @@ export function mergeAllowFromEntries( current: Array | null | undefined, additions: Array, ): string[] { - const merged = [...(current ?? []), ...additions].map((v) => String(v).trim()).filter(Boolean); + const merged = normalizeStringEntries([...(current ?? []), ...additions]); return [...new Set(merged)]; } @@ -144,9 +145,7 @@ export function normalizeAllowFromEntries( entries: Array, normalizeEntry?: (value: string) => string | null | undefined, ): string[] { - const normalized = entries - .map((entry) => String(entry).trim()) - .filter(Boolean) + const normalized = normalizeStringEntries(entries) .map((entry) => { if (entry === "*") { return "*"; @@ -154,8 +153,7 @@ export function normalizeAllowFromEntries( if (!normalizeEntry) { return entry; } - const value = normalizeEntry(entry); - return typeof value === "string" ? value.trim() : ""; + return normalizeOptionalString(normalizeEntry(entry)) ?? ""; }) .filter(Boolean); return [...new Set(normalized)]; @@ -1215,7 +1213,7 @@ export async function promptParsedAllowFromForAccount { - const raw = String(value ?? "").trim(); + const raw = normalizeOptionalString(value) ?? ""; if (!raw) { return "Required"; } @@ -1517,7 +1515,7 @@ export async function promptResolvedAllowFrom(params: { message: params.message, placeholder: params.placeholder, initialValue: params.existing[0] ? String(params.existing[0]) : undefined, - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + validate: (value) => (normalizeOptionalString(value) ? undefined : "Required"), }); const parts = params.parseInputs(String(entry)); if (!params.token) { diff --git a/src/channels/plugins/setup-wizard.ts b/src/channels/plugins/setup-wizard.ts index 1aa75f22d57..40ec73e0196 100644 --- a/src/channels/plugins/setup-wizard.ts +++ b/src/channels/plugins/setup-wizard.ts @@ -693,7 +693,7 @@ export function buildChannelSetupWizardAdapterFromSetupWizard(params: { initialValue, placeholder: textInput.placeholder, validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed && textInput.required !== false) { return "Required"; } diff --git a/src/channels/plugins/status.ts b/src/channels/plugins/status.ts index a508256cab5..f37731d22d8 100644 --- a/src/channels/plugins/status.ts +++ b/src/channels/plugins/status.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../../config/config.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { projectSafeChannelAccountSnapshotFields } from "../account-snapshot-fields.js"; import { inspectReadOnlyChannelAccount } from "../read-only-account-inspect.js"; import type { ChannelAccountSnapshot, ChannelPlugin } from "./types.js"; @@ -21,7 +22,7 @@ async function buildSnapshotFromAccount(params: { probe: params.probe, audit: params.audit, }); - return typeof snapshot.accountId === "string" && snapshot.accountId.trim().length > 0 + return normalizeOptionalString(snapshot.accountId) ? snapshot : { ...snapshot, diff --git a/src/shared/node-resolve.ts b/src/shared/node-resolve.ts index 6546dab6d62..93b14f9c7fc 100644 --- a/src/shared/node-resolve.ts +++ b/src/shared/node-resolve.ts @@ -1,4 +1,5 @@ import { type NodeMatchCandidate, resolveNodeIdFromCandidates } from "./node-match.js"; +import { normalizeOptionalString } from "./string-coerce.js"; type ResolveNodeFromListOptions = { allowDefault?: boolean; @@ -10,7 +11,7 @@ export function resolveNodeIdFromNodeList( query?: string, options: ResolveNodeFromListOptions = {}, ): string { - const q = String(query ?? "").trim(); + const q = normalizeOptionalString(query) ?? ""; if (!q) { if (options.allowDefault === true && options.pickDefaultNode) { const picked = options.pickDefaultNode(nodes); diff --git a/src/shared/string-normalization.ts b/src/shared/string-normalization.ts index 2bc97276c72..f9e8eec03ec 100644 --- a/src/shared/string-normalization.ts +++ b/src/shared/string-normalization.ts @@ -1,7 +1,7 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString } from "./string-coerce.js"; export function normalizeStringEntries(list?: ReadonlyArray) { - return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); + return (list ?? []).map((entry) => normalizeOptionalString(String(entry)) ?? "").filter(Boolean); } export function normalizeStringEntriesLower(list?: ReadonlyArray) { @@ -40,7 +40,7 @@ export function normalizeSingleOrTrimmedStringList(value: unknown): string[] { export function normalizeCsvOrLooseStringList(value: unknown): string[] { if (Array.isArray(value)) { - return value.map((entry) => String(entry).trim()).filter(Boolean); + return normalizeStringEntries(value); } if (typeof value === "string") { return value