diff --git a/scripts/web-live-dom-probe.mjs b/scripts/web-live-dom-probe.mjs index fb1f52c2..360d53e0 100644 --- a/scripts/web-live-dom-probe.mjs +++ b/scripts/web-live-dom-probe.mjs @@ -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) };