From aec24f4599be734ace008f48f4ba687224ff12af Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 01:29:26 +0100 Subject: [PATCH] refactor: dedupe messaging trimmed readers --- extensions/googlechat/src/channel.adapters.ts | 15 ++++++------ extensions/googlechat/src/monitor-access.ts | 7 ++++-- extensions/googlechat/src/setup-surface.ts | 16 ++++++++----- .../mattermost/src/mattermost/client.ts | 7 ++++-- .../mattermost/src/mattermost/interactions.ts | 9 ++++--- .../mattermost/src/mattermost/model-picker.ts | 16 ++++++++----- .../mattermost/src/mattermost/monitor.ts | 24 ++++++++++--------- .../mattermost/src/mattermost/send.test.ts | 11 +++++++++ extensions/mattermost/src/mattermost/send.ts | 11 +++++---- .../src/mattermost/target-resolution.ts | 3 ++- extensions/qqbot/src/api.ts | 13 +++++----- extensions/qqbot/src/channel-config-shared.ts | 9 ++++--- extensions/qqbot/src/inbound-attachments.ts | 3 ++- extensions/qqbot/src/outbound-deliver.ts | 7 ++++-- extensions/qqbot/src/outbound.ts | 7 ++++-- extensions/qqbot/src/setup-surface.ts | 4 ++-- extensions/qqbot/src/stt.ts | 3 ++- extensions/qqbot/src/utils/file-utils.ts | 11 ++++++--- extensions/signal/src/accounts.ts | 12 +++++----- extensions/signal/src/monitor.ts | 17 +++++++++---- .../signal/src/monitor/event-handler.ts | 6 ++--- .../signal/src/monitor/inbound-context.ts | 3 ++- extensions/signal/src/rpc-context.ts | 11 +++++---- extensions/signal/src/shared.ts | 9 ++++--- .../slack/src/approval-handler.runtime.ts | 6 ++--- extensions/slack/src/approval-native.ts | 6 ++--- extensions/slack/src/blocks-render.ts | 3 ++- extensions/slack/src/channel-type.ts | 10 ++++++-- extensions/slack/src/channel.ts | 10 +++++--- extensions/slack/src/directory-live.ts | 9 +++---- extensions/slack/src/exec-approvals.ts | 3 ++- extensions/slack/src/monitor/commands.ts | 7 ++++-- extensions/slack/src/monitor/context.ts | 9 ++++--- .../events/interactions.block-actions.ts | 24 ++++++++++--------- .../message-handler/prepare-content.ts | 11 ++++++--- .../src/monitor/message-handler/prepare.ts | 2 +- extensions/slack/src/monitor/room-context.ts | 3 ++- extensions/slack/src/send.ts | 7 ++++-- 38 files changed, 218 insertions(+), 126 deletions(-) diff --git a/extensions/googlechat/src/channel.adapters.ts b/extensions/googlechat/src/channel.adapters.ts index 97d58002886..d05f14c1da2 100644 --- a/extensions/googlechat/src/channel.adapters.ts +++ b/extensions/googlechat/src/channel.adapters.ts @@ -11,7 +11,10 @@ import { import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; import type { OutboundMediaLoadOptions } from "openclaw/plugin-sdk/outbound-media"; import { sanitizeForPlainText } from "openclaw/plugin-sdk/outbound-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { type ResolvedGoogleChatAccount, chunkTextForOutbound, @@ -147,7 +150,7 @@ export const googlechatOutboundAdapter = { textChunkLimit: 4000, sanitizeText: ({ text }: { text: string }) => sanitizeForPlainText(text), resolveTarget: ({ to }: { to?: string }) => { - const trimmed = to?.trim() ?? ""; + const trimmed = normalizeOptionalString(to) ?? ""; if (trimmed) { const normalized = normalizeGoogleChatTarget(trimmed); @@ -189,9 +192,7 @@ export const googlechatOutboundAdapter = { }); const space = await resolveGoogleChatOutboundSpace({ account, target: to }); const thread = - typeof threadId === "number" - ? String(threadId) - : (threadId ?? replyToId ?? undefined); + typeof threadId === "number" ? String(threadId) : (threadId ?? replyToId ?? undefined); const { sendGoogleChatMessage } = await loadGoogleChatChannelRuntime(); const result = await sendGoogleChatMessage({ account, @@ -236,9 +237,7 @@ export const googlechatOutboundAdapter = { }); const space = await resolveGoogleChatOutboundSpace({ account, target: to }); const thread = - typeof threadId === "number" - ? String(threadId) - : (threadId ?? replyToId ?? undefined); + typeof threadId === "number" ? String(threadId) : (threadId ?? replyToId ?? undefined); const maxBytes = resolveChannelMediaMaxBytes({ cfg: cfg, resolveChannelLimitMb: ({ cfg, accountId }) => diff --git a/extensions/googlechat/src/monitor-access.ts b/extensions/googlechat/src/monitor-access.ts index f6330b14794..732372aca2c 100644 --- a/extensions/googlechat/src/monitor-access.ts +++ b/extensions/googlechat/src/monitor-access.ts @@ -21,7 +21,7 @@ import type { GoogleChatCoreRuntime } from "./monitor-types.js"; import type { GoogleChatAnnotation, GoogleChatMessage, GoogleChatSpace } from "./types.js"; function normalizeUserId(raw?: string | null): string { - const trimmed = raw?.trim() ?? ""; + const trimmed = normalizeOptionalString(raw) ?? ""; if (!trimmed) { return ""; } @@ -129,7 +129,10 @@ const warnedDeprecatedUsersEmailAllowFrom = new Set(); const warnedMutableGroupKeys = new Set(); function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void, entries: string[]) { - const deprecated = entries.map((v) => String(v).trim()).filter((v) => /^users\/.+@.+/i.test(v)); + const deprecated = entries + .map((v) => normalizeOptionalString(v)) + .filter((v): v is string => Boolean(v)) + .filter((v) => /^users\/.+@.+/i.test(v)); if (deprecated.length === 0) { return; } diff --git a/extensions/googlechat/src/setup-surface.ts b/extensions/googlechat/src/setup-surface.ts index c09d33d1b22..dd68fa38ce5 100644 --- a/extensions/googlechat/src/setup-surface.ts +++ b/extensions/googlechat/src/setup-surface.ts @@ -11,6 +11,10 @@ import { type ChannelSetupDmPolicy, type ChannelSetupWizard, } from "openclaw/plugin-sdk/setup"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveDefaultGoogleChatAccountId, resolveGoogleChatAccount } from "./accounts.js"; const channel = "googlechat" as const; @@ -157,8 +161,8 @@ export const googlechatSetupWizard: ChannelSetupWizard = { placeholder: "/path/to/service-account.json", shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "file", - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => String(value).trim(), + validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), + normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => applySetupAccountConfigPatch({ cfg, @@ -173,8 +177,8 @@ export const googlechatSetupWizard: ChannelSetupWizard = { placeholder: '{"type":"service_account", ... }', shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "inline", - validate: ({ value }) => (String(value ?? "").trim() ? undefined : "Required"), - normalizeValue: ({ value }) => String(value).trim(), + validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), + normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => applySetupAccountConfigPatch({ cfg, @@ -202,7 +206,7 @@ export const googlechatSetupWizard: ChannelSetupWizard = { placeholder: audienceType === "project-number" ? "1234567890" : "https://your.host/googlechat", initialValue: account.config.audience || undefined, - validate: (value) => (String(value ?? "").trim() ? undefined : "Required"), + validate: (value) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), }); return { cfg: migrateBaseNameToDefaultAccount({ @@ -212,7 +216,7 @@ export const googlechatSetupWizard: ChannelSetupWizard = { accountId, patch: { audienceType, - audience: String(audience).trim(), + audience: normalizeOptionalString(audience) ?? "", }, }), channelKey: channel, diff --git a/extensions/mattermost/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts index 46d6dbe515c..6f4a14f1e46 100644 --- a/extensions/mattermost/src/mattermost/client.ts +++ b/extensions/mattermost/src/mattermost/client.ts @@ -2,7 +2,10 @@ import { fetchWithSsrFGuard, ssrfPolicyFromPrivateNetworkOptIn, } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { z } from "openclaw/plugin-sdk/zod"; export type MattermostFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -552,7 +555,7 @@ export async function uploadMattermostFile( }, ): Promise { const form = new FormData(); - const fileName = params.fileName?.trim() || "upload"; + const fileName = normalizeOptionalString(params.fileName) ?? "upload"; const bytes = Uint8Array.from(params.buffer); const blob = params.contentType ? new Blob([bytes], { type: params.contentType }) diff --git a/extensions/mattermost/src/mattermost/interactions.ts b/extensions/mattermost/src/mattermost/interactions.ts index 61408a1e18b..f64a504ef95 100644 --- a/extensions/mattermost/src/mattermost/interactions.ts +++ b/extensions/mattermost/src/mattermost/interactions.ts @@ -1,7 +1,10 @@ import { createHmac } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { getMattermostRuntime } from "../runtime.js"; import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js"; import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "./runtime-api.js"; @@ -320,8 +323,8 @@ export function buildButtonProps(params: { const buttons = rawButtons .map((btn) => ({ - id: String(btn.id ?? btn.callback_data ?? "").trim(), - name: String(btn.text ?? btn.name ?? btn.label ?? "").trim(), + id: normalizeStringifiedOptionalString(btn.id ?? btn.callback_data) ?? "", + name: normalizeStringifiedOptionalString(btn.text ?? btn.name ?? btn.label) ?? "", style: btn.style ?? "default", context: typeof btn.context === "object" && btn.context !== null diff --git a/extensions/mattermost/src/mattermost/model-picker.ts b/extensions/mattermost/src/mattermost/model-picker.ts index 57bcbf429ff..74d4e95429b 100644 --- a/extensions/mattermost/src/mattermost/model-picker.ts +++ b/extensions/mattermost/src/mattermost/model-picker.ts @@ -1,4 +1,8 @@ import { createHash } from "node:crypto"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { MattermostInteractiveButtonInput } from "./interactions.js"; import { loadSessionStore, @@ -35,14 +39,14 @@ export type MattermostModelPickerRenderedView = { }; function splitModelRef(modelRef?: string | null): { provider: string; model: string } | null { - const trimmed = modelRef?.trim(); + const trimmed = normalizeOptionalString(modelRef); const match = trimmed?.match(/^([^/]+)\/(.+)$/u); if (!match) { return null; } const provider = normalizeProviderId(match[1]); // Mattermost copy should normalize accidental whitespace around the model. - const model = match[2].trim(); + const model = normalizeOptionalString(match[2]); if (!provider || !model) { return null; } @@ -128,7 +132,7 @@ function buildButton(params: { ownerUserId: params.ownerUserId, provider: normalizeProviderId(params.provider ?? ""), page: normalizePage(params.page), - model: String(params.model ?? "").trim(), + model: normalizeStringifiedOptionalString(params.model) ?? "", }; return { @@ -179,8 +183,8 @@ export function parseMattermostModelPickerContext( return null; } - const ownerUserId = readContextString(context, "ownerUserId").trim(); - const action = readContextString(context, "action").trim(); + const ownerUserId = normalizeOptionalString(readContextString(context, "ownerUserId")) ?? ""; + const action = normalizeOptionalString(readContextString(context, "action")) ?? ""; if (!ownerUserId) { return null; } @@ -205,7 +209,7 @@ export function parseMattermostModelPickerContext( } if (action === "select") { - const model = readContextString(context, "model").trim(); + const model = normalizeOptionalString(readContextString(context, "model")) ?? ""; if (!model) { return null; } diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 5961a582424..7692e3c7a0c 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -122,7 +122,9 @@ function isLoopbackHost(hostname: string): boolean { } function normalizeInteractionSourceIps(values?: string[]): string[] { - return (values ?? []).map((value) => value.trim()).filter(Boolean); + return (values ?? []) + .map((value) => normalizeOptionalString(value)) + .filter((value): value is string => Boolean(value)); } const recentInboundMessages = createDedupeCache({ @@ -143,8 +145,7 @@ function resolveRuntime(opts: MonitorMattermostOpts): RuntimeEnv { } function isSystemPost(post: MattermostPost): boolean { - const type = post.type?.trim(); - return Boolean(type); + return normalizeOptionalString(post.type) !== undefined; } function channelChatType(kind: ChatType): "direct" | "group" | "channel" { @@ -264,7 +265,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} accountId: account.accountId, }); const allowNameMatching = isDangerousNameMatchingEnabled(account.config); - const botToken = opts.botToken?.trim() || account.botToken?.trim(); + const botToken = + normalizeOptionalString(opts.botToken) ?? normalizeOptionalString(account.botToken); if (!botToken) { throw new Error( `Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`, @@ -1041,10 +1043,10 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const chatType = channelChatType(kind); const senderName = - payload.data?.sender_name?.trim() || - (await resolveUserInfo(senderId))?.username?.trim() || + normalizeOptionalString(payload.data?.sender_name) ?? + normalizeOptionalString((await resolveUserInfo(senderId))?.username) ?? senderId; - const rawText = post.message?.trim() || ""; + const rawText = normalizeOptionalString(post.message) ?? ""; const dmPolicy = account.config.dmPolicy ?? "pairing"; const normalizedAllowFrom = normalizeMattermostAllowList(account.config.allowFrom ?? []); const normalizedGroupAllowFrom = normalizeMattermostAllowList( @@ -1533,7 +1535,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const action = isRemoved ? "removed" : "added"; const senderInfo = await resolveUserInfo(userId); - const senderName = senderInfo?.username?.trim() || userId; + const senderName = normalizeOptionalString(senderInfo?.username) ?? userId; // Resolve the channel from broadcast or post to route to the correct agent session const channelId = resolveMattermostReactionChannelId(payload); @@ -1632,7 +1634,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (!channelId) { return null; } - const threadId = entry.post.root_id?.trim(); + const threadId = normalizeOptionalString(entry.post.root_id); const threadKey = threadId ? `thread:${threadId}` : "channel"; return `mattermost:${account.accountId}:${channelId}:${threadKey}`; }, @@ -1640,7 +1642,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (entry.post.file_ids && entry.post.file_ids.length > 0) { return false; } - const text = entry.post.message?.trim() ?? ""; + const text = normalizeOptionalString(entry.post.message) ?? ""; if (!text) { return false; } @@ -1656,7 +1658,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} return; } const combinedText = entries - .map((entry) => entry.post.message?.trim() ?? "") + .map((entry) => normalizeOptionalString(entry.post.message) ?? "") .filter(Boolean) .join("\n"); const mergedPost: MattermostPost = { diff --git a/extensions/mattermost/src/mattermost/send.test.ts b/extensions/mattermost/src/mattermost/send.test.ts index c3c7076702f..6dc03d95854 100644 --- a/extensions/mattermost/src/mattermost/send.test.ts +++ b/extensions/mattermost/src/mattermost/send.test.ts @@ -58,6 +58,17 @@ vi.mock("openclaw/plugin-sdk/text-runtime", () => ({ const normalized = value.trim(); return normalized.length > 0 ? normalized : undefined; }), + normalizeStringifiedOptionalString: vi.fn((value: unknown) => { + if (typeof value === "string") { + const normalized = value.trim(); + return normalized.length > 0 ? normalized : undefined; + } + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + const normalized = String(value).trim(); + return normalized.length > 0 ? normalized : undefined; + } + return undefined; + }), })); vi.mock("./accounts.js", () => ({ diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index 1d6d9eb804a..e03dba6a8c4 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -3,6 +3,7 @@ import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; import { convertMarkdownTables, normalizeLowercaseStringOrEmpty, + normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; @@ -84,8 +85,8 @@ function cacheKey(baseUrl: string, token: string): string { } function normalizeMessage(text: string, mediaUrl?: string): string { - const trimmed = text.trim(); - const media = mediaUrl?.trim(); + const trimmed = normalizeOptionalString(text) ?? ""; + const media = normalizeOptionalString(mediaUrl); return [trimmed, media].filter(Boolean).join("\n"); } @@ -323,7 +324,7 @@ async function resolveMattermostSendContext( cfg, accountId: opts.accountId, }); - const token = opts.botToken?.trim() || account.botToken?.trim(); + const token = normalizeOptionalString(opts.botToken) ?? normalizeOptionalString(account.botToken); if (!token) { throw new Error( `Mattermost bot token missing for account "${account.accountId}" (set channels.mattermost.accounts.${account.accountId}.botToken or MATTERMOST_BOT_TOKEN for default).`, @@ -336,7 +337,7 @@ async function resolveMattermostSendContext( ); } - const trimmedTo = to?.trim() ?? ""; + const trimmedTo = normalizeOptionalString(to) ?? ""; const opaqueTarget = await resolveMattermostOpaqueTarget({ input: trimmedTo, token, @@ -414,7 +415,7 @@ export async function sendMessageMattermost( text: opts.attachmentText, }); } - let message = text?.trim() ?? ""; + let message = normalizeOptionalString(text) ?? ""; let fileIds: string[] | undefined; let uploadError: Error | undefined; const mediaUrl = opts.mediaUrl?.trim(); diff --git a/extensions/mattermost/src/mattermost/target-resolution.ts b/extensions/mattermost/src/mattermost/target-resolution.ts index b56b2a816e0..09c1c0aff92 100644 --- a/extensions/mattermost/src/mattermost/target-resolution.ts +++ b/extensions/mattermost/src/mattermost/target-resolution.ts @@ -1,4 +1,5 @@ import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveMattermostAccount } from "./accounts.js"; import { createMattermostClient, @@ -65,7 +66,7 @@ export async function resolveMattermostOpaqueTarget(params: { params.cfg && (!params.token || !params.baseUrl) ? resolveMattermostAccount({ cfg: params.cfg, accountId: params.accountId }) : null; - const token = params.token?.trim() || account?.botToken?.trim(); + const token = normalizeOptionalString(params.token) ?? normalizeOptionalString(account?.botToken); const baseUrl = normalizeMattermostBaseUrl(params.baseUrl ?? account?.baseUrl); if (!token || !baseUrl) { return null; diff --git a/extensions/qqbot/src/api.ts b/extensions/qqbot/src/api.ts index c2b50f994d8..9ab281655b2 100644 --- a/extensions/qqbot/src/api.ts +++ b/extensions/qqbot/src/api.ts @@ -1,6 +1,7 @@ import { createRequire } from "node:module"; import os from "node:os"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { debugLog, debugError } from "./utils/debug-log.js"; import { sanitizeFileName } from "./utils/platform.js"; import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js"; @@ -37,17 +38,17 @@ const onMessageSentHookMap = new Map(); /** Register an outbound-message hook scoped to one appId. */ export function onMessageSent(appId: string, callback: OnMessageSentCallback): void { - onMessageSentHookMap.set(String(appId).trim(), callback); + onMessageSentHookMap.set(normalizeOptionalString(appId) ?? "", callback); } /** Initialize per-app API behavior such as markdown support. */ export function initApiConfig(appId: string, options: { markdownSupport?: boolean }): void { - markdownSupportMap.set(String(appId).trim(), options.markdownSupport === true); + markdownSupportMap.set(normalizeOptionalString(appId) ?? "", options.markdownSupport === true); } /** Return whether markdown is enabled for the given appId. */ export function isMarkdownSupport(appId: string): boolean { - return markdownSupportMap.get(String(appId).trim()) ?? false; + return markdownSupportMap.get(normalizeOptionalString(appId) ?? "") ?? false; } // Keep token state per appId to avoid multi-account cross-talk. @@ -58,7 +59,7 @@ const tokenFetchPromises = new Map>(); * Resolve an access token with caching and singleflight semantics. */ export async function getAccessToken(appId: string, clientSecret: string): Promise { - const normalizedAppId = String(appId).trim(); + const normalizedAppId = normalizeOptionalString(appId) ?? ""; const cachedToken = tokenCacheMap.get(normalizedAppId); // Refresh slightly ahead of expiry without making short-lived tokens unusable. @@ -153,7 +154,7 @@ async function doFetchToken(appId: string, clientSecret: string): Promise { const result = await apiRequest(accessToken, method, path, body); - const hook = onMessageSentHookMap.get(String(appId).trim()); + const hook = onMessageSentHookMap.get(normalizeOptionalString(appId) ?? ""); if (result.ext_info?.ref_idx && hook) { try { hook(result.ext_info.ref_idx, meta); diff --git a/extensions/qqbot/src/channel-config-shared.ts b/extensions/qqbot/src/channel-config-shared.ts index 639c33821e0..ba20be5aafe 100644 --- a/extensions/qqbot/src/channel-config-shared.ts +++ b/extensions/qqbot/src/channel-config-shared.ts @@ -6,7 +6,10 @@ import { } from "openclaw/plugin-sdk/core"; import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input"; import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { DEFAULT_ACCOUNT_ID, applyQQBotAccountConfig, @@ -117,8 +120,8 @@ export function formatQQBotAllowFrom(params: { allowFrom: Array | undefined | null; }): string[] { return (params.allowFrom ?? []) - .map((entry) => String(entry).trim()) - .filter(Boolean) + .map((entry) => normalizeStringifiedOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)) .map((entry) => entry.replace(/^qqbot:/i, "")) .map((entry) => entry.toUpperCase()); } diff --git a/extensions/qqbot/src/inbound-attachments.ts b/extensions/qqbot/src/inbound-attachments.ts index 7407b570d56..ab06282e751 100644 --- a/extensions/qqbot/src/inbound-attachments.ts +++ b/extensions/qqbot/src/inbound-attachments.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { transcribeAudio, resolveSTTConfig } from "./stt.js"; import { convertSilkToWav, isVoiceAttachment, formatDuration } from "./utils/audio-convert.js"; import { downloadFile } from "./utils/file-utils.js"; @@ -110,7 +111,7 @@ export async function processAttachments( // Phase 2: convert/transcribe voice attachments and classify everything else. const processTasks = downloadResults.map( async ({ att, attUrl, isVoice, localPath, audioPath }) => { - const asrReferText = typeof att.asr_refer_text === "string" ? att.asr_refer_text.trim() : ""; + const asrReferText = normalizeOptionalString(att.asr_refer_text) ?? ""; const wavUrl = isVoice && att.voice_wav_url ? att.voice_wav_url.startsWith("//") diff --git a/extensions/qqbot/src/outbound-deliver.ts b/extensions/qqbot/src/outbound-deliver.ts index b1445f9976c..1266ea23def 100644 --- a/extensions/qqbot/src/outbound-deliver.ts +++ b/extensions/qqbot/src/outbound-deliver.ts @@ -6,7 +6,10 @@ * 2. `sendPlainReply` handles plain replies, including markdown images and mixed text/media. */ -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { sendC2CMessage, sendDmMessage, @@ -186,7 +189,7 @@ export async function parseAndSendMediaTags( } const tagName = normalizeLowercaseStringOrEmpty(match[1]); - let mediaPath = decodeMediaPath(match[2]?.trim() ?? "", log, prefix); + let mediaPath = decodeMediaPath(normalizeOptionalString(match[2]) ?? "", log, prefix); if (mediaPath) { const typeMap: Record = { diff --git a/extensions/qqbot/src/outbound.ts b/extensions/qqbot/src/outbound.ts index 09e38c0eff0..ae2924383a5 100644 --- a/extensions/qqbot/src/outbound.ts +++ b/extensions/qqbot/src/outbound.ts @@ -1,6 +1,9 @@ import * as path from "path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { getAccessToken, sendC2CFileMessage, @@ -889,7 +892,7 @@ export async function sendText(ctx: OutboundContext): Promise { const tagName = normalizeLowercaseStringOrEmpty(match[1]); - let mediaPath = match[2]?.trim() ?? ""; + let mediaPath = normalizeOptionalString(match[2]) ?? ""; if (mediaPath.startsWith("MEDIA:")) { mediaPath = mediaPath.slice("MEDIA:".length); } diff --git a/extensions/qqbot/src/setup-surface.ts b/extensions/qqbot/src/setup-surface.ts index 7556654ec8f..05ff48de9f5 100644 --- a/extensions/qqbot/src/setup-surface.ts +++ b/extensions/qqbot/src/setup-surface.ts @@ -103,7 +103,7 @@ export const qqbotSetupWizard: ChannelSetupWizard = { const resolved = resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }); const hasConfiguredValue = Boolean( hasConfiguredSecretInput(resolved.config.clientSecret) || - resolved.config.clientSecretFile?.trim() || + normalizeOptionalString(resolved.config.clientSecretFile) || resolved.clientSecret, ); return { @@ -136,7 +136,7 @@ export const qqbotSetupWizard: ChannelSetupWizard = { const resolved = resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }); const hasConfiguredValue = Boolean( hasConfiguredSecretInput(resolved.config.clientSecret) || - resolved.config.clientSecretFile?.trim() || + normalizeOptionalString(resolved.config.clientSecretFile) || resolved.clientSecret, ); return { diff --git a/extensions/qqbot/src/stt.ts b/extensions/qqbot/src/stt.ts index e8988f8ce28..24801180263 100644 --- a/extensions/qqbot/src/stt.ts +++ b/extensions/qqbot/src/stt.ts @@ -6,6 +6,7 @@ import * as fs from "node:fs"; import path from "node:path"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { asRecord, readString } from "./config-record-shared.js"; import { sanitizeFileName } from "./utils/platform.js"; @@ -89,5 +90,5 @@ export async function transcribeAudio( } const result = (await resp.json()) as { text?: string }; - return result.text?.trim() || null; + return normalizeOptionalString(result.text) ?? null; } diff --git a/extensions/qqbot/src/utils/file-utils.ts b/extensions/qqbot/src/utils/file-utils.ts index eec3b768dd4..f7509cab660 100644 --- a/extensions/qqbot/src/utils/file-utils.ts +++ b/extensions/qqbot/src/utils/file-utils.ts @@ -4,7 +4,10 @@ import * as path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { fetchRemoteMedia } from "openclaw/plugin-sdk/media-runtime"; import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; /** Maximum file size accepted by the QQ Bot API. */ export const MAX_UPLOAD_SIZE = 20 * 1024 * 1024; @@ -146,9 +149,11 @@ export async function downloadFile( ssrfPolicy: QQBOT_MEDIA_SSRF_POLICY, }); - let filename = originalFilename?.trim() || ""; + let filename = normalizeOptionalString(originalFilename) ?? ""; if (!filename) { - filename = fetched.fileName?.trim() || path.basename(parsedUrl.pathname) || "download"; + filename = + (normalizeOptionalString(fetched.fileName) ?? path.basename(parsedUrl.pathname)) || + "download"; } const ts = Date.now(); diff --git a/extensions/signal/src/accounts.ts b/extensions/signal/src/accounts.ts index c3d43b483a6..b55767d4c72 100644 --- a/extensions/signal/src/accounts.ts +++ b/extensions/signal/src/accounts.ts @@ -41,14 +41,14 @@ export function resolveSignalAccount(params: { const merged = mergeSignalAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; - const host = merged.httpHost?.trim() || "127.0.0.1"; + const host = normalizeOptionalString(merged.httpHost) ?? "127.0.0.1"; const port = merged.httpPort ?? 8080; - const baseUrl = merged.httpUrl?.trim() || `http://${host}:${port}`; + const baseUrl = normalizeOptionalString(merged.httpUrl) ?? `http://${host}:${port}`; const configured = Boolean( - merged.account?.trim() || - merged.httpUrl?.trim() || - merged.cliPath?.trim() || - merged.httpHost?.trim() || + normalizeOptionalString(merged.account) || + normalizeOptionalString(merged.httpUrl) || + normalizeOptionalString(merged.cliPath) || + normalizeOptionalString(merged.httpHost) || typeof merged.httpPort === "number" || typeof merged.autoStart === "boolean", ); diff --git a/extensions/signal/src/monitor.ts b/extensions/signal/src/monitor.ts index fda76edcf20..914f15bf3fc 100644 --- a/extensions/signal/src/monitor.ts +++ b/extensions/signal/src/monitor.ts @@ -24,8 +24,11 @@ import { type BackoffPolicy, type RuntimeEnv, } from "openclaw/plugin-sdk/runtime-env"; -import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime"; -import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeE164, + normalizeOptionalString, + normalizeStringEntries, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveSignalAccount } from "./accounts.js"; import { signalCheck, signalRpcRequest } from "./client.js"; import { formatSignalDaemonExit, spawnSignalDaemon, type SignalDaemonHandle } from "./daemon.js"; @@ -164,7 +167,10 @@ function isSignalReactionMessage( } const emoji = reaction.emoji?.trim(); const timestamp = reaction.targetSentTimestamp; - const hasTarget = Boolean(reaction.targetAuthor?.trim() || reaction.targetAuthorUuid?.trim()); + const hasTarget = Boolean( + normalizeOptionalString(reaction.targetAuthor) || + normalizeOptionalString(reaction.targetAuthorUuid), + ); return Boolean(emoji && typeof timestamp === "number" && timestamp > 0 && hasTarget); } @@ -356,8 +362,9 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi const groupHistories = new Map(); const textLimit = resolveTextChunkLimit(cfg, "signal", accountInfo.accountId); const chunkMode = resolveChunkMode(cfg, "signal", accountInfo.accountId); - const baseUrl = opts.baseUrl?.trim() || accountInfo.baseUrl; - const account = opts.account?.trim() || accountInfo.config.account?.trim(); + const baseUrl = normalizeOptionalString(opts.baseUrl) ?? accountInfo.baseUrl; + const account = + normalizeOptionalString(opts.account) ?? normalizeOptionalString(accountInfo.config.account); const dmPolicy = accountInfo.config.dmPolicy ?? "pairing"; const allowFrom = normalizeAllowList(opts.allowFrom ?? accountInfo.config.allowFrom); const groupAllowFrom = normalizeAllowList( diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 6eb5b7ee9e3..dbe228cd5bd 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -42,7 +42,7 @@ import { DM_GROUP_ACCESS_REASON, resolvePinnedMainDmOwnerFromAllowlist, } from "openclaw/plugin-sdk/security-runtime"; -import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { normalizeE164, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { formatSignalPairingIdLine, formatSignalSenderDisplay, @@ -416,7 +416,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { if (params.reaction.isRemove) { return true; // Ignore reaction removals } - const emojiLabel = params.reaction.emoji?.trim() || "emoji"; + const emojiLabel = normalizeOptionalString(params.reaction.emoji) ?? "emoji"; const senderName = params.envelope.sourceName ?? params.senderDisplay; logVerbose(`signal reaction: ${emojiLabel} from ${senderName}`); const groupId = params.reaction.groupInfo?.groupId ?? undefined; @@ -546,7 +546,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { groupAllowFrom: deps.groupAllowFrom, sender, }); - const quoteText = dataMessage?.quote?.text?.trim() ?? ""; + const quoteText = normalizeOptionalString(dataMessage?.quote?.text) ?? ""; const { contextVisibilityMode, quoteSenderAllowed, visibleQuoteText, visibleQuoteSender } = resolveSignalQuoteContext({ cfg: deps.cfg, diff --git a/extensions/signal/src/monitor/inbound-context.ts b/extensions/signal/src/monitor/inbound-context.ts index 94f8e89da0f..fc06cd9d218 100644 --- a/extensions/signal/src/monitor/inbound-context.ts +++ b/extensions/signal/src/monitor/inbound-context.ts @@ -3,6 +3,7 @@ import { evaluateSupplementalContextVisibility, type ContextVisibilityDecision, } from "openclaw/plugin-sdk/security-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { formatSignalSenderDisplay, isSignalSenderAllowed, @@ -30,7 +31,7 @@ export function resolveSignalQuoteContext(params: { channel: "signal", accountId: params.accountId, }); - const quoteText = params.dataMessage?.quote?.text?.trim() ?? ""; + const quoteText = normalizeOptionalString(params.dataMessage?.quote?.text) ?? ""; const quoteSender = resolveSignalSender({ sourceNumber: params.dataMessage?.quote?.author ?? null, sourceUuid: params.dataMessage?.quote?.authorUuid ?? null, diff --git a/extensions/signal/src/rpc-context.ts b/extensions/signal/src/rpc-context.ts index 1a02247c09d..1ad0602e8ea 100644 --- a/extensions/signal/src/rpc-context.ts +++ b/extensions/signal/src/rpc-context.ts @@ -1,19 +1,22 @@ +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveSignalAccount } from "./accounts.js"; export function resolveSignalRpcContext( opts: { baseUrl?: string; account?: string; accountId?: string }, accountInfo?: ReturnType, ) { - const hasBaseUrl = Boolean(opts.baseUrl?.trim()); - const hasAccount = Boolean(opts.account?.trim()); + const hasBaseUrl = Boolean(normalizeOptionalString(opts.baseUrl)); + const hasAccount = Boolean(normalizeOptionalString(opts.account)); if ((!hasBaseUrl || !hasAccount) && !accountInfo) { throw new Error("Signal account config is required when baseUrl or account is missing"); } const resolvedAccount = accountInfo; - const baseUrl = opts.baseUrl?.trim() || resolvedAccount?.baseUrl; + const baseUrl = normalizeOptionalString(opts.baseUrl) ?? resolvedAccount?.baseUrl; if (!baseUrl) { throw new Error("Signal base URL is required"); } - const account = opts.account?.trim() || resolvedAccount?.config.account?.trim(); + const account = + normalizeOptionalString(opts.account) ?? + normalizeOptionalString(resolvedAccount?.config.account); return { baseUrl, account }; } diff --git a/extensions/signal/src/shared.ts b/extensions/signal/src/shared.ts index 5d9d591d05a..9f8f2f0bae3 100644 --- a/extensions/signal/src/shared.ts +++ b/extensions/signal/src/shared.ts @@ -6,7 +6,10 @@ import { import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channel-policy"; import { createChannelPluginBase, getChatChannelMeta } from "openclaw/plugin-sdk/core"; import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; -import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeE164, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { listSignalAccountIds, resolveDefaultSignalAccountId, @@ -35,8 +38,8 @@ export const signalConfigAdapter = createScopedChannelConfigAdapter account.config.allowFrom, formatAllowFrom: (allowFrom) => allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) + .map((entry) => normalizeStringifiedOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)) .map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, "")))) .filter(Boolean), resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo, diff --git a/extensions/slack/src/approval-handler.runtime.ts b/extensions/slack/src/approval-handler.runtime.ts index 8eb43046267..9f64c8bbe02 100644 --- a/extensions/slack/src/approval-handler.runtime.ts +++ b/extensions/slack/src/approval-handler.runtime.ts @@ -13,7 +13,7 @@ import { buildApprovalInteractiveReplyFromActionDescriptors, type ExecApprovalRequest, } from "openclaw/plugin-sdk/infra-runtime"; -import { logError } from "openclaw/plugin-sdk/text-runtime"; +import { logError, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { slackNativeApprovalAdapter } from "./approval-native.js"; import { isSlackExecApprovalClientEnabled, @@ -47,7 +47,7 @@ function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): context: SlackApprovalHandlerContext; } | null { const context = params.context as SlackApprovalHandlerContext | undefined; - const accountId = params.accountId?.trim() || ""; + const accountId = normalizeOptionalString(params.accountId) ?? ""; if (!context?.app || !accountId) { return null; } @@ -71,7 +71,7 @@ function formatSlackApprover(resolvedBy?: string | null): string | null { if (normalized) { return `<@${normalized}>`; } - const trimmed = resolvedBy?.trim(); + const trimmed = normalizeOptionalString(resolvedBy); return trimmed ? trimmed : null; } diff --git a/extensions/slack/src/approval-native.ts b/extensions/slack/src/approval-native.ts index 2cc3b9248e7..daba85620e0 100644 --- a/extensions/slack/src/approval-native.ts +++ b/extensions/slack/src/approval-native.ts @@ -54,7 +54,7 @@ function normalizeSlackThreadMatchKey(threadId?: string): string { function resolveTurnSourceSlackOriginTarget(request: ApprovalRequest): SlackOriginTarget | null { const turnSourceChannel = normalizeLowercaseStringOrEmpty(request.request.turnSourceChannel); - const turnSourceTo = request.request.turnSourceTo?.trim() || ""; + const turnSourceTo = normalizeOptionalString(request.request.turnSourceTo) ?? ""; if (turnSourceChannel !== "slack" || !turnSourceTo) { return null; } @@ -67,7 +67,7 @@ function resolveTurnSourceSlackOriginTarget(request: ApprovalRequest): SlackOrig } const threadId = typeof request.request.turnSourceThreadId === "string" - ? request.request.turnSourceThreadId.trim() || undefined + ? normalizeOptionalString(request.request.turnSourceThreadId) : typeof request.request.turnSourceThreadId === "number" ? String(request.request.turnSourceThreadId) : undefined; @@ -85,7 +85,7 @@ function resolveSessionSlackOriginTarget(sessionTarget: { to: sessionTarget.to, threadId: typeof sessionTarget.threadId === "string" - ? sessionTarget.threadId.trim() || undefined + ? normalizeOptionalString(sessionTarget.threadId) : typeof sessionTarget.threadId === "number" ? String(sessionTarget.threadId) : undefined, diff --git a/extensions/slack/src/blocks-render.ts b/extensions/slack/src/blocks-render.ts index ea411e8d116..6bb614b4521 100644 --- a/extensions/slack/src/blocks-render.ts +++ b/extensions/slack/src/blocks-render.ts @@ -1,6 +1,7 @@ import type { Block, KnownBlock } from "@slack/web-api"; import { reduceInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime"; import type { InteractiveReply } from "openclaw/plugin-sdk/interactive-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { truncateSlackText } from "./truncate.js"; export const SLACK_REPLY_BUTTON_ACTION_ID = "openclaw:reply_button"; @@ -88,7 +89,7 @@ export function buildSlackInteractiveBlocks(interactive?: InteractiveReply): Sla placeholder: { type: "plain_text", text: truncateSlackText( - block.placeholder?.trim() || "Choose an option", + normalizeOptionalString(block.placeholder) ?? "Choose an option", SLACK_PLAIN_TEXT_MAX, ), emoji: true, diff --git a/extensions/slack/src/channel-type.ts b/extensions/slack/src/channel-type.ts index 3a00c827f1b..5f1c17dbd6e 100644 --- a/extensions/slack/src/channel-type.ts +++ b/extensions/slack/src/channel-type.ts @@ -1,4 +1,7 @@ -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveSlackAccount } from "./accounts.js"; import { createSlackWebClient } from "./client.js"; import { normalizeAllowListLower } from "./monitor/allow-list.js"; @@ -49,7 +52,10 @@ export async function resolveSlackChannelType(params: { return "channel"; } - const token = account.botToken?.trim() || account.config.userToken?.trim() || ""; + const token = + normalizeOptionalString(account.botToken) ?? + normalizeOptionalString(account.config.userToken) ?? + ""; if (!token) { SLACK_CHANNEL_TYPE_CACHE.set(cacheKey, "unknown"); return "unknown"; diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index d066a283901..732296c0a28 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -271,7 +271,7 @@ const resolveSlackAllowlistGroupOverrides = createFlatAllowlistOverrideResolver( const resolveSlackAllowlistNames = createAccountScopedAllowlistNameResolver({ resolveAccount: resolveSlackAccount, resolveToken: (account: ResolvedSlackAccount) => - account.config.userToken?.trim() || account.botToken?.trim(), + normalizeOptionalString(account.config.userToken) ?? normalizeOptionalString(account.botToken), resolveNames: async ({ token, entries }) => (await loadSlackResolveUsersModule()).resolveSlackUserAllowlist({ token, entries }), }); @@ -385,7 +385,9 @@ export const slackPlugin: ChannelPlugin = crea const account = resolveSlackAccount({ cfg, accountId }); if (kind === "group") { return resolveTargetsWithOptionalToken({ - token: account.config.userToken?.trim() || account.botToken?.trim(), + token: + normalizeOptionalString(account.config.userToken) ?? + normalizeOptionalString(account.botToken), inputs, missingTokenNote: "missing Slack token", resolveWithToken: async ({ token, inputs }) => @@ -398,7 +400,9 @@ export const slackPlugin: ChannelPlugin = crea }); } return resolveTargetsWithOptionalToken({ - token: account.config.userToken?.trim() || account.botToken?.trim(), + token: + normalizeOptionalString(account.config.userToken) ?? + normalizeOptionalString(account.botToken), inputs, missingTokenNote: "missing Slack token", resolveWithToken: async ({ token, inputs }) => diff --git a/extensions/slack/src/directory-live.ts b/extensions/slack/src/directory-live.ts index 388cbd4dcdd..c6e00ef3b12 100644 --- a/extensions/slack/src/directory-live.ts +++ b/extensions/slack/src/directory-live.ts @@ -4,6 +4,7 @@ import type { } from "openclaw/plugin-sdk/directory-runtime"; import { normalizeLowercaseStringOrEmpty, + normalizeOptionalString, normalizeOptionalLowercaseString, } from "openclaw/plugin-sdk/text-runtime"; import { resolveSlackAccount } from "./accounts.js"; @@ -107,11 +108,11 @@ export async function listSlackDirectoryPeersLive( if (!id) { return null; } - const handle = member.name?.trim(); + const handle = normalizeOptionalString(member.name); const display = - member.profile?.display_name?.trim() || - member.profile?.real_name?.trim() || - member.real_name?.trim() || + normalizeOptionalString(member.profile?.display_name) || + normalizeOptionalString(member.profile?.real_name) || + normalizeOptionalString(member.real_name) || handle; return { kind: "user", diff --git a/extensions/slack/src/exec-approvals.ts b/extensions/slack/src/exec-approvals.ts index 243cd29bc0f..3f5fcfab3b6 100644 --- a/extensions/slack/src/exec-approvals.ts +++ b/extensions/slack/src/exec-approvals.ts @@ -5,10 +5,11 @@ import { } from "openclaw/plugin-sdk/approval-client-runtime"; import { doesApprovalRequestMatchChannelAccount } from "openclaw/plugin-sdk/approval-native-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveSlackAccount } from "./accounts.js"; export function normalizeSlackApproverId(value: string | number): string | undefined { - const trimmed = String(value).trim(); + const trimmed = normalizeStringifiedOptionalString(value); if (!trimmed) { return undefined; } diff --git a/extensions/slack/src/monitor/commands.ts b/extensions/slack/src/monitor/commands.ts index 1d83d9f74d1..fd9d37d82e2 100644 --- a/extensions/slack/src/monitor/commands.ts +++ b/extensions/slack/src/monitor/commands.ts @@ -1,4 +1,5 @@ import type { SlackSlashCommandConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; /** * Strip Slack mentions (<@U123>, <@U123|name>) so command detection works on @@ -18,12 +19,14 @@ export function normalizeSlackSlashCommandName(raw: string) { export function resolveSlackSlashCommandConfig( raw?: SlackSlashCommandConfig, ): Required { - const normalizedName = normalizeSlackSlashCommandName(raw?.name?.trim() || "openclaw"); + const normalizedName = normalizeSlackSlashCommandName( + normalizeOptionalString(raw?.name) ?? "openclaw", + ); const name = normalizedName || "openclaw"; return { enabled: raw?.enabled === true, name, - sessionPrefix: raw?.sessionPrefix?.trim() || "slack:slash", + sessionPrefix: normalizeOptionalString(raw?.sessionPrefix) ?? "slack:slash", ephemeral: raw?.ephemeral !== false, }; } diff --git a/extensions/slack/src/monitor/context.ts b/extensions/slack/src/monitor/context.ts index 670e85ff778..23bc0e45a65 100644 --- a/extensions/slack/src/monitor/context.ts +++ b/extensions/slack/src/monitor/context.ts @@ -12,7 +12,10 @@ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { getChildLogger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { SlackMessageEvent } from "../types.js"; import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; import type { SlackChannelConfigEntries } from "./channel-config.js"; @@ -162,7 +165,7 @@ export function createSlackMonitorContext(params: { channelType?: string | null; senderId?: string | null; }) => { - const channelId = p.channelId?.trim() ?? ""; + const channelId = normalizeOptionalString(p.channelId) ?? ""; if (!channelId) { return params.mainKey; } @@ -175,7 +178,7 @@ export function createSlackMonitorContext(params: { ? `slack:group:${channelId}` : `slack:channel:${channelId}`; const chatType = isDirectMessage ? "direct" : isGroup ? "group" : "channel"; - const senderId = p.senderId?.trim() ?? ""; + const senderId = normalizeOptionalString(p.senderId) ?? ""; // Resolve through shared channel/account bindings so system events route to // the same agent session as regular inbound messages. diff --git a/extensions/slack/src/monitor/events/interactions.block-actions.ts b/extensions/slack/src/monitor/events/interactions.block-actions.ts index 3b67265ecde..c272ba666f8 100644 --- a/extensions/slack/src/monitor/events/interactions.block-actions.ts +++ b/extensions/slack/src/monitor/events/interactions.block-actions.ts @@ -1,6 +1,7 @@ import type { SlackActionMiddlewareArgs } from "@slack/bolt"; import type { Block, KnownBlock } from "@slack/web-api"; import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { SLACK_REPLY_BUTTON_ACTION_ID, SLACK_REPLY_SELECT_ACTION_ID } from "../../blocks-render.js"; import { dispatchSlackPluginInteractiveHandler } from "../../interactive-dispatch.js"; import { authorizeSlackSystemEventSender } from "../auth.js"; @@ -326,7 +327,8 @@ function formatInteractionConfirmationText(params: { selectedLabel: string; userId?: string; }): string { - const actor = params.userId?.trim() ? ` by <@${params.userId.trim()}>` : ""; + const userId = normalizeOptionalString(params.userId); + const actor = userId ? ` by <@${userId}>` : ""; return `:white_check_mark: *${escapeSlackMrkdwn(params.selectedLabel)}* selected${actor}`; } @@ -334,13 +336,13 @@ function buildSlackPluginInteractionData(params: { actionId: string; summary: SlackActionSummary; }): string | null { - const actionId = params.actionId.trim(); + const actionId = normalizeOptionalString(params.actionId) ?? ""; if (!actionId) { return null; } const payload = - params.summary.value?.trim() || - params.summary.selectedValues?.map((value) => value.trim()).find(Boolean) || + normalizeOptionalString(params.summary.value) || + params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || ""; if ( actionId === SLACK_REPLY_BUTTON_ACTION_ID || @@ -371,15 +373,15 @@ function buildSlackPluginInteractionId(params: { summary: SlackActionSummary; }): string { const primaryValue = - params.summary.value?.trim() || - params.summary.selectedValues?.map((value) => value.trim()).find(Boolean) || + normalizeOptionalString(params.summary.value) || + params.summary.selectedValues?.map((value) => normalizeOptionalString(value)).find(Boolean) || ""; return [ - params.userId?.trim() || "", - params.channelId?.trim() || "", - params.messageTs?.trim() || "", - params.triggerId?.trim() || "", - params.actionId.trim(), + normalizeOptionalString(params.userId) ?? "", + normalizeOptionalString(params.channelId) ?? "", + normalizeOptionalString(params.messageTs) ?? "", + normalizeOptionalString(params.triggerId) ?? "", + normalizeOptionalString(params.actionId) ?? "", primaryValue, ].join(":"); } diff --git a/extensions/slack/src/monitor/message-handler/prepare-content.ts b/extensions/slack/src/monitor/message-handler/prepare-content.ts index 54a5183bfb0..326e81acbd9 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-content.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-content.ts @@ -1,4 +1,5 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { SlackFile, SlackMessageEvent } from "../../types.js"; import { MAX_SLACK_MEDIA_FILES, @@ -72,7 +73,7 @@ export async function resolveSlackMessageContent(params: { !mediaPlaceholder && fallbackFiles.length > 0 ? fallbackFiles .slice(0, MAX_SLACK_MEDIA_FILES) - .map((file) => file.name?.trim() || "file") + .map((file) => normalizeOptionalString(file.name) ?? "file") .join(", ") : undefined; const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : undefined; @@ -80,14 +81,18 @@ export async function resolveSlackMessageContent(params: { const botAttachmentText = params.isBotMessage && !attachmentContent?.text ? (params.message.attachments ?? []) - .map((attachment) => attachment.text?.trim() || attachment.fallback?.trim()) + .map( + (attachment) => + normalizeOptionalString(attachment.text) ?? + normalizeOptionalString(attachment.fallback), + ) .filter(Boolean) .join("\n") : undefined; const rawBody = [ - (params.message.text ?? "").trim(), + normalizeOptionalString(params.message.text), attachmentContent?.text, botAttachmentText, mediaPlaceholder, diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 9b6157a0f7e..d1a6e917d39 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -65,7 +65,7 @@ function resolveCachedMentionRegexes( ctx: SlackMonitorContext, agentId: string | undefined, ): RegExp[] { - const key = agentId?.trim() || "__default__"; + const key = normalizeOptionalString(agentId) ?? "__default__"; let byAgent = mentionRegexCache.get(ctx); if (!byAgent) { byAgent = new Map(); diff --git a/extensions/slack/src/monitor/room-context.ts b/extensions/slack/src/monitor/room-context.ts index 4f9ec651a53..f634ad6832a 100644 --- a/extensions/slack/src/monitor/room-context.ts +++ b/extensions/slack/src/monitor/room-context.ts @@ -1,4 +1,5 @@ import { buildUntrustedChannelMetadata } from "openclaw/plugin-sdk/security-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; export function resolveSlackRoomContextHints(params: { isRoomish: boolean; @@ -17,7 +18,7 @@ export function resolveSlackRoomContextHints(params: { : undefined; const systemPromptParts = [ - params.isRoomish ? params.channelConfig?.systemPrompt?.trim() || null : null, + params.isRoomish ? (normalizeOptionalString(params.channelConfig?.systemPrompt) ?? null) : null, ].filter((entry): entry is string => Boolean(entry)); const groupSystemPrompt = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined; diff --git a/extensions/slack/src/send.ts b/extensions/slack/src/send.ts index f9bb3660f12..822a251677a 100644 --- a/extensions/slack/src/send.ts +++ b/extensions/slack/src/send.ts @@ -11,7 +11,10 @@ import { import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { SlackTokenSource } from "./accounts.js"; import { resolveSlackAccount } from "./accounts.js"; import { buildSlackBlocksFallbackText } from "./blocks-fallback.js"; @@ -308,7 +311,7 @@ export async function sendMessageSlack( message: string, opts: SlackSendOpts = {}, ): Promise { - const trimmedMessage = message?.trim() ?? ""; + const trimmedMessage = normalizeOptionalString(message) ?? ""; if (isSilentReplyText(trimmedMessage) && !opts.mediaUrl && !opts.blocks) { logVerbose("slack send: suppressed NO_REPLY token before API call"); return { messageId: "suppressed", channelId: "" };