326 lines
9.9 KiB
JavaScript
326 lines
9.9 KiB
JavaScript
export const DEFAULT_PACKAGE_BASE_URL = normalizeUrl(
|
|
import.meta.env.VITE_PACKAGE_BASE_URL || window.location.origin
|
|
);
|
|
export const DEFAULT_AGENT_BASE_URL = normalizeUrl(
|
|
import.meta.env.VITE_AGENT_BASE_URL || 'http://127.0.0.1:5010'
|
|
);
|
|
export const DEFAULT_APP_OPEN_URL = normalizeUrl(
|
|
import.meta.env.VITE_APP_OPEN_URL || 'http://127.0.0.1'
|
|
);
|
|
|
|
export function normalizeUrl(value) {
|
|
const text = String(value || '').trim();
|
|
return text.replace(/\/+$/, '');
|
|
}
|
|
|
|
export function joinUrl(baseUrl, path) {
|
|
const normalizedBaseUrl = normalizeUrl(baseUrl);
|
|
const normalizedPath = String(path || '').startsWith('/') ? path : `/${path || ''}`;
|
|
return `${normalizedBaseUrl}${normalizedPath}`;
|
|
}
|
|
|
|
function normalizeOpenUrl(value) {
|
|
const text = normalizeUrl(value);
|
|
if (!text) return '';
|
|
|
|
try {
|
|
const parsed = new URL(text);
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? text : '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function getAppOpenUrl(app) {
|
|
return normalizeOpenUrl(
|
|
app?.openUrl
|
|
|| app?.open_url
|
|
|| app?.webUrl
|
|
|| app?.web_url
|
|
|| app?.homepageUrl
|
|
|| app?.homepage_url
|
|
|| app?.homepage
|
|
) || normalizeOpenUrl(DEFAULT_APP_OPEN_URL);
|
|
}
|
|
|
|
async function requestJson(baseUrl, path, options = {}) {
|
|
const {
|
|
timeoutMs = 8000,
|
|
body,
|
|
headers,
|
|
...fetchOptions
|
|
} = options;
|
|
const url = joinUrl(baseUrl, path);
|
|
const controller = new AbortController();
|
|
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...fetchOptions,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
...headers
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
signal: controller.signal
|
|
});
|
|
|
|
const text = await response.text();
|
|
let payload = null;
|
|
if (text) {
|
|
try {
|
|
payload = JSON.parse(text);
|
|
} catch {
|
|
payload = text;
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`${response.status} ${formatErrorDetail(payload || response.statusText)}`);
|
|
}
|
|
|
|
return payload;
|
|
} catch (error) {
|
|
if (error?.name === 'AbortError') {
|
|
throw new Error(`Request timeout: ${url}`);
|
|
}
|
|
if (error instanceof TypeError) {
|
|
throw new Error(`Cannot fetch ${url}. Check endpoint reachability and CORS for this Web Client origin.`);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
window.clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
function formatErrorDetail(detail) {
|
|
if (Array.isArray(detail)) {
|
|
return detail.map(formatErrorDetail).filter(Boolean).join('; ');
|
|
}
|
|
|
|
if (detail && typeof detail === 'object') {
|
|
const location = Array.isArray(detail.loc) ? detail.loc.join('.') : '';
|
|
const messageParts = [
|
|
detail.msg,
|
|
detail.message,
|
|
detail.error,
|
|
detail.detail
|
|
]
|
|
.map((item) => String(item || '').trim())
|
|
.filter(Boolean);
|
|
|
|
if (Array.isArray(detail.missingPackageFiles) && detail.missingPackageFiles.length > 0) {
|
|
const missingFiles = detail.missingPackageFiles
|
|
.map(formatMissingPackageFile)
|
|
.filter(Boolean)
|
|
.join('; ');
|
|
|
|
if (missingFiles) {
|
|
messageParts.push(`Missing package files: ${missingFiles}`);
|
|
}
|
|
}
|
|
|
|
const message = [...new Set(messageParts)].join('. ');
|
|
|
|
if (message) {
|
|
return location ? `${location}: ${message}` : String(message);
|
|
}
|
|
|
|
try {
|
|
return JSON.stringify(detail);
|
|
} catch {
|
|
return String(detail);
|
|
}
|
|
}
|
|
|
|
return String(detail || 'Request failed');
|
|
}
|
|
|
|
function formatMissingPackageFile(item) {
|
|
if (!item || typeof item !== 'object') return String(item || '').trim();
|
|
|
|
const packageName = String(item.packageName || item.componentId || 'package').trim();
|
|
const version = String(item.version || '').trim();
|
|
const downloadUrl = String(item.downloadUrl || '').trim();
|
|
const label = [packageName, version].filter(Boolean).join(' ');
|
|
|
|
return downloadUrl ? `${label} (${downloadUrl})` : label;
|
|
}
|
|
|
|
export async function fetchPackageApps(packageBaseUrl) {
|
|
const payload = await requestJson(packageBaseUrl, '/api/apps', { timeoutMs: 10000 });
|
|
return Array.isArray(payload?.apps) ? payload.apps.map(normalizePackageApp) : [];
|
|
}
|
|
|
|
export async function fetchLatestAgentPackage(packageBaseUrl, arch = 'amd64') {
|
|
const query = arch ? `?arch=${encodeURIComponent(arch)}` : '';
|
|
return normalizeLatestAgentPackage(
|
|
await requestJson(packageBaseUrl, `/api/agent/latest${query}`, { timeoutMs: 7000 })
|
|
);
|
|
}
|
|
|
|
export async function fetchApplicationDetail(packageBaseUrl, appId) {
|
|
return requestJson(packageBaseUrl, `/api/apps/${encodeURIComponent(appId)}`, { timeoutMs: 10000 });
|
|
}
|
|
|
|
export async function fetchApplicationManifest(packageBaseUrl, appId, version) {
|
|
return requestJson(
|
|
packageBaseUrl,
|
|
`/api/apps/${encodeURIComponent(appId)}/versions/${encodeURIComponent(version)}/manifest`,
|
|
{ timeoutMs: 10000 }
|
|
);
|
|
}
|
|
|
|
export async function fetchAgentHealth(agentBaseUrl) {
|
|
return requestJson(agentBaseUrl, '/health', { timeoutMs: 2800 });
|
|
}
|
|
|
|
export async function fetchAgentSystemInfo(agentBaseUrl) {
|
|
return requestJson(agentBaseUrl, '/system-info', { timeoutMs: 5000 });
|
|
}
|
|
|
|
export async function fetchInstalledApps(agentBaseUrl) {
|
|
const payload = await requestJson(agentBaseUrl, '/apps/installed', { timeoutMs: 7000 });
|
|
return Array.isArray(payload) ? payload.map(normalizeInstalledApp) : [];
|
|
}
|
|
|
|
export async function queueInstall(agentBaseUrl, app) {
|
|
return requestJson(agentBaseUrl, '/apps/install', {
|
|
method: 'POST',
|
|
timeoutMs: 10000,
|
|
body: {
|
|
appId: app.appId,
|
|
appName: app.appName,
|
|
version: app.version
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function queueUpdate(agentBaseUrl, app, installedApp) {
|
|
return requestJson(agentBaseUrl, '/apps/update', {
|
|
method: 'POST',
|
|
timeoutMs: 10000,
|
|
body: {
|
|
appId: app.appId,
|
|
appName: app.appName,
|
|
currentVersion: installedApp?.version || '',
|
|
targetVersion: app.version
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function queueRemove(agentBaseUrl, app) {
|
|
return requestJson(agentBaseUrl, '/apps/remove', {
|
|
method: 'POST',
|
|
timeoutMs: 10000,
|
|
body: {
|
|
appId: app.appId,
|
|
purge: false
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function fetchTaskStatus(agentBaseUrl, taskId) {
|
|
return normalizeTask(await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}`, { timeoutMs: 7000 }));
|
|
}
|
|
|
|
export async function fetchTaskLogs(agentBaseUrl, taskId) {
|
|
const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/logs`, { timeoutMs: 7000 });
|
|
return Array.isArray(payload?.logs) ? payload.logs.map(normalizeLog) : [];
|
|
}
|
|
|
|
export async function fetchTaskComponents(agentBaseUrl, taskId) {
|
|
const payload = await requestJson(agentBaseUrl, `/tasks/${encodeURIComponent(taskId)}/components`, { timeoutMs: 7000 });
|
|
return Array.isArray(payload?.components) ? payload.components.map(normalizeComponent) : [];
|
|
}
|
|
|
|
function normalizePackageApp(app) {
|
|
return {
|
|
appId: String(app.appId || app.app_id || app.id || '').trim(),
|
|
appCode: String(app.appCode || app.app_code || app.code || app.appId || app.app_id || '').trim(),
|
|
appName: String(app.appName || app.app_name || app.name || '').trim(),
|
|
version: String(app.version || '').trim(),
|
|
status: String(app.status || 'Released').trim(),
|
|
packageCount: Number(app.packageCount || app.package_count || 0),
|
|
openUrl: normalizeOpenUrl(
|
|
app.openUrl
|
|
|| app.open_url
|
|
|| app.webUrl
|
|
|| app.web_url
|
|
|| app.homepageUrl
|
|
|| app.homepage_url
|
|
|| app.homepage
|
|
)
|
|
};
|
|
}
|
|
|
|
function normalizeInstalledApp(app) {
|
|
return {
|
|
appId: String(app.appId || app.app_id || '').trim(),
|
|
appName: String(app.appName || app.app_name || '').trim(),
|
|
version: String(app.installedVersion || app.version || app.package_version || '').trim(),
|
|
status: String(app.status || 'installed').trim(),
|
|
installedAt: app.installedAt || app.installed_at || '',
|
|
updatedAt: app.updatedAt || app.updated_at || '',
|
|
openUrl: normalizeOpenUrl(
|
|
app.openUrl
|
|
|| app.open_url
|
|
|| app.webUrl
|
|
|| app.web_url
|
|
|| app.homepageUrl
|
|
|| app.homepage_url
|
|
|| app.homepage
|
|
)
|
|
};
|
|
}
|
|
|
|
function normalizeLatestAgentPackage(agentPackage) {
|
|
return {
|
|
version: String(agentPackage?.version || '').trim(),
|
|
arch: String(agentPackage?.arch || '').trim(),
|
|
fileName: String(agentPackage?.fileName || agentPackage?.file_name || '').trim(),
|
|
sizeLabel: String(agentPackage?.sizeLabel || agentPackage?.size_label || '').trim(),
|
|
downloadUrl: String(agentPackage?.downloadUrl || agentPackage?.download_url || '').trim(),
|
|
installCommand: String(agentPackage?.installCommand || agentPackage?.install_command || '').trim()
|
|
};
|
|
}
|
|
|
|
function normalizeTask(task) {
|
|
return {
|
|
taskId: task.taskId || task.task_id,
|
|
type: task.type,
|
|
appId: task.appId || task.app_id,
|
|
appName: task.appName || task.app_name,
|
|
status: task.status,
|
|
progress: Number(task.progress || 0),
|
|
currentStep: task.currentStep || task.current_step,
|
|
currentComponentId: task.currentComponentId || task.current_component_id,
|
|
errorMessage: task.errorMessage || task.error_message,
|
|
createdAt: task.createdAt || task.created_at,
|
|
startedAt: task.startedAt || task.started_at,
|
|
finishedAt: task.finishedAt || task.finished_at
|
|
};
|
|
}
|
|
|
|
function normalizeLog(log) {
|
|
return {
|
|
time: log.time || log.timestamp || '',
|
|
level: log.level || 'info',
|
|
message: log.message || ''
|
|
};
|
|
}
|
|
|
|
function normalizeComponent(component) {
|
|
return {
|
|
componentId: component.componentId || component.component_id,
|
|
type: component.type,
|
|
status: component.status,
|
|
progress: Number(component.progress || 0),
|
|
currentStep: component.currentStep || component.current_step,
|
|
errorMessage: component.errorMessage || component.error_message,
|
|
startedAt: component.startedAt || component.started_at,
|
|
finishedAt: component.finishedAt || component.finished_at
|
|
};
|
|
}
|