mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
refactor: dedupe provider ui trimmed readers
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user