diff --git a/extensions/comfy/workflow-runtime.ts b/extensions/comfy/workflow-runtime.ts index 4514bd5e8a1..bbd632a98be 100644 --- a/extensions/comfy/workflow-runtime.ts +++ b/extensions/comfy/workflow-runtime.ts @@ -17,7 +17,11 @@ import { ssrfPolicyFromDangerouslyAllowPrivateNetwork, type SsrFPolicy, } from "openclaw/plugin-sdk/ssrf-runtime"; -import { isRecord, resolveUserPath } from "openclaw/plugin-sdk/text-runtime"; +import { + isRecord, + normalizeOptionalString, + resolveUserPath, +} from "openclaw/plugin-sdk/text-runtime"; const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188"; const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org"; @@ -87,15 +91,6 @@ export function _setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | comfyFetchGuard = impl ?? fetchWithSsrFGuard; } -function readConfigString(config: ComfyProviderConfig, key: string): string | undefined { - const value = config[key]; - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - function readConfigBoolean(config: ComfyProviderConfig, key: string): boolean | undefined { const value = config[key]; return typeof value === "boolean" ? value : undefined; @@ -161,11 +156,11 @@ export function getComfyCapabilityConfig( } export function resolveComfyMode(config: ComfyProviderConfig): ComfyMode { - return readConfigString(config, "mode") === "cloud" ? "cloud" : "local"; + return normalizeOptionalString(config.mode) === "cloud" ? "cloud" : "local"; } function getRequiredConfigString(config: ComfyProviderConfig, key: string): string { - const value = readConfigString(config, key); + const value = normalizeOptionalString(config[key]); if (!value) { throw new Error(`models.providers.comfy.${key} is required`); } @@ -180,7 +175,7 @@ function resolveComfyWorkflowSource(config: ComfyProviderConfig): { if (isRecord(workflow)) { return { workflow: structuredClone(workflow) }; } - const workflowPath = readConfigString(config, "workflowPath"); + const workflowPath = normalizeOptionalString(config.workflowPath); return { workflowPath }; } @@ -230,7 +225,7 @@ function resolveComfyNetworkPolicy(params: { return {}; } - const hostname = parsed.hostname.trim().toLowerCase(); + const hostname = normalizeOptionalString(parsed.hostname)?.toLowerCase() ?? ""; if (!hostname || !params.allowPrivateNetwork || !isPrivateOrLoopbackHost(hostname)) { return {}; } diff --git a/src/gateway/connection-details.ts b/src/gateway/connection-details.ts index 752e1e3cda9..078a03ca1cd 100644 --- a/src/gateway/connection-details.ts +++ b/src/gateway/connection-details.ts @@ -17,10 +17,6 @@ type GatewayConnectionDetailResolvers = { resolveGatewayPort?: (cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) => number; }; -function trimToUndefined(value: string | undefined): string | undefined { - return normalizeOptionalString(value); -} - export function buildGatewayConnectionDetailsWithResolvers( options: { config?: OpenClawConfig; @@ -46,7 +42,7 @@ export function buildGatewayConnectionDetailsWithResolvers( const cliUrlOverride = normalizeOptionalString(options.url); const envUrlOverride = cliUrlOverride ? undefined - : trimToUndefined(process.env.OPENCLAW_GATEWAY_URL); + : normalizeOptionalString(process.env.OPENCLAW_GATEWAY_URL); const urlOverride = cliUrlOverride ?? envUrlOverride; const remoteUrl = normalizeOptionalString(remote?.url); const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl; diff --git a/src/gateway/exec-approval-ios-push.ts b/src/gateway/exec-approval-ios-push.ts index 320ebb5b5de..75e4251f3c5 100644 --- a/src/gateway/exec-approval-ios-push.ts +++ b/src/gateway/exec-approval-ios-push.ts @@ -20,6 +20,7 @@ import { type ApnsRelayConfig, } from "../infra/push-apns.js"; import { roleScopesAllow } from "../shared/operator-scope-compat.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; const APPROVALS_SCOPE = "operator.approvals"; const OPERATOR_ROLE = "operator"; @@ -47,7 +48,7 @@ type ApprovalDeliveryState = { }; function isIosPlatform(platform: string | undefined): boolean { - const normalized = platform?.trim().toLowerCase() ?? ""; + const normalized = normalizeOptionalString(platform)?.toLowerCase() ?? ""; return normalized.startsWith("ios") || normalized.startsWith("ipados"); } diff --git a/src/gateway/protocol/client-info.ts b/src/gateway/protocol/client-info.ts index e63b3cd2461..2829e5dfab2 100644 --- a/src/gateway/protocol/client-info.ts +++ b/src/gateway/protocol/client-info.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../../shared/string-coerce.js"; + export const GATEWAY_CLIENT_IDS = { WEBCHAT_UI: "webchat-ui", CONTROL_UI: "openclaw-control-ui", @@ -53,7 +55,7 @@ const GATEWAY_CLIENT_ID_SET = new Set(Object.values(GATEWAY_CLI const GATEWAY_CLIENT_MODE_SET = new Set(Object.values(GATEWAY_CLIENT_MODES)); export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined { - const normalized = raw?.trim().toLowerCase(); + const normalized = normalizeOptionalString(raw)?.toLowerCase(); if (!normalized) { return undefined; } @@ -67,7 +69,7 @@ export function normalizeGatewayClientName(raw?: string | null): GatewayClientNa } export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined { - const normalized = raw?.trim().toLowerCase(); + const normalized = normalizeOptionalString(raw)?.toLowerCase(); if (!normalized) { return undefined; } diff --git a/src/gateway/resolve-configured-secret-input-string.ts b/src/gateway/resolve-configured-secret-input-string.ts index 9b3687b8844..8ca3de10569 100644 --- a/src/gateway/resolve-configured-secret-input-string.ts +++ b/src/gateway/resolve-configured-secret-input-string.ts @@ -2,6 +2,7 @@ import type { OpenClawConfig } from "../config/types.js"; import { resolveSecretInputRef } from "../config/types.secrets.js"; import { secretRefKey } from "../secrets/ref-contract.js"; import { resolveSecretRefValues } from "../secrets/resolve.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type SecretInputUnresolvedReasonStyle = "generic" | "detailed"; // pragma: allowlist secret export type ConfiguredSecretInputSource = @@ -9,14 +10,6 @@ export type ConfiguredSecretInputSource = | "secretRef" // pragma: allowlist secret | "fallback"; -function trimToUndefined(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - function buildUnresolvedReason(params: { path: string; style: SecretInputUnresolvedReasonStyle; @@ -48,7 +41,7 @@ export async function resolveConfiguredSecretInputString(params: { defaults: params.config.secrets?.defaults, }); if (!ref) { - return { value: trimToUndefined(params.value) }; + return { value: normalizeOptionalString(params.value) }; } const refLabel = `${ref.source}:${ref.provider}:${ref.id}`; @@ -68,8 +61,8 @@ export async function resolveConfiguredSecretInputString(params: { }), }; } - const trimmed = resolvedValue.trim(); - if (trimmed.length === 0) { + const trimmed = normalizeOptionalString(resolvedValue); + if (!trimmed) { return { unresolvedRefReason: buildUnresolvedReason({ path: params.path, @@ -109,7 +102,7 @@ export async function resolveConfiguredSecretInputWithFallback(params: { value: params.value, defaults: params.config.secrets?.defaults, }); - const configValue = !ref ? trimToUndefined(params.value) : undefined; + const configValue = !ref ? normalizeOptionalString(params.value) : undefined; if (configValue) { return { value: configValue, diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 8ad45cb8005..0b4791e2bd9 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -35,6 +35,7 @@ import { enqueueSystemEvent } from "../infra/system-events.js"; import { getChildLogger } from "../logging.js"; import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type GatewayCronState = { cron: CronService; @@ -44,14 +45,6 @@ export type GatewayCronState = { const CRON_WEBHOOK_TIMEOUT_MS = 10_000; -function trimToOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - function redactWebhookUrl(url: string): string { try { const parsed = new URL(url); @@ -71,7 +64,7 @@ function resolveCronWebhookTarget(params: { legacyNotify?: boolean; legacyWebhook?: string; }): CronWebhookTarget | null { - const mode = params.delivery?.mode?.trim().toLowerCase(); + const mode = normalizeOptionalString(params.delivery?.mode)?.toLowerCase(); if (mode === "webhook") { const url = normalizeHttpWebhookUrl(params.delivery?.to); return url ? { url, source: "delivery" } : null; @@ -314,7 +307,7 @@ export function buildGatewayCronService(params: { }, sendCronFailureAlert: async ({ job, text, channel, to, mode, accountId }) => { const { agentId, cfg: runtimeConfig } = resolveCronAgent(job.agentId); - const webhookToken = trimToOptionalString(params.cfg.cron?.webhookToken); + const webhookToken = normalizeOptionalString(params.cfg.cron?.webhookToken); // Webhook mode requires a URL - fail closed if missing if (mode === "webhook" && !to) { @@ -375,8 +368,8 @@ export function buildGatewayCronService(params: { onEvent: (evt) => { params.broadcast("cron", evt, { dropIfSlow: true }); if (evt.action === "finished") { - const webhookToken = trimToOptionalString(params.cfg.cron?.webhookToken); - const legacyWebhook = trimToOptionalString(params.cfg.cron?.webhook); + const webhookToken = normalizeOptionalString(params.cfg.cron?.webhookToken); + const legacyWebhook = normalizeOptionalString(params.cfg.cron?.webhook); const job = cron.getJob(evt.jobId); const legacyNotify = (job as { notify?: unknown } | undefined)?.notify === true; const webhookTarget = resolveCronWebhookTarget({ diff --git a/src/gateway/server-mobile-nodes.ts b/src/gateway/server-mobile-nodes.ts index c4baeaf1437..2352b340ec1 100644 --- a/src/gateway/server-mobile-nodes.ts +++ b/src/gateway/server-mobile-nodes.ts @@ -1,7 +1,8 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { NodeRegistry } from "./node-registry.js"; const isMobilePlatform = (platform: unknown): boolean => { - const p = typeof platform === "string" ? platform.trim().toLowerCase() : ""; + const p = normalizeOptionalString(platform)?.toLowerCase() ?? ""; if (!p) { return false; } diff --git a/src/gateway/server/ws-connection/auth-context.ts b/src/gateway/server/ws-connection/auth-context.ts index 233ad9e6712..325139a5ed2 100644 --- a/src/gateway/server/ws-connection/auth-context.ts +++ b/src/gateway/server/ws-connection/auth-context.ts @@ -1,4 +1,5 @@ import type { IncomingMessage } from "node:http"; +import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, @@ -40,19 +41,11 @@ export type ConnectAuthDecision = { authMethod: GatewayAuthResult["method"]; }; -function trimToUndefined(value: string | undefined): string | undefined { - if (!value) { - return undefined; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - function resolveSharedConnectAuth( connectAuth: HandshakeConnectAuth | null | undefined, ): { token?: string; password?: string } | undefined { - const token = trimToUndefined(connectAuth?.token); - const password = trimToUndefined(connectAuth?.password); + const token = normalizeOptionalString(connectAuth?.token); + const password = normalizeOptionalString(connectAuth?.password); if (!token && !password) { return undefined; } @@ -63,11 +56,11 @@ function resolveDeviceTokenCandidate(connectAuth: HandshakeConnectAuth | null | token?: string; source?: DeviceTokenCandidateSource; } { - const explicitDeviceToken = trimToUndefined(connectAuth?.deviceToken); + const explicitDeviceToken = normalizeOptionalString(connectAuth?.deviceToken); if (explicitDeviceToken) { return { token: explicitDeviceToken, source: "explicit-device-token" }; } - const fallbackToken = trimToUndefined(connectAuth?.token); + const fallbackToken = normalizeOptionalString(connectAuth?.token); if (!fallbackToken) { return {}; } @@ -77,7 +70,7 @@ function resolveDeviceTokenCandidate(connectAuth: HandshakeConnectAuth | null | function resolveBootstrapTokenCandidate( connectAuth: HandshakeConnectAuth | null | undefined, ): string | undefined { - return trimToUndefined(connectAuth?.bootstrapToken); + return normalizeOptionalString(connectAuth?.bootstrapToken); } export async function resolveConnectAuthState(params: {