diff --git a/src/channels/allowlist-match.ts b/src/channels/allowlist-match.ts index 8c105f1e51b..e65f58e3c6b 100644 --- a/src/channels/allowlist-match.ts +++ b/src/channels/allowlist-match.ts @@ -1,3 +1,8 @@ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../shared/string-coerce.js"; + export type AllowlistMatchSource = | "wildcard" | "id" @@ -37,7 +42,9 @@ export function compileAllowlist(entries: ReadonlyArray): CompiledAllowl function compileSimpleAllowlist(entries: ReadonlyArray): CompiledAllowlist { return compileAllowlist( - entries.map((entry) => String(entry).trim().toLowerCase()).filter(Boolean), + entries + .map((entry) => normalizeOptionalLowercaseString(String(entry))) + .filter((entry): entry is string => Boolean(entry)), ); } @@ -98,8 +105,8 @@ export function resolveAllowlistMatchSimple(params: { return { allowed: true, matchKey: "*", matchSource: "wildcard" }; } - const senderId = params.senderId.toLowerCase(); - const senderName = params.senderName?.toLowerCase(); + const senderId = normalizeLowercaseStringOrEmpty(params.senderId); + const senderName = normalizeOptionalLowercaseString(params.senderName); return resolveAllowlistCandidates({ compiledAllowlist: allowFrom, candidates: [ diff --git a/src/channels/allowlists/resolve-utils.ts b/src/channels/allowlists/resolve-utils.ts index 84a3da97b5e..ded157d20fd 100644 --- a/src/channels/allowlists/resolve-utils.ts +++ b/src/channels/allowlists/resolve-utils.ts @@ -1,5 +1,6 @@ import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; import type { RuntimeEnv } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { summarizeStringEntries } from "../../shared/string-sample.js"; export type AllowlistUserResolutionLike = { @@ -16,7 +17,7 @@ function dedupeAllowlistEntries(entries: string[]): string[] { if (!normalized) { continue; } - const key = normalized.toLowerCase(); + const key = normalizeLowercaseStringOrEmpty(normalized); if (seen.has(key)) { continue; } diff --git a/src/channels/channel-config.ts b/src/channels/channel-config.ts index e13ecb96837..fd74617c012 100644 --- a/src/channels/channel-config.ts +++ b/src/channels/channel-config.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type ChannelMatchSource = "direct" | "parent" | "wildcard"; export type ChannelEntryMatch = { @@ -32,9 +34,7 @@ export function resolveChannelMatchConfig< } export function normalizeChannelSlug(value: string): string { - return value - .trim() - .toLowerCase() + return normalizeLowercaseStringOrEmpty(value) .replace(/^#/, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); diff --git a/src/channels/conversation-binding-context.ts b/src/channels/conversation-binding-context.ts index a7289f78ba6..ac5509e287d 100644 --- a/src/channels/conversation-binding-context.ts +++ b/src/channels/conversation-binding-context.ts @@ -2,6 +2,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js"; import { getActivePluginChannelRegistry } from "../plugins/runtime.js"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "../shared/string-coerce.js"; @@ -78,7 +79,7 @@ function resolveChannelTargetId(params: { return undefined; } - const lower = target.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(target); const channelPrefix = `${params.channel}:`; if (lower.startsWith(channelPrefix)) { return resolveChannelTargetId({ diff --git a/src/channels/native-command-session-targets.ts b/src/channels/native-command-session-targets.ts index 8d50029843b..5850f769e42 100644 --- a/src/channels/native-command-session-targets.ts +++ b/src/channels/native-command-session-targets.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type ResolveNativeCommandSessionTargetsParams = { agentId: string; sessionPrefix: string; @@ -13,7 +15,9 @@ export function resolveNativeCommandSessionTargets( const rawSessionKey = params.boundSessionKey ?? `agent:${params.agentId}:${params.sessionPrefix}:${params.userId}`; return { - sessionKey: params.lowercaseSessionKey ? rawSessionKey.toLowerCase() : rawSessionKey, + sessionKey: params.lowercaseSessionKey + ? normalizeLowercaseStringOrEmpty(rawSessionKey) + : rawSessionKey, commandTargetSessionKey: params.boundSessionKey ?? params.targetSessionKey, }; } diff --git a/src/channels/plugins/acp-configured-binding-consumer.ts b/src/channels/plugins/acp-configured-binding-consumer.ts index d453726b357..ed2bdfd2a2a 100644 --- a/src/channels/plugins/acp-configured-binding-consumer.ts +++ b/src/channels/plugins/acp-configured-binding-consumer.ts @@ -13,6 +13,10 @@ import { resolveDefaultAgentId, } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/config.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../../shared/string-coerce.js"; import type { ConfiguredBindingRuleConfig, ConfiguredBindingTargetFactory, @@ -26,8 +30,9 @@ function resolveAgentRuntimeAcpDefaults(params: { cfg: OpenClawConfig; ownerAgen cwd?: string; backend?: string; } { + const ownerAgentId = normalizeLowercaseStringOrEmpty(params.ownerAgentId); const agent = params.cfg.agents?.list?.find( - (entry) => entry.id?.trim().toLowerCase() === params.ownerAgentId.toLowerCase(), + (entry) => normalizeOptionalLowercaseString(entry.id) === ownerAgentId, ); if (!agent || agent.runtime?.type !== "acp") { return {}; diff --git a/src/channels/plugins/config-writes.ts b/src/channels/plugins/config-writes.ts index d764555b283..fc9891323ee 100644 --- a/src/channels/plugins/config-writes.ts +++ b/src/channels/plugins/config-writes.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../../config/config.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; import { authorizeConfigWriteShared, @@ -40,7 +41,7 @@ export function resolveExplicitConfigWriteTarget(scope: ConfigWriteScope): Confi export function resolveConfigWriteTargetFromPath(path: string[]): ConfigWriteTarget { return resolveConfigWriteTargetFromPathShared({ path, - normalizeChannelId: (raw) => raw.trim().toLowerCase() as ChannelId, + normalizeChannelId: (raw) => normalizeLowercaseStringOrEmpty(raw) as ChannelId, }); } diff --git a/src/channels/plugins/session-conversation.ts b/src/channels/plugins/session-conversation.ts index 60146950dcc..f3895c740a2 100644 --- a/src/channels/plugins/session-conversation.ts +++ b/src/channels/plugins/session-conversation.ts @@ -5,7 +5,10 @@ import { type ParsedThreadSessionSuffix, type RawSessionConversationRef, } from "../../sessions/session-key-utils.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { normalizeChannelId as normalizeChatChannelId } from "../registry.js"; import { getLoadedChannelPlugin, normalizeChannelId as normalizeAnyChannelId } from "./registry.js"; @@ -55,7 +58,8 @@ function normalizeResolvedChannel(channel: string): string { return ( normalizeAnyChannelId(channel) ?? normalizeChatChannelId(channel) ?? - channel.trim().toLowerCase() + normalizeOptionalLowercaseString(channel) ?? + "" ); } diff --git a/src/channels/targets.ts b/src/channels/targets.ts index b07f5a45ddf..d2b7ef941a6 100644 --- a/src/channels/targets.ts +++ b/src/channels/targets.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type { DirectoryConfigParams } from "./plugins/directory-types.js"; export type { ChannelDirectoryEntry } from "./plugins/types.js"; @@ -16,7 +18,7 @@ export type MessagingTargetParseOptions = { }; export function normalizeTargetId(kind: MessagingTargetKind, id: string): string { - return `${kind}:${id}`.toLowerCase(); + return normalizeLowercaseStringOrEmpty(`${kind}:${id}`); } export function buildMessagingTarget( diff --git a/src/channels/thread-bindings-policy.ts b/src/channels/thread-bindings-policy.ts index aade4d61d4a..100bf35e3f6 100644 --- a/src/channels/thread-bindings-policy.ts +++ b/src/channels/thread-bindings-policy.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; import { normalizeAccountId } from "../routing/session-key.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { getChannelPlugin } from "./plugins/index.js"; const DEFAULT_THREAD_BINDING_IDLE_HOURS = 24; @@ -28,9 +29,7 @@ export type ThreadBindingSpawnPolicy = { }; function normalizeChannelId(value: string | undefined | null): string { - return String(value ?? "") - .trim() - .toLowerCase(); + return normalizeLowercaseStringOrEmpty(value); } export function supportsAutomaticThreadBindingSpawn(channel: string): boolean { diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 401abf01873..0574db7ddd4 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -8,6 +8,7 @@ import type { ChannelId, ChannelPlugin, ChannelSetupInput } from "../../channels import { replaceConfigFile, type OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; +import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { createClackPrompter } from "../../wizard/clack-prompter.js"; import { applyAgentBindings, describeBinding } from "../agents.bindings.js"; import { isCatalogChannelInstalled } from "../channel-setup/discovery.js"; @@ -28,16 +29,18 @@ export type ChannelsAddOptions = { } & Omit; function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | null) { - const trimmed = raw.trim().toLowerCase(); + const trimmed = normalizeOptionalLowercaseString(raw); if (!trimmed) { return undefined; } const workspaceDir = cfg ? resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)) : undefined; return listChannelPluginCatalogEntries({ workspaceDir }).find((entry) => { - if (entry.id.toLowerCase() === trimmed) { + if (normalizeOptionalLowercaseString(entry.id) === trimmed) { return true; } - return (entry.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === trimmed); + return (entry.meta.aliases ?? []).some( + (alias) => normalizeOptionalLowercaseString(alias) === trimmed, + ); }); } diff --git a/src/commands/channels/capabilities.ts b/src/commands/channels/capabilities.ts index 325f6e8513a..f380aaf8888 100644 --- a/src/commands/channels/capabilities.ts +++ b/src/commands/channels/capabilities.ts @@ -18,7 +18,10 @@ import { import { danger } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; import { resolveInstallableChannelPlugin } from "../channel-setup/channel-plugin-resolution.js"; import { formatChannelAccountLabel, requireValidConfig } from "./shared.js"; @@ -220,7 +223,7 @@ export async function channelsCapabilitiesCommand( } let cfg = loadedCfg; const timeoutMs = normalizeTimeout(opts.timeout, 10_000); - const rawChannel = typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : ""; + const rawChannel = normalizeLowercaseStringOrEmpty(opts.channel); const rawTarget = typeof opts.target === "string" ? opts.target.trim() : ""; if (opts.account && (!rawChannel || rawChannel === "all")) { diff --git a/src/commands/channels/logs.ts b/src/commands/channels/logs.ts index e3863b9289a..d23af5cd5ad 100644 --- a/src/commands/channels/logs.ts +++ b/src/commands/channels/logs.ts @@ -3,6 +3,7 @@ import { listChannelPlugins } from "../../channels/plugins/index.js"; import { getResolvedLoggerSettings } from "../../logging.js"; import { parseLogLine } from "../../logging/parse-log-line.js"; import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; export type ChannelsLogsOptions = { @@ -20,7 +21,7 @@ const getChannelSet = () => new Set([...listChannelPlugins().map((plugin) => plugin.id), "all"]); function parseChannelFilter(raw?: string) { - const trimmed = raw?.trim().toLowerCase(); + const trimmed = normalizeLowercaseStringOrEmpty(raw); if (!trimmed) { return "all"; } diff --git a/src/commands/channels/resolve.ts b/src/commands/channels/resolve.ts index c3b7db9faff..4fe5004a6e4 100644 --- a/src/commands/channels/resolve.ts +++ b/src/commands/channels/resolve.ts @@ -6,6 +6,10 @@ import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../../con import { danger } from "../../globals.js"; import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../../shared/string-coerce.js"; import { resolveInstallableChannelPlugin } from "../channel-setup/channel-plugin-resolution.js"; export type ChannelsResolveOptions = { @@ -71,10 +75,10 @@ function detectAutoKindForPlugin( return generic; } const trimmed = input.trim(); - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); const prefixes = [plugin.id, ...(plugin.meta?.aliases ?? [])] - .map((entry) => entry.trim().toLowerCase()) - .filter(Boolean); + .map((entry) => normalizeOptionalLowercaseString(entry)) + .filter((entry): entry is string => Boolean(entry)); for (const prefix of prefixes) { if (!lowered.startsWith(`${prefix}:`)) { continue;