Create browser.py
Browse files- browser.py +161 -0
browser.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Manus-style Browser Server
|
| 4 |
+
CDP screencast β WebSocket β Chrome-skin frontend
|
| 5 |
+
"""
|
| 6 |
+
import asyncio, json, logging, os
|
| 7 |
+
from typing import Optional, Set
|
| 8 |
+
import uvicorn
|
| 9 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 10 |
+
from fastapi.responses import HTMLResponse
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
from playwright.async_api import async_playwright, Browser, BrowserContext, CDPSession, Page
|
| 13 |
+
|
| 14 |
+
log = logging.getLogger("browser")
|
| 15 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
| 16 |
+
|
| 17 |
+
app = FastAPI()
|
| 18 |
+
|
| 19 |
+
# ββ Global state ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
_pw = None
|
| 21 |
+
browser: Optional[Browser] = None
|
| 22 |
+
context: Optional[BrowserContext] = None
|
| 23 |
+
page: Optional[Page] = None
|
| 24 |
+
cdp: Optional[CDPSession] = None
|
| 25 |
+
clients: Set[WebSocket] = set()
|
| 26 |
+
|
| 27 |
+
VIEWPORT_W = 1280
|
| 28 |
+
VIEWPORT_H = 800
|
| 29 |
+
|
| 30 |
+
# ββ Broadcast helpers βββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
+
async def broadcast(msg: dict):
|
| 32 |
+
if not clients: return
|
| 33 |
+
text = json.dumps(msg)
|
| 34 |
+
dead = set()
|
| 35 |
+
for ws in clients:
|
| 36 |
+
try: await ws.send_text(text)
|
| 37 |
+
except: dead.add(ws)
|
| 38 |
+
clients.difference_update(dead)
|
| 39 |
+
|
| 40 |
+
async def push_nav():
|
| 41 |
+
if page:
|
| 42 |
+
try:
|
| 43 |
+
title = await page.title()
|
| 44 |
+
await broadcast({"type": "nav", "url": page.url, "title": title})
|
| 45 |
+
except: pass
|
| 46 |
+
|
| 47 |
+
# ββ Browser init ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
async def init_browser():
|
| 49 |
+
global _pw, browser, context, page, cdp
|
| 50 |
+
_pw = await async_playwright().start()
|
| 51 |
+
browser = await _pw.chromium.launch(
|
| 52 |
+
headless=True,
|
| 53 |
+
args=[
|
| 54 |
+
"--no-sandbox", "--disable-dev-shm-usage",
|
| 55 |
+
"--disable-setuid-sandbox", "--disable-gpu",
|
| 56 |
+
"--no-first-run", "--no-default-browser-check",
|
| 57 |
+
"--disable-background-timer-throttling",
|
| 58 |
+
"--disable-renderer-backgrounding",
|
| 59 |
+
f"--window-size={VIEWPORT_W},{VIEWPORT_H}",
|
| 60 |
+
]
|
| 61 |
+
)
|
| 62 |
+
context = await browser.new_context(
|
| 63 |
+
viewport={"width": VIEWPORT_W, "height": VIEWPORT_H},
|
| 64 |
+
user_agent=(
|
| 65 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
| 66 |
+
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
| 67 |
+
)
|
| 68 |
+
)
|
| 69 |
+
page = await context.new_page()
|
| 70 |
+
|
| 71 |
+
# Navigation events β update URL bar for all clients
|
| 72 |
+
async def on_nav(frame):
|
| 73 |
+
if frame == page.main_frame:
|
| 74 |
+
await push_nav()
|
| 75 |
+
page.on("framenavigated", on_nav)
|
| 76 |
+
|
| 77 |
+
# CDP screencast
|
| 78 |
+
cdp = await context.new_cdp_session(page)
|
| 79 |
+
|
| 80 |
+
async def on_frame(params):
|
| 81 |
+
await broadcast({"type": "frame", "data": params["data"]})
|
| 82 |
+
try:
|
| 83 |
+
await cdp.send("Page.screencastFrameAck", {"sessionId": params["sessionId"]})
|
| 84 |
+
except: pass
|
| 85 |
+
|
| 86 |
+
cdp.on("Page.screencastFrame", on_frame)
|
| 87 |
+
await cdp.send("Page.startScreencast", {
|
| 88 |
+
"format": "jpeg", "quality": 80,
|
| 89 |
+
"maxWidth": VIEWPORT_W, "maxHeight": VIEWPORT_H,
|
| 90 |
+
"everyNthFrame": 1,
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
await page.goto("https://www.google.com")
|
| 94 |
+
log.info("β
Browser ready")
|
| 95 |
+
|
| 96 |
+
@app.on_event("startup")
|
| 97 |
+
async def startup(): await init_browser()
|
| 98 |
+
|
| 99 |
+
# ββ WebSocket endpoint ββββββββββββββββββββββββββββββββββββββββββββ
|
| 100 |
+
@app.websocket("/ws")
|
| 101 |
+
async def ws_handler(websocket: WebSocket):
|
| 102 |
+
await websocket.accept()
|
| 103 |
+
clients.add(websocket)
|
| 104 |
+
await push_nav() # Send current URL to new client
|
| 105 |
+
try:
|
| 106 |
+
while True:
|
| 107 |
+
ev = json.loads(await websocket.receive_text())
|
| 108 |
+
t = ev.get("type")
|
| 109 |
+
if not page: continue
|
| 110 |
+
|
| 111 |
+
if t == "navigate":
|
| 112 |
+
url = ev["url"].strip()
|
| 113 |
+
if not url.startswith(("http://", "https://")):
|
| 114 |
+
url = ("https://" + url) if ("." in url and " " not in url) \
|
| 115 |
+
else f"https://www.google.com/search?q={url}"
|
| 116 |
+
await page.goto(url, wait_until="domcontentloaded")
|
| 117 |
+
|
| 118 |
+
elif t == "back": await page.go_back()
|
| 119 |
+
elif t == "forward": await page.go_forward()
|
| 120 |
+
elif t == "reload": await page.reload(wait_until="domcontentloaded")
|
| 121 |
+
|
| 122 |
+
elif t == "click":
|
| 123 |
+
await page.mouse.click(ev["x"], ev["y"])
|
| 124 |
+
elif t == "dblclick":
|
| 125 |
+
await page.mouse.dblclick(ev["x"], ev["y"])
|
| 126 |
+
elif t == "mousemove":
|
| 127 |
+
await page.mouse.move(ev["x"], ev["y"])
|
| 128 |
+
elif t == "mousedown":
|
| 129 |
+
await page.mouse.down()
|
| 130 |
+
elif t == "mouseup":
|
| 131 |
+
await page.mouse.up()
|
| 132 |
+
elif t == "wheel":
|
| 133 |
+
await page.mouse.wheel(ev.get("dx", 0), ev.get("dy", 0))
|
| 134 |
+
elif t == "keydown":
|
| 135 |
+
key = ev["key"]
|
| 136 |
+
if ev.get("ctrl"): await page.keyboard.down("Control")
|
| 137 |
+
if ev.get("shift"): await page.keyboard.down("Shift")
|
| 138 |
+
if ev.get("alt"): await page.keyboard.down("Alt")
|
| 139 |
+
await page.keyboard.down(key)
|
| 140 |
+
await page.keyboard.up(key)
|
| 141 |
+
if ev.get("ctrl"): await page.keyboard.up("Control")
|
| 142 |
+
if ev.get("shift"): await page.keyboard.up("Shift")
|
| 143 |
+
if ev.get("alt"): await page.keyboard.up("Alt")
|
| 144 |
+
elif t == "type":
|
| 145 |
+
await page.keyboard.type(ev["text"])
|
| 146 |
+
|
| 147 |
+
except WebSocketDisconnect:
|
| 148 |
+
clients.discard(websocket)
|
| 149 |
+
except Exception as e:
|
| 150 |
+
log.error(f"WS error: {e}")
|
| 151 |
+
clients.discard(websocket)
|
| 152 |
+
|
| 153 |
+
@app.get("/")
|
| 154 |
+
async def index():
|
| 155 |
+
with open("/app/static/browser.html") as f:
|
| 156 |
+
return HTMLResponse(f.read())
|
| 157 |
+
|
| 158 |
+
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
uvicorn.run(app, host="0.0.0.0", port=8888, log_level="warning")
|