From 88b394ba1bfef3f6e37bb876e9797290919a3bae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 13:16:01 +0100 Subject: [PATCH] refactor: dedupe feishu and bluebubbles lowercase helpers --- extensions/bluebubbles/src/attachments.ts | 5 ++-- .../bluebubbles/src/monitor-normalize.ts | 5 ++-- extensions/bluebubbles/src/reactions.ts | 3 +- extensions/bluebubbles/src/send.ts | 3 +- extensions/bluebubbles/src/targets.ts | 30 ++++++++----------- extensions/feishu/src/conversation-id.ts | 14 +++++---- extensions/feishu/src/directory.ts | 12 ++++++-- extensions/feishu/src/media.ts | 8 ++--- extensions/feishu/src/outbound.ts | 3 +- extensions/feishu/src/policy.ts | 4 ++- extensions/feishu/src/post.ts | 3 +- extensions/feishu/src/send.ts | 3 +- 12 files changed, 54 insertions(+), 39 deletions(-) diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 359307fcef6..5a25f348bed 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; @@ -49,7 +50,7 @@ function sanitizeFilename(input: string | undefined, fallback: string): string { function ensureExtension(filename: string, extension: string, fallbackBase: string): string { const currentExt = path.extname(filename); - if (currentExt.toLowerCase() === extension) { + if (normalizeLowercaseStringOrEmpty(currentExt) === extension) { return filename; } const base = currentExt ? filename.slice(0, -currentExt.length) : filename; @@ -58,7 +59,7 @@ function ensureExtension(filename: string, extension: string, fallbackBase: stri function resolveVoiceInfo(filename: string, contentType?: string) { const normalizedType = normalizeOptionalLowercaseString(contentType); - const extension = path.extname(filename).toLowerCase(); + const extension = normalizeLowercaseStringOrEmpty(path.extname(filename)); const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false); const isCaf = diff --git a/extensions/bluebubbles/src/monitor-normalize.ts b/extensions/bluebubbles/src/monitor-normalize.ts index 44fd57dfaea..903289d46dc 100644 --- a/extensions/bluebubbles/src/monitor-normalize.ts +++ b/extensions/bluebubbles/src/monitor-normalize.ts @@ -1,6 +1,7 @@ import { parseFiniteNumber } from "openclaw/plugin-sdk/infra-runtime"; import { asNullableRecord, + normalizeLowercaseStringOrEmpty, normalizeOptionalString, readStringField, } from "openclaw/plugin-sdk/text-runtime"; @@ -356,7 +357,7 @@ export function normalizeParticipantList(raw: unknown): BlueBubblesParticipant[] if (!normalized?.id) { continue; } - const key = normalized.id.toLowerCase(); + const key = normalizeLowercaseStringOrEmpty(normalized.id); if (seen.has(key)) { continue; } @@ -376,7 +377,7 @@ export function formatGroupMembers(params: { if (!entry?.id) { continue; } - const key = entry.id.toLowerCase(); + const key = normalizeLowercaseStringOrEmpty(entry.id); if (seen.has(key)) { continue; } diff --git a/extensions/bluebubbles/src/reactions.ts b/extensions/bluebubbles/src/reactions.ts index 74844e0f5d1..9d864500f5e 100644 --- a/extensions/bluebubbles/src/reactions.ts +++ b/extensions/bluebubbles/src/reactions.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import type { OpenClawConfig } from "./runtime-api.js"; @@ -120,7 +121,7 @@ export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolea if (!trimmed) { throw new Error("BlueBubbles reaction requires an emoji or name."); } - let raw = trimmed.toLowerCase(); + let raw = normalizeLowercaseStringOrEmpty(trimmed); if (raw.startsWith("-")) { raw = raw.slice(1); } diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 2a18153a600..b62f7d1ac44 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, stripMarkdown, @@ -364,7 +365,7 @@ export async function createChatForHandle(params: { if ( res.status === 400 || res.status === 403 || - errorText.toLowerCase().includes("private api") + normalizeLowercaseStringOrEmpty(errorText).includes("private api") ) { throw new Error( `BlueBubbles send failed: Cannot create new chat - Private API must be enabled. Original error: ${errorText || res.status}`, diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index 95cd7df1a72..83ae14eab50 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -6,14 +6,10 @@ import { resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, } from "openclaw/plugin-sdk/channel-targets"; - -function normalizeOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed || undefined; -} +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; export type BlueBubblesService = "imessage" | "sms" | "auto"; @@ -66,7 +62,7 @@ function stripBlueBubblesPrefix(value: string): string { if (!trimmed) { return ""; } - if (!trimmed.toLowerCase().startsWith("bluebubbles:")) { + if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("bluebubbles:")) { return trimmed; } return trimmed.slice("bluebubbles:".length).trim(); @@ -122,7 +118,7 @@ export function normalizeBlueBubblesHandle(raw: string): string { if (!trimmed) { return ""; } - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) { return normalizeBlueBubblesHandle(trimmed.slice(9)); } @@ -133,7 +129,7 @@ export function normalizeBlueBubblesHandle(raw: string): string { return normalizeBlueBubblesHandle(trimmed.slice(5)); } if (trimmed.includes("@")) { - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } return trimmed.replace(/\s+/g, ""); } @@ -204,7 +200,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): if (parseRawChatGuid(candidate)) { return true; } - const lowered = candidate.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(candidate); if (/^(imessage|sms|auto):/.test(lowered)) { return true; } @@ -234,7 +230,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): if (!normalizedTrimmed) { return false; } - const normalizedLower = normalizedTrimmed.toLowerCase(); + const normalizedLower = normalizeLowercaseStringOrEmpty(normalizedTrimmed); if ( /^(imessage|sms|auto):/.test(normalizedLower) || /^(chat_id|chat_guid|chat_identifier):/.test(normalizedLower) @@ -254,7 +250,7 @@ export function looksLikeBlueBubblesExplicitTargetId(raw: string, normalized?: s if (!candidate) { return false; } - const lowered = candidate.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(candidate); if (/^(imessage|sms|auto):/.test(lowered)) { return true; } @@ -273,7 +269,7 @@ export function looksLikeBlueBubblesExplicitTargetId(raw: string, normalized?: s if (!normalizedTrimmed) { return false; } - const normalizedLower = normalizedTrimmed.toLowerCase(); + const normalizedLower = normalizeLowercaseStringOrEmpty(normalizedTrimmed); if ( /^(imessage|sms|auto):/.test(normalizedLower) || /^(chat_id|chat_guid|chat_identifier):/.test(normalizedLower) @@ -307,7 +303,7 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { if (!trimmed) { throw new Error("BlueBubbles target is required"); } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const servicePrefixed = resolveServicePrefixedTarget({ trimmed, @@ -358,7 +354,7 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget if (!trimmed) { return { kind: "handle", handle: "" }; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const servicePrefixed = resolveServicePrefixedAllowTarget({ trimmed, diff --git a/extensions/feishu/src/conversation-id.ts b/extensions/feishu/src/conversation-id.ts index ff9304f23b9..2cf0450c8ed 100644 --- a/extensions/feishu/src/conversation-id.ts +++ b/extensions/feishu/src/conversation-id.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + export type FeishuGroupSessionScope = | "group" | "group_sender" @@ -50,7 +52,7 @@ export function parseFeishuTargetId(raw: unknown): string | undefined { if (!withoutProvider) { return undefined; } - const lowered = withoutProvider.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(withoutProvider); for (const prefix of ["chat:", "group:", "channel:", "user:", "dm:", "open_id:"]) { if (lowered.startsWith(prefix)) { return normalizeText(withoutProvider.slice(prefix.length)); @@ -68,7 +70,7 @@ export function parseFeishuDirectConversationId(raw: unknown): string | undefine if (!withoutProvider) { return undefined; } - const lowered = withoutProvider.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(withoutProvider); for (const prefix of ["user:", "dm:", "open_id:"]) { if (lowered.startsWith(prefix)) { return normalizeText(withoutProvider.slice(prefix.length)); @@ -176,8 +178,8 @@ export function buildFeishuModelOverrideParentCandidates( } const topicSenderMatch = rawId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i); if (topicSenderMatch) { - const chatId = topicSenderMatch[1]?.trim().toLowerCase(); - const topicId = topicSenderMatch[2]?.trim().toLowerCase(); + const chatId = normalizeLowercaseStringOrEmpty(topicSenderMatch[1]); + const topicId = normalizeLowercaseStringOrEmpty(topicSenderMatch[2]); if (chatId && topicId) { return [`${chatId}:topic:${topicId}`, chatId]; } @@ -185,12 +187,12 @@ export function buildFeishuModelOverrideParentCandidates( } const topicMatch = rawId.match(/^(.+):topic:([^:]+)$/i); if (topicMatch) { - const chatId = topicMatch[1]?.trim().toLowerCase(); + const chatId = normalizeLowercaseStringOrEmpty(topicMatch[1]); return chatId ? [chatId] : []; } const senderMatch = rawId.match(/^(.+):sender:([^:]+)$/i); if (senderMatch) { - const chatId = senderMatch[1]?.trim().toLowerCase(); + const chatId = normalizeLowercaseStringOrEmpty(senderMatch[1]); return chatId ? [chatId] : []; } return []; diff --git a/extensions/feishu/src/directory.ts b/extensions/feishu/src/directory.ts index bd887bf0025..4ff77873f3d 100644 --- a/extensions/feishu/src/directory.ts +++ b/extensions/feishu/src/directory.ts @@ -42,7 +42,11 @@ export async function listFeishuDirectoryPeersLive(params: { for (const user of response.data?.items ?? []) { if (user.open_id) { const name = user.name || ""; - if (!q || user.open_id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) { + if ( + !q || + normalizeLowercaseStringOrEmpty(user.open_id).includes(q) || + normalizeLowercaseStringOrEmpty(name).includes(q) + ) { peers.push({ kind: "user", id: user.open_id, @@ -95,7 +99,11 @@ export async function listFeishuDirectoryGroupsLive(params: { for (const chat of response.data?.items ?? []) { if (chat.chat_id) { const name = chat.name || ""; - if (!q || chat.chat_id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) { + if ( + !q || + normalizeLowercaseStringOrEmpty(chat.chat_id).includes(q) || + normalizeLowercaseStringOrEmpty(name).includes(q) + ) { groups.push({ kind: "group", id: chat.chat_id, diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 8f78d4fd574..5386ad223db 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -4,6 +4,7 @@ import { Readable } from "stream"; import type * as Lark from "@larksuiteoapi/node-sdk"; import { mediaKindFromMime } from "openclaw/plugin-sdk/media-runtime"; import { withTempDownloadPath } from "openclaw/plugin-sdk/temp-path"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { ClawdbotConfig } from "../runtime-api.js"; import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; @@ -106,9 +107,8 @@ function readHeaderValue( if (!headers) { return undefined; } - const target = name.toLowerCase(); for (const [key, value] of Object.entries(headers)) { - if (key.toLowerCase() !== target) { + if (normalizeLowercaseStringOrEmpty(key) !== normalizeLowercaseStringOrEmpty(name)) { continue; } if (typeof value === "string" && value.trim()) { @@ -496,7 +496,7 @@ export async function sendFileFeishu(params: { export function detectFileType( fileName: string, ): "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream" { - const ext = path.extname(fileName).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(fileName)); switch (ext) { case ".opus": case ".ogg": @@ -526,7 +526,7 @@ function resolveFeishuOutboundMediaKind(params: { fileName: string; contentType? msgType: "image" | "file" | "audio" | "media"; } { const { fileName, contentType } = params; - const ext = path.extname(fileName).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(fileName)); const mimeKind = mediaKindFromMime(contentType); const isImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes( diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index db40297565f..c11a183f8ec 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -1,6 +1,7 @@ import fs from "fs"; import path from "path"; import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { parseFeishuCommentTarget } from "./comment-target.js"; @@ -27,7 +28,7 @@ function normalizePossibleLocalImagePath(text: string | undefined): string | nul return null; } - const ext = path.extname(raw).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(raw)); const isImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes( ext, ); diff --git a/extensions/feishu/src/policy.ts b/extensions/feishu/src/policy.ts index f12e339a74b..1b1840714e2 100644 --- a/extensions/feishu/src/policy.ts +++ b/extensions/feishu/src/policy.ts @@ -71,7 +71,9 @@ export function resolveFeishuGroupConfig(params: { } const lowered = normalizeOptionalLowercaseString(groupId) ?? ""; - const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered); + const matchKey = Object.keys(groups).find( + (key) => normalizeOptionalLowercaseString(key) === lowered, + ); if (matchKey) { return groups[matchKey]; } diff --git a/extensions/feishu/src/post.ts b/extensions/feishu/src/post.ts index f582630e5c9..448e9b0f719 100644 --- a/extensions/feishu/src/post.ts +++ b/extensions/feishu/src/post.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { isRecord } from "./comment-shared.js"; import { normalizeFeishuExternalKey } from "./external-keys.js"; @@ -133,7 +134,7 @@ function renderElement( return escapeMarkdownText(toStringOrEmpty(element)); } - const tag = toStringOrEmpty(element.tag).toLowerCase(); + const tag = normalizeLowercaseStringOrEmpty(toStringOrEmpty(element.tag)); switch (tag) { case "text": return renderTextElement(element); diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index 8fa469949b8..3651db4ae19 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -1,6 +1,7 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { convertMarkdownTables, + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "openclaw/plugin-sdk/text-runtime"; import type { ClawdbotConfig } from "../runtime-api.js"; @@ -34,7 +35,7 @@ function shouldFallbackFromReplyTarget(response: { code?: number; msg?: string } if (response.code !== undefined && WITHDRAWN_REPLY_ERROR_CODES.has(response.code)) { return true; } - const msg = response.msg?.toLowerCase() ?? ""; + const msg = normalizeLowercaseStringOrEmpty(response.msg); return msg.includes("withdrawn") || msg.includes("not found"); }