fix: 渐进加载 AgentRun 分页快照
This commit is contained in:
@@ -25,6 +25,7 @@ export async function handleAgentObserverHttp(request, response, options = {}) {
|
||||
}
|
||||
const requestUrl = new URL(request.url ?? "/v1/agent-observer/events", "http://agent-observer.local");
|
||||
const resumeAfterId = text(requestUrl.searchParams.get("afterId") ?? request.headers?.["last-event-id"]);
|
||||
const liveOnly = requestUrl.searchParams.get("mode") === "live";
|
||||
|
||||
try {
|
||||
await (bridge.liveReady ?? bridge.ready);
|
||||
@@ -75,7 +76,20 @@ export async function handleAgentObserverHttp(request, response, options = {}) {
|
||||
if (!response.writableEnded) response.end();
|
||||
};
|
||||
|
||||
const handoff = createWorkbenchKafkaRefreshHandoff({
|
||||
if (liveOnly) {
|
||||
const unsubscribe = bridge.subscribeLiveHwlabEvents((envelope) => { void enqueue("hwlab.event.v1", envelope); });
|
||||
cleanup.push(() => unsubscribe?.());
|
||||
await enqueue("agent-observer.connected", {
|
||||
type: "connected",
|
||||
status: "live",
|
||||
deliverySemantics: "agentrun-snapshot-then-kafka-live",
|
||||
authority: "hwlab.event.v1",
|
||||
replay: null,
|
||||
serverSentAt: new Date().toISOString(),
|
||||
valuesPrinted: false
|
||||
});
|
||||
} else {
|
||||
const handoff = createWorkbenchKafkaRefreshHandoff({
|
||||
allowUnscoped: true,
|
||||
resumeAfterId,
|
||||
liveBufferLimit: policy.liveBufferLimit,
|
||||
@@ -109,14 +123,15 @@ export async function handleAgentObserverHttp(request, response, options = {}) {
|
||||
valuesPrinted: false
|
||||
}),
|
||||
onFailure: fail
|
||||
});
|
||||
cleanup.push(() => handoff.stop("connection-closed"));
|
||||
});
|
||||
cleanup.push(() => handoff.stop("connection-closed"));
|
||||
|
||||
try {
|
||||
await handoff.start();
|
||||
} catch (error) {
|
||||
await fail(error);
|
||||
return;
|
||||
try {
|
||||
await handoff.start();
|
||||
} catch (error) {
|
||||
await fail(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!active()) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Responsibility: expose the AgentRun manager durable run collection through the authenticated HWLAB origin.
|
||||
|
||||
const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";
|
||||
const AGENTRUN_MANAGER_HOST_PATTERN = /^agentrun-mgr\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\.svc\.cluster\.local$/u;
|
||||
|
||||
export async function handleAgentObserverRunsHttp(request, response, url, options = {}) {
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405, { allow: "GET", "content-type": "application/json; charset=utf-8" });
|
||||
response.end(JSON.stringify({ ok: false, error: { code: "method_not_allowed", message: "GET required." } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const env = options.env ?? process.env;
|
||||
const managerUrl = resolveManagerUrl(env);
|
||||
const apiKey = text(env.AGENTRUN_API_KEY) || text(env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY) || text(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_API_KEY) || text(env.HWLAB_API_KEY);
|
||||
const timeoutMs = positiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 30_000);
|
||||
const target = new URL("/api/v1/runs", managerUrl);
|
||||
target.search = url.search;
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
accept: "application/json",
|
||||
"x-source-service-id": "hwlab-cloud-api",
|
||||
"x-hwlab-delegation": "agent-observer-runs-v1",
|
||||
};
|
||||
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
|
||||
const delegated = await (options.fetchImpl ?? fetch)(target, { method: "GET", headers, signal: controller.signal });
|
||||
const body = await delegated.text();
|
||||
const payload = parseJson(body);
|
||||
const data = payload?.data ?? payload;
|
||||
if (!delegated.ok || payload?.ok === false || !data || typeof data !== "object") {
|
||||
sendJson(response, delegated.status || 502, {
|
||||
ok: false,
|
||||
error: {
|
||||
code: text(payload?.error?.code) || text(payload?.failureKind) || "agentrun_run_collection_failed",
|
||||
message: text(payload?.error?.message) || text(payload?.message) || `AgentRun run collection returned HTTP ${delegated.status}.`,
|
||||
httpStatus: delegated.status,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, {
|
||||
ok: true,
|
||||
status: "ok",
|
||||
authority: "agentrun-manager-durable-store",
|
||||
...data,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
valuesPrinted: false,
|
||||
});
|
||||
} catch (error) {
|
||||
const timedOut = controller.signal.aborted || error?.name === "AbortError";
|
||||
sendJson(response, timedOut ? 504 : 502, {
|
||||
ok: false,
|
||||
error: {
|
||||
code: timedOut ? "agentrun_run_collection_timeout" : "agentrun_run_collection_unavailable",
|
||||
message: timedOut ? `AgentRun run collection exceeded ${timeoutMs}ms.` : (error instanceof Error ? error.message : String(error)),
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveManagerUrl(env) {
|
||||
const raw = text(env.AGENTRUN_MGR_URL) || text(env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL) || text(env.AGENTRUN_MANAGER_URL) || DEFAULT_AGENTRUN_MGR_URL;
|
||||
const parsed = new URL(raw);
|
||||
const allowLocal = truthy(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL);
|
||||
const localAllowed = allowLocal && ["127.0.0.1", "localhost"].includes(parsed.hostname);
|
||||
if (!AGENTRUN_MANAGER_HOST_PATTERN.test(parsed.hostname) && !localAllowed) {
|
||||
throw Object.assign(new Error("AgentRun manager URL must use the declared internal k3s service."), { code: "agentrun_manager_url_invalid" });
|
||||
}
|
||||
return raw.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function positiveInteger(value, fallback) {
|
||||
const number = Number(value);
|
||||
return Number.isInteger(number) && number > 0 ? number : fallback;
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
function text(value) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : "";
|
||||
}
|
||||
|
||||
function parseJson(value) {
|
||||
try { return value ? JSON.parse(value) : null; } catch { return null; }
|
||||
}
|
||||
|
||||
function sendJson(response, status, payload) {
|
||||
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
||||
response.end(JSON.stringify(payload));
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import { handleWorkbenchDebugFakeSseHttp } from "./workbench-debug-fake-sse.ts";
|
||||
import { handleWorkbenchKafkaSseDebugHttp } from "./workbench-kafka-sse-debug.ts";
|
||||
import { handleWorkbenchLaunchHttp } from "./server-workbench-launch-http.ts";
|
||||
import { handleAgentObserverHttp } from "./server-agent-observer-http.ts";
|
||||
import { handleAgentObserverRunsHttp } from "./server-agent-observer-runs-http.ts";
|
||||
import { startWorkbenchEmptySessionGc } from "./workbench-empty-session-gc.ts";
|
||||
import { startHwlabKafkaEventBridge } from "./kafka-event-bridge.ts";
|
||||
import { handleM3IoControlHttp } from "./server-m3-http.ts";
|
||||
@@ -764,6 +765,11 @@ async function handleRestAdapter(request, response, url, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/v1/agent-observer/runs") {
|
||||
await handleAgentObserverRunsHttp(request, response, url, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/v1/workbench/launches") {
|
||||
await handleWorkbenchLaunchHttp(request, response, options);
|
||||
return;
|
||||
@@ -1018,7 +1024,7 @@ function navIdForRestPath(pathname, method = "GET") {
|
||||
if (pathname === "/v1/users/me/profile" || pathname === "/v1/users/me/password") return "system.settings";
|
||||
if (pathname === "/v1/workbench/debug/fake-sse" || pathname.startsWith("/v1/workbench/debug/fake-sse/") || pathname === "/v1/workbench/debug/kafka-sse" || pathname.startsWith("/v1/workbench/debug/kafka-sse/")) return "workbench.debug";
|
||||
if (pathname === "/v1/workbench/events" || pathname === "/v1/workbench/projection-events" || pathname === "/v1/workbench/launches" || pathname === "/v1/workbench/sessions" || pathname.startsWith("/v1/workbench/sessions/") || pathname.startsWith("/v1/workbench/turns/") || pathname.startsWith("/v1/workbench/traces/")) return "workbench.code";
|
||||
if (pathname === "/v1/agent-observer/events") return "workbench.code";
|
||||
if (pathname === "/v1/agent-observer/events" || pathname === "/v1/agent-observer/runs") return "workbench.code";
|
||||
if (pathname === "/v1/agent/chat" || pathname === "/v1/agent/sessions" || pathname.startsWith("/v1/agent/sessions/") || pathname === "/v1/agent/chat/inspect" || pathname.startsWith("/v1/agent/chat/result/") || pathname.startsWith("/v1/agent/turns/") || pathname.startsWith("/v1/agent/traces/") || pathname === "/v1/agent/chat/cancel" || pathname === "/v1/agent/chat/steer") return "workbench.code";
|
||||
if (pathname === "/v1/admin/provider-profiles" || pathname.startsWith("/v1/admin/provider-profiles/")) return "admin.providerProfiles";
|
||||
if (pathname === "/v1/admin/secrets" || pathname.startsWith("/v1/admin/secrets/")) return "admin.secrets";
|
||||
|
||||
Reference in New Issue
Block a user