diff --git a/extensions/anthropic-vertex/region.ts b/extensions/anthropic-vertex/region.ts index d7407ba2c91..ca4e322093d 100644 --- a/extensions/anthropic-vertex/region.ts +++ b/extensions/anthropic-vertex/region.ts @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs"; import { homedir, platform } from "node:os"; import { join } from "node:path"; import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; const ANTHROPIC_VERTEX_DEFAULT_REGION = "global"; const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/; @@ -64,7 +65,10 @@ export function resolveAnthropicVertexClientRegion(params?: { function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean { const explicitMetadataOptIn = normalizeOptionalSecretInput(env.ANTHROPIC_VERTEX_USE_GCP_METADATA); - return explicitMetadataOptIn === "1" || explicitMetadataOptIn?.toLowerCase() === "true"; + return ( + explicitMetadataOptIn === "1" || + normalizeLowercaseStringOrEmpty(explicitMetadataOptIn) === "true" + ); } function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string { diff --git a/extensions/discord/src/media-detection.ts b/extensions/discord/src/media-detection.ts index aef9f64bfda..9afa48bd2f8 100644 --- a/extensions/discord/src/media-detection.ts +++ b/extensions/discord/src/media-detection.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + const DISCORD_VIDEO_MEDIA_EXTENSIONS = new Set([".avi", ".m4v", ".mkv", ".mov", ".mp4", ".webm"]); function normalizeMediaPathForExtension(mediaUrl: string): string { @@ -7,11 +9,11 @@ function normalizeMediaPathForExtension(mediaUrl: string): string { } try { const parsed = new URL(trimmed); - return parsed.pathname.toLowerCase(); + return normalizeLowercaseStringOrEmpty(parsed.pathname); } catch { const withoutHash = trimmed.split("#", 1)[0] ?? trimmed; const withoutQuery = withoutHash.split("?", 1)[0] ?? withoutHash; - return withoutQuery.toLowerCase(); + return normalizeLowercaseStringOrEmpty(withoutQuery); } } diff --git a/src/agents/model-ref-shared.ts b/src/agents/model-ref-shared.ts index 91fda4a8022..09d1a7466d7 100644 --- a/src/agents/model-ref-shared.ts +++ b/src/agents/model-ref-shared.ts @@ -2,6 +2,7 @@ import { normalizeGooglePreviewModelId, normalizeNativeXaiModelId, } from "../plugin-sdk/provider-model-id-normalize.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { normalizeProviderId } from "./provider-id.js"; export type StaticModelRef = { @@ -18,7 +19,9 @@ export function modelKey(provider: string, model: string): string { if (!modelId) { return providerId; } - return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`) + return normalizeLowercaseStringOrEmpty(modelId).startsWith( + `${normalizeLowercaseStringOrEmpty(providerId)}/`, + ) ? modelId : `${providerId}/${modelId}`; } @@ -28,7 +31,7 @@ export function normalizeAnthropicModelId(model: string): string { if (!trimmed) { return trimmed; } - switch (trimmed.toLowerCase()) { + switch (normalizeLowercaseStringOrEmpty(trimmed)) { case "opus-4.6": return "claude-opus-4-6"; case "opus-4.5": @@ -48,7 +51,9 @@ function normalizeHuggingfaceModelId(model: string): string { return trimmed; } const prefix = "huggingface/"; - return trimmed.toLowerCase().startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed; + return normalizeLowercaseStringOrEmpty(trimmed).startsWith(prefix) + ? trimmed.slice(prefix.length) + : trimmed; } export function normalizeStaticProviderModelId(provider: string, model: string): string { diff --git a/src/channels/plugins/module-loader.ts b/src/channels/plugins/module-loader.ts index cb246a4ef6f..44a4af7faa0 100644 --- a/src/channels/plugins/module-loader.ts +++ b/src/channels/plugins/module-loader.ts @@ -7,6 +7,7 @@ import { buildPluginLoaderJitiOptions, resolvePluginLoaderJitiConfig, } from "../../plugins/sdk-alias.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; const nodeRequire = createRequire(import.meta.url); @@ -36,7 +37,9 @@ function createModuleLoader() { let loadModule = createModuleLoader(); export function isJavaScriptModulePath(modulePath: string): boolean { - return [".js", ".mjs", ".cjs"].includes(path.extname(modulePath).toLowerCase()); + return [".js", ".mjs", ".cjs"].includes( + normalizeLowercaseStringOrEmpty(path.extname(modulePath)), + ); } export function resolveCompiledBundledModulePath(modulePath: string): string { diff --git a/src/plugin-sdk/anthropic-vertex-auth-presence.ts b/src/plugin-sdk/anthropic-vertex-auth-presence.ts index 4a3d2e068a0..79d72e5f6cb 100644 --- a/src/plugin-sdk/anthropic-vertex-auth-presence.ts +++ b/src/plugin-sdk/anthropic-vertex-auth-presence.ts @@ -1,7 +1,10 @@ import { readFileSync } from "node:fs"; import { homedir, platform } from "node:os"; import { join } from "node:path"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; const GCLOUD_DEFAULT_ADC_PATH = join( @@ -13,7 +16,10 @@ const GCLOUD_DEFAULT_ADC_PATH = join( function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean { const explicitMetadataOptIn = normalizeOptionalSecretInput(env.ANTHROPIC_VERTEX_USE_GCP_METADATA); - return explicitMetadataOptIn === "1" || explicitMetadataOptIn?.toLowerCase() === "true"; + return ( + explicitMetadataOptIn === "1" || + normalizeLowercaseStringOrEmpty(explicitMetadataOptIn) === "true" + ); } function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string { diff --git a/src/plugin-sdk/windows-spawn.ts b/src/plugin-sdk/windows-spawn.ts index 688d708a3be..735c6931ef4 100644 --- a/src/plugin-sdk/windows-spawn.ts +++ b/src/plugin-sdk/windows-spawn.ts @@ -1,6 +1,9 @@ import { readFileSync, statSync } from "node:fs"; import path from "node:path"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; export type WindowsSpawnResolution = | "direct" @@ -82,7 +85,9 @@ export function resolveWindowsExecutablePath(command: string, env: NodeJS.Proces for (const dir of pathEntries) { for (const ext of pathExt) { - for (const candidateExt of [ext, ext.toLowerCase(), ext.toUpperCase()]) { + const normalizedExt = normalizeLowercaseStringOrEmpty(ext); + const uppercaseExt = ext.toUpperCase(); + for (const candidateExt of [ext, normalizedExt, uppercaseExt]) { const candidate = path.join(dir, `${command}${candidateExt}`); if (isFilePath(candidate)) { return candidate; @@ -116,7 +121,7 @@ function resolveEntrypointFromCmdShim(wrapperPath: string): string | null { } } const nonNode = candidates.find((candidate) => { - const base = path.basename(candidate).toLowerCase(); + const base = normalizeLowercaseStringOrEmpty(path.basename(candidate)); return base !== "node.exe" && base !== "node"; }); return nonNode ?? null; @@ -211,7 +216,7 @@ export function resolveWindowsSpawnProgramCandidate( } const resolvedCommand = resolveWindowsExecutablePath(params.command, env); - const ext = path.extname(resolvedCommand).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(resolvedCommand)); if (ext === ".js" || ext === ".cjs" || ext === ".mjs") { return { command: execPath, @@ -226,7 +231,7 @@ export function resolveWindowsSpawnProgramCandidate( resolveEntrypointFromCmdShim(resolvedCommand) ?? resolveEntrypointFromPackageJson(resolvedCommand, params.packageName); if (entrypoint) { - const entryExt = path.extname(entrypoint).toLowerCase(); + const entryExt = normalizeLowercaseStringOrEmpty(path.extname(entrypoint)); if (entryExt === ".exe") { return { command: entrypoint, diff --git a/src/process/exec.ts b/src/process/exec.ts index ae22ecf4ea1..263dd76493c 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -6,6 +6,7 @@ import { promisify } from "node:util"; import { danger, shouldLogVerbose } from "../globals.js"; import { markOpenClawExecEnv } from "../infra/openclaw-exec-env.js"; import { logDebug, logError } from "../logger.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { resolveCommandStdio } from "./spawn-utils.js"; import { resolveWindowsCommandShim } from "./windows-command.js"; @@ -17,7 +18,7 @@ function isWindowsBatchCommand(resolvedCommand: string): boolean { if (process.platform !== "win32") { return false; } - const ext = path.extname(resolvedCommand).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(resolvedCommand)); return ext === ".cmd" || ext === ".bat"; } @@ -49,10 +50,10 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null { if (process.platform !== "win32" || argv.length === 0) { return null; } - const basename = path - .basename(argv[0]) - .toLowerCase() - .replace(/\.(cmd|exe|bat)$/, ""); + const basename = normalizeLowercaseStringOrEmpty(path.basename(argv[0])).replace( + /\.(cmd|exe|bat)$/, + "", + ); const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null; if (!cliName) { return null; @@ -64,7 +65,7 @@ function resolveNpmArgvForWindows(argv: string[]): string[] | null { // Fall back to npm.cmd/npx.cmd so we still route through cmd wrapper // (avoids direct .cmd spawn EINVAL on patched Node). const command = argv[0] ?? ""; - const ext = path.extname(command).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(command)); const shimmedCommand = ext ? command : `${command}.cmd`; return [shimmedCommand, ...argv.slice(1)]; } diff --git a/src/process/windows-command.ts b/src/process/windows-command.ts index c8e5981e2ef..6226c25529c 100644 --- a/src/process/windows-command.ts +++ b/src/process/windows-command.ts @@ -1,5 +1,6 @@ import path from "node:path"; import process from "node:process"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; export function resolveWindowsCommandShim(params: { command: string; @@ -9,7 +10,7 @@ export function resolveWindowsCommandShim(params: { if ((params.platform ?? process.platform) !== "win32") { return params.command; } - const basename = path.basename(params.command).toLowerCase(); + const basename = normalizeLowercaseStringOrEmpty(path.basename(params.command)); if (path.extname(basename)) { return params.command; }