feat: add in-app LM Remote configuration
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
export class RemoteConfigFormError extends Error {
|
||||
constructor(field, message) {
|
||||
super(message);
|
||||
this.name = "RemoteConfigFormError";
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTimeout(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
throw new RemoteConfigFormError("timeout", "Timeout must be a whole number.");
|
||||
}
|
||||
const timeout = Number(text);
|
||||
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 300) {
|
||||
throw new RemoteConfigFormError(
|
||||
"timeout",
|
||||
"Timeout must be between 1 and 300 seconds."
|
||||
);
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
function normalizeRemoteUrl(value, allowEmpty) {
|
||||
const remoteUrl = String(value ?? "").trim();
|
||||
if (!remoteUrl) {
|
||||
if (allowEmpty) return "";
|
||||
throw new RemoteConfigFormError(
|
||||
"remote_url",
|
||||
"Enter a remote LoRA Manager URL."
|
||||
);
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(remoteUrl);
|
||||
} catch {
|
||||
throw new RemoteConfigFormError("remote_url", "Enter a valid remote URL.");
|
||||
}
|
||||
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||
throw new RemoteConfigFormError(
|
||||
"remote_url",
|
||||
"Remote URL must use http:// or https://."
|
||||
);
|
||||
}
|
||||
if (!parsed.hostname) {
|
||||
throw new RemoteConfigFormError(
|
||||
"remote_url",
|
||||
"Remote URL must include a host name."
|
||||
);
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
throw new RemoteConfigFormError(
|
||||
"remote_url",
|
||||
"Credentials are not allowed in the remote URL."
|
||||
);
|
||||
}
|
||||
if (parsed.search || parsed.hash) {
|
||||
throw new RemoteConfigFormError(
|
||||
"remote_url",
|
||||
"Remote URL cannot contain a query or fragment."
|
||||
);
|
||||
}
|
||||
return remoteUrl.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function buildMappings(rows) {
|
||||
const pathMappings = new Map();
|
||||
for (const row of rows || []) {
|
||||
const remote = String(row?.remote ?? "").trim();
|
||||
const local = String(row?.local ?? "").trim();
|
||||
if (!remote && !local) continue;
|
||||
if (!remote || !local) {
|
||||
throw new RemoteConfigFormError(
|
||||
"path_mappings",
|
||||
"Each path mapping needs both a remote and local path."
|
||||
);
|
||||
}
|
||||
const normalizedRemote = remote.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
||||
if (pathMappings.has(normalizedRemote)) {
|
||||
throw new RemoteConfigFormError(
|
||||
"path_mappings",
|
||||
`Duplicate remote path prefix: ${normalizedRemote}`
|
||||
);
|
||||
}
|
||||
pathMappings.set(normalizedRemote, local);
|
||||
}
|
||||
return Object.fromEntries(pathMappings);
|
||||
}
|
||||
|
||||
export function mappingsToRows(pathMappings) {
|
||||
if (!pathMappings || typeof pathMappings !== "object") return [];
|
||||
return Object.entries(pathMappings).map(([remote, local]) => ({
|
||||
remote: String(remote),
|
||||
local: String(local),
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildConfigDraft({ remoteUrl, timeout, mappingRows }) {
|
||||
return {
|
||||
remote_url: normalizeRemoteUrl(remoteUrl, true),
|
||||
timeout: normalizeTimeout(timeout),
|
||||
path_mappings: buildMappings(mappingRows),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildConnectionDraft({
|
||||
remoteUrl,
|
||||
timeout,
|
||||
effective,
|
||||
overrides,
|
||||
}) {
|
||||
return {
|
||||
remote_url: normalizeRemoteUrl(
|
||||
overrides?.remote_url ? effective?.remote_url : remoteUrl,
|
||||
false
|
||||
),
|
||||
timeout: normalizeTimeout(
|
||||
overrides?.timeout ? effective?.timeout : timeout
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function didEffectiveRemoteUrlChange(previousPayload, nextPayload) {
|
||||
const previous = String(previousPayload?.effective?.remote_url || "");
|
||||
const next = String(nextPayload?.effective?.remote_url || "");
|
||||
return previous !== next;
|
||||
}
|
||||
|
||||
export function isConfigWritable(payload) {
|
||||
return payload?.storage?.writable !== false;
|
||||
}
|
||||
Reference in New Issue
Block a user