refactor: dedupe infra trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-07 23:46:22 +01:00
parent 134588fc17
commit c7ecc1ebf4
17 changed files with 66 additions and 50 deletions

View File

@@ -1,7 +1,8 @@
import type { ChannelApprovalNativeTarget } from "../channels/plugins/types.adapters.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function buildChannelApprovalNativeTargetKey(target: ChannelApprovalNativeTarget): string {
return `${target.to.trim()}\u0000${
target.threadId == null ? "" : String(target.threadId).trim()
return `${normalizeOptionalString(target.to) ?? ""}\u0000${
target.threadId == null ? "" : (normalizeOptionalString(String(target.threadId)) ?? "")
}`;
}

View File

@@ -1,7 +1,9 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export function formatBonjourError(err: unknown): string {
if (err instanceof Error) {
const trimmedMessage = err.message.trim();
const msg = trimmedMessage || err.name || String(err).trim();
const msg = trimmedMessage || err.name || (normalizeOptionalString(String(err)) ?? "");
if (err.name && err.name !== "Error") {
return msg === err.name ? err.name : `${err.name}: ${msg}`;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
resolveGatewayDiscoveryEndpoint,
type GatewayBonjourBeacon,
@@ -25,13 +26,14 @@ export function buildGatewayDiscoveryTarget(
): GatewayDiscoveryTarget {
const endpoint = resolveGatewayDiscoveryEndpoint(beacon);
const sshPort = pickSshPort(beacon);
const sshUser = opts?.sshUser?.trim() ?? "";
const sshUser = normalizeOptionalString(opts?.sshUser) ?? "";
const baseSshTarget = endpoint ? (sshUser ? `${sshUser}@${endpoint.host}` : endpoint.host) : null;
const sshTarget =
baseSshTarget && sshPort && sshPort !== 22 ? `${baseSshTarget}:${sshPort}` : baseSshTarget;
return {
title: (beacon.displayName || beacon.instanceName || "Gateway").trim(),
domain: (beacon.domain || "local.").trim(),
title:
normalizeOptionalString(beacon.displayName || beacon.instanceName || "Gateway") ?? "Gateway",
domain: normalizeOptionalString(beacon.domain || "local.") ?? "local.",
endpoint,
wsUrl: endpoint?.wsUrl ?? null,
sshPort,

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type HeartbeatReasonKind =
| "retry"
| "interval"
@@ -9,7 +11,7 @@ export type HeartbeatReasonKind =
| "other";
function trimReason(reason?: string): string {
return typeof reason === "string" ? reason.trim() : "";
return normalizeOptionalString(reason) ?? "";
}
export function normalizeHeartbeatWakeReason(reason?: string): string {

View File

@@ -8,6 +8,7 @@ import { parseDurationMs } from "../cli/parse-duration.js";
import type { OpenClawConfig } from "../config/config.js";
import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
type HeartbeatConfig = AgentDefaultsConfig["heartbeat"];
@@ -53,7 +54,7 @@ export function resolveHeartbeatIntervalMs(
if (!raw) {
return null;
}
const trimmed = String(raw).trim();
const trimmed = normalizeOptionalString(String(raw)) ?? "";
if (!trimmed) {
return null;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
isHeartbeatActionWakeReason,
normalizeHeartbeatWakeReason,
@@ -71,7 +72,7 @@ function normalizeWakeReason(reason?: string): string {
}
function normalizeWakeTarget(value?: string): string | undefined {
const trimmed = typeof value === "string" ? value.trim() : "";
const trimmed = normalizeOptionalString(value) ?? "";
return trimmed || undefined;
}

View File

@@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import os from "node:os";
import { promisify } from "node:util";
import { normalizeOptionalString } from "../shared/string-coerce.js";
const execFileAsync = promisify(execFile);
@@ -12,7 +13,7 @@ async function tryScutil(key: "ComputerName" | "LocalHostName") {
timeout: 1000,
windowsHide: true,
});
const value = String(stdout ?? "").trim();
const value = normalizeOptionalString(String(stdout ?? "")) ?? "";
return value.length > 0 ? value : null;
} catch {
return null;
@@ -20,7 +21,7 @@ async function tryScutil(key: "ComputerName" | "LocalHostName") {
}
function fallbackHostName() {
const trimmed = os.hostname().trim();
const trimmed = normalizeOptionalString(os.hostname()) ?? "";
return trimmed.replace(/\.local$/i, "") || "openclaw";
}

View File

@@ -1,6 +1,7 @@
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { normalizeAccountId } from "../../utils/account-id.js";
import {
INTERNAL_MESSAGE_CHANNEL,
@@ -47,15 +48,11 @@ export function resolveAgentDeliveryPlan(params: {
/** Turn-source `threadId` — paired with `turnSourceChannel`. */
turnSourceThreadId?: string | number;
}): AgentDeliveryPlan {
const requestedRaw =
typeof params.requestedChannel === "string" ? params.requestedChannel.trim() : "";
const requestedRaw = normalizeOptionalString(params.requestedChannel) ?? "";
const normalizedRequested = requestedRaw ? normalizeMessageChannel(requestedRaw) : undefined;
const requestedChannel = normalizedRequested || "last";
const explicitTo =
typeof params.explicitTo === "string" && params.explicitTo.trim()
? params.explicitTo.trim()
: undefined;
const explicitTo = normalizeOptionalString(params.explicitTo) ?? undefined;
// Resolve turn-source channel for cross-channel safety.
const normalizedTurnSource = params.turnSourceChannel
@@ -65,10 +62,7 @@ export function resolveAgentDeliveryPlan(params: {
normalizedTurnSource && isDeliverableMessageChannel(normalizedTurnSource)
? normalizedTurnSource
: undefined;
const turnSourceTo =
typeof params.turnSourceTo === "string" && params.turnSourceTo.trim()
? params.turnSourceTo.trim()
: undefined;
const turnSourceTo = normalizeOptionalString(params.turnSourceTo) ?? undefined;
const turnSourceAccountId = normalizeAccountId(params.turnSourceAccountId);
const turnSourceThreadId =
params.turnSourceThreadId != null && params.turnSourceThreadId !== ""

View File

@@ -1,4 +1,7 @@
import { hasNonEmptyString as sharedHasNonEmptyString } from "../../shared/string-coerce.js";
import {
hasNonEmptyString as sharedHasNonEmptyString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { MESSAGE_ACTION_TARGET_MODE } from "./message-action-spec.js";
export const hasNonEmptyString = sharedHasNonEmptyString;
@@ -13,7 +16,7 @@ export function applyTargetToParams(params: {
action: string;
args: Record<string, unknown>;
}): void {
const target = typeof params.args.target === "string" ? params.args.target.trim() : "";
const target = normalizeOptionalString(params.args.target) ?? "";
const hasLegacyTo = hasNonEmptyString(params.args.to);
const hasLegacyChannelId = hasNonEmptyString(params.args.channelId);
const mode =

View File

@@ -2,6 +2,7 @@ import type {
ChannelMessageActionName,
ChannelThreadingToolContext,
} from "../../channels/plugins/types.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import {
isDeliverableMessageChannel,
normalizeMessageChannel,
@@ -16,18 +17,16 @@ export function normalizeMessageActionInput(params: {
}): Record<string, unknown> {
const normalizedArgs = { ...params.args };
const { action, toolContext } = params;
const explicitChannel =
typeof normalizedArgs.channel === "string" ? normalizedArgs.channel.trim() : "";
const explicitChannel = normalizeOptionalString(normalizedArgs.channel) ?? "";
const inferredChannel =
explicitChannel || normalizeMessageChannel(toolContext?.currentChannelProvider) || "";
const explicitTarget =
typeof normalizedArgs.target === "string" ? normalizedArgs.target.trim() : "";
const explicitTarget = normalizeOptionalString(normalizedArgs.target) ?? "";
const hasLegacyTargetFields =
typeof normalizedArgs.to === "string" || typeof normalizedArgs.channelId === "string";
const hasLegacyTarget =
(typeof normalizedArgs.to === "string" && normalizedArgs.to.trim().length > 0) ||
(typeof normalizedArgs.channelId === "string" && normalizedArgs.channelId.trim().length > 0);
(normalizeOptionalString(normalizedArgs.to) ?? "").length > 0 ||
(normalizeOptionalString(normalizedArgs.channelId) ?? "").length > 0;
if (explicitTarget && hasLegacyTargetFields) {
delete normalizedArgs.to;
@@ -40,16 +39,15 @@ export function normalizeMessageActionInput(params: {
actionRequiresTarget(action) &&
!actionHasTarget(action, normalizedArgs, { channel: inferredChannel })
) {
const inferredTarget = toolContext?.currentChannelId?.trim();
const inferredTarget = normalizeOptionalString(toolContext?.currentChannelId);
if (inferredTarget) {
normalizedArgs.target = inferredTarget;
}
}
if (!explicitTarget && actionRequiresTarget(action) && hasLegacyTarget) {
const legacyTo = typeof normalizedArgs.to === "string" ? normalizedArgs.to.trim() : "";
const legacyChannelId =
typeof normalizedArgs.channelId === "string" ? normalizedArgs.channelId.trim() : "";
const legacyTo = normalizeOptionalString(normalizedArgs.to) ?? "";
const legacyChannelId = normalizeOptionalString(normalizedArgs.channelId) ?? "";
const legacyTarget = legacyTo || legacyChannelId;
if (legacyTarget) {
normalizedArgs.target = legacyTarget;

View File

@@ -236,7 +236,7 @@ async function resolveActionTarget(params: {
accountId?: string | null;
}): Promise<ResolvedMessagingTarget | undefined> {
let resolvedTarget: ResolvedMessagingTarget | undefined;
const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
const toRaw = normalizeOptionalString(params.args.to) ?? "";
if (toRaw) {
const resolved = await resolveResolvedTargetOrThrow({
cfg: params.cfg,
@@ -247,8 +247,7 @@ async function resolveActionTarget(params: {
params.args.to = resolved.to;
resolvedTarget = resolved;
}
const channelIdRaw =
typeof params.args.channelId === "string" ? params.args.channelId.trim() : "";
const channelIdRaw = normalizeOptionalString(params.args.channelId) ?? "";
if (channelIdRaw) {
const resolved = await resolveResolvedTargetOrThrow({
cfg: params.cfg,

View File

@@ -1,6 +1,9 @@
import { getBootstrapChannelPlugin } from "../../channels/plugins/bootstrap-registry.js";
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
export type MessageActionTargetMode = "to" | "channelId" | "none";
@@ -109,11 +112,11 @@ export function actionHasTarget(
params: Record<string, unknown>,
options?: { channel?: string },
): boolean {
const to = typeof params.to === "string" ? params.to.trim() : "";
const to = normalizeOptionalString(params.to) ?? "";
if (to) {
return true;
}
const channelId = typeof params.channelId === "string" ? params.channelId.trim() : "";
const channelId = normalizeOptionalString(params.channelId) ?? "";
if (channelId) {
return true;
}
@@ -125,7 +128,7 @@ export function actionHasTarget(
spec.aliases.some((alias) => {
const value = params[alias];
if (typeof value === "string") {
return value.trim().length > 0;
return Boolean(normalizeOptionalString(value));
}
if (typeof value === "number") {
return Number.isFinite(value);

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type PluginInstallPathIssue = {
kind: "custom-path" | "missing-path";
@@ -16,7 +17,7 @@ function resolvePluginInstallCandidatePaths(
}
return [install.sourcePath, install.installPath]
.map((value) => (typeof value === "string" ? value.trim() : ""))
.map((value) => normalizeOptionalString(value) ?? "")
.filter(Boolean);
}

View File

@@ -5,7 +5,10 @@ import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js";
import type { OpenClawConfig } from "../config/config.js";
import type { NodeRegistry } from "../gateway/node-registry.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { listNodePairing, updatePairedNodeMetadata } from "./node-pairing.js";
type RemoteNodeRecord = {
@@ -214,12 +217,12 @@ function parseBinProbePayload(payloadJSON: string | null | undefined, payload?:
? (JSON.parse(payloadJSON) as { stdout?: unknown; bins?: unknown })
: (payload as { stdout?: unknown; bins?: unknown });
if (Array.isArray(parsed.bins)) {
return parsed.bins.map((bin) => String(bin).trim()).filter(Boolean);
return parsed.bins.map((bin) => normalizeOptionalString(String(bin)) ?? "").filter(Boolean);
}
if (typeof parsed.stdout === "string") {
return parsed.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.map((line) => normalizeOptionalString(line) ?? "")
.filter(Boolean);
}
} catch {

View File

@@ -3,7 +3,10 @@
// events ephemeral. Events are session-scoped and require an explicit key.
import { resolveGlobalMap } from "../shared/global-singleton.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import {
mergeDeliveryContext,
normalizeDeliveryContext,
@@ -38,7 +41,7 @@ type SystemEventOptions = {
};
function requireSessionKey(key?: string | null): string {
const trimmed = typeof key === "string" ? key.trim() : "";
const trimmed = normalizeOptionalString(key) ?? "";
if (!trimmed) {
throw new Error("system events require a sessionKey");
}

View File

@@ -3,6 +3,7 @@ import os from "node:os";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { resolveRuntimeServiceVersion } from "../version.js";
import { pickBestEffortPrimaryLanIPv4 } from "./network-discovery-display.js";
@@ -55,7 +56,7 @@ function initSelfPresence() {
const res = spawnSync("sysctl", ["-n", "hw.model"], {
encoding: "utf-8",
});
const out = typeof res.stdout === "string" ? res.stdout.trim() : "";
const out = normalizeOptionalString(res.stdout) ?? "";
return out.length > 0 ? out : undefined;
}
return os.arch();
@@ -64,7 +65,7 @@ function initSelfPresence() {
const res = spawnSync("sw_vers", ["-productVersion"], {
encoding: "utf-8",
});
const out = typeof res.stdout === "string" ? res.stdout.trim() : "";
const out = normalizeOptionalString(res.stdout) ?? "";
return out.length > 0 ? out : os.release();
};
const platform = (() => {
@@ -178,7 +179,7 @@ function mergeStringList(...values: Array<string[] | undefined>): string[] | und
continue;
}
for (const item of list) {
const trimmed = String(item).trim();
const trimmed = normalizeOptionalString(String(item)) ?? "";
if (trimmed) {
out.add(trimmed);
}

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js";
export type VoiceWakeConfig = {
@@ -16,7 +17,7 @@ function resolvePath(baseDir?: string) {
function sanitizeTriggers(triggers: string[] | undefined | null): string[] {
const cleaned = (triggers ?? [])
.map((w) => (typeof w === "string" ? w.trim() : ""))
.map((w) => normalizeOptionalString(w) ?? "")
.filter((w) => w.length > 0);
return cleaned.length > 0 ? cleaned : DEFAULT_TRIGGERS;
}