Merge pull request #2100 from pikasTech/feat/2097-hwlab-node-py
feat: add HWLAB node Python GUI
This commit is contained in:
@@ -20,6 +20,40 @@ test("cloud-api exposes hwpod-node-ops contract on /v1", async () => {
|
||||
assert.equal(payload.hwpod.specDiscoveryRoute, "/v1/hwpod/specs");
|
||||
assert.ok(payload.hwpod.supportedOps.includes("workspace.ls"));
|
||||
assert.ok(payload.hwpod.supportedOps.includes("workspace.insert-after"));
|
||||
assert.equal(payload.hwlabNode.updateRoute, "/v1/hwlab-node/update");
|
||||
assert.equal(payload.hwlabNode.updateContractVersion, "hwlab-node-update-v1");
|
||||
} finally {
|
||||
await close(server);
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud-api exposes hwlab-node Python update metadata", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_PUBLIC_ENDPOINT: "https://hwlab.pikapython.com",
|
||||
HWLAB_NODE_PY_LATEST_VERSION: "0.1.1",
|
||||
HWLAB_NODE_PY_DOWNLOAD_PATH: "/downloads/hwlab-node.py",
|
||||
HWLAB_NODE_PY_SHA256: "abc123"
|
||||
}
|
||||
});
|
||||
await listen(server);
|
||||
try {
|
||||
const response = await fetch(`${serverUrl(server)}/v1/hwlab-node/update?current=0.1.0&channel=stable&platform=windows`);
|
||||
const payload = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.contractVersion, "hwlab-node-update-v1");
|
||||
assert.equal(payload.updateAvailable, true);
|
||||
assert.equal(payload.latestVersion, "0.1.1");
|
||||
assert.equal(payload.downloadUrl, "https://hwlab.pikapython.com/downloads/hwlab-node.py");
|
||||
assert.equal(payload.sha256, "abc123");
|
||||
|
||||
const currentResponse = await fetch(`${serverUrl(server)}/v1/hwlab-node/update?current=0.1.1&channel=stable&platform=windows`);
|
||||
const currentPayload = await currentResponse.json();
|
||||
assert.equal(currentResponse.status, 200);
|
||||
assert.equal(currentPayload.updateAvailable, false);
|
||||
assert.equal(currentPayload.downloadUrl, null);
|
||||
} finally {
|
||||
await close(server);
|
||||
}
|
||||
|
||||
@@ -623,11 +623,21 @@ async function handleRestAdapter(request, response, url, options) {
|
||||
nodeRole: "thin-hwpod-node-executor",
|
||||
supportedOps: Array.from(HWPOD_NODE_OPS)
|
||||
},
|
||||
hwlabNode: {
|
||||
updateRoute: "/v1/hwlab-node/update",
|
||||
updateContractVersion: "hwlab-node-update-v1",
|
||||
defaultUpdateIntervalSeconds: 300
|
||||
},
|
||||
m3IoControl: describeM3IoControl(options)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/v1/hwlab-node/update") {
|
||||
handleHwlabNodeUpdateHttp(request, response, url, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/v1/auth/session" || url.pathname === "/v1/auth/login" || url.pathname === "/v1/auth/logout" || url.pathname === "/v1/auth/register") {
|
||||
const authUrl = new URL(url.href);
|
||||
authUrl.pathname = url.pathname.replace(/^\/v1\/auth/u, "/auth");
|
||||
@@ -949,6 +959,80 @@ async function handleHwpodNodeOpsHttp(request, response, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleHwlabNodeUpdateHttp(request, response, url, options) {
|
||||
if (request.method !== "GET") {
|
||||
sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "GET required." } });
|
||||
return;
|
||||
}
|
||||
const env = options.env ?? process.env;
|
||||
const currentVersion = cleanText(url.searchParams.get("current") || url.searchParams.get("version"));
|
||||
const channel = cleanText(url.searchParams.get("channel")) || "stable";
|
||||
const platform = cleanText(url.searchParams.get("platform")) || "unknown";
|
||||
const latestVersion = cleanText(env.HWLAB_NODE_PY_LATEST_VERSION) || "0.1.0";
|
||||
const releaseNotesUrl = cleanText(env.HWLAB_NODE_PY_RELEASE_NOTES_URL) || null;
|
||||
const downloadUrl = hwlabNodeDownloadUrl(env);
|
||||
const sha256 = cleanText(env.HWLAB_NODE_PY_SHA256) || null;
|
||||
const newer = currentVersion ? compareVersionStrings(latestVersion, currentVersion) > 0 : Boolean(latestVersion);
|
||||
const updateAvailable = Boolean(downloadUrl && newer);
|
||||
sendJson(response, 200, {
|
||||
ok: true,
|
||||
contractVersion: "hwlab-node-update-v1",
|
||||
serviceId: "hwlab-node",
|
||||
route: "/v1/hwlab-node/update",
|
||||
channel,
|
||||
platform,
|
||||
currentVersion: currentVersion || null,
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
downloadUrl: updateAvailable ? downloadUrl : null,
|
||||
sha256: updateAvailable ? sha256 : null,
|
||||
releaseNotesUrl,
|
||||
manualDefault: true,
|
||||
autoApplyDefault: false,
|
||||
checkIntervalSeconds: parsePositiveInteger(env.HWLAB_NODE_PY_UPDATE_INTERVAL_SECONDS, 300),
|
||||
observedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
function hwlabNodeDownloadUrl(env) {
|
||||
const explicit = cleanText(env.HWLAB_NODE_PY_DOWNLOAD_URL);
|
||||
if (explicit) return explicit;
|
||||
const downloadPath = cleanText(env.HWLAB_NODE_PY_DOWNLOAD_PATH);
|
||||
if (!downloadPath) return null;
|
||||
if (/^https?:\/\//iu.test(downloadPath)) return downloadPath;
|
||||
const publicEndpoint = cleanText(env.HWLAB_PUBLIC_ENDPOINT) || "https://hwlab.pikapython.com";
|
||||
try {
|
||||
return new URL(downloadPath, publicEndpoint.endsWith("/") ? publicEndpoint : `${publicEndpoint}/`).toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function compareVersionStrings(left, right) {
|
||||
const a = versionParts(left);
|
||||
const b = versionParts(right);
|
||||
for (let index = 0; index < Math.max(a.length, b.length, 3); index += 1) {
|
||||
const delta = (a[index] ?? 0) - (b[index] ?? 0);
|
||||
if (delta !== 0) return delta > 0 ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function versionParts(value) {
|
||||
return String(value ?? "")
|
||||
.trim()
|
||||
.replace(/^v/iu, "")
|
||||
.split(/[.+-]/u)
|
||||
.slice(0, 4)
|
||||
.map((part) => parseInt(part, 10))
|
||||
.map((part) => (Number.isFinite(part) && part >= 0 ? part : 0));
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text || "";
|
||||
}
|
||||
|
||||
async function handleHwpodSpecDiscoveryHttp(request, response, url, options) {
|
||||
const probe = truthyFlag(url.searchParams.get("probe"));
|
||||
const observedAt = new Date().toISOString();
|
||||
@@ -1041,7 +1125,7 @@ async function forwardHwpodNodeOpsPlan(targetUrl, plan, requestMeta, options) {
|
||||
});
|
||||
}
|
||||
if (target.ok && target.nodeId && target.nodeId !== plan.nodeId) {
|
||||
return directHwpodNodeBlocked(plan, "hwpod_node_not_connected", `${plan.nodeId} is not connected via WebSocket and HWLAB_HWPOD_NODE_OPS_URL points to ${target.nodeId}`, {
|
||||
return directHwpodNodeBlocked(plan, "hwpod_node_id_mismatch", `HWLAB_HWPOD_NODE_OPS_URL points to ${target.nodeId}, not requested node ${plan.nodeId}`, {
|
||||
requestedNodeId: plan.nodeId,
|
||||
targetNodeId: target.nodeId,
|
||||
targetUrl: redactNodeOpsUrl(targetUrl),
|
||||
|
||||
+1237
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user