diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 64ff2234..40db6596 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -367,6 +367,7 @@ async function workbenchSessionSnapshot() { async function sendPrompt(text) { if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text"); const beforeUrl = currentPageUrl(); + const beforeEvidence = await promptSideEffectSnapshot(); const primaryEditor = page.locator("#command-input").last(); const editor = await primaryEditor.isVisible().catch(() => false) ? primaryEditor @@ -407,6 +408,17 @@ async function sendPrompt(text) { const chatResponse = await chatResponsePromise; await page.waitForTimeout(500); if (chatResponse?.waitError) { + const sideEffect = await waitForPromptSideEffect(beforeEvidence, 12000); + if (sideEffect.submitted) { + return { + beforeUrl, + afterUrl: currentPageUrl(), + textHash: sha256Text(text), + textBytes: Buffer.byteLength(text), + chatSubmit: { status: null, statusText: null, urlPath: "/v1/agent/chat", waitError: chatResponse.waitError, sideEffectObserved: true, sideEffect }, + pageId + }; + } throw new Error("sendPrompt did not observe POST /v1/agent/chat response after submit: " + (chatResponse.waitError.message || chatResponse.waitError.name || "timeout")); } const chatStatus = chatResponse.status(); @@ -422,6 +434,47 @@ async function sendPrompt(text) { return { beforeUrl, afterUrl: currentPageUrl(), textHash: sha256Text(text), textBytes: Buffer.byteLength(text), chatSubmit: { status: chatStatus, statusText: chatResponse.statusText(), urlPath: chatUrlPath }, pageId }; } +async function waitForPromptSideEffect(beforeEvidence, timeoutMs) { + await page.waitForFunction((before) => { + const current = (() => { + const text = document.body?.innerText || ""; + const runIds = Array.from(new Set(text.match(/run_[A-Za-z0-9_:-]+/gu) || [])).slice(-20); + const traceIds = Array.from(new Set(text.match(/trc_[A-Za-z0-9_:-]+/gu) || [])).slice(-20); + const running = /Trace running|最近\s*\d+\s*(?:秒|分钟|分|小时)前|Code Agent\s*耗时/iu.test(text); + const executionError = /AgentRun error|agentrun:error:|provider-stream-disconnected|provider-unavailable/iu.test(text); + const textBytes = new TextEncoder().encode(text).length; + return { runIds, traceIds, running, executionError, textBytes }; + })(); + const beforeRuns = Array.isArray(before?.runIds) ? before.runIds : []; + const beforeTraces = Array.isArray(before?.traceIds) ? before.traceIds : []; + const newRun = current.runIds.some((id) => !beforeRuns.includes(id)); + const newTrace = current.traceIds.some((id) => !beforeTraces.includes(id)); + const meaningfulChange = Math.abs(current.textBytes - Number(before?.textBytes || 0)) > 20 && (current.running || current.executionError); + return newRun || newTrace || meaningfulChange; + }, beforeEvidence || {}, { timeout: timeoutMs }).catch(() => null); + const after = await promptSideEffectSnapshot(); + const beforeRuns = Array.isArray(beforeEvidence?.runIds) ? beforeEvidence.runIds : []; + const beforeTraces = Array.isArray(beforeEvidence?.traceIds) ? beforeEvidence.traceIds : []; + const newRunIds = after.runIds.filter((id) => !beforeRuns.includes(id)); + const newTraceIds = after.traceIds.filter((id) => !beforeTraces.includes(id)); + const meaningfulChange = Math.abs(after.textBytes - Number(beforeEvidence?.textBytes || 0)) > 20 && (after.running || after.executionError); + return { ...after, newRunIds, newTraceIds, submitted: newRunIds.length > 0 || newTraceIds.length > 0 || meaningfulChange, valuesRedacted: true }; +} + +async function promptSideEffectSnapshot() { + return page.evaluate(() => { + const text = document.body?.innerText || ""; + return { + runIds: Array.from(new Set(text.match(/run_[A-Za-z0-9_:-]+/gu) || [])).slice(-20), + traceIds: Array.from(new Set(text.match(/trc_[A-Za-z0-9_:-]+/gu) || [])).slice(-20), + running: /Trace running|最近\s*\d+\s*(?:秒|分钟|分|小时)前|Code Agent\s*耗时/iu.test(text), + executionError: /AgentRun error|agentrun:error:|provider-stream-disconnected|provider-unavailable/iu.test(text), + textBytes: new TextEncoder().encode(text).length, + valuesRedacted: true + }; + }).catch(() => ({ runIds: [], traceIds: [], running: false, executionError: false, textBytes: 0, valuesRedacted: true })); +} + async function selectProvider(provider) { const target = String(provider || "").trim(); if (!target) throw new Error("selectProvider requires provider name");