fix(probe): stabilize public workbench session probe
This commit is contained in:
+173
-11
@@ -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]")),
|
||||
|
||||
Reference in New Issue
Block a user