refactor: dedupe extension lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 14:56:14 +01:00
parent 948d139399
commit 9314bb7180
28 changed files with 72 additions and 48 deletions

View File

@@ -460,7 +460,7 @@ export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
applyConfigDefaults: ({ config, env }) => applyAnthropicConfigDefaults({ config, env }),
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
resolveSyntheticAuth: ({ provider }) =>
provider.trim().toLowerCase() === CLAUDE_CLI_BACKEND_ID
normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID
? resolveClaudeCliSyntheticAuth()
: undefined,
buildReplayPolicy: buildAnthropicReplayPolicy,

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { normalizeString } from "../record-shared.js";
import type { SnapshotAriaNode } from "./client.js";
import {
@@ -17,7 +18,7 @@ export type ChromeMcpSnapshotNode = {
};
function normalizeRole(node: ChromeMcpSnapshotNode): string {
const role = typeof node.role === "string" ? node.role.trim().toLowerCase() : "";
const role = normalizeLowercaseStringOrEmpty(node.role);
return role || "generic";
}

View File

@@ -19,6 +19,7 @@ import {
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
isRecord,
normalizeOptionalLowercaseString,
normalizeOptionalString,
resolveUserPath,
} from "openclaw/plugin-sdk/text-runtime";
@@ -263,7 +264,7 @@ async function readJsonResponse<T>(params: {
}
function inferFileExtension(params: { fileName?: string; mimeType?: string }): string {
const normalizedMime = params.mimeType?.toLowerCase().trim();
const normalizedMime = normalizeOptionalLowercaseString(params.mimeType);
if (normalizedMime?.includes("jpeg")) {
return "jpg";
}

View File

@@ -1,4 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { PluginLogger } from "../api.js";
import { resolveRequestClientIp } from "../runtime-api.js";
import type { DiffArtifactStore } from "./store.js";
@@ -170,7 +171,7 @@ function setSharedHeaders(res: ServerResponse, contentType: string): void {
}
function normalizeRemoteClientKey(remoteAddress: string | undefined): string {
const normalized = remoteAddress?.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(remoteAddress);
if (!normalized) {
return "unknown";
}

View File

@@ -12,6 +12,7 @@
*/
import type * as Lark from "@larksuiteoapi/node-sdk";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
// Feishu text_color values (1-7)
const TEXT_COLOR: Record<string, number> = {
@@ -86,7 +87,7 @@ export function parseColorMarkup(content: string): Segment[] {
}
} else {
// Tagged segment
const tagStr = match[1].toLowerCase().trim();
const tagStr = normalizeLowercaseStringOrEmpty(match[1]);
const text = match[2];
const tags = tagStr.split(/\s+/);

View File

@@ -2,7 +2,7 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { createInterface, type Interface } from "node:readline";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
import { normalizeLowercaseStringOrEmpty, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
import { DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS } from "./constants.js";
export type IMessageRpcError = {
@@ -42,7 +42,7 @@ function isTestEnv(): boolean {
if (process.env.NODE_ENV === "test") {
return true;
}
const vitest = process.env.VITEST?.trim().toLowerCase();
const vitest = normalizeLowercaseStringOrEmpty(process.env.VITEST);
return Boolean(vitest);
}

View File

@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { resolveMemoryRemDreamingConfig } from "openclaw/plugin-sdk/memory-core-host-status";
import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
colorize,
defaultRuntime,
@@ -215,13 +216,13 @@ function matchesPromotionSelector(
},
selector: string,
): boolean {
const trimmed = selector.trim().toLowerCase();
const trimmed = normalizeLowercaseStringOrEmpty(selector);
if (!trimmed) {
return false;
}
return (
candidate.key.toLowerCase() === trimmed ||
candidate.key.toLowerCase().includes(trimmed) ||
normalizeLowercaseStringOrEmpty(candidate.key) === trimmed ||
normalizeLowercaseStringOrEmpty(candidate.key).includes(trimmed) ||
candidate.path.toLowerCase().includes(trimmed) ||
candidate.snippet.toLowerCase().includes(trimmed)
);

View File

@@ -18,6 +18,7 @@ import {
type MemoryLightDreamingConfig,
type MemoryRemDreamingConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
import { generateAndAppendDreamNarrative, type NarrativePhaseData } from "./dreaming-narrative.js";
import {
@@ -1114,7 +1115,7 @@ function jaccardSimilarity(left: string, right: string): number {
const leftTokens = tokenizeSnippet(left);
const rightTokens = tokenizeSnippet(right);
if (leftTokens.size === 0 || rightTokens.size === 0) {
return left.trim().toLowerCase() === right.trim().toLowerCase() ? 1 : 0;
return normalizeLowercaseStringOrEmpty(left) === normalizeLowercaseStringOrEmpty(right) ? 1 : 0;
}
let intersection = 0;
for (const token of leftTokens) {

View File

@@ -37,6 +37,7 @@ import {
type MemorySource,
type MemorySyncProgressUpdate,
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
createEmbeddingProvider,
type EmbeddingProvider,
@@ -91,7 +92,9 @@ const log = createSubsystemLogger("memory");
function shouldIgnoreMemoryWatchPath(watchPath: string): boolean {
const normalized = path.normalize(watchPath);
const parts = normalized.split(path.sep).map((segment) => segment.trim().toLowerCase());
const parts = normalized
.split(path.sep)
.map((segment) => normalizeLowercaseStringOrEmpty(segment));
return parts.some((segment) => IGNORED_MEMORY_WATCH_DIR_NAMES.has(segment));
}

View File

@@ -42,6 +42,7 @@ import {
type ResolvedQmdConfig,
type ResolvedQmdMcporterConfig,
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { asRecord } from "../dreaming-shared.js";
import { resolveQmdCollectionPatternFlags, type QmdCollectionPatternFlag } from "./qmd-compat.js";
@@ -140,7 +141,9 @@ function resolveQmdEmbedLockOptions(embedTimeoutMs: number) {
function shouldIgnoreMemoryWatchPath(watchPath: string): boolean {
const normalized = path.normalize(watchPath);
const parts = normalized.split(path.sep).map((segment) => segment.trim().toLowerCase());
const parts = normalized
.split(path.sep)
.map((segment) => normalizeLowercaseStringOrEmpty(segment));
return parts.some((segment) => IGNORED_MEMORY_WATCH_DIR_NAMES.has(segment));
}

View File

@@ -1,9 +1,10 @@
import * as ssrf from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { vi } from "vitest";
export function mockPublicPinnedHostname() {
return vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
const addresses = ["93.184.216.34"];
const lookup = ((host: string, options?: unknown, callback?: unknown) => {
const cb =
@@ -13,7 +14,7 @@ export function mockPublicPinnedHostname() {
if (!cb) {
return;
}
if (host.trim().toLowerCase().replace(/\.$/, "") !== normalized) {
if (normalizeLowercaseStringOrEmpty(host).replace(/\.$/, "") !== normalized) {
cb(null, []);
return;
}

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status";
import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
deriveConceptTags,
MAX_CONCEPT_TAGS,
@@ -241,7 +242,10 @@ function buildEntryKey(result: {
}
function hashQuery(query: string): string {
return createHash("sha1").update(query.trim().toLowerCase()).digest("hex").slice(0, 12);
return createHash("sha1")
.update(normalizeLowercaseStringOrEmpty(query))
.digest("hex")
.slice(0, 12);
}
function mergeQueryHashes(existing: string[], queryHash: string): string[] {

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { WikiClaim, WikiPageSummary } from "./markdown.js";
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -120,7 +121,7 @@ function resolveLatestTimestamp(candidates: Array<string | undefined>): string |
}
export function normalizeClaimStatus(status?: string): string {
return status?.trim().toLowerCase() || "supported";
return normalizeLowercaseStringOrEmpty(status) || "supported";
}
export function isClaimContestedStatus(status?: string): boolean {

View File

@@ -308,7 +308,7 @@ export function createSynologyChatPlugin(): SynologyChatPlugin {
text: {
idLabel: "synologyChatUserId",
message: "OpenClaw: your access has been approved.",
normalizeAllowEntry: (entry: string) => entry.toLowerCase().trim(),
normalizeAllowEntry: (entry: string) => normalizeLowercaseStringOrEmpty(entry),
notify: async ({ cfg, id, message }) => {
const account = resolveAccount(cfg);
if (!account.incomingUrl) {

View File

@@ -8,6 +8,7 @@ import {
type ReplyPayload,
} from "openclaw/plugin-sdk/reply-runtime";
import type { MockFn } from "openclaw/plugin-sdk/testing";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { beforeEach, vi } from "vitest";
import type { TelegramBotDeps } from "./bot-deps.js";
@@ -208,7 +209,7 @@ function createModelsProviderDataFromConfig(cfg: OpenClawConfig): {
} {
const byProvider = new Map<string, Set<string>>();
const add = (providerRaw: string | undefined, modelRaw: string | undefined) => {
const provider = providerRaw?.trim().toLowerCase();
const provider = normalizeLowercaseStringOrEmpty(providerRaw);
const model = modelRaw?.trim();
if (!provider || !model) {
return;

View File

@@ -4,6 +4,7 @@ import {
formatErrorMessage,
readErrorName,
} from "openclaw/plugin-sdk/error-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const TELEGRAM_NETWORK_ORIGIN = Symbol("openclaw.telegram.network-origin");
@@ -254,7 +255,7 @@ export function isRecoverableTelegramNetworkError(
return true;
}
const message = formatErrorMessage(candidate).trim().toLowerCase();
const message = normalizeLowercaseStringOrEmpty(formatErrorMessage(candidate));
if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) {
return true;
}

View File

@@ -230,7 +230,7 @@ export type AdminCommand =
* - "pending" - list all pending approvals
*/
export function parseAdminCommand(text: string): AdminCommand | null {
const trimmed = text.trim().toLowerCase();
const trimmed = normalizeLowercaseStringOrEmpty(text);
// "blocked" - list blocked ships
if (trimmed === "blocked") {

View File

@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { resetInboundDedupe } from "openclaw/plugin-sdk/reply-runtime";
import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { afterAll, afterEach, beforeAll, beforeEach, vi, type Mock } from "vitest";
import type { WebInboundMessage, WebListenerCloseReason } from "./inbound.js";
import {
@@ -152,7 +153,7 @@ export function installWebAutoReplyUnitTestHooks(opts?: { pinDns?: boolean }) {
.spyOn(ssrf, "resolvePinnedHostname")
.mockImplementation(async (hostname) => {
// SSRF guard pins DNS; stub resolution to avoid live lookups in unit tests.
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
const addresses = [TEST_NET_IP];
return {
hostname: normalized,

View File

@@ -1,5 +1,7 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
export function isStatusCommand(body: string) {
const trimmed = body.trim().toLowerCase();
const trimmed = normalizeLowercaseStringOrEmpty(body);
if (!trimmed) {
return false;
}

View File

@@ -1,6 +1,7 @@
import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { vi } from "vitest";
import type { MockBaileysSocket } from "../../../test/mocks/baileys.js";
import { createMockBaileys } from "../../../test/mocks/baileys.js";
@@ -105,7 +106,7 @@ function resolveChannelContextVisibilityModeMock(params: {
function resolveGroupSessionKeyMock(ctx: { From?: string; ChatType?: string; Provider?: string }) {
const from = ctx.From?.trim() ?? "";
const chatType = ctx.ChatType?.trim().toLowerCase();
const chatType = normalizeLowercaseStringOrEmpty(ctx.ChatType);
if (!from) {
return null;
}
@@ -119,7 +120,7 @@ function resolveGroupSessionKeyMock(ctx: { From?: string; ChatType?: string; Pro
}
return {
key: `whatsapp:group:${from.toLowerCase()}`,
channel: ctx.Provider?.trim().toLowerCase() || "whatsapp",
channel: normalizeLowercaseStringOrEmpty(ctx.Provider) || "whatsapp",
id: from.toLowerCase(),
chatType: chatType === "channel" ? "channel" : "group",
};

View File

@@ -22,6 +22,7 @@ import {
} from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import { fetchZaiUsage, resolveLegacyPiAgentAccessToken } from "openclaw/plugin-sdk/provider-usage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildZaiModelDefinition } from "./model-definitions.js";
@@ -292,7 +293,7 @@ export default definePluginEntry({
...ZAI_TOOL_STREAM_HOOKS,
isBinaryThinking: () => true,
isModernModelRef: ({ modelId }) => {
const lower = modelId.trim().toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(modelId);
return (
lower.startsWith("glm-5") ||
lower.startsWith("glm-4.7") ||

View File

@@ -2,15 +2,10 @@ import {
buildChannelOutboundSessionRoute,
type ChannelOutboundSessionRouteParams,
} from "openclaw/plugin-sdk/core";
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function normalizeOptionalLowercaseString(value: unknown): string | undefined {
const normalized = normalizeLowercaseStringOrEmpty(value);
return normalized || undefined;
}
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/text-runtime";
export function stripZalouserTargetPrefix(raw: string): string {
return raw

View File

@@ -1,6 +1,7 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
function isGemini31Model(modelId: string): boolean {
@@ -9,7 +10,7 @@ function isGemini31Model(modelId: string): boolean {
}
function isGemma4Model(modelId: string): boolean {
return modelId.trim().toLowerCase().startsWith("gemma-4");
return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4");
}
function mapThinkLevelToGoogleThinkingLevel(
@@ -106,9 +107,7 @@ export function sanitizeGoogleThinkingPayload(params: {
}
const mappedLevel =
explicitMappedLevel ??
normalizedThinkingLevel ??
(hadThinkingBudget ? "MINIMAL" : undefined);
explicitMappedLevel ?? normalizedThinkingLevel ?? (hadThinkingBudget ? "MINIMAL" : undefined);
if (mappedLevel) {
thinkingConfigObj.thinkingLevel = mappedLevel;

View File

@@ -77,7 +77,7 @@ function parseExecDirectiveArgs(raw: string): Omit<
if (idx === -1) {
return null;
}
const key = token.slice(0, idx).trim().toLowerCase();
const key = normalizeOptionalLowercaseString(token.slice(0, idx));
const value = token.slice(idx + 1).trim();
if (!key) {
return null;

View File

@@ -10,6 +10,7 @@ import {
isTrustedSafeBinPath,
normalizeTrustedSafeBinDirs,
} from "../../../infra/exec-safe-bin-trust.js";
import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js";
import { sanitizeForLog } from "../../../terminal/ansi.js";
import { asObjectRecord } from "./object.js";
@@ -42,7 +43,7 @@ function normalizeConfiguredSafeBins(entries: unknown): string[] {
return Array.from(
new Set(
entries
.map((entry) => (typeof entry === "string" ? entry.trim().toLowerCase() : ""))
.map((entry) => normalizeOptionalLowercaseString(entry) ?? "")
.filter((entry) => entry.length > 0),
),
).toSorted();

View File

@@ -1,9 +1,10 @@
import { vi } from "vitest";
import * as ssrf from "../../../infra/net/ssrf.js";
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
export function mockPublicPinnedHostname() {
return vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
const addresses = ["93.184.216.34"];
return {
hostname: normalized,

View File

@@ -20,6 +20,10 @@ import { hasNonEmptyString } from "../infra/outbound/channel-target.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { collectDeepCodeSafetyFindings } from "./audit-deep-code-safety.js";
import { collectDeepProbeFindings } from "./audit-deep-probe-findings.js";
import {
@@ -395,9 +399,7 @@ export function collectGatewayConfigFindings(
? cfg.gateway?.tools?.allow
: [];
const gatewayToolsAllow = new Set(
gatewayToolsAllowRaw
.map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
.filter(Boolean),
gatewayToolsAllowRaw.map((v) => normalizeOptionalLowercaseString(v) ?? "").filter(Boolean),
);
const reenabledOverHttp = DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) =>
gatewayToolsAllow.has(name),
@@ -689,7 +691,7 @@ function isStrictLoopbackTrustedProxyEntry(entry: string): boolean {
return rawIp.trim() === "127.0.0.1" && prefix === 32;
}
if (ipVersion === 6) {
return prefix === 128 && rawIp.trim().toLowerCase() === "::1";
return prefix === 128 && normalizeLowercaseStringOrEmpty(rawIp) === "::1";
}
return false;
}
@@ -959,7 +961,7 @@ export function collectExecRuntimeFindings(cfg: OpenClawConfig): SecurityAuditFi
return Array.from(
new Set(
entries
.map((entry) => (typeof entry === "string" ? entry.trim().toLowerCase() : ""))
.map((entry) => normalizeOptionalLowercaseString(entry) ?? "")
.filter((entry) => entry.length > 0),
),
).toSorted();

View File

@@ -1,9 +1,10 @@
import { vi } from "vitest";
import * as ssrf from "../infra/net/ssrf.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export function mockPinnedHostnameResolution(addresses: string[] = ["93.184.216.34"]) {
return vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation(async (hostname) => {
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
const normalized = normalizeLowercaseStringOrEmpty(hostname).replace(/\.$/, "");
const pinnedAddresses = [...addresses];
return {
hostname: normalized,