From 5e751eff64ffbd53a67ebe6c698ac5bcaf046357 Mon Sep 17 00:00:00 2001 From: lyon Date: Sun, 14 Jun 2026 02:40:58 +0800 Subject: [PATCH] fix: stabilize v03 live web entry --- .../workbench-r3-live-hwpod-parity.test.ts | 14 +++++++++++++ web/hwlab-cloud-web/src/stores/auth.ts | 1 + web/hwlab-cloud-web/src/stores/workbench.ts | 20 +++++++++++++++++-- web/hwlab-cloud-web/src/views/LoginView.vue | 9 +++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) 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 18b34a2d..2ec7118e 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 @@ -3,6 +3,7 @@ import test from "node:test"; import type { ApiResult, HwpodNodeOpsResponse, HwpodSpecsResponse, LiveProbePayload, LiveSurface } from "../src/types/index.ts"; import { summarizeBuilds, summarizeHwpodNodeOps, summarizeProbeRows } from "../src/stores/workbench-live.ts"; +import { liveCall } from "../src/stores/workbench.ts"; test("R3 probe summary exposes React WorkbenchProbe rows", () => { const summary = summarizeProbeRows(liveSurface()); @@ -33,6 +34,19 @@ test("R3 HWPOD summary combines specs availability, node-ops and blocker stream" assert.ok(summary.streamLines.some((line) => line.includes("op_01_workspace_ls workspace.ls hwpod_node_unavailable"))); }); +test("R3 live refresh keeps partial evidence when one endpoint throws", async () => { + const failed = await liveCall("live builds", async () => { + throw new Error("network edge reset"); + }); + assert.equal(failed.ok, false); + assert.equal(failed.status, 0); + assert.match(failed.error ?? "", /live builds: network edge reset/); + + const passed = await liveCall("health/live", async () => ok({ status: "ok", revision: "155530d8d9355887a23a08e341291994cc312f54" })); + assert.equal(passed.ok, true); + assert.equal(passed.data?.revision, "155530d8d9355887a23a08e341291994cc312f54"); +}); + function liveSurface(): LiveSurface { const liveProbe = ok({ serviceId: "hwlab-cloud-api", environment: "v03", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd", observedAt: "2026-06-13T17:59:15.000Z" }); return { diff --git a/web/hwlab-cloud-web/src/stores/auth.ts b/web/hwlab-cloud-web/src/stores/auth.ts index a93dff84..9d799db4 100644 --- a/web/hwlab-cloud-web/src/stores/auth.ts +++ b/web/hwlab-cloud-web/src/stores/auth.ts @@ -61,6 +61,7 @@ export const useAuthStore = defineStore("auth", () => { session.value = sessionFromPayload(response.data); authState.value = "authenticated"; checked.value = true; + document.body.dataset.authState = "authenticated"; } async function logout(): Promise { diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 060414f8..97eb382b 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -2,7 +2,7 @@ import { computed, ref } from "vue"; import { defineStore } from "pinia"; import { api } from "@/api"; import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; -import type { AgentChatResponse, AgentChatResultResponse, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, WorkspaceRecord } from "@/types"; +import type { AgentChatResponse, AgentChatResultResponse, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, WorkspaceRecord } from "@/types"; import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils"; import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session"; @@ -60,7 +60,15 @@ export const useWorkbenchStore = defineStore("workbench", () => { } async function refreshLive(): Promise { - const [healthLive, health, restIndex, adapter, hwpodSpecs, hwpodNodeOps, liveBuilds] = await Promise.all([api.healthLive(), api.health(), api.restIndex(), api.adapter(), api.hwpod.specs(), api.hwpod.nodeOpsHealth(), api.liveBuilds()]); + const [healthLive, health, restIndex, adapter, 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() }; } @@ -313,6 +321,14 @@ function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] return source.map((message) => ({ ...message, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" })); } +export async function liveCall(label: string, call: () => Promise>): Promise> { + try { + return await call(); + } catch (error) { + return { ok: false, status: 0, data: null, error: `${label}: ${error instanceof Error ? error.message : String(error)}` }; + } +} + function makeMessage(role: ChatMessage["role"], text: string, status: ChatMessage["status"], extra: Partial = {}): ChatMessage { return { id: nextProtocolId("msg"), role, title: extra.title ?? (role === "user" ? "用户" : "Code Agent"), text, status, createdAt: new Date().toISOString(), ...extra }; } diff --git a/web/hwlab-cloud-web/src/views/LoginView.vue b/web/hwlab-cloud-web/src/views/LoginView.vue index 194d1965..60a89698 100644 --- a/web/hwlab-cloud-web/src/views/LoginView.vue +++ b/web/hwlab-cloud-web/src/views/LoginView.vue @@ -1,13 +1,22 @@