refactor: dedupe discord trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-07 23:25:44 +01:00
parent 96fe85fb77
commit c7347a492e
28 changed files with 144 additions and 106 deletions

View File

@@ -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.",

View File

@@ -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;
}

View File

@@ -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) ??

View File

@@ -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<DiscordChannelPermissionsAudit> {
const started = Date.now();
const token = params.token?.trim() ?? "";
const token = normalizeOptionalString(params.token) ?? "";
if (!token || params.channelIds.length === 0) {
return {
ok: true,

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<string, unknown> | 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;
}

View File

@@ -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;
}

View File

@@ -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];
}

View File

@@ -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;
}

View File

@@ -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<string>();
const names = new Set<string>();
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;
}

View File

@@ -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;
}

View File

@@ -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),

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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<PersistedThreadBindingRecord>;
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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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(
() => [],

View File

@@ -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<DiscordSendResult> {
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,

View File

@@ -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,