refactor: dedupe ui foundry trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:34:40 +01:00
parent aec24f4599
commit b52f106533
9 changed files with 89 additions and 82 deletions

View File

@@ -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<string> {
@@ -60,7 +68,7 @@ export async function execAzAsync(args: string[]): Promise<string> {
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

View File

@@ -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<string>();
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;
}

View File

@@ -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;

View File

@@ -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)
);
}

View File

@@ -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}`;
}

View File

@@ -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}`

View File

@@ -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=<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, {

View File

@@ -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;
}

View File

@@ -23,6 +23,7 @@ type PersistedUiSettings = Omit<UiSettings, "token" | "sessionKey" | "lastActive
import { isSupportedLocale } from "../i18n/index.ts";
import { getSafeLocalStorage, getSafeSessionStorage } from "../local-storage.ts";
import { inferBasePathFromPathname, normalizeBasePath } from "./navigation.ts";
import { normalizeOptionalString } from "./string-coerce.ts";
import { parseThemeSelection, type ThemeMode, type ThemeName } from "./theme.ts";
export const BORDER_RADIUS_STOPS = [0, 25, 50, 75, 100] as const;
@@ -75,8 +76,7 @@ function deriveDefaultGatewayUrl(): { pageUrl: string; effectiveUrl: string } {
const proto = location.protocol === "https:" ? "wss" : "ws";
const configured =
typeof window !== "undefined" &&
typeof window.__OPENCLAW_CONTROL_UI_BASE_PATH__ === "string" &&
window.__OPENCLAW_CONTROL_UI_BASE_PATH__.trim();
normalizeOptionalString(window.__OPENCLAW_CONTROL_UI_BASE_PATH__);
const basePath = configured
? normalizeBasePath(configured)
: inferBasePathFromPathname(location.pathname);
@@ -93,7 +93,7 @@ function getSessionStorage(): Storage | null {
}
function normalizeGatewayTokenScope(gatewayUrl: string): string {
const trimmed = gatewayUrl.trim();
const trimmed = normalizeOptionalString(gatewayUrl) ?? "";
if (!trimmed) {
return "default";
}
@@ -122,27 +122,20 @@ function resolveScopedSessionSelection(
): ScopedSessionSelection {
const scope = normalizeGatewayTokenScope(gatewayUrl);
const scoped = parsed.sessionsByGateway?.[scope];
if (
scoped &&
typeof scoped.sessionKey === "string" &&
scoped.sessionKey.trim() &&
typeof scoped.lastActiveSessionKey === "string" &&
scoped.lastActiveSessionKey.trim()
) {
const scopedSessionKey = normalizeOptionalString(scoped?.sessionKey);
const scopedLastActiveSessionKey = normalizeOptionalString(scoped?.lastActiveSessionKey);
if (scopedSessionKey && scopedLastActiveSessionKey) {
return {
sessionKey: scoped.sessionKey.trim(),
lastActiveSessionKey: scoped.lastActiveSessionKey.trim(),
sessionKey: scopedSessionKey,
lastActiveSessionKey: scopedLastActiveSessionKey,
};
}
const legacySessionKey =
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
? parsed.sessionKey.trim()
: defaults.sessionKey;
const legacySessionKey = normalizeOptionalString(parsed.sessionKey) ?? defaults.sessionKey;
const legacyLastActiveSessionKey =
typeof parsed.lastActiveSessionKey === "string" && parsed.lastActiveSessionKey.trim()
? parsed.lastActiveSessionKey.trim()
: legacySessionKey || defaults.lastActiveSessionKey;
normalizeOptionalString(parsed.lastActiveSessionKey) ??
legacySessionKey ??
defaults.lastActiveSessionKey;
return {
sessionKey: legacySessionKey,
@@ -157,8 +150,8 @@ function loadSessionToken(gatewayUrl: string): string {
return "";
}
storage.removeItem(LEGACY_TOKEN_SESSION_KEY);
const token = storage.getItem(tokenSessionKeyForGateway(gatewayUrl)) ?? "";
return token.trim();
const token = storage.getItem(tokenSessionKeyForGateway(gatewayUrl));
return normalizeOptionalString(token) ?? "";
} catch {
return "";
}
@@ -172,7 +165,7 @@ function persistSessionToken(gatewayUrl: string, token: string) {
}
storage.removeItem(LEGACY_TOKEN_SESSION_KEY);
const key = tokenSessionKeyForGateway(gatewayUrl);
const normalized = token.trim();
const normalized = normalizeOptionalString(token) ?? "";
if (normalized) {
storage.setItem(key, normalized);
return;
@@ -215,10 +208,7 @@ export function loadSettings(): UiSettings {
return defaults;
}
const parsed = JSON.parse(raw) as PersistedUiSettings;
const parsedGatewayUrl =
typeof parsed.gatewayUrl === "string" && parsed.gatewayUrl.trim()
? parsed.gatewayUrl.trim()
: defaults.gatewayUrl;
const parsedGatewayUrl = normalizeOptionalString(parsed.gatewayUrl) ?? defaults.gatewayUrl;
const gatewayUrl = parsedGatewayUrl === pageDerivedUrl ? defaultUrl : parsedGatewayUrl;
const scopedSessionSelection = resolveScopedSessionSelection(gatewayUrl, parsed, defaults);
const { theme, mode } = parseThemeSelection(