mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
refactor: dedupe agent lowercase helpers
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user