diff --git a/cmd/hwlab-hwpod-api/main.ts b/cmd/hwlab-hwpod-api/main.ts index efa2db29..e2ca66fe 100644 --- a/cmd/hwlab-hwpod-api/main.ts +++ b/cmd/hwlab-hwpod-api/main.ts @@ -10,7 +10,7 @@ import { createHwpodNodeWsRegistry } from "../../internal/cloud/hwpod-node-ws-re const runtime = hwpodRuntime(); const nodeRegistry = createHwpodNodeWsRegistry({ env: process.env }); -const app = createHwpodHttpApp({ nodeOpsApiUrl: runtime.nodeOpsApiUrl, temporal: runtime.temporal, specRegistry: runtime.specRegistry, nodeRegistry }); +const app = createHwpodHttpApp({ nodeOpsApiUrl: runtime.nodeOpsApiUrl, temporal: runtime.temporal, specRegistry: runtime.specRegistry, nodeRegistry, env: process.env }); const host = process.env.HWPOD_API_HOST || "0.0.0.0"; const port = positivePort(process.env.HWPOD_API_PORT, 6681); const expectedNodeToken = required(process.env.HWPOD_NODE_WS_TOKEN, "HWPOD_NODE_WS_TOKEN"); diff --git a/internal/cloud/hwpod-node-ops.test.ts b/internal/cloud/hwpod-node-ops.test.ts index f21f0749..de98b13e 100644 --- a/internal/cloud/hwpod-node-ops.test.ts +++ b/internal/cloud/hwpod-node-ops.test.ts @@ -13,6 +13,7 @@ import { createCloudApiBunServer } from "./bun-server.ts"; import { createHwpodNodeWsRegistry } from "./hwpod-node-ws-registry.ts"; import { createMemoryHwpodOperationLedger } from "./hwpod-operation-ledger.ts"; import { createCloudApiServer } from "./server.ts"; +import { hwlabNodeBundledArtifact } from "../hwpod/node-artifact.ts"; import { connectHwpodNodeWs, createHwpodNodeServer } from "../../tools/src/hwpod-node-lib.ts"; function createHwpodTestCloudApiServer(options: any = {}) { @@ -92,6 +93,7 @@ test("cloud-api exposes hwlab-node Python update metadata", async () => { }); test("cloud-api serves bundled hwlab-node Python updater artifact by default", async () => { + const bundled = hwlabNodeBundledArtifact(); const server = createHwpodTestCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_PUBLIC_ENDPOINT: "https://hwlab.pikapython.com" } }); await listen(server); try { @@ -99,18 +101,18 @@ test("cloud-api serves bundled hwlab-node Python updater artifact by default", a const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.updateAvailable, true); - assert.equal(payload.latestVersion, "0.1.2"); + assert.equal(payload.latestVersion, bundled.version); assert.equal(payload.downloadUrl, "https://hwlab.pikapython.com/v1/hwlab-node/download/hwlab-node.py"); assert.match(payload.sha256, /^[a-f0-9]{64}$/u); - assert.equal(payload.artifact.version, "0.1.2"); + assert.equal(payload.artifact.version, bundled.version); assert.equal(payload.artifact.fileName, "hwlab-node.py"); assert.ok(payload.artifact.sizeBytes > 0); const download = await fetch(`${serverUrl(server)}/v1/hwlab-node/download/hwlab-node.py`); const text = await download.text(); assert.equal(download.status, 200); - assert.equal(download.headers.get("x-hwlab-node-version"), "0.1.2"); - assert.ok(text.includes('APP_VERSION = "0.1.2"')); + assert.equal(download.headers.get("x-hwlab-node-version"), bundled.version); + assert.equal(text, bundled.content); } finally { await close(server); } diff --git a/internal/cloud/server-hwpod-http.ts b/internal/cloud/server-hwpod-http.ts index ba035623..4342317c 100644 --- a/internal/cloud/server-hwpod-http.ts +++ b/internal/cloud/server-hwpod-http.ts @@ -3,9 +3,7 @@ * Implementation reference: draft-2026-07-13-p0-cloud-console。 * Responsibility: 提供 HWLAB Node 更新、HWPOD typed topology、显式有界 readiness 探测与 node-ops 转发。 */ -import { createHash, randomUUID } from "node:crypto"; -import { readFileSync } from "node:fs"; -import path from "node:path"; +import { randomUUID } from "node:crypto"; import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; @@ -14,10 +12,8 @@ import { hwpodSpecDiscoveryPayload, hwpodSpecWorkspaceProbePlan } from "./hwpod-spec-discovery.ts"; -import { - buildHwpodTopologyReadModel, - HWPOD_NODE_READINESS_DEFINITIONS -} from "./hwpod-topology-read-model.ts"; +import { buildHwpodTopologyReadModel } from "./hwpod-topology-read-model.ts"; +import { buildHwlabNodeUpdatePayload, hwlabNodeBundledArtifact } from "../hwpod/node-artifact.ts"; import { getHeader, parsePositiveInteger, @@ -92,52 +88,7 @@ export function handleHwlabNodeUpdateHttp(request, response, url, options) { 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 bundled = hwlabNodeBundledMetadata(env); - const latestVersion = cleanText(env.HWLAB_NODE_PY_LATEST_VERSION) || bundled.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) || bundled.sha256 || null; - const fileName = nodeArtifactFileName(downloadUrl) || "hwlab-node.py"; - const sizeBytes = parsePositiveInteger(env.HWLAB_NODE_PY_FILE_SIZE_BYTES, bundled.sizeBytes); - 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, - artifact: { - version: latestVersion, - fileName, - sizeBytes, - sha256, - downloadUrl - }, - releaseNotes: hwlabNodeReleaseNotes(env), - releaseNotesUrl, - installConditions: [ - { id: "windows-python", label: "Windows 交互用户的原生 Python", required: true }, - { id: "tkinter", label: "Python tkinter 图形能力", required: true }, - { id: "yaml-managed-config", label: "owning YAML 声明 nodeId、工作区与凭据引用", required: true }, - { id: "outbound-websocket", label: "可主动出站连接 Cloud API WebSocket", required: true } - ], - readinessStages: hwlabNodeReadinessStages(), - manualDefault: true, - autoApplyDefault: false, - checkIntervalSeconds: parsePositiveInteger(env.HWLAB_NODE_PY_UPDATE_INTERVAL_SECONDS, 300), - observedAt: new Date().toISOString() - }); + sendJson(response, 200, buildHwlabNodeUpdatePayload(url, options.env ?? process.env)); } export function handleHwlabNodeDownloadHttp(request, response, options) { @@ -145,7 +96,7 @@ export function handleHwlabNodeDownloadHttp(request, response, options) { sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "GET required." } }); return; } - const bundled = hwlabNodeBundledMetadata(options.env ?? process.env); + const bundled = hwlabNodeBundledArtifact(options.env ?? process.env); if (!bundled.content) { sendJson(response, 404, { ok: false, error: { code: "hwlab_node_bundle_missing", message: "bundled hwlab-node.py is not available" } }); return; @@ -212,71 +163,6 @@ function hwpodNodeOpsRequestMeta(request, options, label) { }; } -function hwlabNodeBundledMetadata(env) { - const bundlePath = cleanText(env.HWLAB_NODE_PY_BUNDLE_PATH) || path.resolve(process.cwd(), "tools/hwlab-node.py"); - try { - const content = readFileSync(bundlePath, "utf8"); - const version = cleanText(content.match(/APP_VERSION\s*=\s*["']([^"']+)["']/u)?.[1]); - const sha256 = createHash("sha256").update(content, "utf8").digest("hex"); - return { path: bundlePath, content, version, sha256, sizeBytes: Buffer.byteLength(content, "utf8") }; - } catch { - return { path: bundlePath, content: "", version: "", sha256: "", sizeBytes: 0 }; - } -} - -function nodeArtifactFileName(downloadUrl) { - try { - const name = path.posix.basename(new URL(downloadUrl).pathname); - return cleanText(name); - } catch { - return ""; - } -} - -function hwlabNodeReleaseNotes(env) { - const configured = cleanText(env.HWLAB_NODE_PY_RELEASE_NOTES); - if (configured) return configured.split(/\r?\n|\|/u).map((item) => item.trim()).filter(Boolean).slice(0, 12); - return ["单文件 Python/tkinter 节点,提供主动出站 WebSocket、有界诊断和 SHA-256 校验自更新。"]; -} - -function hwlabNodeReadinessStages() { - return HWPOD_NODE_READINESS_DEFINITIONS.map((stage) => ({ ...stage, nextAction: { ...stage.nextAction } })); -} - -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) || "/v1/hwlab-node/download/hwlab-node.py"; - if (/^https?:\/\//iu.test(downloadPath)) return downloadPath; - const publicEndpoint = cleanText(env.HWLAB_PUBLIC_ENDPOINT); - if (!publicEndpoint) return downloadPath.startsWith("/") ? downloadPath : `/${downloadPath}`; - 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 || ""; diff --git a/internal/hwpod/http.ts b/internal/hwpod/http.ts index 168e81ef..13c8a589 100644 --- a/internal/hwpod/http.ts +++ b/internal/hwpod/http.ts @@ -10,14 +10,23 @@ import { compileHwpodNodeOpsPlan } from "../../tools/src/hwpod-harness-lib.ts"; import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; import { validateHwpodOperationInput, validateHwpodWebOperationRequest } from "./contracts.ts"; import { buildHwpodTopologyReadModel } from "../cloud/hwpod-topology-read-model.ts"; +import { buildHwlabNodeUpdatePayload, hwlabNodeBundledArtifact } from "./node-artifact.ts"; -export function createHwpodHttpApp(options: { nodeOpsApiUrl: string; temporal: any; specRegistry: any; nodeRegistry: any }) { +export function createHwpodHttpApp(options: { nodeOpsApiUrl: string; temporal: any; specRegistry: any; nodeRegistry: any; env?: Record }) { return { async fetch(request: Request) { const url = new URL(request.url); try { if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", status: "live", valuesPrinted: false }); if (url.pathname === "/health/ready") return readiness(options); + if (url.pathname === "/v1/hwlab-node/update" && request.method === "GET") { + return json(200, buildHwlabNodeUpdatePayload(url, options.env ?? process.env)); + } + if (url.pathname === "/v1/hwlab-node/download/hwlab-node.py" && request.method === "GET") { + const artifact = hwlabNodeBundledArtifact(options.env ?? process.env); + if (!artifact.content) return json(404, { ok: false, error: { code: "hwlab_node_bundle_missing", message: "bundled hwlab-node.py is not available" } }); + return new Response(artifact.content, { status: 200, headers: { "content-type": "text/x-python; charset=utf-8", "cache-control": "no-store", "x-hwlab-node-version": artifact.version || "unknown", "x-hwlab-node-sha256": artifact.sha256 } }); + } if (url.pathname === "/v1/hwpod" && request.method === "GET") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", executionAuthority: "temporal", nodeOpsApiUrl: options.nodeOpsApiUrl, valuesPrinted: false }); if (url.pathname === "/v1/hwpod/specs" && request.method === "GET") return json(200, await options.specRegistry.list()); if (url.pathname === "/v1/hwpod/specs" && request.method === "POST") { diff --git a/internal/hwpod/node-artifact.ts b/internal/hwpod/node-artifact.ts new file mode 100644 index 00000000..595c8155 --- /dev/null +++ b/internal/hwpod/node-artifact.ts @@ -0,0 +1,116 @@ +/* + * SPEC: PJ2026-010103 HWPOD 服务;PJ2026-010405 云端控制台。 + * 责任: 为 Cloud API 与独立 HWPOD API 提供同一 Python 节点 artifact 合同。 + */ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import { HWPOD_NODE_READINESS_DEFINITIONS } from "../cloud/hwpod-topology-read-model.ts"; + +type Env = Record; + +export function buildHwlabNodeUpdatePayload(url: URL, env: 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 bundled = hwlabNodeBundledArtifact(env); + const latestVersion = cleanText(env.HWLAB_NODE_PY_LATEST_VERSION) || bundled.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) || bundled.sha256 || null; + const fileName = nodeArtifactFileName(downloadUrl) || "hwlab-node.py"; + const sizeBytes = positiveInteger(env.HWLAB_NODE_PY_FILE_SIZE_BYTES, bundled.sizeBytes); + const newer = currentVersion ? compareVersionStrings(latestVersion, currentVersion) > 0 : Boolean(latestVersion); + const updateAvailable = Boolean(downloadUrl && newer); + return { + 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, + artifact: { version: latestVersion, fileName, sizeBytes, sha256, downloadUrl }, + releaseNotes: hwlabNodeReleaseNotes(env), + releaseNotesUrl, + installConditions: [ + { id: "windows-python", label: "Windows 交互用户的原生 Python", required: true }, + { id: "tkinter", label: "Python tkinter 图形能力", required: true }, + { id: "yaml-managed-config", label: "owning YAML 声明 nodeId、工作区与凭据引用", required: true }, + { id: "outbound-websocket", label: "可主动出站连接 Cloud API WebSocket", required: true } + ], + readinessStages: HWPOD_NODE_READINESS_DEFINITIONS.map((stage) => ({ ...stage, nextAction: { ...stage.nextAction } })), + manualDefault: true, + autoApplyDefault: false, + checkIntervalSeconds: positiveInteger(env.HWLAB_NODE_PY_UPDATE_INTERVAL_SECONDS, 300), + observedAt: new Date().toISOString() + }; +} + +export function hwlabNodeBundledArtifact(env: Env = process.env) { + const bundlePath = cleanText(env.HWLAB_NODE_PY_BUNDLE_PATH) || path.resolve(process.cwd(), "tools/hwlab-node.py"); + try { + const content = readFileSync(bundlePath, "utf8"); + const version = cleanText(content.match(/APP_VERSION\s*=\s*["']([^"']+)["']/u)?.[1]); + const sha256 = createHash("sha256").update(content, "utf8").digest("hex"); + return { path: bundlePath, content, version, sha256, sizeBytes: Buffer.byteLength(content, "utf8") }; + } catch { + return { path: bundlePath, content: "", version: "", sha256: "", sizeBytes: 0 }; + } +} + +function hwlabNodeDownloadUrl(env: Env) { + const explicit = cleanText(env.HWLAB_NODE_PY_DOWNLOAD_URL); + if (explicit) return explicit; + const downloadPath = cleanText(env.HWLAB_NODE_PY_DOWNLOAD_PATH) || "/v1/hwlab-node/download/hwlab-node.py"; + if (/^https?:\/\//iu.test(downloadPath)) return downloadPath; + const publicEndpoint = cleanText(env.HWPOD_PUBLIC_BASE_URL) || cleanText(env.HWLAB_PUBLIC_ENDPOINT); + if (!publicEndpoint) return downloadPath.startsWith("/") ? downloadPath : `/${downloadPath}`; + try { + return new URL(downloadPath, publicEndpoint.endsWith("/") ? publicEndpoint : `${publicEndpoint}/`).toString(); + } catch { + return null; + } +} + +function nodeArtifactFileName(downloadUrl: string | null) { + try { return cleanText(path.posix.basename(new URL(downloadUrl ?? "").pathname)); } + catch { return ""; } +} + +function hwlabNodeReleaseNotes(env: Env) { + const configured = cleanText(env.HWLAB_NODE_PY_RELEASE_NOTES); + if (configured) return configured.split(/\r?\n|\|/u).map((item) => item.trim()).filter(Boolean).slice(0, 12); + return ["单文件 Python/tkinter 节点,提供主动出站 WebSocket、有界诊断和 SHA-256 校验自更新。"]; +} + +function compareVersionStrings(left: string, right: string) { + 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: string) { + return value.trim().replace(/^v/iu, "").split(/[.+-]/u).slice(0, 4) + .map((part) => Number.parseInt(part, 10)) + .map((part) => (Number.isFinite(part) && part >= 0 ? part : 0)); +} + +function positiveInteger(value: unknown, fallback: number) { + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback; +} + +function cleanText(value: unknown) { + const text = String(value ?? "").trim(); + return text || ""; +} diff --git a/internal/hwpod/spec-registry.test.ts b/internal/hwpod/spec-registry.test.ts index 6871dec0..1ed499f7 100644 --- a/internal/hwpod/spec-registry.test.ts +++ b/internal/hwpod/spec-registry.test.ts @@ -53,6 +53,32 @@ test("L1 HWPOD HTTP exposes runtime CRUD and typed frozen errors", async () => { await app.close(); }); +test("L1 HWPOD HTTP owns Python node metadata and download routes", async () => { + const registry = createHwpodSpecRegistry({ store: memoryStore() }); + const temporal = { close: async () => {}, start: async () => ({}), describe: async () => ({}), result: async () => ({}) }; + const app = createHwpodHttpApp({ + nodeOpsApiUrl: "http://native-hwpod.test", + temporal, + specRegistry: registry, + nodeRegistry: nodeRegistry(), + env: { ...process.env, HWPOD_PUBLIC_BASE_URL: "https://lab-dev-hwpod.hwpod.com" }, + }); + + const metadata = await request(app, "GET", "/v1/hwlab-node/update?platform=windows&channel=stable"); + expect(metadata).toMatchObject({ status: 200, body: { ok: true, contractVersion: "hwlab-node-update-v1", artifact: { fileName: "hwlab-node.py" } } }); + expect(metadata.body.artifact.version).toMatch(/^\d+\.\d+\.\d+$/u); + expect(metadata.body.artifact.sizeBytes).toBeGreaterThan(0); + expect(metadata.body.artifact.sha256).toMatch(/^[a-f0-9]{64}$/u); + expect(metadata.body.artifact.downloadUrl).toBe("https://lab-dev-hwpod.hwpod.com/v1/hwlab-node/download/hwlab-node.py"); + + const download = await app.fetch(new Request("http://hwpod.test/v1/hwlab-node/download/hwlab-node.py")); + expect(download.status).toBe(200); + expect(download.headers.get("content-type")).toContain("text/x-python"); + expect(download.headers.get("x-hwlab-node-sha256")).toBe(metadata.body.artifact.sha256); + expect(await download.text()).toContain(`APP_VERSION = "${metadata.body.artifact.version}"`); + await app.close(); +}); + test("L0 HWPOD topology is built from the local registry without Cloud API", async () => { const registry = createHwpodSpecRegistry({ store: memoryStore(), diff --git a/web/hwlab-cloud-web/scripts/hwpod-native-vite-config.test.ts b/web/hwlab-cloud-web/scripts/hwpod-native-vite-config.test.ts index 830c3702..e5ded489 100644 --- a/web/hwlab-cloud-web/scripts/hwpod-native-vite-config.test.ts +++ b/web/hwlab-cloud-web/scripts/hwpod-native-vite-config.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { parseHwpodNativeAccessProfile } from "./hwpod-native-vite.config"; +import { hwpodNativeProxy, parseHwpodNativeAccessProfile } from "./hwpod-native-vite.config"; import { firstAllowedNavPath } from "../src/stores/auth"; describe("HWPOD native Cloud Console profile", () => { @@ -17,4 +17,10 @@ describe("HWPOD native Cloud Console profile", () => { it("fails closed without a declared navigation profile", () => { expect(() => parseHwpodNativeAccessProfile(undefined)).toThrow(/HWPOD_WEB_ACCESS_PROFILE_JSON/u); }); + + it("proxies onboarding metadata and downloads to the independent HWPOD API", () => { + expect(hwpodNativeProxy("http://127.0.0.1:6681")).toMatchObject({ + "/v1/hwlab-node": { target: "http://127.0.0.1:6681", changeOrigin: false }, + }); + }); }); diff --git a/web/hwlab-cloud-web/scripts/hwpod-native-vite.config.ts b/web/hwlab-cloud-web/scripts/hwpod-native-vite.config.ts index 9c7cc1b9..d3977b0c 100644 --- a/web/hwlab-cloud-web/scripts/hwpod-native-vite.config.ts +++ b/web/hwlab-cloud-web/scripts/hwpod-native-vite.config.ts @@ -19,17 +19,22 @@ export default defineConfig(({ command }) => ({ host: requiredString(process.env.HWPOD_WEB_HOST, "HWPOD_WEB_HOST"), port: requiredPort(process.env.HWPOD_WEB_PORT), strictPort: true, - proxy: { - "/v1/hwpod-node/ws": { target: requiredApiUrl(), changeOrigin: false, ws: true }, - "/v1/hwpod": { target: requiredApiUrl(), changeOrigin: false }, - "/health/ready": { target: requiredApiUrl(), changeOrigin: false }, - } + proxy: hwpodNativeProxy(requiredApiUrl()) } : undefined, plugins: command === "serve" ? [nativeRuntimeConfigPlugin(process.env.HWPOD_WEB_RUNTIME_CONFIG), nativeSessionPlugin(process.env.HWPOD_WEB_ACCESS_PROFILE_JSON), healthPlugin(), vue()] : [vue()] })); +export function hwpodNativeProxy(target: string) { + return { + "/v1/hwpod-node/ws": { target, changeOrigin: false, ws: true }, + "/v1/hwpod": { target, changeOrigin: false }, + "/v1/hwlab-node": { target, changeOrigin: false }, + "/health/ready": { target, changeOrigin: false }, + }; +} + function healthPlugin(): Plugin { return { name: "hwpod-native-health", configureServer(server) { server.middlewares.use((request, response, next) => { if (request.url !== "/health/live") return next(); response.statusCode = 200; response.setHeader("content-type", "application/json"); response.end(JSON.stringify({ ok: true, serviceId: "hwlab-hwpod-web", status: "live", valuesPrinted: false })); }); } }; } function nativeRuntimeConfigPlugin(serialized: string | undefined): Plugin { const config = parseWorkbenchNativeRuntimeConfig(requiredString(serialized, "HWPOD_WEB_RUNTIME_CONFIG"));