refactor: dedupe lowercase helper readers

This commit is contained in:
Peter Steinberger
2026-04-07 14:48:12 +01:00
parent eba04199f8
commit 948d139399
13 changed files with 82 additions and 33 deletions

View File

@@ -19,7 +19,10 @@ import { resolveSessionParentSessionKey } from "../../channels/plugins/session-c
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import type { ThinkLevel } from "./directives.js";
export type ModelDirectiveSelection = {
@@ -211,7 +214,7 @@ function scoreFuzzyMatch(params: {
} {
const provider = normalizeProviderId(params.provider);
const model = params.model;
const fragment = params.fragment.trim().toLowerCase();
const fragment = normalizeLowercaseStringOrEmpty(params.fragment);
const providerLower = provider.toLowerCase();
const modelLower = model.toLowerCase();
const haystack = `${providerLower}/${modelLower}`;
@@ -541,7 +544,7 @@ export function resolveModelDirectiveSelection(params: {
provider?: string;
fragment: string;
}): { selection?: ModelDirectiveSelection; error?: string } => {
const fragment = params.fragment.trim().toLowerCase();
const fragment = normalizeLowercaseStringOrEmpty(params.fragment);
if (!fragment) {
return {};
}

View File

@@ -4,6 +4,7 @@ import { resolveCronStyleNow } from "../../agents/current-time.js";
import { resolveUserTimezone } from "../../agents/date-time.js";
import type { OpenClawConfig } from "../../config/config.js";
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
const MAX_CONTEXT_CHARS = 3000;
const DEFAULT_POST_COMPACTION_SECTIONS = ["Session Startup", "Red Lines"];
@@ -18,12 +19,12 @@ function matchesSectionSet(sectionNames: string[], expectedSections: string[]):
const counts = new Map<string, number>();
for (const name of expectedSections) {
const normalized = name.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(name);
counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
}
for (const name of sectionNames) {
const normalized = name.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(name);
const count = counts.get(normalized);
if (!count) {
return false;

View File

@@ -171,7 +171,7 @@ export function resolveChannelModelOverride(
keys,
parentKeys,
wildcardKey: "*",
normalizeKey: (value) => value.trim().toLowerCase(),
normalizeKey: (value) => normalizeOptionalLowercaseString(value) ?? "",
});
const raw = match.entry ?? match.wildcardEntry;
if (typeof raw !== "string") {

View File

@@ -35,6 +35,7 @@ import {
registerMemoryEmbeddingProvider,
} from "../plugins/memory-embedding-providers.js";
import { writeRuntimeJson, defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { canonicalizeSpeechProviderId, listSpeechProviders } from "../tts/provider-registry.js";
@@ -1725,11 +1726,11 @@ export function registerCapabilityCli(program: Command) {
const cfg = loadConfig();
const selectedSearchProvider =
typeof cfg.tools?.web?.search?.provider === "string"
? cfg.tools.web.search.provider.trim().toLowerCase()
? normalizeLowercaseStringOrEmpty(cfg.tools.web.search.provider)
: "";
const selectedFetchProvider =
typeof cfg.tools?.web?.fetch?.provider === "string"
? cfg.tools.web.fetch.provider.trim().toLowerCase()
? normalizeLowercaseStringOrEmpty(cfg.tools.web.fetch.provider)
: "";
const result = {
search: listWebSearchProviders({ config: cfg }).map((provider) => ({

View File

@@ -1,5 +1,6 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export const DEFAULT_LIVE_IMAGE_MODELS: Record<string, string> = {
fal: "fal/fal-ai/flux/dev",
@@ -16,8 +17,8 @@ export function parseCaseFilter(raw?: string): Set<string> | null {
}
const values = trimmed
.split(",")
.map((entry) => entry.trim().toLowerCase())
.filter(Boolean);
.map((entry) => normalizeOptionalLowercaseString(entry))
.filter((entry): entry is string => Boolean(entry));
return values.length > 0 ? new Set(values) : null;
}
@@ -55,7 +56,11 @@ export function parseProviderModelMap(raw?: string): Map<string, string> {
if (slash <= 0 || slash === trimmed.length - 1) {
continue;
}
entries.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
continue;
}
entries.set(providerId, trimmed);
}
return entries;
}
@@ -72,7 +77,11 @@ export function resolveConfiguredLiveImageModels(cfg: OpenClawConfig): Map<strin
if (slash <= 0 || slash === trimmed.length - 1) {
return;
}
resolved.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
return;
}
resolved.set(providerId, trimmed);
};
if (typeof configured === "string") {
add(configured);

View File

@@ -2,6 +2,10 @@ import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
export const DEFAULT_MEMORY_DREAMING_ENABLED = false;
export const DEFAULT_MEMORY_DREAMING_TIMEZONE = undefined;
@@ -190,7 +194,7 @@ function normalizeBoolean(value: unknown, fallback: boolean): boolean {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(value);
if (normalized === "true") {
return true;
}
@@ -591,7 +595,7 @@ export function resolveMemoryDreamingWorkspaces(cfg: OpenClawConfig): MemoryDrea
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
continue;
}
const id = entry.id.trim().toLowerCase();
const id = normalizeOptionalLowercaseString(entry.id);
if (!id || seenAgents.has(id)) {
continue;
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
const MEMORY_MULTIMODAL_SPECS = {
image: {
labelPrefix: "Image file",
@@ -73,7 +75,7 @@ export function buildMemoryMultimodalLabel(
}
export function buildCaseInsensitiveExtensionGlob(extension: string): string {
const normalized = extension.trim().replace(/^\./, "").toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(extension).replace(/^\./, "");
if (!normalized) {
return "*";
}
@@ -88,7 +90,7 @@ export function classifyMemoryMultimodalPath(
if (!isMemoryMultimodalEnabled(settings)) {
return null;
}
const lower = filePath.trim().toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(filePath);
for (const modality of settings.modalities) {
for (const extension of getMemoryMultimodalExtensions(modality)) {
if (lower.endsWith(extension)) {

View File

@@ -1,5 +1,6 @@
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
const log = createSubsystemLogger("memory");
@@ -52,7 +53,7 @@ export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResul
function isQmdNoResultsOutput(raw: string): boolean {
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim().toLowerCase().replace(/\s+/g, " "))
.map((line) => normalizeLowercaseStringOrEmpty(line).replace(/\s+/g, " "))
.filter((line) => line.length > 0);
return lines.some((line) => isQmdNoResultsLine(line));
}

View File

@@ -1,4 +1,8 @@
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../../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);
const rawPrefix = normalizeOptionalLowercaseString(match.rawKeyPrefix);
if (rawPrefix && !rawKey.startsWith(rawPrefix)) {
continue;

View File

@@ -8,6 +8,8 @@
* This module extracts meaningful keywords from such queries to improve FTS results.
*/
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
// Common stop words that don't add search value
const STOP_WORDS_EN = new Set([
// Articles and determiners
@@ -673,7 +675,7 @@ function isValidKeyword(token: string): boolean {
function tokenize(text: string, opts?: { ftsTokenizer?: "unicode61" | "trigram" }): string[] {
const useTrigram = opts?.ftsTokenizer === "trigram";
const tokens: string[] = [];
const normalized = text.toLowerCase().trim();
const normalized = normalizeLowercaseStringOrEmpty(text);
// Split into segments (English words, Chinese character sequences, etc.)
const segments = normalized.split(/[\s\p{P}]+/u).filter(Boolean);

View File

@@ -1,5 +1,6 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
export const DEFAULT_LIVE_MUSIC_MODELS: Record<string, string> = {
google: "google/lyria-3-clip-preview",
@@ -24,8 +25,8 @@ export function parseCsvFilter(raw?: string): Set<string> | null {
}
const values = trimmed
.split(",")
.map((entry) => entry.trim().toLowerCase())
.filter(Boolean);
.map((entry) => normalizeOptionalLowercaseString(entry))
.filter((entry): entry is string => Boolean(entry));
return values.length > 0 ? new Set(values) : null;
}
@@ -40,7 +41,11 @@ export function parseProviderModelMap(raw?: string): Map<string, string> {
if (slash <= 0 || slash === trimmed.length - 1) {
continue;
}
entries.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
continue;
}
entries.set(providerId, trimmed);
}
return entries;
}
@@ -57,7 +62,11 @@ export function resolveConfiguredLiveMusicModels(cfg: OpenClawConfig): Map<strin
if (slash <= 0 || slash === trimmed.length - 1) {
return;
}
resolved.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
return;
}
resolved.set(providerId, trimmed);
};
if (typeof configured === "string") {
add(configured);

View File

@@ -10,6 +10,7 @@ import { resolveStateDir, type OpenClawConfig } from "../config/config.js";
import { coerceSecretRef } from "../config/types.secrets.js";
import { resolveSecretInputRef, type SecretRef } from "../config/types.secrets.js";
import { formatErrorMessage } from "../infra/errors.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { resolveConfigDir, resolveUserPath } from "../utils.js";
import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
import { iterateAuthProfileCredentials } from "./auth-profiles-scan.js";
@@ -127,7 +128,7 @@ const SENSITIVE_MODEL_PROVIDER_HEADER_NAME_FRAGMENTS = [
];
function isLikelySensitiveModelProviderHeaderName(value: string): boolean {
const normalized = value.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(value);
if (!normalized) {
return false;
}

View File

@@ -1,5 +1,9 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
export const DEFAULT_LIVE_VIDEO_MODELS: Record<string, string> = {
alibaba: "alibaba/wan2.6-t2v",
@@ -22,7 +26,7 @@ export function resolveLiveVideoResolution(params: {
providerId: string;
modelRef: string;
}): "480P" | "768P" | "1080P" {
const providerId = params.providerId.trim().toLowerCase();
const providerId = normalizeLowercaseStringOrEmpty(params.providerId);
if (providerId === "minimax") {
return "768P";
}
@@ -47,8 +51,8 @@ export function parseCsvFilter(raw?: string): Set<string> | null {
}
const values = trimmed
.split(",")
.map((entry) => entry.trim().toLowerCase())
.filter(Boolean);
.map((entry) => normalizeOptionalLowercaseString(entry))
.filter((entry): entry is string => Boolean(entry));
return values.length > 0 ? new Set(values) : null;
}
@@ -63,7 +67,11 @@ export function parseProviderModelMap(raw?: string): Map<string, string> {
if (slash <= 0 || slash === trimmed.length - 1) {
continue;
}
entries.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
continue;
}
entries.set(providerId, trimmed);
}
return entries;
}
@@ -80,7 +88,11 @@ export function resolveConfiguredLiveVideoModels(cfg: OpenClawConfig): Map<strin
if (slash <= 0 || slash === trimmed.length - 1) {
return;
}
resolved.set(trimmed.slice(0, slash).trim().toLowerCase(), trimmed);
const providerId = normalizeOptionalLowercaseString(trimmed.slice(0, slash));
if (!providerId) {
return;
}
resolved.set(providerId, trimmed);
};
if (typeof configured === "string") {
add(configured);
@@ -97,7 +109,7 @@ export function canRunBufferBackedVideoToVideoLiveLane(params: {
providerId: string;
modelRef: string;
}): boolean {
const providerId = params.providerId.trim().toLowerCase();
const providerId = normalizeLowercaseStringOrEmpty(params.providerId);
if (REMOTE_URL_VIDEO_TO_VIDEO_PROVIDERS.has(providerId)) {
return false;
}
@@ -116,7 +128,7 @@ export function canRunBufferBackedImageToVideoLiveLane(params: {
providerId: string;
modelRef: string;
}): boolean {
const providerId = params.providerId.trim().toLowerCase();
const providerId = normalizeLowercaseStringOrEmpty(params.providerId);
if (BUFFER_BACKED_IMAGE_TO_VIDEO_UNSUPPORTED_PROVIDERS.has(providerId)) {
return false;
}