refactor: dedupe bluebubbles readers

This commit is contained in:
Peter Steinberger
2026-04-07 06:48:20 +01:00
parent 41b1d3647c
commit 9fcef82f2d
7 changed files with 37 additions and 31 deletions

View File

@@ -5,6 +5,7 @@ import {
} from "openclaw/plugin-sdk/account-resolution";
import { resolveChannelStreamingChunkMode } from "openclaw/plugin-sdk/channel-streaming";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
@@ -58,7 +59,7 @@ export function resolveBlueBubblesAccount(params: {
return {
accountId,
enabled: baseEnabled !== false && accountEnabled,
name: merged.name?.trim() || undefined,
name: normalizeOptionalString(merged.name),
config: merged,
configured,
baseUrl,

View File

@@ -17,6 +17,7 @@ import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { type ResolvedBlueBubblesAccount } from "./accounts.js";
import { bluebubblesMessageActions } from "./actions.js";
import {
@@ -135,7 +136,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
looksLikeId: looksLikeBlueBubblesExplicitTargetId,
hint: "<handle|chat_guid:GUID|chat_id:ID|chat_identifier:ID>",
resolveTarget: async ({ normalized }) => {
const to = normalized?.trim();
const to = normalizeOptionalString(normalized);
if (!to) {
return null;
}
@@ -160,7 +161,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
// Helper to extract a clean handle from any BlueBubbles target format
const extractCleanDisplay = (value: string | undefined): string | null => {
const trimmed = value?.trim();
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
return null;
}
@@ -196,7 +197,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
};
// Try to get a clean display from the display parameter first
const trimmedDisplay = display?.trim();
const trimmedDisplay = normalizeOptionalString(display);
if (trimmedDisplay) {
if (!shouldParseDisplay(trimmedDisplay)) {
return trimmedDisplay;
@@ -214,7 +215,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
}
// Last resort: return display or target as-is
return display?.trim() || target?.trim() || "";
return normalizeOptionalString(display) || normalizeOptionalString(target) || "";
},
},
setup: blueBubblesSetupAdapter,
@@ -286,7 +287,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
},
threading: {
buildToolContext: ({ context, hasRepliedRef }) => ({
currentChannelId: context.To?.trim() || undefined,
currentChannelId: normalizeOptionalString(context.To),
currentThreadTs: context.ReplyToIdFull ?? context.ReplyToId,
hasRepliedRef,
}),
@@ -314,7 +315,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
deliveryMode: "direct",
textChunkLimit: 4000,
resolveTarget: ({ to }) => {
const trimmed = to?.trim();
const trimmed = normalizeOptionalString(to);
if (!trimmed) {
return {
ok: false,
@@ -328,7 +329,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
channel: "bluebubbles",
sendText: async ({ cfg, to, text, accountId, replyToId }) => {
const runtime = await loadBlueBubblesChannelRuntime();
const rawReplyToId = typeof replyToId === "string" ? replyToId.trim() : "";
const rawReplyToId = normalizeOptionalString(replyToId) ?? "";
const replyToMessageGuid = rawReplyToId
? runtime.resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
: "";

View File

@@ -1,4 +1,5 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { BaseProbeResult } from "./runtime-api.js";
import { normalizeSecretInputString } from "./secret-input.js";
import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js";
@@ -24,7 +25,7 @@ const serverInfoCache = new Map<string, { info: BlueBubblesServerInfo; expires:
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
function buildCacheKey(accountId?: string): string {
return accountId?.trim() || "default";
return normalizeOptionalString(accountId) || "default";
}
/**

View File

@@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
import { normalizeOptionalString, stripMarkdown } from "openclaw/plugin-sdk/text-runtime";
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
import {
getCachedBlueBubblesPrivateApiStatus,
@@ -137,8 +137,9 @@ function extractChatGuid(chat: BlueBubblesChatRecord): string | null {
chat.chat_identifier,
];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
const value = normalizeOptionalString(candidate);
if (value) {
return value;
}
}
return null;
@@ -159,8 +160,7 @@ function extractChatIdentifierFromChatGuid(chatGuid: string): string | null {
if (parts.length < 3) {
return null;
}
const identifier = parts[2]?.trim();
return identifier ? identifier : null;
return normalizeOptionalString(parts[2]) ?? null;
}
function extractParticipantAddresses(chat: BlueBubblesChatRecord): string[] {
@@ -479,7 +479,7 @@ export async function sendMessageBlueBubbles(
);
}
const effectId = resolveEffectId(opts.effectId);
const wantsReplyThread = Boolean(opts.replyToMessageGuid?.trim());
const wantsReplyThread = normalizeOptionalString(opts.replyToMessageGuid) !== undefined;
const wantsEffect = Boolean(effectId);
const privateApiDecision = resolvePrivateApiDecision({
privateApiStatus,

View File

@@ -8,6 +8,7 @@ import {
type ChannelSetupWizard,
type OpenClawConfig,
} from "openclaw/plugin-sdk/setup";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { resolveBlueBubblesAccount, resolveDefaultBlueBubblesAccountId } from "./accounts.js";
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
@@ -39,7 +40,7 @@ function validateBlueBubblesAllowFromEntry(value: string): string | null {
if (parsed.kind === "handle" && !parsed.handle) {
return null;
}
return value.trim() || null;
return normalizeOptionalString(value) ?? null;
} catch {
return null;
}
@@ -76,7 +77,7 @@ const promptBlueBubblesAllowFrom = createPromptParsedAllowFromForAccount({
});
function validateBlueBubblesServerUrlInput(value: unknown): string | undefined {
const trimmed = typeof value === "string" ? value.trim() : "";
const trimmed = normalizeOptionalString(value) ?? "";
if (!trimmed) {
return "Required";
}
@@ -108,11 +109,11 @@ function applyBlueBubblesSetupPatch(
}
function resolveBlueBubblesServerUrl(cfg: OpenClawConfig, accountId: string): string | undefined {
return resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl?.trim() || undefined;
return normalizeOptionalString(resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl);
}
function resolveBlueBubblesWebhookPath(cfg: OpenClawConfig, accountId: string): string | undefined {
return resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath?.trim() || undefined;
return normalizeOptionalString(resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath);
}
function validateBlueBubblesWebhookPath(value: string): string | undefined {

View File

@@ -6,6 +6,7 @@ import {
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
} from "openclaw/plugin-sdk/channel-targets";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
export type BlueBubblesService = "imessage" | "sms" | "auto";
@@ -29,7 +30,7 @@ const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4
const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i;
function parseRawChatGuid(value: string): string | null {
const trimmed = value.trim();
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
return null;
}
@@ -37,9 +38,9 @@ function parseRawChatGuid(value: string): string | null {
if (parts.length !== 3) {
return null;
}
const service = parts[0]?.trim();
const separator = parts[1]?.trim();
const identifier = parts[2]?.trim();
const service = normalizeOptionalString(parts[0]);
const separator = normalizeOptionalString(parts[1]);
const identifier = normalizeOptionalString(parts[2]);
if (!service || !identifier) {
return null;
}
@@ -54,7 +55,7 @@ function stripPrefix(value: string, prefix: string): string {
}
function stripBlueBubblesPrefix(value: string): string {
const trimmed = value.trim();
const trimmed = normalizeOptionalString(value) ?? "";
if (!trimmed) {
return "";
}
@@ -65,7 +66,7 @@ function stripBlueBubblesPrefix(value: string): string {
}
function looksLikeRawChatIdentifier(value: string): boolean {
const trimmed = value.trim();
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
return false;
}
@@ -139,7 +140,7 @@ export function extractHandleFromChatGuid(chatGuid: string): string | null {
const parts = chatGuid.split(";");
// DM format: service;-;handle (3 parts, middle is "-")
if (parts.length === 3 && parts[1] === "-") {
const handle = parts[2]?.trim();
const handle = normalizeOptionalString(parts[2]);
if (handle) {
return normalizeBlueBubblesHandle(handle);
}
@@ -222,7 +223,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string):
return true;
}
if (normalized) {
const normalizedTrimmed = normalized.trim();
const normalizedTrimmed = normalizeOptionalString(normalized);
if (!normalizedTrimmed) {
return false;
}
@@ -346,7 +347,7 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
}
export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget {
const trimmed = raw.trim();
const trimmed = normalizeOptionalString(raw) ?? "";
if (!trimmed) {
return { kind: "handle", handle: "" };
}
@@ -412,11 +413,11 @@ export function formatBlueBubblesChatTarget(params: {
if (params.chatId && Number.isFinite(params.chatId)) {
return `chat_id:${params.chatId}`;
}
const guid = params.chatGuid?.trim();
const guid = normalizeOptionalString(params.chatGuid);
if (guid) {
return `chat_guid:${guid}`;
}
const identifier = params.chatIdentifier?.trim();
const identifier = normalizeOptionalString(params.chatIdentifier);
if (identifier) {
return `chat_identifier:${identifier}`;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-path";
import type { BlueBubblesAccountConfig } from "./types.js";
@@ -6,7 +7,7 @@ export { normalizeWebhookPath };
export const DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook";
export function resolveWebhookPathFromConfig(config?: BlueBubblesAccountConfig): string {
const raw = config?.webhookPath?.trim();
const raw = normalizeOptionalString(config?.webhookPath);
if (raw) {
return normalizeWebhookPath(raw);
}