fix: update web probe workbench session endpoints

This commit is contained in:
lyon
2026-06-19 12:20:36 +08:00
parent b780503e78
commit fa2ad4845a
+96 -123
View File
@@ -721,7 +721,7 @@ function freshSessionCandidate(before, after) {
}
async function realignFreshSession(page, args, sessionOrConversationId) {
const selected = await selectConversationViaApi(page, args, sessionOrConversationId, "web-live-dom-probe-fresh-realign").catch((error) => ({ ok: false, phase: "select-conversation", error: error instanceof Error ? error.message : String(error) }));
const selected = await selectSessionViaRoute(page, args, sessionOrConversationId, "web-live-dom-probe-fresh-realign").catch((error) => ({ ok: false, phase: "route-session", error: error instanceof Error ? error.message : String(error) }));
const targetUrl = new URL(`/workbench/sessions/${encodeURIComponent(sessionOrConversationId)}`, args.url);
targetUrl.searchParams.set("projectId", args.projectId);
const target = targetUrl.toString();
@@ -760,29 +760,24 @@ async function waitForWorkbenchSessionRouteReady(page, args, timeoutMs) {
return { ok: workspace.ok && commandInput.ok, timeoutMs, workspace, commandInput, dom };
}
async function selectConversationViaApi(page, args, conversationId, updatedByClient) {
return page.evaluate(async ({ projectId, conversationId, updatedByClient }) => {
const workspaceResponse = await fetch(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { credentials: "same-origin" });
const workspaceBody = await workspaceResponse.json().catch(() => null);
const workspaceId = workspaceBody?.workspace?.workspaceId;
if (!workspaceId) return { ok: false, phase: "workspace", status: workspaceResponse.status, workspaceId: null, error: workspaceBody?.error ?? null };
const selectResponse = await fetch(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, {
method: "POST",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify({ projectId, conversationId, updatedByClient })
});
const selectBody = await selectResponse.json().catch(() => null);
return {
ok: selectResponse.ok,
phase: "select-conversation",
status: selectResponse.status,
workspaceId,
conversationId: selectBody?.workspace?.selectedConversationId ?? conversationId,
revision: selectBody?.workspace?.revision ?? null,
error: selectBody?.error ?? null
};
}, { projectId: args.projectId, conversationId, updatedByClient });
async function selectSessionViaRoute(page, args, conversationId, updatedByClient) {
const targetUrl = new URL(`/workbench/sessions/${encodeURIComponent(conversationId)}`, args.url);
const target = targetUrl.toString();
const navigation = await page.goto(target, { waitUntil: "domcontentloaded", timeout: Math.min(args.timeoutMs, 30000) })
.then((response) => ({ ok: true, status: response?.status() ?? null, url: page.url() }))
.catch((error) => ({ ok: false, status: null, error: error instanceof Error ? error.message : String(error), url: page.url() }));
const ready = await waitForWorkbenchSessionRouteReady(page, args, Math.min(args.timeoutMs, 30000));
return {
ok: navigation.ok && ready.ok,
phase: "route-session",
status: navigation.status,
sessionId: conversationId,
conversationId,
updatedByClient,
target,
navigation,
ready
};
}
function freshSessionAlignment(before, after) {
@@ -855,7 +850,7 @@ async function waitForSessionShellReady(page, args, actions) {
const workspace = document.querySelector("#workspace");
const create = document.querySelector("#session-create");
const input = document.querySelector("#command-input");
return Boolean(workspace && create && !create.disabled && input && !input.disabled && statusText && !/加载中/u.test(statusText));
return Boolean(workspace && create && !create.disabled && input && !input.disabled && !/加载中|等待 workspace/u.test(statusText));
}, null, { timeout: timeoutMs }).catch(() => null);
const after = await collectSessionState(page, args);
const result = { action: "session-shell-ready", ready: Boolean(ready), timeoutMs, before, after };
@@ -902,13 +897,15 @@ async function collectSessionState(page, args = null) {
};
});
if (!args) return dom;
const workspace = await fetchJsonFromPage(page, `/v1/workbench/workspace?projectId=${encodeURIComponent(args.projectId)}`, { timeoutMs: 8000 });
const conversations = await fetchJsonFromPage(page, `/v1/agent/conversations?projectId=${encodeURIComponent(args.projectId)}`, { timeoutMs: 8000 });
const currentSessionId = firstNonEmpty(dom.routeConversationId, dom.activeTab?.sessionId, dom.activeTab?.conversationId);
const sessions = await fetchJsonFromPage(page, "/v1/workbench/sessions?limit=50", { timeoutMs: 8000 });
const sessionDetail = currentSessionId ? await fetchJsonFromPage(page, `/v1/workbench/sessions/${encodeURIComponent(currentSessionId)}`, { timeoutMs: 8000 }) : null;
const sessionMessages = currentSessionId ? await fetchJsonFromPage(page, `/v1/workbench/sessions/${encodeURIComponent(currentSessionId)}/messages`, { timeoutMs: 8000 }) : null;
return {
...dom,
api: {
workspace: summarizeWorkspaceFetch(workspace),
conversations: summarizeConversationsFetch(conversations)
workspace: summarizeCurrentSessionFetch(currentSessionId, sessionDetail, sessionMessages),
conversations: summarizeWorkbenchSessionsFetch(sessions)
}
};
}
@@ -961,52 +958,65 @@ async function fetchJsonFromPage(page, requestPath, options = {}) {
}, { requestPath, timeoutMs });
}
function summarizeWorkspaceFetch(fetchResult) {
const workspace = fetchResult?.json?.workspace ?? fetchResult?.json?.data?.workspace ?? null;
const selected = workspace?.selectedConversation ?? null;
function summarizeCurrentSessionFetch(sessionId, detailFetch, messagesFetch) {
const detailBody = detailFetch?.json?.session ?? detailFetch?.json?.data?.session ?? detailFetch?.json?.data ?? detailFetch?.json ?? null;
const messages = messagesFromFetch(messagesFetch);
const latestMessage = messages.at(-1) ?? null;
return {
ok: fetchResult?.ok === true,
path: fetchResult?.path ?? null,
status: fetchResult?.status ?? null,
error: fetchResult?.error ?? null,
bodyPreview: fetchResult?.ok === true ? null : fetchResult?.bodyPreview ?? null,
workspaceId: workspace?.workspaceId ?? null,
projectId: normalizeProbeProjectId(workspace?.projectId) ?? null,
revision: workspace?.revision ?? null,
selectedConversationId: workspace?.selectedConversationId ?? selected?.conversationId ?? null,
selectedAgentSessionId: workspace?.selectedAgentSessionId ?? selected?.sessionId ?? null,
activeTraceId: workspace?.activeTraceId ?? selected?.lastTraceId ?? null,
selectedConversationStatus: selected?.status ?? workspace?.sessionStatus ?? null,
selectedMessagesCount: Array.isArray(selected?.messages) ? selected.messages.length : selected?.messagesCount ?? null
ok: detailFetch?.ok === true || messagesFetch?.ok === true,
path: detailFetch?.path ?? messagesFetch?.path ?? null,
status: detailFetch?.status ?? messagesFetch?.status ?? null,
error: detailFetch?.error ?? messagesFetch?.error ?? null,
bodyPreview: detailFetch?.ok === true || messagesFetch?.ok === true ? null : detailFetch?.bodyPreview ?? messagesFetch?.bodyPreview ?? null,
workspaceId: null,
projectId: normalizeProbeProjectId(detailBody?.projectId) ?? null,
revision: detailBody?.revision ?? detailBody?.updatedAt ?? null,
selectedConversationId: firstNonEmpty(detailBody?.sessionId, detailBody?.id, sessionId),
selectedAgentSessionId: firstNonEmpty(detailBody?.sessionId, detailBody?.id, sessionId),
activeTraceId: firstNonEmpty(detailBody?.lastTraceId, detailBody?.traceId, latestMessage?.traceId, latestMessage?.runnerTrace?.traceId),
selectedConversationStatus: detailBody?.status ?? detailBody?.state ?? latestMessage?.status ?? null,
selectedMessagesCount: messages.length
};
}
function summarizeConversationsFetch(fetchResult) {
const conversations = Array.isArray(fetchResult?.json?.conversations)
? fetchResult.json.conversations
: Array.isArray(fetchResult?.json?.data?.conversations)
? fetchResult.json.data.conversations
: [];
function summarizeWorkbenchSessionsFetch(fetchResult) {
const sessions = sessionsFromFetch(fetchResult);
return {
ok: fetchResult?.ok === true,
path: fetchResult?.path ?? null,
status: fetchResult?.status ?? null,
error: fetchResult?.error ?? null,
bodyPreview: fetchResult?.ok === true ? null : fetchResult?.bodyPreview ?? null,
count: conversations.length,
items: conversations.slice(0, 16).map((item) => ({
conversationId: item?.conversationId ?? null,
count: sessions.length,
items: sessions.slice(0, 16).map((item) => ({
conversationId: firstNonEmpty(item?.sessionId, item?.id),
projectId: normalizeProbeProjectId(item?.projectId) ?? null,
sessionId: item?.sessionId ?? null,
sessionId: firstNonEmpty(item?.sessionId, item?.id),
threadId: item?.threadId ?? null,
status: item?.status ?? null,
messagesCount: Array.isArray(item?.messages) ? item.messages.length : item?.messagesCount ?? null,
lastTraceId: item?.lastTraceId ?? null,
messagesCount: Array.isArray(item?.messages) ? item.messages.length : item?.messagesCount ?? item?.messageCount ?? null,
lastTraceId: firstNonEmpty(item?.lastTraceId, item?.traceId),
updatedAt: item?.updatedAt ?? null
}))
};
}
function sessionsFromFetch(fetchResult) {
if (Array.isArray(fetchResult?.json?.sessions)) return fetchResult.json.sessions;
if (Array.isArray(fetchResult?.json?.data?.sessions)) return fetchResult.json.data.sessions;
if (Array.isArray(fetchResult?.json?.items)) return fetchResult.json.items;
if (Array.isArray(fetchResult?.json?.data?.items)) return fetchResult.json.data.items;
return [];
}
function messagesFromFetch(fetchResult) {
if (Array.isArray(fetchResult?.json?.messages)) return fetchResult.json.messages;
if (Array.isArray(fetchResult?.json?.data?.messages)) return fetchResult.json.data.messages;
if (Array.isArray(fetchResult?.json?.items)) return fetchResult.json.items;
if (Array.isArray(fetchResult?.json?.data?.items)) return fetchResult.json.data.items;
return [];
}
async function submitPrompt(page, args, actions) {
const readyBefore = await waitForCommandReady(page, args, actions);
const fresh = [...actions].reverse().find((item) => item.action === "fresh-session") ?? null;
@@ -1280,7 +1290,7 @@ function classifyRunProbeError(error, context = {}) {
const login = failureDom.login && typeof failureDom.login === "object" ? failureDom.login : {};
if (/\/login(?:\?|$)/u.test(finalUrl) || failureDom.authState === "login" || login.cardVisible === true) return "auth-redirect-login";
if (/session_required|session-not-selected/u.test(message)) return "session-not-selected";
if (/pre-submit-realign|session-(?:route|workspace|active-tab)-not-aligned|select-conversation/u.test(message)) return "session-not-selected";
if (/pre-submit-realign|session-(?:route|workspace|active-tab)-not-aligned|route-session/u.test(message)) return "session-not-selected";
if (/composer|command-send|command bar|disabledReason|button_disabled/u.test(message)) return "composer-disabled";
if (/fresh session/u.test(message)) return "fresh-session-not-aligned";
if (/agent final response did not reach terminal DOM state/u.test(message)) return "agent-terminal-timeout";
@@ -1341,66 +1351,21 @@ async function collectCommandState(page) {
}
async function selectConversationInBrowserSession(page, args, actions) {
const result = await page.evaluate(async ({ projectId, conversationId }) => {
const summarizeAuthSession = (body) => ({
const authSession = await page.evaluate(async () => {
const response = await fetch("/auth/session", { credentials: "same-origin" });
const body = await response.json().catch(() => null);
return {
status: response.status,
authenticated: body?.authenticated ?? null,
userId: body?.user?.id ?? body?.actor?.id ?? null,
username: body?.user?.username ?? body?.actor?.username ?? null,
role: body?.user?.role ?? body?.actor?.role ?? null,
status: body?.user?.status ?? body?.actor?.status ?? null
});
const summarizeWorkspace = (workspace) => workspace ? {
workspaceId: workspace.workspaceId ?? null,
ownerUserId: workspace.ownerUserId ?? null,
projectId: workspace.projectId ?? null,
selectedConversationId: workspace.selectedConversationId ?? null,
selectedAgentSessionId: workspace.selectedAgentSessionId ?? null,
selectedConversationStatus: workspace.selectedConversation?.status ?? null,
selectedMessagesCount: workspace.selectedConversation?.messages?.length ?? workspace.selectedConversation?.messagesCount ?? null,
revision: workspace.revision ?? null
} : null;
const sessionResponse = await fetch("/auth/session", { credentials: "same-origin" });
const sessionBody = await sessionResponse.json().catch(() => null);
const workspaceResponse = await fetch(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { credentials: "same-origin" });
const workspaceBody = await workspaceResponse.json().catch(() => null);
const workspaceId = workspaceBody?.workspace?.workspaceId;
if (!workspaceId) return {
ok: false,
phase: "workspace",
status: workspaceResponse.status,
workspaceId: null,
authSession: summarizeAuthSession(sessionBody),
workspace: summarizeWorkspace(workspaceBody?.workspace),
bodyStatus: workspaceBody?.status ?? null,
error: workspaceBody?.error ?? null
accountStatus: body?.user?.status ?? body?.actor?.status ?? null
};
const selectResponse = await fetch(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, {
method: "POST",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify({ projectId, conversationId, updatedByClient: "web-live-dom-probe" })
});
const selectBody = await selectResponse.json().catch(() => null);
return {
ok: selectResponse.ok,
phase: "select-conversation",
status: selectResponse.status,
workspaceId,
authSession: summarizeAuthSession(sessionBody),
workspace: summarizeWorkspace(workspaceBody?.workspace),
responseStatus: selectBody?.status ?? null,
error: selectBody?.error ?? null,
conversationId: selectBody?.workspace?.selectedConversationId ?? conversationId,
selectedMessageCount: selectBody?.workspace?.selectedConversation?.messages?.length ?? selectBody?.workspace?.selectedConversation?.messagesCount ?? null,
sessionStatus: selectBody?.workspace?.selectedConversation?.status ?? selectBody?.workspace?.sessionStatus ?? null
};
}, { projectId: args.projectId, conversationId: args.conversationId });
actions.push({ action: "select-conversation", ...result });
if (!result.ok) throw new Error(`browser session select-conversation failed: ${JSON.stringify(result)}`);
await page.reload({ waitUntil: "domcontentloaded", timeout: args.timeoutMs });
await waitForAuthBootstrap(page, args, actions);
await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs });
await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
});
const result = await selectSessionViaRoute(page, args, args.conversationId, "web-live-dom-probe");
actions.push({ action: "select-session", authSession, ...result });
if (!result.ok) throw new Error(`browser session route selection failed: ${JSON.stringify(result)}`);
}
async function waitForMessageCards(page, args, actions) {
@@ -1471,19 +1436,27 @@ async function collectDomEvidence(page, args) {
const workspace = document.querySelector("#workspace");
const list = document.querySelector("#conversation-list");
const cards = [...document.querySelectorAll(".message-card")];
const conversationResponse = await fetch(`/v1/agent/conversations?projectId=${encodeURIComponent(projectId)}`, { credentials: "same-origin" }).catch(() => null);
const conversationBody = conversationResponse ? await conversationResponse.json().catch(() => null) : null;
const conversations = Array.isArray(conversationBody?.conversations) ? conversationBody.conversations : [];
const sessionsResponse = await fetch("/v1/workbench/sessions?limit=50", { credentials: "same-origin" }).catch(() => null);
const sessionsBody = sessionsResponse ? await sessionsResponse.json().catch(() => null) : null;
const sessions = Array.isArray(sessionsBody?.sessions)
? sessionsBody.sessions
: Array.isArray(sessionsBody?.data?.sessions)
? sessionsBody.data.sessions
: Array.isArray(sessionsBody?.items)
? sessionsBody.items
: Array.isArray(sessionsBody?.data?.items)
? sessionsBody.data.items
: [];
const sessionTabs = [...document.querySelectorAll("#session-tabs .session-tab")];
const activeSessionTab = sessionTabs.find((item) => item.getAttribute("data-active") === "true") ?? null;
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) ?? window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
const sessionIds = conversations.map((item) => item?.sessionId).filter((value) => typeof value === "string" && value).slice(0, 8);
const conversationTraceIds = conversations.map((item) => item?.lastTraceId).filter((value) => typeof value === "string" && value);
const sessionIds = sessions.map((item) => item?.sessionId ?? item?.id).filter((value) => typeof value === "string" && value).slice(0, 8);
const sessionTraceIds = sessions.map((item) => item?.lastTraceId ?? item?.traceId).filter((value) => typeof value === "string" && value);
const domTraceIds = cards.map((card) => {
const trace = card.querySelector(".trace-timeline, .message-trace");
return firstTraceId(card.getAttribute("data-trace-id"), trace?.getAttribute("data-trace-id"), trace?.getAttribute("data-traceid"), card.textContent, trace?.textContent, document.querySelector(".composer-mode")?.textContent);
}).filter((value) => typeof value === "string" && value);
const traceIds = [...new Set([...conversationTraceIds, ...domTraceIds])].slice(0, 8);
const traceIds = [...new Set([...sessionTraceIds, ...domTraceIds])].slice(0, 8);
const scroll = workspace ? {
scrollTop: Math.round(workspace.scrollTop),
scrollHeight: Math.round(workspace.scrollHeight),
@@ -1516,8 +1489,8 @@ async function collectDomEvidence(page, args) {
titles: sessionTabs.map((item) => item.querySelector("strong")?.textContent?.trim() ?? "").slice(0, 8),
sessionIds,
traceIds,
conversationsStatus: conversationResponse?.status ?? null,
conversationsCount: conversations.length
sessionsStatus: sessionsResponse?.status ?? null,
sessionsCount: sessions.length
},
messageCount: cards.length,
messageRoles: cards.map((card) => cardRole(card) ?? "unknown"),
@@ -1578,7 +1551,7 @@ function observeBootstrapRoutes(page) {
page.on("request", (request) => {
try {
const url = new URL(request.url());
if (!["/auth/bootstrap", "/auth/session", "/auth/workspace-bootstrap", "/v1/workbench/workspace"].includes(url.pathname)) return;
if (!["/auth/bootstrap", "/auth/session", "/auth/workspace-bootstrap", "/v1/workbench/sessions"].includes(url.pathname)) return;
routes.push({ method: request.method(), path: url.pathname });
} catch {
// Ignore malformed browser-internal URLs.
@@ -1592,7 +1565,7 @@ function summarizeBootstrapRoutes(routes) {
return {
authBootstrapRequested: paths.includes("/auth/bootstrap"),
authSessionRequested: paths.includes("/auth/session"),
workbenchWorkspaceRequested: paths.includes("/v1/workbench/workspace"),
workbenchSessionsRequested: paths.includes("/v1/workbench/sessions"),
legacyWorkspaceBootstrapRequested: paths.includes("/auth/workspace-bootstrap"),
routes: routes.slice(0, 20)
};