refactor: dedupe messaging trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:29:26 +01:00
parent e0b4f3b995
commit aec24f4599
38 changed files with 218 additions and 126 deletions

View File

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

View File

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

View File

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

View File

@@ -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<Response>;
@@ -552,7 +555,7 @@ export async function uploadMattermostFile(
},
): Promise<MattermostFileInfo> {
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 })

View File

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

View File

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

View File

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

View File

@@ -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", () => ({

View File

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

View File

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

View File

@@ -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<string, OnMessageSentCallback>();
/** 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<string, Promise<string>>();
* Resolve an access token with caching and singleflight semantics.
*/
export async function getAccessToken(appId: string, clientSecret: string): Promise<string> {
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<string
/** Clear one token cache or all token caches. */
export function clearTokenCache(appId?: string): void {
if (appId) {
const normalizedAppId = String(appId).trim();
const normalizedAppId = normalizeOptionalString(appId) ?? "";
tokenCacheMap.delete(normalizedAppId);
debugLog(`[qqbot-api:${normalizedAppId}] Token cache cleared manually.`);
} else {
@@ -347,7 +348,7 @@ async function sendAndNotify(
meta: OutboundMeta,
): Promise<MessageResponse> {
const result = await apiRequest<MessageResponse>(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);

View File

@@ -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<string | number> | 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());
}

View File

@@ -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("//")

View File

@@ -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<string, QueueItem["type"]> = {

View File

@@ -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<OutboundResult> {
const tagName = normalizeLowercaseStringOrEmpty(match[1]);
let mediaPath = match[2]?.trim() ?? "";
let mediaPath = normalizeOptionalString(match[2]) ?? "";
if (mediaPath.startsWith("MEDIA:")) {
mediaPath = mediaPath.slice("MEDIA:".length);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, HistoryEntry[]>();
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(

View File

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

View File

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

View File

@@ -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<typeof resolveSignalAccount>,
) {
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 };
}

View File

@@ -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<ResolvedSign
resolveAllowFrom: (account: ResolvedSignalAccount) => 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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ResolvedSlackAccount, SlackProbe> = 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<ResolvedSlackAccount, SlackProbe> = 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 }) =>

View File

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

View File

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

View File

@@ -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<SlackSlashCommandConfig> {
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,
};
}

View File

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

View File

@@ -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(":");
}

View File

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

View File

@@ -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<string, RegExp[]>();

View File

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

View File

@@ -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<SlackSendResult> {
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: "" };