diff --git a/src/channels/chat-type.ts b/src/channels/chat-type.ts index c9be141adf3..fced60739fb 100644 --- a/src/channels/chat-type.ts +++ b/src/channels/chat-type.ts @@ -1,7 +1,9 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export type ChatType = "direct" | "group" | "channel"; export function normalizeChatType(raw?: string): ChatType | undefined { - const value = raw?.trim().toLowerCase(); + const value = normalizeOptionalString(raw)?.toLowerCase(); if (!value) { return undefined; } diff --git a/src/channels/ids.ts b/src/channels/ids.ts index d45d3100e51..a43472b404c 100644 --- a/src/channels/ids.ts +++ b/src/channels/ids.ts @@ -1,4 +1,5 @@ import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type ChatChannelId = string; @@ -9,8 +10,7 @@ type BundledChatChannelEntry = { }; function normalizeChannelKey(raw?: string | null): string | undefined { - const normalized = raw?.trim().toLowerCase(); - return normalized || undefined; + return normalizeOptionalString(raw)?.toLowerCase(); } function listBundledChatChannelEntries(): BundledChatChannelEntry[] { diff --git a/src/channels/inbound-debounce-policy.ts b/src/channels/inbound-debounce-policy.ts index 7101ba6f131..fa0326baa7e 100644 --- a/src/channels/inbound-debounce-policy.ts +++ b/src/channels/inbound-debounce-policy.ts @@ -6,6 +6,7 @@ import { type InboundDebounceCreateParams, } from "../auto-reply/inbound-debounce.js"; import type { OpenClawConfig } from "../config/types.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export function shouldDebounceTextInbound(params: { text: string | null | undefined; @@ -20,7 +21,7 @@ export function shouldDebounceTextInbound(params: { if (params.hasMedia) { return false; } - const text = params.text?.trim() ?? ""; + const text = normalizeOptionalString(params.text) ?? ""; if (!text) { return false; } diff --git a/src/channels/registry.ts b/src/channels/registry.ts index afbb6f3f780..c592ead089a 100644 --- a/src/channels/registry.ts +++ b/src/channels/registry.ts @@ -1,4 +1,5 @@ import { getActivePluginChannelRegistry, getActivePluginRegistry } from "../plugins/runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { getChatChannelMeta, listChatChannels, type ChatChannelMeta } from "./chat-meta.js"; import { CHANNEL_IDS, @@ -31,14 +32,12 @@ function findRegisteredChannelPluginEntry( normalizedKey: string, ): RegisteredChannelPluginEntry | undefined { return listRegisteredChannelPluginEntries().find((entry) => { - const id = String(entry.plugin.id ?? "") - .trim() - .toLowerCase(); + const id = normalizeOptionalString(String(entry.plugin.id ?? ""))?.toLowerCase() ?? ""; if (id && id === normalizedKey) { return true; } return (entry.plugin.meta?.aliases ?? []).some( - (alias) => alias.trim().toLowerCase() === normalizedKey, + (alias) => normalizeOptionalString(alias)?.toLowerCase() === normalizedKey, ); }); } @@ -56,8 +55,7 @@ function findRegisteredChannelPluginEntryById( } const normalizeChannelKey = (raw?: string | null): string | undefined => { - const normalized = raw?.trim().toLowerCase(); - return normalized || undefined; + return normalizeOptionalString(raw)?.toLowerCase(); }; export { CHAT_CHANNEL_ALIASES, @@ -87,7 +85,7 @@ export function normalizeAnyChannelId(raw?: string | null): ChannelId | null { export function listRegisteredChannelPluginIds(): ChannelId[] { return listRegisteredChannelPluginEntries().flatMap((entry) => { - const id = entry.plugin.id?.trim(); + const id = normalizeOptionalString(entry.plugin.id); return id ? [id as ChannelId] : []; }); } diff --git a/src/channels/sender-identity.ts b/src/channels/sender-identity.ts index d3117f0a576..5652d578df1 100644 --- a/src/channels/sender-identity.ts +++ b/src/channels/sender-identity.ts @@ -1,4 +1,5 @@ import type { MsgContext } from "../auto-reply/templating.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeChatType } from "./chat-type.js"; export function validateSenderIdentity(ctx: MsgContext): string[] { @@ -7,10 +8,10 @@ export function validateSenderIdentity(ctx: MsgContext): string[] { const chatType = normalizeChatType(ctx.ChatType); const isDirect = chatType === "direct"; - const senderId = ctx.SenderId?.trim() || ""; - const senderName = ctx.SenderName?.trim() || ""; - const senderUsername = ctx.SenderUsername?.trim() || ""; - const senderE164 = ctx.SenderE164?.trim() || ""; + const senderId = normalizeOptionalString(ctx.SenderId) || ""; + const senderName = normalizeOptionalString(ctx.SenderName) || ""; + const senderUsername = normalizeOptionalString(ctx.SenderUsername) || ""; + const senderE164 = normalizeOptionalString(ctx.SenderE164) || ""; if (!isDirect) { if (!senderId && !senderName && !senderUsername && !senderE164) { diff --git a/src/channels/sender-label.ts b/src/channels/sender-label.ts index e8d4132f0a5..4b15acd5320 100644 --- a/src/channels/sender-label.ts +++ b/src/channels/sender-label.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export type SenderLabelParams = { name?: string; username?: string; @@ -7,8 +9,7 @@ export type SenderLabelParams = { }; function normalize(value?: string): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; + return normalizeOptionalString(value); } function normalizeSenderLabelParams(params: SenderLabelParams) { diff --git a/src/channels/status-reactions.ts b/src/channels/status-reactions.ts index f10f19e1b0a..c3203aac4b5 100644 --- a/src/channels/status-reactions.ts +++ b/src/channels/status-reactions.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + /** * Channel-agnostic status reaction controller. * Provides a unified interface for displaying agent status via message reactions. @@ -102,7 +104,7 @@ export function resolveToolEmoji( toolName: string | undefined, emojis: Required, ): string { - const normalized = toolName?.trim().toLowerCase() ?? ""; + const normalized = normalizeOptionalString(toolName)?.toLowerCase() ?? ""; if (!normalized) { return emojis.tool; } diff --git a/src/channels/thread-binding-id.ts b/src/channels/thread-binding-id.ts index c9db30e3637..071bf67a82b 100644 --- a/src/channels/thread-binding-id.ts +++ b/src/channels/thread-binding-id.ts @@ -1,8 +1,10 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export function resolveThreadBindingConversationIdFromBindingId(params: { accountId: string; bindingId?: string; }): string | undefined { - const bindingId = params.bindingId?.trim(); + const bindingId = normalizeOptionalString(params.bindingId); if (!bindingId) { return undefined; } @@ -10,6 +12,6 @@ export function resolveThreadBindingConversationIdFromBindingId(params: { if (!bindingId.startsWith(prefix)) { return undefined; } - const conversationId = bindingId.slice(prefix.length).trim(); + const conversationId = normalizeOptionalString(bindingId.slice(prefix.length)); return conversationId || undefined; } diff --git a/src/channels/thread-bindings-messages.ts b/src/channels/thread-bindings-messages.ts index fd3d4ccdeaa..a00b5bc5cdc 100644 --- a/src/channels/thread-bindings-messages.ts +++ b/src/channels/thread-bindings-messages.ts @@ -1,4 +1,5 @@ import { prefixSystemMessage } from "../infra/system-message.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; const DEFAULT_THREAD_BINDING_FAREWELL_TEXT = "Session ended. Messages here will no longer be routed."; @@ -32,8 +33,8 @@ export function resolveThreadBindingThreadName(params: { agentId?: string; label?: string; }): string { - const label = params.label?.trim(); - const base = label || params.agentId?.trim() || "agent"; + const label = normalizeOptionalString(params.label); + const base = label || normalizeOptionalString(params.agentId) || "agent"; const raw = `🤖 ${base}`.replace(/\s+/g, " ").trim(); return raw.slice(0, 100); } @@ -46,12 +47,12 @@ export function resolveThreadBindingIntroText(params: { sessionCwd?: string; sessionDetails?: string[]; }): string { - const label = params.label?.trim(); - const base = label || params.agentId?.trim() || "agent"; + const label = normalizeOptionalString(params.label); + const base = label || normalizeOptionalString(params.agentId) || "agent"; const normalized = base.replace(/\s+/g, " ").trim().slice(0, 100) || "agent"; const idleTimeoutMs = normalizeThreadBindingDurationMs(params.idleTimeoutMs); const maxAgeMs = normalizeThreadBindingDurationMs(params.maxAgeMs); - const cwd = params.sessionCwd?.trim(); + const cwd = normalizeOptionalString(params.sessionCwd); const details = (params.sessionDetails ?? []) .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); @@ -86,7 +87,7 @@ export function resolveThreadBindingFarewellText(params: { idleTimeoutMs: number; maxAgeMs: number; }): string { - const custom = params.farewellText?.trim(); + const custom = normalizeOptionalString(params.farewellText); if (custom) { return prefixSystemMessage(custom); } diff --git a/src/sessions/send-policy.ts b/src/sessions/send-policy.ts index 3e36288628c..2c16a3b50c6 100644 --- a/src/sessions/send-policy.ts +++ b/src/sessions/send-policy.ts @@ -1,11 +1,12 @@ import { normalizeChatType } from "../channels/chat-type.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionChatType, SessionEntry } from "../config/sessions.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type SessionSendPolicyDecision = "allow" | "deny"; export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecision | undefined { - const value = raw?.trim().toLowerCase(); + const value = normalizeOptionalString(raw)?.toLowerCase(); if (value === "allow") { return "allow"; } @@ -16,7 +17,7 @@ export function normalizeSendPolicy(raw?: string | null): SessionSendPolicyDecis } function normalizeMatchValue(raw?: string | null) { - const value = raw?.trim().toLowerCase(); + const value = normalizeOptionalString(raw)?.toLowerCase(); return value ? value : undefined; } @@ -45,7 +46,7 @@ function deriveChannelFromKey(key?: string) { } function deriveChatTypeFromKey(key?: string): SessionChatType | undefined { - const normalizedKey = stripAgentSessionKeyPrefix(key)?.trim().toLowerCase(); + const normalizedKey = normalizeOptionalString(stripAgentSessionKeyPrefix(key))?.toLowerCase(); if (!normalizedKey) { return undefined; } diff --git a/src/sessions/session-key-utils.ts b/src/sessions/session-key-utils.ts index 00e4b173179..1486811f6a8 100644 --- a/src/sessions/session-key-utils.ts +++ b/src/sessions/session-key-utils.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export type ParsedAgentSessionKey = { agentId: string; rest: string; @@ -22,7 +24,7 @@ export type RawSessionConversationRef = { export function parseAgentSessionKey( sessionKey: string | undefined | null, ): ParsedAgentSessionKey | null { - const raw = (sessionKey ?? "").trim().toLowerCase(); + const raw = normalizeOptionalString(sessionKey)?.toLowerCase(); if (!raw) { return null; } @@ -33,7 +35,7 @@ export function parseAgentSessionKey( if (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; @@ -58,7 +60,7 @@ export function isCronSessionKey(sessionKey: string | undefined | null): boolean } export function isSubagentSessionKey(sessionKey: string | undefined | null): boolean { - const raw = (sessionKey ?? "").trim(); + const raw = normalizeOptionalString(sessionKey); if (!raw) { return false; } @@ -70,7 +72,7 @@ export function isSubagentSessionKey(sessionKey: string | undefined | null): boo } export function getSubagentDepth(sessionKey: string | undefined | null): number { - const raw = (sessionKey ?? "").trim().toLowerCase(); + const raw = normalizeOptionalString(sessionKey)?.toLowerCase(); if (!raw) { return 0; } @@ -78,7 +80,7 @@ export function getSubagentDepth(sessionKey: string | undefined | null): number } export function isAcpSessionKey(sessionKey: string | undefined | null): boolean { - const raw = (sessionKey ?? "").trim(); + const raw = normalizeOptionalString(sessionKey); if (!raw) { return false; } @@ -91,14 +93,13 @@ export function isAcpSessionKey(sessionKey: string | undefined | null): boolean } function normalizeSessionConversationChannel(value: string | undefined | null): string | undefined { - const trimmed = (value ?? "").trim().toLowerCase(); - return trimmed || undefined; + return normalizeOptionalString(value)?.toLowerCase(); } export function parseThreadSessionSuffix( sessionKey: string | undefined | null, ): ParsedThreadSessionSuffix { - const raw = (sessionKey ?? "").trim(); + const raw = normalizeOptionalString(sessionKey); if (!raw) { return { baseSessionKey: undefined, threadId: undefined }; } @@ -111,7 +112,7 @@ export function parseThreadSessionSuffix( const baseSessionKey = markerIndex === -1 ? raw : raw.slice(0, markerIndex); const threadIdRaw = markerIndex === -1 ? undefined : raw.slice(markerIndex + marker.length); - const threadId = threadIdRaw?.trim() || undefined; + const threadId = normalizeOptionalString(threadIdRaw); return { baseSessionKey, threadId }; } @@ -119,30 +120,27 @@ export function parseThreadSessionSuffix( export function parseRawSessionConversationRef( sessionKey: string | undefined | null, ): RawSessionConversationRef | null { - const raw = (sessionKey ?? "").trim(); + const raw = normalizeOptionalString(sessionKey); if (!raw) { return null; } const rawParts = raw.split(":").filter(Boolean); const bodyStartIndex = - rawParts.length >= 3 && rawParts[0]?.trim().toLowerCase() === "agent" ? 2 : 0; + rawParts.length >= 3 && normalizeOptionalString(rawParts[0])?.toLowerCase() === "agent" ? 2 : 0; const parts = rawParts.slice(bodyStartIndex); if (parts.length < 3) { return null; } const channel = normalizeSessionConversationChannel(parts[0]); - const kind = parts[1]?.trim().toLowerCase(); + const kind = normalizeOptionalString(parts[1])?.toLowerCase(); if (!channel || (kind !== "group" && kind !== "channel")) { return null; } - const rawId = parts.slice(2).join(":").trim(); - const prefix = rawParts - .slice(0, bodyStartIndex + 2) - .join(":") - .trim(); + const rawId = normalizeOptionalString(parts.slice(2).join(":")); + const prefix = normalizeOptionalString(rawParts.slice(0, bodyStartIndex + 2).join(":")); if (!rawId || !prefix) { return null; } @@ -157,7 +155,7 @@ export function resolveThreadParentSessionKey( if (!threadId) { return null; } - const parent = baseSessionKey?.trim(); + const parent = normalizeOptionalString(baseSessionKey); if (!parent) { return null; } diff --git a/src/sessions/transcript-events.ts b/src/sessions/transcript-events.ts index c870b9407f0..4c540d209ef 100644 --- a/src/sessions/transcript-events.ts +++ b/src/sessions/transcript-events.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export type SessionTranscriptUpdate = { sessionFile: string; sessionKey?: string; @@ -26,18 +28,18 @@ export function emitSessionTranscriptUpdate(update: string | SessionTranscriptUp message: update.message, messageId: update.messageId, }; - const trimmed = normalized.sessionFile.trim(); + const trimmed = normalizeOptionalString(normalized.sessionFile); if (!trimmed) { return; } const nextUpdate: SessionTranscriptUpdate = { sessionFile: trimmed, - ...(typeof normalized.sessionKey === "string" && normalized.sessionKey.trim() - ? { sessionKey: normalized.sessionKey.trim() } + ...(normalizeOptionalString(normalized.sessionKey) + ? { sessionKey: normalizeOptionalString(normalized.sessionKey) } : {}), ...(normalized.message !== undefined ? { message: normalized.message } : {}), - ...(typeof normalized.messageId === "string" && normalized.messageId.trim() - ? { messageId: normalized.messageId.trim() } + ...(normalizeOptionalString(normalized.messageId) + ? { messageId: normalizeOptionalString(normalized.messageId) } : {}), }; for (const listener of SESSION_TRANSCRIPT_LISTENERS) {