THEZYZSTUDIO commited on
Commit
32fa524
Β·
verified Β·
1 Parent(s): 5b981fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -282
app.py CHANGED
@@ -1,65 +1,41 @@
1
  """
2
- THE Z AI β€” Computer Mode Server (fixed)
3
- ======================================
4
-
5
- Fixes included:
6
- - Robust Linux/Xvfb screenshot capture with scrot fallback
7
- - Explicit DISPLAY/XAUTHORITY handling
8
- - Safer Firefox launch helper (optional)
9
- - Clearer logging and error reporting
10
- - Better WebSocket resilience
11
-
12
- Compatible with the existing front-end WebSocket protocol.
13
  """
14
 
15
- from __future__ import annotations
16
-
17
  import asyncio
18
  import base64
19
  import io
20
  import json
21
- import logging
22
  import os
23
- import shutil
24
  import subprocess
25
- import tempfile
26
  import time
 
27
  from pathlib import Path
28
- from typing import Any, Dict, Optional
29
 
30
  import pyautogui
31
  import pyperclip
 
32
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
33
  from fastapi.middleware.cors import CORSMiddleware
34
  from fastapi.responses import JSONResponse
35
- from PIL import Image, ImageGrab
36
  import uvicorn
37
 
38
- # ---------------------------------------------------------------------
39
- # Environment / logging
40
- # ---------------------------------------------------------------------
41
-
42
- logging.basicConfig(
43
- level=os.environ.get("LOG_LEVEL", "INFO").upper(),
44
- format="[%(asctime)s] %(levelname)s %(name)s: %(message)s",
45
- )
46
- logger = logging.getLogger("z-computer-mode")
47
-
48
  DISPLAY = os.environ.get("DISPLAY", ":1")
49
- XAUTHORITY = os.environ.get("XAUTHORITY", "/home/zai/.Xauthority")
50
  os.environ["DISPLAY"] = DISPLAY
51
- os.environ["XAUTHORITY"] = XAUTHORITY
52
- os.environ.setdefault("PYTHONUNBUFFERED", "1")
53
-
54
- # PyAutoGUI can be too aggressive in headless environments.
55
  pyautogui.FAILSAFE = False
56
- pyautogui.PAUSE = 0.03
57
-
58
- # ---------------------------------------------------------------------
59
- # App setup
60
- # ---------------------------------------------------------------------
61
 
62
  app = FastAPI(title="Z-Computer-Mode API")
 
63
  app.add_middleware(
64
  CORSMiddleware,
65
  allow_origins=["*"],
@@ -68,141 +44,54 @@ app.add_middleware(
68
  allow_headers=["*"],
69
  )
70
 
 
71
  active_connections: list[WebSocket] = []
72
  stream_active = False
73
- stream_quality = int(os.environ.get("STREAM_QUALITY", "60"))
74
- stream_fps = int(os.environ.get("STREAM_FPS", "3"))
75
- stream_scale = float(os.environ.get("STREAM_SCALE", "0.5"))
76
-
77
 
78
- # ---------------------------------------------------------------------
79
- # Helpers
80
- # ---------------------------------------------------------------------
81
 
82
- def _env_with_display(extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
83
- env = dict(os.environ)
84
- env["DISPLAY"] = DISPLAY
85
- env["XAUTHORITY"] = XAUTHORITY
86
- if extra:
87
- env.update(extra)
88
- return env
89
 
90
-
91
- def _ensure_xauthority() -> None:
92
  try:
93
- Path(XAUTHORITY).parent.mkdir(parents=True, exist_ok=True)
94
- Path(XAUTHORITY).touch(exist_ok=True)
95
- except Exception as exc:
96
- logger.warning("Could not prepare XAUTHORITY file: %s", exc)
97
-
98
-
99
- def _image_to_b64(img: Image.Image, quality: int = 70) -> str:
100
- buf = io.BytesIO()
101
- img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
102
- return base64.b64encode(buf.getvalue()).decode("ascii")
103
-
104
-
105
- def _capture_with_scrot() -> Optional[Image.Image]:
106
- scrot = shutil.which("scrot")
107
- if not scrot:
108
- return None
109
-
110
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
111
- tmp_path = tmp.name
112
-
113
- try:
114
- # -z hides the cursor; remove it if you want cursor visible.
115
- proc = subprocess.run(
116
- [scrot, "-z", tmp_path],
117
- capture_output=True,
118
- text=True,
119
- timeout=15,
120
- env=_env_with_display(),
121
- )
122
- if proc.returncode != 0:
123
- logger.debug("scrot failed: %s %s", proc.stdout, proc.stderr)
124
- return None
125
- if not Path(tmp_path).exists() or Path(tmp_path).stat().st_size == 0:
126
- return None
127
- return Image.open(tmp_path)
128
- except Exception as exc:
129
- logger.debug("scrot capture failed: %s", exc)
130
- return None
131
- finally:
132
- try:
133
- Path(tmp_path).unlink(missing_ok=True)
134
- except Exception:
135
- pass
136
-
137
-
138
- def _capture_with_pil() -> Optional[Image.Image]:
139
- # ImageGrab on Linux often needs an explicit xdisplay.
140
- try:
141
- try:
142
- return ImageGrab.grab(xdisplay=DISPLAY)
143
- except TypeError:
144
- # Older Pillow versions may not accept xdisplay.
145
- return ImageGrab.grab()
146
- except Exception as exc:
147
- logger.debug("ImageGrab failed: %s", exc)
148
- return None
149
-
150
-
151
- def capture_screen(scale: float = 0.5, quality: int = 60) -> str:
152
- """Capture the Xvfb display and return a base64 JPEG string.
153
-
154
- Strategy:
155
- 1) scrot (most reliable on Linux/Xvfb)
156
- 2) PIL.ImageGrab as fallback
157
- 3) return empty string on failure
158
- """
159
- try:
160
- _ensure_xauthority()
161
-
162
- img = _capture_with_scrot()
163
- if img is None:
164
- img = _capture_with_pil()
165
- if img is None:
166
- return ""
167
-
168
- # Normalize and optionally resize.
169
- if scale and 0 < scale < 1.0:
170
- w = max(1, int(img.width * scale))
171
- h = max(1, int(img.height * scale))
172
  img = img.resize((w, h), Image.LANCZOS)
173
-
174
- return _image_to_b64(img, quality=quality)
175
- except Exception as exc:
176
- logger.exception("capture_screen failed: %s", exc)
177
  return ""
178
 
179
 
180
- def run_command(cmd: str, timeout: int = 30) -> Dict[str, Any]:
181
- """Run a shell command and return stdout/stderr/returncode."""
182
  try:
183
  result = subprocess.run(
184
- cmd,
185
- shell=True,
186
- capture_output=True,
187
- text=True,
188
- timeout=timeout,
189
- env=_env_with_display(),
190
  )
191
  return {
192
- "stdout": result.stdout[-8000:] if result.stdout else "",
193
- "stderr": result.stderr[-4000:] if result.stderr else "",
194
  "returncode": result.returncode,
195
  }
196
  except subprocess.TimeoutExpired:
197
  return {"stdout": "", "stderr": f"Command timed out after {timeout}s", "returncode": -1}
198
- except Exception as exc:
199
- return {"stdout": "", "stderr": str(exc), "returncode": -1}
200
 
201
 
202
- async def broadcast(msg: Dict[str, Any]) -> None:
203
- txt = json.dumps(msg, ensure_ascii=False)
204
- disconnected: list[WebSocket] = []
205
- for ws in list(active_connections):
 
206
  try:
207
  await ws.send_text(txt)
208
  except Exception:
@@ -212,263 +101,233 @@ async def broadcast(msg: Dict[str, Any]) -> None:
212
  active_connections.remove(ws)
213
 
214
 
215
- async def screen_stream_loop() -> None:
 
 
216
  global stream_active
217
- interval = 1.0 / max(1, stream_fps)
218
  while stream_active and active_connections:
219
  try:
220
- frame = capture_screen(scale=stream_scale, quality=stream_quality)
221
  if frame:
222
  await broadcast({"type": "frame", "data": frame})
223
- except Exception as exc:
224
- logger.debug("screen_stream_loop error: %s", exc)
225
  await asyncio.sleep(interval)
226
  stream_active = False
227
 
228
 
229
- def _open_app_process(app_cmd: str) -> None:
230
- if not app_cmd:
231
- return
232
- subprocess.Popen(
233
- app_cmd,
234
- shell=True,
235
- stdout=subprocess.DEVNULL,
236
- stderr=subprocess.DEVNULL,
237
- env=_env_with_display(),
238
- start_new_session=True,
239
- )
240
-
241
-
242
- # Optional helper if you want the server to launch Firefox itself.
243
- # Not required by the protocol, but useful when start.sh is misordered.
244
- def launch_firefox_if_missing() -> None:
245
- if not os.environ.get("AUTO_LAUNCH_FIREFOX", "0").lower() in {"1", "true", "yes"}:
246
- return
247
 
248
- try:
249
- # Heuristic only; do not fail if Firefox is already running.
250
- subprocess.Popen(
251
- "DISPLAY='{}' firefox --no-sandbox --new-instance about:blank".format(DISPLAY),
252
- shell=True,
253
- stdout=subprocess.DEVNULL,
254
- stderr=subprocess.DEVNULL,
255
- env=_env_with_display(),
256
- start_new_session=True,
257
- )
258
- logger.info("Firefox launch requested by AUTO_LAUNCH_FIREFOX")
259
- except Exception as exc:
260
- logger.warning("Firefox auto-launch failed: %s", exc)
261
-
262
-
263
- # ---------------------------------------------------------------------
264
- # Action handler
265
- # ---------------------------------------------------------------------
266
-
267
- async def handle_action(ws: WebSocket, msg: Dict[str, Any]) -> None:
268
- global stream_active, stream_fps, stream_quality, stream_scale
269
  action = msg.get("action", "")
270
- data = msg.get("data", {}) or {}
271
 
 
272
  if action == "screenshot":
273
  frame = capture_screen(scale=0.8, quality=75)
274
- await ws.send_text(json.dumps({"type": "screenshot", "data": frame, "ts": int(time.time() * 1000)}, ensure_ascii=False))
 
 
 
 
275
 
 
276
  elif action == "terminal":
277
- cmd = str(data.get("cmd", ""))
278
  if cmd:
279
- result = run_command(cmd, timeout=int(data.get("timeout", 30)))
280
  await ws.send_text(json.dumps({
281
  "type": "terminal_result",
282
  "cmd": cmd,
283
  "stdout": result["stdout"],
284
  "stderr": result["stderr"],
285
  "returncode": result["returncode"],
286
- }, ensure_ascii=False))
 
287
  await asyncio.sleep(0.5)
288
  frame = capture_screen(scale=0.7, quality=70)
289
  if frame:
290
- await ws.send_text(json.dumps({"type": "screenshot", "data": frame, "ts": int(time.time() * 1000), "auto": True}, ensure_ascii=False))
291
-
 
 
 
 
 
 
292
  elif action == "mouse_move":
293
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
294
  pyautogui.moveTo(x, y, duration=0.1)
295
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_move"}, ensure_ascii=False))
296
 
 
297
  elif action == "mouse_click":
298
- x = int(data.get("x", 0))
299
- y = int(data.get("y", 0))
300
- button = str(data.get("button", "left"))
301
- double = bool(data.get("double", False))
302
  pyautogui.moveTo(x, y, duration=0.08)
303
  if double:
304
  pyautogui.doubleClick(x, y, button=button)
305
  else:
306
  pyautogui.click(x, y, button=button)
307
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_click"}, ensure_ascii=False))
308
 
 
309
  elif action == "mouse_drag":
310
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
311
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
312
  pyautogui.moveTo(x1, y1)
313
  pyautogui.dragTo(x2, y2, duration=0.3, button="left")
314
- await ws.send_text(json.dumps({"type": "ack", "action": "mouse_drag"}, ensure_ascii=False))
315
 
 
316
  elif action == "keyboard_type":
317
- text = str(data.get("text", ""))
318
  pyautogui.typewrite(text, interval=0.03)
319
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_type"}, ensure_ascii=False))
320
 
 
321
  elif action == "keyboard_hotkey":
322
  keys = data.get("keys", [])
323
  if keys:
324
  pyautogui.hotkey(*keys)
325
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_hotkey"}, ensure_ascii=False))
326
 
 
327
  elif action == "keyboard_press":
328
- key = str(data.get("key", ""))
329
  if key:
330
  pyautogui.press(key)
331
- await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_press"}, ensure_ascii=False))
332
 
 
333
  elif action == "clipboard_write":
334
- text = str(data.get("text", ""))
335
  pyperclip.copy(text)
336
- await ws.send_text(json.dumps({"type": "ack", "action": "clipboard_write"}, ensure_ascii=False))
337
 
 
338
  elif action == "clipboard_read":
339
  text = pyperclip.paste()
340
- await ws.send_text(json.dumps({"type": "clipboard_content", "text": text}, ensure_ascii=False))
341
 
 
342
  elif action == "scroll":
343
- x = int(data.get("x", 0))
344
- y = int(data.get("y", 0))
345
  clicks = int(data.get("clicks", 3))
346
  pyautogui.scroll(clicks, x=x, y=y)
347
- await ws.send_text(json.dumps({"type": "ack", "action": "scroll"}, ensure_ascii=False))
348
 
 
349
  elif action == "open_app":
350
- app_cmd = str(data.get("cmd", ""))
351
  if app_cmd:
352
- _open_app_process(app_cmd)
353
- await ws.send_text(json.dumps({"type": "ack", "action": "open_app"}, ensure_ascii=False))
 
 
 
 
 
354
  await asyncio.sleep(3)
355
  frame = capture_screen(scale=0.7, quality=70)
356
  if frame:
357
- await ws.send_text(json.dumps({"type": "screenshot", "data": frame, "ts": int(time.time() * 1000), "auto": True}, ensure_ascii=False))
 
 
 
 
 
358
 
 
359
  elif action == "start_stream":
360
- stream_fps = int(data.get("fps", 3))
 
361
  stream_quality = int(data.get("quality", 60))
362
- stream_scale = float(data.get("scale", 0.5))
363
  if not stream_active:
364
  stream_active = True
365
  asyncio.create_task(screen_stream_loop())
366
- await ws.send_text(json.dumps({"type": "ack", "action": "start_stream"}, ensure_ascii=False))
367
 
 
368
  elif action == "stop_stream":
369
  stream_active = False
370
- await ws.send_text(json.dumps({"type": "ack", "action": "stop_stream"}, ensure_ascii=False))
371
 
 
372
  elif action == "screen_info":
373
  try:
374
  w, h = pyautogui.size()
375
  mx, my = pyautogui.position()
376
- except Exception:
377
  w, h, mx, my = 1920, 1080, 0, 0
378
  await ws.send_text(json.dumps({
379
  "type": "screen_info",
380
- "width": w,
381
- "height": h,
382
- "mouse_x": mx,
383
- "mouse_y": my,
384
- }, ensure_ascii=False))
385
 
386
  else:
387
- await ws.send_text(json.dumps({"type": "error", "msg": f"Unknown action: {action}"}, ensure_ascii=False))
388
 
389
 
390
- # ---------------------------------------------------------------------
391
- # WebSocket + REST endpoints
392
- # ---------------------------------------------------------------------
393
 
394
  @app.websocket("/ws")
395
  async def websocket_endpoint(ws: WebSocket):
396
  await ws.accept()
397
  active_connections.append(ws)
398
-
399
  try:
400
  w, h = pyautogui.size()
401
- except Exception:
402
  w, h = 1920, 1080
403
-
404
  await ws.send_text(json.dumps({
405
  "type": "connected",
406
  "screen_width": w,
407
  "screen_height": h,
408
- "display": DISPLAY,
409
- "msg": "Z Computer Mode β€” Connected",
410
- }, ensure_ascii=False))
411
-
412
  try:
413
  while True:
414
  raw = await ws.receive_text()
415
- try:
416
- msg = json.loads(raw)
417
- except json.JSONDecodeError:
418
- await ws.send_text(json.dumps({"type": "error", "msg": "Invalid JSON"}, ensure_ascii=False))
419
- continue
420
  await handle_action(ws, msg)
421
  except WebSocketDisconnect:
422
  pass
423
- except Exception as exc:
424
- logger.exception("WebSocket error: %s", exc)
425
  finally:
426
  if ws in active_connections:
427
  active_connections.remove(ws)
428
 
429
 
 
 
430
  @app.get("/screenshot")
431
  async def rest_screenshot():
432
  frame = capture_screen(scale=0.75, quality=70)
433
- return JSONResponse({"image": frame, "ts": int(time.time() * 1000), "display": DISPLAY})
434
 
435
 
436
  @app.post("/terminal")
437
  async def rest_terminal(body: dict):
438
- cmd = str(body.get("cmd", ""))
439
- result = run_command(cmd, timeout=int(body.get("timeout", 30)))
440
  return JSONResponse(result)
441
 
442
 
443
  @app.get("/health")
444
  async def health():
445
- try:
446
- w, h = pyautogui.size()
447
- except Exception:
448
- w, h = 1920, 1080
449
- return {
450
- "status": "ok",
451
- "display": DISPLAY,
452
- "xauthority": XAUTHORITY,
453
- "xauthority_exists": Path(XAUTHORITY).exists(),
454
- "scrot": shutil.which("scrot"),
455
- "firefox": shutil.which("firefox"),
456
- "screen_width": w,
457
- "screen_height": h,
458
- }
459
-
460
-
461
- @app.on_event("startup")
462
- async def _startup() -> None:
463
- _ensure_xauthority()
464
- launch_firefox_if_missing()
465
- logger.info("Z Computer Mode API started on DISPLAY=%s", DISPLAY)
466
 
467
 
468
- # ---------------------------------------------------------------------
469
- # Entry
470
- # ---------------------------------------------------------------------
471
 
472
  if __name__ == "__main__":
473
  port = int(os.environ.get("PORT", 7860))
474
- uvicorn.run("app_fixed:app", host="0.0.0.0", port=port, log_level=os.environ.get("UVICORN_LOG_LEVEL", "warning"))
 
1
  """
2
+ THE Z AI β€” Computer Mode Server
3
+ ================================
4
+ سيرفر Ψ§Ω„ΨͺΨ­ΩƒΩ… Ψ¨Ψ§Ω„ΩƒΩ…Ψ¨ΩŠΩˆΨͺΨ± ΨΉΩ† Ψ¨ΨΉΨ― Ω„Ω„Ψ°ΩƒΨ§Ψ‘ Ψ§Ω„Ψ§Ψ΅Ψ·Ω†Ψ§ΨΉΩŠ
5
+ يوفر:
6
+ - WebSocket Ω„Ω„ΨͺΨ­ΩƒΩ… Ψ§Ω„Ω„Ψ­ΨΈΩŠ (Ψ§Ω„Ω…Ψ§ΩˆΨ³ΨŒ Ω„ΩˆΨ­Ψ© المفاΨͺيح، Ψ§Ω„ΨͺΨ±Ω…ΩŠΩ†Ψ§Ω„)
7
+ - Ψͺءوير Ψ³ΩƒΨ±ΩŠΩ† شوΨͺ وΨ₯Ψ±Ψ³Ψ§Ω„Ω‡ Ω„Ω„ΨΉΩ…ΩŠΩ„
8
+ - Ψ¨Ψ« Ψ΄Ψ§Ψ΄Ψ© Ω…Ψ¨Ψ§Ψ΄Ψ± (base64 frames)
9
+ - ΨͺΩ†ΩΩŠΨ° Ψ£ΩˆΨ§Ω…Ψ± bash ΩˆΩ‚Ψ±Ψ§Ψ‘Ψ© Ψ§Ω„Ω†Ψ§ΨͺΨ¬
 
 
 
10
  """
11
 
 
 
12
  import asyncio
13
  import base64
14
  import io
15
  import json
 
16
  import os
 
17
  import subprocess
18
+ import threading
19
  import time
20
+ import traceback
21
  from pathlib import Path
 
22
 
23
  import pyautogui
24
  import pyperclip
25
+ from PIL import Image, ImageGrab
26
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from fastapi.responses import JSONResponse
 
29
  import uvicorn
30
 
31
+ # ─── ENV ────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
32
  DISPLAY = os.environ.get("DISPLAY", ":1")
 
33
  os.environ["DISPLAY"] = DISPLAY
 
 
 
 
34
  pyautogui.FAILSAFE = False
35
+ pyautogui.PAUSE = 0.05
 
 
 
 
36
 
37
  app = FastAPI(title="Z-Computer-Mode API")
38
+
39
  app.add_middleware(
40
  CORSMiddleware,
41
  allow_origins=["*"],
 
44
  allow_headers=["*"],
45
  )
46
 
47
+ # ─── Active Connections ──────────────────────────────
48
  active_connections: list[WebSocket] = []
49
  stream_active = False
50
+ stream_quality = 60 # JPEG quality
51
+ stream_fps = 3 # frames per second (low to save bandwidth)
52
+ stream_scale = 0.5 # downscale factor
 
53
 
 
 
 
54
 
55
+ # ─── Utility ─────────────────────────────────────────
 
 
 
 
 
 
56
 
57
+ def capture_screen(scale=stream_scale, quality=stream_quality) -> str:
58
+ """Capture screen β†’ base64 JPEG string"""
59
  try:
60
+ img = ImageGrab.grab()
61
+ if scale < 1.0:
62
+ w = int(img.width * scale)
63
+ h = int(img.height * scale)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  img = img.resize((w, h), Image.LANCZOS)
65
+ buf = io.BytesIO()
66
+ img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
67
+ return base64.b64encode(buf.getvalue()).decode()
68
+ except Exception as e:
69
  return ""
70
 
71
 
72
+ def run_command(cmd: str, timeout: int = 30) -> dict:
73
+ """Run a shell command and return stdout/stderr"""
74
  try:
75
  result = subprocess.run(
76
+ cmd, shell=True, capture_output=True, text=True,
77
+ timeout=timeout, env={**os.environ, "DISPLAY": DISPLAY}
 
 
 
 
78
  )
79
  return {
80
+ "stdout": result.stdout[-8000:] if len(result.stdout) > 8000 else result.stdout,
81
+ "stderr": result.stderr[-4000:] if len(result.stderr) > 4000 else result.stderr,
82
  "returncode": result.returncode,
83
  }
84
  except subprocess.TimeoutExpired:
85
  return {"stdout": "", "stderr": f"Command timed out after {timeout}s", "returncode": -1}
86
+ except Exception as e:
87
+ return {"stdout": "", "stderr": str(e), "returncode": -1}
88
 
89
 
90
+ async def broadcast(msg: dict):
91
+ """Send JSON message to all connected WebSocket clients"""
92
+ txt = json.dumps(msg)
93
+ disconnected = []
94
+ for ws in active_connections:
95
  try:
96
  await ws.send_text(txt)
97
  except Exception:
 
101
  active_connections.remove(ws)
102
 
103
 
104
+ # ─── Screen Streaming Loop ───────────────────────────
105
+
106
+ async def screen_stream_loop():
107
  global stream_active
108
+ interval = 1.0 / stream_fps
109
  while stream_active and active_connections:
110
  try:
111
+ frame = capture_screen()
112
  if frame:
113
  await broadcast({"type": "frame", "data": frame})
114
+ except Exception:
115
+ pass
116
  await asyncio.sleep(interval)
117
  stream_active = False
118
 
119
 
120
+ # ─── Action Handler ──────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ async def handle_action(ws: WebSocket, msg: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  action = msg.get("action", "")
124
+ data = msg.get("data", {})
125
 
126
+ # ── SCREENSHOT ──────────────────────────────────
127
  if action == "screenshot":
128
  frame = capture_screen(scale=0.8, quality=75)
129
+ await ws.send_text(json.dumps({
130
+ "type": "screenshot",
131
+ "data": frame,
132
+ "ts": int(time.time() * 1000)
133
+ }))
134
 
135
+ # ── TERMINAL COMMAND ─────────────────────────────
136
  elif action == "terminal":
137
+ cmd = data.get("cmd", "")
138
  if cmd:
139
+ result = run_command(cmd, timeout=data.get("timeout", 30))
140
  await ws.send_text(json.dumps({
141
  "type": "terminal_result",
142
  "cmd": cmd,
143
  "stdout": result["stdout"],
144
  "stderr": result["stderr"],
145
  "returncode": result["returncode"],
146
+ }))
147
+ # Auto-screenshot after terminal command
148
  await asyncio.sleep(0.5)
149
  frame = capture_screen(scale=0.7, quality=70)
150
  if frame:
151
+ await ws.send_text(json.dumps({
152
+ "type": "screenshot",
153
+ "data": frame,
154
+ "ts": int(time.time() * 1000),
155
+ "auto": True
156
+ }))
157
+
158
+ # ── MOUSE MOVE ───────────────────────────────────
159
  elif action == "mouse_move":
160
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
161
  pyautogui.moveTo(x, y, duration=0.1)
162
+ await ws.send_text(json.dumps({"type": "ack", "action": "mouse_move"}))
163
 
164
+ # ── MOUSE CLICK ──────────────────────────────────
165
  elif action == "mouse_click":
166
+ x = int(data.get("x", 0))
167
+ y = int(data.get("y", 0))
168
+ button = data.get("button", "left")
169
+ double = data.get("double", False)
170
  pyautogui.moveTo(x, y, duration=0.08)
171
  if double:
172
  pyautogui.doubleClick(x, y, button=button)
173
  else:
174
  pyautogui.click(x, y, button=button)
175
+ await ws.send_text(json.dumps({"type": "ack", "action": "mouse_click"}))
176
 
177
+ # ── MOUSE DRAG ───────────────────────────────────
178
  elif action == "mouse_drag":
179
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
180
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
181
  pyautogui.moveTo(x1, y1)
182
  pyautogui.dragTo(x2, y2, duration=0.3, button="left")
183
+ await ws.send_text(json.dumps({"type": "ack", "action": "mouse_drag"}))
184
 
185
+ # ── KEYBOARD TYPE ────────────────────────────────
186
  elif action == "keyboard_type":
187
+ text = data.get("text", "")
188
  pyautogui.typewrite(text, interval=0.03)
189
+ await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_type"}))
190
 
191
+ # ── KEYBOARD HOTKEY ──────────────────────────────
192
  elif action == "keyboard_hotkey":
193
  keys = data.get("keys", [])
194
  if keys:
195
  pyautogui.hotkey(*keys)
196
+ await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_hotkey"}))
197
 
198
+ # ── KEYBOARD PRESS ───────────────────────────────
199
  elif action == "keyboard_press":
200
+ key = data.get("key", "")
201
  if key:
202
  pyautogui.press(key)
203
+ await ws.send_text(json.dumps({"type": "ack", "action": "keyboard_press"}))
204
 
205
+ # ── CLIPBOARD WRITE ──────────────────────────────
206
  elif action == "clipboard_write":
207
+ text = data.get("text", "")
208
  pyperclip.copy(text)
209
+ await ws.send_text(json.dumps({"type": "ack", "action": "clipboard_write"}))
210
 
211
+ # ── CLIPBOARD READ ───────────────────────────────
212
  elif action == "clipboard_read":
213
  text = pyperclip.paste()
214
+ await ws.send_text(json.dumps({"type": "clipboard_content", "text": text}))
215
 
216
+ # ── SCROLL ───────────────────────────────────────
217
  elif action == "scroll":
218
+ x = int(data.get("x", 0))
219
+ y = int(data.get("y", 0))
220
  clicks = int(data.get("clicks", 3))
221
  pyautogui.scroll(clicks, x=x, y=y)
222
+ await ws.send_text(json.dumps({"type": "ack", "action": "scroll"}))
223
 
224
+ # ── OPEN APP ─────────────────────────────────────
225
  elif action == "open_app":
226
+ app_cmd = data.get("cmd", "")
227
  if app_cmd:
228
+ subprocess.Popen(
229
+ app_cmd, shell=True,
230
+ env={**os.environ, "DISPLAY": DISPLAY},
231
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
232
+ )
233
+ await ws.send_text(json.dumps({"type": "ack", "action": "open_app"}))
234
+ # Auto-screenshot after app opens (wait for it to load)
235
  await asyncio.sleep(3)
236
  frame = capture_screen(scale=0.7, quality=70)
237
  if frame:
238
+ await ws.send_text(json.dumps({
239
+ "type": "screenshot",
240
+ "data": frame,
241
+ "ts": int(time.time() * 1000),
242
+ "auto": True
243
+ }))
244
 
245
+ # ── START STREAM ─────────────────────────────────
246
  elif action == "start_stream":
247
+ global stream_active, stream_fps, stream_quality, stream_scale
248
+ stream_fps = int(data.get("fps", 3))
249
  stream_quality = int(data.get("quality", 60))
250
+ stream_scale = float(data.get("scale", 0.5))
251
  if not stream_active:
252
  stream_active = True
253
  asyncio.create_task(screen_stream_loop())
254
+ await ws.send_text(json.dumps({"type": "ack", "action": "start_stream"}))
255
 
256
+ # ── STOP STREAM ──────────────────────────────────
257
  elif action == "stop_stream":
258
  stream_active = False
259
+ await ws.send_text(json.dumps({"type": "ack", "action": "stop_stream"}))
260
 
261
+ # ── GET SCREEN INFO ──────────────────────────────
262
  elif action == "screen_info":
263
  try:
264
  w, h = pyautogui.size()
265
  mx, my = pyautogui.position()
266
+ except:
267
  w, h, mx, my = 1920, 1080, 0, 0
268
  await ws.send_text(json.dumps({
269
  "type": "screen_info",
270
+ "width": w, "height": h,
271
+ "mouse_x": mx, "mouse_y": my,
272
+ }))
 
 
273
 
274
  else:
275
+ await ws.send_text(json.dumps({"type": "error", "msg": f"Unknown action: {action}"}))
276
 
277
 
278
+ # ─── WebSocket Endpoint ──────────────────────────────
 
 
279
 
280
  @app.websocket("/ws")
281
  async def websocket_endpoint(ws: WebSocket):
282
  await ws.accept()
283
  active_connections.append(ws)
284
+ # Send welcome
285
  try:
286
  w, h = pyautogui.size()
287
+ except:
288
  w, h = 1920, 1080
 
289
  await ws.send_text(json.dumps({
290
  "type": "connected",
291
  "screen_width": w,
292
  "screen_height": h,
293
+ "msg": "Z Computer Mode β€” Connected"
294
+ }))
 
 
295
  try:
296
  while True:
297
  raw = await ws.receive_text()
298
+ msg = json.loads(raw)
 
 
 
 
299
  await handle_action(ws, msg)
300
  except WebSocketDisconnect:
301
  pass
302
+ except Exception as e:
303
+ pass
304
  finally:
305
  if ws in active_connections:
306
  active_connections.remove(ws)
307
 
308
 
309
+ # ─── REST Fallback ───────────────────────────────────
310
+
311
  @app.get("/screenshot")
312
  async def rest_screenshot():
313
  frame = capture_screen(scale=0.75, quality=70)
314
+ return JSONResponse({"image": frame, "ts": int(time.time() * 1000)})
315
 
316
 
317
  @app.post("/terminal")
318
  async def rest_terminal(body: dict):
319
+ cmd = body.get("cmd", "")
320
+ result = run_command(cmd, timeout=body.get("timeout", 30))
321
  return JSONResponse(result)
322
 
323
 
324
  @app.get("/health")
325
  async def health():
326
+ return {"status": "ok", "display": DISPLAY}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
 
329
+ # ─── Entry ───────────────────────────────────────────
 
 
330
 
331
  if __name__ == "__main__":
332
  port = int(os.environ.get("PORT", 7860))
333
+ uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="warning")