refactor: dedupe lowercase helper readers

This commit is contained in:
Peter Steinberger
2026-04-07 13:04:52 +01:00
parent b96155e4e7
commit a15a5a1edc
10 changed files with 40 additions and 20 deletions

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type {
MSTeamsConversationStoreEntry,
StoredConversationReference,
@@ -62,8 +63,8 @@ export function findPreferredDmConversationByUserId(
}
matches.sort((a, b) => {
const aType = a.reference.conversation?.conversationType?.toLowerCase() ?? "";
const bType = b.reference.conversation?.conversationType?.toLowerCase() ?? "";
const aType = normalizeLowercaseStringOrEmpty(a.reference.conversation?.conversationType ?? "");
const bType = normalizeLowercaseStringOrEmpty(b.reference.conversation?.conversationType ?? "");
const aPersonal = aType === "personal" ? 1 : 0;
const bPersonal = bType === "personal" ? 1 : 0;
if (aPersonal !== bPersonal) {

View File

@@ -7,8 +7,6 @@
* @see https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4
*/
import type { DriveItemProperties } from "./graph-upload.js";
/**
* Build a native Teams file card attachment for Bot Framework.
*
@@ -19,6 +17,9 @@ import type { DriveItemProperties } from "./graph-upload.js";
* @param file - DriveItem properties from getDriveItemProperties()
* @returns Attachment object for Bot Framework sendActivity()
*/
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { DriveItemProperties } from "./graph-upload.js";
export function buildTeamsFileInfoCard(file: DriveItemProperties): {
contentType: string;
contentUrl: string;
@@ -39,7 +40,8 @@ export function buildTeamsFileInfoCard(file: DriveItemProperties): {
// Extract file extension from filename
const lastDot = file.name.lastIndexOf(".");
const fileType = lastDot >= 0 ? file.name.slice(lastDot + 1).toLowerCase() : "";
const fileType =
lastDot >= 0 ? normalizeLowercaseStringOrEmpty(file.name.slice(lastDot + 1)) : "";
return {
contentType: "application/vnd.microsoft.teams.card.file.info",

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import {
type ChunkMode,
isSilentReplyText,
@@ -305,7 +306,9 @@ export async function buildActivity(
// Determine conversation type and file type
// Teams only accepts base64 data URLs for images
const conversationType = conversationRef.conversation?.conversationType?.toLowerCase();
const conversationType = normalizeOptionalLowercaseString(
conversationRef.conversation?.conversationType,
);
const isPersonal = conversationType === "personal";
const isImage = media.kind === "image";

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import {
DEFAULT_ACCOUNT_ID,
createChannelPairingController,
@@ -24,7 +25,7 @@ export async function resolveMSTeamsSenderAccess(params: {
const activity = params.activity;
const msteamsCfg = params.cfg.channels?.msteams;
const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
const convType = activity.conversation?.conversationType?.toLowerCase();
const convType = normalizeOptionalLowercaseString(activity.conversation?.conversationType);
const isDirectMessage = convType === "personal" || (!convType && !activity.conversation?.isGroup);
const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
const senderName = activity.from?.name ?? activity.from?.id ?? senderId;

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
resolveChannelMediaMaxBytes,
type OpenClawConfig,
@@ -139,7 +140,9 @@ export async function resolveMSTeamsSendContext(params: {
const tokenProvider: MSTeamsAccessTokenProvider = createMSTeamsTokenProvider(app);
// Determine conversation type from stored reference
const storedConversationType = ref.conversation?.conversationType?.toLowerCase() ?? "";
const storedConversationType = normalizeLowercaseStringOrEmpty(
ref.conversation?.conversationType ?? "",
);
let conversationType: MSTeamsConversationType;
if (storedConversationType === "personal") {
conversationType = "personal";

View File

@@ -1,10 +1,13 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
export type HttpHeaderMap = Record<string, string | string[] | undefined>;
export function getHeader(headers: HttpHeaderMap, name: string): string | undefined {
const target = name.toLowerCase();
const target = normalizeLowercaseStringOrEmpty(name);
const direct = headers[target];
const value =
direct ?? Object.entries(headers).find(([key]) => key.toLowerCase() === target)?.[1];
direct ??
Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
if (Array.isArray(value)) {
return value[0];
}

View File

@@ -1,6 +1,7 @@
/**
* Voice mapping and XML utilities for voice call providers.
*/
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
/**
* Escape XML special characters for TwiML and other XML responses.
@@ -49,14 +50,14 @@ export function mapVoiceToPolly(voice: string | undefined): string {
}
// Map OpenAI voices to Polly equivalents
return OPENAI_TO_POLLY_MAP[voice.toLowerCase()] || DEFAULT_POLLY_VOICE;
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || DEFAULT_POLLY_VOICE;
}
/**
* Check if a voice name is a known OpenAI voice.
*/
export function isOpenAiVoice(voice: string): boolean {
return voice.toLowerCase() in OPENAI_TO_POLLY_MAP;
return normalizeLowercaseStringOrEmpty(voice) in OPENAI_TO_POLLY_MAP;
}
/**

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { getHeader } from "./http-headers.js";
import type { WebhookContext } from "./types.js";
@@ -190,7 +191,7 @@ function extractHostname(hostHeader: string): string | null {
return null; // Malformed IPv6
}
hostname = hostHeader.substring(1, endBracket);
return hostname.toLowerCase();
return normalizeLowercaseStringOrEmpty(hostname);
}
// Handle IPv4/domain with optional port
@@ -206,7 +207,7 @@ function extractHostname(hostHeader: string): string | null {
return null;
}
return hostname.toLowerCase();
return normalizeLowercaseStringOrEmpty(hostname);
}
function extractHostnameFromHeader(headerValue: string): string | null {

View File

@@ -13,6 +13,7 @@ import type {
MemoryQmdSearchMode,
} from "../../../../src/config/types.memory.js";
import { normalizeAgentId } from "../../../../src/routing/session-key.js";
import { normalizeLowercaseStringOrEmpty } from "../../../../src/shared/string-coerce.js";
import { resolveUserPath } from "../../../../src/utils.js";
import { splitShellArgs } from "../../../../src/utils/shell-argv.js";
@@ -107,7 +108,7 @@ const DEFAULT_QMD_SCOPE: SessionSendPolicyConfig = {
};
function sanitizeName(input: string): string {
const lower = input.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
const lower = normalizeLowercaseStringOrEmpty(input).replace(/[^a-z0-9-]+/g, "-");
const trimmed = lower.replace(/^-+|-+$/g, "");
return trimmed || "collection";
}

View File

@@ -1,4 +1,8 @@
import { parseAgentSessionKey } from "../../../../src/sessions/session-key-utils.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../../../../src/shared/string-coerce.js";
import type { ResolvedQmdConfig } from "./backend-config.js";
type ParsedQmdSessionScope = {
@@ -15,7 +19,7 @@ export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?
const channel = parsed.channel;
const chatType = parsed.chatType;
const normalizedKey = parsed.normalizedKey ?? "";
const rawKey = sessionKey?.trim().toLowerCase() ?? "";
const rawKey = normalizeLowercaseStringOrEmpty(sessionKey ?? "");
for (const rule of scope.rules ?? []) {
if (!rule) {
continue;
@@ -27,8 +31,8 @@ export function isQmdScopeAllowed(scope: ResolvedQmdConfig["scope"], sessionKey?
if (match.chatType && match.chatType !== chatType) {
continue;
}
const normalizedPrefix = match.keyPrefix?.trim().toLowerCase() || undefined;
const rawPrefix = match.rawKeyPrefix?.trim().toLowerCase() || undefined;
const normalizedPrefix = normalizeOptionalLowercaseString(match.keyPrefix) || undefined;
const rawPrefix = normalizeOptionalLowercaseString(match.rawKeyPrefix) || undefined;
if (rawPrefix && !rawKey.startsWith(rawPrefix)) {
continue;
@@ -76,7 +80,7 @@ function parseQmdSessionScope(key?: string): ParsedQmdSessionScope {
}
return {
normalizedKey: normalized,
channel: parts[0]?.toLowerCase(),
channel: normalizeOptionalLowercaseString(parts[0]),
chatType: chatType ?? "direct",
};
}
@@ -98,7 +102,7 @@ function normalizeQmdSessionKey(key?: string): string | undefined {
return undefined;
}
const parsed = parseAgentSessionKey(trimmed);
const normalized = (parsed?.rest ?? trimmed).toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(parsed?.rest ?? trimmed);
if (normalized.startsWith("subagent:")) {
return undefined;
}