refactor: dedupe provider ui trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:13:25 +01:00
parent bf03babd2b
commit e0b4f3b995
18 changed files with 238 additions and 166 deletions

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -41,7 +42,9 @@ type BytePlusTaskResponse = {
};
function resolveBytePlusVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.byteplus?.baseUrl?.trim() || BYTEPLUS_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.byteplus?.baseUrl) ?? BYTEPLUS_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -53,13 +56,14 @@ function resolveBytePlusImageUrl(req: VideoGenerationRequest): string | undefine
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("BytePlus reference image is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
async function pollBytePlusTask(params: {
@@ -81,12 +85,14 @@ async function pollBytePlusTask(params: {
);
await assertOkOrThrowHttpError(response, "BytePlus video status request failed");
const payload = (await response.json()) as BytePlusTaskResponse;
switch (payload.status?.trim()) {
switch (normalizeOptionalString(payload.status)) {
case "succeeded":
return payload;
case "failed":
case "cancelled":
throw new Error(payload.error?.message?.trim() || "BytePlus video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) || "BytePlus video generation failed",
);
case "queued":
case "running":
default:
@@ -109,7 +115,7 @@ async function downloadBytePlusVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "BytePlus generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -195,11 +201,12 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
});
}
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_BYTEPLUS_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_BYTEPLUS_VIDEO_MODEL,
content,
};
if (req.aspectRatio?.trim()) {
body.ratio = req.aspectRatio.trim();
const aspectRatio = normalizeOptionalString(req.aspectRatio);
if (aspectRatio) {
body.ratio = aspectRatio;
}
if (req.resolution) {
body.resolution = req.resolution;
@@ -226,7 +233,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
try {
await assertOkOrThrowHttpError(response, "BytePlus video generation failed");
const submitted = (await response.json()) as BytePlusTaskCreateResponse;
const taskId = submitted.id?.trim();
const taskId = normalizeOptionalString(submitted.id);
if (!taskId) {
throw new Error("BytePlus video generation response missing task id");
}
@@ -237,7 +244,7 @@ export function buildBytePlusVideoGenerationProvider(): VideoGenerationProvider
baseUrl,
fetchFn,
});
const videoUrl = completed.content?.video_url?.trim();
const videoUrl = normalizeOptionalString(completed.content?.video_url);
if (!videoUrl) {
throw new Error("BytePlus video generation completed without a video URL");
}

View File

@@ -9,7 +9,10 @@ import {
type SsrFPolicy,
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} 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 {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -64,10 +67,10 @@ function buildPolicy(allowPrivateNetwork: boolean): SsrFPolicy | undefined {
}
function extractFalVideoEntry(payload: FalVideoResponse) {
if (payload.video?.url?.trim()) {
if (normalizeOptionalString(payload.video?.url)) {
return payload.video;
}
return payload.videos?.find((entry) => entry.url?.trim());
return payload.videos?.find((entry) => normalizeOptionalString(entry.url));
}
async function downloadFalVideo(
@@ -82,7 +85,7 @@ async function downloadFalVideo(
});
try {
await assertOkOrThrowHttpError(response, "fal generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -120,10 +123,10 @@ function buildFalVideoRequestBody(params: {
};
const input = params.req.inputImages?.[0];
if (input) {
requestBody.image_url = input.url?.trim()
? input.url.trim()
requestBody.image_url = normalizeOptionalString(input.url)
? normalizeOptionalString(input.url)
: input.buffer
? toDataUrl(input.buffer, input.mimeType?.trim() || "image/png")
? toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png")
: undefined;
}
// MiniMax Live on fal currently documents prompt + optional image_url only.
@@ -132,11 +135,13 @@ function buildFalVideoRequestBody(params: {
if (isFalMiniMaxLiveModel(params.model)) {
return requestBody;
}
if (params.req.aspectRatio?.trim()) {
requestBody.aspect_ratio = params.req.aspectRatio.trim();
const aspectRatio = normalizeOptionalString(params.req.aspectRatio);
if (aspectRatio) {
requestBody.aspect_ratio = aspectRatio;
}
if (params.req.size?.trim()) {
requestBody.size = params.req.size.trim();
const size = normalizeOptionalString(params.req.size);
if (size) {
requestBody.size = size;
}
if (params.req.resolution) {
requestBody.resolution = params.req.resolution;
@@ -198,7 +203,7 @@ async function waitForFalQueueResult(params: {
auditContext: "fal-video-status",
errorContext: "fal video status request failed",
})) as FalQueueResponse;
const status = payload.status?.trim().toUpperCase();
const status = normalizeOptionalString(payload.status)?.toUpperCase();
if (status) {
lastStatus = status;
}
@@ -218,8 +223,8 @@ async function waitForFalQueueResult(params: {
}
if (status === "FAILED" || status === "CANCELLED") {
throw new Error(
payload.detail?.trim() ||
payload.error?.message?.trim() ||
normalizeOptionalString(payload.detail) ||
normalizeOptionalString(payload.error?.message) ||
`fal video generation ${normalizeLowercaseStringOrEmpty(status)}`,
);
}
@@ -288,7 +293,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
}
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: req.cfg?.models?.providers?.fal?.baseUrl?.trim(),
baseUrl: normalizeOptionalString(req.cfg?.models?.providers?.fal?.baseUrl),
defaultBaseUrl: DEFAULT_FAL_BASE_URL,
allowPrivateNetwork: false,
defaultHeaders: {
@@ -299,7 +304,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
capability: "video",
transport: "http",
});
const model = req.model?.trim() || DEFAULT_FAL_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) || DEFAULT_FAL_VIDEO_MODEL;
const requestBody = buildFalVideoRequestBody({ req, model });
const policy = buildPolicy(allowPrivateNetwork);
const queueBaseUrl = resolveFalQueueBaseUrl(baseUrl);
@@ -316,8 +321,8 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
auditContext: "fal-video-submit",
errorContext: "fal video generation failed",
})) as FalQueueResponse;
const statusUrl = submitted.status_url?.trim();
const responseUrl = submitted.response_url?.trim();
const statusUrl = normalizeOptionalString(submitted.status_url);
const responseUrl = normalizeOptionalString(submitted.response_url);
if (!statusUrl || !responseUrl) {
throw new Error("fal video generation response missing queue URLs");
}
@@ -331,7 +336,7 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
});
const videoPayload = extractFalVideoPayload(payload);
const entry = extractFalVideoEntry(videoPayload);
const url = entry?.url?.trim();
const url = normalizeOptionalString(entry?.url);
if (!url) {
throw new Error("fal video generation response missing output URL");
}
@@ -340,7 +345,9 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
videos: [video],
model,
metadata: {
...(submitted.request_id?.trim() ? { requestId: submitted.request_id.trim() } : {}),
...(normalizeOptionalString(submitted.request_id)
? { requestId: normalizeOptionalString(submitted.request_id) }
: {}),
...(videoPayload.prompt ? { prompt: videoPayload.prompt } : {}),
},
};

View File

@@ -8,6 +8,7 @@ import {
applyAgentDefaultModelPrimary,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js";
import { parseGoogleOauthApiKey } from "./oauth-token-shared.js";
export { normalizeAntigravityModelId, normalizeGoogleModelId };
@@ -31,7 +32,7 @@ function isCanonicalGoogleApiOriginShorthand(value: string): boolean {
}
export function normalizeGoogleApiBaseUrl(baseUrl?: string): string {
const raw = trimTrailingSlashes(baseUrl?.trim() || DEFAULT_GOOGLE_API_BASE_URL);
const raw = trimTrailingSlashes(normalizeOptionalString(baseUrl) || DEFAULT_GOOGLE_API_BASE_URL);
try {
const url = new URL(raw);
url.hash = "";

View File

@@ -7,6 +7,7 @@ import type {
} from "openclaw/plugin-sdk/music-generation";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeGoogleApiBaseUrl } from "./api.js";
const DEFAULT_GOOGLE_MUSIC_MODEL = "lyria-3-clip-preview";
@@ -33,13 +34,13 @@ type GoogleGenerateMusicResponse = {
};
function resolveConfiguredGoogleMusicBaseUrl(req: MusicGenerationRequest): string | undefined {
const configured = req.cfg?.models?.providers?.google?.baseUrl?.trim();
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
}
function buildMusicPrompt(req: MusicGenerationRequest): string {
const parts = [req.prompt.trim()];
const lyrics = req.lyrics?.trim();
const lyrics = normalizeOptionalString(req.lyrics);
if (req.instrumental === true) {
parts.push("Instrumental only. No vocals, no sung lyrics, no spoken word.");
}
@@ -68,16 +69,20 @@ function extractTracks(params: { payload: GoogleGenerateMusicResponse; model: st
const tracks: GeneratedMusicAsset[] = [];
for (const candidate of params.payload.candidates ?? []) {
for (const part of candidate.content?.parts ?? []) {
if (part.text?.trim()) {
lyrics.push(part.text.trim());
const text = normalizeOptionalString(part.text);
if (text) {
lyrics.push(text);
continue;
}
const inline = part.inlineData ?? part.inline_data;
const data = inline?.data?.trim();
const data = normalizeOptionalString(inline?.data);
if (!data) {
continue;
}
const mimeType = inline?.mimeType?.trim() || inline?.mime_type?.trim() || "audio/mpeg";
const mimeType =
normalizeOptionalString(inline?.mimeType) ||
normalizeOptionalString(inline?.mime_type) ||
"audio/mpeg";
tracks.push({
buffer: Buffer.from(data, "base64"),
mimeType,
@@ -143,7 +148,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
throw new Error("Google API key missing");
}
const model = req.model?.trim() || DEFAULT_GOOGLE_MUSIC_MODEL;
const model = normalizeOptionalString(req.model) || DEFAULT_GOOGLE_MUSIC_MODEL;
if (req.format) {
const supportedFormats = resolveSupportedFormats(model);
if (!supportedFormats.includes(req.format)) {
@@ -168,7 +173,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
{ text: buildMusicPrompt(req) },
...(req.inputImages ?? []).map((image) => ({
inlineData: {
mimeType: image.mimeType?.trim() || "image/png",
mimeType: normalizeOptionalString(image.mimeType) || "image/png",
data: image.buffer?.toString("base64") ?? "",
},
})),
@@ -192,7 +197,7 @@ export function buildGoogleMusicGenerationProvider(): MusicGenerationProvider {
metadata: {
inputImageCount: req.inputImages?.length ?? 0,
instrumental: req.instrumental === true,
...(req.lyrics?.trim() ? { requestedLyrics: true } : {}),
...(normalizeOptionalString(req.lyrics) ? { requestedLyrics: true } : {}),
...(req.format ? { requestedFormat: req.format } : {}),
},
};

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { GoogleGenAI } from "@google/genai";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -21,12 +22,12 @@ const GOOGLE_VIDEO_MAX_DURATION_SECONDS =
GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS[GOOGLE_VIDEO_ALLOWED_DURATION_SECONDS.length - 1];
function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined {
const configured = req.cfg?.models?.providers?.google?.baseUrl?.trim();
const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
return configured ? normalizeGoogleApiBaseUrl(configured) : undefined;
}
function parseVideoSize(size: string | undefined): { width: number; height: number } | undefined {
const trimmed = size?.trim();
const trimmed = normalizeOptionalString(size);
if (!trimmed) {
return undefined;
}
@@ -46,7 +47,7 @@ function resolveAspectRatio(params: {
aspectRatio?: string;
size?: string;
}): "16:9" | "9:16" | undefined {
const direct = params.aspectRatio?.trim();
const direct = normalizeOptionalString(params.aspectRatio);
if (direct === "16:9" || direct === "9:16") {
return direct;
}
@@ -103,7 +104,7 @@ function resolveInputImage(req: VideoGenerationRequest) {
}
return {
imageBytes: input.buffer.toString("base64"),
mimeType: input.mimeType?.trim() || "image/png",
mimeType: normalizeOptionalString(input.mimeType) || "image/png",
};
}
@@ -114,7 +115,7 @@ function resolveInputVideo(req: VideoGenerationRequest) {
}
return {
videoBytes: input.buffer.toString("base64"),
mimeType: input.mimeType?.trim() || "video/mp4",
mimeType: normalizeOptionalString(input.mimeType) || "video/mp4",
};
}
@@ -230,7 +231,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
},
});
let operation = await client.models.generateVideos({
model: req.model?.trim() || DEFAULT_GOOGLE_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
prompt: req.prompt,
image: resolveInputImage(req),
video: resolveInputVideo(req),
@@ -267,7 +268,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
if (inline?.videoBytes) {
return {
buffer: Buffer.from(inline.videoBytes, "base64"),
mimeType: inline.mimeType?.trim() || "video/mp4",
mimeType: normalizeOptionalString(inline.mimeType) || "video/mp4",
fileName: `video-${index + 1}.mp4`,
};
}
@@ -283,7 +284,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
);
return {
videos,
model: req.model?.trim() || DEFAULT_GOOGLE_VIDEO_MODEL,
model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
metadata: operation.name
? {
operationName: operation.name,

View File

@@ -95,7 +95,7 @@ async function downloadTrackFromUrl(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated music download failed");
const mimeType = response.headers.get("content-type")?.trim() || "audio/mpeg";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "audio/mpeg";
const ext = extensionForMime(mimeType)?.replace(/^\./u, "") || "mp3";
return {
buffer: Buffer.from(await response.arrayBuffer()),
@@ -148,7 +148,7 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
if ((req.inputImages?.length ?? 0) > 0) {
throw new Error("MiniMax music generation does not support image reference inputs.");
}
if (req.instrumental === true && req.lyrics?.trim()) {
if (req.instrumental === true && normalizeOptionalString(req.lyrics)) {
throw new Error("MiniMax music generation cannot use lyrics when instrumental=true.");
}
if (req.format && req.format !== "mp3") {
@@ -179,7 +179,7 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
jsonHeaders.set("Content-Type", "application/json");
const model = resolveMinimaxMusicModel(req.model);
const lyrics = req.lyrics?.trim();
const lyrics = normalizeOptionalString(req.lyrics);
const body = {
model,
prompt: buildPrompt(req),
@@ -209,10 +209,11 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
const payload = (await res.json()) as MinimaxMusicCreateResponse;
assertMinimaxBaseResp(payload.base_resp, "MiniMax music generation failed");
const audioCandidate = payload.audio?.trim() || payload.data?.audio?.trim();
const audioCandidate =
normalizeOptionalString(payload.audio) ?? normalizeOptionalString(payload.data?.audio);
const audioUrl =
payload.audio_url?.trim() ||
payload.data?.audio_url?.trim() ||
normalizeOptionalString(payload.audio_url) ||
normalizeOptionalString(payload.data?.audio_url) ||
(isLikelyRemoteUrl(audioCandidate) ? audioCandidate : undefined);
const inlineAudio = isLikelyRemoteUrl(audioCandidate) ? undefined : audioCandidate;
const lyrics = decodePossibleText(payload.lyrics ?? payload.data?.lyrics ?? "");
@@ -239,7 +240,9 @@ export function buildMinimaxMusicGenerationProvider(): MusicGenerationProvider {
...(lyrics ? { lyrics: [lyrics] } : {}),
model,
metadata: {
...(payload.task_id?.trim() ? { taskId: payload.task_id.trim() } : {}),
...(normalizeOptionalString(payload.task_id)
? { taskId: normalizeOptionalString(payload.task_id) }
: {}),
...(audioUrl ? { audioUrl } : {}),
instrumental: req.instrumental === true,
...(lyrics ? { requestedLyrics: true } : {}),

View File

@@ -15,6 +15,7 @@ import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-aut
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
import type { MiniMaxRegion } from "./oauth.js";
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
@@ -82,15 +83,13 @@ function resolvePortalCatalog(ctx: ProviderCatalogContext) {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PORTAL_PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const explicitApiKey = normalizeOptionalString(explicitProvider?.apiKey);
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? MINIMAX_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : undefined;
const explicitBaseUrl = normalizeOptionalString(explicitProvider?.baseUrl);
return {
provider: buildPortalProviderCatalog({

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -51,7 +52,7 @@ type MinimaxFileRetrieveResponse = {
function resolveMinimaxVideoBaseUrl(
cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"],
): string {
const direct = cfg?.models?.providers?.minimax?.baseUrl?.trim();
const direct = normalizeOptionalString(cfg?.models?.providers?.minimax?.baseUrl);
if (!direct) {
return DEFAULT_MINIMAX_VIDEO_BASE_URL;
}
@@ -80,13 +81,14 @@ function resolveFirstFrameImage(req: VideoGenerationRequest): string | undefined
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("MiniMax image-to-video input is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
function resolveDurationSeconds(params: {
@@ -128,11 +130,14 @@ async function pollMinimaxVideo(params: {
await assertOkOrThrowHttpError(response, "MiniMax video status request failed");
const payload = (await response.json()) as MinimaxQueryResponse;
assertMinimaxBaseResp(payload.base_resp, "MiniMax video generation failed");
switch (payload.status?.trim()) {
switch (normalizeOptionalString(payload.status)) {
case "Success":
return payload;
case "Fail":
throw new Error(payload.base_resp?.status_msg?.trim() || "MiniMax video generation failed");
throw new Error(
normalizeOptionalString(payload.base_resp?.status_msg) ||
"MiniMax video generation failed",
);
case "Preparing":
case "Processing":
default:
@@ -155,7 +160,7 @@ async function downloadVideoFromUrl(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -188,7 +193,7 @@ async function downloadVideoFromFileId(params: {
);
const metadata = (await metadataResponse.json()) as MinimaxFileRetrieveResponse;
assertMinimaxBaseResp(metadata.base_resp, "MiniMax generated video metadata request failed");
const downloadUrl = metadata.file?.download_url?.trim();
const downloadUrl = normalizeOptionalString(metadata.file?.download_url);
if (!downloadUrl) {
throw new Error("MiniMax generated video metadata missing download_url");
}
@@ -199,13 +204,14 @@ async function downloadVideoFromFileId(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "MiniMax generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
mimeType,
fileName:
metadata.file?.filename?.trim() || `video-1.${mimeType.includes("webm") ? "webm" : "mp4"}`,
normalizeOptionalString(metadata.file?.filename) ||
`video-1.${mimeType.includes("webm") ? "webm" : "mp4"}`,
};
}
@@ -276,7 +282,7 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
capability: "video",
transport: "http",
});
const model = req.model?.trim() || DEFAULT_MINIMAX_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_MINIMAX_VIDEO_MODEL;
const body: Record<string, unknown> = {
model,
prompt: req.prompt,
@@ -308,7 +314,7 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
await assertOkOrThrowHttpError(response, "MiniMax video generation failed");
const submitted = (await response.json()) as MinimaxCreateResponse;
assertMinimaxBaseResp(submitted.base_resp, "MiniMax video generation failed");
const taskId = submitted.task_id?.trim();
const taskId = normalizeOptionalString(submitted.task_id);
if (!taskId) {
throw new Error("MiniMax video generation response missing task_id");
}
@@ -319,8 +325,8 @@ export function buildMinimaxVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const videoUrl = completed.video_url?.trim();
const fileId = completed.file_id?.trim();
const videoUrl = normalizeOptionalString(completed.video_url);
const fileId = normalizeOptionalString(completed.file_id);
const video = videoUrl
? await downloadVideoFromUrl({
url: videoUrl,

View File

@@ -4,6 +4,7 @@ import {
cloneFirstTemplateModel,
matchesExactOrPrefix,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
type SyntheticOpenAIModelCatalogEntry = {
provider: string;
@@ -22,11 +23,11 @@ export function toOpenAIDataUrl(buffer: Buffer, mimeType: string): string {
}
export function resolveConfiguredOpenAIBaseUrl(cfg: OpenClawConfig | undefined): string {
return cfg?.models?.providers?.openai?.baseUrl?.trim() || OPENAI_API_BASE_URL;
return normalizeOptionalString(cfg?.models?.providers?.openai?.baseUrl) ?? OPENAI_API_BASE_URL;
}
export function isOpenAIApiBaseUrl(baseUrl?: string): boolean {
const trimmed = baseUrl?.trim();
const trimmed = normalizeOptionalString(baseUrl);
if (!trimmed) {
return false;
}
@@ -34,7 +35,7 @@ export function isOpenAIApiBaseUrl(baseUrl?: string): boolean {
}
export function isOpenAICodexBaseUrl(baseUrl?: string): boolean {
const trimmed = baseUrl?.trim();
const trimmed = normalizeOptionalString(baseUrl);
if (!trimmed) {
return false;
}

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -58,14 +59,14 @@ function resolveSize(params: {
aspectRatio?: string;
resolution?: string;
}): (typeof OPENAI_VIDEO_SIZES)[number] | undefined {
const explicitSize = params.size?.trim();
const explicitSize = normalizeOptionalString(params.size);
if (
explicitSize &&
OPENAI_VIDEO_SIZES.includes(explicitSize as (typeof OPENAI_VIDEO_SIZES)[number])
) {
return explicitSize as (typeof OPENAI_VIDEO_SIZES)[number];
}
switch (params.aspectRatio?.trim()) {
switch (normalizeOptionalString(params.aspectRatio)) {
case "9:16":
return "720x1280";
case "16:9":
@@ -98,7 +99,8 @@ function resolveReferenceAsset(req: VideoGenerationRequest) {
);
}
const mimeType =
asset.mimeType?.trim() || ((req.inputVideos?.length ?? 0) > 0 ? "video/mp4" : "image/png");
normalizeOptionalString(asset.mimeType) ||
((req.inputVideos?.length ?? 0) > 0 ? "video/mp4" : "image/png");
const extension = mimeType.includes("video")
? "mp4"
: mimeType.includes("jpeg")
@@ -107,7 +109,7 @@ function resolveReferenceAsset(req: VideoGenerationRequest) {
? "webp"
: "png";
const fileName =
asset.fileName?.trim() ||
normalizeOptionalString(asset.fileName) ||
`${(req.inputVideos?.length ?? 0) > 0 ? "reference-video" : "reference-image"}.${extension}`;
return new File([toBlobBytes(asset.buffer)], fileName, { type: mimeType });
}
@@ -135,7 +137,9 @@ async function pollOpenAIVideo(params: {
return payload;
}
if (payload.status === "failed") {
throw new Error(payload.error?.message?.trim() || "OpenAI video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) || "OpenAI video generation failed",
);
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
@@ -164,7 +168,7 @@ async function downloadOpenAIVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "OpenAI video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -236,7 +240,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
transport: "http",
});
const model = req.model?.trim() || DEFAULT_OPENAI_VIDEO_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_OPENAI_VIDEO_MODEL;
const seconds = resolveDurationSeconds(req.durationSeconds);
const size = resolveSize({
size: req.size,
@@ -262,7 +266,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
input_reference: {
image_url: toOpenAIDataUrl(
inputImage.buffer,
inputImage.mimeType?.trim() || "image/png",
normalizeOptionalString(inputImage.mimeType) ?? "image/png",
),
},
},
@@ -322,7 +326,7 @@ export function buildOpenAIVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "OpenAI video generation failed");
const submitted = (await response.json()) as OpenAIVideoResponse;
const videoId = submitted.id?.trim();
const videoId = normalizeOptionalString(submitted.id);
if (!videoId) {
throw new Error("OpenAI video generation response missing video id");
}

View File

@@ -6,7 +6,10 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -50,7 +53,9 @@ const RUNWAY_TEXT_ASPECT_RATIOS = ["16:9", "9:16"] as const;
const RUNWAY_EDIT_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "3:4", "4:3", "21:9"] as const;
function resolveRunwayBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.runway?.baseUrl?.trim() || DEFAULT_RUNWAY_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.runway?.baseUrl) ?? DEFAULT_RUNWAY_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -64,14 +69,14 @@ function resolveSourceUri(
if (!asset) {
return undefined;
}
const url = asset.url?.trim();
const url = normalizeOptionalString(asset.url);
if (url) {
return url;
}
if (!asset.buffer) {
return undefined;
}
return toDataUrl(asset.buffer, asset.mimeType?.trim() || fallbackMimeType);
return toDataUrl(asset.buffer, normalizeOptionalString(asset.mimeType) ?? fallbackMimeType);
}
function resolveDurationSeconds(value: number | undefined): number {
@@ -84,9 +89,9 @@ function resolveDurationSeconds(value: number | undefined): number {
function resolveRunwayRatio(req: VideoGenerationRequest): string {
const hasImageInput = (req.inputImages?.length ?? 0) > 0;
const requested =
req.size?.trim() ||
normalizeOptionalString(req.size) ||
(() => {
switch (req.aspectRatio?.trim()) {
switch (normalizeOptionalString(req.aspectRatio)) {
case "9:16":
return "720:1280";
case "16:9":
@@ -136,7 +141,7 @@ function buildCreateBody(req: VideoGenerationRequest): Record<string, unknown> {
const endpoint = resolveEndpoint(req);
const duration = resolveDurationSeconds(req.durationSeconds);
const ratio = resolveRunwayRatio(req);
const model = req.model?.trim() || DEFAULT_RUNWAY_MODEL;
const model = normalizeOptionalString(req.model) ?? DEFAULT_RUNWAY_MODEL;
if (endpoint === "/v1/text_to_video") {
if (!TEXT_ONLY_MODELS.has(model)) {
throw new Error(
@@ -210,10 +215,9 @@ async function pollRunwayTask(params: {
case "FAILED":
case "CANCELLED":
throw new Error(
(typeof payload.failure === "string"
? payload.failure
: payload.failure?.message
)?.trim() || `Runway video generation ${normalizeLowercaseStringOrEmpty(payload.status)}`,
normalizeOptionalString(
typeof payload.failure === "string" ? payload.failure : payload.failure?.message,
) || `Runway video generation ${normalizeLowercaseStringOrEmpty(payload.status)}`,
);
case "PENDING":
case "RUNNING":
@@ -240,7 +244,7 @@ async function downloadRunwayVideos(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "Runway generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
videos.push({
buffer: Buffer.from(arrayBuffer),
@@ -325,7 +329,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "Runway video generation failed");
const submitted = (await response.json()) as RunwayTaskCreateResponse;
const taskId = submitted.id?.trim();
const taskId = normalizeOptionalString(submitted.id);
if (!taskId) {
throw new Error("Runway video generation response missing task id");
}
@@ -336,9 +340,9 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const outputUrls = completed.output?.filter(
(value) => typeof value === "string" && value.trim(),
);
const outputUrls = completed.output
?.map((value) => normalizeOptionalString(value))
.filter((value): value is string => Boolean(value));
if (!outputUrls?.length) {
throw new Error("Runway video generation completed without output URLs");
}
@@ -349,7 +353,7 @@ export function buildRunwayVideoGenerationProvider(): VideoGenerationProvider {
});
return {
videos,
model: req.model?.trim() || DEFAULT_RUNWAY_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_RUNWAY_MODEL,
metadata: {
taskId,
status: completed.status,

View File

@@ -6,6 +6,7 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -38,7 +39,9 @@ type TogetherVideoResponse = {
};
function resolveTogetherVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.together?.baseUrl?.trim() || TOGETHER_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.together?.baseUrl) ?? TOGETHER_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -48,14 +51,17 @@ function toDataUrl(buffer: Buffer, mimeType: string): string {
function extractTogetherVideoUrl(payload: TogetherVideoResponse): string | undefined {
if (Array.isArray(payload.outputs)) {
for (const entry of payload.outputs) {
const url = entry.video_url?.trim() || entry.url?.trim();
const url = normalizeOptionalString(entry.video_url) ?? normalizeOptionalString(entry.url);
if (url) {
return url;
}
}
return undefined;
}
return payload.outputs?.video_url?.trim() || payload.outputs?.url?.trim();
return (
normalizeOptionalString(payload.outputs?.video_url) ??
normalizeOptionalString(payload.outputs?.url)
);
}
async function pollTogetherVideo(params: {
@@ -81,7 +87,9 @@ async function pollTogetherVideo(params: {
return payload;
}
if (payload.status === "failed") {
throw new Error(payload.error?.message?.trim() || "Together video generation failed");
throw new Error(
normalizeOptionalString(payload.error?.message) ?? "Together video generation failed",
);
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
@@ -100,7 +108,7 @@ async function downloadTogetherVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "Together generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -171,14 +179,15 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
transport: "http",
});
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_TOGETHER_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_TOGETHER_VIDEO_MODEL,
prompt: req.prompt,
};
if (typeof req.durationSeconds === "number" && Number.isFinite(req.durationSeconds)) {
body.seconds = String(Math.max(1, Math.round(req.durationSeconds)));
}
if (req.size?.trim()) {
const match = /^(\d+)x(\d+)$/u.exec(req.size.trim());
const size = normalizeOptionalString(req.size);
if (size) {
const match = /^(\d+)x(\d+)$/u.exec(size);
if (match) {
body.width = Number.parseInt(match[1] ?? "", 10);
body.height = Number.parseInt(match[2] ?? "", 10);
@@ -186,10 +195,10 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
}
if (req.inputImages?.[0]) {
const input = req.inputImages[0];
const value = input.url?.trim()
? input.url.trim()
const value = normalizeOptionalString(input.url)
? normalizeOptionalString(input.url)
: input.buffer
? toDataUrl(input.buffer, input.mimeType?.trim() || "image/png")
? toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png")
: undefined;
if (!value) {
throw new Error("Together reference image is missing image data.");
@@ -208,7 +217,7 @@ export function buildTogetherVideoGenerationProvider(): VideoGenerationProvider
try {
await assertOkOrThrowHttpError(response, "Together video generation failed");
const submitted = (await response.json()) as TogetherVideoResponse;
const videoId = submitted.id?.trim();
const videoId = normalizeOptionalString(submitted.id);
if (!videoId) {
throw new Error("Together video generation response missing id");
}

View File

@@ -47,7 +47,9 @@ type VideoGenerationSourceInput = {
};
function resolveXaiVideoBaseUrl(req: VideoGenerationRequest): string {
return req.cfg?.models?.providers?.xai?.baseUrl?.trim() || DEFAULT_XAI_VIDEO_BASE_URL;
return (
normalizeOptionalString(req.cfg?.models?.providers?.xai?.baseUrl) ?? DEFAULT_XAI_VIDEO_BASE_URL
);
}
function toDataUrl(buffer: Buffer, mimeType: string): string {
@@ -58,20 +60,21 @@ function resolveImageUrl(input: VideoGenerationSourceInput | undefined): string
if (!input) {
return undefined;
}
if (input.url?.trim()) {
return input.url.trim();
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) {
return inputUrl;
}
if (!input.buffer) {
throw new Error("xAI image-to-video input is missing image data.");
}
return toDataUrl(input.buffer, input.mimeType?.trim() || "image/png");
return toDataUrl(input.buffer, normalizeOptionalString(input.mimeType) ?? "image/png");
}
function resolveInputVideoUrl(input: VideoGenerationSourceInput | undefined): string | undefined {
if (!input) {
return undefined;
}
const url = input.url?.trim();
const url = normalizeOptionalString(input.url);
if (url) {
return url;
}
@@ -138,7 +141,7 @@ function buildCreateBody(req: VideoGenerationRequest): Record<string, unknown> {
const mode = resolveXaiVideoMode(req);
const body: Record<string, unknown> = {
model: req.model?.trim() || DEFAULT_XAI_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_XAI_VIDEO_MODEL,
prompt: req.prompt,
};
@@ -216,7 +219,10 @@ async function pollXaiVideo(params: {
return payload;
case "failed":
case "expired":
throw new Error(payload.error?.message?.trim() || `xAI video generation ${payload.status}`);
throw new Error(
normalizeOptionalString(payload.error?.message) ??
`xAI video generation ${payload.status}`,
);
case "queued":
case "processing":
default:
@@ -239,7 +245,7 @@ async function downloadXaiVideo(params: {
params.fetchFn,
);
await assertOkOrThrowHttpError(response, "xAI generated video download failed");
const mimeType = response.headers.get("content-type")?.trim() || "video/mp4";
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
const arrayBuffer = await response.arrayBuffer();
return {
buffer: Buffer.from(arrayBuffer),
@@ -324,10 +330,11 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
try {
await assertOkOrThrowHttpError(response, "xAI video generation failed");
const submitted = (await response.json()) as XaiVideoCreateResponse;
const requestId = submitted.request_id?.trim();
const requestId = normalizeOptionalString(submitted.request_id);
if (!requestId) {
throw new Error(
submitted.error?.message?.trim() || "xAI video generation response missing request_id",
normalizeOptionalString(submitted.error?.message) ??
"xAI video generation response missing request_id",
);
}
const completed = await pollXaiVideo({
@@ -337,7 +344,7 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
baseUrl,
fetchFn,
});
const videoUrl = completed.video?.url?.trim();
const videoUrl = normalizeOptionalString(completed.video?.url);
if (!videoUrl) {
throw new Error("xAI video generation completed without an output URL");
}
@@ -348,7 +355,7 @@ export function buildXaiVideoGenerationProvider(): VideoGenerationProvider {
});
return {
videos: [video],
model: req.model?.trim() || DEFAULT_XAI_VIDEO_MODEL,
model: normalizeOptionalString(req.model) ?? DEFAULT_XAI_VIDEO_MODEL,
metadata: {
requestId,
status: completed.status,

View File

@@ -16,7 +16,7 @@ import { loadSessions } from "./controllers/sessions.ts";
import { icons } from "./icons.ts";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts";
import { parseAgentSessionKey } from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
import type { ThemeMode } from "./theme.ts";
import {
listThinkingLevelLabels,
@@ -34,11 +34,11 @@ function resolveSidebarChatSessionKey(state: AppViewState): string {
const snapshot = state.hello?.snapshot as
| { sessionDefaults?: SessionDefaultsSnapshot }
| undefined;
const mainSessionKey = snapshot?.sessionDefaults?.mainSessionKey?.trim();
const mainSessionKey = normalizeOptionalString(snapshot?.sessionDefaults?.mainSessionKey);
if (mainSessionKey) {
return mainSessionKey;
}
const mainKey = snapshot?.sessionDefaults?.mainKey?.trim();
const mainKey = normalizeOptionalString(snapshot?.sessionDefaults?.mainKey);
if (mainKey) {
return mainKey;
}
@@ -857,8 +857,8 @@ export function resolveSessionDisplayName(
key: string,
row?: SessionsListResult["sessions"][number],
): string {
const label = row?.label?.trim() || "";
const displayName = row?.displayName?.trim() || "";
const label = normalizeOptionalString(row?.label) ?? "";
const displayName = normalizeOptionalString(row?.displayName) ?? "";
const { prefix, fallbackName } = parseSessionKey(key);
const applyTypedPrefix = (name: string): string => {
@@ -951,7 +951,7 @@ export function resolveSessionOptionGroups(
resolveAgentGroupLabel(state, parsed.agentId),
)
: ensureGroup("other", "Other Sessions");
const scopeLabel = parsed?.rest?.trim() || key;
const scopeLabel = normalizeOptionalString(parsed?.rest) ?? key;
const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
group.options.push({
key,
@@ -1065,7 +1065,8 @@ function resolveAgentGroupLabel(state: AppViewState, agentIdRaw: string): string
const agent = (state.agentsList?.agents ?? []).find(
(entry) => normalizeLowercaseStringOrEmpty(entry.id) === normalized,
);
const name = agent?.identity?.name?.trim() || agent?.name?.trim() || "";
const name =
normalizeOptionalString(agent?.identity?.name) ?? normalizeOptionalString(agent?.name) ?? "";
return name && name !== agentIdRaw ? `${name} (${agentIdRaw})` : agentIdRaw;
}
@@ -1074,13 +1075,13 @@ function resolveSessionScopedOptionLabel(
row?: SessionsListResult["sessions"][number],
rest?: string,
) {
const base = rest?.trim() || key;
const base = normalizeOptionalString(rest) ?? key;
if (!row) {
return base;
}
const label = row.label?.trim() || "";
const displayName = row.displayName?.trim() || "";
const label = normalizeOptionalString(row.label) ?? "";
const displayName = normalizeOptionalString(row.displayName) ?? "";
if ((label && label !== key) || (displayName && displayName !== key)) {
return resolveSessionDisplayName(key, row);
}

View File

@@ -109,7 +109,11 @@ import {
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "./string-coerce.ts";
import { agentLogoUrl } from "./views/agents-utils.ts";
import {
resolveAgentConfig,
@@ -208,7 +212,7 @@ function isHttpUrl(value: string): boolean {
}
function normalizeSuggestionValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
function uniquePreserveOrder(values: string[]): string[] {
@@ -408,9 +412,7 @@ export function renderApp(state: AppViewState) {
new Set(
[
...(state.agentsList?.agents?.map((entry) => entry.id.trim()) ?? []),
...state.cronJobs
.map((job) => (typeof job.agentId === "string" ? job.agentId.trim() : ""))
.filter(Boolean),
...state.cronJobs.map((job) => normalizeOptionalString(job.agentId) ?? "").filter(Boolean),
].filter(Boolean),
),
);
@@ -424,7 +426,7 @@ export function renderApp(state: AppViewState) {
if (job.payload.kind !== "agentTurn" || typeof job.payload.model !== "string") {
return "";
}
return job.payload.model.trim();
return normalizeOptionalString(job.payload.model) ?? "";
})
.filter(Boolean),
].filter(Boolean),
@@ -1289,7 +1291,7 @@ export function renderApp(state: AppViewState) {
const entry = Array.isArray(list)
? (list[index] as { skills?: unknown })
: undefined;
const normalizedSkill = skillName.trim();
const normalizedSkill = normalizeOptionalString(skillName) ?? "";
if (!normalizedSkill) {
return;
}
@@ -1297,7 +1299,9 @@ export function renderApp(state: AppViewState) {
state.agentSkillsReport?.skills?.map((skill) => skill.name).filter(Boolean) ??
[];
const existing = Array.isArray(entry?.skills)
? entry.skills.map((name) => String(name).trim()).filter(Boolean)
? entry.skills
.map((name) => normalizeStringifiedOptionalString(name) ?? "")
.filter(Boolean)
: undefined;
const base = existing ?? allSkills;
const next = new Set(base);

View File

@@ -6,6 +6,16 @@ export function normalizeOptionalString(value: unknown): string | undefined {
return trimmed ? trimmed : undefined;
}
export function normalizeStringifiedOptionalString(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOptionalString(value);
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return normalizeOptionalString(String(value));
}
return undefined;
}
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
return normalizeOptionalString(value)?.toLowerCase();
}

View File

@@ -4,7 +4,7 @@ import {
normalizeToolName,
resolveToolProfilePolicy,
} from "../../../../src/agents/tool-policy-shared.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts";
import type {
AgentIdentityResult,
AgentsFilesListResult,
@@ -193,7 +193,9 @@ export function normalizeAgentLabel(agent: {
name?: string;
identity?: { name?: string };
}) {
return agent.name?.trim() || agent.identity?.name?.trim() || agent.id;
return (
normalizeOptionalString(agent.name) ?? normalizeOptionalString(agent.identity?.name) ?? agent.id
);
}
const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|\/)/i;
@@ -203,9 +205,9 @@ export function resolveAgentAvatarUrl(
agentIdentity?: AgentIdentityResult | null,
): string | null {
const candidates = [
agentIdentity?.avatar?.trim(),
agent.identity?.avatarUrl?.trim(),
agent.identity?.avatar?.trim(),
normalizeOptionalString(agentIdentity?.avatar),
normalizeOptionalString(agent.identity?.avatarUrl),
normalizeOptionalString(agent.identity?.avatar),
];
for (const candidate of candidates) {
if (!candidate) {
@@ -219,7 +221,7 @@ export function resolveAgentAvatarUrl(
}
export function agentLogoUrl(basePath: string): string {
const base = basePath?.trim() ? basePath.replace(/\/$/, "") : "";
const base = normalizeOptionalString(basePath)?.replace(/\/$/, "") ?? "";
return base ? `${base}/favicon.svg` : "favicon.svg";
}
@@ -251,19 +253,19 @@ export function resolveAgentEmoji(
agent: { identity?: { emoji?: string; avatar?: string } },
agentIdentity?: AgentIdentityResult | null,
) {
const identityEmoji = agentIdentity?.emoji?.trim();
const identityEmoji = normalizeOptionalString(agentIdentity?.emoji);
if (identityEmoji && isLikelyEmoji(identityEmoji)) {
return identityEmoji;
}
const agentEmoji = agent.identity?.emoji?.trim();
const agentEmoji = normalizeOptionalString(agent.identity?.emoji);
if (agentEmoji && isLikelyEmoji(agentEmoji)) {
return agentEmoji;
}
const identityAvatar = agentIdentity?.avatar?.trim();
const identityAvatar = normalizeOptionalString(agentIdentity?.avatar);
if (identityAvatar && isLikelyEmoji(identityAvatar)) {
return identityAvatar;
}
const avatar = agent.identity?.avatar?.trim();
const avatar = normalizeOptionalString(agent.identity?.avatar);
if (avatar && isLikelyEmoji(avatar)) {
return avatar;
}
@@ -341,9 +343,9 @@ export function buildAgentContext(
? resolveModelLabel(config.defaults?.model)
: resolveModelLabel(agent.model);
const identityName =
agentIdentity?.name?.trim() ||
agent.identity?.name?.trim() ||
agent.name?.trim() ||
normalizeOptionalString(agentIdentity?.name) ||
normalizeOptionalString(agent.identity?.name) ||
normalizeOptionalString(agent.name) ||
config.entry?.name ||
agent.id;
const identityAvatar = resolveAgentAvatarUrl(agent, agentIdentity) ? "custom" : "—";
@@ -364,11 +366,11 @@ export function resolveModelLabel(model?: unknown): string {
return "-";
}
if (typeof model === "string") {
return model.trim() || "-";
return normalizeOptionalString(model) || "-";
}
if (typeof model === "object" && model) {
const record = model as { primary?: string; fallbacks?: string[] };
const primary = record.primary?.trim();
const primary = normalizeOptionalString(record.primary);
if (primary) {
const fallbackCount = Array.isArray(record.fallbacks) ? record.fallbacks.length : 0;
return fallbackCount > 0 ? `${primary} (+${fallbackCount} fallback)` : primary;
@@ -387,7 +389,7 @@ export function resolveModelPrimary(model?: unknown): string | null {
return null;
}
if (typeof model === "string") {
const trimmed = model.trim();
const trimmed = normalizeOptionalString(model);
return trimmed || null;
}
if (typeof model === "object" && model) {
@@ -402,7 +404,7 @@ export function resolveModelPrimary(model?: unknown): string | null {
: typeof record.value === "string"
? record.value
: null;
const primary = candidate?.trim();
const primary = normalizeOptionalString(candidate);
return primary || null;
}
return null;

View File

@@ -8,6 +8,7 @@ import type {
} from "../controllers/devices.ts";
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "../controllers/exec-approvals.ts";
import { formatRelativeTimestamp, formatList } from "../format.ts";
import { normalizeOptionalString } from "../string-coerce.ts";
import { renderExecApprovals, resolveExecApprovalsState } from "./nodes-exec-approvals.ts";
import { resolveConfigAgents, resolveNodeTargets, type NodeTargetOption } from "./nodes-shared.ts";
export type NodesProps = {
@@ -111,9 +112,9 @@ function renderDevices(props: NodesProps) {
}
function renderPendingDevice(req: PendingDevice, props: NodesProps) {
const name = req.displayName?.trim() || req.deviceId;
const name = normalizeOptionalString(req.displayName) || req.deviceId;
const age = typeof req.ts === "number" ? formatRelativeTimestamp(req.ts) : t("common.na");
const roleValue = req.role?.trim() || formatList(req.roles);
const roleValue = normalizeOptionalString(req.role) || formatList(req.roles);
const scopesValue = formatList(req.scopes);
const repair = req.isRepair ? " · repair" : "";
const ip = req.remoteIp ? ` · ${req.remoteIp}` : "";
@@ -141,7 +142,7 @@ function renderPendingDevice(req: PendingDevice, props: NodesProps) {
}
function renderPairedDevice(device: PairedDevice, props: NodesProps) {
const name = device.displayName?.trim() || device.deviceId;
const name = normalizeOptionalString(device.displayName) || device.deviceId;
const ip = device.remoteIp ? ` · ${device.remoteIp}` : "";
const roles = `roles: ${formatList(device.roles)}`;
const scopes = `scopes: ${formatList(device.scopes)}`;