refactor: dedupe agent lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 17:07:35 +01:00
parent 9e61209780
commit d56fe040b4
30 changed files with 87 additions and 52 deletions

View File

@@ -293,7 +293,7 @@ function normalizePathForComparison(input: string): string {
// Keep lexical path for non-existent directories.
}
if (process.platform === "win32") {
return normalized.toLowerCase();
return normalizeLowercaseStringOrEmpty(normalized);
}
return normalized;
}

View File

@@ -4,6 +4,7 @@ import {
} from "../infra/outbound/best-effort-delivery.js";
import { sendMessage } from "../infra/outbound/message.js";
import { isCronSessionKey, isSubagentSessionKey } from "../sessions/session-key-utils.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { isGatewayMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
import {
formatExecDeniedUserMessage,
@@ -90,7 +91,7 @@ function formatDirectExecApprovalFollowupText(
}
if (parsed.kind === "finished") {
const metadata = parsed.metadata.toLowerCase();
const metadata = normalizeLowercaseStringOrEmpty(parsed.metadata);
const body = sanitizeUserFacingText(parsed.body, {
errorContext: !metadata.includes("code 0"),
}).trim();

View File

@@ -398,7 +398,7 @@ export function parseCliJsonl(
const item = isRecord(parsed.item) ? parsed.item : null;
if (item && typeof item.text === "string") {
const type = typeof item.type === "string" ? item.type.toLowerCase() : "";
const type = normalizeLowercaseStringOrEmpty(item.type);
if (!type || type.includes("message")) {
texts.push(item.text);
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export type ExecApprovalResult =
| {
kind: "denied";
@@ -73,7 +75,7 @@ export function formatExecDeniedUserMessage(resultText: string): string | null {
return null;
}
const metadata = parsed.metadata.toLowerCase();
const metadata = normalizeLowercaseStringOrEmpty(parsed.metadata);
if (metadata.includes("approval-timeout")) {
return "Command did not run: approval timed out.";
}

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { buildCopilotIdeHeaders } from "./copilot-dynamic-headers.js";
import { resolveProviderEndpoint } from "./provider-attribution.js";
@@ -69,7 +70,7 @@ function resolveCopilotProxyHost(proxyEp: string): string | null {
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}
return url.hostname.toLowerCase();
return normalizeLowercaseStringOrEmpty(url.hostname);
} catch {
return null;
}

View File

@@ -9,6 +9,7 @@ import {
} from "@mariozechner/pi-ai";
import { parseGeminiAuth } from "../infra/gemini-auth.js";
import { normalizeGoogleApiBaseUrl } from "../infra/google-api-base-url.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { buildGuardedModelFetch } from "./provider-transport-fetch.js";
import { stripSystemPromptCacheBoundary } from "./system-prompt-cache-boundary.js";
import { transformTransportMessages } from "./transport-message-transform.js";
@@ -112,11 +113,11 @@ type GoogleSseChunk = {
let toolCallCounter = 0;
function isGemini3ProModel(modelId: string): boolean {
return /gemini-3(?:\.\d+)?-pro/.test(modelId.toLowerCase());
return /gemini-3(?:\.\d+)?-pro/.test(normalizeLowercaseStringOrEmpty(modelId));
}
function isGemini3FlashModel(modelId: string): boolean {
return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase());
return /gemini-3(?:\.\d+)?-flash/.test(normalizeLowercaseStringOrEmpty(modelId));
}
function requiresToolCallId(modelId: string): boolean {
@@ -124,7 +125,7 @@ function requiresToolCallId(modelId: string): boolean {
}
function supportsMultimodalFunctionResponse(modelId: string): boolean {
const match = modelId.toLowerCase().match(/^gemini(?:-live)?-(\d+)/);
const match = normalizeLowercaseStringOrEmpty(modelId).match(/^gemini(?:-live)?-(\d+)/);
if (!match) {
return true;
}

View File

@@ -1,5 +1,8 @@
import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { normalizeProviderId } from "./model-selection.js";
const KEY_SPLIT_RE = /[\s,;]+/g;
@@ -161,7 +164,7 @@ export function collectGeminiApiKeys(): string[] {
}
export function isApiKeyRateLimitError(message: string): boolean {
const lower = message.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(message);
if (lower.includes("rate_limit")) {
return true;
}
@@ -188,7 +191,7 @@ export function isAnthropicRateLimitError(message: string): boolean {
}
export function isAnthropicBillingError(message: string): boolean {
const lower = message.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(message);
if (lower.includes("credit balance")) {
return true;
}

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import type { TaskRecord } from "../tasks/task-registry.types.js";
import {
buildSessionAsyncTaskStatusDetails,
@@ -89,7 +90,7 @@ export function buildActiveMediaGenerationTaskPromptContextForSession(params: {
}
const provider = getMediaGenerationTaskProviderId(task, params.sourcePrefix);
const lines = [
`An active ${params.nounLabel.toLowerCase()} background task already exists for this session.`,
`An active ${normalizeLowercaseStringOrEmpty(params.nounLabel)} background task already exists for this session.`,
`Task ${task.taskId} is currently ${task.status}${provider ? ` via ${provider}` : ""}.`,
task.progressSummary ? `Current progress: ${task.progressSummary}.` : null,
`Do not call \`${params.toolName}\` again for the same request while that task is queued or running.`,

View File

@@ -12,7 +12,10 @@ import {
shouldDeferProviderSyntheticProfileAuthWithPlugin,
} from "../plugins/provider-runtime.js";
import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import {
type AuthProfileStore,
@@ -141,7 +144,7 @@ function resolveProviderAuthOverride(
function isLocalBaseUrl(baseUrl: string): boolean {
try {
const host = new URL(baseUrl).hostname.toLowerCase();
const host = normalizeLowercaseStringOrEmpty(new URL(baseUrl).hostname);
return (
host === "localhost" ||
host === "127.0.0.1" ||

View File

@@ -10,6 +10,7 @@ import {
import { Type } from "@sinclair/typebox";
import { formatErrorMessage } from "../infra/errors.js";
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizeProviderId } from "./provider-id.js";
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
@@ -104,7 +105,7 @@ function parseModality(modality: string | null): Array<"text" | "image"> {
if (!modality) {
return ["text"];
}
const normalized = modality.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(modality);
const parts = normalized.split(/[^a-z]+/).filter(Boolean);
const hasImage = parts.includes("image");
return hasImage ? ["text", "image"] : ["text"];

View File

@@ -176,7 +176,7 @@ export function inferUniqueProviderFromConfiguredModels(params: {
if (!model) {
return undefined;
}
const normalized = model.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(model);
const providers = new Set<string>();
const addProvider = (provider: string) => {
const normalizedProvider = normalizeProviderId(provider);
@@ -717,20 +717,22 @@ export function resolveThinkingDefault(params: {
catalog?: ModelCatalogEntry[];
}): ThinkLevel {
const normalizedProvider = normalizeProviderId(params.provider);
const normalizedModel = params.model.toLowerCase().replace(/\./g, "-");
const normalizedModel = normalizeLowercaseStringOrEmpty(params.model).replace(/\./g, "-");
const catalogCandidate = params.catalog?.find(
(entry) => entry.provider === params.provider && entry.id === params.model,
);
const configuredModels = params.cfg.agents?.defaults?.models;
const canonicalKey = modelKey(params.provider, params.model);
const legacyKey = legacyModelKey(params.provider, params.model);
const normalizedCanonicalKey = normalizeLowercaseStringOrEmpty(canonicalKey);
const normalizedLegacyKey = normalizeOptionalLowercaseString(legacyKey);
const primarySelection = normalizeModelSelection(params.cfg.agents?.defaults?.model);
const normalizedPrimarySelection = normalizeOptionalLowercaseString(primarySelection);
const explicitModelConfigured =
(configuredModels ? canonicalKey in configuredModels : false) ||
Boolean(legacyKey && configuredModels && legacyKey in configuredModels) ||
normalizedPrimarySelection === canonicalKey.toLowerCase() ||
Boolean(legacyKey && normalizedPrimarySelection === legacyKey.toLowerCase()) ||
normalizedPrimarySelection === normalizedCanonicalKey ||
Boolean(normalizedLegacyKey && normalizedPrimarySelection === normalizedLegacyKey) ||
normalizedPrimarySelection === normalizeLowercaseStringOrEmpty(params.model);
const perModelThinking =
configuredModels?.[canonicalKey]?.params?.thinking ??

View File

@@ -14,7 +14,7 @@ const NON_CREDENTIAL_FIELD_NAMES = new Set([
]);
function normalizeFieldName(value: string): string {
return value.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
return normalizeLowercaseStringOrEmpty(value.replaceAll(/[^a-z0-9]/gi, ""));
}
function isCredentialFieldName(key: string): boolean {

View File

@@ -5,7 +5,7 @@ import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
function isGemini31Model(modelId: string): boolean {
const normalized = modelId.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(modelId);
return normalized.includes("gemini-3.1-pro") || normalized.includes("gemini-3.1-flash");
}

View File

@@ -991,7 +991,9 @@ export async function handleToolExecutionEnd(
const approvalData: AgentApprovalEventData = {
phase: "resolved",
kind: "exec",
status: parsedApprovalResult.metadata.toLowerCase().includes("approval-request-failed")
status: normalizeOptionalLowercaseString(parsedApprovalResult.metadata)?.includes(
"approval-request-failed",
)
? "failed"
: "denied",
title: "Command approval resolved",

View File

@@ -403,7 +403,7 @@ export function extractMessagingToolSend(
const channelRaw = typeof args.channel === "string" ? args.channel.trim() : "";
const providerHint = providerRaw || channelRaw;
const providerId = providerHint ? normalizeChannelId(providerHint) : null;
const provider = providerId ?? (providerHint ? providerHint.toLowerCase() : "message");
const provider = providerId ?? normalizeOptionalLowercaseString(providerHint) ?? "message";
const to = normalizeTargetForProvider(provider, toRaw);
return to ? { tool: toolName, provider, accountId, to } : undefined;
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export function normalizeStructuredPromptSection(text: string): string {
return text
.replace(/\r\n?/g, "\n")
@@ -9,7 +11,7 @@ export function normalizePromptCapabilityIds(capabilities: ReadonlyArray<string>
const seen = new Set<string>();
const normalized: string[] = [];
for (const capability of capabilities) {
const value = normalizeStructuredPromptSection(capability).toLowerCase();
const value = normalizeLowercaseStringOrEmpty(normalizeStructuredPromptSection(capability));
if (!value || seen.has(value)) {
continue;
}

View File

@@ -3,6 +3,7 @@ import type { ModelDefinitionConfig } from "../config/types.js";
import type { ConfiguredModelProviderRequest } from "../config/types.provider-request.js";
import { assertSecretInputResolved } from "../config/types.secrets.js";
import type { PinnedDispatcherPolicy } from "../infra/net/ssrf.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import type {
ProviderRequestCapabilities,
ProviderRequestCapability,
@@ -358,7 +359,7 @@ export function mergeProviderRequestHeaders(
merged = Object.create(null) as Record<string, string>;
}
for (const [key, value] of Object.entries(headers)) {
const normalizedKey = key.toLowerCase();
const normalizedKey = normalizeLowercaseStringOrEmpty(key);
if (FORBIDDEN_HEADER_KEYS.has(normalizedKey)) {
continue;
}
@@ -496,12 +497,12 @@ function applyResolvedAuthHeader(
return headers;
}
const next = mergeProviderRequestHeaders(headers) ?? Object.create(null);
const keysToDelete = new Set([auth.headerName.toLowerCase()]);
const keysToDelete = new Set([normalizeLowercaseStringOrEmpty(auth.headerName)]);
if (auth.mode === "header") {
keysToDelete.add("authorization");
}
for (const key of Object.keys(next)) {
if (keysToDelete.has(key.toLowerCase())) {
if (keysToDelete.has(normalizeLowercaseStringOrEmpty(key))) {
delete next[key];
}
}
@@ -601,12 +602,12 @@ export function resolveProviderRequestPolicyConfig(
auth,
);
const protectedAttributionKeys = new Set(
Object.keys(policy.attributionHeaders ?? {}).map((key) => key.toLowerCase()),
Object.keys(policy.attributionHeaders ?? {}).map((key) => normalizeLowercaseStringOrEmpty(key)),
);
const unprotectedCallerHeaders = params.callerHeaders
? Object.fromEntries(
Object.entries(params.callerHeaders).filter(
([key]) => !protectedAttributionKeys.has(key.toLowerCase()),
([key]) => !protectedAttributionKeys.has(normalizeLowercaseStringOrEmpty(key)),
),
)
: undefined;

View File

@@ -48,7 +48,10 @@ function resolveArchiveType(spec: SkillInstallSpec, filename: string): string |
if (explicit) {
return explicit;
}
const lower = filename.toLowerCase();
const lower = normalizeOptionalLowercaseString(filename);
if (!lower) {
return undefined;
}
if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
return "tar.gz";
}

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import type { SkillsInstallPreferences } from "./skills/types.js";
export {
@@ -38,7 +39,7 @@ export function resolveSkillsInstallPreferences(config?: OpenClawConfig): Skills
const raw = config?.skills?.install;
const preferBrew = raw?.preferBrew ?? true;
const managerRaw = typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : "";
const manager = managerRaw.toLowerCase();
const manager = normalizeLowercaseStringOrEmpty(managerRaw);
const nodeManager: SkillsInstallPreferences["nodeManager"] =
manager === "pnpm" || manager === "yarn" || manager === "bun" || manager === "npm"
? manager

View File

@@ -7,7 +7,10 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import type { BootstrapContextMode } from "./bootstrap-files.js";
import {
mapToolContextToSpawnedRunMetadata,
@@ -452,11 +455,11 @@ export async function spawnSubagentDirect(
cfg?.agents?.defaults?.subagents?.allowAgents ??
[];
const allowAny = allowAgents.some((value) => value.trim() === "*");
const normalizedTargetId = targetAgentId.toLowerCase();
const normalizedTargetId = normalizeLowercaseStringOrEmpty(targetAgentId);
const allowSet = new Set(
allowAgents
.filter((value) => value.trim() && value.trim() !== "*")
.map((value) => normalizeAgentId(value).toLowerCase()),
.map((value) => normalizeLowercaseStringOrEmpty(normalizeAgentId(value))),
);
if (!allowAny && !allowSet.has(normalizedTargetId)) {
const allowedText = allowSet.size > 0 ? Array.from(allowSet).join(", ") : "none";

View File

@@ -70,10 +70,10 @@ function normalizeActionName(value: unknown): string | undefined {
function normalizeFingerprintValue(value: unknown): string | undefined {
if (typeof value === "string") {
const normalized = value.trim();
return normalized ? normalized.toLowerCase() : undefined;
return normalized ? normalizeLowercaseStringOrEmpty(normalized) : undefined;
}
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
return String(value).toLowerCase();
return normalizeLowercaseStringOrEmpty(String(value));
}
return undefined;
}
@@ -189,7 +189,7 @@ export function buildToolActionFingerprint(
appendFingerprintAlias(parts, record, "jobid", ["jobId", "job_id"]) || hasStableTarget;
hasStableTarget = appendFingerprintAlias(parts, record, "id", ["id"]) || hasStableTarget;
hasStableTarget = appendFingerprintAlias(parts, record, "model", ["model"]) || hasStableTarget;
const normalizedMeta = meta?.trim().replace(/\s+/g, " ").toLowerCase();
const normalizedMeta = normalizeOptionalLowercaseString(meta?.trim().replace(/\s+/g, " "));
// Meta text often carries volatile details (for example "N chars").
// Prefer stable arg-derived keys for matching; only fall back to meta
// when no stable target key is available.

View File

@@ -9,6 +9,7 @@ import { logVerbose, shouldLogVerbose } from "../../globals.js";
import { isInboundPathAllowed } from "../../media/inbound-path-policy.js";
import { getDefaultMediaLocalRoots } from "../../media/local-roots.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";
import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
import { type AnyAgentTool, imageResult, jsonResult, readStringParam } from "./common.js";
@@ -160,8 +161,7 @@ export function createCanvasTool(options?: { config?: OpenClawConfig }): AnyAgen
return jsonResult({ ok: true });
}
case "snapshot": {
const formatRaw =
typeof params.outputFormat === "string" ? params.outputFormat.toLowerCase() : "png";
const formatRaw = normalizeLowercaseStringOrEmpty(params.outputFormat) || "png";
const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png";
const maxWidth =
typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth)

View File

@@ -361,7 +361,7 @@ async function buildReminderContextLines(params: {
}
function stripThreadSuffixFromSessionKey(sessionKey: string): string {
const normalized = sessionKey.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(sessionKey);
const idx = normalized.lastIndexOf(":thread:");
if (idx <= 0) {
return sessionKey;

View File

@@ -6,6 +6,7 @@ import {
type OperatorScope,
} from "../../gateway/method-scopes.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { readStringParam } from "./common.js";
@@ -53,7 +54,7 @@ function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: strin
const origin = url.origin;
// Key: protocol + host only, lowercased. (host includes IPv6 brackets + port when present)
const key = `${url.protocol}//${url.host.toLowerCase()}`;
const key = `${url.protocol}//${normalizeLowercaseStringOrEmpty(url.host)}`;
return { origin, key };
}

View File

@@ -15,6 +15,7 @@ import {
} from "../../cli/nodes-screen.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { imageMimeFromFormat } from "../../media/mime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import type { ImageSanitizationLimits } from "../image-sanitization.js";
import { sanitizeToolResultImages } from "../tool-images.js";
import type { GatewayCallOptions } from "./gateway.js";
@@ -62,7 +63,7 @@ async function executeCameraSnap({
const node = requireString(params, "node");
const resolvedNode = await resolveNode(gatewayOpts, node);
const nodeId = resolvedNode.nodeId;
const facingRaw = typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
const facingRaw = normalizeLowercaseStringOrEmpty(params.facing) || "front";
const facings: CameraFacing[] =
facingRaw === "both"
? ["front", "back"]
@@ -107,7 +108,7 @@ async function executeCameraSnap({
idempotencyKey: crypto.randomUUID(),
});
const payload = parseCameraSnapPayload(raw?.payload);
const normalizedFormat = payload.format.toLowerCase();
const normalizedFormat = normalizeLowercaseStringOrEmpty(payload.format);
if (normalizedFormat !== "jpg" && normalizedFormat !== "jpeg" && normalizedFormat !== "png") {
throw new Error(`unsupported camera.snap format: ${payload.format}`);
}
@@ -210,7 +211,7 @@ async function executePhotosLatest({
for (const [index, photoRaw] of photos.entries()) {
const photo = parseCameraSnapPayload(photoRaw);
const normalizedFormat = photo.format.toLowerCase();
const normalizedFormat = normalizeLowercaseStringOrEmpty(photo.format);
if (normalizedFormat !== "jpg" && normalizedFormat !== "jpeg" && normalizedFormat !== "png") {
throw new Error(`unsupported photos.latest format: ${photo.format}`);
}
@@ -272,7 +273,7 @@ async function executeCameraClip({
const node = requireString(params, "node");
const resolvedNode = await resolveNode(gatewayOpts, node);
const nodeId = resolvedNode.nodeId;
const facing = typeof params.facing === "string" ? params.facing.toLowerCase() : "front";
const facing = normalizeLowercaseStringOrEmpty(params.facing) || "front";
if (facing !== "front" && facing !== "back") {
throw new Error("invalid facing (front|back)");
}

View File

@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { OperatorScope } from "../../gateway/method-scopes.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { resolveNodePairApprovalScopes } from "../../infra/node-pairing-authz.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveSessionAgentId } from "../agent-scope.js";
import { resolveImageSanitizationLimits } from "../image-sanitization.js";
@@ -73,7 +74,7 @@ async function resolveNodePairApproveScopes(
}
function isPairingRequiredMessage(message: string): boolean {
const lower = message.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(message);
return lower.includes("pairing required") || lower.includes("not_paired");
}

View File

@@ -40,7 +40,7 @@ function messageFromError(error: unknown): string {
}
function shouldFallbackToPairList(error: unknown): boolean {
const message = messageFromError(error).toLowerCase();
const message = normalizeOptionalLowercaseString(messageFromError(error)) ?? "";
if (!message.includes("node.list")) {
return false;
}

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { sanitizeHtml, stripInvisibleUnicode } from "./web-fetch-visibility.js";
export type ExtractMode = "markdown" | "text";
@@ -169,7 +170,7 @@ function exceedsEstimatedHtmlNestingDepth(html: string, maxDepth: number): boole
j += 1;
}
const tagName = html.slice(nameStart, j).toLowerCase();
const tagName = normalizeLowercaseStringOrEmpty(html.slice(nameStart, j));
if (!tagName) {
continue;
}

View File

@@ -1,4 +1,7 @@
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../../shared/string-coerce.js";
// CSS property values that indicate an element is hidden
const HIDDEN_STYLE_PATTERNS: Array<[string, RegExp]> = [
@@ -24,7 +27,7 @@ const HIDDEN_CLASS_NAMES = new Set([
]);
function hasHiddenClass(className: string): boolean {
const classes = className.toLowerCase().split(/\s+/);
const classes = normalizeLowercaseStringOrEmpty(className).split(/\s+/);
return classes.some((cls) => HIDDEN_CLASS_NAMES.has(cls));
}
@@ -88,7 +91,7 @@ function isStyleHidden(style: string): boolean {
}
function shouldRemoveElement(element: Element): boolean {
const tagName = element.tagName.toLowerCase();
const tagName = normalizeLowercaseStringOrEmpty(element.tagName);
// Always-remove tags
if (["meta", "template", "svg", "canvas", "iframe", "object", "embed"].includes(tagName)) {

View File

@@ -5,6 +5,7 @@ import { logDebug } from "../../logger.js";
import type { RuntimeWebFetchMetadata } from "../../secrets/runtime-web-tools.types.js";
import { wrapExternalContent, wrapWebContent } from "../../security/external-content.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
@@ -130,7 +131,7 @@ function looksLikeHtml(value: string): boolean {
if (!trimmed) {
return false;
}
const head = trimmed.slice(0, 256).toLowerCase();
const head = normalizeLowercaseStringOrEmpty(trimmed.slice(0, 256));
return head.startsWith("<!doctype html") || head.startsWith("<html");
}