fix: stabilize v03 live web entry

This commit is contained in:
lyon
2026-06-14 02:40:58 +08:00
parent 155530d8d9
commit 5e751eff64
4 changed files with 42 additions and 2 deletions
@@ -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<LiveProbePayload>({ status: "ok", revision: "155530d8d9355887a23a08e341291994cc312f54" }));
assert.equal(passed.ok, true);
assert.equal(passed.data?.revision, "155530d8d9355887a23a08e341291994cc312f54");
});
function liveSurface(): LiveSurface {
const liveProbe = ok<LiveProbePayload>({ serviceId: "hwlab-cloud-api", environment: "v03", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd", observedAt: "2026-06-13T17:59:15.000Z" });
return {
+1
View File
@@ -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<void> {
+18 -2
View File
@@ -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<void> {
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<T>(label: string, call: () => Promise<ApiResult<T>>): Promise<ApiResult<T>> {
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> = {}): ChatMessage {
return { id: nextProtocolId("msg"), role, title: extra.title ?? (role === "user" ? "用户" : "Code Agent"), text, status, createdAt: new Date().toISOString(), ...extra };
}
@@ -1,13 +1,22 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const username = ref("admin");
const password = ref("");
async function submit(): Promise<void> {
await auth.login(username.value, password.value);
if (auth.isAuthenticated) await router.replace(nextPath());
}
function nextPath(): string {
const redirect = route.query.redirect;
return typeof redirect === "string" && redirect.startsWith("/") && !redirect.startsWith("//") ? redirect : "/workbench";
}
</script>