// SPEC: PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer. // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel. // Responsibility: Source string for the pure-client HWLAB web-probe observer runner. import { nodeWebObserveRunnerCommandActionsSource } from "./hwlab-node-web-observe-runner-actions-source"; import { nodeWebObserveRunnerPerformanceSource } from "./hwlab-node-web-observe-runner-performance-source"; import { nodeWebObserveRunnerRuntimeSource } from "./hwlab-node-web-observe-runner-runtime-source"; import { nodeWebObserveRunnerControlSource } from "./hwlab-node-web-observe-runner-control-source"; import { nodeWebObserveRunnerWorkbenchSource } from "./hwlab-node-web-observe-runner-workbench-source"; import { nodeWebObserveRunnerSamplingSource } from "./hwlab-node-web-observe-runner-sampling-source"; import { nodeWebObserveRunnerUtilitySource } from "./hwlab-node-web-observe-runner-utility-source"; export function nodeWebObserveRunnerSource(): string { return String.raw`#!/usr/bin/env node import { createHash, randomBytes } from "node:crypto"; import { appendFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; const specRef = "PJ2026-01040111 长程观测 draft-2026-06-20-p0-passive-web-probe-observer"; const startedAtMs = Date.now(); const startedAt = new Date(startedAtMs).toISOString(); const baseUrl = normalizeBaseUrl(process.env.HWLAB_WEB_BASE_URL); const username = process.env.HWLAB_WEB_USER || "admin"; const password = process.env.HWLAB_WEB_PASS || ""; const stateDir = path.resolve(process.env.UNIDESK_WEB_OBSERVE_STATE_DIR || ".state/web-observe/manual"); const jobId = safeId(process.env.UNIDESK_WEB_OBSERVE_JOB_ID || "webobs-" + Date.now().toString(36) + "-" + randomBytes(3).toString("hex")); const targetPath = process.env.UNIDESK_WEB_OBSERVE_TARGET_PATH || "/workbench"; const sampleIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SAMPLE_INTERVAL_MS, 5000); const screenshotIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_SCREENSHOT_INTERVAL_MS, 300000); const screenshotCaptureTimeoutMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_SCREENSHOT_CAPTURE_TIMEOUT_MS, 15000, 1000, 120000); const maxSamples = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SAMPLES, 0); const observerRefreshIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS, 180000); const maxRunMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_RUN_MS, 0); const viewport = parseViewport(process.env.UNIDESK_WEB_OBSERVE_VIEWPORT || "1440x900"); const browserProxyMode = parseBrowserProxyMode(process.env.UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE || "auto"); const webPerformanceRequestBodyMaxBytes = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_WEB_PERFORMANCE_BODY_MAX_BYTES, 65536, 1024, 1048576); const authLoginMaxAttempts = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_ATTEMPTS, 6, 1, 20); const authLoginRequestTimeoutMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_REQUEST_TIMEOUT_MS, 30000, 1000, 120000); const authLoginInitialDelayMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_INITIAL_DELAY_MS, 500, 0, 60000); const authLoginMaxDelayMs = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_DELAY_MS, 10000, 0, 120000); const navigationMaxAttempts = boundedInteger(process.env.UNIDESK_WEB_OBSERVE_NAVIGATION_MAX_ATTEMPTS, 4, 1, 10); const alertThresholds = parseAlertThresholds(process.env.UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON); const browserFreezePolicy = parseBrowserFreezePolicy(process.env.UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON); const projectManagement = parseProjectManagementConfig(process.env.UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON); const playwrightProxy = proxyConfigFromEnv(baseUrl); const chromiumLaunchOptions = chromiumLaunchOptionsForProxy(playwrightProxy); const pageId = "control-" + randomBytes(4).toString("hex"); const observerPageId = "observer-" + randomBytes(4).toString("hex"); const dirs = { commandsPending: path.join(stateDir, "commands", "pending"), commandsProcessing: path.join(stateDir, "commands", "processing"), commandsDone: path.join(stateDir, "commands", "done"), commandsFailed: path.join(stateDir, "commands", "failed"), screenshots: path.join(stateDir, "screenshots"), performance: path.join(stateDir, "performance"), performanceCaptures: path.join(stateDir, "performance", "captures"), analysis: path.join(stateDir, "analysis"), archive: path.join(stateDir, "archive"), }; const files = { manifest: path.join(stateDir, "manifest.json"), heartbeat: path.join(stateDir, "heartbeat.json"), control: path.join(stateDir, "control.jsonl"), samples: path.join(stateDir, "samples.jsonl"), network: path.join(stateDir, "network.jsonl"), console: path.join(stateDir, "console.jsonl"), errors: path.join(stateDir, "errors.jsonl"), artifacts: path.join(stateDir, "artifacts.jsonl"), browserProcess: path.join(stateDir, "browser-process.jsonl"), performanceEvents: path.join(stateDir, "performance-events.jsonl"), }; let browser; let context; let page; let observerPage; let lastObserverRefreshAtMs = Date.now(); let sampleSeq = 0; let commandSeq = 0; let artifactSeq = 0; let activeCommandId = null; let activeCommandType = null; let stopping = false; let terminalStatus = "starting"; let lastScreenshotAtMs = 0; let screenshotCaptureState = null; let auth = null; let pageLoadSeq = 0; let controlPageEpoch = 0; let observerPageEpoch = 0; let currentPageProvenance = null; let heartbeatPulseTimer = null; let browserProcessMonitorStop = null; let browserProcessMonitorSeq = 0; let browserFreezeBlocker = null; const browserProcessHistory = []; const browserPageRuntimeBaselines = new Map(); const browserFreezeSignalHistory = []; const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] }; try { if (!password) throw new Error("missing HWLAB_WEB_PASS"); await prepareDirs(); await rotateExistingJsonlArtifacts(); await writeManifest({ status: "starting" }); await writeHeartbeat({ status: "starting" }); heartbeatPulseTimer = startHeartbeatPulse(); if (jsonlRotation.files.length > 0) await appendJsonl(files.control, eventRecord("jsonl-rotated", { stamp: jsonlRotation.stamp, archiveDir: path.relative(stateDir, dirs.archive), files: jsonlRotation.files, valuesRedacted: true })); const launcher = await import(pathToFileURL(path.resolve("scripts/src/browser-launcher.mjs")).href); const { chromium } = await launcher.importPlaywright(); browser = await launcher.launchChromium(chromium, chromiumLaunchOptions); context = await browser.newContext({ viewport, ...(playwrightProxy === null ? {} : { proxy: playwrightProxy }) }); page = await context.newPage(); attachPassiveListeners(page, "control", pageId); auth = await runControlCommand({ id: "startup-login", type: "login", createdAt: startedAt, source: "startup" }, async () => authenticate(context)); const startupGoto = await runControlCommand({ id: "startup-goto", type: "goto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => gotoTarget(targetPath)); if (startupGoto?.degraded === true) { const error = new Error("startup target page is not ready: " + (startupGoto.degradedReason || "target-not-ready")); error.details = startupGoto; error.navigationReadiness = startupGoto.readiness || null; throw error; } observerPage = await context.newPage(); attachPassiveListeners(observerPage, "observer", observerPageId); await runControlCommand({ id: "startup-observer-goto", type: "observerGoto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => { const result = await syncObserverPageToControlSession("startup"); if (!result?.ok) { await appendJsonl(files.control, eventRecord("observer-startup-degraded", { reason: result?.failureKind || result?.reason || "observer-not-ready", result: sanitize(result), valuesRedacted: true, })); } return result ?? { ok: false, reason: "observer-not-ready", pageRole: "observer", pageId: observerPageId, valuesRedacted: true }; }); browserProcessMonitorStop = startBrowserProcessMonitor(); terminalStatus = "running"; await writeManifest({ status: "running", auth: publicAuth(auth) }); await writeHeartbeat({ status: "running" }); while (!stopping) { await drainOneCommand(); if (browserFreezeBlocker) break; await samplePage("interval"); if (browserFreezeBlocker) break; if (maxSamples > 0 && sampleSeq >= maxSamples) { await appendJsonl(files.control, controlRecord({ id: "max-samples", type: "stop", source: "sampler" }, "completed", { reason: "max-samples", maxSamples })); break; } if (maxRunMs > 0 && Date.now() - startedAtMs >= maxRunMs) { await appendJsonl(files.control, controlRecord({ id: "max-run-ms", type: "stop", source: "sampler" }, "completed", { reason: "max-run-ms", maxRunMs, elapsedMs: Date.now() - startedAtMs })); break; } await sleep(sampleIntervalMs); } if (browserFreezeBlocker) { terminalStatus = "failed"; await writeHeartbeat({ status: "failed", blocker: browserFreezeBlocker }); await writeManifest({ status: "failed", completedAt: new Date().toISOString(), blocker: browserFreezeBlocker }); process.exitCode = browserFreezePolicy.kill.exitCode; } else { terminalStatus = "completed"; await writeHeartbeat({ status: "completed" }); await writeManifest({ status: "completed", completedAt: new Date().toISOString() }); process.exitCode = 0; } } catch (error) { terminalStatus = "failed"; await appendJsonl(files.errors, eventRecord("runner-error", { error: errorSummary(error) })).catch(() => {}); await writeHeartbeat({ status: "failed", error: errorSummary(error) }).catch(() => {}); await writeManifest({ status: "failed", error: errorSummary(error) }).catch(() => {}); process.exitCode = 2; } finally { if (browserProcessMonitorStop) browserProcessMonitorStop(); if (heartbeatPulseTimer) clearInterval(heartbeatPulseTimer); if (browser) await browser.close().catch(() => {}); } ${nodeWebObserveRunnerRuntimeSource()} ${nodeWebObserveRunnerPerformanceSource()} ${nodeWebObserveRunnerControlSource()} ${nodeWebObserveRunnerCommandActionsSource()} ${nodeWebObserveRunnerWorkbenchSource()} ${nodeWebObserveRunnerSamplingSource()} ${nodeWebObserveRunnerUtilitySource()} `; }