From 67dc6e82b90586a43e5bb82ed2c8185da846837c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 22:22:46 +0100 Subject: [PATCH] refactor: dedupe misc lowercase helpers --- extensions/nostr/src/nostr-bus.ts | 3 +- extensions/nostr/src/nostr-profile-http.ts | 7 +++-- extensions/openshell/src/backend.ts | 4 +-- extensions/openshell/src/mirror.ts | 5 ++-- extensions/phone-control/index.ts | 6 ++-- .../synology-chat/src/webhook-handler.ts | 3 +- extensions/zalouser/src/monitor.ts | 18 ++++++------ .../plugins/directory-config-helpers.ts | 2 +- src/channels/plugins/pairing.ts | 13 ++++----- src/chat/tool-content.ts | 4 ++- src/cron/run-log.ts | 2 +- src/hooks/message-hook-mappers.ts | 9 ++++-- .../approval-native-route-coordinator.ts | 7 +++-- src/logging/parse-log-line.ts | 4 ++- src/media-understanding/apply.ts | 7 +++-- src/media-understanding/runner.entries.ts | 3 +- src/media-understanding/runner.ts | 4 ++- src/pairing/setup-code.ts | 7 +++-- src/routing/resolve-route.ts | 28 +++++++++++-------- src/sessions/send-policy.ts | 9 ++++-- src/tasks/task-registry.maintenance.ts | 5 ++-- src/tts/directives.ts | 2 +- src/tts/tts-core.ts | 15 +++++----- src/video-generation/dashscope-compatible.ts | 3 +- src/web-search/runtime.ts | 13 +++++---- src/wizard/clack-prompter.ts | 6 ++-- 26 files changed, 112 insertions(+), 77 deletions(-) diff --git a/extensions/nostr/src/nostr-bus.ts b/extensions/nostr/src/nostr-bus.ts index e6e5cc210ed..6a10c6cb314 100644 --- a/extensions/nostr/src/nostr-bus.ts +++ b/extensions/nostr/src/nostr-bus.ts @@ -7,6 +7,7 @@ import { type Event, } from "nostr-tools"; import { decrypt, encrypt } from "nostr-tools/nip04"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { createDirectDmPreCryptoGuardPolicy, type DirectDmPreCryptoGuardPolicyOverrides, @@ -877,7 +878,7 @@ export function normalizePubkey(input: string): string { if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) { throw new Error("Pubkey must be 64 hex characters or npub format"); } - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } /** diff --git a/extensions/nostr/src/nostr-profile-http.ts b/extensions/nostr/src/nostr-profile-http.ts index e1772a1c5d4..a6b2d366ec4 100644 --- a/extensions/nostr/src/nostr-profile-http.ts +++ b/extensions/nostr/src/nostr-profile-http.ts @@ -9,6 +9,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, readStringValue, } from "openclaw/plugin-sdk/text-runtime"; @@ -114,7 +115,7 @@ function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: s return { ok: false, error: "URL must use https:// protocol" }; } - const hostname = url.hostname.toLowerCase(); + const hostname = normalizeLowercaseStringOrEmpty(url.hostname); if (isBlockedHostnameOrIp(hostname)) { return { ok: false, error: "URL must not point to private/internal addresses" }; @@ -197,7 +198,7 @@ function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean { return false; } - const ipLower = remoteAddress.toLowerCase().replace(/^\[|\]$/g, ""); + const ipLower = normalizeLowercaseStringOrEmpty(remoteAddress).replace(/^\[|\]$/g, ""); // IPv6 loopback if (ipLower === "::1") { @@ -221,7 +222,7 @@ function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean { function isLoopbackOriginLike(value: string): boolean { try { const url = new URL(value); - const hostname = url.hostname.toLowerCase(); + const hostname = normalizeLowercaseStringOrEmpty(url.hostname); return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; } catch { return false; diff --git a/extensions/openshell/src/backend.ts b/extensions/openshell/src/backend.ts index 6deaebe6076..2816ba3a0c4 100644 --- a/extensions/openshell/src/backend.ts +++ b/extensions/openshell/src/backend.ts @@ -19,6 +19,7 @@ import { runSshSandboxCommand, sanitizeEnvVars, } from "openclaw/plugin-sdk/sandbox"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { buildExecRemoteCommand, buildRemoteCommand, @@ -504,8 +505,7 @@ function resolveOpenShellPluginConfigFromConfig( function buildOpenShellSandboxName(scopeKey: string): string { const trimmed = scopeKey.trim() || "session"; - const safe = trimmed - .toLowerCase() + const safe = normalizeLowercaseStringOrEmpty(trimmed) .replace(/[^a-z0-9._-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 32); diff --git a/extensions/openshell/src/mirror.ts b/extensions/openshell/src/mirror.ts index f56edca8725..1884668f9fd 100644 --- a/extensions/openshell/src/mirror.ts +++ b/extensions/openshell/src/mirror.ts @@ -1,12 +1,13 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; export const DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS = ["hooks", "git-hooks", ".git"] as const; const COPY_TREE_FS_CONCURRENCY = 16; function createExcludeMatcher(excludeDirs?: readonly string[]) { - const excluded = new Set((excludeDirs ?? []).map((d) => d.toLowerCase())); - return (name: string) => excluded.has(name.toLowerCase()); + const excluded = new Set((excludeDirs ?? []).map((d) => normalizeLowercaseStringOrEmpty(d))); + return (name: string) => excluded.has(normalizeLowercaseStringOrEmpty(name)); } function createConcurrencyLimiter(limit: number) { diff --git a/extensions/phone-control/index.ts b/extensions/phone-control/index.ts index ebdfa0125c4..d55b618a34c 100644 --- a/extensions/phone-control/index.ts +++ b/extensions/phone-control/index.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import path from "node:path"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, - normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; import { definePluginEntry, @@ -260,7 +260,7 @@ function formatHelp(): string { } function parseGroup(raw: string | undefined): ArmGroup | null { - const value = normalizeOptionalString(raw)?.toLowerCase() ?? ""; + const value = normalizeOptionalLowercaseString(raw) ?? ""; if (!value) { return null; } @@ -354,7 +354,7 @@ export default definePluginEntry({ handler: async (ctx) => { const args = ctx.args?.trim() ?? ""; const tokens = args.split(/\s+/).filter(Boolean); - const action = tokens[0]?.toLowerCase() ?? ""; + const action = normalizeLowercaseStringOrEmpty(tokens[0]); const stateDir = api.runtime.state.resolveStateDir(); const statePath = resolveStatePath(stateDir); diff --git a/extensions/synology-chat/src/webhook-handler.ts b/extensions/synology-chat/src/webhook-handler.ts index 56e21a04e45..8064143255a 100644 --- a/extensions/synology-chat/src/webhook-handler.ts +++ b/extensions/synology-chat/src/webhook-handler.ts @@ -5,6 +5,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import * as querystring from "node:querystring"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, @@ -264,7 +265,7 @@ function extractTokenFromHeaders(req: IncomingMessage): string | undefined { * - text <- text | message | content */ function parsePayload(req: IncomingMessage, body: string): SynologyWebhookPayload | null { - const contentType = String(req.headers["content-type"] ?? "").toLowerCase(); + const contentType = normalizeLowercaseStringOrEmpty(req.headers["content-type"]); let bodyFields: Record = {}; if (contentType.includes("application/json")) { diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 0a7e71d2812..3d11ff85906 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -156,24 +156,24 @@ function resolveZalouserInboundSessionKey(params: { return params.route.sessionKey; } - const directSessionKey = params.core.channel.routing - .buildAgentSessionKey({ + const directSessionKey = normalizeLowercaseStringOrEmpty( + params.core.channel.routing.buildAgentSessionKey({ agentId: params.route.agentId, channel: "zalouser", accountId: params.route.accountId, peer: { kind: "direct", id: params.senderId }, dmScope: resolveZalouserDmSessionScope(params.config), identityLinks: params.config.session?.identityLinks, - }) - .toLowerCase(); - const legacySessionKey = params.core.channel.routing - .buildAgentSessionKey({ + }), + ); + const legacySessionKey = normalizeLowercaseStringOrEmpty( + params.core.channel.routing.buildAgentSessionKey({ agentId: params.route.agentId, channel: "zalouser", accountId: params.route.accountId, peer: { kind: "group", id: params.senderId }, - }) - .toLowerCase(); + }), + ); const hasDirectSession = params.core.channel.session.readSessionUpdatedAt({ storePath: params.storePath, @@ -844,7 +844,7 @@ export async function monitorZalouserProvider( mapping.push(`${entry}→${cleaned}`); continue; } - const matches = byName.get(cleaned.toLowerCase()) ?? []; + const matches = byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? []; const match = matches[0]; const id = match?.groupId ? String(match.groupId) : undefined; if (id) { diff --git a/src/channels/plugins/directory-config-helpers.ts b/src/channels/plugins/directory-config-helpers.ts index e2dd60bb677..a7498302089 100644 --- a/src/channels/plugins/directory-config-helpers.ts +++ b/src/channels/plugins/directory-config-helpers.ts @@ -17,7 +17,7 @@ export function applyDirectoryQueryAndLimit( ): string[] { const q = resolveDirectoryQuery(params.query); const limit = resolveDirectoryLimit(params.limit); - const filtered = ids.filter((id) => (q ? id.toLowerCase().includes(q) : true)); + const filtered = ids.filter((id) => (q ? normalizeLowercaseStringOrEmpty(id).includes(q) : true)); return typeof limit === "number" ? filtered.slice(0, limit) : filtered; } diff --git a/src/channels/plugins/pairing.ts b/src/channels/plugins/pairing.ts index f179ae6983e..9334c78a03d 100644 --- a/src/channels/plugins/pairing.ts +++ b/src/channels/plugins/pairing.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { type ChannelId, getChannelPlugin, @@ -29,20 +30,18 @@ export function requirePairingAdapter(channelId: ChannelId): ChannelPairingAdapt } export function resolvePairingChannel(raw: unknown): ChannelId { - const value = ( + const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) - : "" - ) - .trim() - .toLowerCase(); - const normalized = normalizeChannelId(value); + : ""; + const normalizedValue = normalizeLowercaseStringOrEmpty(value); + const normalized = normalizeChannelId(normalizedValue); const channels = listPairingChannels(); if (!normalized || !channels.includes(normalized)) { throw new Error( - `Invalid channel: ${value || "(empty)"} (expected one of: ${channels.join(", ")})`, + `Invalid channel: ${normalizedValue || "(empty)"} (expected one of: ${channels.join(", ")})`, ); } return normalized; diff --git a/src/chat/tool-content.ts b/src/chat/tool-content.ts index d93152e269a..0b229dfc80a 100644 --- a/src/chat/tool-content.ts +++ b/src/chat/tool-content.ts @@ -1,7 +1,9 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type ToolContentBlock = Record; export function normalizeToolContentType(value: unknown): string { - return typeof value === "string" ? value.toLowerCase() : ""; + return normalizeLowercaseStringOrEmpty(value); } export function isToolCallContentType(value: unknown): boolean { diff --git a/src/cron/run-log.ts b/src/cron/run-log.ts index bd7dd5bc73d..ef01c84f03e 100644 --- a/src/cron/run-log.ts +++ b/src/cron/run-log.ts @@ -351,7 +351,7 @@ function filterRunLogEntries( if (!opts.query) { return true; } - return opts.queryTextForEntry(entry).toLowerCase().includes(opts.query); + return normalizeLowercaseStringOrEmpty(opts.queryTextForEntry(entry)).includes(opts.query); }); } diff --git a/src/hooks/message-hook-mappers.ts b/src/hooks/message-hook-mappers.ts index 0d4ce036a67..f0d07cfa225 100644 --- a/src/hooks/message-hook-mappers.ts +++ b/src/hooks/message-hook-mappers.ts @@ -8,7 +8,10 @@ import type { PluginHookMessageReceivedEvent, PluginHookMessageSentEvent, } from "../plugins/types.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import type { MessagePreprocessedHookContext, MessageReceivedHookContext, @@ -76,7 +79,9 @@ export function deriveInboundMessageHookContext( : typeof ctx.Body === "string" ? ctx.Body : ""); - const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase(); + const channelId = normalizeLowercaseStringOrEmpty( + ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "", + ); const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined; const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel); const mediaPaths = Array.isArray(ctx.MediaPaths) diff --git a/src/infra/approval-native-route-coordinator.ts b/src/infra/approval-native-route-coordinator.ts index 0a935a4f2d2..7f6c2c1c597 100644 --- a/src/infra/approval-native-route-coordinator.ts +++ b/src/infra/approval-native-route-coordinator.ts @@ -1,5 +1,8 @@ import type { ChannelApprovalKind } from "../channels/plugins/types.adapters.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import type { ChannelApprovalNativeDeliveryPlan, ChannelApprovalNativePlannedTarget, @@ -61,7 +64,7 @@ let approvalRouteRuntimeSeq = 0; const MAX_APPROVAL_ROUTE_NOTICE_TTL_MS = 5 * 60_000; function normalizeChannel(value?: string | null): string { - return value?.trim().toLowerCase() || ""; + return normalizeLowercaseStringOrEmpty(value); } function clearPendingApprovalRouteNotice(approvalId: string): void { diff --git a/src/logging/parse-log-line.ts b/src/logging/parse-log-line.ts index 97623efa8ea..442357b8ef2 100644 --- a/src/logging/parse-log-line.ts +++ b/src/logging/parse-log-line.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; + export type ParsedLogLine = { time?: string; level?: string; @@ -51,7 +53,7 @@ export function parseLogLine(raw: string): ParsedLogLine | null { : typeof meta?.date === "string" ? meta.date : undefined, - level: levelRaw ? levelRaw.toLowerCase() : undefined, + level: normalizeOptionalLowercaseString(levelRaw), subsystem: nameMeta.subsystem, module: nameMeta.module, message: extractMessage(parsed), diff --git a/src/media-understanding/apply.ts b/src/media-understanding/apply.ts index bc5f0d6014c..3f18c3ec3fe 100644 --- a/src/media-understanding/apply.ts +++ b/src/media-understanding/apply.ts @@ -10,7 +10,10 @@ import { resolveInputFileLimits, } from "../media/input-files.js"; import { wrapExternalContent } from "../security/external-content.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../shared/string-coerce.js"; import { resolveAttachmentKind } from "./attachments.js"; import { runWithConcurrency } from "./concurrency.js"; import { DEFAULT_ECHO_TRANSCRIPT_FORMAT, sendTranscriptEcho } from "./echo-transcript.js"; @@ -285,7 +288,7 @@ function resolveTextMimeFromName(name?: string): string | undefined { if (!name) { return undefined; } - const ext = path.extname(name).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(name)); return TEXT_EXT_MIME.get(ext); } diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 2173f80649b..ff83a8c8e9d 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -21,6 +21,7 @@ import { resolveProxyFetchFromEnv } from "../infra/net/proxy-fetch.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { runFfmpeg } from "../media/ffmpeg-exec.js"; import { runExec } from "../process/exec.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { MediaAttachmentCache } from "./attachments.js"; import { CLI_OUTPUT_MAX_BUFFER, @@ -226,7 +227,7 @@ async function resolveCliMediaPath(params: { return params.mediaPath; } - const ext = path.extname(params.mediaPath).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(params.mediaPath)); if (ext === ".wav") { return params.mediaPath; } diff --git a/src/media-understanding/runner.ts b/src/media-understanding/runner.ts index 043a45f78cb..5b8266effe0 100644 --- a/src/media-understanding/runner.ts +++ b/src/media-understanding/runner.ts @@ -297,7 +297,9 @@ async function probeGeminiCli(): Promise { const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], { timeoutMs: 8000, }); - return Boolean(extractGeminiResponse(stdout) ?? stdout.toLowerCase().includes("ok")); + return Boolean( + extractGeminiResponse(stdout) ?? normalizeLowercaseStringOrEmpty(stdout).includes("ok"), + ); } catch { return false; } diff --git a/src/pairing/setup-code.ts b/src/pairing/setup-code.ts index 5560cabed7b..baee70439fc 100644 --- a/src/pairing/setup-code.ts +++ b/src/pairing/setup-code.ts @@ -20,7 +20,10 @@ import { isRfc1918Ipv4Address, parseCanonicalIpAddress, } from "../shared/net/ip.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js"; export type PairingSetupPayload = { @@ -101,7 +104,7 @@ function isPrivateLanIpHost(host: string): boolean { if (!isIpv6Address(parsed)) { return false; } - const normalized = parsed.toString().toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(parsed.toString()); return ( normalized.startsWith("fe80:") || normalized.startsWith("fc") || normalized.startsWith("fd") ); diff --git a/src/routing/resolve-route.ts b/src/routing/resolve-route.ts index a86513131a9..85eb8744d6a 100644 --- a/src/routing/resolve-route.ts +++ b/src/routing/resolve-route.ts @@ -678,18 +678,22 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR const choose = (agentId: string, matchedBy: ResolvedAgentRoute["matchedBy"]) => { const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId); - const sessionKey = buildAgentSessionKey({ - agentId: resolvedAgentId, - channel, - accountId, - peer, - dmScope, - identityLinks, - }).toLowerCase(); - const mainSessionKey = buildAgentMainSessionKey({ - agentId: resolvedAgentId, - mainKey: DEFAULT_MAIN_KEY, - }).toLowerCase(); + const sessionKey = normalizeLowercaseStringOrEmpty( + buildAgentSessionKey({ + agentId: resolvedAgentId, + channel, + accountId, + peer, + dmScope, + identityLinks, + }), + ); + const mainSessionKey = normalizeLowercaseStringOrEmpty( + buildAgentMainSessionKey({ + agentId: resolvedAgentId, + mainKey: DEFAULT_MAIN_KEY, + }), + ); const route = { agentId: resolvedAgentId, channel, diff --git a/src/sessions/send-policy.ts b/src/sessions/send-policy.ts index 0d1ad6e76ae..7757dcade4a 100644 --- a/src/sessions/send-policy.ts +++ b/src/sessions/send-policy.ts @@ -1,7 +1,10 @@ import { normalizeChatType } from "../channels/chat-type.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionChatType, SessionEntry } from "../config/sessions.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../shared/string-coerce.js"; export type SessionSendPolicyDecision = "allow" | "deny"; @@ -102,8 +105,8 @@ export function resolveSendPolicy(params: { normalizeChatType(deriveChatTypeFromKey(params.sessionKey)); const rawSessionKey = params.sessionKey ?? ""; const strippedSessionKey = stripAgentSessionKeyPrefix(rawSessionKey) ?? ""; - const rawSessionKeyNorm = rawSessionKey.toLowerCase(); - const strippedSessionKeyNorm = strippedSessionKey.toLowerCase(); + const rawSessionKeyNorm = normalizeLowercaseStringOrEmpty(rawSessionKey); + const strippedSessionKeyNorm = normalizeLowercaseStringOrEmpty(strippedSessionKey); let allowedMatch = false; for (const rule of policy.rules ?? []) { diff --git a/src/tasks/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts index 4a0079ada64..b104bef2167 100644 --- a/src/tasks/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -4,6 +4,7 @@ import { isCronJobActive } from "../cron/active-jobs.js"; import { getAgentRunContext } from "../infra/agent-events.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { deriveSessionChatType } from "../sessions/session-chat-type.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { deleteTaskRecordById, ensureTaskRegistryReady, @@ -81,9 +82,9 @@ function findSessionEntryByKey(store: Record, sessionKey: strin if (direct) { return direct; } - const normalized = sessionKey.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(sessionKey); for (const [key, entry] of Object.entries(store)) { - if (key.toLowerCase() === normalized) { + if (normalizeLowercaseStringOrEmpty(key) === normalized) { return entry; } } diff --git a/src/tts/directives.ts b/src/tts/directives.ts index ccf907493a6..4b74c513e2b 100644 --- a/src/tts/directives.ts +++ b/src/tts/directives.ts @@ -76,7 +76,7 @@ export function parseTtsDirectives( if (!rawKey || !rawValue) { continue; } - const key = rawKey.toLowerCase(); + const key = normalizeLowercaseStringOrEmpty(rawKey); if (key === "provider") { if (policy.allowProvider) { const providerId = normalizeLowercaseStringOrEmpty(rawValue); diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 656c81a1a70..df6e6e65045 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -10,7 +10,10 @@ import { import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "../agents/simple-completion-transport.js"; import type { OpenClawConfig } from "../config/config.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import type { ResolvedTtsConfig } from "./tts.js"; const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes @@ -40,11 +43,10 @@ export function requireInRange(value: number, min: number, max: number, label: s } export function normalizeLanguageCode(code?: string): string | undefined { - const trimmed = normalizeOptionalString(code); - if (!trimmed) { + const normalized = normalizeOptionalLowercaseString(code); + if (!normalized) { return undefined; } - const normalized = trimmed.toLowerCase(); if (!/^[a-z]{2}$/.test(normalized)) { throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); } @@ -52,11 +54,10 @@ export function normalizeLanguageCode(code?: string): string | undefined { } export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined { - const trimmed = normalizeOptionalString(mode); - if (!trimmed) { + const normalized = normalizeOptionalLowercaseString(mode); + if (!normalized) { return undefined; } - const normalized = trimmed.toLowerCase(); if (normalized === "auto" || normalized === "on" || normalized === "off") { return normalized; } diff --git a/src/video-generation/dashscope-compatible.ts b/src/video-generation/dashscope-compatible.ts index 632e489594a..eecf8112d6f 100644 --- a/src/video-generation/dashscope-compatible.ts +++ b/src/video-generation/dashscope-compatible.ts @@ -1,4 +1,5 @@ import { assertOkOrThrowHttpError, fetchWithTimeout } from "openclaw/plugin-sdk/provider-http"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import type { GeneratedVideoAsset, VideoGenerationRequest, @@ -139,7 +140,7 @@ export async function pollDashscopeVideoTaskUntilComplete(params: { throw new Error( payload.output?.message?.trim() || payload.message?.trim() || - `${params.providerLabel} video generation task ${params.taskId} ${status.toLowerCase()}`, + `${params.providerLabel} video generation task ${params.taskId} ${normalizeLowercaseStringOrEmpty(status)}`, ); } await new Promise((resolve) => setTimeout(resolve, DEFAULT_VIDEO_GENERATION_POLL_INTERVAL_MS)); diff --git a/src/web-search/runtime.ts b/src/web-search/runtime.ts index 70dd16b2eb5..e8c1562b829 100644 --- a/src/web-search/runtime.ts +++ b/src/web-search/runtime.ts @@ -9,7 +9,10 @@ import { resolveRuntimeWebSearchProviders } from "../plugins/web-search-provider import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js"; import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js"; import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../shared/string-coerce.js"; import { hasWebProviderEntryCredential, providerRequiresCredential, @@ -280,11 +283,9 @@ function hasExplicitWebSearchSelection(params: { if (configuredProviderId && availableProviderIds.has(configuredProviderId)) { return true; } - const runtimeConfiguredId = ( - params.runtimeWebSearch?.selectedProvider ?? params.runtimeWebSearch?.providerConfigured - ) - ?.trim() - .toLowerCase(); + const runtimeConfiguredId = normalizeOptionalLowercaseString( + params.runtimeWebSearch?.selectedProvider ?? params.runtimeWebSearch?.providerConfigured, + ); if ( params.runtimeWebSearch?.providerSource === "configured" && runtimeConfiguredId && diff --git a/src/wizard/clack-prompter.ts b/src/wizard/clack-prompter.ts index 998ed72e5fc..26addff29c0 100644 --- a/src/wizard/clack-prompter.ts +++ b/src/wizard/clack-prompter.ts @@ -12,6 +12,7 @@ import { text, } from "@clack/prompts"; import { createCliProgress } from "../cli/progress.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { stripAnsi } from "../terminal/ansi.js"; import { note as emitNote } from "../terminal/note.js"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; @@ -28,8 +29,7 @@ function guardCancel(value: T | symbol): T { } function normalizeSearchTokens(search: string): string[] { - return search - .toLowerCase() + return normalizeLowercaseStringOrEmpty(search) .split(/\s+/) .map((token) => token.trim()) .filter((token) => token.length > 0); @@ -39,7 +39,7 @@ function buildOptionSearchText(option: Option): string { const label = stripAnsi(option.label ?? ""); const hint = stripAnsi(option.hint ?? ""); const value = String(option.value ?? ""); - return `${label} ${hint} ${value}`.toLowerCase(); + return normalizeLowercaseStringOrEmpty(`${label} ${hint} ${value}`); } export function tokenizedOptionFilter(search: string, option: Option): boolean {