/* * SPEC: PJ2026-0104010803 Workbench唯一投影; HWLAB#1832 Workbench runtime Go 微服务拆分. * 职责: Cloud API 到 hwlab-workbench-runtime 的单一路径只读客户端;配置后不回退本地 PG。 */ const DEFAULT_WORKBENCH_RUNTIME_TIMEOUT_MS = 10000; export function createWorkbenchRuntimeClient(options = {}) { const env = options.env ?? process.env; const baseUrl = normalizeBaseUrl(env.HWLAB_WORKBENCH_RUNTIME_URL); if (!baseUrl) return null; const timeoutMs = boundedInteger(env.HWLAB_WORKBENCH_RUNTIME_TIMEOUT_MS, DEFAULT_WORKBENCH_RUNTIME_TIMEOUT_MS, 1000, 60000); const fetchFn = options.fetch ?? globalThis.fetch; const traceparent = normalizeTraceparent(options.traceparent ?? options.traceParent); return { serviceId: "hwlab-workbench-runtime", queryWorkbenchFacts: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/facts", body: params }), queryAgentTraceEvents: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/trace-events", body: params }), listWorkbenchSessions: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/sessions", body: params }), readAtomicWorkbenchProjectionSync: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/sync", body: params }), readWorkbenchProjectionOutbox: (params = {}) => postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path: "/v1/workbench-runtime/projection-outbox", body: params }).then((result) => Array.isArray(result?.rows) ? result.rows : []) }; } async function postRuntime({ fetchFn, baseUrl, timeoutMs, traceparent, path, body }) { if (typeof fetchFn !== "function") { throw runtimeDependencyError({ code: "workbench_runtime_fetch_unavailable", message: "fetch is unavailable for workbench runtime dependency", retryable: false }); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); let response; try { const headers = { "content-type": "application/json", accept: "application/json" }; if (traceparent) headers.traceparent = traceparent; response = await fetchFn(new URL(path, baseUrl), { method: "POST", headers, body: JSON.stringify(body ?? {}), signal: controller.signal }); } catch (error) { throw runtimeDependencyError({ code: error?.name === "AbortError" ? "workbench_runtime_timeout" : "workbench_runtime_unreachable", message: error?.message ?? "workbench runtime request failed", retryable: true, cause: error }); } finally { clearTimeout(timer); } const payload = await responseJson(response); if (!response.ok) { const error = payload?.error ?? {}; throw runtimeDependencyError({ code: error.code ?? `workbench_runtime_http_${response.status}`, message: error.message ?? `workbench runtime request failed with HTTP ${response.status}`, retryable: response.status >= 500 || error.retryable === true, status: response.status, payload }); } return payload; } async function responseJson(response) { try { return await response.json(); } catch { return null; } } function normalizeTraceparent(value) { const text = String(value ?? "").trim().toLowerCase(); return /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/u.test(text) ? text : ""; } function runtimeDependencyError({ code, message, retryable, status = null, payload = null, cause = null }) { const error = new Error(message || code || "workbench runtime dependency failed"); error.name = "WorkbenchRuntimeDependencyError"; error.code = code ?? "workbench_runtime_dependency_failed"; if (cause) error.cause = cause; error.data = { serviceId: "hwlab-workbench-runtime", status, retryable: retryable === true, transient: retryable === true, payload, valuesRedacted: true }; return error; } function normalizeBaseUrl(value) { const text = String(value ?? "").trim(); if (!text) return null; return text.endsWith("/") ? text : `${text}/`; } function boundedInteger(value, fallback, min, max) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.max(min, Math.min(max, Math.trunc(parsed))); }