From 29ad398b1f20d02d2e0343f76d71f803794a5ed3 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 15 Jun 2026 19:54:19 +0800 Subject: [PATCH] fix(probe): stabilize public workbench session probe --- scripts/web-live-dom-probe.mjs | 184 +++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 11 deletions(-) diff --git a/scripts/web-live-dom-probe.mjs b/scripts/web-live-dom-probe.mjs index ea463e82..0d0f6f79 100644 --- a/scripts/web-live-dom-probe.mjs +++ b/scripts/web-live-dom-probe.mjs @@ -39,7 +39,13 @@ export function parseProbeArgs(argv) { command, url: process.env.HWLAB_WEB_PROBE_URL ?? process.env.HWLAB_WEB_URL ?? "http://74.48.78.17:19666/", user: process.env.HWLAB_WEB_USER ?? "admin", - pass: process.env.HWLAB_WEB_PASS ?? "hwlab2026", + pass: process.env.HWLAB_WEB_PASS ?? "", + credentials: { + userSource: process.env.HWLAB_WEB_USER ? "env:HWLAB_WEB_USER" : "default", + passSource: process.env.HWLAB_WEB_PASS ? "env:HWLAB_WEB_PASS" : "unresolved", + valuesRedacted: true + }, + credentialError: null, viewport: process.env.HWLAB_WEB_PROBE_VIEWPORT ?? "1440x900", message: process.env.HWLAB_WEB_PROBE_MESSAGE ?? "", freshSession: false, @@ -66,8 +72,14 @@ export function parseProbeArgs(argv) { return value; }; if (arg === "--url" || arg.startsWith("--url=")) args.url = readValue("--url"); - else if (arg === "--user" || arg.startsWith("--user=")) args.user = readValue("--user"); - else if (arg === "--pass" || arg.startsWith("--pass=")) args.pass = readValue("--pass"); + else if (arg === "--user" || arg.startsWith("--user=")) { + args.user = readValue("--user"); + args.credentials.userSource = "cli:--user"; + } + else if (arg === "--pass" || arg.startsWith("--pass=")) { + args.pass = readValue("--pass"); + args.credentials.passSource = "cli:--pass"; + } else if (arg === "--viewport" || arg.startsWith("--viewport=")) args.viewport = readValue("--viewport"); else if (arg === "--message" || arg.startsWith("--message=")) args.message = readValue("--message"); else if (arg === "--project-id" || arg.startsWith("--project-id=")) args.projectId = readValue("--project-id"); @@ -84,9 +96,120 @@ export function parseProbeArgs(argv) { else if (command === "status" && !args.jobId) args.jobId = arg; else throw new Error(`unknown web-live-dom-probe argument: ${arg}`); } + resolveProbeCredentials(args); return args; } +function resolveProbeCredentials(args) { + if (args.command === "help" || args.command === "status") return; + if (args.pass) return; + const target = inferProbeCredentialTarget(args.url); + if (!target) { + args.pass = "hwlab2026"; + args.credentials = { ...args.credentials, passSource: "legacy-default", valuesRedacted: true }; + return; + } + if (args.credentials.userSource === "default") { + args.user = target.username; + args.credentials.userSource = "target-default"; + } + const material = readProbeSecretSource(target.sourceRef, target.sourceKey); + args.credentials = { + ...args.credentials, + node: target.node, + lane: target.lane, + sourceRef: target.sourceRef, + sourceKey: target.sourceKey, + sourcePath: material.sourcePath ? displayProbeSecretPath(material.sourcePath) : null, + sourcePresent: material.present, + sourceFingerprint: material.fingerprint, + passSource: material.ok ? "secret-source" : "missing-secret-source", + valuesRedacted: true + }; + if (material.ok) { + args.pass = material.value; + return; + } + args.credentialError = { + code: "web_login_secret_missing", + message: `Web login secret source is missing for ${target.node}/${target.lane}`, + reason: material.reason + }; +} + +function inferProbeCredentialTarget(url) { + let parsed; + try { + parsed = new URL(url); + } catch { + return null; + } + if (parsed.hostname === "hwlab.pikapython.com") { + return { + node: "D601", + lane: "v03", + username: "admin", + sourceRef: "hwlab/d601-v03-bootstrap-admin.env", + sourceKey: "HWLAB_BOOTSTRAP_ADMIN_PASSWORD" + }; + } + if (parsed.hostname === "74.48.78.17" && (parsed.port === "20666" || parsed.port === "20667")) { + return { + node: "G14", + lane: "v03", + username: "admin", + sourceRef: "hwlab/g14-v03-bootstrap-admin.env", + sourceKey: "HWLAB_BOOTSTRAP_ADMIN_PASSWORD" + }; + } + return null; +} + +function readProbeSecretSource(sourceRef, sourceKey) { + const sourcePath = probeSecretSourcePaths(sourceRef).find((candidate) => fs.existsSync(candidate)); + if (!sourcePath) return { ok: false, present: false, sourcePath: probeSecretSourcePaths(sourceRef)[0] ?? null, fingerprint: null, reason: "secret-source-missing" }; + const values = parseEnvFile(fs.readFileSync(sourcePath, "utf8")); + const value = values[sourceKey]; + if (!value) return { ok: false, present: true, sourcePath, fingerprint: null, reason: "secret-key-missing" }; + return { ok: true, present: true, sourcePath, value, fingerprint: `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 16)}`, reason: null }; +} + +function probeSecretSourcePaths(sourceRef) { + const roots = [ + process.env.HWLAB_WEB_SECRET_SOURCE_ROOT, + process.env.UNIDESK_SECRET_SOURCE_ROOT, + path.join(repoRoot, ".state", "secrets"), + repoRoot.includes("/.worktree/") ? path.join(repoRoot.slice(0, repoRoot.indexOf("/.worktree/")), ".state", "secrets") : null, + "/root/unidesk/.state/secrets" + ].filter(Boolean); + return [...new Set(roots.map((root) => path.join(root, sourceRef)))]; +} + +function parseEnvFile(text) { + const values = {}; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const eq = line.indexOf("="); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + const rawValue = line.slice(eq + 1).trim(); + values[key] = rawValue.startsWith("'") && rawValue.endsWith("'") + ? rawValue.slice(1, -1).replace(/'"'"'/gu, "'") + : rawValue.startsWith('"') && rawValue.endsWith('"') + ? rawValue.slice(1, -1) + : rawValue; + } + return values; +} + +function displayProbeSecretPath(sourcePath) { + if (sourcePath.startsWith(`${repoRoot}/`)) return sourcePath.slice(repoRoot.length + 1); + const unideskRoot = "/root/unidesk/"; + if (sourcePath.startsWith(unideskRoot)) return sourcePath.slice(unideskRoot.length); + return sourcePath; +} + export async function runProbe(args) { const startedAt = new Date().toISOString(); const reportPath = ensureNotRepoReportsPath(repoRoot, path.resolve(repoRoot, args.reportPath), "--report"); @@ -95,6 +218,31 @@ export async function runProbe(args) { const actions = []; let browser; let result; + if (args.credentialError) { + result = { + ok: false, + status: "blocked", + command: "web-live-dom-probe run", + generatedAt: new Date().toISOString(), + startedAt, + url: args.url, + actions, + credentials: args.credentials, + error: args.credentialError.code, + credentialError: args.credentialError, + artifacts: { reportPath, screenshotPath } + }; + await mkdir(path.dirname(reportPath), { recursive: true }); + await writeFile(reportPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); + result.artifacts = { + ...(result.artifacts ?? {}), + reportPath, + reportSha256: await fileSha256(reportPath), + screenshotPath, + screenshotSha256: await fileSha256(screenshotPath) + }; + return result; + } try { const { chromium } = await importPlaywright(); browser = await launchChromium(chromium); @@ -125,6 +273,7 @@ export async function runProbe(args) { viewport: { width, height }, browser: browser.hwlabLaunchPlan ?? null, actions, + credentials: args.credentials, bootstrapRoutes: summarizeBootstrapRoutes(bootstrapRoutes), dom, promptValidation, @@ -149,6 +298,7 @@ export async function runProbe(args) { url: args.url, browser: browser?.hwlabLaunchPlan ?? error?.hwlabBrowserLaunch ?? null, actions, + credentials: args.credentials, error: error instanceof Error ? error.message : String(error), artifacts: { reportPath, screenshotPath } }; @@ -194,6 +344,16 @@ function validatePromptFinalResponse(dom, args) { } async function startProbe(args) { + if (args.credentialError) { + return { + ok: false, + status: "blocked", + command: "web-live-dom-probe start", + error: args.credentialError.code, + credentialError: args.credentialError, + credentials: args.credentials + }; + } await mkdir(stateRoot, { recursive: true }); const jobId = args.jobId || `web-live-dom-probe-${Date.now()}-${process.pid}`; const reportPath = path.join(stateRoot, `${jobId}.result.json`); @@ -206,7 +366,6 @@ async function startProbe(args) { "run", "--url", args.url, "--user", args.user, - "--pass", args.pass, "--viewport", args.viewport, "--project-id", args.projectId, "--report", reportPath, @@ -225,6 +384,7 @@ async function startProbe(args) { const child = spawn(process.execPath, childArgs, { cwd: repoRoot, detached: true, + env: { ...process.env, HWLAB_WEB_USER: args.user, HWLAB_WEB_PASS: args.pass }, stdio: ["ignore", stdout, stderr] }); child.unref(); @@ -389,12 +549,14 @@ async function createFreshSession(page, args, actions) { const timeoutMs = Math.min(args.timeoutMs, 30000); const settled = await page.waitForFunction((initial) => { const statusText = document.querySelector("#session-status")?.textContent?.trim() ?? null; - const changed = Boolean(statusText && statusText !== initial.statusText && !/加载中|等待 workspace/u.test(statusText)); + const statusReady = Boolean(statusText && !/加载中|等待 workspace/u.test(statusText)); + const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); + const activeTitle = activeTab?.querySelector("strong")?.textContent?.trim() ?? activeTab?.getAttribute("title") ?? null; + const activeChanged = Boolean(activeTitle && activeTitle !== initial.activeTabTitle); + const statusChanged = Boolean(statusText && statusText !== initial.statusText); const messageCount = document.querySelectorAll(".message-card").length; - const emptyVisible = Boolean(document.querySelector("[data-conversation-empty]")); const input = document.querySelector("#command-input"); - const send = document.querySelector("#command-send"); - return changed && messageCount === 0 && emptyVisible && input && send && !input.disabled && !send.disabled; + return statusReady && (activeChanged || statusChanged) && messageCount === 0 && input && !input.disabled; }, before, { timeout: timeoutMs }).catch(() => null); const after = await collectSessionState(page); actions.push({ action: "fresh-session", selector, settled: Boolean(settled), timeoutMs, shellReady, before, after }); @@ -409,7 +571,7 @@ async function waitForSessionShellReady(page, args, actions) { const statusText = document.querySelector("#session-status")?.textContent?.trim() ?? ""; const workspace = document.querySelector("#workspace"); const create = document.querySelector("#session-create"); - return Boolean(workspace && create && !create.disabled && statusText && !/加载中/u.test(statusText)); + return Boolean(workspace && create && !create.disabled && statusText && !/加载中|等待 workspace/u.test(statusText)); }, null, { timeout: timeoutMs }).catch(() => null); const after = await collectSessionState(page); const result = { action: "session-shell-ready", ready: Boolean(ready), timeoutMs, before, after }; @@ -420,10 +582,10 @@ async function waitForSessionShellReady(page, args, actions) { async function collectSessionState(page) { return page.evaluate(() => { - const activeTab = document.querySelector(".session-tab[aria-selected='true']"); + const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']"); return { statusText: document.querySelector("#session-status")?.textContent?.trim() ?? null, - activeTabTitle: activeTab?.getAttribute("title") ?? null, + activeTabTitle: activeTab?.querySelector("strong")?.textContent?.trim() ?? activeTab?.getAttribute("title") ?? null, tabCount: document.querySelectorAll(".session-tab").length, messageCount: document.querySelectorAll(".message-card").length, emptyVisible: Boolean(document.querySelector("[data-conversation-empty]")),