From 424b65b69716f14bd2280120fbdb243a909594ea Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 08:02:44 +0100 Subject: [PATCH] refactor: dedupe bluebubbles and zalouser readers --- extensions/bluebubbles/src/attachments.ts | 3 ++- extensions/bluebubbles/src/monitor-normalize.ts | 16 +++++++++++----- extensions/bluebubbles/src/monitor-processing.ts | 10 +++++----- .../bluebubbles/src/participant-contact-names.ts | 3 ++- .../src/cli/browser-cli-state.cookies-storage.ts | 2 +- extensions/llm-task/src/llm-task-tool.ts | 5 +++-- extensions/memory-wiki/src/markdown.ts | 2 +- extensions/zalouser/src/zalo-js.ts | 12 ++++++++---- scripts/debug-claude-usage.ts | 3 ++- scripts/lib/plugin-npm-release.ts | 3 ++- src/agents/chutes-oauth.ts | 7 ++++--- src/agents/pi-embedded-runner/run/payloads.ts | 2 +- .../doctor-service-audit.test-helpers.ts | 4 +++- 13 files changed, 45 insertions(+), 27 deletions(-) diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index e9466da98e7..15dd2457550 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import path from "node:path"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { assertMultipartActionOk, postMultipartFormData } from "./multipart.js"; import { @@ -161,7 +162,7 @@ export async function sendBlueBubblesAttachment(params: { const wantsVoice = asVoice === true; const fallbackName = wantsVoice ? "Audio Message" : "attachment"; filename = sanitizeFilename(filename, fallbackName); - contentType = contentType?.trim() || undefined; + contentType = normalizeOptionalString(contentType); const { baseUrl, password, accountId, allowPrivateNetwork } = resolveAccount(opts); const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId); const privateApiEnabled = isBlueBubblesPrivateApiStatusEnabled(privateApiStatus); diff --git a/extensions/bluebubbles/src/monitor-normalize.ts b/extensions/bluebubbles/src/monitor-normalize.ts index 84d4c505128..44fd57dfaea 100644 --- a/extensions/bluebubbles/src/monitor-normalize.ts +++ b/extensions/bluebubbles/src/monitor-normalize.ts @@ -1,5 +1,9 @@ import { parseFiniteNumber } from "openclaw/plugin-sdk/infra-runtime"; -import { asNullableRecord, readStringField } from "openclaw/plugin-sdk/text-runtime"; +import { + asNullableRecord, + normalizeOptionalString, + readStringField, +} from "openclaw/plugin-sdk/text-runtime"; import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js"; import type { BlueBubblesAttachment } from "./types.js"; @@ -164,8 +168,8 @@ function extractReplyMetadata(message: Record): { : undefined; return { - replyToId: (replyToId ?? fallbackReplyId)?.trim() || undefined, - replyToBody: replyToBody?.trim() || undefined, + replyToId: normalizeOptionalString(replyToId ?? fallbackReplyId), + replyToBody: normalizeOptionalString(replyToBody), replyToSender: normalizedSender || undefined, }; } @@ -336,7 +340,7 @@ function normalizeParticipantEntry(entry: unknown): BlueBubblesParticipant | nul if (!normalizedId) { return null; } - const name = nameRaw?.trim() || undefined; + const name = normalizeOptionalString(nameRaw); return { id: normalizedId, name }; } @@ -563,7 +567,9 @@ export function resolveTapbackContext(message: NormalizedWebhookMessage): { if (!hasTapbackType && !hasTapbackMarker) { return null; } - const replyToId = message.associatedMessageGuid?.trim() || message.replyToId?.trim() || undefined; + const replyToId = + normalizeOptionalString(message.associatedMessageGuid) ?? + normalizeOptionalString(message.replyToId); const actionHint = resolveTapbackActionHint(associatedType); const emojiHint = message.associatedMessageEmoji?.trim() || REACTION_TYPE_MAP.get(associatedType ?? -1)?.emoji; diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 2f99d5eb618..a406072072f 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -4,6 +4,7 @@ import { sendMediaWithLeadingCaption, } from "openclaw/plugin-sdk/reply-payload"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { downloadBlueBubblesAttachment } from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { resolveBlueBubblesConversationRoute } from "./conversation-route.js"; @@ -92,8 +93,7 @@ const pendingOutboundMessageIds: PendingOutboundMessageId[] = []; let pendingOutboundMessageIdCounter = 0; function trimOrUndefined(value?: string | null): string | undefined { - const trimmed = value?.trim(); - return trimmed ? trimmed : undefined; + return normalizeOptionalString(value); } function normalizeSnippet(value: string): string { @@ -732,7 +732,7 @@ export async function processMessage( chatId: message.chatId ?? undefined, chatIdentifier: message.chatIdentifier ?? undefined, }); - const groupName = message.chatName?.trim() || undefined; + const groupName = normalizeOptionalString(message.chatName); if (accessDecision.decision !== "allow") { if (isGroup) { @@ -1105,11 +1105,11 @@ export async function processMessage( // The sender identity is included in the envelope body via formatInboundEnvelope. const senderLabel = message.senderName || `user:${message.senderId}`; const fromLabel = isGroup - ? `${message.chatName?.trim() || "Group"} id:${peerId}` + ? `${normalizeOptionalString(message.chatName) || "Group"} id:${peerId}` : senderLabel !== message.senderId ? `${senderLabel} id:${message.senderId}` : senderLabel; - const groupSubject = isGroup ? message.chatName?.trim() || undefined : undefined; + const groupSubject = isGroup ? normalizeOptionalString(message.chatName) : undefined; const groupMembers = isGroup ? formatGroupMembers({ participants: message.participants, diff --git a/extensions/bluebubbles/src/participant-contact-names.ts b/extensions/bluebubbles/src/participant-contact-names.ts index 1218801a0be..29b41e34783 100644 --- a/extensions/bluebubbles/src/participant-contact-names.ts +++ b/extensions/bluebubbles/src/participant-contact-names.ts @@ -2,6 +2,7 @@ import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_pro import { access, readdir } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import type { BlueBubblesParticipant } from "./monitor-normalize.js"; const execFileAsync = promisify(execFile) as ExecFileRunner; @@ -313,7 +314,7 @@ export async function enrichBlueBubblesParticipantsWithContactNames( try { const resolved = await lookup([...pendingPhoneKeys]); for (const phoneKey of pendingPhoneKeys) { - const name = resolved.get(phoneKey)?.trim() || undefined; + const name = normalizeOptionalString(resolved.get(phoneKey)); writeCacheEntry(phoneKey, name, nowMs); if (name) { cachedNames.set(phoneKey, name); diff --git a/extensions/browser/src/cli/browser-cli-state.cookies-storage.ts b/extensions/browser/src/cli/browser-cli-state.cookies-storage.ts index 38a65d3e411..87289524592 100644 --- a/extensions/browser/src/cli/browser-cli-state.cookies-storage.ts +++ b/extensions/browser/src/cli/browser-cli-state.cookies-storage.ts @@ -146,7 +146,7 @@ export function registerBrowserCookiesAndStorageCommands( method: "GET", path: `/storage/${kind}`, query: { - key: key?.trim() || undefined, + key: normalizeOptionalString(key), targetId, profile, }, diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 1c326e2559f..eb3a2e283d0 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { Type } from "@sinclair/typebox"; import Ajv from "ajv"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { formatXHighModelHint, normalizeThinkLevel, @@ -96,8 +97,8 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { const defaultsModel = api.config?.agents?.defaults?.model; const primary = typeof defaultsModel === "string" - ? defaultsModel.trim() - : (defaultsModel?.primary?.trim() ?? undefined); + ? normalizeOptionalString(defaultsModel) + : normalizeOptionalString(defaultsModel?.primary); const primaryProvider = typeof primary === "string" ? primary.split("/")[0] : undefined; const primaryModel = typeof primary === "string" ? primary.split("/").slice(1).join("/") : undefined; diff --git a/extensions/memory-wiki/src/markdown.ts b/extensions/memory-wiki/src/markdown.ts index aeb32d2cc13..583851d31e9 100644 --- a/extensions/memory-wiki/src/markdown.ts +++ b/extensions/memory-wiki/src/markdown.ts @@ -81,7 +81,7 @@ export function renderWikiMarkdown(params: { export function extractTitleFromMarkdown(body: string): string | undefined { const match = body.match(/^#\s+(.+?)\s*$/m); - return match?.[1]?.trim() || undefined; + return normalizeOptionalString(match?.[1]); } export function normalizeSourceIds(value: unknown): string[] { diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index 01639a4416f..93fac667340 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media"; import { resolveStateDir as resolvePluginStateDir } from "openclaw/plugin-sdk/state-paths"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { normalizeZaloReactionIcon } from "./reaction.js"; import type { ZaloAuthStatus, @@ -500,7 +501,7 @@ function resolveUploadedVoiceAsset( continue; } if (fileType === "others" || fileType === "video") { - return { fileUrl, fileName: item.fileName?.trim() || undefined }; + return { fileUrl, fileName: normalizeOptionalString(item.fileName) }; } } return undefined; @@ -963,7 +964,8 @@ export async function listZaloGroupMembers( continue; } currentById.set(id, { - displayName: member.dName?.trim() || member.zaloName?.trim() || undefined, + displayName: + normalizeOptionalString(member.dName) ?? normalizeOptionalString(member.zaloName), avatar: member.avatar || undefined, }); } @@ -990,7 +992,9 @@ export async function listZaloGroupMembers( continue; } profileMap.set(id, { - displayName: profileValue.displayName?.trim() || profileValue.zaloName?.trim() || undefined, + displayName: + normalizeOptionalString(profileValue.displayName) ?? + normalizeOptionalString(profileValue.zaloName), avatar: profileValue.avatar || undefined, }); } @@ -1024,7 +1028,7 @@ export async function resolveZaloGroupContext( | undefined; const context: ZaloGroupContext = { groupId: normalizedGroupId, - name: groupInfo?.name?.trim() || undefined, + name: normalizeOptionalString(groupInfo?.name), members: extractGroupMembersFromInfo(groupInfo), }; writeCachedGroupContext(profile, context); diff --git a/scripts/debug-claude-usage.ts b/scripts/debug-claude-usage.ts index 443fa6beadb..01d56f7b07d 100644 --- a/scripts/debug-claude-usage.ts +++ b/scripts/debug-claude-usage.ts @@ -3,6 +3,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { normalizeOptionalString } from "../src/shared/string-coerce.ts"; type Args = { agentId: string; @@ -36,7 +37,7 @@ const parseArgs = (): Args => { continue; } if (arg === "--session-key" && args[i + 1]) { - sessionKey = String(args[++i]).trim() || undefined; + sessionKey = normalizeOptionalString(String(args[++i])); continue; } } diff --git a/scripts/lib/plugin-npm-release.ts b/scripts/lib/plugin-npm-release.ts index 4f646a40bae..442198171a1 100644 --- a/scripts/lib/plugin-npm-release.ts +++ b/scripts/lib/plugin-npm-release.ts @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; +import { normalizeOptionalString } from "../../src/shared/string-coerce.ts"; import { parseReleaseVersion } from "../openclaw-npm-release-check.ts"; import { resolveNpmPublishPlan } from "./npm-publish-plan.mjs"; @@ -270,7 +271,7 @@ export function collectPublishablePluginPackages( version, channel: parsedVersion.channel, publishTag: resolveNpmPublishPlan(version).publishTag, - installNpmSpec: packageJson.openclaw?.install?.npmSpec?.trim() || undefined, + installNpmSpec: normalizeOptionalString(packageJson.openclaw?.install?.npmSpec), }); } diff --git a/src/agents/chutes-oauth.ts b/src/agents/chutes-oauth.ts index 02adf10ce01..74dd5bdf17e 100644 --- a/src/agents/chutes-oauth.ts +++ b/src/agents/chutes-oauth.ts @@ -1,5 +1,6 @@ import { createHash, randomBytes } from "node:crypto"; import type { OAuthCredentials } from "@mariozechner/pi-ai"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai"; export const CHUTES_AUTHORIZE_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/authorize`; @@ -66,8 +67,8 @@ export function parseOAuthCallbackInput( } } - const code = url.searchParams.get("code")?.trim(); - const state = url.searchParams.get("state")?.trim(); + const code = normalizeOptionalString(url.searchParams.get("code")); + const state = normalizeOptionalString(url.searchParams.get("state")); if (!code) { return { error: "Missing 'code' parameter in URL" }; } @@ -181,7 +182,7 @@ export async function refreshChutesTokens(params: { if (!clientId) { throw new Error("Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth)."); } - const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined; + const clientSecret = normalizeOptionalString(process.env.CHUTES_CLIENT_SECRET); const body = new URLSearchParams({ grant_type: "refresh_token", diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 6a7d980fcb6..a025403aa6b 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -156,7 +156,7 @@ export function buildEmbeddedRunPayloads(params: { }) : undefined; const rawErrorMessage = lastAssistantErrored - ? params.lastAssistant?.errorMessage?.trim() || undefined + ? normalizeOptionalString(params.lastAssistant?.errorMessage) : undefined; const rawErrorFingerprint = rawErrorMessage ? getApiErrorPayloadFingerprint(rawErrorMessage) diff --git a/src/commands/doctor-service-audit.test-helpers.ts b/src/commands/doctor-service-audit.test-helpers.ts index f1e1b8fd6ed..d27003bc503 100644 --- a/src/commands/doctor-service-audit.test-helpers.ts +++ b/src/commands/doctor-service-audit.test-helpers.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export const testServiceAuditCodes = { gatewayEntrypointMismatch: "gateway-entrypoint-mismatch", gatewayTokenMismatch: "gateway-token-mismatch", @@ -11,5 +13,5 @@ export function readEmbeddedGatewayTokenForTest( ) { return command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file" ? undefined - : command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined; + : normalizeOptionalString(command?.environment?.OPENCLAW_GATEWAY_TOKEN); }