refactor: dedupe trim reader aliases

This commit is contained in:
Peter Steinberger
2026-04-07 08:56:52 +01:00
parent 9c9b0effda
commit 7087845f58
8 changed files with 98 additions and 137 deletions

View File

@@ -91,10 +91,6 @@ export function _setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard |
comfyFetchGuard = impl ?? fetchWithSsrFGuard;
}
function readConfigString(config: ComfyProviderConfig, key: string): string | undefined {
return normalizeOptionalString(config[key]);
}
function readConfigBoolean(config: ComfyProviderConfig, key: string): boolean | undefined {
const value = config[key];
return typeof value === "boolean" ? value : undefined;
@@ -554,9 +550,9 @@ export function isComfyCapabilityConfigured(params: {
const capabilityConfig = getComfyCapabilityConfig(config, params.capability);
const hasWorkflow = Boolean(
resolveComfyWorkflowSource(capabilityConfig).workflow ||
readConfigString(capabilityConfig, "workflowPath"),
normalizeOptionalString(capabilityConfig.workflowPath),
);
const hasPromptNode = Boolean(readConfigString(capabilityConfig, "promptNodeId"));
const hasPromptNode = Boolean(normalizeOptionalString(capabilityConfig.promptNodeId));
if (!hasWorkflow || !hasPromptNode) {
return false;
}
@@ -586,11 +582,11 @@ export async function runComfyWorkflow(params: {
const workflow = await loadComfyWorkflow(capabilityConfig);
const promptNodeId = getRequiredConfigString(capabilityConfig, "promptNodeId");
const promptInputName =
readConfigString(capabilityConfig, "promptInputName") ?? DEFAULT_PROMPT_INPUT_NAME;
const inputImageNodeId = readConfigString(capabilityConfig, "inputImageNodeId");
normalizeOptionalString(capabilityConfig.promptInputName) ?? DEFAULT_PROMPT_INPUT_NAME;
const inputImageNodeId = normalizeOptionalString(capabilityConfig.inputImageNodeId);
const inputImageInputName =
readConfigString(capabilityConfig, "inputImageInputName") ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
const outputNodeId = readConfigString(capabilityConfig, "outputNodeId");
normalizeOptionalString(capabilityConfig.inputImageInputName) ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
const pollIntervalMs =
readConfigInteger(capabilityConfig, "pollIntervalMs") ?? DEFAULT_POLL_INTERVAL_MS;
const timeoutMs =
@@ -619,7 +615,7 @@ export async function runComfyWorkflow(params: {
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: readConfigString(capabilityConfig, "baseUrl"),
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
defaultBaseUrl:
mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
allowPrivateNetwork:

View File

@@ -16,6 +16,7 @@ import {
createComputedAccountStatusAdapter,
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { mattermostApprovalAuth } from "./approval-auth.js";
import {
chunkTextForOutbound,
@@ -242,25 +243,18 @@ const mattermostMessageActions: ChannelMessageActionAdapter = {
},
};
function readTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function parseMattermostReactActionParams(params: Record<string, unknown>): {
postId: string;
emojiName: string;
remove: boolean;
} {
const postId = readTrimmedString(params.messageId) ?? readTrimmedString(params.postId);
const postId =
normalizeOptionalString(params.messageId) ?? normalizeOptionalString(params.postId);
if (!postId) {
throw new Error("Mattermost react requires messageId (post id)");
}
const emojiName = readTrimmedString(params.emoji)?.replace(/^:+|:+$/g, "");
const emojiName = normalizeOptionalString(params.emoji)?.replace(/^:+|:+$/g, "");
if (!emojiName) {
throw new Error("Mattermost react requires emoji");
}
@@ -273,7 +267,7 @@ function parseMattermostReactActionParams(params: Record<string, unknown>): {
}
function readMattermostReplyToId(params: Record<string, unknown>): string | undefined {
return readTrimmedString(params.replyToId) ?? readTrimmedString(params.replyTo);
return normalizeOptionalString(params.replyToId) ?? normalizeOptionalString(params.replyTo);
}
export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = createChatChannelPlugin({

View File

@@ -7,10 +7,6 @@ import type { EnvelopeFormatOptions } from "../envelope.js";
import { formatEnvelopeTimestamp } from "../envelope.js";
import type { TemplateContext } from "../templating.js";
function safeTrim(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function formatConversationTimestamp(
value: unknown,
envelope?: EnvelopeFormatOptions,
@@ -22,9 +18,10 @@ function formatConversationTimestamp(
}
function resolveInboundChannel(ctx: TemplateContext): string | undefined {
let channelValue = safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface);
let channelValue =
normalizeOptionalString(ctx.OriginatingChannel) ?? normalizeOptionalString(ctx.Surface);
if (!channelValue) {
const provider = safeTrim(ctx.Provider);
const provider = normalizeOptionalString(ctx.Provider);
if (provider !== "webchat" && ctx.Surface !== "webchat") {
channelValue = provider;
}
@@ -47,7 +44,7 @@ function resolveInboundFormattingHints(ctx: TemplateContext):
getLoadedChannelPlugin(normalizedChannel)?.agentPrompt ??
getBundledChannelPlugin(normalizedChannel)?.agentPrompt;
return agentPrompt?.inboundFormattingHints?.({
accountId: safeTrim(ctx.AccountId) ?? undefined,
accountId: normalizeOptionalString(ctx.AccountId) ?? undefined,
});
}
@@ -71,11 +68,11 @@ export function buildInboundMetaSystemPrompt(
const payload = {
schema: "openclaw.inbound_meta.v1",
chat_id: safeTrim(ctx.OriginatingTo),
account_id: safeTrim(ctx.AccountId),
chat_id: normalizeOptionalString(ctx.OriginatingTo),
account_id: normalizeOptionalString(ctx.AccountId),
channel: channelValue,
provider: safeTrim(ctx.Provider),
surface: safeTrim(ctx.Surface),
provider: normalizeOptionalString(ctx.Provider),
surface: normalizeOptionalString(ctx.Surface),
chat_type: chatType ?? (isDirect ? "direct" : undefined),
response_format:
options?.includeFormattingHints === false ? undefined : resolveInboundFormattingHints(ctx),
@@ -108,34 +105,34 @@ export function buildInboundUserContextPrefix(
);
const shouldIncludeConversationInfo = !isDirect || includeDirectConversationInfo;
const messageId = safeTrim(ctx.MessageSid);
const messageIdFull = safeTrim(ctx.MessageSidFull);
const messageId = normalizeOptionalString(ctx.MessageSid);
const messageIdFull = normalizeOptionalString(ctx.MessageSidFull);
const resolvedMessageId = messageId ?? messageIdFull;
const timestampStr = formatConversationTimestamp(ctx.Timestamp, envelope);
const conversationInfo = {
message_id: shouldIncludeConversationInfo ? resolvedMessageId : undefined,
reply_to_id: shouldIncludeConversationInfo ? safeTrim(ctx.ReplyToId) : undefined,
sender_id: shouldIncludeConversationInfo ? safeTrim(ctx.SenderId) : undefined,
conversation_label: isDirect ? undefined : safeTrim(ctx.ConversationLabel),
reply_to_id: shouldIncludeConversationInfo ? normalizeOptionalString(ctx.ReplyToId) : undefined,
sender_id: shouldIncludeConversationInfo ? normalizeOptionalString(ctx.SenderId) : undefined,
conversation_label: isDirect ? undefined : normalizeOptionalString(ctx.ConversationLabel),
sender: shouldIncludeConversationInfo
? (safeTrim(ctx.SenderName) ??
safeTrim(ctx.SenderE164) ??
safeTrim(ctx.SenderId) ??
safeTrim(ctx.SenderUsername))
? (normalizeOptionalString(ctx.SenderName) ??
normalizeOptionalString(ctx.SenderE164) ??
normalizeOptionalString(ctx.SenderId) ??
normalizeOptionalString(ctx.SenderUsername))
: undefined,
timestamp: timestampStr,
group_subject: safeTrim(ctx.GroupSubject),
group_channel: safeTrim(ctx.GroupChannel),
group_space: safeTrim(ctx.GroupSpace),
thread_label: safeTrim(ctx.ThreadLabel),
group_subject: normalizeOptionalString(ctx.GroupSubject),
group_channel: normalizeOptionalString(ctx.GroupChannel),
group_space: normalizeOptionalString(ctx.GroupSpace),
thread_label: normalizeOptionalString(ctx.ThreadLabel),
topic_id: ctx.MessageThreadId != null ? String(ctx.MessageThreadId) : undefined,
is_forum: ctx.IsForum === true ? true : undefined,
is_group_chat: !isDirect ? true : undefined,
was_mentioned: ctx.WasMentioned === true ? true : undefined,
has_reply_context: ctx.ReplyToBody ? true : undefined,
has_forwarded_context: ctx.ForwardedFrom ? true : undefined,
has_thread_starter: safeTrim(ctx.ThreadStarterBody) ? true : undefined,
has_thread_starter: normalizeOptionalString(ctx.ThreadStarterBody) ? true : undefined,
history_count:
Array.isArray(ctx.InboundHistory) && ctx.InboundHistory.length > 0
? ctx.InboundHistory.length
@@ -154,17 +151,17 @@ export function buildInboundUserContextPrefix(
const senderInfo = {
label: resolveSenderLabel({
name: safeTrim(ctx.SenderName),
username: safeTrim(ctx.SenderUsername),
tag: safeTrim(ctx.SenderTag),
e164: safeTrim(ctx.SenderE164),
id: safeTrim(ctx.SenderId),
name: normalizeOptionalString(ctx.SenderName),
username: normalizeOptionalString(ctx.SenderUsername),
tag: normalizeOptionalString(ctx.SenderTag),
e164: normalizeOptionalString(ctx.SenderE164),
id: normalizeOptionalString(ctx.SenderId),
}),
id: safeTrim(ctx.SenderId),
name: safeTrim(ctx.SenderName),
username: safeTrim(ctx.SenderUsername),
tag: safeTrim(ctx.SenderTag),
e164: safeTrim(ctx.SenderE164),
id: normalizeOptionalString(ctx.SenderId),
name: normalizeOptionalString(ctx.SenderName),
username: normalizeOptionalString(ctx.SenderUsername),
tag: normalizeOptionalString(ctx.SenderTag),
e164: normalizeOptionalString(ctx.SenderE164),
};
if (senderInfo?.label) {
blocks.push(
@@ -174,7 +171,7 @@ export function buildInboundUserContextPrefix(
);
}
if (safeTrim(ctx.ThreadStarterBody)) {
if (normalizeOptionalString(ctx.ThreadStarterBody)) {
blocks.push(
[
"Thread starter (untrusted, for context):",
@@ -192,7 +189,7 @@ export function buildInboundUserContextPrefix(
"```json",
JSON.stringify(
{
sender_label: safeTrim(ctx.ReplyToSender),
sender_label: normalizeOptionalString(ctx.ReplyToSender),
is_quote: ctx.ReplyToIsQuote === true ? true : undefined,
body: ctx.ReplyToBody,
},
@@ -211,12 +208,12 @@ export function buildInboundUserContextPrefix(
"```json",
JSON.stringify(
{
from: safeTrim(ctx.ForwardedFrom),
type: safeTrim(ctx.ForwardedFromType),
username: safeTrim(ctx.ForwardedFromUsername),
title: safeTrim(ctx.ForwardedFromTitle),
signature: safeTrim(ctx.ForwardedFromSignature),
chat_type: safeTrim(ctx.ForwardedFromChatType),
from: normalizeOptionalString(ctx.ForwardedFrom),
type: normalizeOptionalString(ctx.ForwardedFromType),
username: normalizeOptionalString(ctx.ForwardedFromUsername),
title: normalizeOptionalString(ctx.ForwardedFromTitle),
signature: normalizeOptionalString(ctx.ForwardedFromSignature),
chat_type: normalizeOptionalString(ctx.ForwardedFromChatType),
date_ms: typeof ctx.ForwardedDate === "number" ? ctx.ForwardedDate : undefined,
},
null,

View File

@@ -17,10 +17,6 @@ const CREDENTIAL_STATUS_KEYS = [
type CredentialStatusKey = (typeof CREDENTIAL_STATUS_KEYS)[number];
function readTrimmedString(record: Record<string, unknown>, key: string): string | undefined {
return normalizeOptionalString(record[key]);
}
function readBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
return typeof record[key] === "boolean" ? record[key] : undefined;
}
@@ -130,20 +126,16 @@ export function projectCredentialSnapshotFields(
if (!record) {
return {};
}
const tokenSource = normalizeOptionalString(record.tokenSource);
const botTokenSource = normalizeOptionalString(record.botTokenSource);
const appTokenSource = normalizeOptionalString(record.appTokenSource);
const signingSecretSource = normalizeOptionalString(record.signingSecretSource);
return {
...(readTrimmedString(record, "tokenSource")
? { tokenSource: readTrimmedString(record, "tokenSource") }
: {}),
...(readTrimmedString(record, "botTokenSource")
? { botTokenSource: readTrimmedString(record, "botTokenSource") }
: {}),
...(readTrimmedString(record, "appTokenSource")
? { appTokenSource: readTrimmedString(record, "appTokenSource") }
: {}),
...(readTrimmedString(record, "signingSecretSource")
? { signingSecretSource: readTrimmedString(record, "signingSecretSource") }
: {}),
...(tokenSource ? { tokenSource } : {}),
...(botTokenSource ? { botTokenSource } : {}),
...(appTokenSource ? { appTokenSource } : {}),
...(signingSecretSource ? { signingSecretSource } : {}),
...(readCredentialStatus(record, "tokenStatus")
? { tokenStatus: readCredentialStatus(record, "tokenStatus") }
: {}),
@@ -169,9 +161,16 @@ export function projectSafeChannelAccountSnapshotFields(
if (!record) {
return {};
}
const name = normalizeOptionalString(record.name);
const healthState = normalizeOptionalString(record.healthState);
const mode = normalizeOptionalString(record.mode);
const dmPolicy = normalizeOptionalString(record.dmPolicy);
const baseUrl = normalizeOptionalString(record.baseUrl);
const cliPath = normalizeOptionalString(record.cliPath);
const dbPath = normalizeOptionalString(record.dbPath);
return {
...(readTrimmedString(record, "name") ? { name: readTrimmedString(record, "name") } : {}),
...(name ? { name } : {}),
...(readBoolean(record, "linked") !== undefined
? { linked: readBoolean(record, "linked") }
: {}),
@@ -187,27 +186,19 @@ export function projectSafeChannelAccountSnapshotFields(
...(readNumber(record, "lastInboundAt") !== undefined
? { lastInboundAt: readNumber(record, "lastInboundAt") }
: {}),
...(readTrimmedString(record, "healthState")
? { healthState: readTrimmedString(record, "healthState") }
: {}),
...(readTrimmedString(record, "mode") ? { mode: readTrimmedString(record, "mode") } : {}),
...(readTrimmedString(record, "dmPolicy")
? { dmPolicy: readTrimmedString(record, "dmPolicy") }
: {}),
...(healthState ? { healthState } : {}),
...(mode ? { mode } : {}),
...(dmPolicy ? { dmPolicy } : {}),
...(readStringArray(record, "allowFrom")
? { allowFrom: readStringArray(record, "allowFrom") }
: {}),
...projectCredentialSnapshotFields(account),
...(readTrimmedString(record, "baseUrl")
? { baseUrl: stripUrlUserInfo(readTrimmedString(record, "baseUrl")!) }
: {}),
...(baseUrl ? { baseUrl: stripUrlUserInfo(baseUrl) } : {}),
...(readBoolean(record, "allowUnmentionedGroups") !== undefined
? { allowUnmentionedGroups: readBoolean(record, "allowUnmentionedGroups") }
: {}),
...(readTrimmedString(record, "cliPath")
? { cliPath: readTrimmedString(record, "cliPath") }
: {}),
...(readTrimmedString(record, "dbPath") ? { dbPath: readTrimmedString(record, "dbPath") } : {}),
...(cliPath ? { cliPath } : {}),
...(dbPath ? { dbPath } : {}),
...(readNumber(record, "port") !== undefined ? { port: readNumber(record, "port") } : {}),
};
}

View File

@@ -75,16 +75,16 @@ export function applyExistingCronSchedulePatch(
}
function normalizeScheduleOptions(options: ScheduleOptionInput): NormalizedScheduleOptions {
const staggerRaw = readTrimmedString(options.stagger);
const staggerRaw = normalizeOptionalString(options.stagger) ?? "";
const useExact = Boolean(options.exact);
if (staggerRaw && useExact) {
throw new Error("Choose either --stagger or --exact, not both");
}
return {
at: readTrimmedString(options.at),
every: readTrimmedString(options.every),
cronExpr: readTrimmedString(options.cron),
tz: readOptionalString(options.tz),
at: normalizeOptionalString(options.at) ?? "",
every: normalizeOptionalString(options.every) ?? "",
cronExpr: normalizeOptionalString(options.cron) ?? "",
tz: normalizeOptionalString(options.tz),
requestedStaggerMs: parseCronStaggerMs({ staggerRaw, useExact }),
};
}
@@ -125,11 +125,3 @@ function resolveDirectSchedule(options: NormalizedScheduleOptions): CronSchedule
}
return undefined;
}
function readOptionalString(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function readTrimmedString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}

View File

@@ -1,5 +1,6 @@
import { spawnSync } from "node:child_process";
import os from "node:os";
import { normalizeOptionalString } from "../shared/string-coerce.js";
export type OsSummary = {
platform: NodeJS.Platform;
@@ -8,13 +9,9 @@ export type OsSummary = {
label: string;
};
function safeTrim(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function macosVersion(): string {
const res = spawnSync("sw_vers", ["-productVersion"], { encoding: "utf-8" });
const out = safeTrim(res.stdout);
const out = normalizeOptionalString(res.stdout) ?? "";
return out || os.release();
}

View File

@@ -38,12 +38,8 @@ export type InteractiveReply = {
blocks: InteractiveReplyBlock[];
};
function readTrimmedString(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function normalizeButtonStyle(value: unknown): InteractiveButtonStyle | undefined {
const style = readTrimmedString(value)?.toLowerCase();
const style = normalizeOptionalString(value)?.toLowerCase();
return style === "primary" || style === "secondary" || style === "success" || style === "danger"
? style
: undefined;
@@ -54,11 +50,11 @@ function normalizeInteractiveButton(raw: unknown): InteractiveReplyButton | unde
return undefined;
}
const record = raw as Record<string, unknown>;
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
const label = normalizeOptionalString(record.label) ?? normalizeOptionalString(record.text);
const value =
readTrimmedString(record.value) ??
readTrimmedString(record.callbackData) ??
readTrimmedString(record.callback_data);
normalizeOptionalString(record.value) ??
normalizeOptionalString(record.callbackData) ??
normalizeOptionalString(record.callback_data);
if (!label || !value) {
return undefined;
}
@@ -74,8 +70,8 @@ function normalizeInteractiveOption(raw: unknown): InteractiveReplyOption | unde
return undefined;
}
const record = raw as Record<string, unknown>;
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
const value = readTrimmedString(record.value);
const label = normalizeOptionalString(record.label) ?? normalizeOptionalString(record.text);
const value = normalizeOptionalString(record.value);
if (!label || !value) {
return undefined;
}
@@ -87,9 +83,9 @@ function normalizeInteractiveBlock(raw: unknown): InteractiveReplyBlock | undefi
return undefined;
}
const record = raw as Record<string, unknown>;
const type = readTrimmedString(record.type)?.toLowerCase();
const type = normalizeOptionalString(record.type)?.toLowerCase();
if (type === "text") {
const text = readTrimmedString(record.text);
const text = normalizeOptionalString(record.text);
return text ? { type: "text", text } : undefined;
}
if (type === "buttons") {
@@ -109,7 +105,7 @@ function normalizeInteractiveBlock(raw: unknown): InteractiveReplyBlock | undefi
return options.length > 0
? {
type: "select",
placeholder: readTrimmedString(record.placeholder),
placeholder: normalizeOptionalString(record.placeholder),
options,
}
: undefined;
@@ -148,10 +144,12 @@ export function hasReplyContent(params: {
hasChannelData?: boolean;
extraContent?: boolean;
}): boolean {
const text = normalizeOptionalString(params.text);
const mediaUrl = normalizeOptionalString(params.mediaUrl);
return Boolean(
params.text?.trim() ||
params.mediaUrl?.trim() ||
params.mediaUrls?.some((entry) => Boolean(entry?.trim())) ||
text ||
mediaUrl ||
params.mediaUrls?.some((entry) => Boolean(normalizeOptionalString(entry))) ||
hasInteractiveReplyBlocks(params.interactive) ||
params.hasChannelData ||
params.extraContent,
@@ -186,7 +184,7 @@ export function resolveInteractiveTextFallback(params: {
text?: string;
interactive?: InteractiveReply;
}): string | undefined {
const text = readTrimmedString(params.text);
const text = normalizeOptionalString(params.text);
if (text) {
return params.text;
}

View File

@@ -3,12 +3,8 @@ import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registr
import type { OpenClawConfig } from "../config/config.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
function normalizeChannelId(value?: string | null): string | undefined {
return normalizeOptionalString(value)?.toLowerCase();
}
function findChannelMessagingAdapter(channelId?: string | null) {
const normalized = normalizeChannelId(channelId);
const normalized = normalizeOptionalString(channelId)?.toLowerCase();
if (!normalized) {
return undefined;
}