diff --git a/internal/cloud/bun-server.ts b/internal/cloud/bun-server.ts index 3757f7e0..834c99d3 100644 --- a/internal/cloud/bun-server.ts +++ b/internal/cloud/bun-server.ts @@ -12,9 +12,11 @@ export async function createCloudApiBunServer(options: any = {}) { const host = options.host ?? "0.0.0.0"; const port = options.port ?? 0; + const idleTimeout = parsePositiveInteger(options.idleTimeout ?? env.HWLAB_CLOUD_API_IDLE_TIMEOUT_SECONDS, 120); const server = Bun.serve({ hostname: host, port, + idleTimeout, async fetch(request, bunServer) { const url = new URL(request.url); if (url.pathname === "/v1/hwpod-node/ws") { @@ -64,6 +66,12 @@ async function proxyHttpToInnerServer(request: Request, innerPort: number) { }); } +function parsePositiveInteger(value: unknown, fallback: number) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.floor(parsed); +} + function json(body: any, status = 200) { return new Response(`${JSON.stringify(body)}\n`, { status, diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index a884bd7d..e73aff3b 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -4,7 +4,6 @@ const POST_PROXY_ROUTES = new Set([ "/auth/login", "/auth/register", "/auth/logout", - "/json-rpc", "/v1/agent/chat", "/v1/agent/chat/cancel", "/v1/agent/chat/steer", diff --git a/internal/dev-entrypoint/cloud-web-runtime.mjs b/internal/dev-entrypoint/cloud-web-runtime.mjs index adebaff2..56007312 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.mjs @@ -6,13 +6,6 @@ import { promisify } from "node:util"; import { isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs"; import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs"; - -const readOnlyRpcMethods = new Set([ - "system.health", - "cloud.adapter.describe", - "audit.event.query", - "evidence.record.query" -]); const gzipAsync = promisify(gzip); const GZIP_MIN_BYTES = 1024; const CLIENT_DISCONNECT_ERROR_CODES = new Set([ @@ -127,7 +120,7 @@ function isCloudWebClientRouteRequest(request, routePath) { if (request.method !== "GET" && request.method !== "HEAD") return false; if (!routePath || routePath === "/") return true; if (routePath.startsWith("/auth/") || routePath === "/auth") return false; - if (routePath.startsWith("/v1/") || routePath === "/v1" || routePath === "/json-rpc") return false; + if (routePath.startsWith("/v1/") || routePath === "/v1") return false; if (STATIC_ASSET_EXTENSION_PATTERN.test(path.basename(routePath))) return false; const accept = String(request.headers.accept || "").toLowerCase(); return !accept || accept.includes("text/html") || accept.includes("*/*"); @@ -364,27 +357,6 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c body = await readRequestBody(request); } - if (url.pathname === "/json-rpc") { - try { - const envelope = body ? JSON.parse(body) : {}; - if (!readOnlyRpcMethods.has(envelope.method)) { - sendJson(response, 403, { - error: "readonly_rpc_required", - serviceId, - method: envelope.method || null - }); - return; - } - } catch (error) { - sendJson(response, 400, { - error: "invalid_json", - serviceId, - reason: error.message - }); - return; - } - } - const target = new URL(url.pathname + url.search, cloudApiBaseUrl); try { await requestUpstream({ target, request, response, body: request.method === "GET" ? "" : body, url, cloudApiProxyTimeoutMs }); diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index a4f02e86..88853592 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -31,12 +31,6 @@ test("cloud web route policy proxies public Code Agent chat without gating other publicRoute: false, routeKey: "POST /v1/m3/io" }); - assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/json-rpc"), { - proxy: true, - authRequired: true, - publicRoute: false, - routeKey: "POST /json-rpc" - }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/traces/trc_live"), { proxy: true, authRequired: false, diff --git a/web/hwlab-cloud-web/scripts/workbench-r3-live-hwpod-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r3-live-hwpod-parity.test.ts index 2ec7118e..1ede8493 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r3-live-hwpod-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r3-live-hwpod-parity.test.ts @@ -7,8 +7,8 @@ import { liveCall } from "../src/stores/workbench.ts"; test("R3 probe summary exposes React WorkbenchProbe rows", () => { const summary = summarizeProbeRows(liveSurface()); - assert.equal(summary.rows.length, 4); - assert.equal(summary.okCount, 4); + assert.equal(summary.rows.length, 3); + assert.equal(summary.okCount, 3); assert.equal(summary.tone, "ok"); assert.ok(summary.rows.some((row) => row.path === "/health/live" && row.detail.includes("revision=c0cbdbb38d"))); }); @@ -53,7 +53,6 @@ function liveSurface(): LiveSurface { healthLive: liveProbe, health: ok({ serviceId: "hwlab-edge-proxy", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }), restIndex: ok({ serviceId: "hwlab-cloud-api", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }), - adapter: ok({ serviceId: "hwlab-cloud-api", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }), hwpodSpecs: ok({ ok: true, status: "completed", count: 1, availableCount: 0, nodeOpsContractVersion: "hwpod-node-ops-v1", specs: [{ name: "d601-f103-v2", hwpodId: "d601-f103-v2", nodeId: "node-d601-f103-v2", workspacePath: "F:\\Work\\D601-HWLAB", source: { caseId: "d601-f103-v2-compile" }, availability: { ok: false, status: "blocked", checkedAt: "2026-06-13T17:59:15.785Z", results: [{ opId: "op_01_workspace_ls", op: "workspace.ls", ok: false, status: "blocked", blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "no outbound WebSocket hwpod-node is connected" } }] } }] }), hwpodNodeOps: ok({ ok: true, status: "ready", contractVersion: "hwpod-node-ops-v1", route: "/v1/hwpod-node-ops", supportedOps: ["node.health", "workspace.ls", "debug.build"], websocket: { connectedCount: 0, pendingCount: 0, nodes: [] } }), liveBuilds: ok({ status: "ok", counts: { total: 2, withBuildTime: 0, unavailable: 2, external: 0 }, services: [{ serviceId: "hwlab-cloud-api", status: "build_time_unavailable", build: { createdAt: null, unavailableReason: "构建时间不可用:live tag 与 deploy metadata 不匹配", metadataSource: "live-health" }, image: { tag: "env-abc", digest: "sha256:abcdef0123456789" }, commit: { id: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, { serviceId: "hwlab-cloud-web", status: "build_time_unavailable", build: { createdAt: null, unavailableReason: "构建时间不可用:live tag 与 deploy metadata 不匹配" }, image: { tag: "env-web" }, commit: { id: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }] }), diff --git a/web/hwlab-cloud-web/src/api/index.ts b/web/hwlab-cloud-web/src/api/index.ts index a04442ab..d0e09349 100644 --- a/web/hwlab-cloud-web/src/api/index.ts +++ b/web/hwlab-cloud-web/src/api/index.ts @@ -37,7 +37,6 @@ export const api = { healthLive: (): Promise> => fetchJson("/health/live", { timeoutMs: 12000, timeoutName: "health/live" }), health: (): Promise> => fetchJson("/health", { timeoutMs: 12000, timeoutName: "health" }), restIndex: (): Promise> => fetchJson("/v1", { timeoutMs: 12000, timeoutName: "REST index" }), - adapter: (): Promise> => fetchJson("/json-rpc", { method: "POST", body: JSON.stringify({ jsonrpc: "2.0", id: "cloud-web-system-health", method: "system.health", params: {} }), timeoutMs: 12000, timeoutName: "adapter" }), liveBuilds: (): Promise> => fetchJson("/v1/live-builds", { timeoutMs: 12000, timeoutName: "live builds" }), skills: (): Promise> => fetchJson("/v1/skills", { timeoutMs: 12000, timeoutName: "skills" }) }; diff --git a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue index 325730e5..460964d9 100644 --- a/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue +++ b/web/hwlab-cloud-web/src/components/agent/TraceTimeline.vue @@ -14,6 +14,7 @@ const expanded = ref(false); const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []); const eventCount = computed(() => props.trace?.eventCount ?? events.value.length); const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record[])); +const emptyTraceLabel = computed(() => props.trace?.fullTraceLoaded === true ? "思考中..." : "加载中..."); const toolPreviewLimit = 140; watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => { @@ -117,7 +118,7 @@ function traceRecord(trace: RunnerTrace | null | undefined): Record - + diff --git a/web/hwlab-cloud-web/src/stores/workbench-live.ts b/web/hwlab-cloud-web/src/stores/workbench-live.ts index bc2e6a6a..42e379eb 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-live.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-live.ts @@ -89,12 +89,11 @@ export interface HwpodNodeOpsSummary { const PROBE_DEFINITIONS = Object.freeze([ { key: "cloud-api-live", label: "Cloud API 实况身份", method: "GET", path: "/health/live" }, { key: "cloud-api-health", label: "Cloud API health", method: "GET", path: "/health" }, - { key: "rest-index", label: "REST index", method: "GET", path: "/v1" }, - { key: "adapter", label: "Codex app-server adapter", method: "POST", path: "/json-rpc" } + { key: "rest-index", label: "REST index", method: "GET", path: "/v1" } ]); export function summarizeProbeRows(live: LiveSurface | null): ProbeSummary { - const results = [live?.healthLive, live?.health, live?.restIndex, live?.adapter]; + const results = [live?.healthLive, live?.health, live?.restIndex]; const rows = PROBE_DEFINITIONS.map((definition, index) => rowFromProbe(definition, results[index] ?? null)); const okCount = rows.filter((row) => row.status === "ok").length; const tone: Tone = okCount === rows.length ? "ok" : rows.some((row) => row.status === "blocked" || row.status === "error") ? "blocked" : "pending"; diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 0f66f640..87682bc7 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -86,16 +86,15 @@ export const useWorkbenchStore = defineStore("workbench", () => { } async function refreshLive(): Promise { - const [healthLive, health, restIndex, adapter, hwpodSpecs, hwpodNodeOps, liveBuilds] = await Promise.all([ + const [healthLive, health, restIndex, hwpodSpecs, hwpodNodeOps, liveBuilds] = await Promise.all([ liveCall("health/live", api.healthLive), liveCall("health", api.health), liveCall("REST index", api.restIndex), - liveCall("adapter", api.adapter), liveCall("hwpod specs", api.hwpod.specs), liveCall("hwpod node ops", api.hwpod.nodeOpsHealth), liveCall("live builds", api.liveBuilds) ]); - live.value = { healthLive, health, restIndex, adapter, hwpodSpecs, hwpodNodeOps, liveBuilds, loadedAt: new Date().toISOString() }; + live.value = { healthLive, health, restIndex, hwpodSpecs, hwpodNodeOps, liveBuilds, loadedAt: new Date().toISOString() }; } async function createSession(): Promise { diff --git a/web/hwlab-cloud-web/src/types/index.ts b/web/hwlab-cloud-web/src/types/index.ts index 4aaf57f7..75427e8a 100644 --- a/web/hwlab-cloud-web/src/types/index.ts +++ b/web/hwlab-cloud-web/src/types/index.ts @@ -53,7 +53,6 @@ export interface LiveSurface { healthLive: ApiResult; health: ApiResult; restIndex: ApiResult; - adapter: ApiResult; hwpodSpecs: ApiResult; hwpodNodeOps: ApiResult; liveBuilds: ApiResult;