diff --git a/extensions/microsoft-foundry/cli.ts b/extensions/microsoft-foundry/cli.ts index 17339dd307e..f73c44e67ff 100644 --- a/extensions/microsoft-foundry/cli.ts +++ b/extensions/microsoft-foundry/cli.ts @@ -1,4 +1,8 @@ import { execFile, execFileSync, spawn } from "node:child_process"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { AzAccessToken, AzAccount } from "./shared.js"; import { COGNITIVE_SERVICES_RESOURCE } from "./shared.js"; @@ -38,11 +42,15 @@ function buildAzCommandError(error: Error, stderr: string, stdout: string): Erro } export function execAz(args: string[]): string { - return execFileSync("az", args, { - encoding: "utf-8", - timeout: 30_000, - shell: process.platform === "win32", - }).trim(); + return ( + normalizeOptionalString( + execFileSync("az", args, { + encoding: "utf-8", + timeout: 30_000, + shell: process.platform === "win32", + }), + ) ?? "" + ); } export async function execAzAsync(args: string[]): Promise { @@ -60,7 +68,7 @@ export async function execAzAsync(args: string[]): Promise { reject(buildAzCommandError(error, String(stderr ?? ""), String(stdout ?? ""))); return; } - resolve(String(stdout).trim()); + resolve(normalizeStringifiedOptionalString(stdout) ?? ""); }, ); }); @@ -177,7 +185,7 @@ export async function azLoginDeviceCodeWithOptions(params: { resolve(); return; } - const output = [...stderrChunks, ...stdoutChunks].join("").trim(); + const output = normalizeOptionalString([...stderrChunks, ...stdoutChunks].join("")) ?? ""; reject( new Error( output diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index a2a73379af2..c50fbfd0450 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -1,5 +1,9 @@ import type { ProviderAuthContext } from "openclaw/plugin-sdk/core"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { azLoginDeviceCode, azLoginDeviceCodeWithOptions, @@ -63,8 +67,9 @@ export function listFoundryResources(subscriptionId?: string): FoundryResourceOp if (account.kind !== "AIServices") { continue; } - const endpoint = account.customSubdomain?.trim() - ? `https://${account.customSubdomain.trim()}.services.ai.azure.com` + const customSubdomain = normalizeOptionalString(account.customSubdomain); + const endpoint = customSubdomain + ? `https://${customSubdomain}.services.ai.azure.com` : undefined; if (!endpoint) { continue; @@ -247,7 +252,7 @@ async function promptEndpointAndModelBase( placeholder: "https://xxx.openai.azure.com or https://xxx.services.ai.azure.com", ...(options?.endpointInitialValue ? { initialValue: options.endpointInitialValue } : {}), validate: (v) => { - const val = String(v ?? "").trim(); + const val = normalizeStringifiedOptionalString(v) ?? ""; if (!val) { return "Endpoint URL is required"; } @@ -266,7 +271,7 @@ async function promptEndpointAndModelBase( ...(options?.modelInitialValue ? { initialValue: options.modelInitialValue } : {}), placeholder: "gpt-4o", validate: (v) => { - const val = String(v ?? "").trim(); + const val = normalizeStringifiedOptionalString(v) ?? ""; if (!val) { return "Model ID is required"; } @@ -347,21 +352,21 @@ export function extractTenantSuggestions( const seen = new Set(); const regex = /([0-9a-fA-F-]{36})(?:\s+'([^'\r\n]+)')?/g; for (const match of rawMessage.matchAll(regex)) { - const id = match[1]?.trim(); + const id = normalizeOptionalString(match[1]); if (!id || seen.has(id)) { continue; } seen.add(id); suggestions.push({ id, - ...(match[2]?.trim() ? { label: match[2].trim() } : {}), + ...(normalizeOptionalString(match[2]) ? { label: normalizeOptionalString(match[2]) } : {}), }); } return suggestions; } export function isValidTenantIdentifier(value: string): boolean { - const trimmed = value.trim(); + const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed) { return false; } @@ -403,7 +408,7 @@ export async function promptTenantId( message: params?.required ? "Azure tenant ID" : "Azure tenant ID (optional)", placeholder: params?.suggestions?.[0]?.id ?? "00000000-0000-0000-0000-000000000000", validate: (value) => { - const trimmed = String(value ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(value) ?? ""; if (!trimmed) { return params?.required ? "Tenant ID is required" : undefined; } diff --git a/extensions/microsoft-foundry/runtime.ts b/extensions/microsoft-foundry/runtime.ts index b094f7a0a0d..96630be06f5 100644 --- a/extensions/microsoft-foundry/runtime.ts +++ b/extensions/microsoft-foundry/runtime.ts @@ -1,6 +1,7 @@ import type { ProviderPrepareRuntimeAuthContext } from "openclaw/plugin-sdk/core"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { ensureAuthProfileStore } from "openclaw/plugin-sdk/provider-auth"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { getAccessTokenResultAsync } from "./cli.js"; import { type CachedTokenEntry, @@ -45,11 +46,9 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC const credential = ctx.profileId ? authStore.profiles[ctx.profileId] : undefined; const metadata = credential?.type === "api_key" ? credential.metadata : undefined; const modelId = - typeof ctx.modelId === "string" && ctx.modelId.trim().length > 0 - ? ctx.modelId.trim() - : typeof metadata?.modelId === "string" && metadata.modelId.trim().length > 0 - ? metadata.modelId.trim() - : ctx.modelId; + normalizeOptionalString(ctx.modelId) ?? + normalizeOptionalString(metadata?.modelId) ?? + ctx.modelId; const activeModelNameHint = ctx.modelId === metadata?.modelId ? metadata?.modelName : undefined; const modelNameHint = resolveConfiguredModelNameHint( modelId, @@ -62,9 +61,8 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC ? ctx.model.api : undefined; const endpoint = - typeof metadata?.endpoint === "string" && metadata.endpoint.trim().length > 0 - ? metadata.endpoint.trim() - : extractFoundryEndpoint(ctx.model.baseUrl ?? ""); + normalizeOptionalString(metadata?.endpoint) ?? + extractFoundryEndpoint(ctx.model.baseUrl ?? ""); const baseUrl = endpoint ? buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, configuredApi) : undefined; diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index 477024934b8..c42f23d2770 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -5,7 +5,10 @@ import { type SecretInput, } from "openclaw/plugin-sdk/provider-auth"; import type { ModelApi, ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; export const PROVIDER_ID = "microsoft-foundry"; export const DEFAULT_API = "openai-completions"; @@ -162,7 +165,7 @@ export function isFoundryProviderApi(value?: string | null): value is FoundryPro } export function normalizeFoundryEndpoint(endpoint: string): string { - const trimmed = endpoint.trim(); + const trimmed = normalizeOptionalString(endpoint) ?? ""; if (!trimmed) { return trimmed; } @@ -256,11 +259,11 @@ export function resolveConfiguredModelNameHint( modelId: string, modelNameHint?: string | null, ): string | undefined { - const trimmedName = typeof modelNameHint === "string" ? modelNameHint.trim() : ""; + const trimmedName = normalizeOptionalString(modelNameHint) ?? ""; if (trimmedName) { return trimmedName; } - const trimmedId = modelId.trim(); + const trimmedId = normalizeOptionalString(modelId) ?? ""; return trimmedId ? trimmedId : undefined; } @@ -482,7 +485,7 @@ export function resolveFoundryTargetProfileId(config: FoundryConfigShape): strin } // Prefer the explicitly ordered profile; fall back to the sole entry when there is exactly one. return ( - config.auth?.order?.[PROVIDER_ID]?.find((profileId) => profileId.trim().length > 0) ?? + config.auth?.order?.[PROVIDER_ID]?.find((profileId) => normalizeOptionalString(profileId)) ?? (configuredProfileEntries.length === 1 ? configuredProfileEntries[0]?.[0] : undefined) ); } diff --git a/ui/src/ui/app-channels.ts b/ui/src/ui/app-channels.ts index eb05e83e81b..fa63085667c 100644 --- a/ui/src/ui/app-channels.ts +++ b/ui/src/ui/app-channels.ts @@ -6,6 +6,7 @@ import { waitWhatsAppLogin, } from "./controllers/channels.ts"; import { loadConfig, saveConfig } from "./controllers/config.ts"; +import { normalizeOptionalString } from "./string-coerce.ts"; import type { NostrProfile } from "./types.ts"; import { createNostrProfileFormState } from "./views/channels.nostr-profile-form.ts"; @@ -67,15 +68,15 @@ function buildNostrProfileUrl(accountId: string, suffix = ""): string { } function resolveGatewayHttpAuthHeader(host: OpenClawApp): string | null { - const deviceToken = host.hello?.auth?.deviceToken?.trim(); + const deviceToken = normalizeOptionalString(host.hello?.auth?.deviceToken); if (deviceToken) { return `Bearer ${deviceToken}`; } - const token = host.settings.token.trim(); + const token = normalizeOptionalString(host.settings.token); if (token) { return `Bearer ${token}`; } - const password = host.password.trim(); + const password = normalizeOptionalString(host.password); if (password) { return `Bearer ${password}`; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 38a5f86319a..bff166eece9 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -43,6 +43,7 @@ import { import { GatewayBrowserClient } from "./gateway.ts"; import type { Tab } from "./navigation.ts"; import type { UiSettings } from "./storage.ts"; +import { normalizeOptionalString } from "./string-coerce.ts"; import type { AgentsListResult, PresenceEntry, @@ -111,7 +112,7 @@ export function resolveControlUiClientVersion(params: { serverVersion: string | null; pageUrl?: string; }): string | undefined { - const serverVersion = params.serverVersion?.trim(); + const serverVersion = normalizeOptionalString(params.serverVersion); if (!serverVersion) { return undefined; } @@ -137,16 +138,16 @@ function normalizeSessionKeyForDefaults( value: string | undefined, defaults: SessionDefaultsSnapshot, ): string { - const raw = (value ?? "").trim(); - const mainSessionKey = defaults.mainSessionKey?.trim(); + const raw = normalizeOptionalString(value) ?? ""; + const mainSessionKey = normalizeOptionalString(defaults.mainSessionKey); if (!mainSessionKey) { return raw; } if (!raw) { return mainSessionKey; } - const mainKey = defaults.mainKey?.trim() || "main"; - const defaultAgentId = defaults.defaultAgentId?.trim(); + const mainKey = normalizeOptionalString(defaults.mainKey) ?? "main"; + const defaultAgentId = normalizeOptionalString(defaults.defaultAgentId); const isAlias = raw === "main" || raw === mainKey || @@ -217,8 +218,8 @@ export function connectGateway(host: GatewayHost, options?: ConnectGatewayOption }); const client = new GatewayBrowserClient({ url: host.settings.gatewayUrl, - token: host.settings.token.trim() ? host.settings.token : undefined, - password: host.password.trim() ? host.password : undefined, + token: normalizeOptionalString(host.settings.token) ? host.settings.token : undefined, + password: normalizeOptionalString(host.password) ? host.password : undefined, clientName: "openclaw-control-ui", clientVersion, mode: "webchat", @@ -397,10 +398,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { if (evt.event === "shutdown") { const payload = evt.payload as { reason?: unknown; restartExpectedMs?: unknown } | undefined; - const reason = - payload && typeof payload.reason === "string" && payload.reason.trim() - ? payload.reason.trim() - : "gateway stopping"; + const reason = normalizeOptionalString(payload?.reason) ?? "gateway stopping"; const shutdownMessage = typeof payload?.restartExpectedMs === "number" ? `Restarting: ${reason}` diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index 80437908d20..890f3b0c3f9 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -34,6 +34,7 @@ import { type Tab, } from "./navigation.ts"; import { saveSettings, type UiSettings } from "./storage.ts"; +import { normalizeOptionalString } from "./string-coerce.ts"; import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts"; import { resolveTheme, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts"; import type { AgentsListResult, AttentionItem } from "./types.ts"; @@ -73,7 +74,10 @@ type SettingsHost = { export function applySettings(host: SettingsHost, next: UiSettings) { const normalized = { ...next, - lastActiveSessionKey: next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || "main", + lastActiveSessionKey: + normalizeOptionalString(next.lastActiveSessionKey) ?? + normalizeOptionalString(next.sessionKey) ?? + "main", }; host.settings = normalized; saveSettings(normalized); @@ -109,7 +113,7 @@ export function applySettingsFromUrl(host: SettingsHost) { const hashParams = new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl"); - const nextGatewayUrl = gatewayUrlRaw?.trim() ?? ""; + const nextGatewayUrl = normalizeOptionalString(gatewayUrlRaw) ?? ""; const gatewayUrlChanged = Boolean(nextGatewayUrl && nextGatewayUrl !== host.settings.gatewayUrl); // Prefer fragment tokens over query tokens. Fragments avoid server-side request // logs and referrer leakage; query-param tokens remain a one-time legacy fallback @@ -119,9 +123,9 @@ export function applySettingsFromUrl(host: SettingsHost) { const tokenRaw = hashToken ?? queryToken; const passwordRaw = params.get("password") ?? hashParams.get("password"); const sessionRaw = params.get("session") ?? hashParams.get("session"); - const shouldResetSessionForToken = Boolean( - tokenRaw?.trim() && !sessionRaw?.trim() && !gatewayUrlChanged, - ); + const token = normalizeOptionalString(tokenRaw); + const session = normalizeOptionalString(sessionRaw); + const shouldResetSessionForToken = Boolean(token && !session && !gatewayUrlChanged); let shouldCleanUrl = false; if (params.has("token")) { @@ -136,7 +140,6 @@ export function applySettingsFromUrl(host: SettingsHost) { "[openclaw] Auth token passed as query parameter (?token=). Use URL fragment instead: #token=. Query parameters may appear in server logs.", ); } - const token = tokenRaw.trim(); if (token && gatewayUrlChanged) { host.pendingGatewayToken = token; } else if (token && token !== host.settings.token) { @@ -163,7 +166,6 @@ export function applySettingsFromUrl(host: SettingsHost) { } if (sessionRaw != null) { - const session = sessionRaw.trim(); if (session) { host.sessionKey = session; applySettings(host, { @@ -177,7 +179,7 @@ export function applySettingsFromUrl(host: SettingsHost) { if (gatewayUrlRaw != null) { if (gatewayUrlChanged) { host.pendingGatewayUrl = nextGatewayUrl; - if (!tokenRaw?.trim()) { + if (!token) { host.pendingGatewayToken = null; } } else { @@ -328,8 +330,9 @@ export function inferBasePath() { return ""; } const configured = window.__OPENCLAW_CONTROL_UI_BASE_PATH__; - if (typeof configured === "string" && configured.trim()) { - return normalizeBasePath(configured); + const normalizedConfigured = normalizeOptionalString(configured); + if (normalizedConfigured) { + return normalizeBasePath(normalizedConfigured); } return inferBasePathFromPathname(window.location.pathname); } @@ -433,7 +436,7 @@ export function onPopState(host: SettingsHost) { } const url = new URL(window.location.href); - const session = url.searchParams.get("session")?.trim(); + const session = normalizeOptionalString(url.searchParams.get("session")); if (session) { host.sessionKey = session; applySettings(host, { diff --git a/ui/src/ui/session-key.ts b/ui/src/ui/session-key.ts index d78d297dc38..8594699053a 100644 --- a/ui/src/ui/session-key.ts +++ b/ui/src/ui/session-key.ts @@ -1,6 +1,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, + normalizeOptionalString, } from "./string-coerce.ts"; export type ParsedAgentSessionKey = { @@ -27,7 +28,7 @@ export function parseAgentSessionKey( if (parts.length < 3 || parts[0] !== "agent") { return null; } - const agentId = parts[1]?.trim(); + const agentId = normalizeOptionalString(parts[1]); const rest = parts.slice(2).join(":"); if (!agentId || !rest) { return null; @@ -40,7 +41,7 @@ export function normalizeMainKey(value: string | undefined | null): string { } export function normalizeAgentId(value: string | undefined | null): string { - const trimmed = (value ?? "").trim(); + const trimmed = normalizeOptionalString(value) ?? ""; if (!trimmed) { return DEFAULT_AGENT_ID; } @@ -71,7 +72,7 @@ export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | nu } export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean { - const raw = (sessionKey ?? "").trim(); + const raw = normalizeOptionalString(sessionKey) ?? ""; if (!raw) { return false; } diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts index 61b398c3f66..bc5c72dfb51 100644 --- a/ui/src/ui/storage.ts +++ b/ui/src/ui/storage.ts @@ -23,6 +23,7 @@ type PersistedUiSettings = Omit