refactor: dedupe misc lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 22:22:46 +01:00
parent 775b78e186
commit 67dc6e82b9
26 changed files with 112 additions and 77 deletions

View File

@@ -7,6 +7,7 @@ import {
type Event,
} from "nostr-tools";
import { decrypt, encrypt } from "nostr-tools/nip04";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
createDirectDmPreCryptoGuardPolicy,
type DirectDmPreCryptoGuardPolicyOverrides,
@@ -877,7 +878,7 @@ export function normalizePubkey(input: string): string {
if (!/^[0-9a-fA-F]{64}$/.test(trimmed)) {
throw new Error("Pubkey must be 64 hex characters or npub format");
}
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
/**

View File

@@ -9,6 +9,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
readStringValue,
} from "openclaw/plugin-sdk/text-runtime";
@@ -114,7 +115,7 @@ function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: s
return { ok: false, error: "URL must use https:// protocol" };
}
const hostname = url.hostname.toLowerCase();
const hostname = normalizeLowercaseStringOrEmpty(url.hostname);
if (isBlockedHostnameOrIp(hostname)) {
return { ok: false, error: "URL must not point to private/internal addresses" };
@@ -197,7 +198,7 @@ function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean {
return false;
}
const ipLower = remoteAddress.toLowerCase().replace(/^\[|\]$/g, "");
const ipLower = normalizeLowercaseStringOrEmpty(remoteAddress).replace(/^\[|\]$/g, "");
// IPv6 loopback
if (ipLower === "::1") {
@@ -221,7 +222,7 @@ function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean {
function isLoopbackOriginLike(value: string): boolean {
try {
const url = new URL(value);
const hostname = url.hostname.toLowerCase();
const hostname = normalizeLowercaseStringOrEmpty(url.hostname);
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
} catch {
return false;

View File

@@ -19,6 +19,7 @@ import {
runSshSandboxCommand,
sanitizeEnvVars,
} from "openclaw/plugin-sdk/sandbox";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
buildExecRemoteCommand,
buildRemoteCommand,
@@ -504,8 +505,7 @@ function resolveOpenShellPluginConfigFromConfig(
function buildOpenShellSandboxName(scopeKey: string): string {
const trimmed = scopeKey.trim() || "session";
const safe = trimmed
.toLowerCase()
const safe = normalizeLowercaseStringOrEmpty(trimmed)
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 32);

View File

@@ -1,12 +1,13 @@
import fs from "node:fs/promises";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
export const DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS = ["hooks", "git-hooks", ".git"] as const;
const COPY_TREE_FS_CONCURRENCY = 16;
function createExcludeMatcher(excludeDirs?: readonly string[]) {
const excluded = new Set((excludeDirs ?? []).map((d) => d.toLowerCase()));
return (name: string) => excluded.has(name.toLowerCase());
const excluded = new Set((excludeDirs ?? []).map((d) => normalizeLowercaseStringOrEmpty(d)));
return (name: string) => excluded.has(normalizeLowercaseStringOrEmpty(name));
}
function createConcurrencyLimiter(limit: number) {

View File

@@ -1,8 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import {
definePluginEntry,
@@ -260,7 +260,7 @@ function formatHelp(): string {
}
function parseGroup(raw: string | undefined): ArmGroup | null {
const value = normalizeOptionalString(raw)?.toLowerCase() ?? "";
const value = normalizeOptionalLowercaseString(raw) ?? "";
if (!value) {
return null;
}
@@ -354,7 +354,7 @@ export default definePluginEntry({
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
const action = normalizeLowercaseStringOrEmpty(tokens[0]);
const stateDir = api.runtime.state.resolveStateDir();
const statePath = resolveStatePath(stateDir);

View File

@@ -5,6 +5,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import * as querystring from "node:querystring";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
beginWebhookRequestPipelineOrReject,
createWebhookInFlightLimiter,
@@ -264,7 +265,7 @@ function extractTokenFromHeaders(req: IncomingMessage): string | undefined {
* - text <- text | message | content
*/
function parsePayload(req: IncomingMessage, body: string): SynologyWebhookPayload | null {
const contentType = String(req.headers["content-type"] ?? "").toLowerCase();
const contentType = normalizeLowercaseStringOrEmpty(req.headers["content-type"]);
let bodyFields: Record<string, unknown> = {};
if (contentType.includes("application/json")) {

View File

@@ -156,24 +156,24 @@ function resolveZalouserInboundSessionKey(params: {
return params.route.sessionKey;
}
const directSessionKey = params.core.channel.routing
.buildAgentSessionKey({
const directSessionKey = normalizeLowercaseStringOrEmpty(
params.core.channel.routing.buildAgentSessionKey({
agentId: params.route.agentId,
channel: "zalouser",
accountId: params.route.accountId,
peer: { kind: "direct", id: params.senderId },
dmScope: resolveZalouserDmSessionScope(params.config),
identityLinks: params.config.session?.identityLinks,
})
.toLowerCase();
const legacySessionKey = params.core.channel.routing
.buildAgentSessionKey({
}),
);
const legacySessionKey = normalizeLowercaseStringOrEmpty(
params.core.channel.routing.buildAgentSessionKey({
agentId: params.route.agentId,
channel: "zalouser",
accountId: params.route.accountId,
peer: { kind: "group", id: params.senderId },
})
.toLowerCase();
}),
);
const hasDirectSession =
params.core.channel.session.readSessionUpdatedAt({
storePath: params.storePath,
@@ -844,7 +844,7 @@ export async function monitorZalouserProvider(
mapping.push(`${entry}${cleaned}`);
continue;
}
const matches = byName.get(cleaned.toLowerCase()) ?? [];
const matches = byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? [];
const match = matches[0];
const id = match?.groupId ? String(match.groupId) : undefined;
if (id) {

View File

@@ -17,7 +17,7 @@ export function applyDirectoryQueryAndLimit(
): string[] {
const q = resolveDirectoryQuery(params.query);
const limit = resolveDirectoryLimit(params.limit);
const filtered = ids.filter((id) => (q ? id.toLowerCase().includes(q) : true));
const filtered = ids.filter((id) => (q ? normalizeLowercaseStringOrEmpty(id).includes(q) : true));
return typeof limit === "number" ? filtered.slice(0, limit) : filtered;
}

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import {
type ChannelId,
getChannelPlugin,
@@ -29,20 +30,18 @@ export function requirePairingAdapter(channelId: ChannelId): ChannelPairingAdapt
}
export function resolvePairingChannel(raw: unknown): ChannelId {
const value = (
const value =
typeof raw === "string"
? raw
: typeof raw === "number" || typeof raw === "boolean"
? String(raw)
: ""
)
.trim()
.toLowerCase();
const normalized = normalizeChannelId(value);
: "";
const normalizedValue = normalizeLowercaseStringOrEmpty(value);
const normalized = normalizeChannelId(normalizedValue);
const channels = listPairingChannels();
if (!normalized || !channels.includes(normalized)) {
throw new Error(
`Invalid channel: ${value || "(empty)"} (expected one of: ${channels.join(", ")})`,
`Invalid channel: ${normalizedValue || "(empty)"} (expected one of: ${channels.join(", ")})`,
);
}
return normalized;

View File

@@ -1,7 +1,9 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export type ToolContentBlock = Record<string, unknown>;
export function normalizeToolContentType(value: unknown): string {
return typeof value === "string" ? value.toLowerCase() : "";
return normalizeLowercaseStringOrEmpty(value);
}
export function isToolCallContentType(value: unknown): boolean {

View File

@@ -351,7 +351,7 @@ function filterRunLogEntries(
if (!opts.query) {
return true;
}
return opts.queryTextForEntry(entry).toLowerCase().includes(opts.query);
return normalizeLowercaseStringOrEmpty(opts.queryTextForEntry(entry)).includes(opts.query);
});
}

View File

@@ -8,7 +8,10 @@ import type {
PluginHookMessageReceivedEvent,
PluginHookMessageSentEvent,
} from "../plugins/types.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type {
MessagePreprocessedHookContext,
MessageReceivedHookContext,
@@ -76,7 +79,9 @@ export function deriveInboundMessageHookContext(
: typeof ctx.Body === "string"
? ctx.Body
: "");
const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
const channelId = normalizeLowercaseStringOrEmpty(
ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "",
);
const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel);
const mediaPaths = Array.isArray(ctx.MediaPaths)

View File

@@ -1,5 +1,8 @@
import type { ChannelApprovalKind } from "../channels/plugins/types.adapters.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type {
ChannelApprovalNativeDeliveryPlan,
ChannelApprovalNativePlannedTarget,
@@ -61,7 +64,7 @@ let approvalRouteRuntimeSeq = 0;
const MAX_APPROVAL_ROUTE_NOTICE_TTL_MS = 5 * 60_000;
function normalizeChannel(value?: string | null): string {
return value?.trim().toLowerCase() || "";
return normalizeLowercaseStringOrEmpty(value);
}
function clearPendingApprovalRouteNotice(approvalId: string): void {

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export type ParsedLogLine = {
time?: string;
level?: string;
@@ -51,7 +53,7 @@ export function parseLogLine(raw: string): ParsedLogLine | null {
: typeof meta?.date === "string"
? meta.date
: undefined,
level: levelRaw ? levelRaw.toLowerCase() : undefined,
level: normalizeOptionalLowercaseString(levelRaw),
subsystem: nameMeta.subsystem,
module: nameMeta.module,
message: extractMessage(parsed),

View File

@@ -10,7 +10,10 @@ import {
resolveInputFileLimits,
} from "../media/input-files.js";
import { wrapExternalContent } from "../security/external-content.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { resolveAttachmentKind } from "./attachments.js";
import { runWithConcurrency } from "./concurrency.js";
import { DEFAULT_ECHO_TRANSCRIPT_FORMAT, sendTranscriptEcho } from "./echo-transcript.js";
@@ -285,7 +288,7 @@ function resolveTextMimeFromName(name?: string): string | undefined {
if (!name) {
return undefined;
}
const ext = path.extname(name).toLowerCase();
const ext = normalizeLowercaseStringOrEmpty(path.extname(name));
return TEXT_EXT_MIME.get(ext);
}

View File

@@ -21,6 +21,7 @@ import { resolveProxyFetchFromEnv } from "../infra/net/proxy-fetch.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { runFfmpeg } from "../media/ffmpeg-exec.js";
import { runExec } from "../process/exec.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { MediaAttachmentCache } from "./attachments.js";
import {
CLI_OUTPUT_MAX_BUFFER,
@@ -226,7 +227,7 @@ async function resolveCliMediaPath(params: {
return params.mediaPath;
}
const ext = path.extname(params.mediaPath).toLowerCase();
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.mediaPath));
if (ext === ".wav") {
return params.mediaPath;
}

View File

@@ -297,7 +297,9 @@ async function probeGeminiCli(): Promise<boolean> {
const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], {
timeoutMs: 8000,
});
return Boolean(extractGeminiResponse(stdout) ?? stdout.toLowerCase().includes("ok"));
return Boolean(
extractGeminiResponse(stdout) ?? normalizeLowercaseStringOrEmpty(stdout).includes("ok"),
);
} catch {
return false;
}

View File

@@ -20,7 +20,10 @@ import {
isRfc1918Ipv4Address,
parseCanonicalIpAddress,
} from "../shared/net/ip.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
export type PairingSetupPayload = {
@@ -101,7 +104,7 @@ function isPrivateLanIpHost(host: string): boolean {
if (!isIpv6Address(parsed)) {
return false;
}
const normalized = parsed.toString().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(parsed.toString());
return (
normalized.startsWith("fe80:") || normalized.startsWith("fc") || normalized.startsWith("fd")
);

View File

@@ -678,18 +678,22 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
const choose = (agentId: string, matchedBy: ResolvedAgentRoute["matchedBy"]) => {
const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId);
const sessionKey = buildAgentSessionKey({
agentId: resolvedAgentId,
channel,
accountId,
peer,
dmScope,
identityLinks,
}).toLowerCase();
const mainSessionKey = buildAgentMainSessionKey({
agentId: resolvedAgentId,
mainKey: DEFAULT_MAIN_KEY,
}).toLowerCase();
const sessionKey = normalizeLowercaseStringOrEmpty(
buildAgentSessionKey({
agentId: resolvedAgentId,
channel,
accountId,
peer,
dmScope,
identityLinks,
}),
);
const mainSessionKey = normalizeLowercaseStringOrEmpty(
buildAgentMainSessionKey({
agentId: resolvedAgentId,
mainKey: DEFAULT_MAIN_KEY,
}),
);
const route = {
agentId: resolvedAgentId,
channel,

View File

@@ -1,7 +1,10 @@
import { normalizeChatType } from "../channels/chat-type.js";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionChatType, SessionEntry } from "../config/sessions.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
export type SessionSendPolicyDecision = "allow" | "deny";
@@ -102,8 +105,8 @@ export function resolveSendPolicy(params: {
normalizeChatType(deriveChatTypeFromKey(params.sessionKey));
const rawSessionKey = params.sessionKey ?? "";
const strippedSessionKey = stripAgentSessionKeyPrefix(rawSessionKey) ?? "";
const rawSessionKeyNorm = rawSessionKey.toLowerCase();
const strippedSessionKeyNorm = strippedSessionKey.toLowerCase();
const rawSessionKeyNorm = normalizeLowercaseStringOrEmpty(rawSessionKey);
const strippedSessionKeyNorm = normalizeLowercaseStringOrEmpty(strippedSessionKey);
let allowedMatch = false;
for (const rule of policy.rules ?? []) {

View File

@@ -4,6 +4,7 @@ import { isCronJobActive } from "../cron/active-jobs.js";
import { getAgentRunContext } from "../infra/agent-events.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { deriveSessionChatType } from "../sessions/session-chat-type.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
deleteTaskRecordById,
ensureTaskRegistryReady,
@@ -81,9 +82,9 @@ function findSessionEntryByKey(store: Record<string, unknown>, sessionKey: strin
if (direct) {
return direct;
}
const normalized = sessionKey.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(sessionKey);
for (const [key, entry] of Object.entries(store)) {
if (key.toLowerCase() === normalized) {
if (normalizeLowercaseStringOrEmpty(key) === normalized) {
return entry;
}
}

View File

@@ -76,7 +76,7 @@ export function parseTtsDirectives(
if (!rawKey || !rawValue) {
continue;
}
const key = rawKey.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(rawKey);
if (key === "provider") {
if (policy.allowProvider) {
const providerId = normalizeLowercaseStringOrEmpty(rawValue);

View File

@@ -10,7 +10,10 @@ import {
import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js";
import { prepareModelForSimpleCompletion } from "../agents/simple-completion-transport.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type { ResolvedTtsConfig } from "./tts.js";
const TEMP_FILE_CLEANUP_DELAY_MS = 5 * 60 * 1000; // 5 minutes
@@ -40,11 +43,10 @@ export function requireInRange(value: number, min: number, max: number, label: s
}
export function normalizeLanguageCode(code?: string): string | undefined {
const trimmed = normalizeOptionalString(code);
if (!trimmed) {
const normalized = normalizeOptionalLowercaseString(code);
if (!normalized) {
return undefined;
}
const normalized = trimmed.toLowerCase();
if (!/^[a-z]{2}$/.test(normalized)) {
throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)");
}
@@ -52,11 +54,10 @@ export function normalizeLanguageCode(code?: string): string | undefined {
}
export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | "off" | undefined {
const trimmed = normalizeOptionalString(mode);
if (!trimmed) {
const normalized = normalizeOptionalLowercaseString(mode);
if (!normalized) {
return undefined;
}
const normalized = trimmed.toLowerCase();
if (normalized === "auto" || normalized === "on" || normalized === "off") {
return normalized;
}

View File

@@ -1,4 +1,5 @@
import { assertOkOrThrowHttpError, fetchWithTimeout } from "openclaw/plugin-sdk/provider-http";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import type {
GeneratedVideoAsset,
VideoGenerationRequest,
@@ -139,7 +140,7 @@ export async function pollDashscopeVideoTaskUntilComplete(params: {
throw new Error(
payload.output?.message?.trim() ||
payload.message?.trim() ||
`${params.providerLabel} video generation task ${params.taskId} ${status.toLowerCase()}`,
`${params.providerLabel} video generation task ${params.taskId} ${normalizeLowercaseStringOrEmpty(status)}`,
);
}
await new Promise((resolve) => setTimeout(resolve, DEFAULT_VIDEO_GENERATION_POLL_INTERVAL_MS));

View File

@@ -9,7 +9,10 @@ import { resolveRuntimeWebSearchProviders } from "../plugins/web-search-provider
import { sortWebSearchProvidersForAutoDetect } from "../plugins/web-search-providers.shared.js";
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import {
hasWebProviderEntryCredential,
providerRequiresCredential,
@@ -280,11 +283,9 @@ function hasExplicitWebSearchSelection(params: {
if (configuredProviderId && availableProviderIds.has(configuredProviderId)) {
return true;
}
const runtimeConfiguredId = (
params.runtimeWebSearch?.selectedProvider ?? params.runtimeWebSearch?.providerConfigured
)
?.trim()
.toLowerCase();
const runtimeConfiguredId = normalizeOptionalLowercaseString(
params.runtimeWebSearch?.selectedProvider ?? params.runtimeWebSearch?.providerConfigured,
);
if (
params.runtimeWebSearch?.providerSource === "configured" &&
runtimeConfiguredId &&

View File

@@ -12,6 +12,7 @@ import {
text,
} from "@clack/prompts";
import { createCliProgress } from "../cli/progress.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { stripAnsi } from "../terminal/ansi.js";
import { note as emitNote } from "../terminal/note.js";
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
@@ -28,8 +29,7 @@ function guardCancel<T>(value: T | symbol): T {
}
function normalizeSearchTokens(search: string): string[] {
return search
.toLowerCase()
return normalizeLowercaseStringOrEmpty(search)
.split(/\s+/)
.map((token) => token.trim())
.filter((token) => token.length > 0);
@@ -39,7 +39,7 @@ function buildOptionSearchText<T>(option: Option<T>): string {
const label = stripAnsi(option.label ?? "");
const hint = stripAnsi(option.hint ?? "");
const value = String(option.value ?? "");
return `${label} ${hint} ${value}`.toLowerCase();
return normalizeLowercaseStringOrEmpty(`${label} ${hint} ${value}`);
}
export function tokenizedOptionFilter<T>(search: string, option: Option<T>): boolean {