From c7347a492ee26a1b933d2fb07e308dfc8f8a079a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 23:25:44 +0100 Subject: [PATCH] refactor: dedupe discord trimmed readers --- .../discord/src/actions/handle-action.ts | 3 ++- .../discord/src/approval-handler.runtime.ts | 4 +-- extensions/discord/src/approval-native.ts | 11 +++++--- extensions/discord/src/audit-core.ts | 6 ++--- extensions/discord/src/channel-actions.ts | 5 ++-- extensions/discord/src/channel.ts | 13 +++++++--- extensions/discord/src/directory-cache.ts | 12 ++++++--- extensions/discord/src/doctor-contract.ts | 5 ++-- extensions/discord/src/doctor.ts | 3 ++- extensions/discord/src/group-policy.ts | 3 ++- extensions/discord/src/mentions.ts | 10 +++++--- extensions/discord/src/monitor/allow-list.ts | 9 ++++--- .../src/monitor/message-handler.preflight.ts | 14 +++++------ .../discord/src/monitor/message-utils.ts | 24 ++++++++---------- .../src/monitor/model-picker-preferences.ts | 3 ++- .../discord/src/monitor/native-command-ui.ts | 4 ++- .../discord/src/monitor/native-command.ts | 9 ++++--- extensions/discord/src/monitor/presence.ts | 7 +++--- .../discord/src/monitor/provider.allowlist.ts | 2 +- .../monitor/thread-bindings.discord-api.ts | 9 ++++--- .../src/monitor/thread-bindings.lifecycle.ts | 7 ++++-- .../src/monitor/thread-bindings.manager.ts | 15 ++++++----- .../src/monitor/thread-bindings.state.ts | 25 ++++++++----------- extensions/discord/src/monitor/threading.ts | 14 +++++++---- extensions/discord/src/outbound-adapter.ts | 9 ++++--- extensions/discord/src/security-audit.ts | 6 +++-- extensions/discord/src/send.outbound.ts | 11 +++----- extensions/discord/src/subagent-hooks.ts | 7 ++++-- 28 files changed, 144 insertions(+), 106 deletions(-) diff --git a/extensions/discord/src/actions/handle-action.ts b/extensions/discord/src/actions/handle-action.ts index f98ac0c9ecc..e816c8744c6 100644 --- a/extensions/discord/src/actions/handle-action.ts +++ b/extensions/discord/src/actions/handle-action.ts @@ -8,6 +8,7 @@ import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param"; import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions"; import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-contract"; import { normalizeInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime"; +import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/text-runtime"; import { handleDiscordAction } from "../../action-runtime-api.js"; import { buildDiscordInteractiveComponents } from "../shared-interactive.js"; import { resolveDiscordChannelId } from "../targets.js"; @@ -119,7 +120,7 @@ export async function handleDiscordMessageAction( if (action === "react") { const messageIdRaw = resolveReactionMessageId({ args: params, toolContext: ctx.toolContext }); - const messageId = messageIdRaw != null ? String(messageIdRaw).trim() : ""; + const messageId = normalizeOptionalStringifiedId(messageIdRaw) ?? ""; if (!messageId) { throw new Error( "messageId required. Provide messageId explicitly or react to the current inbound message.", diff --git a/extensions/discord/src/approval-handler.runtime.ts b/extensions/discord/src/approval-handler.runtime.ts index ab3a710b3e8..d5b92bbbb28 100644 --- a/extensions/discord/src/approval-handler.runtime.ts +++ b/extensions/discord/src/approval-handler.runtime.ts @@ -24,7 +24,7 @@ import type { ExecApprovalActionDescriptor, ExecApprovalDecision, } from "openclaw/plugin-sdk/infra-runtime"; -import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime"; +import { logDebug, logError, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { shouldHandleDiscordApprovalRequest } from "./approval-native.js"; import { isDiscordExecApprovalClientEnabled } from "./exec-approvals.js"; import { createDiscordClient, stripUndefinedFields } from "./send.shared.js"; @@ -52,7 +52,7 @@ function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): context: DiscordApprovalHandlerContext; } | null { const context = params.context as DiscordApprovalHandlerContext | undefined; - const accountId = params.accountId?.trim() || ""; + const accountId = normalizeOptionalString(params.accountId) ?? ""; if (!context?.token || !accountId) { return null; } diff --git a/extensions/discord/src/approval-native.ts b/extensions/discord/src/approval-native.ts index 9e3e0da48c1..b0c3ede490c 100644 --- a/extensions/discord/src/approval-native.ts +++ b/extensions/discord/src/approval-native.ts @@ -3,7 +3,10 @@ import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/ap import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime"; import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, PluginApprovalRequest } from "openclaw/plugin-sdk/infra-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js"; import { createChannelApproverDmTargetResolver, @@ -131,9 +134,11 @@ function createDiscordOriginTargetResolver(configOverride?: DiscordExecApprovalC request, channel: "discord", }); - const sessionKind = extractDiscordSessionKind(request.request.sessionKey?.trim() || null); + const sessionKind = extractDiscordSessionKind( + normalizeOptionalString(request.request.sessionKey) ?? null, + ); const turnSourceChannel = normalizeLowercaseStringOrEmpty(request.request.turnSourceChannel); - const rawTurnSourceTo = request.request.turnSourceTo?.trim() || ""; + const rawTurnSourceTo = normalizeOptionalString(request.request.turnSourceTo) ?? ""; const turnSourceTo = normalizeDiscordOriginChannelId(rawTurnSourceTo); const threadId = normalizeDiscordThreadId(request.request.turnSourceThreadId) ?? diff --git a/extensions/discord/src/audit-core.ts b/extensions/discord/src/audit-core.ts index 49951747cac..003dd0128cc 100644 --- a/extensions/discord/src/audit-core.ts +++ b/extensions/discord/src/audit-core.ts @@ -3,7 +3,7 @@ import type { DiscordGuildEntry, } from "openclaw/plugin-sdk/config-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { isRecord } from "openclaw/plugin-sdk/text-runtime"; +import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; export type DiscordChannelPermissionsAuditEntry = { channelId: string; @@ -50,7 +50,7 @@ export function listConfiguredGuildChannelKeys( continue; } for (const [key, value] of Object.entries(channelsRaw)) { - const channelId = String(key).trim(); + const channelId = normalizeOptionalString(String(key)) ?? ""; if (!channelId) { continue; } @@ -88,7 +88,7 @@ export async function auditDiscordChannelPermissionsWithFetcher(params: { }>; }): Promise { const started = Date.now(); - const token = params.token?.trim() ?? ""; + const token = normalizeOptionalString(params.token) ?? ""; if (!token || params.channelIds.length === 0) { return { ok: true, diff --git a/extensions/discord/src/channel-actions.ts b/extensions/discord/src/channel-actions.ts index 290da038ae0..93879ddb045 100644 --- a/extensions/discord/src/channel-actions.ts +++ b/extensions/discord/src/channel-actions.ts @@ -9,6 +9,7 @@ import type { ChannelMessageToolDiscovery, } from "openclaw/plugin-sdk/channel-contract"; import type { DiscordActionConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { extractToolSend } from "openclaw/plugin-sdk/tool-send"; import { createDiscordActionGate, @@ -168,12 +169,12 @@ function describeDiscordMessageTool({ export const discordMessageActions: ChannelMessageActionAdapter = { describeMessageTool: describeDiscordMessageTool, extractToolSend: ({ args }) => { - const action = typeof args.action === "string" ? args.action.trim() : ""; + const action = normalizeOptionalString(args.action) ?? ""; if (action === "sendMessage") { return extractToolSend(args, "sendMessage"); } if (action === "threadReply") { - const channelId = typeof args.channelId === "string" ? args.channelId.trim() : ""; + const channelId = normalizeOptionalString(args.channelId) ?? ""; return channelId ? { to: `channel:${channelId}` } : null; } return null; diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 21772aa252e..241d6c28a68 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -25,7 +25,11 @@ import { createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers"; import { resolveTargetsWithOptionalToken } from "openclaw/plugin-sdk/target-resolver-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; import { listDiscordAccountIds, resolveDiscordAccount, @@ -137,7 +141,7 @@ function resolveDiscordAttachedOutboundTarget(params: { if (params.threadId == null) { return params.to; } - const threadId = String(params.threadId).trim(); + const threadId = normalizeOptionalStringifiedId(params.threadId) ?? ""; return threadId ? `channel:${threadId}` : params.to; } @@ -206,7 +210,8 @@ function resolveDiscordStartupDelayMs(cfg: OpenClawConfig, accountId: string): n const candidate = resolveDiscordAccount({ cfg, accountId: candidateId }); return ( candidate.enabled && - (resolveConfiguredFromCredentialStatuses(candidate) ?? Boolean(candidate.token.trim())) + (resolveConfiguredFromCredentialStatuses(candidate) ?? + Boolean(normalizeOptionalString(candidate.token))) ); }); const startupIndex = startupAccountIds.findIndex((candidateId) => candidateId === accountId); @@ -367,7 +372,7 @@ function resolveDiscordCommandConversation(params: { const targets = [params.originatingTo, params.commandTo, params.fallbackTo]; if (params.threadId) { const parentConversationId = - normalizeDiscordMessagingTarget(params.threadParentId?.trim() ?? "") || + normalizeDiscordMessagingTarget(normalizeOptionalString(params.threadParentId) ?? "") || parseDiscordParentChannelFromSessionKey(params.parentSessionKey) || resolveDiscordConversationIdFromTargets(targets); return { diff --git a/extensions/discord/src/directory-cache.ts b/extensions/discord/src/directory-cache.ts index 571dcec95c7..40fa523f915 100644 --- a/extensions/discord/src/directory-cache.ts +++ b/extensions/discord/src/directory-cache.ts @@ -1,5 +1,9 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; const DISCORD_DIRECTORY_CACHE_MAX_ENTRIES = 4000; const DISCORD_DISCRIMINATOR_SUFFIX = /#\d{4}$/; @@ -12,7 +16,7 @@ function normalizeAccountCacheKey(accountId?: string | null): string { } function normalizeSnowflake(value: string | number | bigint): string | null { - const text = String(value ?? "").trim(); + const text = normalizeOptionalStringifiedId(value) ?? ""; if (!/^\d+$/.test(text)) { return null; } @@ -20,12 +24,12 @@ function normalizeSnowflake(value: string | number | bigint): string | null { } function normalizeHandleKey(raw: string): string | null { - let handle = raw.trim(); + let handle = normalizeOptionalString(raw) ?? ""; if (!handle) { return null; } if (handle.startsWith("@")) { - handle = handle.slice(1).trim(); + handle = normalizeOptionalString(handle.slice(1)) ?? ""; } if (!handle || /\s/.test(handle)) { return null; diff --git a/extensions/discord/src/doctor-contract.ts b/extensions/discord/src/doctor-contract.ts index 557299802b5..c9a6699cc5e 100644 --- a/extensions/discord/src/doctor-contract.ts +++ b/extensions/discord/src/doctor-contract.ts @@ -3,6 +3,7 @@ import type { ChannelDoctorLegacyConfigRule, } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeStringEntries } from "openclaw/plugin-sdk/text-runtime"; import { resolveDiscordPreviewStreamMode } from "./preview-streaming.js"; function asObjectRecord(value: unknown): Record | null { @@ -23,8 +24,8 @@ function allowFromListsMatch(left: unknown, right: unknown): boolean { if (!Array.isArray(left) || !Array.isArray(right)) { return false; } - const normalizedLeft = left.map((value) => String(value).trim()).filter(Boolean); - const normalizedRight = right.map((value) => String(value).trim()).filter(Boolean); + const normalizedLeft = normalizeStringEntries(left); + const normalizedRight = normalizeStringEntries(right); if (normalizedLeft.length !== normalizedRight.length) { return false; } diff --git a/extensions/discord/src/doctor.ts b/extensions/discord/src/doctor.ts index f0b3ee15c21..9be485b683d 100644 --- a/extensions/discord/src/doctor.ts +++ b/extensions/discord/src/doctor.ts @@ -1,6 +1,7 @@ import { type ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract"; import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { collectProviderDangerousNameMatchingScopes } from "openclaw/plugin-sdk/runtime-doctor"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { normalizeCompatibilityConfig as normalizeDiscordCompatibilityConfig } from "./doctor-contract.js"; import { DISCORD_LEGACY_CONFIG_RULES } from "./doctor-shared.js"; import { isDiscordMutableAllowEntry } from "./security-doctor.js"; @@ -241,7 +242,7 @@ function collectDiscordMutableAllowlistWarnings(cfg: OpenClawConfig): string[] { return; } for (const entry of list) { - const text = String(entry).trim(); + const text = normalizeOptionalString(String(entry)) ?? ""; if (!text || text === "*" || !isDiscordMutableAllowEntry(text)) { continue; } diff --git a/extensions/discord/src/group-policy.ts b/extensions/discord/src/group-policy.ts index 4357c95ef38..ecba7a22f94 100644 --- a/extensions/discord/src/group-policy.ts +++ b/extensions/discord/src/group-policy.ts @@ -5,6 +5,7 @@ import { type GroupToolPolicyConfig, } from "openclaw/plugin-sdk/channel-policy"; import { normalizeAtHashSlug } from "openclaw/plugin-sdk/string-normalization-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { DiscordConfig } from "./runtime-api.js"; function normalizeDiscordSlug(value?: string | null) { @@ -21,7 +22,7 @@ function resolveDiscordGuildEntry(guilds: DiscordConfig["guilds"], groupSpace?: if (!guilds || Object.keys(guilds).length === 0) { return null; } - const space = groupSpace?.trim() ?? ""; + const space = normalizeOptionalString(groupSpace) ?? ""; if (space && guilds[space]) { return guilds[space]; } diff --git a/extensions/discord/src/mentions.ts b/extensions/discord/src/mentions.ts index 28a8abf3b0e..81c8345b92a 100644 --- a/extensions/discord/src/mentions.ts +++ b/extensions/discord/src/mentions.ts @@ -1,4 +1,8 @@ -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveDiscordDirectoryUserId } from "./directory-cache.js"; const MARKDOWN_CODE_SEGMENT_PATTERN = /```[\s\S]*?```|`[^`\n]*`/g; @@ -6,7 +10,7 @@ const MENTION_CANDIDATE_PATTERN = /(^|[\s([{"'.,;:!?])@([a-z0-9_.-]{2,32}(?:#[0- const DISCORD_RESERVED_MENTIONS = new Set(["everyone", "here"]); function normalizeSnowflake(value: string | number | bigint): string | null { - const text = String(value ?? "").trim(); + const text = normalizeOptionalStringifiedId(value) ?? ""; if (!/^\d+$/.test(text)) { return null; } @@ -44,7 +48,7 @@ function rewritePlainTextMentions(text: string, accountId?: string | null): stri return text; } return text.replace(MENTION_CANDIDATE_PATTERN, (match, prefix, rawHandle) => { - const handle = String(rawHandle ?? "").trim(); + const handle = normalizeOptionalString(rawHandle) ?? ""; if (!handle) { return match; } diff --git a/extensions/discord/src/monitor/allow-list.ts b/extensions/discord/src/monitor/allow-list.ts index f62ac253e58..8a955e74337 100644 --- a/extensions/discord/src/monitor/allow-list.ts +++ b/extensions/discord/src/monitor/allow-list.ts @@ -7,7 +7,10 @@ import { type ChannelMatchSource, } from "openclaw/plugin-sdk/channel-targets"; import { evaluateGroupRouteAccessForPolicy } from "openclaw/plugin-sdk/group-access"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { formatDiscordUserTag } from "./format.js"; export type DiscordAllowList = { @@ -57,9 +60,9 @@ export function normalizeDiscordAllowList(raw: string[] | undefined, prefixes: s } const ids = new Set(); const names = new Set(); - const allowAll = raw.some((entry) => String(entry).trim() === "*"); + const allowAll = raw.some((entry) => (normalizeOptionalString(String(entry)) ?? "") === "*"); for (const entry of raw) { - const text = String(entry).trim(); + const text = normalizeOptionalString(String(entry)) ?? ""; if (!text || text === "*") { continue; } diff --git a/extensions/discord/src/monitor/message-handler.preflight.ts b/extensions/discord/src/monitor/message-handler.preflight.ts index d6a4e97ad53..9baf71cd575 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.ts @@ -19,7 +19,7 @@ import { type HistoryEntry, } from "openclaw/plugin-sdk/reply-history"; import { getChildLogger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { logDebug } from "openclaw/plugin-sdk/text-runtime"; +import { logDebug, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveDefaultDiscordAccountId } from "../accounts.js"; import { resolveDiscordConversationIdentity } from "../conversation-identity.js"; import { @@ -234,18 +234,16 @@ export function shouldIgnoreBoundThreadWebhookMessage(params: { webhookId?: string | null; threadBinding?: BoundThreadLookupRecordLike; }): boolean { - const webhookId = params.webhookId?.trim() || ""; + const webhookId = normalizeOptionalString(params.webhookId) ?? ""; if (!webhookId) { return false; } const boundWebhookId = - typeof params.threadBinding?.webhookId === "string" - ? params.threadBinding.webhookId.trim() - : typeof params.threadBinding?.metadata?.webhookId === "string" - ? params.threadBinding.metadata.webhookId.trim() - : ""; + normalizeOptionalString(params.threadBinding?.webhookId) ?? + normalizeOptionalString(params.threadBinding?.metadata?.webhookId) ?? + ""; if (!boundWebhookId) { - const threadId = params.threadId?.trim() || ""; + const threadId = normalizeOptionalString(params.threadId) ?? ""; if (!threadId) { return false; } diff --git a/extensions/discord/src/monitor/message-utils.ts b/extensions/discord/src/monitor/message-utils.ts index bc4b589dbc4..f49e4f12064 100644 --- a/extensions/discord/src/monitor/message-utils.ts +++ b/extensions/discord/src/monitor/message-utils.ts @@ -5,7 +5,11 @@ import { saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime"; import { buildMediaPayload } from "openclaw/plugin-sdk/reply-payload"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; import { mergeAbortSignals } from "./timeouts.js"; const DISCORD_CDN_HOSTNAMES = [ @@ -118,13 +122,7 @@ export function __resetDiscordChannelInfoCacheForTest() { } function normalizeDiscordChannelId(value: unknown): string { - if (typeof value === "string") { - return value.trim(); - } - if (typeof value === "number" || typeof value === "bigint") { - return String(value).trim(); - } - return ""; + return normalizeOptionalStringifiedId(value) ?? ""; } export function resolveDiscordMessageChannelId(params: { @@ -608,8 +606,8 @@ function buildDiscordMediaPlaceholder(params: { export function resolveDiscordEmbedText( embed?: { title?: string | null; description?: string | null } | null, ): string { - const title = embed?.title?.trim() || ""; - const description = embed?.description?.trim() || ""; + const title = normalizeOptionalString(embed?.title) ?? ""; + const description = normalizeOptionalString(embed?.description) ?? ""; if (title && description) { return `${title}\n${description}`; } @@ -625,13 +623,13 @@ export function resolveDiscordMessageText( null, ); const rawText = - message.content?.trim() || + normalizeOptionalString(message.content) || buildDiscordMediaPlaceholder({ attachments: message.attachments ?? undefined, stickers: resolveDiscordMessageStickers(message), }) || embedText || - options?.fallbackText?.trim() || + normalizeOptionalString(options?.fallbackText) || ""; const baseText = resolveDiscordMentions(rawText, message); if (!options?.includeForwarded) { @@ -732,7 +730,7 @@ function resolveDiscordReferencedForwardMessage(message: Message): Message | nul } function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): string { - const content = snapshot.content?.trim() ?? ""; + const content = normalizeOptionalString(snapshot.content) ?? ""; const attachmentText = buildDiscordMediaPlaceholder({ attachments: snapshot.attachments ?? undefined, stickers: resolveDiscordSnapshotStickers(snapshot), diff --git a/extensions/discord/src/monitor/model-picker-preferences.ts b/extensions/discord/src/monitor/model-picker-preferences.ts index 9d7d647a5bf..95265495a03 100644 --- a/extensions/discord/src/monitor/model-picker-preferences.ts +++ b/extensions/discord/src/monitor/model-picker-preferences.ts @@ -5,6 +5,7 @@ import { withFileLock } from "openclaw/plugin-sdk/file-lock"; import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store"; import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; const MODEL_PICKER_PREFERENCES_LOCK_OPTIONS = { retries: { @@ -63,7 +64,7 @@ function resolvePreferencesStorePath(env: NodeJS.ProcessEnv = process.env): stri } function normalizeId(value?: string): string { - return value?.trim() ?? ""; + return normalizeOptionalString(value) ?? ""; } export function buildDiscordModelPickerPreferenceKey( diff --git a/extensions/discord/src/monitor/native-command-ui.ts b/extensions/discord/src/monitor/native-command-ui.ts index 1983ae16914..f3aa31ec51a 100644 --- a/extensions/discord/src/monitor/native-command-ui.ts +++ b/extensions/discord/src/monitor/native-command-ui.ts @@ -31,6 +31,7 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { chunkItems, normalizeLowercaseStringOrEmpty, + normalizeOptionalString, withTimeout, } from "openclaw/plugin-sdk/text-runtime"; import { resolveDiscordChannelInfo } from "./message-utils.js"; @@ -171,7 +172,8 @@ export function shouldOpenDiscordModelPickerFromCommand(params: { return null; } - const serializedArgs = serializeCommandArgs(params.command, params.commandArgs)?.trim() ?? ""; + const serializedArgs = + normalizeOptionalString(serializeCommandArgs(params.command, params.commandArgs)) ?? ""; if (context === "model") { const modelValue = resolveCommandArgStringValue(params.commandArgs, "model"); return !modelValue && !serializedArgs ? context : null; diff --git a/extensions/discord/src/monitor/native-command.ts b/extensions/discord/src/monitor/native-command.ts index dd392f0ec97..b576982feff 100644 --- a/extensions/discord/src/monitor/native-command.ts +++ b/extensions/discord/src/monitor/native-command.ts @@ -47,7 +47,10 @@ import { } from "openclaw/plugin-sdk/reply-payload"; import { createSubsystemLogger, logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/runtime-group-policy"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { loadWebMedia } from "openclaw/plugin-sdk/web-media"; import { resolveDiscordMaxLinesPerMessage } from "../accounts.js"; import { chunkDiscordTextWithMode } from "../chunk.js"; @@ -160,10 +163,10 @@ function resolveDiscordNativeCommandAllowlistAccess(params: { return { configured: false, allowed: false } as const; } // Check guild-level entries (e.g. "guild:123456") before user matching. - const guildId = params.guildId?.trim(); + const guildId = normalizeOptionalString(params.guildId); if (guildId) { for (const entry of rawAllowList) { - const text = String(entry).trim(); + const text = normalizeOptionalString(String(entry)) ?? ""; if (text.startsWith("guild:") && text.slice("guild:".length) === guildId) { return { configured: true, allowed: true } as const; } diff --git a/extensions/discord/src/monitor/presence.ts b/extensions/discord/src/monitor/presence.ts index cfe8125e50e..6dc97ff65c9 100644 --- a/extensions/discord/src/monitor/presence.ts +++ b/extensions/discord/src/monitor/presence.ts @@ -1,5 +1,6 @@ import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway"; import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; const DEFAULT_CUSTOM_ACTIVITY_TYPE = 4; const CUSTOM_STATUS_NAME = "Custom Status"; @@ -12,10 +13,10 @@ type DiscordPresenceConfig = Pick< export function resolveDiscordPresenceUpdate( config: DiscordPresenceConfig, ): UpdatePresenceData | null { - const activityText = typeof config.activity === "string" ? config.activity.trim() : ""; - const status = typeof config.status === "string" ? config.status.trim() : ""; + const activityText = normalizeOptionalString(config.activity) ?? ""; + const status = normalizeOptionalString(config.status) ?? ""; const activityType = config.activityType; - const activityUrl = typeof config.activityUrl === "string" ? config.activityUrl.trim() : ""; + const activityUrl = normalizeOptionalString(config.activityUrl) ?? ""; const hasActivity = Boolean(activityText); const hasStatus = Boolean(status); diff --git a/extensions/discord/src/monitor/provider.allowlist.ts b/extensions/discord/src/monitor/provider.allowlist.ts index ccfc83f092a..375885c5506 100644 --- a/extensions/discord/src/monitor/provider.allowlist.ts +++ b/extensions/discord/src/monitor/provider.allowlist.ts @@ -107,7 +107,7 @@ function toAllowlistEntries(value: unknown): string[] | undefined { if (!Array.isArray(value)) { return undefined; } - return value.map((entry) => String(entry).trim()).filter((entry) => Boolean(entry)); + return normalizeStringEntries(value); } function hasGuildEntries(value: GuildEntries): boolean { diff --git a/extensions/discord/src/monitor/thread-bindings.discord-api.ts b/extensions/discord/src/monitor/thread-bindings.discord-api.ts index d144bb22b72..0fff80bd663 100644 --- a/extensions/discord/src/monitor/thread-bindings.discord-api.ts +++ b/extensions/discord/src/monitor/thread-bindings.discord-api.ts @@ -1,6 +1,7 @@ import { ChannelType, Routes } from "discord-api-types/v10"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { createDiscordRestClient } from "../client.js"; import { sendMessageDiscord, sendWebhookMessageDiscord } from "../send.js"; import { createThreadDiscord } from "../send.messages.js"; @@ -177,8 +178,8 @@ export async function createWebhookForChannel(params: { name: "OpenClaw Agents", }, })) as { id?: string; token?: string }; - const webhookId = typeof created?.id === "string" ? created.id.trim() : ""; - const webhookToken = typeof created?.token === "string" ? created.token.trim() : ""; + const webhookId = normalizeOptionalString(created?.id) ?? ""; + const webhookToken = normalizeOptionalString(created?.token) ?? ""; if (!webhookId || !webhookToken) { return {}; } @@ -250,7 +251,7 @@ export async function resolveChannelIdForBinding(params: { parent_id?: string; parentId?: string; }; - const channelId = typeof channel?.id === "string" ? channel.id.trim() : ""; + const channelId = normalizeOptionalString(channel?.id) ?? ""; const type = channel?.type; const parentId = typeof channel?.parent_id === "string" @@ -292,7 +293,7 @@ export async function createThreadForBinding(params: { token: params.token, }, ); - const createdId = typeof created?.id === "string" ? created.id.trim() : ""; + const createdId = normalizeOptionalString(created?.id) ?? ""; return createdId || null; } catch (err) { logVerbose( diff --git a/extensions/discord/src/monitor/thread-bindings.lifecycle.ts b/extensions/discord/src/monitor/thread-bindings.lifecycle.ts index 8f66986c660..ddf903620a8 100644 --- a/extensions/discord/src/monitor/thread-bindings.lifecycle.ts +++ b/extensions/discord/src/monitor/thread-bindings.lifecycle.ts @@ -1,6 +1,9 @@ import { readAcpSessionEntry, type AcpSessionStoreEntry } from "openclaw/plugin-sdk/acp-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { parseDiscordTarget } from "../targets.js"; import { resolveChannelIdForBinding } from "./thread-bindings.discord-api.js"; import { getThreadBindingManager } from "./thread-bindings.manager.js"; @@ -132,7 +135,7 @@ export async function autoBindSpawnedDiscordSubagent(params: { } } if (!channelId) { - const to = params.to?.trim() || ""; + const to = normalizeOptionalString(params.to) ?? ""; if (!to) { return null; } diff --git a/extensions/discord/src/monitor/thread-bindings.manager.ts b/extensions/discord/src/monitor/thread-bindings.manager.ts index 7c42ba26049..553c929b5b8 100644 --- a/extensions/discord/src/monitor/thread-bindings.manager.ts +++ b/extensions/discord/src/monitor/thread-bindings.manager.ts @@ -378,7 +378,7 @@ export function createThreadBindingManager( bindTarget: async (bindParams) => { const cfg = resolveCurrentCfg(); let threadId = normalizeThreadId(bindParams.threadId); - let channelId = bindParams.channelId?.trim() || ""; + let channelId = normalizeOptionalString(bindParams.channelId) ?? ""; const directConversationBinding = isDirectConversationBindingId(threadId) || isDirectConversationBindingId(channelId); @@ -396,7 +396,7 @@ export function createThreadBindingManager( accountId, token: resolveCurrentToken(), channelId, - threadName: bindParams.threadName?.trim() || threadName, + threadName: normalizeOptionalString(bindParams.threadName) ?? threadName, })) ?? undefined; } @@ -422,14 +422,14 @@ export function createThreadBindingManager( return null; } - const targetSessionKey = bindParams.targetSessionKey.trim(); + const targetSessionKey = normalizeOptionalString(bindParams.targetSessionKey) ?? ""; if (!targetSessionKey) { return null; } const targetKind = normalizeTargetKind(bindParams.targetKind, targetSessionKey); - let webhookId = bindParams.webhookId?.trim() || ""; - let webhookToken = bindParams.webhookToken?.trim() || ""; + let webhookId = normalizeOptionalString(bindParams.webhookId) ?? ""; + let webhookToken = normalizeOptionalString(bindParams.webhookToken) ?? ""; if (!directConversationBinding && (!webhookId || !webhookToken)) { const cachedWebhook = findReusableWebhook({ accountId, channelId }); webhookId = cachedWebhook.webhookId ?? ""; @@ -594,11 +594,10 @@ export function createThreadBindingManager( if (!targetSessionKey) { return null; } - const conversationId = input.conversation.conversationId.trim(); + const conversationId = normalizeOptionalString(input.conversation.conversationId) ?? ""; const placement = input.placement === "child" ? "child" : "current"; const metadata = input.metadata ?? {}; - const label = - typeof metadata.label === "string" ? metadata.label.trim() || undefined : undefined; + const label = normalizeOptionalString(metadata.label); const threadName = typeof metadata.threadName === "string" ? normalizeOptionalString(metadata.threadName) diff --git a/extensions/discord/src/monitor/thread-bindings.state.ts b/extensions/discord/src/monitor/thread-bindings.state.ts index 31a563408ab..7f3e4a98070 100644 --- a/extensions/discord/src/monitor/thread-bindings.state.ts +++ b/extensions/discord/src/monitor/thread-bindings.state.ts @@ -5,6 +5,7 @@ import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugi import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; import { normalizeLowercaseStringOrEmpty, + normalizeOptionalString, normalizeOptionalStringifiedId, } from "openclaw/plugin-sdk/text-runtime"; import { @@ -143,26 +144,22 @@ function normalizePersistedBinding(threadIdKey: string, raw: unknown): ThreadBin } const value = raw as Partial; const threadId = normalizeThreadId(value.threadId ?? threadIdKey); - const channelId = typeof value.channelId === "string" ? value.channelId.trim() : ""; + const channelId = normalizeOptionalString(value.channelId) ?? ""; const targetSessionKey = - typeof value.targetSessionKey === "string" - ? value.targetSessionKey.trim() - : typeof value.sessionKey === "string" - ? value.sessionKey.trim() - : ""; + normalizeOptionalString(value.targetSessionKey) ?? + normalizeOptionalString(value.sessionKey) ?? + ""; if (!threadId || !channelId || !targetSessionKey) { return null; } const accountId = normalizeAccountId(value.accountId); const targetKind = normalizeTargetKind(value.targetKind, targetSessionKey); - const agentIdRaw = typeof value.agentId === "string" ? value.agentId.trim() : ""; + const agentIdRaw = normalizeOptionalString(value.agentId) ?? ""; const agentId = agentIdRaw || resolveAgentIdFromSessionKey(targetSessionKey); - const label = typeof value.label === "string" ? value.label.trim() || undefined : undefined; - const webhookId = - typeof value.webhookId === "string" ? value.webhookId.trim() || undefined : undefined; - const webhookToken = - typeof value.webhookToken === "string" ? value.webhookToken.trim() || undefined : undefined; - const boundBy = typeof value.boundBy === "string" ? value.boundBy.trim() || "system" : "system"; + const label = normalizeOptionalString(value.label); + const webhookId = normalizeOptionalString(value.webhookId); + const webhookToken = normalizeOptionalString(value.webhookToken); + const boundBy = normalizeOptionalString(value.boundBy) ?? "system"; const boundAt = typeof value.boundAt === "number" && Number.isFinite(value.boundAt) ? Math.floor(value.boundAt) @@ -395,7 +392,7 @@ export function isRecentlyUnboundThreadWebhookMessage(params: { threadId: string; webhookId?: string | null; }): boolean { - const webhookId = params.webhookId?.trim() || ""; + const webhookId = normalizeOptionalString(params.webhookId) ?? ""; if (!webhookId) { return false; } diff --git a/extensions/discord/src/monitor/threading.ts b/extensions/discord/src/monitor/threading.ts index bae0d9cc123..7436c254d83 100644 --- a/extensions/discord/src/monitor/threading.ts +++ b/extensions/discord/src/monitor/threading.ts @@ -8,7 +8,11 @@ import { import { createReplyReferencePlanner } from "openclaw/plugin-sdk/reply-reference"; import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; -import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalString, + normalizeOptionalStringifiedId, + truncateUtf16Safe, +} from "openclaw/plugin-sdk/text-runtime"; import type { DiscordChannelConfigResolved } from "./allow-list.js"; import type { DiscordMessageEvent } from "./listeners.js"; import { @@ -285,7 +289,7 @@ function buildDiscordThreadStarterPayload(params: { } function resolveDiscordThreadStarterText(starter: DiscordThreadStarterRestMessage): string { - const content = starter.content?.trim() ?? ""; + const content = normalizeOptionalString(starter.content) ?? ""; const embedText = resolveDiscordEmbedText(starter.embeds?.[0]); const forwardedText = resolveDiscordForwardedMessagesTextFromSnapshots(starter.message_snapshots); return content || embedText || forwardedText; @@ -341,7 +345,7 @@ export function resolveDiscordReplyTarget(opts: { if (opts.replyToMode === "off") { return undefined; } - const replyToId = opts.replyToId?.trim(); + const replyToId = normalizeOptionalString(opts.replyToId); if (!replyToId) { return undefined; } @@ -384,11 +388,11 @@ export function resolveDiscordAutoThreadContext(params: { messageChannelId: string; createdThreadId?: string | null; }): DiscordAutoThreadContext | null { - const createdThreadId = String(params.createdThreadId ?? "").trim(); + const createdThreadId = normalizeOptionalStringifiedId(params.createdThreadId) ?? ""; if (!createdThreadId) { return null; } - const messageChannelId = params.messageChannelId.trim(); + const messageChannelId = normalizeOptionalString(params.messageChannelId) ?? ""; if (!messageChannelId) { return null; } diff --git a/extensions/discord/src/outbound-adapter.ts b/extensions/discord/src/outbound-adapter.ts index 69eb812a3fa..dd51d481c37 100644 --- a/extensions/discord/src/outbound-adapter.ts +++ b/extensions/discord/src/outbound-adapter.ts @@ -13,7 +13,10 @@ import { sendPayloadMediaSequenceOrFallback, sendTextMediaPayload, } from "openclaw/plugin-sdk/reply-payload"; -import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; import type { DiscordComponentMessageSpec } from "./components.js"; import { getThreadBindingManager, type ThreadBindingRecord } from "./monitor/thread-bindings.js"; import { normalizeDiscordOutboundTarget } from "./normalize.js"; @@ -57,7 +60,7 @@ function resolveDiscordOutboundTarget(params: { if (params.threadId == null) { return params.to; } - const threadId = String(params.threadId).trim(); + const threadId = normalizeOptionalStringifiedId(params.threadId) ?? ""; if (!threadId) { return params.to; } @@ -86,7 +89,7 @@ async function maybeSendDiscordWebhookText(params: { if (params.threadId == null) { return null; } - const threadId = String(params.threadId).trim(); + const threadId = normalizeOptionalStringifiedId(params.threadId) ?? ""; if (!threadId) { return null; } diff --git a/extensions/discord/src/security-audit.ts b/extensions/discord/src/security-audit.ts index 498cbbec48a..1dc0a2b72b4 100644 --- a/extensions/discord/src/security-audit.ts +++ b/extensions/discord/src/security-audit.ts @@ -5,6 +5,7 @@ import { resolveNativeSkillsEnabled, } from "openclaw/plugin-sdk/config-runtime"; import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { ResolvedDiscordAccount } from "./accounts.js"; import type { OpenClawConfig } from "./runtime-api.js"; import { isDiscordMutableAllowEntry } from "./security-doctor.js"; @@ -21,7 +22,7 @@ function addDiscordNameBasedEntries(params: { if (!isDiscordMutableAllowEntry(String(value))) { continue; } - const text = String(value).trim(); + const text = normalizeOptionalString(String(value)) ?? ""; if (!text) { continue; } @@ -44,7 +45,8 @@ export async function collectDiscordSecurityAuditFindings(params: { remediation?: string; }> = []; const discordCfg = params.account.config ?? {}; - const accountId = params.accountId?.trim() || params.account.accountId || "default"; + const accountId = + normalizeOptionalString(params.accountId) ?? params.account.accountId ?? "default"; const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(discordCfg); const storeAllowFrom = await readChannelAllowFromStore("discord", process.env, accountId).catch( () => [], diff --git a/extensions/discord/src/send.outbound.ts b/extensions/discord/src/send.outbound.ts index bf89f7dede3..20eb4fad63c 100644 --- a/extensions/discord/src/send.outbound.ts +++ b/extensions/discord/src/send.outbound.ts @@ -105,10 +105,7 @@ const DISCORD_THREAD_NAME_LIMIT = 100; /** Derive a thread title from the first non-empty line of the message text. */ function deriveForumThreadName(text: string): string { const firstLine = - text - .split("\n") - .find((l) => l.trim()) - ?.trim() ?? ""; + normalizeOptionalString(text.split("\n").find((line) => normalizeOptionalString(line))) ?? ""; return firstLine.slice(0, DISCORD_THREAD_NAME_LIMIT) || new Date().toISOString().slice(0, 16); } @@ -361,13 +358,13 @@ export async function sendWebhookMessageDiscord( text: string, opts: DiscordWebhookSendOpts, ): Promise { - const webhookId = opts.webhookId.trim(); - const webhookToken = opts.webhookToken.trim(); + const webhookId = normalizeOptionalString(opts.webhookId) ?? ""; + const webhookToken = normalizeOptionalString(opts.webhookToken) ?? ""; if (!webhookId || !webhookToken) { throw new Error("Discord webhook id/token are required"); } - const replyTo = typeof opts.replyTo === "string" ? opts.replyTo.trim() : ""; + const replyTo = normalizeOptionalString(opts.replyTo) ?? ""; const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined; const { account, proxyFetch } = resolveDiscordClientAccountContext({ cfg: opts.cfg, diff --git a/extensions/discord/src/subagent-hooks.ts b/extensions/discord/src/subagent-hooks.ts index 22f06f28824..fdd8d3826f1 100644 --- a/extensions/discord/src/subagent-hooks.ts +++ b/extensions/discord/src/subagent-hooks.ts @@ -1,5 +1,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalStringifiedId, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveDiscordAccount } from "./accounts.js"; import { autoBindSpawnedDiscordSubagent, @@ -153,7 +156,7 @@ export function handleDiscordSubagentDeliveryTarget(event: DiscordSubagentDelive const requesterAccountId = event.requesterOrigin?.accountId?.trim(); const requesterThreadId = event.requesterOrigin?.threadId != null && event.requesterOrigin.threadId !== "" - ? String(event.requesterOrigin.threadId).trim() + ? (normalizeOptionalStringifiedId(event.requesterOrigin.threadId) ?? "") : ""; const bindings = listThreadBindingsBySessionKey({ targetSessionKey: event.childSessionKey,