refactor: dedupe helper string normalization

This commit is contained in:
Peter Steinberger
2026-04-07 07:05:34 +01:00
parent 997a16fa50
commit 1b2f640c5a
16 changed files with 58 additions and 76 deletions

View File

@@ -1,7 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function normalizeText(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}

View File

@@ -6,6 +6,7 @@ import { onAgentEvent } from "../infra/agent-events.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { scopedHeartbeatWakeOptions } from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { recordTaskRunProgressByRunId } from "../tasks/task-executor.js";
const DEFAULT_STREAM_FLUSH_MS = 2_500;
@@ -30,11 +31,7 @@ function truncate(value: string, maxChars: number): string {
}
function toTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
function toFiniteNumber(value: unknown): number | undefined {
@@ -49,14 +46,14 @@ function resolveAcpStreamLogPathFromSessionFile(sessionFile: string, sessionId:
export function resolveAcpSpawnStreamLogPath(params: {
childSessionKey: string;
}): string | undefined {
const childSessionKey = params.childSessionKey.trim();
const childSessionKey = normalizeOptionalString(params.childSessionKey);
if (!childSessionKey) {
return undefined;
}
const storeEntry = readAcpSessionEntry({
sessionKey: childSessionKey,
});
const sessionId = storeEntry?.entry?.sessionId?.trim();
const sessionId = normalizeOptionalString(storeEntry?.entry?.sessionId);
if (!storeEntry || !sessionId) {
return undefined;
}
@@ -87,8 +84,8 @@ export function startAcpSpawnParentStreamRelay(params: {
maxRelayLifetimeMs?: number;
emitStartNotice?: boolean;
}): AcpSpawnParentRelayHandle {
const runId = params.runId.trim();
const parentSessionKey = params.parentSessionKey.trim();
const runId = normalizeOptionalString(params.runId) ?? "";
const parentSessionKey = normalizeOptionalString(params.parentSessionKey) ?? "";
if (!runId || !parentSessionKey) {
return {
dispose: () => {},

View File

@@ -1,7 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function normalizeSubagentSessionKey(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}

View File

@@ -1,4 +1,5 @@
import type { SessionEntry } from "../config/sessions.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { formatProviderModelRef } from "./model-runtime.js";
import type { RuntimeFallbackAttempt } from "./reply/agent-runner-execution.js";
@@ -10,8 +11,7 @@ export type FallbackNoticeState = Pick<
>;
export function normalizeFallbackModelRef(value?: string): string | undefined {
const trimmed = String(value ?? "").trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MAX): string {

View File

@@ -1,5 +1,6 @@
import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js";
import type { PluginPackageChannel } from "../plugins/manifest.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "./ids.js";
import { resolveChannelExposure } from "./plugins/exposure.js";
import type { ChannelMeta } from "./plugins/types.js";
@@ -12,7 +13,7 @@ function toChatChannelMeta(params: {
id: ChatChannelId;
channel: PluginPackageChannel;
}): ChatChannelMeta {
const label = params.channel.label?.trim();
const label = normalizeOptionalString(params.channel.label);
if (!label) {
throw new Error(`Missing label for bundled chat channel "${params.id}"`);
}
@@ -21,10 +22,10 @@ function toChatChannelMeta(params: {
return {
id: params.id,
label,
selectionLabel: params.channel.selectionLabel?.trim() || label,
docsPath: params.channel.docsPath?.trim() || `/channels/${params.id}`,
docsLabel: params.channel.docsLabel?.trim() || undefined,
blurb: params.channel.blurb?.trim() || "",
selectionLabel: normalizeOptionalString(params.channel.selectionLabel) || label,
docsPath: normalizeOptionalString(params.channel.docsPath) || `/channels/${params.id}`,
docsLabel: normalizeOptionalString(params.channel.docsLabel),
blurb: normalizeOptionalString(params.channel.blurb) || "",
...(params.channel.aliases?.length ? { aliases: params.channel.aliases } : {}),
...(params.channel.order !== undefined ? { order: params.channel.order } : {}),
...(params.channel.selectionDocsPrefix !== undefined
@@ -36,11 +37,11 @@ function toChatChannelMeta(params: {
...(params.channel.selectionExtras?.length
? { selectionExtras: params.channel.selectionExtras }
: {}),
...(params.channel.detailLabel?.trim()
? { detailLabel: params.channel.detailLabel.trim() }
...(normalizeOptionalString(params.channel.detailLabel)
? { detailLabel: normalizeOptionalString(params.channel.detailLabel)! }
: {}),
...(params.channel.systemImage?.trim()
? { systemImage: params.channel.systemImage.trim() }
...(normalizeOptionalString(params.channel.systemImage)
? { systemImage: normalizeOptionalString(params.channel.systemImage)! }
: {}),
...(params.channel.markdownCapable !== undefined
? { markdownCapable: params.channel.markdownCapable }
@@ -69,7 +70,7 @@ export function buildChatChannelMetaById(): Record<ChatChannelId, ChatChannelMet
if (!channel) {
continue;
}
const rawId = channel?.id?.trim();
const rawId = normalizeOptionalString(channel.id);
if (!rawId || !CHAT_CHANNEL_ID_SET.has(rawId)) {
continue;
}

View File

@@ -1,4 +1,5 @@
import type { CronSchedule } from "../../cron/types.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { parseAt, parseCronStaggerMs, parseDurationMs } from "./shared.js";
type ScheduleOptionInput = {
@@ -126,8 +127,7 @@ function resolveDirectSchedule(options: NormalizedScheduleOptions): CronSchedule
}
function readOptionalString(value: unknown): string | undefined {
const trimmed = readTrimmedString(value);
return trimmed || undefined;
return normalizeOptionalString(value);
}
function readTrimmedString(value: unknown): string {

View File

@@ -8,6 +8,7 @@ import {
} from "../agents/skills-clawhub.js";
import { loadConfig } from "../config/config.js";
import { defaultRuntime } from "../runtime.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js";
@@ -67,7 +68,7 @@ export function registerSkillsCli(program: Command) {
.action(async (queryParts: string[], opts: { limit?: number; json?: boolean }) => {
try {
const results = await searchSkillsFromClawHub({
query: queryParts.join(" ").trim() || undefined,
query: normalizeOptionalString(queryParts.join(" ")),
limit: opts.limit,
});
if (opts.json) {

View File

@@ -14,6 +14,7 @@ import { readClaudeCliCredentialsCached } from "../agents/cli-credentials.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveExecutablePath } from "../infra/executable-path.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { note } from "../terminal/note.js";
import { shortenHomePath } from "../utils.js";
@@ -31,18 +32,12 @@ function resolveConfiguredPrimaryModelRef(
value: string | { primary?: string; fallbacks?: string[] } | undefined,
): string | undefined {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const primary = value.primary;
if (typeof primary !== "string") {
return undefined;
}
const trimmed = primary.trim();
return trimmed || undefined;
return normalizeOptionalString(value.primary);
}
function usesClaudeCliModelSelection(cfg: OpenClawConfig): boolean {
@@ -83,7 +78,7 @@ function resolveClaudeCliCommand(cfg: OpenClawConfig): string {
if (key.trim().toLowerCase() !== CLAUDE_CLI_PROVIDER) {
continue;
}
const command = entry?.command?.trim();
const command = normalizeOptionalString(entry?.command);
if (command) {
return command;
}
@@ -120,7 +115,7 @@ export function resolveClaudeCliProjectDirForWorkspace(params: {
workspaceDir: string;
homeDir?: string;
}): string {
const homeDir = params.homeDir?.trim() || process.env.HOME || os.homedir();
const homeDir = normalizeOptionalString(params.homeDir) || process.env.HOME || os.homedir();
const canonicalWorkspaceDir = canonicalizeWorkspaceDir(params.workspaceDir);
return path.join(
homeDir,

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { AgentModelConfig } from "./types.agents-shared.js";
type AgentModelListLike = {
@@ -7,14 +8,12 @@ type AgentModelListLike = {
export function resolveAgentModelPrimaryValue(model?: AgentModelConfig): string | undefined {
if (typeof model === "string") {
const trimmed = model.trim();
return trimmed || undefined;
return normalizeOptionalString(model);
}
if (!model || typeof model !== "object") {
return undefined;
}
const primary = model.primary?.trim();
return primary || undefined;
return normalizeOptionalString(model.primary);
}
export function resolveAgentModelFallbackValues(model?: AgentModelConfig): string[] {
@@ -26,7 +25,7 @@ export function resolveAgentModelFallbackValues(model?: AgentModelConfig): strin
export function toAgentModelListLike(model?: AgentModelConfig): AgentModelListLike | undefined {
if (typeof model === "string") {
const primary = model.trim();
const primary = normalizeOptionalString(model);
return primary ? { primary } : undefined;
}
if (!model || typeof model !== "object") {

View File

@@ -1,6 +1,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { loadConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { getHeader } from "./http-utils.js";
@@ -16,7 +17,7 @@ function resolveScopedSessionKey(
cfg: ReturnType<typeof loadConfig>,
rawSessionKey: string | undefined,
): string {
const trimmed = rawSessionKey?.trim();
const trimmed = normalizeOptionalString(rawSessionKey);
return !trimmed || trimmed === "main" ? resolveMainSessionKey(cfg) : trimmed;
}
@@ -95,6 +96,6 @@ export function resolveMcpRequestContext(
sessionKey: resolveScopedSessionKey(cfg, getHeader(req, "x-session-key")),
messageProvider:
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined,
accountId: getHeader(req, "x-openclaw-account-id")?.trim() || undefined,
accountId: normalizeOptionalString(getHeader(req, "x-openclaw-account-id")),
};
}

View File

@@ -1,16 +1,12 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
export function parseRestartRequestParams(params: unknown): {
sessionKey: string | undefined;
note: string | undefined;
restartDelayMs: number | undefined;
} {
const sessionKey =
typeof (params as { sessionKey?: unknown }).sessionKey === "string"
? (params as { sessionKey?: string }).sessionKey?.trim() || undefined
: undefined;
const note =
typeof (params as { note?: unknown }).note === "string"
? (params as { note?: string }).note?.trim() || undefined
: undefined;
const sessionKey = normalizeOptionalString((params as { sessionKey?: unknown }).sessionKey);
const note = normalizeOptionalString((params as { note?: unknown }).note);
const restartDelayMsRaw = (params as { restartDelayMs?: unknown }).restartDelayMs;
const restartDelayMs =
typeof restartDelayMsRaw === "number" && Number.isFinite(restartDelayMsRaw)

View File

@@ -8,6 +8,7 @@ import { loadConfig } from "../config/config.js";
import { resolveMainSessionKey } from "../config/sessions.js";
import { logWarn } from "../logger.js";
import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
@@ -225,9 +226,9 @@ export async function handleToolsInvokeHttpRequest(
const messageChannel = normalizeMessageChannel(
getHeader(req, "x-openclaw-message-channel") ?? "",
);
const accountId = getHeader(req, "x-openclaw-account-id")?.trim() || undefined;
const agentTo = getHeader(req, "x-openclaw-message-to")?.trim() || undefined;
const agentThreadId = getHeader(req, "x-openclaw-thread-id")?.trim() || undefined;
const accountId = normalizeOptionalString(getHeader(req, "x-openclaw-account-id"));
const agentTo = normalizeOptionalString(getHeader(req, "x-openclaw-message-to"));
const agentThreadId = normalizeOptionalString(getHeader(req, "x-openclaw-thread-id"));
const { agentId, tools } = resolveGatewayScopedTools({
cfg,
sessionKey,

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type InteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
export type InteractiveReplyButton = {
@@ -37,11 +39,7 @@ export type InteractiveReply = {
};
function readTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
function normalizeButtonStyle(value: unknown): InteractiveButtonStyle | undefined {

View File

@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { homedir, platform } from "node:os";
import { join } from "node:path";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
const GCLOUD_DEFAULT_ADC_PATH = join(
@@ -16,11 +17,7 @@ function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.en
}
function normalizeOptionalPathInput(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {

View File

@@ -1,10 +1,10 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { OpenClawConfig } from "./config-runtime.js";
type ApprovalKind = "exec" | "plugin";
function defaultNormalizeSenderId(value: string): string | undefined {
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
export function createResolvedApproverActionAuthAdapter(params: {

View File

@@ -4,6 +4,7 @@ import { matchesApprovalRequestFilters } from "../infra/approval-request-filters
import { getExecApprovalReplyMetadata } from "../infra/exec-approval-reply.js";
import type { ExecApprovalRequest } from "../infra/exec-approvals.js";
import type { PluginApprovalRequest } from "../infra/plugin-approvals.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { OpenClawConfig } from "./config-runtime.js";
import { normalizeAccountId } from "./routing.js";
@@ -24,8 +25,7 @@ type ApprovalProfileParams = {
};
function defaultNormalizeSenderId(value: string): string | undefined {
const trimmed = value.trim();
return trimmed || undefined;
return normalizeOptionalString(value);
}
function isApprovalTargetsMode(cfg: OpenClawConfig): boolean {