fix(web): avoid live 404 probes

This commit is contained in:
Codex
2026-06-03 18:47:42 +08:00
parent b556f4160d
commit d11c91d36e
4 changed files with 44 additions and 5 deletions
@@ -46,6 +46,7 @@ export function isCloudApiProxyRoute(method, pathname) {
}
if (normalizedMethod === "POST") {
return POST_PROXY_ROUTES.has(normalizedPath) ||
normalizedPath.startsWith("/v1/rpc/") ||
normalizedPath.startsWith("/v1/agent/sessions/") ||
isDevicePodPostProxyRoute(normalizedPath) ||
isWorkbenchWorkspaceWriteProxyRoute(normalizedMethod, normalizedPath);
@@ -24,6 +24,21 @@ test("cloud web proxies skills routes through authenticated cloud-api", () => {
});
});
test("cloud web proxies authenticated REST RPC bridge routes", () => {
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/rpc/system.health"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/rpc/system.health"
});
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/rpc/system.health"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "GET /v1/rpc/system.health"
});
});
test("cloud web proxies only supported authenticated device-pod write routes", () => {
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/device-pods/D601-F103-V2/jobs"), {
proxy: true,
+4
View File
@@ -57,6 +57,10 @@ assertIncludes(appSource, "SkillsView", "skills component must exist");
assertIncludes(appSource, "SettingsView", "settings component must exist");
assertIncludes(appTsx, "useWorkbenchStore(auth.authState === \"authenticated\")", "Workbench data requests must wait until auth is authenticated");
assertIncludes(apiClientSource, "adapter: (): Promise<ApiResult<LiveProbePayload>> => fetchJson(\"/v1/rpc/system.health\", { method: \"POST\"", "adapter health must use POST /v1/rpc/system.health");
assertIncludes(appSource, "selectVisibleDevicePodId", "Device Pod detail probes must be selected from the authenticated visible list");
assertIncludes(appSource, "api.devicePods()", "Device Pod list must be fetched before detail probes");
assert.doesNotMatch(appSource, /api\.devicePods\(\),\s*api\.devicePodStatus/u, "Device Pod status must not be requested in the initial parallel live probe");
assertIncludes(readRepo("internal/dev-entrypoint/cloud-web-routes.mjs"), "normalizedPath.startsWith(\"/v1/rpc/\")", "Cloud Web must proxy REST RPC bridge routes to cloud-api");
assertIncludes(appSource, "id=\"command-input\"", "React runtime must render command input selector");
assertIncludes(appSource, "id=\"conversation-list\"", "React runtime must render conversation list selector");
assertIncludes(appSource, "id=\"device-pod-sidebar\"", "React runtime must render Device Pod sidebar selector");
+24 -5
View File
@@ -3,10 +3,14 @@ import { useCallback, useEffect, useMemo, useReducer } from "react";
import { api } from "../services/api/client";
import type {
AgentChatResponse,
ApiResult,
ChatMessage,
CodeAgentAvailability,
DevicePodEventsResponse,
ConversationRecord,
DevicePodItem,
DevicePodListResponse,
DevicePodStatusResponse,
LiveSurface,
ProviderProfile,
SessionTab,
@@ -129,16 +133,19 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
}, []);
const refreshLive = useCallback(async (devicePodId?: string) => {
const selected = devicePodId ?? state.selectedDevicePodId;
const [healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents] = await Promise.all([
const requestedDevicePodId = devicePodId ?? state.selectedDevicePodId;
const [healthLive, health, restIndex, adapter, devicePods] = await Promise.all([
api.healthLive(),
api.health(),
api.restIndex(),
api.adapter(),
api.devicePods(),
api.devicePodStatus(selected),
api.devicePodEvents(selected)
api.devicePods()
]);
const visibleDevicePods = devicePodsFromResult(devicePods);
const selected = selectVisibleDevicePodId(devicePods.data, visibleDevicePods, requestedDevicePodId);
const [devicePodStatus, devicePodEvents] = selected
? await Promise.all([api.devicePodStatus(selected), api.devicePodEvents(selected)])
: [null, null] satisfies [ApiResult<DevicePodStatusResponse> | null, ApiResult<DevicePodEventsResponse> | null];
const live: LiveSurface = { healthLive, health, restIndex, adapter, devicePods, devicePodStatus, devicePodEvents, loadedAt: new Date().toISOString() };
dispatch({ type: "live:set", live, availability: availabilityFromLive(live), selectedDevicePodId: selected });
}, [state.selectedDevicePodId]);
@@ -320,3 +327,15 @@ export function devicePodsFromLive(live: LiveSurface | null): DevicePodItem[] {
const payload = live?.devicePods.data;
return payload?.devicePods ?? payload?.items ?? [];
}
function devicePodsFromResult(result: ApiResult<DevicePodListResponse>): DevicePodItem[] {
return result.data?.devicePods ?? result.data?.items ?? [];
}
function selectVisibleDevicePodId(payload: DevicePodListResponse | null, pods: DevicePodItem[], requestedDevicePodId: string): string {
const payloadSelected = nonEmptyString(payload?.selectedDevicePodId);
const requested = nonEmptyString(requestedDevicePodId);
const requestedVisible = requested ? pods.some((pod) => pod.devicePodId === requested) : false;
if (requested && requestedVisible) return requested;
return payloadSelected ?? pods[0]?.devicePodId ?? "";
}