Kế hoạch Refactor Browser Evaluate CDP

Ngữ cảnh

act:evaluate thực thi JavaScript do người dùng cung cấp trong trang. Ngày nay nó chạy qua Playwright (page.evaluate hoặc locator.evaluate). Playwright tuần tự hóa lệnh CDP mỗi trang, vì vậy một evaluate bị kẹt hoặc chạy lâu có thể chặn hàng đợi lệnh trang và làm cho mọi hành động sau trên tab đó trông "bị kẹt".

PR #13498 thêm lưới an toàn thực tế (evaluate có giới hạn, lan truyền abort và khôi phục best-effort). Tài liệu này mô tả một refactor lớn hơn làm cho act:evaluate vốn dĩ cô lập khỏi Playwright để một evaluate bị kẹt không thể làm kẹt các hoạt động Playwright bình thường.

Mục tiêu

  • act:evaluate không thể chặn vĩnh viễn các hành động browser sau trên cùng tab.
  • Timeout là nguồn chân lý duy nhất end to end để người gọi có thể dựa vào ngân sách.
  • Abort và timeout được xử lý theo cùng cách qua HTTP và dispatch trong process.
  • Nhắm mục tiêu element cho evaluate được hỗ trợ mà không chuyển mọi thứ khỏi Playwright.
  • Duy trì khả năng tương thích ngược cho người gọi và payload hiện có.

Ngoài phạm vi

  • Thay thế tất cả hành động browser (click, type, wait, v.v.) bằng triển khai CDP.
  • Xóa lưới an toàn hiện có được giới thiệu trong PR #13498 (nó vẫn là dự phòng hữu ích).
  • Giới thiệu khả năng không an toàn mới ngoài cổng browser.evaluateEnabled hiện có.
  • Thêm cô lập process (worker process/thread) cho evaluate. Nếu chúng ta vẫn thấy trạng thái kẹt khó khôi phục sau refactor này, đó là ý tưởng follow-up.

Kiến trúc hiện tại (Tại sao nó bị kẹt)

Ở mức cao:

  • Người gọi gửi act:evaluate đến dịch vụ điều khiển browser.
  • Handler route gọi vào Playwright để thực thi JavaScript.
  • Playwright tuần tự hóa lệnh trang, vì vậy một evaluate không bao giờ kết thúc chặn hàng đợi.
  • Hàng đợi bị kẹt có nghĩa là các hoạt động click/type/wait sau trên tab có thể trông bị treo.

Kiến trúc đề xuất

1. Lan truyền deadline

Giới thiệu một khái niệm ngân sách duy nhất và rút ra mọi thứ từ nó:

  • Người gọi đặt timeoutMs (hoặc một deadline trong tương lai).
  • Timeout yêu cầu bên ngoài, logic handler route và ngân sách thực thi bên trong trang tất cả đều sử dụng cùng ngân sách, với khoảng trống nhỏ khi cần cho overhead tuần tự hóa.
  • Abort được lan truyền như một AbortSignal ở mọi nơi để hủy nhất quán.

Hướng triển khai:

  • Thêm helper nhỏ (ví dụ: createBudget({ timeoutMs, signal })) trả về:
    • signal: AbortSignal được liên kết
    • deadlineAtMs: deadline tuyệt đối
    • remainingMs(): ngân sách còn lại cho hoạt động con
  • Sử dụng helper này trong:
    • src/browser/client-fetch.ts (HTTP và dispatch trong process)
    • src/node-host/runner.ts (đường dẫn proxy)
    • Triển khai hành động browser (Playwright và CDP)

2. Engine Evaluate riêng (Đường dẫn CDP)

Thêm triển khai evaluate dựa trên CDP không chia sẻ hàng đợi lệnh per page của Playwright. Thuộc tính chính là transport evaluate là kết nối WebSocket riêng và phiên CDP riêng được đính kèm vào mục tiêu.

Hướng triển khai:

  • Module mới, ví dụ: src/browser/cdp-evaluate.ts, mà:
    • Kết nối đến endpoint CDP được cấu hình (socket cấp browser).
    • Sử dụng Target.attachToTarget({ targetId, flatten: true }) để lấy sessionId.
    • Chạy:
      • Runtime.evaluate cho evaluate cấp trang, hoặc
      • DOM.resolveNode cộng Runtime.callFunctionOn cho evaluate element.
    • Khi timeout hoặc abort:
      • Gửi Runtime.terminateExecution best-effort cho phiên.
      • Đóng WebSocket và trả về lỗi rõ ràng.

Lưu ý:

  • Điều này vẫn thực thi JavaScript trong trang, vì vậy termination có thể có tác dụng phụ. Lợi ích là nó không làm kẹt hàng đợi Playwright và nó có thể hủy ở lớp transport bằng cách kill phiên CDP.

3. Câu chuyện Ref (Nhắm mục tiêu element mà không cần refactor đầy đủ)

Phần khó là nhắm mục tiêu element. CDP cần handle DOM hoặc backendDOMNodeId, trong khi ngày nay hầu hết hành động browser sử dụng locator Playwright dựa trên ref từ snapshot.

Cách tiếp cận được khuyến nghị: giữ ref hiện có, nhưng đính kèm id có thể giải quyết CDP tùy chọn.

3.1 Mở rộng thông tin Ref được lưu trữ

Mở rộng metadata ref role được lưu trữ để tùy chọn bao gồm id CDP:

  • Ngày nay: { role, name, nth }
  • Đề xuất: { role, name, nth, backendDOMNodeId?: number }

Điều này giữ tất cả hành động dựa trên Playwright hiện có hoạt động và cho phép evaluate CDP chấp nhận cùng giá trị ref khi backendDOMNodeId có sẵn.

3.2 Điền backendDOMNodeId khi Snapshot

Khi tạo snapshot role:

  1. Tạo bản đồ ref role hiện có như ngày nay (role, name, nth).
  2. Lấy cây AX qua CDP (Accessibility.getFullAXTree) và tính bản đồ song song của (role, name, nth) -> backendDOMNodeId sử dụng cùng quy tắc xử lý trùng lặp.
  3. Hợp nhất id trở lại thông tin ref được lưu trữ cho tab hiện tại.

Nếu ánh xạ thất bại cho một ref, để backendDOMNodeId undefined. Điều này làm cho tính năng best-effort và an toàn để triển khai.

3.3 Hành vi Evaluate với Ref

Trong act:evaluate:

  • Nếu ref có mặt và có backendDOMNodeId, chạy evaluate element qua CDP.
  • Nếu ref có mặt nhưng không có backendDOMNodeId, quay về đường dẫn Playwright (với lưới an toàn).

Lối thoát tùy chọn:

  • Mở rộng shape yêu cầu để chấp nhận backendDOMNodeId trực tiếp cho người gọi nâng cao (và cho debug), trong khi giữ ref là giao diện chính.

4. Giữ đường dẫn khôi phục cuối cùng

Ngay cả với evaluate CDP, có những cách khác để làm kẹt tab hoặc kết nối. Giữ cơ chế khôi phục hiện có (terminate execution + ngắt kết nối Playwright) như phương án cuối cùng cho:

  • người gọi cũ
  • môi trường nơi CDP attach bị chặn
  • trường hợp biên Playwright không mong đợi

Kế hoạch triển khai (Iteration đơn)

Deliverable

  • Engine evaluate dựa trên CDP chạy bên ngoài hàng đợi lệnh per-page Playwright.
  • Ngân sách timeout/abort end-to-end duy nhất được sử dụng nhất quán bởi người gọi và handler.
  • Metadata ref có thể tùy chọn mang backendDOMNodeId cho evaluate element.
  • act:evaluate ưu tiên engine CDP khi có thể và quay về Playwright khi không.
  • Test chứng minh một evaluate bị kẹt không làm kẹt các hành động sau.
  • Log/metric làm cho thất bại và dự phòng hiển thị.

Checklist triển khai

  1. Thêm helper "budget" được chia sẻ để liên kết timeoutMs + upstream AbortSignal thành:
    • một AbortSignal duy nhất
    • một deadline tuyệt đối
    • helper remainingMs() cho hoạt động downstream
  2. Cập nhật tất cả đường dẫn người gọi để sử dụng helper đó để timeoutMs có nghĩa giống mọi nơi:
    • src/browser/client-fetch.ts (HTTP và dispatch trong process)
    • src/node-host/runner.ts (đường dẫn node proxy)
    • Wrapper CLI gọi /act (thêm --timeout-ms vào browser evaluate)
  3. Triển khai src/browser/cdp-evaluate.ts:
    • kết nối đến socket CDP cấp browser
    • Target.attachToTarget để lấy sessionId
    • chạy Runtime.evaluate cho evaluate trang
    • chạy DOM.resolveNode + Runtime.callFunctionOn cho evaluate element
    • khi timeout/abort: best-effort Runtime.terminateExecution sau đó đóng socket
  4. Mở rộng metadata ref role được lưu trữ để tùy chọn bao gồm backendDOMNodeId:
    • giữ hành vi { role, name, nth } hiện có cho hành động Playwright
    • thêm backendDOMNodeId?: number cho nhắm mục tiêu element CDP
  5. Điền backendDOMNodeId trong quá trình tạo snapshot (best-effort):
    • lấy cây AX qua CDP (Accessibility.getFullAXTree)
    • tính (role, name, nth) -> backendDOMNodeId và hợp nhất vào bản đồ ref được lưu trữ
    • nếu ánh xạ không rõ ràng hoặc thiếu, để id undefined
  6. Cập nhật routing act:evaluate:
    • nếu không có ref: luôn sử dụng evaluate CDP
    • nếu ref giải quyết thành backendDOMNodeId: sử dụng evaluate element CDP
    • nếu không: quay về evaluate Playwright (vẫn có giới hạn và có thể abort)
  7. Giữ đường dẫn khôi phục "phương án cuối cùng" hiện có như dự phòng, không phải đường dẫn mặc định.
  8. Thêm test:
    • evaluate bị kẹt timeout trong ngân sách và click/type tiếp theo thành công
    • abort hủy evaluate (ngắt kết nối client hoặc timeout) và mở khóa các hành động tiếp theo
    • thất bại ánh xạ quay về Playwright một cách sạch sẽ
  9. Thêm khả năng quan sát:
    • thời lượng evaluate và counter timeout
    • sử dụng terminateExecution
    • tỷ lệ dự phòng (CDP -> Playwright) và lý do

Tiêu chí chấp nhận

  • Một act:evaluate cố tình bị treo trả về trong ngân sách người gọi và không làm kẹt tab cho các hành động sau.
  • timeoutMs hoạt động nhất quán qua CLI, công cụ agent, node proxy và lệnh gọi trong process.
  • Nếu ref có thể được ánh xạ thành backendDOMNodeId, evaluate element sử dụng CDP; nếu không đường dẫn dự phòng vẫn có giới hạn và có thể khôi phục.

Kế hoạch test

  • Test đơn vị:
    • Logic khớp (role, name, nth) giữa ref role và node cây AX.
    • Hành vi helper budget (khoảng trống, toán thời gian còn lại).
  • Test tích hợp:
    • Timeout evaluate CDP trả về trong ngân sách và không chặn hành động tiếp theo.
    • Abort hủy evaluate và kích hoạt termination best-effort.
  • Test hợp đồng:
    • Đảm bảo BrowserActRequestBrowserActResponse vẫn tương thích.

Rủi ro và giảm thiểu

  • Ánh xạ không hoàn hảo:
    • Giảm thiểu: ánh xạ best-effort, dự phòng sang evaluate Playwright và thêm công cụ debug.
  • Runtime.terminateExecution có tác dụng phụ:
    • Giảm thiểu: chỉ sử dụng khi timeout/abort và ghi lại hành vi trong lỗi.
  • Overhead bổ sung:
    • Giảm thiểu: chỉ lấy cây AX khi snapshot được yêu cầu, cache per target và giữ phiên CDP ngắn hạn.
  • Hạn chế relay extension:
    • Giảm thiểu: sử dụng API attach cấp browser khi socket per page không có sẵn và giữ đường dẫn Playwright hiện tại như dự phòng.

Câu hỏi mở

  • Engine mới nên có thể cấu hình như playwright, cdp hay auto?
  • Chúng ta có muốn hiển thị định dạng "nodeRef" mới cho người dùng nâng cao hay giữ chỉ ref?
  • Snapshot frame và snapshot scoped selector nên tham gia vào ánh xạ AX như thế nào?