abc1181 commited on
Commit
a88b4d0
Β·
verified Β·
1 Parent(s): 3f7f1bb

Create browser.py

Browse files
Files changed (1) hide show
  1. 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")