fix: remove frontend json-rpc probe (#1344)

This commit is contained in:
Lyon
2026-06-16 22:27:51 +08:00
committed by GitHub
parent 32fec9a909
commit 32a2e2d39f
10 changed files with 17 additions and 48 deletions
+8
View File
@@ -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,
@@ -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",
+1 -29
View File
@@ -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 });
-6
View File
@@ -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,
@@ -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<HwpodSpecsResponse>({ 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<HwpodNodeOpsResponse>({ 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" }] }),
-1
View File
@@ -37,7 +37,6 @@ export const api = {
healthLive: (): Promise<ApiResult<LiveProbePayload>> => fetchJson("/health/live", { timeoutMs: 12000, timeoutName: "health/live" }),
health: (): Promise<ApiResult<LiveProbePayload>> => fetchJson("/health", { timeoutMs: 12000, timeoutName: "health" }),
restIndex: (): Promise<ApiResult<LiveProbePayload>> => fetchJson("/v1", { timeoutMs: 12000, timeoutName: "REST index" }),
adapter: (): Promise<ApiResult<LiveProbePayload>> => 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<ApiResult<LiveBuildsPayload>> => fetchJson("/v1/live-builds", { timeoutMs: 12000, timeoutName: "live builds" }),
skills: (): Promise<ApiResult<SkillsResponse>> => fetchJson("/v1/skills", { timeoutMs: 12000, timeoutName: "skills" })
};
@@ -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<string, unknown>[]));
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<string, unkn
</template>
</li>
</ol>
<LoadingState v-if="readableRows.length === 0" class="trace-empty" label="思考中..." compact />
<LoadingState v-if="readableRows.length === 0" class="trace-empty" :label="emptyTraceLabel" compact />
</div>
</details>
</section>
@@ -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";
+2 -3
View File
@@ -86,16 +86,15 @@ export const useWorkbenchStore = defineStore("workbench", () => {
}
async function refreshLive(): Promise<void> {
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<void> {
-1
View File
@@ -53,7 +53,6 @@ export interface LiveSurface {
healthLive: ApiResult<LiveProbePayload>;
health: ApiResult<LiveProbePayload>;
restIndex: ApiResult<LiveProbePayload>;
adapter: ApiResult<LiveProbePayload>;
hwpodSpecs: ApiResult<HwpodSpecsResponse>;
hwpodNodeOps: ApiResult<HwpodNodeOpsResponse>;
liveBuilds: ApiResult<LiveBuildsPayload>;