diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index d04b709dad7..b7a9eaa756f 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -13,7 +13,10 @@ import type { ModelDefinitionConfig, ModelProviderConfig, } from "openclaw/plugin-sdk/provider-model-shared"; -import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; const log = createSubsystemLogger("bedrock-discovery"); @@ -69,7 +72,7 @@ function buildCacheKey(params: { } function includesTextModalities(modalities?: Array): boolean { - return (modalities ?? []).some((entry) => entry.toLowerCase() === "text"); + return (modalities ?? []).some((entry) => normalizeOptionalLowercaseString(entry) === "text"); } function isActive(summary: BedrockModelSummary): boolean { @@ -81,7 +84,7 @@ function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image const inputs = summary.inputModalities ?? []; const mapped = new Set<"text" | "image">(); for (const modality of inputs) { - const lower = modality.toLowerCase(); + const lower = normalizeOptionalLowercaseString(modality); if (lower === "text") { mapped.add("text"); } @@ -96,7 +99,9 @@ function mapInputModalities(summary: BedrockModelSummary): Array<"text" | "image } function inferReasoningSupport(summary: BedrockModelSummary): boolean { - const haystack = `${summary.modelId ?? ""} ${summary.modelName ?? ""}`.toLowerCase(); + const haystack = normalizeLowercaseStringOrEmpty( + `${summary.modelId ?? ""} ${summary.modelName ?? ""}`, + ); return haystack.includes("reasoning") || haystack.includes("thinking"); } @@ -256,7 +261,9 @@ function resolveInferenceProfiles( const models = profile.models ?? []; const matchesFilter = models.some((m) => { const provider = m.modelArn?.split("/")?.[1]?.split(".")?.[0]; - return provider ? providerFilter.includes(provider.toLowerCase()) : false; + return provider + ? providerFilter.includes(normalizeOptionalLowercaseString(provider) ?? "") + : false; }); if (!matchesFilter) { continue; @@ -265,7 +272,9 @@ function resolveInferenceProfiles( // Look up the underlying foundation model to inherit its capabilities. const baseModelId = resolveBaseModelId(profile); - const baseModel = baseModelId ? foundationModels.get(baseModelId.toLowerCase()) : undefined; + const baseModel = baseModelId + ? foundationModels.get(normalizeLowercaseStringOrEmpty(baseModelId)) + : undefined; discovered.push({ id: profile.inferenceProfileId, @@ -356,8 +365,9 @@ export async function discoverBedrockModels(params: { maxTokens: defaultMaxTokens, }); discovered.push(def); - seenIds.add(def.id.toLowerCase()); - foundationModels.set(def.id.toLowerCase(), def); + const normalizedId = normalizeLowercaseStringOrEmpty(def.id); + seenIds.add(normalizedId); + foundationModels.set(normalizedId, def); } // Merge inference profiles — inherit capabilities from foundation models. @@ -368,9 +378,10 @@ export async function discoverBedrockModels(params: { foundationModels, ); for (const profile of inferenceProfiles) { - if (!seenIds.has(profile.id.toLowerCase())) { + const normalizedId = normalizeLowercaseStringOrEmpty(profile.id); + if (!seenIds.has(normalizedId)) { discovered.push(profile); - seenIds.add(profile.id.toLowerCase()); + seenIds.add(normalizedId); } } diff --git a/extensions/line/src/card-command.ts b/extensions/line/src/card-command.ts index 1fb02e8adb9..2f3574470ff 100644 --- a/extensions/line/src/card-command.ts +++ b/extensions/line/src/card-command.ts @@ -1,5 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { createActionCard, createImageCard, @@ -136,7 +137,7 @@ function parseCardArgs(argsStr: string): { // Extract type (first word) const typeMatch = argsStr.match(/^(\w+)/); if (typeMatch) { - result.type = typeMatch[1].toLowerCase(); + result.type = normalizeLowercaseStringOrEmpty(typeMatch[1]); argsStr = argsStr.slice(typeMatch[0].length).trim(); } diff --git a/extensions/line/src/outbound-media.ts b/extensions/line/src/outbound-media.ts index 88eab9317da..1bcb5ba79a6 100644 --- a/extensions/line/src/outbound-media.ts +++ b/extensions/line/src/outbound-media.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + export type LineOutboundMediaKind = "image" | "video" | "audio"; export type LineOutboundMediaResolved = { @@ -31,7 +33,7 @@ export function validateLineMediaUrl(url: string): void { } export function detectLineMediaKind(mimeType: string): LineOutboundMediaKind { - const normalized = mimeType.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(mimeType); if (normalized.startsWith("image/")) { return "image"; } @@ -54,7 +56,7 @@ function isHttpsUrl(url: string): boolean { function detectLineMediaKindFromUrl(url: string): LineOutboundMediaKind | undefined { try { - const pathname = new URL(url).pathname.toLowerCase(); + const pathname = normalizeLowercaseStringOrEmpty(new URL(url).pathname); if (/\.(png|jpe?g|gif|webp|bmp|heic|heif|avif)$/i.test(pathname)) { return "image"; } diff --git a/extensions/line/src/reply-payload-transform.ts b/extensions/line/src/reply-payload-transform.ts index 7375592cd3d..85f88e57420 100644 --- a/extensions/line/src/reply-payload-transform.ts +++ b/extensions/line/src/reply-payload-transform.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { ReplyPayload } from "../runtime-api.js"; import { createAgendaCard, @@ -33,8 +34,7 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { ...(result.channelData?.line as LineChannelData | undefined), }; const toSlug = (value: string): string => - value - .toLowerCase() + normalizeLowercaseStringOrEmpty(value) .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") || "device"; const lineActionData = (action: string, extras?: Record): string => { @@ -85,10 +85,10 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const [question, yesPart, noPart] = parts; const [yesLabel, yesData] = yesPart.includes(":") ? yesPart.split(":").map((s) => s.trim()) - : [yesPart, yesPart.toLowerCase()]; + : [yesPart, normalizeLowercaseStringOrEmpty(yesPart)]; const [noLabel, noData] = noPart.includes(":") ? noPart.split(":").map((s) => s.trim()) - : [noPart, noPart.toLowerCase()]; + : [noPart, normalizeLowercaseStringOrEmpty(noPart)]; lineData.templateMessage = { type: "confirm", @@ -116,7 +116,7 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { if (index === -1) { return -1; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); if (lower.startsWith("http://") || lower.startsWith("https://")) { return -1; } @@ -161,7 +161,7 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const parts = mediaPlayerMatch[1].split("|").map((s) => s.trim()); if (parts.length >= 1) { const [title, artist, source, imageUrl, statusStr] = parts; - const isPlaying = statusStr?.toLowerCase() === "playing"; + const isPlaying = normalizeLowercaseStringOrEmpty(statusStr) === "playing"; const validImageUrl = imageUrl?.startsWith("https://") ? imageUrl : undefined; const deviceKey = toSlug(source || title || "media"); const card = createMediaPlayerCard({ @@ -281,7 +281,7 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload { const controls = controlsStr ? controlsStr.split(",").map((ctrlStr) => { const [label, data] = ctrlStr.split(":").map((s) => s.trim()); - const action = data || label.toLowerCase().replace(/\s+/g, "_"); + const action = data || normalizeLowercaseStringOrEmpty(label).replace(/\s+/g, "_"); return { label, data: lineActionData(action, { "line.device": deviceKey }) }; }) : []; diff --git a/extensions/line/src/rich-menu.ts b/extensions/line/src/rich-menu.ts index ed4872d3991..f344b78c354 100644 --- a/extensions/line/src/rich-menu.ts +++ b/extensions/line/src/rich-menu.ts @@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises"; import { messagingApi } from "@line/bot-sdk"; import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { resolveLineAccount } from "./accounts.js"; import { datetimePickerAction, messageAction, postbackAction, uriAction } from "./actions.js"; import { resolveLineChannelAccessToken } from "./channel-access-token.js"; @@ -104,7 +105,9 @@ export async function uploadRichMenuImage( const blobClient = getBlobClient(opts); const imageData = await readFile(imagePath); - const contentType = imagePath.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg"; + const contentType = normalizeLowercaseStringOrEmpty(imagePath).endsWith(".png") + ? "image/png" + : "image/jpeg"; await blobClient.setRichMenuImage(richMenuId, new Blob([imageData], { type: contentType })); diff --git a/extensions/qqbot/src/gateway.ts b/extensions/qqbot/src/gateway.ts index b185b3a6265..e40ea89a04c 100644 --- a/extensions/qqbot/src/gateway.ts +++ b/extensions/qqbot/src/gateway.ts @@ -1,5 +1,6 @@ import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import WebSocket from "ws"; import { clearTokenCache, @@ -242,10 +243,11 @@ export async function startGateway(ctx: GatewayContext): Promise { return; } - const contentLower = content.toLowerCase(); + const contentLower = normalizeLowercaseStringOrEmpty(content); const isUrgentCommand = URGENT_COMMANDS.some( (cmd) => - contentLower === cmd.toLowerCase() || contentLower.startsWith(cmd.toLowerCase() + " "), + contentLower === normalizeLowercaseStringOrEmpty(cmd) || + contentLower.startsWith(normalizeLowercaseStringOrEmpty(cmd) + " "), ); if (isUrgentCommand) { log?.info( diff --git a/extensions/qqbot/src/outbound-deliver.ts b/extensions/qqbot/src/outbound-deliver.ts index 3340561ef8d..b1445f9976c 100644 --- a/extensions/qqbot/src/outbound-deliver.ts +++ b/extensions/qqbot/src/outbound-deliver.ts @@ -6,6 +6,7 @@ * 2. `sendPlainReply` handles plain replies, including markdown images and mixed text/media. */ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { sendC2CMessage, sendDmMessage, @@ -151,7 +152,7 @@ export async function parseAndSendMediaTags( const tagCounts = mediaTagMatches.reduce( (acc, m) => { - const t = m[1].toLowerCase(); + const t = normalizeLowercaseStringOrEmpty(m[1]); acc[t] = (acc[t] ?? 0) + 1; return acc; }, @@ -184,7 +185,7 @@ export async function parseAndSendMediaTags( sendQueue.push({ type: "text", content: filterInternalMarkers(textBefore) }); } - const tagName = match[1].toLowerCase(); + const tagName = normalizeLowercaseStringOrEmpty(match[1]); let mediaPath = decodeMediaPath(match[2]?.trim() ?? "", log, prefix); if (mediaPath) { diff --git a/extensions/qqbot/src/outbound.ts b/extensions/qqbot/src/outbound.ts index 1d4b77f8bb2..09e38c0eff0 100644 --- a/extensions/qqbot/src/outbound.ts +++ b/extensions/qqbot/src/outbound.ts @@ -1,5 +1,6 @@ import * as path from "path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { getAccessToken, sendC2CFileMessage, @@ -303,7 +304,7 @@ export async function sendPhoto( return { channel: "qqbot", error: sizeCheck.error! }; } const fileBuffer = await readFileAsync(mediaPath); - const ext = path.extname(mediaPath).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(mediaPath)); const mimeTypes: Record = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", @@ -482,7 +483,7 @@ async function sendVoiceFromLocal( const needsTranscode = shouldTranscodeVoice(mediaPath); if (needsTranscode && !transcodeEnabled) { - const ext = path.extname(mediaPath).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(mediaPath)); debugLog( `${prefix} sendVoice: transcode disabled, format ${ext} needs transcode, returning error for fallback`, ); @@ -886,7 +887,7 @@ export async function sendText(ctx: OutboundContext): Promise { sendQueue.push({ type: "text", content: textBefore }); } - const tagName = match[1].toLowerCase(); + const tagName = normalizeLowercaseStringOrEmpty(match[1]); let mediaPath = match[2]?.trim() ?? ""; if (mediaPath.startsWith("MEDIA:")) { @@ -1368,7 +1369,7 @@ async function sendTextAfterMedia(ctx: MediaTargetContext, text: string): Promis /** Extract a lowercase extension from a path or URL, ignoring query and hash segments. */ function getCleanExt(filePath: string): string { const cleanPath = filePath.split("?")[0].split("#")[0]; - return path.extname(cleanPath).toLowerCase(); + return normalizeLowercaseStringOrEmpty(path.extname(cleanPath)); } /** Check whether a file is an image using MIME first and extension as fallback. */ diff --git a/extensions/qqbot/src/slash-commands.ts b/extensions/qqbot/src/slash-commands.ts index fd63e81821b..f976919b8dc 100644 --- a/extensions/qqbot/src/slash-commands.ts +++ b/extensions/qqbot/src/slash-commands.ts @@ -11,6 +11,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { QQBotAccountConfig } from "./types.js"; import { debugLog } from "./utils/debug-log.js"; import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js"; @@ -134,9 +135,9 @@ const frameworkCommands: Map = new Map(); function registerCommand(cmd: SlashCommand): void { if (cmd.requireAuth) { - frameworkCommands.set(cmd.name.toLowerCase(), cmd); + frameworkCommands.set(normalizeLowercaseStringOrEmpty(cmd.name), cmd); } else { - commands.set(cmd.name.toLowerCase(), cmd); + commands.set(normalizeLowercaseStringOrEmpty(cmd.name), cmd); } } @@ -604,7 +605,9 @@ export async function matchSlashCommand(ctx: SlashCommandContext): Promise = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", diff --git a/extensions/qqbot/src/utils/media-tags.ts b/extensions/qqbot/src/utils/media-tags.ts index 94ee1d2b038..1940f504ff2 100644 --- a/extensions/qqbot/src/utils/media-tags.ts +++ b/extensions/qqbot/src/utils/media-tags.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { expandTilde } from "./platform.js"; // Canonical media tags. `qqmedia` is the generic auto-routing tag. @@ -84,7 +85,7 @@ const FUZZY_MEDIA_TAG_REGEX = new RegExp( /** Normalize a raw tag name into the canonical tag set. */ function resolveTagName(raw: string): (typeof VALID_TAGS)[number] { - const lower = raw.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(raw); if ((VALID_TAGS as readonly string[]).includes(lower)) { return lower as (typeof VALID_TAGS)[number]; } diff --git a/extensions/qqbot/src/utils/text-parsing.ts b/extensions/qqbot/src/utils/text-parsing.ts index c77eda58829..5ebaa117d63 100644 --- a/extensions/qqbot/src/utils/text-parsing.ts +++ b/extensions/qqbot/src/utils/text-parsing.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { RefAttachmentSummary } from "../ref-index-store.js"; /** Replace QQ face tags with readable text labels. */ @@ -62,7 +63,7 @@ export function buildAttachmentSummaries( return undefined; } return attachments.map((att, idx) => { - const ct = att.content_type?.toLowerCase() ?? ""; + const ct = normalizeLowercaseStringOrEmpty(att.content_type); let type: RefAttachmentSummary["type"] = "unknown"; if (ct.startsWith("image/")) { type = "image"; diff --git a/extensions/slack/src/monitor/allow-list.ts b/extensions/slack/src/monitor/allow-list.ts index 56a5e5aca7d..cb43d7956d3 100644 --- a/extensions/slack/src/monitor/allow-list.ts +++ b/extensions/slack/src/monitor/allow-list.ts @@ -59,8 +59,8 @@ export function resolveSlackAllowListMatch(params: { allowNameMatching?: boolean; }): SlackAllowListMatch { const compiledAllowList = compileAllowlist(params.allowList); - const id = params.id?.toLowerCase(); - const name = params.name?.toLowerCase(); + const id = normalizeOptionalLowercaseString(params.id); + const name = normalizeOptionalLowercaseString(params.name); const slug = normalizeSlackSlug(name); const candidates: Array<{ value?: string; source: SlackAllowListSource }> = [ { value: id, source: "id" }, diff --git a/extensions/slack/src/monitor/media.ts b/extensions/slack/src/monitor/media.ts index d58e356a3b9..40882e932c8 100644 --- a/extensions/slack/src/monitor/media.ts +++ b/extensions/slack/src/monitor/media.ts @@ -5,7 +5,10 @@ import type { FetchLike } from "openclaw/plugin-sdk/media-runtime"; import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime"; import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime"; import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url"; -import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; import type { SlackAttachment, SlackFile } from "../types.js"; function isSlackHostname(hostname: string): boolean { @@ -135,7 +138,9 @@ function resolveSlackMediaMimetype( } function looksLikeHtmlBuffer(buffer: Buffer): boolean { - const head = buffer.subarray(0, 512).toString("utf-8").replace(/^\s+/, "").toLowerCase(); + const head = normalizeLowercaseStringOrEmpty( + buffer.subarray(0, 512).toString("utf-8").replace(/^\s+/, ""), + ); return head.startsWith(" !query || choice.label.toLowerCase().includes(query)) + .filter((choice) => !query || normalizeLowercaseStringOrEmpty(choice.label).includes(query)) .slice(0, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX) .map((choice) => ({ text: { type: "plain_text", text: choice.label.slice(0, 75) },