feat: add resource monitoring and provider upgrade

This commit is contained in:
Codex
2026-05-04 13:04:39 +00:00
parent 8726611b6f
commit 2a03f6343c
27 changed files with 1807 additions and 130 deletions
+110 -2
View File
@@ -71,9 +71,13 @@ function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: str
return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode };
}
function dockerCoreJson(path: string): unknown {
function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown {
const code = `
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)});
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
})});
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
@@ -88,6 +92,20 @@ function dockerCoreJson(path: string): unknown {
}
}
async function waitForTaskStatus(taskId: string, expected: string, timeoutMs = 10_000): Promise<unknown> {
const started = Date.now();
let latest: unknown = null;
while (Date.now() - started < timeoutMs) {
latest = dockerCoreJson("/api/tasks?limit=40");
const tasks = (latest as { body?: { tasks?: Array<{ id?: string; status?: string; result?: unknown }> } }).body?.tasks ?? [];
const task = tasks.find((item) => item.id === taskId);
if (task?.status === expected) return { ok: true, task };
if (task?.status === "failed") return { ok: false, task };
await Bun.sleep(500);
}
return { ok: false, timeoutMs, latest };
}
function dockerPortSummary(): unknown {
const result = runCommand([
"docker",
@@ -108,6 +126,54 @@ function dockerPortSummary(): unknown {
};
}
function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown {
const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string }> } }> } };
const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId);
return {
ok: response.ok,
status: response.status,
providerId,
nodeStatus: item?.nodeStatus,
updatedAt: item?.updatedAt,
dockerStatus: item?.dockerStatus === undefined ? null : {
ok: item.dockerStatus.ok,
socketPresent: item.dockerStatus.socketPresent,
collectedAt: item.dockerStatus.collectedAt,
counts: item.dockerStatus.counts,
daemon: item.dockerStatus.daemon,
containersPreview: (item.dockerStatus.containers ?? []).slice(0, 8).map((container) => ({
id: container.id,
name: container.name,
image: container.image,
state: container.state,
status: container.status,
ports: container.ports,
})),
},
};
}
function systemStatusCheckDetail(systemStatus: unknown, providerId: string): unknown {
const response = systemStatus as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; current?: { ok?: boolean; collectedAt?: string; cpu?: unknown; memory?: unknown; disk?: unknown }; history?: unknown[] }> } };
const item = response.body?.systemStatuses?.find((entry) => entry.providerId === providerId);
return {
ok: response.ok,
status: response.status,
providerId,
nodeStatus: item?.nodeStatus,
updatedAt: item?.updatedAt,
current: item?.current === undefined ? null : {
ok: item.current.ok,
collectedAt: item.current.collectedAt,
cpu: item.current.cpu,
memory: item.current.memory,
disk: item.current.disk,
},
historyCount: item?.history?.length ?? 0,
historyPreview: (item?.history ?? []).slice(-8),
};
}
async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
const portSummary = dockerPortSummary() as { rows?: Array<{ name: string; ports: string }> };
const portsText = (portSummary.rows ?? []).map((row) => `${row.name} ${row.ports}`).join("\n");
@@ -137,11 +203,27 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
const coreOverview = dockerCoreJson("/api/overview");
const coreNodes = dockerCoreJson("/api/nodes");
const systemStatus = dockerCoreJson("/api/nodes/system-status?limit=24");
const dockerStatus = dockerCoreJson("/api/nodes/docker-status");
const providerIngress = await fetchProbe(urls.providerIngressHealthUrl);
const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number } }).body;
const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string }> } }).body?.nodes ?? [];
const systemStatuses = (systemStatus as { body?: { systemStatuses?: Array<{ providerId?: string; current?: { cpu?: { percent?: number }; memory?: { percent?: number }; disk?: { percent?: number } }; history?: unknown[] }> } }).body?.systemStatuses ?? [];
const mainSystem = systemStatuses.find((item) => item.providerId === config.providerGateway.id);
const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? [];
const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id);
addCheck(checks, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview);
addCheck(checks, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes);
addCheck(checks, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id));
addCheck(checks, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id));
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
method: "POST",
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
});
const upgradeTaskId = (upgradeDispatch as { body?: { taskId?: string } }).body?.taskId ?? "";
const upgradeTask = upgradeTaskId ? await waitForTaskStatus(upgradeTaskId, "succeeded") : { ok: false, error: "missing taskId", upgradeDispatch };
const taskResult = (upgradeTask as { task?: { result?: { plan?: unknown; mode?: string } }; ok?: boolean }).task?.result;
addCheck(checks, "provider:upgrade-plan", (upgradeDispatch as { ok?: boolean }).ok === true && (upgradeTask as { ok?: boolean }).ok === true && taskResult?.mode === "plan" && taskResult.plan !== undefined, { upgradeDispatch, upgradeTask });
addCheck(checks, "provider-ingress:public-health", (providerIngress as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (providerIngress as { body?: { ok?: boolean } }).body?.ok === true, providerIngress);
}
@@ -192,9 +274,35 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await page.getByTestId(`raw-node-${config.providerGateway.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`).click();
await page.waitForSelector('[data-testid="raw-json"]', { timeout: 5000 });
const rawText = await page.locator('[data-testid="raw-json"]').innerText({ timeout: 5000 });
await page.getByRole("button", { name: "关闭" }).click();
await page.getByRole("button", { name: /资源节点/ }).click();
await page.getByRole("button", { name: /资源监控/ }).click();
await page.waitForSelector('[data-testid="node-monitor-page"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="metric-chart-cpu"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="metric-chart-memory"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="metric-chart-disk"]', { timeout: 10000 });
await page.waitForFunction(() => {
const text = document.body.innerText.toLowerCase();
return text.includes("任务管理器视图") && text.includes("cpu") && text.includes("memory") && text.includes("disk");
}, undefined, { timeout: 10000 });
const monitorText = await page.locator('[data-testid="node-monitor-page"]').innerText({ timeout: 5000 });
await page.getByTestId("upgrade-plan-button").click();
await page.waitForFunction(() => document.body.innerText.includes("预检升级 已下发"), undefined, { timeout: 10000 });
const upgradeControlText = await page.locator('[data-testid="provider-upgrade-control"]').innerText({ timeout: 5000 });
await page.getByRole("button", { name: /Docker 状态/ }).click();
await page.waitForSelector('[data-testid="docker-status-page"]', { timeout: 10000 });
await page.waitForSelector('[data-testid="docker-container-table"]', { timeout: 10000 });
await page.waitForFunction(() => {
const text = document.body.innerText.toLowerCase();
return text.includes("docker desktop 视图") && text.includes("containers");
}, undefined, { timeout: 10000 });
const dockerText = await page.locator('[data-testid="docker-status-page"]').innerText({ timeout: 5000 });
addCheck(checks, "frontend:login-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && bodyText.includes("核心在线"), { screenshotPath });
addCheck(checks, "frontend:no-naked-json-before-click", rawBlocksBefore === 0 && !nakedJsonText, { rawBlocksBefore, nakedJsonText });
addCheck(checks, "frontend:raw-json-explicit-button", rawText.includes('"providerId"') && rawText.includes(config.providerGateway.id), { rawTextPreview: rawText.slice(0, 400) });
addCheck(checks, "frontend:system-monitor-visible", monitorText.includes("任务管理器视图") && monitorText.includes("CPU") && monitorText.includes("Memory") && monitorText.includes("Disk"), { monitorTextPreview: monitorText.slice(0, 800) });
addCheck(checks, "frontend:upgrade-plan-dispatch", upgradeControlText.includes("预检升级 已下发"), { providerId: config.providerGateway.id, upgradeControlPreview: upgradeControlText.slice(0, 500) });
addCheck(checks, "frontend:docker-status-visible", dockerText.toLowerCase().includes("docker desktop 视图") && dockerText.toLowerCase().includes("containers") && (dockerText.includes("unidesk-frontend") || dockerText.includes("unidesk-backend-core")), { dockerTextPreview: dockerText.slice(0, 800) });
addCheck(checks, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors });
return { screenshotPath, bodyText, consoleErrors };
} finally {