Ctrl K
ComunidadGuías…

server/browser-dialogs.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// agent-browser 0.37 answers a page's alert and beforeunload itself, but a confirm or prompt stays open and unseen in its
// headless Chrome, which holds every input to that page until it is answered. A second DevTools client sees and answers
// it; it must enable the Page domain before the dialog opens, since a page blocked by one answers nothing.
import { connectCdp, type Cdp, type CdpListeners } from "./browser-sites.ts";

export interface PageDialog {
  type: "confirm" | "prompt";
  message: string;
  defaultValue: string;
}

/** Only the engine's own Chrome on this machine is ever addressed. */
export function localDevToolsUrl(value: unknown): string | null {
  return typeof value === "string" && /^wss?:\/\/(?:127\.0\.0\.1|localhost|\[::1\]):\d+\/devtools\/browser\/[\w-]+$/u.test(value) ? value : null;
}

const MESSAGE_LIMIT = 2_000;
type Open = { session: string; dialog: PageDialog } | null;

/** One browser's open page dialog, watched over its own DevTools connection. */
export class DialogWatch {
  readonly #onChange: (dialog: PageDialog | null) => void;
  #cdp: Cdp | null = null;
  #open: Open = null;
  #waiters = new Set<() => void>();
  #closed = false;
  #opened = 0;

  constructor(onChange: (dialog: PageDialog | null) => void) {
    this.#onChange = onChange;
  }

  get dialog(): PageDialog | null { return this.#open?.dialog ?? null; }
  get closed(): boolean { return this.#closed; }
  /** How many dialogs have opened since the watch began. */
  get opened(): number { return this.#opened; }

  async start(url: string, connect: (url: string, timeoutMs: number, listeners: CdpListeners) => Promise<Cdp> = connectCdp): Promise<void> {
    const cdp = await connect(url, 10_000, {
      event: ({ method, params, sessionId }) => {
        if (method === "Target.attachedToTarget" && (params?.targetInfo as { type?: unknown } | undefined)?.type === "page" && typeof params?.sessionId === "string") {
          void cdp.send("Page.enable", {}, params.sessionId).catch(() => undefined);
        } else if (method === "Page.javascriptDialogOpening" && sessionId && (params?.type === "confirm" || params?.type === "prompt")) {
          this.#set({ session: sessionId, dialog: {
            type: params.type,
            message: String(params.message ?? "").slice(0, MESSAGE_LIMIT),
            defaultValue: String(params.defaultPrompt ?? "").slice(0, MESSAGE_LIMIT),
          } });
        } else if ((method === "Page.javascriptDialogClosed" || method === "Target.detachedFromTarget") && this.#open && (sessionId ?? params?.sessionId) === this.#open.session) {
          this.#set(null);
        }
      },
      closed: () => this.close(),
    });
    if (this.#closed) { cdp.close(); return; }
    this.#cdp = cdp;
    await cdp.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
  }

  #set(open: Open): void {
    if (open === null && this.#open === null) return;
    if (open) this.#opened += 1;
    this.#open = open;
    if (!open) { for (const done of this.#waiters) done(); this.#waiters.clear(); }
    this.#onChange(open?.dialog ?? null);
  }

  /** False when there was no dialog to answer, or the browser did not take the answer. */
  async answer(accept: boolean, promptText?: string): Promise<boolean> {
    const open = this.#open;
    if (!open || !this.#cdp) return false;
    try {
      await this.#cdp.send("Page.handleJavaScriptDialog", { accept, ...(promptText !== undefined && open.dialog.type === "prompt" ? { promptText } : {}) }, open.session);
    } catch { return false; }
    if (this.#open === open) this.#set(null);
    return true;
  }

  /** Resolves once no dialog is open, the watch stops, or `signal` aborts. */
  settled(signal?: AbortSignal): Promise<void> {
    if (!this.#open || this.#closed || signal?.aborted) return Promise.resolve();
    return new Promise((resolve) => {
      const done = () => { this.#waiters.delete(done); signal?.removeEventListener("abort", done); resolve(); };
      this.#waiters.add(done);
      signal?.addEventListener("abort", done, { once: true });
    });
  }

  close(): void {
    if (this.#closed) return;
    this.#closed = true;
    this.#cdp?.close();
    this.#cdp = null;
    this.#set(null);
  }
}