fix(probe): stabilize public workbench session probe

This commit is contained in:
lyon
2026-06-15 19:54:19 +08:00
parent af737010be
commit 29ad398b1f
+173 -11
View File
@@ -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]")),