refactor: dedupe trimmed string readers

This commit is contained in:
Peter Steinberger
2026-04-07 04:26:13 +01:00
parent b7be963501
commit d03985415d
13 changed files with 46 additions and 67 deletions

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../../../src/shared/string-coerce.js";
import { isRecord } from "../../../src/utils.js";
export type JsonObject = Record<string, unknown>;
@@ -25,7 +26,7 @@ export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
] as const;
function getTrimmedString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value);
}
function readOpenClawBlock(packageJson: unknown) {

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function readString(
meta: Record<string, unknown> | null | undefined,
keys: string[],
@@ -6,9 +8,9 @@ export function readString(
return undefined;
}
for (const key of keys) {
const value = meta[key];
if (typeof value === "string" && value.trim()) {
return value.trim();
const value = normalizeOptionalString(meta[key]);
if (value) {
return value;
}
}
return undefined;

View File

@@ -10,6 +10,7 @@ import {
loadEnabledBundleMcpConfig,
type BundleMcpConfig,
} from "../../plugins/bundle-mcp.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
type PreparedCliBundleMcpConfig = {
backend: CliBackendConfig;
@@ -34,12 +35,10 @@ function findMcpConfigPath(args?: string[]): string | undefined {
for (let i = 0; i < args.length; i += 1) {
const arg = args[i] ?? "";
if (arg === "--mcp-config") {
const next = args[i + 1];
return typeof next === "string" && next.trim() ? next.trim() : undefined;
return normalizeOptionalString(args[i + 1]);
}
if (arg.startsWith("--mcp-config=")) {
const inline = arg.slice("--mcp-config=".length).trim();
return inline || undefined;
return normalizeOptionalString(arg.slice("--mcp-config=".length));
}
}
return undefined;

View File

@@ -2,6 +2,7 @@ import type { Command } from "commander";
import type { CronJob } from "../../cron/types.js";
import { sanitizeAgentId } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
@@ -144,12 +145,8 @@ export function registerCronAddCommand(cron: Command) {
return {
kind: "agentTurn" as const,
message,
model:
typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined,
thinking:
typeof opts.thinking === "string" && opts.thinking.trim()
? opts.thinking.trim()
: undefined,
model: normalizeOptionalString(opts.model),
thinking: normalizeOptionalString(opts.thinking),
timeoutSeconds:
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
lightContext: opts.lightContext === true ? true : undefined,
@@ -250,7 +247,7 @@ export function registerCronAddCommand(cron: Command) {
typeof opts.channel === "string" && opts.channel.trim()
? opts.channel.trim()
: undefined,
to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined,
to: normalizeOptionalString(opts.to),
accountId,
bestEffort: opts.bestEffortDeliver ? true : undefined,
}

View File

@@ -3,6 +3,7 @@ import type { CronJob } from "../../cron/types.js";
import { danger } from "../../globals.js";
import { sanitizeAgentId } from "../../routing/session-key.js";
import { defaultRuntime } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import {
applyExistingCronSchedulePatch,
@@ -169,12 +170,8 @@ export function registerCronEditCommand(cron: Command) {
}
const hasSystemEventPatch = typeof opts.systemEvent === "string";
const model =
typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined;
const thinking =
typeof opts.thinking === "string" && opts.thinking.trim()
? opts.thinking.trim()
: undefined;
const model = normalizeOptionalString(opts.model);
const thinking = normalizeOptionalString(opts.thinking);
const timeoutSeconds = opts.timeoutSeconds
? Number.parseInt(String(opts.timeoutSeconds), 10)
: undefined;

View File

@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import type { SessionScope } from "../config/sessions.js";
import { normalizeAgentId, normalizeMainKey } from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type GatewayAgentListRow = {
id: string;
@@ -62,7 +63,7 @@ export function listGatewayAgentsBasic(cfg: OpenClawConfig): {
continue;
}
configuredById.set(normalizeAgentId(entry.id), {
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
name: normalizeOptionalString(entry.name),
});
}
const explicitIds = new Set(

View File

@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js";
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import type { HookExternalContentSource } from "../security/external-content.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { type HookMappingResolved, resolveHookMappings } from "./hooks-mapping.js";
import { resolveAllowedAgentIds } from "./hooks-policy.js";
@@ -363,30 +364,27 @@ export function normalizeAgentPayload(payload: Record<string, unknown>):
return { ok: false, error: "message required" };
}
const nameRaw = payload.name;
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "Hook";
const name = normalizeOptionalString(nameRaw) ?? "Hook";
const agentIdRaw = payload.agentId;
const agentId =
typeof agentIdRaw === "string" && agentIdRaw.trim() ? agentIdRaw.trim() : undefined;
const agentId = normalizeOptionalString(agentIdRaw);
const idempotencyKey = resolveOptionalHookIdempotencyKey(payload.idempotencyKey);
const wakeMode = payload.wakeMode === "next-heartbeat" ? "next-heartbeat" : "now";
const sessionKeyRaw = payload.sessionKey;
const sessionKey =
typeof sessionKeyRaw === "string" && sessionKeyRaw.trim() ? sessionKeyRaw.trim() : undefined;
const sessionKey = normalizeOptionalString(sessionKeyRaw);
const channel = resolveHookChannel(payload.channel);
if (!channel) {
return { ok: false, error: getHookChannelError() };
}
const toRaw = payload.to;
const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
const to = normalizeOptionalString(toRaw);
const modelRaw = payload.model;
const model = typeof modelRaw === "string" && modelRaw.trim() ? modelRaw.trim() : undefined;
const model = normalizeOptionalString(modelRaw);
if (modelRaw !== undefined && !model) {
return { ok: false, error: "model required" };
}
const deliver = resolveHookDeliver(payload.deliver);
const thinkingRaw = payload.thinking;
const thinking =
typeof thinkingRaw === "string" && thinkingRaw.trim() ? thinkingRaw.trim() : undefined;
const thinking = normalizeOptionalString(thinkingRaw);
const timeoutRaw = payload.timeoutSeconds;
const timeoutSeconds =
typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0

View File

@@ -28,6 +28,7 @@ import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session
import { defaultRuntime } from "../../runtime.js";
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { createRunningTaskRun } from "../../tasks/task-executor.js";
import {
normalizeDeliveryContext,
@@ -653,25 +654,11 @@ export const agentHandlers: GatewayRequestHandlers = {
const wantsDelivery = request.deliver === true;
const explicitTo =
typeof request.replyTo === "string" && request.replyTo.trim()
? request.replyTo.trim()
: typeof request.to === "string" && request.to.trim()
? request.to.trim()
: undefined;
const explicitThreadId =
typeof request.threadId === "string" && request.threadId.trim()
? request.threadId.trim()
: undefined;
const turnSourceChannel =
typeof request.channel === "string" && request.channel.trim()
? request.channel.trim()
: undefined;
const turnSourceTo =
typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined;
const turnSourceAccountId =
typeof request.accountId === "string" && request.accountId.trim()
? request.accountId.trim()
: undefined;
normalizeOptionalString(request.replyTo) ?? normalizeOptionalString(request.to);
const explicitThreadId = normalizeOptionalString(request.threadId);
const turnSourceChannel = normalizeOptionalString(request.channel);
const turnSourceTo = normalizeOptionalString(request.to);
const turnSourceAccountId = normalizeOptionalString(request.accountId);
const deliveryPlan = resolveAgentDeliveryPlan({
sessionEntry,
requestedChannel: request.replyChannel ?? request.channel,

View File

@@ -37,6 +37,7 @@ import { assertNoPathAliasEscape } from "../../infra/path-alias-guards.js";
import { isNotFoundPathError } from "../../infra/path-guards.js";
import { movePathToTrash } from "../../plugin-sdk/browser-maintenance.js";
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { resolveUserPath } from "../../utils.js";
import {
ErrorCodes,
@@ -396,7 +397,7 @@ function sanitizeIdentityLine(value: string): string {
}
function resolveOptionalStringParam(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value);
}
function respondInvalidMethodParams(

View File

@@ -9,8 +9,8 @@ import {
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded-runner/runs.js";
import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js";
import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js";
import { clearSessionQueues } from "../../auto-reply/reply/queue/cleanup.js";
import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js";
import { loadConfig } from "../../config/config.js";
import {
loadSessionStore,
@@ -33,6 +33,7 @@ import {
resolveAgentIdFromSessionKey,
toAgentStoreSessionKey,
} from "../../routing/session-key.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js";
import {
ErrorCodes,
@@ -772,18 +773,13 @@ export const sessionsHandlers: GatewayRequestHandlers = {
}
const p = params;
const cfg = loadConfig();
const requestedKey = typeof p.key === "string" && p.key.trim() ? p.key.trim() : undefined;
const requestedKey = normalizeOptionalString(p.key);
const agentId = normalizeAgentId(
typeof p.agentId === "string" && p.agentId.trim() ? p.agentId : resolveDefaultAgentId(cfg),
normalizeOptionalString(p.agentId) ?? resolveDefaultAgentId(cfg),
);
if (requestedKey) {
const requestedAgentId = parseAgentSessionKey(requestedKey)?.agentId;
if (
requestedAgentId &&
requestedAgentId !== agentId &&
typeof p.agentId === "string" &&
p.agentId.trim()
) {
if (requestedAgentId && requestedAgentId !== agentId && normalizeOptionalString(p.agentId)) {
respond(
false,
undefined,
@@ -795,10 +791,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
return;
}
}
const parentSessionKey =
typeof p.parentSessionKey === "string" && p.parentSessionKey.trim()
? p.parentSessionKey.trim()
: undefined;
const parentSessionKey = normalizeOptionalString(p.parentSessionKey);
let canonicalParentSessionKey: string | undefined;
if (parentSessionKey) {
const parent = loadSessionEntry(parentSessionKey);

View File

@@ -54,6 +54,7 @@ import {
isWorkspaceRelativeAvatarPath,
resolveAvatarMime,
} from "../shared/avatar-policy.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
import {
@@ -666,7 +667,7 @@ export function listAgentsForGateway(cfg: OpenClawConfig): {
}
: undefined;
configuredById.set(normalizeAgentId(entry.id), {
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
name: normalizeOptionalString(entry.name),
identity,
});
}

View File

@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { isAtLeast, parseSemver } from "./runtime-guard.js";
import { compareComparableSemver, parseComparableSemver } from "./semver-compare.js";
import { createTempDownloadTarget } from "./temp-download.js";
@@ -207,7 +208,7 @@ function normalizeBaseUrl(baseUrl?: string): string {
}
function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value);
}
function extractTokenFromClawHubConfig(value: unknown): string | undefined {

View File

@@ -4,6 +4,7 @@ import { promptYesNo } from "../cli/prompt.js";
import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js";
import { runExec } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import { ensureBinary } from "./binaries.js";
@@ -435,7 +436,7 @@ export async function disableTailscaleFunnel(exec: typeof runExec = runExec) {
}
function getString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
return normalizeOptionalString(value);
}
function readRecord(value: unknown): Record<string, unknown> | null {