diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 897366f4..273cf8a5 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -921,7 +921,56 @@ async function selectProvider(provider) { } await page.keyboard.press("Escape").catch(() => null); } - throw new Error("provider option not found: " + target + "; attempts=" + JSON.stringify(attempts.slice(-10))); + const providerCandidates = await collectProviderCandidates(); + throw new Error("provider option not found: " + target + "; providerCandidates=" + JSON.stringify(providerCandidates.slice(0, 50)) + "; attempts=" + JSON.stringify(attempts.slice(-10))); +} + +async function collectProviderCandidates() { + return page.evaluate(() => { + const rows = []; + const seen = new Set(); + const visible = (element) => { + const rect = element.getBoundingClientRect(); + const style = window.getComputedStyle(element); + return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"; + }; + const push = (kind, element, value, text) => { + const normalizedValue = String(value || "").trim().slice(0, 160); + const normalizedText = String(text || "").replace(/\s+/gu, " ").trim().slice(0, 220); + const label = `${normalizedValue} ${normalizedText}`.trim(); + if (!label) return; + const key = `${kind}:${label}`.toLowerCase(); + if (seen.has(key)) return; + seen.add(key); + rows.push({ kind, value: normalizedValue, text: normalizedText, testId: String(element?.getAttribute?.("data-testid") || "").slice(0, 120), ariaLabel: String(element?.getAttribute?.("aria-label") || "").slice(0, 160) }); + }; + for (const select of Array.from(document.querySelectorAll("select")).filter(visible)) { + for (const option of Array.from(select.options || [])) push("select-option", select, option.value, option.textContent || ""); + } + const selector = [ + '[role="option"]', + '[role="menuitem"]', + '[data-radix-collection-item]', + '[data-testid*="provider" i]', + '[data-testid*="profile" i]', + '[data-testid*="model" i]', + '[aria-label*="provider" i]', + '[aria-label*="profile" i]', + '[aria-label*="model" i]', + '[aria-label*="模型"]', + '[aria-label*="提供"]', + 'button' + ].join(", "); + for (const element of Array.from(document.querySelectorAll(selector)).filter(visible)) { + const text = String(element.textContent || "").replace(/\s+/gu, " ").trim(); + const ariaLabel = String(element.getAttribute("aria-label") || ""); + const testId = String(element.getAttribute("data-testid") || ""); + const haystack = `${text} ${ariaLabel} ${testId}`; + if (!/provider|profile|model|模型|提供|codex|openai|deepseek|gpt|api|flash|spark|claude|gemini|moon/iu.test(haystack)) continue; + push("visible-control", element, element.getAttribute("value") || "", text || ariaLabel || testId); + } + return rows.slice(0, 80); + }).catch((error) => [{ kind: "candidate-scan-error", value: "", text: String(error?.message || error).slice(0, 240), testId: "", ariaLabel: "" }]); } async function clickSession(sessionId) {