refactor: dedupe feishu and bluebubbles lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 13:16:01 +01:00
parent ae4f8da94f
commit 88b394ba1b
12 changed files with 54 additions and 39 deletions

View File

@@ -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 =

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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}`,

View File

@@ -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,

View File

@@ -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 [];

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,
);

View File

@@ -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];
}

View File

@@ -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);

View File

@@ -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");
}