refactor: dedupe trimmed string readers

This commit is contained in:
Peter Steinberger
2026-04-07 22:46:27 +01:00
parent 7897fb9c84
commit 67035a6af8
12 changed files with 74 additions and 97 deletions

View File

@@ -26,6 +26,7 @@ import {
wrapWebContent,
writeCachedSearchPayload,
} from "openclaw/plugin-sdk/provider-web-search";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
isNativeMoonshotBaseUrl,
MOONSHOT_BASE_URL,
@@ -92,7 +93,7 @@ function resolveKimiApiKey(kimi?: KimiConfig): string | undefined {
}
function resolveKimiModel(kimi?: KimiConfig): string {
const model = typeof kimi?.model === "string" ? kimi.model.trim() : "";
const model = normalizeOptionalString(kimi?.model) ?? "";
return model || DEFAULT_KIMI_SEARCH_MODEL;
}
@@ -101,7 +102,7 @@ function trimTrailingSlashes(url: string): string {
}
function resolveKimiBaseUrl(kimi?: KimiConfig, openClawConfig?: OpenClawConfig): string {
const explicitBaseUrl = typeof kimi?.baseUrl === "string" ? kimi.baseUrl.trim() : "";
const explicitBaseUrl = normalizeOptionalString(kimi?.baseUrl) ?? "";
if (explicitBaseUrl) {
return trimTrailingSlashes(explicitBaseUrl) || DEFAULT_KIMI_BASE_URL;
}
@@ -141,12 +142,14 @@ function extractKimiCitations(data: KimiSearchResponse): string[] {
search_results?: Array<{ url?: string }>;
url?: string;
};
if (typeof parsed.url === "string" && parsed.url.trim()) {
citations.push(parsed.url.trim());
const parsedUrl = normalizeOptionalString(parsed.url);
if (parsedUrl) {
citations.push(parsedUrl);
}
for (const result of parsed.search_results ?? []) {
if (typeof result.url === "string" && result.url.trim()) {
citations.push(result.url.trim());
const resultUrl = normalizeOptionalString(result.url);
if (resultUrl) {
citations.push(resultUrl);
}
}
} catch {
@@ -352,12 +355,10 @@ async function runKimiSearchProviderSetup(
ctx: WebSearchProviderSetupContext,
): Promise<WebSearchProviderSetupContext["config"]> {
const existingPluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "moonshot");
const existingBaseUrl =
typeof existingPluginConfig?.baseUrl === "string" ? existingPluginConfig.baseUrl.trim() : "";
const existingBaseUrl = normalizeOptionalString(existingPluginConfig?.baseUrl) ?? "";
// Normalize trailing slashes so initialValue matches canonical option values.
const normalizedBaseUrl = existingBaseUrl.replace(/\/+$/, "");
const existingModel =
typeof existingPluginConfig?.model === "string" ? existingPluginConfig.model.trim() : "";
const existingModel = normalizeOptionalString(existingPluginConfig?.model) ?? "";
// Region selection (baseUrl)
const isCustomBaseUrl = normalizedBaseUrl && !isNativeMoonshotBaseUrl(normalizedBaseUrl);

View File

@@ -72,7 +72,7 @@ function resolveActionTarget(
}
function resolveActionMessageId(params: Record<string, unknown>): string {
return typeof params.messageId === "string" ? params.messageId.trim() : "";
return normalizeOptionalString(params.messageId) ?? "";
}
function resolveActionPinnedMessageId(params: Record<string, unknown>): string {
@@ -84,7 +84,7 @@ function resolveActionPinnedMessageId(params: Record<string, unknown>): string {
}
function resolveActionQuery(params: Record<string, unknown>): string {
return typeof params.query === "string" ? params.query.trim() : "";
return normalizeOptionalString(params.query) ?? "";
}
function resolveActionContent(params: Record<string, unknown>): string {
@@ -413,7 +413,7 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
toolParams: ctx.params,
currentChannelId: ctx.toolContext?.currentChannelId,
run: async (target) => {
const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : "";
const emoji = normalizeOptionalString(ctx.params.emoji) ?? "";
const remove = typeof ctx.params.remove === "boolean" ? ctx.params.remove : false;
if (!emoji) {
return {
@@ -487,7 +487,7 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
return actionError("Search requires a target (to) and query.");
}
const limit = typeof ctx.params.limit === "number" ? ctx.params.limit : undefined;
const from = typeof ctx.params.from === "string" ? ctx.params.from.trim() : undefined;
const from = normalizeOptionalString(ctx.params.from);
const { searchMessagesMSTeams } = await loadMSTeamsChannelRuntime();
const result = await searchMessagesMSTeams({
cfg: ctx.cfg,
@@ -502,7 +502,7 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
}
if (ctx.action === "member-info") {
const userId = typeof ctx.params.userId === "string" ? ctx.params.userId.trim() : "";
const userId = normalizeOptionalString(ctx.params.userId) ?? "";
if (!userId) {
return actionError("member-info requires a userId.");
}
@@ -512,7 +512,7 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
}
if (ctx.action === "channel-list") {
const teamId = typeof ctx.params.teamId === "string" ? ctx.params.teamId.trim() : "";
const teamId = normalizeOptionalString(ctx.params.teamId) ?? "";
if (!teamId) {
return actionError("channel-list requires a teamId.");
}
@@ -522,8 +522,8 @@ export const msteamsActionsAdapter: NonNullable<ChannelPlugin["actions"]> = {
}
if (ctx.action === "channel-info") {
const teamId = typeof ctx.params.teamId === "string" ? ctx.params.teamId.trim() : "";
const channelId = typeof ctx.params.channelId === "string" ? ctx.params.channelId.trim() : "";
const teamId = normalizeOptionalString(ctx.params.teamId) ?? "";
const channelId = normalizeOptionalString(ctx.params.channelId) ?? "";
if (!teamId || !channelId) {
return actionError("channel-info requires teamId and channelId.");
}

View File

@@ -1,4 +1,7 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { getMSTeamsRuntime } from "../runtime.js";
import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
import {
@@ -29,21 +32,20 @@ type DownloadCandidate = {
function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate | null {
const contentType = normalizeContentType(att.contentType);
const name = typeof att.name === "string" ? att.name.trim() : "";
const name = normalizeOptionalString(att.name) ?? "";
if (contentType === "application/vnd.microsoft.teams.file.download.info") {
if (!isRecord(att.content)) {
return null;
}
const downloadUrl =
typeof att.content.downloadUrl === "string" ? att.content.downloadUrl.trim() : "";
const downloadUrl = normalizeOptionalString(att.content.downloadUrl) ?? "";
if (!downloadUrl) {
return null;
}
const fileType = typeof att.content.fileType === "string" ? att.content.fileType.trim() : "";
const uniqueId = typeof att.content.uniqueId === "string" ? att.content.uniqueId.trim() : "";
const fileName = typeof att.content.fileName === "string" ? att.content.fileName.trim() : "";
const fileType = normalizeOptionalString(att.content.fileType) ?? "";
const uniqueId = normalizeOptionalString(att.content.uniqueId) ?? "";
const fileName = normalizeOptionalString(att.content.fileName) ?? "";
const fileHint = name || fileName || (uniqueId && fileType ? `${uniqueId}.${fileType}` : "");
return {
@@ -58,7 +60,7 @@ function resolveDownloadCandidate(att: MSTeamsAttachmentLike): DownloadCandidate
};
}
const contentUrl = typeof att.contentUrl === "string" ? att.contentUrl.trim() : "";
const contentUrl = normalizeOptionalString(att.contentUrl) ?? "";
if (!contentUrl) {
return null;
}

View File

@@ -1,6 +1,7 @@
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { fetchWithSsrFGuard, type SsrFPolicy } from "../../runtime-api.js";
import { getMSTeamsRuntime } from "../runtime.js";
@@ -54,7 +55,7 @@ export function buildMSTeamsGraphMessageUrls(params: {
const conversationType = normalizeLowercaseStringOrEmpty(params.conversationType ?? "");
const messageIdCandidates = new Set<string>();
const pushCandidate = (value: string | null | undefined) => {
const trimmed = typeof value === "string" ? value.trim() : "";
const trimmed = normalizeOptionalString(value) ?? "";
if (trimmed) {
messageIdCandidates.add(trimmed);
}
@@ -65,7 +66,7 @@ export function buildMSTeamsGraphMessageUrls(params: {
pushCandidate(readNestedString(params.channelData, ["messageId"]));
pushCandidate(readNestedString(params.channelData, ["teamsMessageId"]));
const replyToId = typeof params.replyToId === "string" ? params.replyToId.trim() : "";
const replyToId = normalizeOptionalString(params.replyToId) ?? "";
if (conversationType === "channel") {
const teamId =

View File

@@ -325,7 +325,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
formatCapabilitiesProbe: ({ probe }) => {
const teamsProbe = probe as ProbeMSTeamsResult | undefined;
const lines: Array<{ text: string; tone?: "error" }> = [];
const appId = typeof teamsProbe?.appId === "string" ? teamsProbe.appId.trim() : "";
const appId = normalizeOptionalString(teamsProbe?.appId) ?? "";
if (appId) {
lines.push({ text: `App: ${appId}` });
}

View File

@@ -1,5 +1,6 @@
import { Type } from "@sinclair/typebox";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
definePluginEntry,
type GatewayRequestHandlerOptions,
@@ -196,8 +197,8 @@ export default definePluginEntry({
};
const resolveCallMessageRequest = async (params: GatewayRequestHandlerOptions["params"]) => {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
const callId = normalizeOptionalString(params?.callId) ?? "";
const message = normalizeOptionalString(params?.message) ?? "";
if (!callId || !message) {
return { error: "callId and message required" } as const;
}
@@ -257,16 +258,13 @@ export default definePluginEntry({
"voicecall.initiate",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const message = typeof params?.message === "string" ? params.message.trim() : "";
const message = normalizeOptionalString(params?.message) ?? "";
if (!message) {
respond(false, { error: "message required" });
return;
}
const rt = await ensureRuntime();
const to =
typeof params?.to === "string" && params.to.trim()
? params.to.trim()
: rt.config.toNumber;
const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
if (!to) {
respond(false, { error: "to required" });
return;
@@ -323,7 +321,7 @@ export default definePluginEntry({
"voicecall.end",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
const callId = normalizeOptionalString(params?.callId) ?? "";
if (!callId) {
respond(false, { error: "callId required" });
return;
@@ -346,11 +344,7 @@ export default definePluginEntry({
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const raw =
typeof params?.callId === "string"
? params.callId.trim()
: typeof params?.sid === "string"
? params.sid.trim()
: "";
normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid) ?? "";
if (!raw) {
respond(false, { error: "callId required" });
return;
@@ -372,8 +366,8 @@ export default definePluginEntry({
"voicecall.start",
async ({ params, respond }: GatewayRequestHandlerOptions) => {
try {
const to = typeof params?.to === "string" ? params.to.trim() : "";
const message = typeof params?.message === "string" ? params.message.trim() : "";
const to = normalizeOptionalString(params?.to) ?? "";
const message = normalizeOptionalString(params?.message) ?? "";
if (!to) {
respond(false, { error: "to required" });
return;
@@ -408,14 +402,11 @@ export default definePluginEntry({
if (typeof params?.action === "string") {
switch (params.action) {
case "initiate_call": {
const message = String(params.message || "").trim();
const message = normalizeOptionalString(params.message) ?? "";
if (!message) {
throw new Error("message required");
}
const to =
typeof params.to === "string" && params.to.trim()
? params.to.trim()
: rt.config.toNumber;
const to = normalizeOptionalString(params.to) ?? rt.config.toNumber;
if (!to) {
throw new Error("to required");
}
@@ -432,8 +423,8 @@ export default definePluginEntry({
return json({ callId: result.callId, initiated: true });
}
case "continue_call": {
const callId = String(params.callId || "").trim();
const message = String(params.message || "").trim();
const callId = normalizeOptionalString(params.callId) ?? "";
const message = normalizeOptionalString(params.message) ?? "";
if (!callId || !message) {
throw new Error("callId and message required");
}
@@ -444,8 +435,8 @@ export default definePluginEntry({
return json({ success: true, transcript: result.transcript });
}
case "speak_to_user": {
const callId = String(params.callId || "").trim();
const message = String(params.message || "").trim();
const callId = normalizeOptionalString(params.callId) ?? "";
const message = normalizeOptionalString(params.message) ?? "";
if (!callId || !message) {
throw new Error("callId and message required");
}
@@ -456,7 +447,7 @@ export default definePluginEntry({
return json({ success: true });
}
case "end_call": {
const callId = String(params.callId || "").trim();
const callId = normalizeOptionalString(params.callId) ?? "";
if (!callId) {
throw new Error("callId required");
}
@@ -467,7 +458,7 @@ export default definePluginEntry({
return json({ success: true });
}
case "get_status": {
const callId = String(params.callId || "").trim();
const callId = normalizeOptionalString(params.callId) ?? "";
if (!callId) {
throw new Error("callId required");
}
@@ -480,7 +471,7 @@ export default definePluginEntry({
const mode = params?.mode ?? "call";
if (mode === "status") {
const sid = typeof params.sid === "string" ? params.sid.trim() : "";
const sid = normalizeOptionalString(params.sid) ?? "";
if (!sid) {
throw new Error("sid required for status");
}
@@ -488,18 +479,12 @@ export default definePluginEntry({
return json(call ? { found: true, call } : { found: false });
}
const to =
typeof params.to === "string" && params.to.trim()
? params.to.trim()
: rt.config.toNumber;
const to = normalizeOptionalString(params.to) ?? rt.config.toNumber;
if (!to) {
throw new Error("to required for call");
}
const result = await rt.manager.initiateCall(to, undefined, {
message:
typeof params.message === "string" && params.message.trim()
? params.message.trim()
: undefined,
message: normalizeOptionalString(params.message),
});
if (!result.success) {
throw new Error(result.error || "initiate failed");

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { VoiceCallConfig } from "./config.js";
import type { CallManagerContext } from "./manager/context.js";
import { processEvent as processManagerEvent } from "./manager/events.js";
@@ -287,8 +288,7 @@ export class CallManager {
}
private maybeSpeakInitialMessageOnAnswered(call: CallRecord): void {
const initialMessage =
typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage.trim() : "";
const initialMessage = normalizeOptionalString(call.metadata?.initialMessage) ?? "";
if (!initialMessage) {
return;

View File

@@ -1,5 +1,8 @@
import crypto from "node:crypto";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type { PlivoConfig, WebhookSecurityConfig } from "../config.js";
import { getHeader } from "../http-headers.js";
import type {
@@ -130,7 +133,7 @@ export class PlivoProvider implements VoiceCallProvider {
ctx: WebhookContext,
options?: WebhookParseOptions,
): ProviderWebhookParseResult {
const flow = typeof ctx.query?.flow === "string" ? ctx.query.flow.trim() : "";
const flow = normalizeOptionalString(ctx.query?.flow) ?? "";
const parsed = this.parseBody(ctx.rawBody);
if (!parsed) {
@@ -518,10 +521,7 @@ export class PlivoProvider implements VoiceCallProvider {
}
private getCallIdFromQuery(ctx: WebhookContext): string | undefined {
const callId =
typeof ctx.query?.callId === "string" && ctx.query.callId.trim()
? ctx.query.callId.trim()
: undefined;
const callId = normalizeOptionalString(ctx.query?.callId);
return callId || undefined;
}

View File

@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { TwilioConfig, WebhookSecurityConfig } from "../config.js";
import { getHeader } from "../http-headers.js";
import type { MediaStreamHandler } from "../media-stream.js";
@@ -46,9 +47,9 @@ function createTwilioRequestDedupeKey(ctx: WebhookContext, verifiedRequestKey?:
const callSid = params.get("CallSid") ?? "";
const callStatus = params.get("CallStatus") ?? "";
const direction = params.get("Direction") ?? "";
const callId = typeof ctx.query?.callId === "string" ? ctx.query.callId.trim() : "";
const flow = typeof ctx.query?.flow === "string" ? ctx.query.flow.trim() : "";
const turnToken = typeof ctx.query?.turnToken === "string" ? ctx.query.turnToken.trim() : "";
const callId = normalizeOptionalString(ctx.query?.callId) ?? "";
const flow = normalizeOptionalString(ctx.query?.flow) ?? "";
const turnToken = normalizeOptionalString(ctx.query?.turnToken) ?? "";
return `twilio:fallback:${crypto
.createHash("sha256")
.update(
@@ -265,14 +266,8 @@ export class TwilioProvider implements VoiceCallProvider {
): ProviderWebhookParseResult {
try {
const params = new URLSearchParams(ctx.rawBody);
const callIdFromQuery =
typeof ctx.query?.callId === "string" && ctx.query.callId.trim()
? ctx.query.callId.trim()
: undefined;
const turnTokenFromQuery =
typeof ctx.query?.turnToken === "string" && ctx.query.turnToken.trim()
? ctx.query.turnToken.trim()
: undefined;
const callIdFromQuery = normalizeOptionalString(ctx.query?.callId);
const turnTokenFromQuery = normalizeOptionalString(ctx.query?.turnToken);
const dedupeKey = createTwilioRequestDedupeKey(ctx, options?.verifiedRequestKey);
const event = this.normalizeEvent(params, {
callIdOverride: callIdFromQuery,

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { WebhookContext } from "../../types.js";
export type TwimlResponseKind = "empty" | "pause" | "queue" | "stored" | "stream";
@@ -40,11 +41,8 @@ function isOutboundDirection(direction: string | null): boolean {
export function readTwimlRequestView(ctx: WebhookContext): TwimlRequestView {
const params = new URLSearchParams(ctx.rawBody);
const type = typeof ctx.query?.type === "string" ? ctx.query.type.trim() : undefined;
const callIdFromQuery =
typeof ctx.query?.callId === "string" && ctx.query.callId.trim()
? ctx.query.callId.trim()
: undefined;
const type = normalizeOptionalString(ctx.query?.type);
const callIdFromQuery = normalizeOptionalString(ctx.query?.callId);
return {
callStatus: params.get("CallStatus"),

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { VoiceCallTtsConfig } from "./config.js";
function resolveProviderVoiceSetting(providerConfig: unknown): string | undefined {
@@ -8,13 +9,7 @@ function resolveProviderVoiceSetting(providerConfig: unknown): string | undefine
voice?: unknown;
voiceId?: unknown;
};
if (typeof candidate.voice === "string" && candidate.voice.trim()) {
return candidate.voice;
}
if (typeof candidate.voiceId === "string" && candidate.voiceId.trim()) {
return candidate.voiceId;
}
return undefined;
return normalizeOptionalString(candidate.voice) ?? normalizeOptionalString(candidate.voiceId);
}
export function resolvePreferredTtsVoice(config: { tts?: VoiceCallTtsConfig }): string | undefined {

View File

@@ -1,6 +1,7 @@
import http from "node:http";
import { URL } from "node:url";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import {
createWebhookInFlightLimiter,
WEBHOOK_BODY_READ_DEFAULTS,
@@ -152,8 +153,7 @@ export class VoiceCallWebhookServer {
return false;
}
const initialMessage =
typeof call.metadata?.initialMessage === "string" ? call.metadata.initialMessage.trim() : "";
const initialMessage = normalizeOptionalString(call.metadata?.initialMessage) ?? "";
return initialMessage.length > 0;
}