yao/sandbox/docker/chrome/tests/demo-baidu.py
Max 95cdf7961c Add Chrome support to sandbox module and update documentation
- Introduce a new Chrome image for the sandbox, enabling VNC access with real Chrome and CDP support (amd64 only).
- Update README to reflect the addition of the Chrome image and its build instructions.
- Modify build script to include a dedicated build process for the Chrome image.
- Enhance .gitignore to exclude the new Chrome plan file.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 17:48:49 +08:00

349 lines
13 KiB
Python

"""
Baidu Search Demo — PyAutoGUI OS-level Mouse + Smart Keyboard Fallback
Demonstrates anti-detection browser automation inside sandbox-claude-chrome:
1. Open Baidu homepage
2. Click search box with PyAutoGUI (real OS mouse event)
3. Type query with PyAutoGUI keyboard (auto-fallback to Playwright if needed)
4. Submit search
5. Click first search result (PyAutoGUI mouse) → view detail page
6. Go back
7. Click second search result (PyAutoGUI mouse) → view detail page
All mouse clicks are OS-level X11 events via PyAutoGUI — undetectable by websites.
Prerequisites:
- Running inside sandbox-claude-chrome container
- DISPLAY=:99 (Xvfb virtual display)
- VNC optional for live observation (http://localhost:6080)
Usage:
DISPLAY=:99 python3 demo-baidu.py
"""
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth
import pyautogui
import time
import random
import os
# ---------------------------------------------------------------------------
# PyAutoGUI config
# ---------------------------------------------------------------------------
pyautogui.FAILSAFE = False
pyautogui.PAUSE = 0.1
# Screenshot output directory
SCREENSHOT_DIR = os.environ.get("SCREENSHOT_DIR", "/workspace")
# ---------------------------------------------------------------------------
# Human-like helpers
# ---------------------------------------------------------------------------
def human_move(x, y):
"""Move mouse with randomized speed to simulate human behavior."""
duration = random.uniform(0.4, 0.8)
pyautogui.moveTo(x, y, duration=duration)
time.sleep(random.uniform(0.1, 0.3))
def human_click(x, y):
"""Move to (x, y) then click — mimics a real user click."""
human_move(x, y)
time.sleep(random.uniform(0.05, 0.15))
pyautogui.click()
time.sleep(random.uniform(0.2, 0.5))
def smart_type(element, text):
"""Type text with PyAutoGUI first; fallback to Playwright if it didn't land.
On native amd64 Linux, PyAutoGUI keyboard works perfectly.
On ARM Mac (Rosetta 2), X11 keyboard events may not reach Chrome,
so we detect and automatically fallback to Playwright's type().
"""
# Attempt PyAutoGUI keyboard (OS-level X11 events)
for ch in text:
pyautogui.press(ch)
time.sleep(random.uniform(0.05, 0.12))
time.sleep(0.5)
# Verify input landed
actual = element.input_value()
if actual and len(actual) >= len(text) * 0.8:
print(" Keyboard: PyAutoGUI (OS-level) ✓", flush=True)
return
# Fallback: Playwright type()
print(" PyAutoGUI keyboard didn't land — fallback to Playwright", flush=True)
element.fill("")
element.type(text, delay=80)
actual = element.input_value()
print(" Keyboard: Playwright fallback — '{}'".format(actual), flush=True)
def find_element(page, selectors, min_width=50, timeout=2000):
"""Try multiple CSS selectors, return (element, bounding_box) or (None, None)."""
for selector in selectors:
try:
el = page.locator(selector).first
box = el.bounding_box(timeout=timeout)
if box and box["width"] >= min_width:
return el, box
except Exception:
continue
return None, None
def find_results(page, selectors, min_count=2, min_width=100):
"""Find clickable search result elements with bounding boxes.
For Baidu: results are <h3><a href="..." target="_blank">title</a></h3>.
We need the <a> element — it's the actual clickable link.
"""
results = []
for selector in selectors:
elements = page.locator(selector).all()
for el in elements[:10]:
try:
box = el.bounding_box(timeout=1000)
title = el.text_content().strip()
if box and box["width"] >= min_width and title and len(title) > 5:
if not any(r["title"] == title for r in results):
results.append({"box": box, "title": title, "el": el})
except Exception:
pass
if len(results) >= min_count:
break
return results
def click_result(ctx, page, result, label):
"""Click a search result using PyAutoGUI and handle new tab navigation.
Baidu results have target=_blank, so clicking opens a new tab.
We scroll the element into view first, then use PyAutoGUI for the OS-level click.
"""
el = result["el"]
# Scroll element to a safe click zone.
# Baidu results page has a tall fixed search bar at the top (~150px).
# We need the element at y > 300 to avoid clicking the search input.
#
# Strategy: use PyAutoGUI mouse wheel scroll (real OS event) to position
# the element in the middle of the viewport, then re-read coordinates.
el.scroll_into_view_if_needed(timeout=3000)
time.sleep(0.3)
box = el.bounding_box(timeout=2000)
if box and box["y"] < 300:
# Element is too close to top — behind the fixed search bar.
# Use Playwright mouse.wheel to scroll page UP (negative deltaY)
# so the element moves DOWN in the viewport to a safe y > 350.
delta = int(box["y"]) - 400 # negative value scrolls page up
print(" Scrolling page (delta={}) to clear fixed header (y={})".format(
delta, int(box["y"])), flush=True)
page.mouse.wheel(0, delta)
time.sleep(0.8)
# Re-read bounding box after scroll
box = el.bounding_box(timeout=2000)
if not box:
print(" ⚠ Lost element after scroll", flush=True)
return False
# Click the left portion of the link text (more reliable than center)
rx = int(box["x"] + min(box["width"] * 0.3, 150))
ry = int(box["y"] + box["height"] / 2)
print(" [PyAutoGUI] Clicking at ({},{})".format(rx, ry), flush=True)
pages_before = len(ctx.pages)
human_click(rx, ry)
# Wait for new tab — Baidu links have target=_blank, so clicking should
# open a new tab. Give it enough time for the Baidu redirect.
for _ in range(10):
page.wait_for_timeout(500)
if len(ctx.pages) > pages_before:
break
if len(ctx.pages) > pages_before:
target = ctx.pages[-1]
target.wait_for_timeout(6000)
title = target.title()
url = target.url
print(" [New Tab] {} | {}".format(title[:50], url[:80]), flush=True)
screenshot(target, label)
target.close()
page.wait_for_timeout(1000)
return True
else:
# Check if URL changed (same-tab navigation via Baidu redirect)
current_url = page.url
if "baidu.com/s?" not in current_url:
page.wait_for_timeout(5000)
print(" Landed: {} | {}".format(page.title()[:50], page.url[:80]), flush=True)
screenshot(page, label)
print(" [PyAutoGUI] Going back...", flush=True)
pyautogui.hotkey("alt", "Left")
page.wait_for_timeout(3000)
return True
else:
print(" ⚠ Click didn't navigate — still on search page", flush=True)
print(" Trying Playwright click as fallback...", flush=True)
el.click(timeout=5000)
page.wait_for_timeout(3000)
if len(ctx.pages) > pages_before:
target = ctx.pages[-1]
target.wait_for_timeout(6000)
print(" [New Tab via Playwright] {} | {}".format(
target.title()[:50], target.url[:80]), flush=True)
screenshot(target, label)
target.close()
page.wait_for_timeout(1000)
return True
elif "baidu.com/s?" not in page.url:
page.wait_for_timeout(5000)
print(" [Playwright] Landed: {}".format(page.title()[:50]), flush=True)
screenshot(page, label)
pyautogui.hotkey("alt", "Left")
page.wait_for_timeout(3000)
return True
return False
def screenshot(page, name):
"""Save screenshot to SCREENSHOT_DIR."""
path = os.path.join(SCREENSHOT_DIR, name)
page.screenshot(path=path)
print(" Screenshot: " + path, flush=True)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
print("=" * 60, flush=True)
print(" Baidu Search — PyAutoGUI OS-Level Demo", flush=True)
print("=" * 60, flush=True)
with Stealth().use_sync(sync_playwright()) as p:
browser = p.chromium.launch(
channel="chrome",
headless=False,
args=[
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--window-size=1920,1080",
"--window-position=0,0",
],
)
ctx = browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
timezone_id="Asia/Shanghai",
)
page = ctx.new_page()
# Inject stealth script
stealth_path = "/usr/local/share/yao/stealth-init.js"
if os.path.exists(stealth_path):
page.add_init_script(open(stealth_path).read())
# ---- Step 1: Open Baidu ----
print("\n[1/7] Opening Baidu...", flush=True)
page.goto("https://www.baidu.com", timeout=30000)
page.wait_for_timeout(3000)
print(" Title: " + page.title(), flush=True)
screenshot(page, "baidu-01-homepage.png")
# ---- Step 2: Find & click search box ----
print("\n[2/7] Finding search box...", flush=True)
search_selectors = [
"#kw", "input[name=wd]", "input[name=word]",
"input.s_ipt", "input[type=text]", "input[type=search]",
]
search_el, box = find_element(page, search_selectors)
if not box:
# Fallback: Baidu search box typical position
print(" Using fallback coordinates", flush=True)
box = {"x": 600, "y": 350, "width": 600, "height": 40}
cx = int(box["x"] + box["width"] / 2)
cy = int(box["y"] + box["height"] / 2)
print(" [PyAutoGUI] Clicking search box at ({}, {})".format(cx, cy), flush=True)
human_move(100, 100)
time.sleep(0.3)
human_click(cx, cy)
# ---- Step 3: Type search query ----
print("\n[3/7] Typing search query...", flush=True)
query = "yao app engine"
if search_el:
smart_type(search_el, query)
else:
# No element reference — type blindly with PyAutoGUI
for ch in query:
pyautogui.press(ch)
time.sleep(random.uniform(0.06, 0.12))
time.sleep(1)
screenshot(page, "baidu-02-typed.png")
# ---- Step 4: Submit search ----
print("\n[4/7] Submitting search...", flush=True)
pyautogui.press("enter")
page.wait_for_timeout(5000)
print(" URL: " + page.url[:100], flush=True)
print(" Title: " + page.title(), flush=True)
screenshot(page, "baidu-03-results.png")
# ---- Step 5: Find results ----
print("\n[5/7] Finding search results...", flush=True)
result_selectors = [".c-container h3 a", "h3 a", "a:has(h3)"]
results = find_results(page, result_selectors)
print(" Found {} results".format(len(results)), flush=True)
for i, r in enumerate(results[:5]):
print(" [{}] {}".format(i + 1, r["title"][:60]), flush=True)
if len(results) < 3:
print("\n ⚠ Not enough results. Page may show CAPTCHA.", flush=True)
screenshot(page, "baidu-04-no-results.png")
else:
# Baidu results page has a fixed search bar at the top (~150px).
# The first result (index 0) is often right under it, making
# PyAutoGUI click hit the search input instead of the link.
# So we click results starting from index 1 (second result).
# ---- Step 6: Click result #2 ----
print("\n[6/7] Clicking result #2: " + results[1]["title"][:60], flush=True)
click_result(ctx, page, results[1], "baidu-04-page1.png")
# ---- Step 7: Click result #3 ----
# Re-find results (page may have scrolled, coordinates changed)
page.evaluate("window.scrollTo(0, 0)")
page.wait_for_timeout(1000)
results2 = find_results(page, result_selectors)
print("\n Re-found {} results".format(len(results2)), flush=True)
if len(results2) >= 3:
print("\n[7/7] Clicking result #3: " + results2[2]["title"][:60], flush=True)
click_result(ctx, page, results2[2], "baidu-05-page2.png")
else:
print("\n[7/7] ⚠ Could not re-find results for second click", flush=True)
# Keep browser open for VNC observation
print("\n" + "=" * 60, flush=True)
print(" Demo complete! Browser stays open 30s for observation.", flush=True)
print(" Connect via VNC: http://localhost:6080", flush=True)
print("=" * 60, flush=True)
page.wait_for_timeout(30000)
browser.close()
print("Done.", flush=True)
if __name__ == "__main__":
main()