fix(agent): harden runner integration and QA

This commit is contained in:
huanghuoguoguo
2026-08-01 09:23:41 +08:00
parent 55f42a4ebc
commit 79611a5513
38 changed files with 1315 additions and 132 deletions
+61 -15
View File
@@ -424,18 +424,33 @@ async function fetchDebugChatHistory(page, { backendUrl, pipelineId, sessionType
}
return await page.evaluate(async ({ backendUrl, pipelineId, sessionType }) => {
const token = localStorage.getItem("token") || "";
const response = await fetch(
`${backendUrl.replace(/\/$/, "")}/api/v1/pipelines/${encodeURIComponent(pipelineId)}/ws/messages/${encodeURIComponent(sessionType)}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} },
);
const json = await response.json().catch(() => ({}));
return {
status: response.ok && json.code === 0 ? "ready" : "fail",
http_status: response.status,
code: json.code ?? null,
messages: json.data?.messages || [],
reason: response.ok && json.code === 0 ? "" : json.msg || `Debug Chat history returned HTTP ${response.status}.`,
};
try {
const response = await fetch(
`${backendUrl.replace(/\/$/, "")}/api/v1/pipelines/${encodeURIComponent(pipelineId)}/ws/messages/${encodeURIComponent(sessionType)}`,
{
headers: token
? {
Authorization: `Bearer ${token}`,
"X-Workspace-Id": localStorage.getItem("langbot_active_workspace_uuid") || "",
}
: {},
},
);
const json = await response.json().catch(() => ({}));
return {
status: response.ok && json.code === 0 ? "ready" : "fail",
http_status: response.status,
code: json.code ?? null,
messages: json.data?.messages || [],
reason: response.ok && json.code === 0 ? "" : json.msg || `Debug Chat history returned HTTP ${response.status}.`,
};
} catch (error) {
return {
status: "retryable",
messages: [],
reason: `Debug Chat history request failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}, { backendUrl, pipelineId, sessionType });
}
@@ -454,6 +469,10 @@ async function waitForFinalDebugChatAssistant(page, {
while (Date.now() < deadline) {
lastHistory = await fetchDebugChatHistory(page, { backendUrl, pipelineId, sessionType });
if (lastHistory.status === "fail") return lastHistory;
if (lastHistory.status === "retryable") {
await page.waitForTimeout(Math.min(250, Math.max(1, deadline - Date.now())));
continue;
}
const assistants = lastHistory.messages.filter((message) => message.role === "assistant");
const latest = assistants.at(-1);
if (assistants.length > beforeAssistantCount && latest?.is_final === true) {
@@ -480,12 +499,23 @@ export async function attachDebugChatImage(page, imagePath) {
if (!await input.count()) {
return { status: "fail", reason: "Could not find a Debug Chat image upload input." };
}
const previews = page.locator('[data-debug-chat-attachment-preview="true"]');
const beforePreviewCount = await previews.count();
await input.setInputFiles(imagePath);
await page.locator("img").last().waitFor({ state: "visible", timeout: 10_000 }).catch(() => {});
const previewVisible = await previews
.nth(beforePreviewCount)
.waitFor({ state: "visible", timeout: 10_000 })
.then(() => true)
.catch(() => false);
if (!previewVisible) {
return { status: "fail", reason: "Debug Chat did not show the selected image preview." };
}
return { status: "ready", reason: `Attached image fixture: ${imagePath}` };
}
export async function sendDebugChatPrompt(page, prompt, imagePath = "") {
const sentImages = page.locator('[data-debug-chat-message-image="true"]');
const beforeSentImageCount = imagePath ? await sentImages.count() : 0;
const imageResult = await attachDebugChatImage(page, imagePath);
if (imageResult.status === "fail") return imageResult;
@@ -500,6 +530,16 @@ export async function sendDebugChatPrompt(page, prompt, imagePath = "") {
const clickedSend = await clickFirstVisible(page, ["Send", "发送", "提交"], 1_500);
if (!clickedSend) await page.keyboard.press("Enter");
await page.getByText(prompt, { exact: false }).last().waitFor({ state: "visible", timeout: 10_000 }).catch(() => {});
if (imagePath) {
const sentImageVisible = await sentImages
.nth(beforeSentImageCount)
.waitFor({ state: "visible", timeout: 15_000 })
.then(() => true)
.catch(() => false);
if (!sentImageVisible) {
return { status: "fail", reason: "The sent Debug Chat message did not render its image attachment." };
}
}
return true;
}
@@ -535,7 +575,7 @@ export async function runDebugChatPrompt(page, {
}
const responseStartedAt = Date.now();
await waitForExpectedDebugChatText(page, {
const expectedTextPromise = waitForExpectedDebugChatText(page, {
expectedText,
expectedTexts: requiredExpectedTexts,
minExpectedCount,
@@ -545,13 +585,19 @@ export async function runDebugChatPrompt(page, {
beforeText,
failureSignals,
});
const finalAssistant = await waitForFinalDebugChatAssistant(page, {
const finalAssistantPromise = waitForFinalDebugChatAssistant(page, {
backendUrl,
pipelineId,
sessionType,
beforeAssistantCount: beforeHistoryAssistantCount,
timeoutMs: Math.max(1, responseTimeoutMs - (Date.now() - responseStartedAt)),
});
if (backendUrl && pipelineId && sessionType) {
await Promise.race([expectedTextPromise, finalAssistantPromise]);
} else {
await expectedTextPromise;
}
const finalAssistant = await finalAssistantPromise;
await waitForDebugChatTextStable(page);
const afterText = await bodyText(page);
+184 -3
View File
@@ -1,8 +1,11 @@
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { basename, join, resolve } from "node:path";
import { env } from "node:process";
const secretRe = /(?:authorization|bearer|token|secret|password|api[_-]?key|jwt|oauth)\s*[:=]\s*["']?[^"',\s]+/gi;
const ACTIVE_WORKSPACE_STORAGE_KEY = "langbot_active_workspace_uuid";
const workspaceUuidCache = new Map();
export function redact(text) {
return String(text ?? "")
@@ -30,6 +33,19 @@ export function localIsoWithOffset(date = new Date()) {
return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}.${ms}${sign}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`;
}
export function boxWorkspaceNamespace(instanceUuid, workspaceUuid) {
const instance = String(instanceUuid || "").trim();
const workspace = String(workspaceUuid || "").trim();
if (!instance || !workspace) {
throw new Error("Box Workspace namespace requires instance and Workspace UUIDs.");
}
const digest = createHash("sha256")
.update(`${instance}\0${workspace}`, "utf8")
.digest("hex")
.slice(0, 24);
return `ws-${digest}`;
}
export function evidencePaths(caseId) {
const runId = env.LBS_RUN_ID || `${timestampSlug()}-${caseId}`;
const evidenceDir = resolve(env.LBS_EVIDENCE_DIR || join("reports", "evidence", runId));
@@ -103,6 +119,27 @@ export async function writeResult(paths, result) {
}
}
export function isTaskFailed(task) {
const status = String(task?.status || task?.state || "").toLowerCase();
const runtimeStatus = String(task?.runtime?.status || task?.runtime?.state || "").toLowerCase();
return ["failed", "error", "cancelled", "canceled"].includes(status)
|| ["failed", "error", "cancelled", "canceled"].includes(runtimeStatus)
|| task?.failed === true
|| Boolean(task?.error)
|| Boolean(task?.runtime?.exception);
}
export function isTaskComplete(task) {
if (isTaskFailed(task)) return false;
const status = String(task?.status || task?.state || "").toLowerCase();
const runtimeStatus = String(task?.runtime?.status || task?.runtime?.state || "").toLowerCase();
return ["done", "completed", "success", "succeeded", "finished"].includes(status)
|| ["done", "completed", "success", "succeeded", "finished"].includes(runtimeStatus)
|| task?.done === true
|| task?.completed === true
|| task?.runtime?.done === true;
}
function browserDiagnosticFindings(source, text) {
const findings = [];
const lines = String(text || "").split(/\r?\n/);
@@ -231,11 +268,98 @@ export async function readRecoveryKey(repo = env.LANGBOT_REPO || "") {
return match?.[1] || "";
}
export async function apiJson(backendUrl, path, { method = "GET", token = "", body } = {}) {
function workspaceCacheKey(backendUrl, token) {
return `${backendUrl.replace(/\/$/, "")}\0${token}`;
}
function isAccountScopedApi(path, method) {
const pathname = path.split("?", 1)[0];
if (pathname === "/api/v1/workspaces/bootstrap") return true;
if (pathname === "/api/v1/workspaces" && method === "GET") return true;
return [
"/api/v1/user/check-token",
"/api/v1/user/info",
"/api/v1/user/account-info",
"/api/v1/user/change-password",
].includes(pathname);
}
export async function resolveWorkspaceUuid(
backendUrl,
token,
preferredWorkspaceUuid = env.LANGBOT_WORKSPACE_UUID || "",
) {
if (!token) throw new Error("A user token is required to resolve the active Workspace.");
const normalizedBackendUrl = backendUrl.replace(/\/$/, "");
const normalizedPreferred = preferredWorkspaceUuid.trim();
const cacheKey = workspaceCacheKey(normalizedBackendUrl, token);
const cached = workspaceUuidCache.get(cacheKey);
if (cached && (!normalizedPreferred || cached === normalizedPreferred)) return cached;
const response = await fetch(`${normalizedBackendUrl}/api/v1/workspaces/bootstrap`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const json = await response.json().catch(() => ({}));
if (response.status >= 400 || json.code !== 0) {
throw new Error(json.msg || `Workspace bootstrap failed with HTTP ${response.status}.`);
}
const workspaces = json.data?.workspaces || [];
const workspaceUuids = workspaces
.map((entry) => entry.workspace?.uuid || entry.uuid || "")
.filter(Boolean);
let workspaceUuid = normalizedPreferred;
if (workspaceUuid && !workspaceUuids.includes(workspaceUuid)) {
throw new Error(`Configured Workspace ${workspaceUuid} is not available to the authenticated Account.`);
}
if (!workspaceUuid && workspaceUuids.length === 1) {
[workspaceUuid] = workspaceUuids;
}
if (!workspaceUuid && workspaceUuids.length === 0) {
throw new Error("The authenticated Account has no available Workspace.");
}
if (!workspaceUuid) {
throw new Error(
"The authenticated Account has multiple Workspaces; set LANGBOT_WORKSPACE_UUID for this QA run.",
);
}
workspaceUuidCache.set(cacheKey, workspaceUuid);
return workspaceUuid;
}
export async function authenticatedApiHeaders(
backendUrl,
token,
{ contentType = "application/json", workspaceUuid = "" } = {},
) {
const resolvedWorkspaceUuid = await resolveWorkspaceUuid(backendUrl, token, workspaceUuid);
return {
Authorization: `Bearer ${token}`,
...(contentType ? { "Content-Type": contentType } : {}),
"X-Workspace-Id": resolvedWorkspaceUuid,
};
}
export async function apiJson(
backendUrl,
path,
{ method = "GET", token = "", body, skipWorkspace = false, workspaceUuid = "" } = {},
) {
const normalizedMethod = method.toUpperCase();
const headers = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`;
if (token) {
headers.Authorization = `Bearer ${token}`;
if (!skipWorkspace && !isAccountScopedApi(path, normalizedMethod)) {
headers["X-Workspace-Id"] = await resolveWorkspaceUuid(backendUrl, token, workspaceUuid);
}
}
const response = await fetch(`${backendUrl.replace(/\/$/, "")}${path}`, {
method,
method: normalizedMethod,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
@@ -301,6 +425,38 @@ export async function setBrowserToken(page, frontendUrl, token) {
await page.evaluate((value) => localStorage.setItem("token", value), token);
}
export async function ensureBrowserWorkspace(
page,
backendUrl,
preferredWorkspaceUuid = env.LANGBOT_WORKSPACE_UUID || "",
) {
const browserContext = await page.evaluate((storageKey) => ({
token: localStorage.getItem("token") || "",
workspaceUuid: localStorage.getItem(storageKey) || "",
}), ACTIVE_WORKSPACE_STORAGE_KEY);
if (!browserContext.token) {
return { status: "blocked", reason: "Browser profile has no localStorage token.", workspace_uuid: "" };
}
try {
const workspaceUuid = await resolveWorkspaceUuid(
backendUrl,
browserContext.token,
preferredWorkspaceUuid || browserContext.workspaceUuid,
);
await page.evaluate(({ storageKey, workspaceUuid: value }) => {
localStorage.setItem(storageKey, value);
}, { storageKey: ACTIVE_WORKSPACE_STORAGE_KEY, workspaceUuid });
return {
status: "pass",
reason: "Browser Workspace selection is initialized.",
workspace_uuid: workspaceUuid,
};
} catch (error) {
return { status: "blocked", reason: error.message, workspace_uuid: "" };
}
}
export async function verifyBrowserToken(page, backendUrl) {
return await page.evaluate(async (baseUrl) => {
const token = localStorage.getItem("token");
@@ -349,11 +505,23 @@ export async function ensureAuthenticatedBrowser(page, {
reason: error.message,
}));
if (current.authenticated) {
const workspace = await ensureBrowserWorkspace(page, backendUrl);
if (workspace.status !== "pass") {
return {
status: workspace.status,
reason: workspace.reason,
backend_token_check: null,
browser_token_check: current,
workspace,
injected: false,
};
}
return {
status: "pass",
reason: "Existing browser token is valid.",
backend_token_check: null,
browser_token_check: current,
workspace,
injected: false,
};
}
@@ -381,11 +549,24 @@ export async function ensureAuthenticatedBrowser(page, {
};
}
const workspace = await ensureBrowserWorkspace(page, backendUrl);
if (workspace.status !== "pass") {
return {
status: workspace.status,
reason: workspace.reason,
backend_token_check: auth.check,
browser_token_check: browserCheck,
workspace,
injected: true,
};
}
return {
status: "pass",
reason: "Browser token injected from local recovery login.",
backend_token_check: auth.check,
browser_token_check: browserCheck,
workspace,
injected: true,
};
}