// 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";
import { nodeWebObserveRunnerRealtimeSource } from "./hwlab-node-web-observe-runner-realtime-source";
import { nodeWebObserveRunnerKafkaDebugReplaySource } from "./hwlab-node-web-observe-runner-kafka-debug-replay-source";
import { nodeWebObserveRunnerTraceReadabilitySource } from "./hwlab-node-web-observe-runner-trace-readability-source";

export function nodeWebObserveRunnerSource(): string {
  return String.raw`#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { createReadStream } from "node:fs";
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 = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SAMPLES, "UNIDESK_WEB_OBSERVE_MAX_SAMPLES", 1, 10000000);
const observerRefreshIntervalMs = positiveInteger(process.env.UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS, 180000);
const maxRunMs = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_RUN_MS, "UNIDESK_WEB_OBSERVE_MAX_RUN_MS", 1000, 86400000);
const maxScreenshots = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_SCREENSHOTS, "UNIDESK_WEB_OBSERVE_MAX_SCREENSHOTS", 1, 10000);
const maxPerformanceEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_ENTRIES", 1, 100000);
const maxBrowserProcessHistoryEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_BROWSER_PROCESS_HISTORY_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_BROWSER_PROCESS_HISTORY_ENTRIES", 1, 100000);
const maxBrowserFreezeSignalHistoryEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_BROWSER_FREEZE_SIGNAL_HISTORY_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_BROWSER_FREEZE_SIGNAL_HISTORY_ENTRIES", 1, 100000);
const maxRealtimeEventEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_ENTRIES", 1, 100000);
const maxRealtimeEventBodyBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_BODY_BYTES, "UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_BODY_BYTES", 1, 16777216);
const maxDomEvidenceRows = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_DOM_EVIDENCE_ROWS, "UNIDESK_WEB_OBSERVE_MAX_DOM_EVIDENCE_ROWS", 1, 100000);
const maxProcessScanEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_PROCESS_SCAN_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_PROCESS_SCAN_ENTRIES", 1, 1000000);
const maxPerformanceCaptureMs = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_CAPTURE_SECONDS, "UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_CAPTURE_SECONDS", 1, 600) * 1000;
const responseBodyMaxBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_RESPONSE_BODY_MAX_BYTES, "UNIDESK_WEB_OBSERVE_RESPONSE_BODY_MAX_BYTES", 1024, 16777216);
const maxJsonlQueueEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_ENTRIES", 1, 100000);
const maxJsonlQueueBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_BYTES, "UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_BYTES", 1024, 67108864);
const maxJsonlLineBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_JSONL_LINE_BYTES, "UNIDESK_WEB_OBSERVE_MAX_JSONL_LINE_BYTES", 1024, 16777216);
const jsonlFlushTimeoutMs = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_JSONL_FLUSH_TIMEOUT_MS, "UNIDESK_WEB_OBSERVE_JSONL_FLUSH_TIMEOUT_MS", 100, 60000);
const maxResponseBodyInFlight = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_RESPONSE_BODY_IN_FLIGHT, "UNIDESK_WEB_OBSERVE_MAX_RESPONSE_BODY_IN_FLIGHT", 1, 64);
const maxPassiveListenerTasks = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_PASSIVE_LISTENER_TASKS, "UNIDESK_WEB_OBSERVE_MAX_PASSIVE_LISTENER_TASKS", 1, 10000);
const passiveTaskFlushTimeoutMs = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_PASSIVE_TASK_FLUSH_TIMEOUT_MS, "UNIDESK_WEB_OBSERVE_PASSIVE_TASK_FLUSH_TIMEOUT_MS", 100, 60000);
const closeStepTimeoutMs = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_CLOSE_STEP_TIMEOUT_MS, "UNIDESK_WEB_OBSERVE_CLOSE_STEP_TIMEOUT_MS", 100, 60000);
const maxProcessReadConcurrency = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_PROCESS_READ_CONCURRENCY, "UNIDESK_WEB_OBSERVE_MAX_PROCESS_READ_CONCURRENCY", 1, 256);
const maxRealtimeTotalEntries = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_ENTRIES, "UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_ENTRIES", 1, 100000);
const maxRealtimeTotalBodyBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_BODY_BYTES, "UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_BODY_BYTES", 1024, 67108864);
const viewport = parseViewport(process.env.UNIDESK_WEB_OBSERVE_VIEWPORT || "1440x900");
const browserProxyMode = parseBrowserProxyMode(process.env.UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE || "auto");
const origin = {
  name: process.env.UNIDESK_WEB_OBSERVE_ORIGIN_NAME || "custom",
  mode: process.env.UNIDESK_WEB_OBSERVE_ORIGIN_MODE || "custom-url",
  configPath: process.env.UNIDESK_WEB_OBSERVE_ORIGIN_CONFIG_PATH || null,
  browserProxyMode,
  browserProxyModeSource: process.env.UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE_SOURCE || "default",
  valuesRedacted: true,
};
const webPerformanceRequestBodyMaxBytes = requiredBoundedInteger(process.env.UNIDESK_WEB_OBSERVE_WEB_PERFORMANCE_BODY_MAX_BYTES, "UNIDESK_WEB_OBSERVE_WEB_PERFORMANCE_BODY_MAX_BYTES", 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 realtimeFanoutProfiles = parseRealtimeFanoutProfiles(process.env.UNIDESK_WEB_OBSERVE_REALTIME_FANOUT_PROFILES_JSON);
const workbenchKafkaDebugReplay = parseWorkbenchKafkaDebugReplayConfig(process.env.UNIDESK_WEB_OBSERVE_WORKBENCH_KAFKA_DEBUG_REPLAY_JSON);
const workbenchTraceReadability = parseWorkbenchTraceReadabilityConfig(process.env.UNIDESK_WEB_OBSERVE_WORKBENCH_TRACE_READABILITY_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 screenshotCount = 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;
let shutdownSignal = null;
let browserResourcesClosePromise = null;
let browserLaunchAttempted = false;
const browserProcessHistory = [];
const browserPageRuntimeBaselines = new Map();
const browserFreezeSignalHistory = [];
let trackedBrowserProcessIdentities = [];
const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] };
const jsonlWriterState = { queue: [], queuedBytes: 0, inFlight: 0, pump: null, acceptedEntries: 0, acceptedBytes: 0, writtenEntries: 0, writtenBytes: 0, droppedEntries: 0, droppedBytes: 0, failedEntries: 0, dropsByFile: {} };
const responseBodyState = { inFlight: 0, accepted: 0, skippedSaturated: 0, completed: 0, failed: 0 };
const passiveTaskState = { tasks: new Set(), accepted: 0, dropped: 0, completed: 0, failed: 0, drainTimedOut: false };
const runnerSleepWakes = new Set();
let lastProcessScan = { ok: false, complete: false, totalPidEntries: 0, scannedPidEntries: 0, failedPidEntries: 0, truncated: false, readConcurrency: maxProcessReadConcurrency, error: "not-scanned", valuesRedacted: true };
let realtimeAggregateLast = null;

for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
  process.once(signal, () => {
    shutdownSignal = signal;
    stopping = true;
    terminalStatus = "stopping";
    interruptRunnerSleep();
    void closeBrowserResources("signal:" + signal);
  });
}

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();
  browserLaunchAttempted = true;
  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) {
    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;
    }
    if (maxSamples > 0 && sampleSeq >= maxSamples) {
      await appendJsonl(files.control, controlRecord({ id: "max-samples", type: "stop", source: "sampler" }, "completed", { reason: "max-samples", maxSamples }));
      break;
    }
    await drainOneCommand();
    if (browserFreezeBlocker) break;
    await samplePage("interval");
    if (browserFreezeBlocker) break;
    await interruptibleRunnerSleep(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);
  interruptRunnerSleep();
  const closeResult = await closeBrowserResources(shutdownSignal === null ? "finally" : "signal:" + shutdownSignal);
  const passiveTaskDrain = await drainPassiveTasks();
  const terminalFlush = await flushJsonlQueue();
  await writeTerminalResourceStats({ closeResult, passiveTaskDrain, terminalFlush }).catch(() => {});
}

async function closeBrowserResources(reason) {
  if (browserResourcesClosePromise) return browserResourcesClosePromise;
  browserResourcesClosePromise = (async () => {
    stopping = true;
    const closingObserverPage = observerPage;
    const closingPage = page;
    const closingContext = context;
    const closingBrowser = browser;
    let preCloseIdentityDiscovery;
    try {
      const summary = await collectChromiumProcessSummary(null);
      preCloseIdentityDiscovery = { ok: true, processScan: summary.processScan, chromiumProcessCount: summary.chromiumProcessCount, valuesRedacted: true };
    } catch (error) {
      preCloseIdentityDiscovery = { ok: false, processScan: lastProcessScan, error: errorSummary(error), valuesRedacted: true };
    }
    observerPage = null;
    page = null;
    context = null;
    browser = null;
    const steps = [];
    const closeStep = async (label, operation) => {
      try {
        await withHardTimeout(Promise.resolve().then(operation), closeStepTimeoutMs, label + " close exceeded " + closeStepTimeoutMs + "ms");
        steps.push({ label, ok: true, valuesRedacted: true });
      } catch (error) {
        steps.push({ label, ok: false, error: errorSummary(error), valuesRedacted: true });
      }
    };
    if (closingObserverPage && !closingObserverPage.isClosed()) await closeStep("observer-page", () => closingObserverPage.close());
    if (closingPage && !closingPage.isClosed()) await closeStep("control-page", () => closingPage.close());
    if (closingContext) await closeStep("browser-context", () => closingContext.close());
    if (closingBrowser) await closeStep("browser", () => closingBrowser.close());
    const browserCleanup = await terminateTrackedBrowserProcesses("browser-close-residual", preCloseIdentityDiscovery);
    return { ok: steps.every((step) => step.ok === true) && browserCleanup.ok === true, reason, closeStepTimeoutMs, steps, browserCleanup, valuesRedacted: true };
  })();
  return browserResourcesClosePromise;
}

${nodeWebObserveRunnerRuntimeSource()}

${nodeWebObserveRunnerPerformanceSource()}

${nodeWebObserveRunnerControlSource()}

${nodeWebObserveRunnerCommandActionsSource()}

${nodeWebObserveRunnerWorkbenchSource()}

${nodeWebObserveRunnerRealtimeSource()}

${nodeWebObserveRunnerKafkaDebugReplaySource()}

${nodeWebObserveRunnerTraceReadabilitySource()}

${nodeWebObserveRunnerSamplingSource()}

${nodeWebObserveRunnerUtilitySource()}


`;
}
