THEZYZSTUDIO commited on
Commit
3ebde61
ยท
verified ยท
1 Parent(s): bf7bc37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +459 -360
app.py CHANGED
@@ -1,36 +1,19 @@
1
  """
2
- THE Z AI โ€” Computer Mode Server v10 โ€” SCREENSHOT RACE-CONDITION FIXED
3
- =======================================================================
4
- ุงู„ู…ุดุงูƒู„ ุงู„ุชูŠ ุชุณุจุจ "ู„ู… ุชุตู„ ู„ู‚ุทุฉ ุดุงุดุฉ" โ€” ูˆุฌุฏู†ุงู‡ุง ูˆุฃุตู„ุญู†ุงู‡ุง:
5
-
6
- ๐Ÿ› BUG 1 โ€” Delta Suppression ูŠูƒุชู… Screenshot ุงู„ู…ุทู„ูˆุจุฉ ุตุฑุงุญุฉู‹:
7
- ููŠ v9ุŒ ุฏุงู„ุฉ shot() ูƒุงู†ุช ุชุชุญู‚ู‚ ู…ู† hash ุงู„ุตูˆุฑุฉ ุงู„ุณุงุจู‚ุฉ ูˆุชูู„ุบูŠ ุงู„ุฅุฑุณุงู„ ุฅุฐุง
8
- "ู„ู… ุชุชุบูŠุฑ ุงู„ุตูˆุฑุฉ" โ€” ู„ูƒู† ุงู„ู€ HTML ูŠุทู„ุจ screenshot ุจุดูƒู„ ุตุฑูŠุญ ูˆูŠุณุชุธู‡ุฑ ูˆุตูˆู„ู‡ุง
9
- ุจู…ู‚ุงุฑู†ุฉ _pcLastFrameTs. ุฅุฐุง ุงู„ุดุงุดุฉ ู„ู… ุชุชุบูŠุฑ (ู…ุซู„ุงู‹ ุจุนุฏ keyboard_hotkey)ุŒ
10
- ุงู„ุณูŠุฑูุฑ ูŠูู„ุบูŠ ุงู„ุฅุฑุณุงู„ โ†’ ุงู„ู€ HTML ูŠู†ุชุธุฑ ุฅู„ู‰ ู…ุง ู„ุง ู†ู‡ุงูŠุฉ โ†’ "ู„ู… ุชุตู„ ู„ู‚ุทุฉ ุดุงุดุฉ".
11
- ุงู„ุฅุตู„ุงุญ: Delta suppression ุชู†ุทุจู‚ ุนู„ู‰ auto_shot ูู‚ุท (ุงู„ุฎู„ููŠุฉ)ุŒ ูˆู„ูŠุณ ุนู„ู‰
12
- ุทู„ุจุงุช screenshot ุงู„ุตุฑูŠุญุฉ (action == "screenshot").
13
-
14
- ๐Ÿ› BUG 2 โ€” auto_shot ูƒู€ background task ูŠุชุฃุฎุฑ ุฃูˆ ูŠูู„ุบู‰:
15
- asyncio.create_task(shot_bg(...)) ูƒุงู†ุช ุชูู†ุดุฆ task ููŠ ุงู„ุฎู„ููŠุฉ ู‚ุฏ ูŠุชุฃุฎุฑ ุชู†ููŠุฐู‡ุง
16
- ู„ุฃู† asyncio ูŠูุฌุฏูˆู„ู‡ุง ู„ุงุญู‚ุงู‹ โ€” ุงู„ู€ HTML ูŠุทู„ุจ screenshot ุซู… ูŠู†ุชุธุฑ 12 ุซุงู†ูŠุฉ
17
- ู„ูƒู† ุงู„ู€ auto_shot ู‚ุฏ ู„ู… ูŠูุฑุณู„ ุจุนุฏ ุจุณุจุจ ุชุฒุงุญู… ุงู„ู€ tasks.
18
- ุงู„ุฅุตู„ุงุญ: ุงู„ุฃูˆุงู…ุฑ ุงู„ุตุฑูŠุญุฉ (screenshot action) ุชูุฌูŠุจ ููˆุฑุงู‹ ุจู€ awaitุŒ ู„ุง create_task.
19
- ูู‚ุท auto_shot ุงู„ุฎู„ููŠ (ุจุนุฏ mouse_move ู…ุซู„ุงู‹) ูŠุจู‚ู‰ ูƒู€ create_task.
20
-
21
- ๐Ÿ› BUG 3 โ€” Rate Limit 0.3s ูŠู…ู†ุน ุงู„ุฑุฏ ุงู„ููˆุฑูŠ:
22
- ุงู„ู€ HTML ูŠุทู„ุจ screenshot ุซู… ูŠู†ุชุธุฑ โ€” ู„ูƒู† ุงู„ุณูŠุฑูุฑ ูƒุงู† ูŠู†ุชุธุฑ 0.3 ุซุงู†ูŠุฉ ุฃูˆู„ุงู‹
23
- ุจุณุจุจ rate limiting. ู…ุน ุชูƒุฏุณ ุงู„ุทู„ุจุงุชุŒ ุงู„ุชุฃุฎุฑ ูŠุชุฑุงูƒู… โ†’ timeout โ†’ "ู„ู… ุชุตู„".
24
- ุงู„ุฅุตู„ุงุญ: Rate limit ุชู†ุทุจู‚ ุนู„ู‰ auto_shot ุงู„ุฎู„ููŠ ูู‚ุทุŒ ู„ุง ุนู„ู‰ ุงู„ุทู„ุจุงุช ุงู„ุตุฑูŠุญุฉ.
25
-
26
- โœ… ุฅุตู„ุงุญุงุช ุฅุถุงููŠุฉ ู…ู† v9 (ุชุจู‚ู‰):
27
- - ุญุฐู --profile (ูƒุงู† ูŠุณุจุจ "Firefox profile cannot be loaded")
28
- - Xvfb ูˆุงุญุฏ ุซุงุจุช ู…ุดุชุฑูƒ
29
- - screenshot retry x3 ุฅุฐุง ุงู„ุตูˆุฑุฉ ุณูˆุฏุงุก
30
- - Heartbeat ping/pong ูƒู„ 20 ุซุงู†ูŠุฉ
31
- - terminal semaphore(4)
32
- - SafeSearch ุนู„ู‰ ูƒู„ URL
33
- - ุชู†ุธูŠู /tmp ูƒู„ 5 ุฏู‚ุงุฆู‚
34
  """
35
 
36
  import asyncio
@@ -43,176 +26,285 @@ import re
43
  import subprocess
44
  import time
45
  import urllib.parse
 
46
 
47
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
48
  from fastapi.middleware.cors import CORSMiddleware
49
  from fastapi.responses import JSONResponse, HTMLResponse
50
  import uvicorn
51
 
52
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
53
- # โ”€โ”€ ุงู„ุฅุนุฏุงุฏ ุงู„ุฃุณุงุณูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
54
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
55
 
56
- # DISPLAY ูˆุงุญุฏ ุซุงุจุช โ€” ูŠูุดุบูŽู‘ู„ ู…ุฑุฉ ูˆุงุญุฏุฉ ูู‚ุท
57
- SHARED_DISPLAY = os.environ.get("DISPLAY", ":99")
58
- os.environ["DISPLAY"] = SHARED_DISPLAY
59
 
60
- # semaphore ู„ู„ุชุญูƒู… ููŠ ุนุฏุฏ ุนู…ู„ูŠุงุช terminal ุงู„ู…ุชูˆุงุฒูŠุฉ
61
- _terminal_sem = asyncio.Semaphore(4)
62
 
63
- # โ”€โ”€ ุชุฎุฒูŠู† ุงู„ุฌู„ุณุงุช: session_id โ†’ {last_bg_shot_ts, last_bg_hash, browser_proc} โ”€โ”€
64
- _active_sessions: dict[int, dict] = {}
65
- _sessions_lock = asyncio.Lock()
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
68
- # โ”€โ”€ ุฅุนุฏุงุฏ Xvfb ุงู„ู…ุดุชุฑูƒ ุงู„ุซุงุจุช โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
69
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
70
 
71
- _xvfb_proc: "subprocess.Popen | None" = None
 
 
 
 
 
72
 
73
- def _ensure_xvfb() -> bool:
74
- """
75
- ูŠุชุฃูƒุฏ ุฃู† Xvfb ูŠุนู…ู„ ุนู„ู‰ SHARED_DISPLAY.
76
- ุฅุฐุง ูƒุงู† ูŠุนู…ู„ ุจุงู„ูุนู„ (ู…ู† Docker ุฃูˆ systemd): ู„ุง ูŠูุนู„ ุดูŠุฆุงู‹.
77
- ุฅุฐุง ู„ู… ูŠูƒู† ูŠุนู…ู„: ูŠูุดุบู‘ู„ู‡ ูˆูŠูุนูŠุฏ True ุนู†ุฏ ุงู„ู†ุฌุงุญ.
78
- """
79
- global _xvfb_proc
80
 
81
- # ุชุญู‚ู‚ ุฃูˆู„ุงู‹: ู‡ู„ DISPLAY ู…ูˆุฌูˆุฏ ุจุงู„ูุนู„ุŸ
 
 
 
 
 
 
 
 
 
 
 
 
82
  try:
83
  r = subprocess.run(
84
- ["xdpyinfo", "-display", SHARED_DISPLAY],
85
  capture_output=True, timeout=3
86
  )
87
  if r.returncode == 0:
88
- print(f"[xvfb] โœ… Display {SHARED_DISPLAY} already active (external)")
89
- return True
90
- except FileNotFoundError:
91
- pass # xdpyinfo ุบูŠุฑ ู…ุชุงุญ โ€” ุณู†ูƒู…ู„ ุจุฏูˆู† ุชุญู‚ู‚
92
  except Exception:
93
  pass
94
 
95
- # ุดุบู‘ู„ Xvfb
96
  try:
97
- _xvfb_proc = subprocess.Popen(
98
- ["Xvfb", SHARED_DISPLAY, "-screen", "0", "1920x1080x24",
99
  "-nolisten", "tcp", "-ac"],
100
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
101
  )
102
- # ุงู†ุชุธุฑ ุญุชู‰ ูŠุตุจุญ ุฌุงู‡ุฒุงู‹ (max 5 ุซูˆุงู†ูŠ)
103
- for _ in range(50):
104
  time.sleep(0.1)
105
  try:
106
  r = subprocess.run(
107
- ["xdpyinfo", "-display", SHARED_DISPLAY],
108
  capture_output=True, timeout=2
109
  )
110
  if r.returncode == 0:
111
- print(f"[xvfb] โœ… Xvfb started on {SHARED_DISPLAY}")
112
- return True
113
  except Exception:
114
  continue
115
- print(f"[xvfb] โš ๏ธ Xvfb may not be ready yet โ€” continuing anyway")
116
- return True
117
  except FileNotFoundError:
118
- print(f"[xvfb] โŒ Xvfb binary not found โ€” screenshots will fail")
119
- return False
120
  except Exception as e:
121
- print(f"[xvfb] โŒ Failed to start: {e}")
122
- return False
123
 
124
- # ุงูƒุชุดุงู ุงู„ู…ุชุตูุญ
125
- def _detect_browser() -> str:
126
- for c in ["firefox", "firefox-esr", "chromium-browser", "chromium", "google-chrome"]:
127
- r = subprocess.run(["which", c], capture_output=True, text=True)
128
- if r.returncode == 0 and r.stdout.strip():
129
- return c
130
- return "firefox"
131
 
132
- BROWSER = _detect_browser()
133
- print(f"๐ŸŒ Browser: {BROWSER}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
136
- # โ”€โ”€ ุฅุฏุงุฑุฉ ุงู„ุฌู„ุณุงุช (Profile-based isolation) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
138
 
139
- async def create_session(ws: WebSocket) -> dict:
140
  """
141
- ูŠู†ุดุฆ session ุจุณูŠุทุฉ ู„ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู….
142
- ู„ุง profile ู…ู†ูุตู„ โ€” Firefox ูŠุนู…ู„ ุจู€ --no-remote ูู‚ุท ู„ู„ุนุฒู„.
143
  """
144
- sid = id(ws)
145
- sess = {
146
- "id": sid,
147
- "last_bg_shot_ts": 0.0,
148
- "last_bg_hash": "",
149
- "browser_proc": None,
150
- "created": time.time(),
151
- }
152
- async with _sessions_lock:
153
- _active_sessions[sid] = sess
154
- print(f"[session] โœ… Created session {sid}")
155
- return sess
156
-
157
-
158
- async def destroy_session(ws: WebSocket):
159
- """ูŠุญุฑุฑ ู…ูˆุงุฑุฏ ุงู„ุฌู„ุณุฉ ุนู†ุฏ ุงู†ู‚ุทุงุน ุงู„ุงุชุตุงู„."""
160
- sid = id(ws)
161
- async with _sessions_lock:
162
- sess = _active_sessions.pop(sid, None)
163
- if not sess:
164
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- def _cleanup():
167
- bp = sess.get("browser_proc")
168
- if bp:
169
- try:
170
- bp.terminate()
171
- bp.wait(timeout=3)
172
- except Exception:
173
- try:
174
- bp.kill()
175
- except Exception:
176
- pass
177
 
178
- await asyncio.to_thread(_cleanup)
179
- print(f"[session] ๐Ÿ—‘๏ธ Destroyed session {sid}")
 
180
 
 
 
 
181
 
182
- def get_browser_cmd(url: str = "") -> list:
183
- """ูŠุจู†ูŠ ุฃู…ุฑ ูุชุญ ุงู„ู…ุชุตูุญ ุจุฏูˆู† profile โ€” --no-remote ูŠู…ู†ุน ุงู„ุชุฏุงุฎู„."""
184
- cmd = [BROWSER, "--no-remote"]
185
- cmd.append(url if url else "about:blank")
186
- return cmd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
 
189
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
190
- # โ”€โ”€ Screenshot Engine (Multi-method + Black Screen Check) โ”€โ”€โ”€โ”€
191
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
192
 
193
  def _is_black_screen(img) -> bool:
194
- """
195
- ูŠูƒุชุดู ู…ุง ุฅุฐุง ูƒุงู†ุช ุงู„ุตูˆุฑุฉ ุณูˆุฏุงุก ุชู‚ุฑูŠุจุงู‹ (Xvfb ูŠุนู…ู„ ู„ูƒู† ู„ุง ุดูŠุก ููŠู‡).
196
- ูŠุฃุฎุฐ ุนูŠู†ุฉ ู…ู† 100 pixel ูˆูŠุญุณุจ ู…ุชูˆุณุท ุงู„ู€ brightness.
197
- """
198
  try:
199
  small = img.resize((100, 100))
200
  pixels = list(small.getdata())
201
  avg = sum(sum(p[:3]) for p in pixels) / (len(pixels) * 3 * 255)
202
- return avg < 0.04 # ุฃู‚ู„ ู…ู† 4% brightness = ุฃุณูˆุฏ ูุนู„ูŠุงู‹
203
  except Exception:
204
  return False
205
 
206
 
207
- def _capture_raw() -> tuple:
208
- """
209
- ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ุจู€ 4 ุทุฑู‚ ู…ุชุณู„ุณู„ุฉ ู…ุน retry ุฅุฐุง ุงู„ุตูˆุฑุฉ ุณูˆุฏุงุก.
210
- ูŠูุนูŠุฏ (PIL.Image | None, width, height).
211
- """
212
  from PIL import Image
213
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
214
  ts = int(time.time() * 1000)
215
- tmp_base = f"/tmp/zss_{ts}"
 
 
216
  created_files = []
217
 
218
  def _try_load(path) -> tuple:
@@ -227,7 +319,7 @@ def _capture_raw() -> tuple:
227
  return None, 0, 0
228
  return img, w, h
229
  except Exception as ex:
230
- print(f"[cap] load error: {ex}")
231
  return None, 0, 0
232
 
233
  def _cleanup_files():
@@ -238,66 +330,56 @@ def _capture_raw() -> tuple:
238
  except Exception:
239
  pass
240
 
241
- # ู†ุญุงูˆู„ 3 ู…ุฑุงุช ุฅุฐุง ุงู„ุตูˆุฑุฉ ุณูˆุฏุงุก (ู…ุซู„ุงู‹: Firefox ู„ุง ูŠุฒุงู„ ูŠุชุญู…ู‘ู„)
242
  for attempt in range(3):
243
  if attempt > 0:
244
  time.sleep(0.8)
245
- print(f"[cap] retry attempt {attempt+1} (black screen)")
246
  try:
247
- # โ”€โ”€ Method 1: scrot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
248
  p1 = f"{tmp_base}_a{attempt}_scrot.png"
249
  created_files.append(p1)
250
  try:
251
- r = subprocess.run(
252
- ["scrot", "-q", "95", p1],
253
- env=env, timeout=8, capture_output=True
254
- )
255
  img, w, h = _try_load(p1)
256
  if img and not _is_black_screen(img):
257
- print(f"[cap] โœ… scrot (attempt {attempt+1})")
258
  return img, w, h
259
- if img:
260
- print(f"[cap] scrot: black screen (attempt {attempt+1})")
261
  except Exception as e:
262
- print(f"[cap] scrot: {e}")
263
 
264
- # โ”€โ”€ Method 2: ImageMagick import โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
265
  p2 = f"{tmp_base}_a{attempt}_im.png"
266
  created_files.append(p2)
267
  try:
268
- subprocess.run(
269
- ["import", "-window", "root", "-silent", p2],
270
- env=env, timeout=10, capture_output=True
271
- )
272
  img, w, h = _try_load(p2)
273
  if img and not _is_black_screen(img):
274
- print(f"[cap] โœ… import (attempt {attempt+1})")
275
  return img, w, h
276
  except Exception as e:
277
- print(f"[cap] import: {e}")
278
 
279
- # โ”€โ”€ Method 3: ffmpeg x11grab โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
280
  p3 = f"{tmp_base}_a{attempt}_ff.png"
281
  created_files.append(p3)
282
  try:
283
- sw, sh = _get_screen_size()
284
  subprocess.run([
285
  "ffmpeg", "-y", "-f", "x11grab",
286
  "-video_size", f"{sw}x{sh}",
287
- "-i", SHARED_DISPLAY,
288
  "-vframes", "1", "-q:v", "1", p3
289
  ], env=env, timeout=12, capture_output=True)
290
  img, w, h = _try_load(p3)
291
  if img and not _is_black_screen(img):
292
- print(f"[cap] โœ… ffmpeg (attempt {attempt+1})")
293
  return img, w, h
294
  except Exception as e:
295
- print(f"[cap] ffmpeg: {e}")
296
 
297
- # โ”€โ”€ Method 4: python-xlib โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
298
  try:
299
  from Xlib import display as Xdisp, X
300
- xd = Xdisp.Display(SHARED_DISPLAY)
301
  root = xd.screen().root
302
  geom = root.get_geometry()
303
  w, h = geom.width, geom.height
@@ -310,31 +392,30 @@ def _capture_raw() -> tuple:
310
  pixels[j], pixels[j+1], pixels[j+2] = r_c, g, b
311
  img = Image.frombytes("RGB", (w, h), bytes(pixels))
312
  if not _is_black_screen(img):
313
- print(f"[cap] โœ… xlib (attempt {attempt+1})")
314
  return img, w, h
315
  except Exception as e:
316
- print(f"[cap] xlib: {e}")
317
 
318
  finally:
319
  _cleanup_files()
320
 
321
- # ุฅุฐุง ูุดู„ุช ูƒู„ ุงู„ู…ุญุงูˆู„ุงุช โ€” placeholder ูˆุงุถุญ
322
- print("[cap] โš ๏ธ All methods failed or black โ€” placeholder")
323
  from PIL import Image as PILImg, ImageDraw
324
- sw, sh = _get_screen_size()
325
  img = PILImg.new("RGB", (sw or 1920, sh or 1080), (15, 20, 40))
326
  draw = ImageDraw.Draw(img)
327
  draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
328
- draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={SHARED_DISPLAY}", fill=(255, 120, 80))
329
- draw.text((10, 32), "Methods tried: scrot, import, ffmpeg, xlib (3 attempts each)", fill=(130, 130, 150))
330
  return img, sw or 1920, sh or 1080
331
 
332
 
333
- def _get_screen_size() -> tuple:
334
  try:
335
  r = subprocess.run(
336
  ["xdotool", "getdisplaygeometry"],
337
- env={**os.environ, "DISPLAY": SHARED_DISPLAY},
338
  capture_output=True, text=True, timeout=5
339
  )
340
  parts = r.stdout.strip().split()
@@ -343,11 +424,11 @@ def _get_screen_size() -> tuple:
343
  return 1920, 1080
344
 
345
 
346
- def _get_mouse_pos() -> tuple:
347
  try:
348
  r = subprocess.run(
349
  ["xdotool", "getmouselocation"],
350
- env={**os.environ, "DISPLAY": SHARED_DISPLAY},
351
  capture_output=True, text=True, timeout=5
352
  )
353
  mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
@@ -357,27 +438,23 @@ def _get_mouse_pos() -> tuple:
357
  return 0, 0
358
 
359
 
360
- def capture_with_grid(scale: float = 0.65, quality: int = 72,
361
  force_mx: int | None = None,
362
  force_my: int | None = None) -> dict:
363
- """
364
- ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ูˆูŠุถูŠู Grid overlay ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ.
365
- ูŠูุนูŠุฏ dict ู…ุน base64 JPEG ูˆุฅุญุฏุงุซูŠุงุช ุงู„ุดุงุดุฉ ูˆุงู„ู…ุงูˆุณ.
366
- """
367
  from PIL import Image, ImageDraw
368
 
369
- img, ow, oh = _capture_raw()
370
  if img is None:
371
  return {"data": "", "width": 1920, "height": 1080, "mouse_x": 0, "mouse_y": 0}
372
 
373
- mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos()
374
 
375
  sw = int(ow * scale)
376
  sh = int(oh * scale)
377
  grid_img = img.resize((sw, sh), Image.LANCZOS)
378
  draw = ImageDraw.Draw(grid_img, "RGBA")
379
 
380
- # ุฎุทูˆุท Grid ูƒู„ 100px
381
  step_x = int(100 * sw / ow)
382
  step_y = int(100 * sh / oh)
383
  gc = (255, 255, 255, 40)
@@ -397,7 +474,6 @@ def capture_with_grid(scale: float = 0.65, quality: int = 72,
397
  draw.text((3, y_sc+2), str(y_r), fill=lc)
398
  y_sc += step_y; y_r += 100
399
 
400
- # cursor
401
  msx = int(mx * sw / ow)
402
  msy = int(my * sh / oh)
403
  r = 11
@@ -405,11 +481,10 @@ def capture_with_grid(scale: float = 0.65, quality: int = 72,
405
  draw.line([(msx-18, msy), (msx+18, msy)], fill=(255, 50, 50, 200), width=1)
406
  draw.line([(msx, msy-18), (msx, msy+18)], fill=(255, 50, 50, 200), width=1)
407
 
408
- # header bar
409
  final = grid_img.convert("RGB")
410
  draw2 = ImageDraw.Draw(final)
411
  draw2.rectangle([(0, 0), (sw, 20)], fill=(0, 0, 0))
412
- draw2.text((4, 3), f"SCREEN {ow}x{oh} | MOUSE:({mx},{my}) | GRID=100px", fill=(0, 220, 160))
413
  draw2.rectangle([(0, sh-20), (sw, sh)], fill=(0, 0, 0))
414
  draw2.text((4, sh-17), "CLICK COORDS = numbers on grid lines (real screen pixels)", fill=(180, 180, 70))
415
 
@@ -421,12 +496,11 @@ def capture_with_grid(scale: float = 0.65, quality: int = 72,
421
 
422
 
423
  def _frame_hash(data: str) -> str:
424
- """hash ุณุฑูŠุน ู„ู„ู€ base64 ู„ู„ูƒุดู ุนู† ุชุบูŠูŠุฑ ุงู„ุตูˆุฑุฉ."""
425
  return hashlib.md5(data[:2000].encode()).hexdigest()
426
 
427
 
428
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
429
- # โ”€โ”€ SafeSearch Enforcement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
430
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
431
 
432
  _BING_RE = re.compile(r"https?://(?:www\.)?bing\.com[^\s'\"]*")
@@ -447,7 +521,7 @@ def _safe_search(text: str) -> str:
447
 
448
 
449
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
450
- # โ”€โ”€ Terminal Execution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
451
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
452
 
453
  _PKILL_RE = re.compile(r"\b(pkill|killall)\s+(-9\s+)?-f\s+(['\"]?)([a-zA-Z0-9_./-]+)\3")
@@ -460,11 +534,11 @@ def _sanitize_pkill(cmd: str) -> str:
460
  return _PKILL_RE.sub(_fix, cmd)
461
 
462
 
463
- async def run_cmd(cmd: str, timeout: int = 60) -> dict:
464
- """ูŠู†ูู‘ุฐ ุฃู…ุฑ bash ู…ุน rate limit ูˆsafe search."""
465
  cmd = _sanitize_pkill(_safe_search(cmd))
466
  async with _terminal_sem:
467
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY,
468
  "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
469
 
470
  def _exec():
@@ -483,11 +557,11 @@ async def run_cmd(cmd: str, timeout: int = 60) -> dict:
483
 
484
 
485
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
486
- # โ”€โ”€ xdotool helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
487
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
488
 
489
- async def xdo(args: list, timeout: int = 10) -> dict:
490
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
491
  def _run():
492
  r = subprocess.run(
493
  ["xdotool"] + args, env=env,
@@ -497,10 +571,10 @@ async def xdo(args: list, timeout: int = 10) -> dict:
497
  return await asyncio.to_thread(_run)
498
 
499
 
500
- async def type_smart(text: str) -> dict:
501
- """ูƒุชุงุจุฉ ู†ุต ุฐูƒูŠุฉ: clipboard ู„ู„ุนุฑุจูŠุฉุŒ xdotool type ู„ู„ุฅู†ุฌู„ูŠุฒูŠุฉ."""
502
  has_arabic = bool(re.search(r'[\u0600-\u06FF]', text))
503
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
504
 
505
  if has_arabic:
506
  def _paste():
@@ -511,76 +585,52 @@ async def type_smart(text: str) -> dict:
511
  p.communicate(text.encode("utf-8"))
512
  await asyncio.to_thread(_paste)
513
  await asyncio.sleep(0.15)
514
- await xdo(["key", "--clearmodifiers", "ctrl+v"])
515
  return {"method": "clipboard+paste"}
516
  else:
517
- r = await xdo(["type", "--clearmodifiers", "--delay", "25", text])
518
  return {"method": "xdotool", "rc": r["rc"]}
519
 
520
 
521
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
522
- # โ”€โ”€ Search Sources (8 sources with smart fallback) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
523
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
524
 
525
  def _search_sources(query: str) -> list:
526
  q = urllib.parse.quote_plus(query)
527
  return [
528
- {
529
- "name": "DuckDuckGo Instant",
530
- "cmd": f"curl -s --max-time 15 'https://api.duckduckgo.com/?q={q}&format=json&no_html=1&skip_disambig=1' | python3 -c \"import sys,json;d=json.load(sys.stdin);a=d.get('AbstractText','');r=d.get('RelatedTopics',[]);print('ANS:',a or 'none');[print('-',x.get('Text','')[:200]) for x in r[:6] if isinstance(x,dict)]\""
531
- },
532
- {
533
- "name": "Google News RSS",
534
- "cmd": f"curl -sL --max-time 15 'https://news.google.com/rss/search?q={q}&hl=ar&gl=AR&ceid=AR:ar' | python3 -c \"import sys,re;x=sys.stdin.read();t=re.findall(r'<title><!\\[CDATA\\[(.*?)\\]\\]></title>|<title>(.*?)</title>',x);[(print(str(i+1)+'. '+(a or b).strip()[:160])) for i,(a,b) in enumerate(t[1:8]) if (a or b).strip()]\""
535
- },
536
- {
537
- "name": "Wikipedia EN",
538
- "cmd": f"curl -s --max-time 12 'https://en.wikipedia.org/api/rest_v1/page/summary/{q}' | python3 -c \"import sys,json;d=json.load(sys.stdin);print(d.get('title','')+'\\n'+d.get('extract','')[:1200])\""
539
- },
540
- {
541
- "name": "DuckDuckGo HTML",
542
- "cmd": f"curl -sL --max-time 15 -H 'User-Agent: Mozilla/5.0' 'https://html.duckduckgo.com/html/?q={q}' | python3 -c \"import sys,re;h=sys.stdin.read();s=re.findall(r'class=.result__snippet[^>]*>(.*?)</a>',h,re.DOTALL);clean=lambda x:re.sub('<[^>]+>','',x).strip();[print(str(i+1)+'. '+clean(x)[:200]) for i,x in enumerate(s[:7])]\""
543
- },
544
- {
545
- "name": "HackerNews",
546
- "cmd": f"curl -s --max-time 12 'https://hn.algolia.com/api/v1/search?query={q}&hitsPerPage=6&tags=story' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+h.get('title','')+' Pts:'+str(h.get('points',0))) for i,h in enumerate(d.get('hits',[])[:5])]\""
547
- },
548
- {
549
- "name": "Bing News RSS",
550
- "cmd": f"curl -sL --max-time 15 'https://www.bing.com/news/search?q={q}&format=RSS' | python3 -c \"import sys,re;x=sys.stdin.read();items=re.findall(r'<item>(.*?)</item>',x,re.DOTALL);[print(str(i+1)+'. '+re.sub('<[^>]+>','',re.search(r'<title>(.*?)</title>',it).group(1) if re.search(r'<title>',it) else '')) for i,it in enumerate(items[:6])]\""
551
- },
552
- {
553
- "name": "ArXiv",
554
- "cmd": f"curl -s --max-time 12 'https://export.arxiv.org/api/query?search_query=all:{q}&max_results=5' | python3 -c \"import sys,re;x=sys.stdin.read();t=re.findall(r'<title>(.*?)</title>',x)[1:];[print(str(i+1)+'. '+ti.strip()) for i,ti in enumerate(t[:5])]\""
555
- },
556
- {
557
- "name": "Reddit",
558
- "cmd": f"curl -sL --max-time 12 -H 'Accept: application/json' 'https://www.reddit.com/search.json?q={q}&sort=new&limit=6' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+p['data'].get('title','')[:160]) for i,p in enumerate(d.get('data',{{}}).get('children',[])[:5])]\""
559
- },
560
  ]
561
 
562
 
563
  def _result_empty(s: str) -> bool:
564
  if not s or len(s.strip()) < 15:
565
  return True
566
- bad = ["no direct answer", "ans: none", "error", "not found", "[]", "empty"]
567
- return all(b in s.lower() for b in bad[:1])
568
 
569
 
570
- async def run_cmd_smart(cmd: str, timeout: int = 60) -> dict:
571
- """ูŠูู†ูู‘ุฐ ุฃู…ุฑ ูˆุฅุฐุง ูุดู„ ูŠุฌุฑุจ 8 ู…ุตุงุฏุฑ ุจุญุซ ุจุฏูŠู„ุฉ."""
572
- res = await run_cmd(cmd, timeout)
573
  if res["returncode"] == 0 and not _result_empty(res["stdout"]):
574
  return res
575
 
576
- # ู‡ู„ ู‡ูˆ ุฃู…ุฑ ุจุญุซุŸ
577
  is_search = "curl" in cmd and any(
578
  x in cmd for x in ["search", "duckduck", "bing", "google", "wikipedia", "reddit"]
579
  )
580
  if not is_search:
581
  return res
582
 
583
- # ุงุณุชุฎุฑุฌ query
584
  m = re.search(r"[?&]q=([^&'\"\s]+)", cmd)
585
  query = urllib.parse.unquote(m.group(1).replace("+", " ")).strip() if m else ""
586
  if len(query) < 3:
@@ -589,7 +639,7 @@ async def run_cmd_smart(cmd: str, timeout: int = 60) -> dict:
589
  sources = _search_sources(query)
590
  results = []
591
  for src in sources:
592
- r2 = await run_cmd(src["cmd"], 20)
593
  if not _result_empty(r2["stdout"]):
594
  results.append(f"[{src['name']}]\n{r2['stdout']}")
595
  if len(results) >= 2:
@@ -604,42 +654,50 @@ async def run_cmd_smart(cmd: str, timeout: int = 60) -> dict:
604
  # โ”€โ”€ FastAPI App โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
605
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
606
 
607
- app = FastAPI(title="Z-Computer-Mode v8")
608
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
609
  allow_methods=["*"], allow_headers=["*"])
610
 
611
- active_connections: list[WebSocket] = []
612
-
613
 
614
  @app.get("/", response_class=HTMLResponse)
615
  async def root():
616
- w, h = _get_screen_size()
617
- n = len(_active_sessions)
 
 
 
 
 
618
  return f"""<!DOCTYPE html><html><head><meta charset="utf-8">
619
- <title>Z Computer Mode v8</title>
620
  <style>body{{background:#080808;color:#00d4aa;font-family:monospace;padding:40px}}
621
- h1{{color:#4a90d9}}.ok{{color:#00ff88}}.info{{color:#888;font-size:13px}}</style>
 
 
622
  </head><body>
623
- <h1>๐Ÿ–ฅ๏ธ THE Z AI โ€” Computer Mode Server v8</h1>
624
- <p class="ok">โœ… Server RUNNING</p>
625
  <p>๐ŸŒ Browser: <b>{BROWSER}</b></p>
626
- <p>๐Ÿ“ Screen: <b>{w}x{h}</b></p>
627
- <p>๐Ÿ‘ฅ Active sessions: <b>{n}</b></p>
628
- <p>๐Ÿ–ฅ๏ธ Display: <b>{SHARED_DISPLAY}</b></p>
629
- <p>๐Ÿ”Œ WebSocket: <b>wss://your-space.hf.space/ws</b></p>
 
 
 
630
  <hr style="border-color:#333">
631
- <p class="info">Endpoints: /health ยท /screenshot ยท /ws (WebSocket)</p>
632
  </body></html>"""
633
 
634
 
635
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
636
- # โ”€โ”€ Action Handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
637
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
638
 
639
  async def handle_action(ws: WebSocket, msg: dict, sess: dict):
640
- action = msg.get("action", "")
641
- data = msg.get("data", {})
642
- sid = id(ws)
643
 
644
  async def send(obj):
645
  try:
@@ -649,38 +707,29 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
649
 
650
  async def shot_bg(label: str = "", delay: float = 0.5,
651
  force_mx: int | None = None, force_my: int | None = None):
652
- """
653
- auto_shot ููŠ ุงู„ุฎู„ููŠุฉ โ€” ูู‚ุท ู„ู„ู…ุนุงูŠู†ุฉ ุงู„ุจุตุฑูŠุฉ (ุจุนุฏ mouse/keyboard/scroll).
654
- ูŠุทุจู‘ู‚: rate limit + delta suppression.
655
- ู„ุง ูŠูุฑุณู„ ุฅุฐุง ุงู„ุตูˆุฑุฉ ู„ู… ุชุชุบูŠุฑ โ€” ุงู„ู…ุณุชุฎุฏู… ูŠุฑู‰ ุงู„ุชุญุฏูŠุซ ุนู†ุฏ ุงู„ุทู„ุจ ุงู„ุตุฑูŠุญ.
656
- """
657
  await asyncio.sleep(delay)
658
-
659
- # Rate limit: ู„ุง ุฃูƒุซุฑ ู…ู† ู„ู‚ุทุฉ ูƒู„ 0.3 ุซุงู†ูŠุฉ ู„ู†ูุณ ุงู„ุฌู„ุณุฉ
660
  now = time.time()
661
  if now - sess.get("last_bg_shot_ts", 0) < 0.3:
662
  await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"]))
663
 
664
  result = await asyncio.to_thread(
665
- capture_with_grid, 0.65, 72, force_mx, force_my
666
  )
667
  sess["last_bg_shot_ts"] = time.time()
668
-
669
  if not result["data"]:
670
  return
671
 
672
- # Delta suppression: ู„ุง ุชุฑุณู„ ุฅุฐุง ุงู„ุตูˆุฑุฉ ู„ู… ุชุชุบูŠุฑ
673
  fh = _frame_hash(result["data"])
674
  if fh == sess.get("last_bg_hash", ""):
675
- return # ู„ู… ุชุชุบูŠุฑ โ€” ู„ุง ุชุฑุณู„
676
  sess["last_bg_hash"] = fh
677
 
678
  await send({
679
  "type": "screenshot",
680
  "data": result["data"],
681
  "ts": int(time.time() * 1000),
682
- "auto": True,
683
- "label": label,
684
  "screen_width": result["width"],
685
  "screen_height": result["height"],
686
  "mouse_x": result["mouse_x"],
@@ -689,22 +738,16 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
689
  })
690
 
691
  async def shot_explicit(label: str = ""):
692
- """
693
- screenshot ุตุฑูŠุญ โ€” ูŠูุฑุณู„ ุฏุงุฆู…ุงู‹ ุจุฏูˆู† delta suppression ูˆู„ุง rate limit.
694
- ูŠูุณุชุฎุฏู… ุนู†ุฏ action == "screenshot" ุฃูˆ ุจุนุฏ terminal/open_app.
695
- ุงู„ู€ HTML ูŠู†ุชุธุฑ ู‡ุฐุง ุงู„ุฑุฏ ุจู…ู‚ุงุฑู†ุฉ _pcLastFrameTs โ€” ูŠุฌุจ ุฃู† ูŠุตู„ ุฏุงุฆู…ุงู‹.
696
- """
697
- result = await asyncio.to_thread(capture_with_grid, 0.65, 75)
698
  if not result["data"]:
699
  return
700
- # ุญุฏู‘ุซ hash ุงู„ุฎู„ููŠ ุฃูŠุถุงู‹ ู„ู…ู†ุน ุฅุฑุณุงู„ ู†ูุณ ุงู„ุตูˆุฑุฉ ู…ุฑุชูŠู† ููŠ auto_shot
701
  sess["last_bg_hash"] = _frame_hash(result["data"])
702
  await send({
703
  "type": "screenshot",
704
  "data": result["data"],
705
  "ts": int(time.time() * 1000),
706
- "auto": False,
707
- "label": label,
708
  "screen_width": result["width"],
709
  "screen_height": result["height"],
710
  "mouse_x": result["mouse_x"],
@@ -712,7 +755,18 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
712
  "has_grid": True,
713
  })
714
 
715
- # โ”€โ”€ screenshot (ุตุฑูŠุญ ู…ู† ุงู„ู€ HTML โ€” ูŠูุฑุณู„ ุฏุงุฆู…ุงู‹ ุจุฏูˆู† ุชุฃุฎูŠุฑ) โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
716
  if action == "screenshot":
717
  await shot_explicit("explicit screenshot")
718
 
@@ -722,7 +776,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
722
  if not cmd:
723
  await send({"type": "terminal_result", "stdout": "", "stderr": "no cmd", "returncode": -1})
724
  return
725
- res = await run_cmd_smart(cmd, int(data.get("timeout", 60)))
726
  await send({
727
  "type": "terminal_result",
728
  "cmd": cmd,
@@ -730,13 +784,12 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
730
  "stderr": res.get("stderr", ""),
731
  "returncode": res["returncode"],
732
  })
733
- # screenshot ุตุฑูŠุญ ุจุนุฏ terminal โ€” ุงู„ู€ HTML ูŠู†ุชุธุฑู‡
734
  asyncio.create_task(shot_explicit(f"after terminal"))
735
 
736
  # โ”€โ”€ mouse_move โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
737
  elif action == "mouse_move":
738
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
739
- await xdo(["mousemove", "--sync", str(x), str(y)])
740
  await send({"type": "ack", "action": "mouse_move", "x": x, "y": y})
741
  asyncio.create_task(shot_bg(f"move ({x},{y})", delay=0.2, force_mx=x, force_my=y))
742
 
@@ -745,12 +798,12 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
745
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
746
  btn = {"left": "1", "middle": "2", "right": "3"}.get(data.get("button", "left"), "1")
747
  double = data.get("double", False)
748
- await xdo(["mousemove", "--sync", str(x), str(y)])
749
  await asyncio.sleep(0.07)
750
  if double:
751
- await xdo(["click", "--repeat", "2", "--delay", "100", btn])
752
  else:
753
- await xdo(["click", btn])
754
  btn_name = {"1": "left", "2": "middle", "3": "right"}.get(btn, "left")
755
  await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn_name})
756
  asyncio.create_task(shot_bg(f"click {btn_name} ({x},{y})", delay=0.5, force_mx=x, force_my=y))
@@ -759,12 +812,12 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
759
  elif action == "mouse_drag":
760
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
761
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
762
- await xdo(["mousemove", str(x1), str(y1)])
763
- await xdo(["mousedown", "1"])
764
  await asyncio.sleep(0.1)
765
- await xdo(["mousemove", str(x2), str(y2)])
766
  await asyncio.sleep(0.1)
767
- await xdo(["mouseup", "1"])
768
  await send({"type": "ack", "action": "mouse_drag"})
769
  asyncio.create_task(shot_bg("after drag", delay=0.4, force_mx=x2, force_my=y2))
770
 
@@ -772,7 +825,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
772
  elif action == "keyboard_type":
773
  text = _safe_search(data.get("text", ""))
774
  if text:
775
- res = await type_smart(text)
776
  await send({"type": "ack", "action": "keyboard_type", "method": res["method"]})
777
  asyncio.create_task(shot_bg("after type", delay=0.5))
778
 
@@ -780,7 +833,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
780
  elif action == "keyboard_hotkey":
781
  keys = data.get("keys", [])
782
  if keys:
783
- await xdo(["key", "--clearmodifiers", "+".join(keys)])
784
  await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys})
785
  asyncio.create_task(shot_bg("after hotkey", delay=0.4))
786
 
@@ -788,7 +841,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
788
  elif action == "keyboard_press":
789
  key = data.get("key", "")
790
  if key:
791
- await xdo(["key", "--clearmodifiers", key])
792
  await send({"type": "ack", "action": "keyboard_press"})
793
  asyncio.create_task(shot_bg("after key", delay=0.4))
794
 
@@ -797,9 +850,9 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
797
  x, y = int(data.get("x", 960)), int(data.get("y", 540))
798
  clicks = max(-5, min(5, int(data.get("clicks", 3))))
799
  btn = "4" if clicks > 0 else "5"
800
- await xdo(["mousemove", str(x), str(y)])
801
  for _ in range(abs(clicks)):
802
- await xdo(["click", btn])
803
  await asyncio.sleep(0.025)
804
  await send({"type": "ack", "action": "scroll", "clicks": clicks})
805
  asyncio.create_task(shot_bg("after scroll", delay=0.4))
@@ -807,7 +860,7 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
807
  # โ”€โ”€ clipboard_write โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
808
  elif action == "clipboard_write":
809
  text = data.get("text", "")
810
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
811
  def _clip():
812
  p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env)
813
  p.communicate(text.encode("utf-8"))
@@ -816,20 +869,20 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
816
 
817
  # โ”€โ”€ clipboard_read โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
818
  elif action == "clipboard_read":
819
- res = await run_cmd("xclip -selection clipboard -o", 5)
820
  await send({"type": "clipboard_content", "text": res["stdout"]})
821
 
822
  # โ”€โ”€ paste โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
823
  elif action == "paste":
824
  text = data.get("text", "")
825
  if text:
826
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
827
  def _clip2():
828
  p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env)
829
  p.communicate(text.encode("utf-8"))
830
  await asyncio.to_thread(_clip2)
831
  await asyncio.sleep(0.1)
832
- await xdo(["key", "--clearmodifiers", "ctrl+v"])
833
  await send({"type": "ack", "action": "paste"})
834
  asyncio.create_task(shot_bg("after paste", delay=0.5))
835
 
@@ -839,24 +892,20 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
839
  if not cmd:
840
  await send({"type": "ack", "action": "open_app"})
841
  return
842
-
843
- # ู†ุธู‘ู lock files ู‚ุจู„ ูุชุญ Firefox (ูŠู…ู†ุน "already running" dialog)
844
  if "firefox" in cmd.lower():
845
  await run_cmd(
846
  "pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
847
  "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
848
  "echo CLEANED",
849
- timeout=8
850
  )
851
-
852
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
853
- proc = subprocess.Popen(
854
- cmd, shell=True, env=env,
855
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
856
- )
857
  if any(b in cmd for b in ("firefox", "chromium", "chrome")):
858
- sess["browser_proc"] = proc
859
-
 
860
  await send({"type": "ack", "action": "open_app", "cmd": cmd})
861
  async def _open_app_shots():
862
  await asyncio.sleep(3.0)
@@ -868,17 +917,21 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
868
  # โ”€โ”€ open_browser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
869
  elif action == "open_browser":
870
  url = _safe_search(data.get("url", "") or "about:blank")
871
- # ู†ุธู‘ู ุฃูŠ Firefox ู‚ุฏูŠู… ุฃูˆู„ุงู‹
872
  await run_cmd(
873
  "pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
874
  "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
875
  "echo CLEANED",
876
- timeout=8
877
  )
878
- cmd = get_browser_cmd(url)
879
- env = {**os.environ, "DISPLAY": SHARED_DISPLAY}
880
- proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
881
- sess["browser_proc"] = proc
 
 
 
 
882
  await send({"type": "ack", "action": "open_browser", "url": url})
883
  async def _open_browser_shots():
884
  await asyncio.sleep(3.0)
@@ -890,55 +943,55 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
890
  # โ”€โ”€ open_tab โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
891
  elif action == "open_tab":
892
  url = _safe_search(data.get("url", "") or "about:blank")
893
- await xdo(["key", "--clearmodifiers", "ctrl+t"])
894
  await asyncio.sleep(0.4)
895
- await xdo(["key", "--clearmodifiers", "ctrl+l"])
896
  await asyncio.sleep(0.2)
897
- await type_smart(url)
898
  await asyncio.sleep(0.2)
899
- await xdo(["key", "--clearmodifiers", "Return"])
900
  await send({"type": "ack", "action": "open_tab", "url": url})
901
  asyncio.create_task(shot_bg("after open_tab", delay=1.5))
902
 
903
  # โ”€โ”€ close_tab โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
904
  elif action == "close_tab":
905
- await xdo(["key", "--clearmodifiers", "ctrl+w"])
906
  await send({"type": "ack", "action": "close_tab"})
907
  asyncio.create_task(shot_bg("after close_tab", delay=0.5))
908
 
909
  # โ”€โ”€ browser_back โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
910
  elif action == "browser_back":
911
- await xdo(["key", "--clearmodifiers", "alt+Left"])
912
  await send({"type": "ack", "action": "browser_back"})
913
  asyncio.create_task(shot_bg("after back", delay=0.8))
914
 
915
  # โ”€โ”€ browser_forward โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
916
  elif action == "browser_forward":
917
- await xdo(["key", "--clearmodifiers", "alt+Right"])
918
  await send({"type": "ack", "action": "browser_forward"})
919
  asyncio.create_task(shot_bg("after forward", delay=0.8))
920
 
921
  # โ”€โ”€ browser_search โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
922
  elif action == "browser_search":
923
  url = _safe_search(data.get("url", "") or data.get("query", ""))
924
- await xdo(["key", "--clearmodifiers", "ctrl+l"])
925
  await asyncio.sleep(0.2)
926
- await type_smart(url)
927
  await asyncio.sleep(0.15)
928
- await xdo(["key", "--clearmodifiers", "Return"])
929
  await send({"type": "ack", "action": "browser_search"})
930
  asyncio.create_task(shot_bg("after search", delay=1.5))
931
 
932
  # โ”€โ”€ screen_info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
933
  elif action == "screen_info":
934
- w, h = await asyncio.to_thread(_get_screen_size)
935
- mx, my = await asyncio.to_thread(_get_mouse_pos)
936
  await send({
937
  "type": "screen_info",
938
  "width": w, "height": h,
939
  "mouse_x": mx, "mouse_y": my,
940
  "browser": BROWSER,
941
- "display": SHARED_DISPLAY,
942
  })
943
 
944
  # โ”€โ”€ unknown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -947,18 +1000,25 @@ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
947
 
948
 
949
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
950
- # โ”€โ”€ WebSocket Endpoint with Heartbeat โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
951
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
952
 
953
  @app.websocket("/ws")
954
- async def websocket_endpoint(ws: WebSocket):
 
 
 
955
  await ws.accept()
956
- active_connections.append(ws)
957
 
958
- sess = await create_session(ws)
 
 
 
 
 
 
959
 
960
  async def _heartbeat():
961
- """Ping ูƒู„ 20 ุซุงู†ูŠุฉ ู„ู…ู†ุน ู‚ุทุน ุงู„ุงุชุตุงู„ ู…ู† proxies."""
962
  while True:
963
  await asyncio.sleep(20)
964
  try:
@@ -969,18 +1029,19 @@ async def websocket_endpoint(ws: WebSocket):
969
  hb_task = asyncio.create_task(_heartbeat())
970
 
971
  try:
972
- w, h = await asyncio.to_thread(_get_screen_size)
973
  await ws.send_text(json.dumps({
974
  "type": "connected",
975
  "screen_width": w, "screen_height": h,
976
  "browser": BROWSER,
977
- "display": SHARED_DISPLAY,
 
978
  "session_id": id(ws),
979
- "msg": f"Z Computer Mode v10 | Browser: {BROWSER} | Screen: {w}x{h}",
980
  }, ensure_ascii=False))
981
 
982
- # ู„ู‚ุทุฉ ุดุงุดุฉ ุฃูˆู„ูŠุฉ โ€” ุฏุงุฆู…ุงู‹ ุชูุฑุณู„ ุจุบุถ ุงู„ู†ุธุฑ ุนู† ุฃูŠ ุดูŠุก
983
- result = await asyncio.to_thread(capture_with_grid, 0.65, 72)
984
  if result["data"]:
985
  await ws.send_text(json.dumps({
986
  "type": "screenshot",
@@ -996,14 +1057,13 @@ async def websocket_endpoint(ws: WebSocket):
996
  sess["last_bg_hash"] = _frame_hash(result["data"])
997
 
998
  except Exception as e:
999
- print(f"[ws] init error: {e}")
1000
 
1001
  try:
1002
  while True:
1003
  raw = await ws.receive_text()
1004
  try:
1005
  msg = json.loads(raw)
1006
- # ุชุฌุงู‡ู„ pong responses
1007
  if msg.get("type") == "pong":
1008
  continue
1009
  await handle_action(ws, msg, sess)
@@ -1012,12 +1072,10 @@ async def websocket_endpoint(ws: WebSocket):
1012
  except WebSocketDisconnect:
1013
  pass
1014
  except Exception as e:
1015
- print(f"[ws] error: {e}")
1016
  finally:
1017
  hb_task.cancel()
1018
- if ws in active_connections:
1019
- active_connections.remove(ws)
1020
- await destroy_session(ws)
1021
 
1022
 
1023
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -1025,8 +1083,11 @@ async def websocket_endpoint(ws: WebSocket):
1025
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1026
 
1027
  @app.get("/screenshot")
1028
- async def rest_screenshot():
1029
- result = await asyncio.to_thread(capture_with_grid, 0.7, 75)
 
 
 
1030
  return JSONResponse({
1031
  "image": result["data"],
1032
  "ts": int(time.time() * 1000),
@@ -1035,24 +1096,33 @@ async def rest_screenshot():
1035
  "mouse_x": result["mouse_x"],
1036
  "mouse_y": result["mouse_y"],
1037
  "has_grid": True,
 
 
1038
  })
1039
 
1040
  @app.post("/terminal")
1041
  async def rest_terminal(body: dict):
1042
- return JSONResponse(await run_cmd_smart(body.get("cmd", ""), body.get("timeout", 60)))
 
 
 
 
1043
 
1044
  @app.get("/health")
1045
  async def health():
1046
- w, h = await asyncio.to_thread(_get_screen_size)
1047
- mx, my = await asyncio.to_thread(_get_mouse_pos)
 
 
 
 
 
1048
  return {
1049
  "status": "ok",
1050
- "version": "v8-stable",
1051
  "browser": BROWSER,
1052
- "display": SHARED_DISPLAY,
1053
- "screen": f"{w}x{h}",
1054
- "mouse": f"{mx},{my}",
1055
- "active_sessions": len(_active_sessions),
1056
  }
1057
 
1058
 
@@ -1061,7 +1131,7 @@ async def health():
1061
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1062
 
1063
  async def _cleanup_tmp():
1064
- """ูŠู†ุธู‘ู /tmp ูƒู„ 5 ุฏู‚ุงุฆู‚ ู„ู…ู†ุน ุงู…ุชู„ุงุก ุงู„ู‚ุฑุต ููŠ HuggingFace."""
1065
  while True:
1066
  await asyncio.sleep(300)
1067
  try:
@@ -1073,13 +1143,42 @@ async def _cleanup_tmp():
1073
  print(f"[cleanup] {e}")
1074
 
1075
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
  @app.on_event("startup")
1077
  async def startup():
1078
- # ุชุดุบูŠู„ Xvfb ุงู„ู…ุดุชุฑูƒ
1079
- await asyncio.to_thread(_ensure_xvfb)
1080
- # ุชุดุบูŠู„ ุงู„ุชู†ุธูŠู ุงู„ุฏูˆุฑูŠ
1081
  asyncio.create_task(_cleanup_tmp())
1082
- print("โœ… Z Computer Mode v8 ready")
 
 
 
1083
 
1084
 
1085
  if __name__ == "__main__":
 
1
  """
2
+ THE Z AI โ€” Computer Mode Server v11 โ€” PER-USER ISOLATED DISPLAYS
3
+ =================================================================
4
+ ูƒู„ ู…ุณุชุฎุฏู… ู„ุฏูŠู‡:
5
+ โœ… Display Xvfb ุฎุงุต (:100, :101, :102 ...)
6
+ โœ… ู…ุชุตูุญ Firefox ู…ุนุฒูˆู„ ุชู…ุงู…ุงู‹ ู„ุง ูŠุชุดุงุฑูƒ ู…ุน ุฃูŠ ู…ุณุชุฎุฏู… ุขุฎุฑ
7
+ โœ… ู„ู‚ุทุงุช ุดุงุดุฉ ู…ุณุชู‚ู„ุฉ โ€” ู…ุง ูŠุฑุงู‡ ู…ุณุชุฎุฏู… A ู„ุง ูŠุฎุชู„ุท ุจู…ุณุชุฎุฏู… B
8
+ โœ… ุชุงุฑูŠุฎ terminal ู…ุณุชู‚ู„ ู„ูƒู„ ู…ุณุชุฎุฏู…
9
+ โœ… ุนู†ุฏ ูƒู„ ุงุชุตุงู„ ุฌุฏูŠุฏ ู„ู„ู…ุณุชุฎุฏู…: ู†ูุณ ุงู„ู€ display (ู„ุง ูŠูุนุงุฏ ุฅู†ุดุงุก Xvfb)
10
+ โœ… ุนู†ุฏ ุทู„ุจ ุฑูŠุณุชุงุฑุช / ูƒู…ุจูŠูˆุชุฑ ุฌุฏูŠุฏ: Xvfb + Firefox ูŠูุนุงุฏุงู† ุชู…ุงู…ุงู‹
11
+
12
+ ุงู„ููƒุฑุฉ:
13
+ - ูƒู„ user_id (email) โ†ฆ display ุฑู‚ู… ุซุงุจุช (ู…ุซู„ุงู‹ :100 ู„ู„ุฃูˆู„ุŒ :101 ู„ู„ุซุงู†ูŠ)
14
+ - ุฅุฐุง ุงู„ู…ุณุชุฎุฏู… ูŠุชุตู„ ู…ุฌุฏุฏุงู‹ุŒ ู†ูุนูŠุฏ ุงุณุชุฎุฏุงู… ู†ูุณ ุงู„ู€ display ุงู„ุฎุงุต ุจู‡
15
+ - ุฅุฐุง ุทู„ุจ ุฑูŠุณุชุงุฑุชุŒ ู†ู‚ุชู„ ุงู„ู€ Xvfb ูˆุงู„ู…ุชุตูุญ ูˆู†ูุนูŠุฏ ุฅู†ุดุงุกู‡ู…ุง
16
+ - ุฅุฐุง ุงู„ู…ุณุชุฎุฏู… ุฌุฏูŠุฏุŒ ู†ุฎุตุต ู„ู‡ display ุฌุฏูŠุฏ ูˆู†ูุดุบู‘ู„ Xvfb ู„ู‡
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  """
18
 
19
  import asyncio
 
26
  import subprocess
27
  import time
28
  import urllib.parse
29
+ import threading
30
 
31
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
32
  from fastapi.middleware.cors import CORSMiddleware
33
  from fastapi.responses import JSONResponse, HTMLResponse
34
  import uvicorn
35
 
36
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
37
+ # โ”€โ”€ ุฅุนุฏุงุฏ Display Pool โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
38
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
39
 
40
+ # ุจุฏุงูŠุฉ ู†ุทุงู‚ ุงู„ู€ displays ุงู„ุงูุชุฑุงุถูŠุฉ (ูŠู…ูƒู† ุชุบูŠูŠุฑู‡)
41
+ DISPLAY_BASE = 100
42
+ DISPLAY_MAX = 200 # ุฃู‚ุตู‰ ุนุฏุฏ ู…ุณุชุฎุฏู…ูŠู† ู…ุชุฒุงู…ู†ูŠู†
43
 
44
+ _display_lock = threading.Lock()
 
45
 
46
+ # user_id โ†’ display_info
47
+ # display_info = {
48
+ # "display": ":101",
49
+ # "xvfb_proc": subprocess.Popen | None,
50
+ # "browser_proc": subprocess.Popen | None,
51
+ # "last_bg_shot_ts": float,
52
+ # "last_bg_hash": str,
53
+ # "active_ws": WebSocket | None, # ุงู„ุงุชุตุงู„ ุงู„ุญุงู„ูŠ ู„ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู…
54
+ # }
55
+ _user_displays: dict[str, dict] = {}
56
+ _display_numbers: set[int] = set() # ุฃุฑู‚ุงู… ุงู„ู€ displays ุงู„ู…ุณุชุฎุฏู…ุฉ
57
+
58
+ # semaphore ู„ู„ุชุญูƒู… ููŠ ุนู…ู„ูŠุงุช terminal (global ู„ูƒู„ ุงู„ุณูŠุฑูุฑ)
59
+ _terminal_sem = asyncio.Semaphore(8)
60
 
61
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
62
+ # โ”€โ”€ ุงูƒุชุดุงู ุงู„ู…ุชุตูุญ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
63
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
64
 
65
+ def _detect_browser() -> str:
66
+ for c in ["firefox", "firefox-esr", "chromium-browser", "chromium", "google-chrome"]:
67
+ r = subprocess.run(["which", c], capture_output=True, text=True)
68
+ if r.returncode == 0 and r.stdout.strip():
69
+ return c
70
+ return "firefox"
71
 
72
+ BROWSER = _detect_browser()
73
+ print(f"๐ŸŒ Browser: {BROWSER}")
74
+
75
+
76
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
77
+ # โ”€โ”€ Xvfb Management (per-display) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
78
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
79
 
80
+ def _next_free_display() -> int:
81
+ """ูŠุฌุฏ ุฑู‚ู… display ุญุฑ ุบูŠุฑ ู…ุณุชุฎุฏู…."""
82
+ used = _display_numbers.copy()
83
+ for n in range(DISPLAY_BASE, DISPLAY_MAX):
84
+ if n not in used:
85
+ return n
86
+ # ุฅุฐุง ุงู…ุชู„ุฃุช ุงู„ู‚ุงุฆู…ุฉ โ€” ุฃุนุฏ ุงุณุชุฎุฏุงู… ุฃู‚ุฏู… display ุบูŠุฑ ู†ุดุท
87
+ return DISPLAY_BASE
88
+
89
+
90
+ def _start_xvfb(display: str) -> "subprocess.Popen | None":
91
+ """ูŠูุดุบู‘ู„ Xvfb ุนู„ู‰ display ู…ุญุฏุฏ ูˆูŠูุนูŠุฏ ุงู„ู€ Popen ุฃูˆ None."""
92
+ # ุชุญู‚ู‚ ุฃูˆู„ุงู‹: ู‡ู„ ุงู„ู€ display ูŠุนู…ู„ ุจุงู„ูุนู„ุŸ
93
  try:
94
  r = subprocess.run(
95
+ ["xdpyinfo", "-display", display],
96
  capture_output=True, timeout=3
97
  )
98
  if r.returncode == 0:
99
+ print(f"[xvfb] โœ… Display {display} already active")
100
+ return None # ูŠุนู…ู„ ุจุงู„ูุนู„ ุจุฏูˆู† Popen ู†ุดุบู‘ู„ู‡
 
 
101
  except Exception:
102
  pass
103
 
 
104
  try:
105
+ proc = subprocess.Popen(
106
+ ["Xvfb", display, "-screen", "0", "1920x1080x24",
107
  "-nolisten", "tcp", "-ac"],
108
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
109
  )
110
+ # ุงู†ุชุธุฑ ุญุชู‰ ูŠุตุจุญ ุฌุงู‡ุฒุงู‹ (max 6 ุซูˆุงู†ูŠ)
111
+ for _ in range(60):
112
  time.sleep(0.1)
113
  try:
114
  r = subprocess.run(
115
+ ["xdpyinfo", "-display", display],
116
  capture_output=True, timeout=2
117
  )
118
  if r.returncode == 0:
119
+ print(f"[xvfb] โœ… Xvfb started on {display}")
120
+ return proc
121
  except Exception:
122
  continue
123
+ print(f"[xvfb] โš ๏ธ Xvfb may not be ready yet on {display}")
124
+ return proc
125
  except FileNotFoundError:
126
+ print(f"[xvfb] โŒ Xvfb binary not found")
127
+ return None
128
  except Exception as e:
129
+ print(f"[xvfb] โŒ Failed to start on {display}: {e}")
130
+ return None
131
 
 
 
 
 
 
 
 
132
 
133
+ def _kill_proc(proc: "subprocess.Popen | None"):
134
+ """ูŠู‚ุชู„ process ุจุฃู…ุงู†."""
135
+ if not proc:
136
+ return
137
+ try:
138
+ proc.terminate()
139
+ proc.wait(timeout=3)
140
+ except Exception:
141
+ try:
142
+ proc.kill()
143
+ except Exception:
144
+ pass
145
+
146
+
147
+ def _kill_display_processes(display: str):
148
+ """ูŠู‚ุชู„ ูƒู„ ุงู„ุนู…ู„ูŠุงุช ุงู„ู…ุฑุชุจุทุฉ ุจู€ display ู…ุนูŠู†."""
149
+ env_d = {**os.environ, "DISPLAY": display}
150
+ # ู‚ุชู„ ุงู„ู…ุชุตูุญ
151
+ for b in ["firefox", "firefox-esr", "chromium", "chrome"]:
152
+ try:
153
+ subprocess.run(
154
+ ["pkill", "-f", f"[{b[0]}]{b[1:]}.*{display}"],
155
+ timeout=3, capture_output=True
156
+ )
157
+ except Exception:
158
+ pass
159
+ # ู‚ุชู„ Xvfb ุนู„ู‰ ู‡ุฐุง ุงู„ู€ display
160
+ try:
161
+ subprocess.run(
162
+ ["pkill", "-f", f"[X]vfb {display}"],
163
+ timeout=3, capture_output=True
164
+ )
165
+ except Exception:
166
+ pass
167
+ time.sleep(0.5)
168
+
169
 
170
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
171
+ # โ”€โ”€ Session Management (per user_id) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
172
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
173
 
174
+ async def get_or_create_user_session(user_id: str, ws: WebSocket) -> dict:
175
  """
176
+ ูŠูุนูŠุฏ session ุงู„ู…ุณุชุฎุฏู… (ุฃูˆ ูŠูู†ุดุฆ ูˆุงุญุฏุฉ ุฌุฏูŠุฏุฉ ุฅุฐุง ู„ู… ุชูƒู† ู…ูˆุฌูˆุฏุฉ).
177
+ ูƒู„ user_id โ† display ุฎุงุต + Xvfb ุฎุงุต.
178
  """
179
+ with _display_lock:
180
+ if user_id in _user_displays:
181
+ sess = _user_displays[user_id]
182
+ # ุญุฏู‘ุซ ุงู„ู€ WebSocket ุงู„ุญุงู„ูŠ
183
+ sess["active_ws"] = ws
184
+ print(f"[session] ๐Ÿ” Reconnected user '{user_id}' on display {sess['display']}")
185
+ return sess
186
+
187
+ # ู…ุณุชุฎุฏู… ุฌุฏูŠุฏ โ€” ุฎุตุต ู„ู‡ display
188
+ disp_num = _next_free_display()
189
+ _display_numbers.add(disp_num)
190
+ display = f":{disp_num}"
191
+
192
+ sess = {
193
+ "user_id": user_id,
194
+ "display": display,
195
+ "xvfb_proc": None, # ุณูŠูุดุบูŽู‘ู„ ู„ุงุญู‚ุงู‹
196
+ "browser_proc": None,
197
+ "last_bg_shot_ts": 0.0,
198
+ "last_bg_hash": "",
199
+ "active_ws": ws,
200
+ "created": time.time(),
201
+ }
202
+ _user_displays[user_id] = sess
203
+ print(f"[session] โœ… New user '{user_id}' โ†’ display {display}")
204
+ return sess
205
+
206
+
207
+ async def reset_user_computer(user_id: str) -> dict:
208
+ """
209
+ ูŠูุนูŠุฏ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ ุงู„ุงูุชุฑุงุถูŠ ู„ู„ู…ุณุชุฎุฏู…:
210
+ ูŠู‚ุชู„ Xvfb ูˆุงู„ู…ุชุตูุญ ูˆูŠูุดุบู‘ู„ Xvfb ุฌุฏูŠุฏุงู‹ ู†ุธูŠูุงู‹.
211
+ """
212
+ with _display_lock:
213
+ sess = _user_displays.get(user_id)
214
+ if not sess:
215
+ return {}
216
 
217
+ display = sess["display"]
218
+ print(f"[reset] ๐Ÿ”„ Resetting computer for user '{user_id}' on {display}")
 
 
 
 
 
 
 
 
 
219
 
220
+ # ู‚ุชู„ ุงู„ู…ุชุตูุญ
221
+ _kill_proc(sess.get("browser_proc"))
222
+ sess["browser_proc"] = None
223
 
224
+ # ู‚ุชู„ Xvfb
225
+ _kill_proc(sess.get("xvfb_proc"))
226
+ sess["xvfb_proc"] = None
227
 
228
+ # ุชู†ุธูŠู ุดุงู…ู„ ู„ู‡ุฐุง ุงู„ู€ display
229
+ _kill_display_processes(display)
230
+
231
+ # ุชู†ุธูŠู lock files Firefox
232
+ try:
233
+ subprocess.run(
234
+ "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null",
235
+ shell=True, timeout=5, capture_output=True
236
+ )
237
+ except Exception:
238
+ pass
239
+
240
+ # ุชุดุบูŠู„ Xvfb ุฌุฏูŠุฏ (ุฎุงุฑุฌ ุงู„ู€ lock ู„ุฃู† start_xvfb ูŠุณุชุบุฑู‚ ูˆู‚ุชุงู‹)
241
+ new_proc = await asyncio.to_thread(_start_xvfb, display)
242
+ with _display_lock:
243
+ if user_id in _user_displays:
244
+ _user_displays[user_id]["xvfb_proc"] = new_proc
245
+ _user_displays[user_id]["last_bg_shot_ts"] = 0.0
246
+ _user_displays[user_id]["last_bg_hash"] = ""
247
+
248
+ print(f"[reset] โœ… Computer reset done for '{user_id}' on {display}")
249
+ return _user_displays.get(user_id, {})
250
+
251
+
252
+ async def ensure_xvfb_for_session(sess: dict):
253
+ """ูŠุชุฃูƒุฏ ุฃู† Xvfb ูŠุนู…ู„ ู„ู‡ุฐุง ุงู„ู€ session โ€” ูŠูุดุบู‘ู„ู‡ ุฅุฐุง ู„ู… ูŠูƒู† ูƒุฐู„ูƒ."""
254
+ display = sess["display"]
255
+
256
+ # ุชุญู‚ู‚ ุฅุฐุง ูƒุงู† ูŠุนู…ู„ ุจุงู„ูุนู„
257
+ try:
258
+ r = subprocess.run(
259
+ ["xdpyinfo", "-display", display],
260
+ capture_output=True, timeout=3
261
+ )
262
+ if r.returncode == 0:
263
+ return # ูŠุนู…ู„
264
+ except Exception:
265
+ pass
266
+
267
+ # ุดุบู‘ู„ู‡
268
+ proc = await asyncio.to_thread(_start_xvfb, display)
269
+ with _display_lock:
270
+ if sess["user_id"] in _user_displays:
271
+ _user_displays[sess["user_id"]]["xvfb_proc"] = proc
272
+
273
+
274
+ async def destroy_user_ws(user_id: str, ws: WebSocket):
275
+ """
276
+ ูŠูุฒูŠู„ ุงู„ู€ WebSocket ู…ู† ุงู„ู€ session ุนู†ุฏ ุงู†ู‚ุทุงุน ุงู„ุงุชุตุงู„.
277
+ ู„ุง ูŠุญุฐู ุงู„ู€ session ู†ูุณู‡ุง โ€” ุงู„ู…ุณุชุฎุฏู… ูŠุญุชูุธ ุจูƒู…ุจูŠูˆุชุฑู‡.
278
+ """
279
+ with _display_lock:
280
+ sess = _user_displays.get(user_id)
281
+ if sess and sess.get("active_ws") is ws:
282
+ sess["active_ws"] = None
283
+ print(f"[session] ๐Ÿ“ด User '{user_id}' disconnected (session kept)")
284
 
285
 
286
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
287
+ # โ”€โ”€ Screenshot Engine (per-display) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
288
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
289
 
290
  def _is_black_screen(img) -> bool:
 
 
 
 
291
  try:
292
  small = img.resize((100, 100))
293
  pixels = list(small.getdata())
294
  avg = sum(sum(p[:3]) for p in pixels) / (len(pixels) * 3 * 255)
295
+ return avg < 0.04
296
  except Exception:
297
  return False
298
 
299
 
300
+ def _capture_raw(display: str) -> tuple:
301
+ """ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ู…ู† display ู…ุญุฏุฏ."""
 
 
 
302
  from PIL import Image
303
+ env = {**os.environ, "DISPLAY": display}
304
  ts = int(time.time() * 1000)
305
+ # ุงุณุชุฎุฏู… display number ููŠ ุงุณู… ุงู„ู…ู„ู ู„ุชูุงุฏูŠ ุงู„ุชุนุงุฑุถ
306
+ disp_safe = display.replace(":", "")
307
+ tmp_base = f"/tmp/zss_{disp_safe}_{ts}"
308
  created_files = []
309
 
310
  def _try_load(path) -> tuple:
 
319
  return None, 0, 0
320
  return img, w, h
321
  except Exception as ex:
322
+ print(f"[cap:{display}] load error: {ex}")
323
  return None, 0, 0
324
 
325
  def _cleanup_files():
 
330
  except Exception:
331
  pass
332
 
 
333
  for attempt in range(3):
334
  if attempt > 0:
335
  time.sleep(0.8)
336
+ print(f"[cap:{display}] retry attempt {attempt+1} (black screen)")
337
  try:
338
+ # Method 1: scrot
339
  p1 = f"{tmp_base}_a{attempt}_scrot.png"
340
  created_files.append(p1)
341
  try:
342
+ subprocess.run(["scrot", "-q", "95", p1],
343
+ env=env, timeout=8, capture_output=True)
 
 
344
  img, w, h = _try_load(p1)
345
  if img and not _is_black_screen(img):
 
346
  return img, w, h
 
 
347
  except Exception as e:
348
+ print(f"[cap:{display}] scrot: {e}")
349
 
350
+ # Method 2: ImageMagick import
351
  p2 = f"{tmp_base}_a{attempt}_im.png"
352
  created_files.append(p2)
353
  try:
354
+ subprocess.run(["import", "-window", "root", "-silent", p2],
355
+ env=env, timeout=10, capture_output=True)
 
 
356
  img, w, h = _try_load(p2)
357
  if img and not _is_black_screen(img):
 
358
  return img, w, h
359
  except Exception as e:
360
+ print(f"[cap:{display}] import: {e}")
361
 
362
+ # Method 3: ffmpeg x11grab
363
  p3 = f"{tmp_base}_a{attempt}_ff.png"
364
  created_files.append(p3)
365
  try:
366
+ sw, sh = _get_screen_size(display)
367
  subprocess.run([
368
  "ffmpeg", "-y", "-f", "x11grab",
369
  "-video_size", f"{sw}x{sh}",
370
+ "-i", display,
371
  "-vframes", "1", "-q:v", "1", p3
372
  ], env=env, timeout=12, capture_output=True)
373
  img, w, h = _try_load(p3)
374
  if img and not _is_black_screen(img):
 
375
  return img, w, h
376
  except Exception as e:
377
+ print(f"[cap:{display}] ffmpeg: {e}")
378
 
379
+ # Method 4: python-xlib
380
  try:
381
  from Xlib import display as Xdisp, X
382
+ xd = Xdisp.Display(display)
383
  root = xd.screen().root
384
  geom = root.get_geometry()
385
  w, h = geom.width, geom.height
 
392
  pixels[j], pixels[j+1], pixels[j+2] = r_c, g, b
393
  img = Image.frombytes("RGB", (w, h), bytes(pixels))
394
  if not _is_black_screen(img):
 
395
  return img, w, h
396
  except Exception as e:
397
+ print(f"[cap:{display}] xlib: {e}")
398
 
399
  finally:
400
  _cleanup_files()
401
 
402
+ # Placeholder
403
+ print(f"[cap:{display}] โš ๏ธ All methods failed โ€” placeholder")
404
  from PIL import Image as PILImg, ImageDraw
405
+ sw, sh = _get_screen_size(display)
406
  img = PILImg.new("RGB", (sw or 1920, sh or 1080), (15, 20, 40))
407
  draw = ImageDraw.Draw(img)
408
  draw.rectangle([(0, 0), (sw, 55)], fill=(30, 40, 80))
409
+ draw.text((10, 10), f"โš ๏ธ Screenshot failed โ€” DISPLAY={display}", fill=(255, 120, 80))
410
+ draw.text((10, 32), "Methods: scrot, import, ffmpeg, xlib (3 attempts)", fill=(130, 130, 150))
411
  return img, sw or 1920, sh or 1080
412
 
413
 
414
+ def _get_screen_size(display: str) -> tuple:
415
  try:
416
  r = subprocess.run(
417
  ["xdotool", "getdisplaygeometry"],
418
+ env={**os.environ, "DISPLAY": display},
419
  capture_output=True, text=True, timeout=5
420
  )
421
  parts = r.stdout.strip().split()
 
424
  return 1920, 1080
425
 
426
 
427
+ def _get_mouse_pos(display: str) -> tuple:
428
  try:
429
  r = subprocess.run(
430
  ["xdotool", "getmouselocation"],
431
+ env={**os.environ, "DISPLAY": display},
432
  capture_output=True, text=True, timeout=5
433
  )
434
  mx = int(re.search(r"x:(\d+)", r.stdout).group(1))
 
438
  return 0, 0
439
 
440
 
441
+ def capture_with_grid(display: str, scale: float = 0.65, quality: int = 72,
442
  force_mx: int | None = None,
443
  force_my: int | None = None) -> dict:
444
+ """ูŠู„ุชู‚ุท ุงู„ุดุงุดุฉ ู…ู† display ู…ุญุฏุฏ ูˆูŠุถูŠู Grid overlay."""
 
 
 
445
  from PIL import Image, ImageDraw
446
 
447
+ img, ow, oh = _capture_raw(display)
448
  if img is None:
449
  return {"data": "", "width": 1920, "height": 1080, "mouse_x": 0, "mouse_y": 0}
450
 
451
+ mx, my = (force_mx, force_my) if force_mx is not None else _get_mouse_pos(display)
452
 
453
  sw = int(ow * scale)
454
  sh = int(oh * scale)
455
  grid_img = img.resize((sw, sh), Image.LANCZOS)
456
  draw = ImageDraw.Draw(grid_img, "RGBA")
457
 
 
458
  step_x = int(100 * sw / ow)
459
  step_y = int(100 * sh / oh)
460
  gc = (255, 255, 255, 40)
 
474
  draw.text((3, y_sc+2), str(y_r), fill=lc)
475
  y_sc += step_y; y_r += 100
476
 
 
477
  msx = int(mx * sw / ow)
478
  msy = int(my * sh / oh)
479
  r = 11
 
481
  draw.line([(msx-18, msy), (msx+18, msy)], fill=(255, 50, 50, 200), width=1)
482
  draw.line([(msx, msy-18), (msx, msy+18)], fill=(255, 50, 50, 200), width=1)
483
 
 
484
  final = grid_img.convert("RGB")
485
  draw2 = ImageDraw.Draw(final)
486
  draw2.rectangle([(0, 0), (sw, 20)], fill=(0, 0, 0))
487
+ draw2.text((4, 3), f"SCREEN {ow}x{oh} | MOUSE:({mx},{my}) | GRID=100px | DSP:{display}", fill=(0, 220, 160))
488
  draw2.rectangle([(0, sh-20), (sw, sh)], fill=(0, 0, 0))
489
  draw2.text((4, sh-17), "CLICK COORDS = numbers on grid lines (real screen pixels)", fill=(180, 180, 70))
490
 
 
496
 
497
 
498
  def _frame_hash(data: str) -> str:
 
499
  return hashlib.md5(data[:2000].encode()).hexdigest()
500
 
501
 
502
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
503
+ # โ”€โ”€ SafeSearch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
504
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
505
 
506
  _BING_RE = re.compile(r"https?://(?:www\.)?bing\.com[^\s'\"]*")
 
521
 
522
 
523
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
524
+ # โ”€โ”€ Terminal Execution (per-display) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€
525
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
526
 
527
  _PKILL_RE = re.compile(r"\b(pkill|killall)\s+(-9\s+)?-f\s+(['\"]?)([a-zA-Z0-9_./-]+)\3")
 
534
  return _PKILL_RE.sub(_fix, cmd)
535
 
536
 
537
+ async def run_cmd(cmd: str, display: str, timeout: int = 60) -> dict:
538
+ """ูŠู†ูู‘ุฐ ุฃู…ุฑ bash ู…ุน DISPLAY ุฎุงุต ุจุงู„ู…ุณุชุฎุฏู…."""
539
  cmd = _sanitize_pkill(_safe_search(cmd))
540
  async with _terminal_sem:
541
+ env = {**os.environ, "DISPLAY": display,
542
  "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
543
 
544
  def _exec():
 
557
 
558
 
559
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
560
+ # โ”€โ”€ xdotool helpers (per-display) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
561
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
562
 
563
+ async def xdo(args: list, display: str, timeout: int = 10) -> dict:
564
+ env = {**os.environ, "DISPLAY": display}
565
  def _run():
566
  r = subprocess.run(
567
  ["xdotool"] + args, env=env,
 
571
  return await asyncio.to_thread(_run)
572
 
573
 
574
+ async def type_smart(text: str, display: str) -> dict:
575
+ """ูƒุชุงุจุฉ ู†ุต ุฐูƒูŠุฉ ุจู€ display ู…ุญุฏุฏ."""
576
  has_arabic = bool(re.search(r'[\u0600-\u06FF]', text))
577
+ env = {**os.environ, "DISPLAY": display}
578
 
579
  if has_arabic:
580
  def _paste():
 
585
  p.communicate(text.encode("utf-8"))
586
  await asyncio.to_thread(_paste)
587
  await asyncio.sleep(0.15)
588
+ await xdo(["key", "--clearmodifiers", "ctrl+v"], display)
589
  return {"method": "clipboard+paste"}
590
  else:
591
+ r = await xdo(["type", "--clearmodifiers", "--delay", "25", text], display)
592
  return {"method": "xdotool", "rc": r["rc"]}
593
 
594
 
595
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
596
+ # โ”€โ”€ Search Sources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
597
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
598
 
599
  def _search_sources(query: str) -> list:
600
  q = urllib.parse.quote_plus(query)
601
  return [
602
+ {"name": "DuckDuckGo Instant",
603
+ "cmd": f"curl -s --max-time 15 'https://api.duckduckgo.com/?q={q}&format=json&no_html=1&skip_disambig=1' | python3 -c \"import sys,json;d=json.load(sys.stdin);a=d.get('AbstractText','');r=d.get('RelatedTopics',[]);print('ANS:',a or 'none');[print('-',x.get('Text','')[:200]) for x in r[:6] if isinstance(x,dict)]\""},
604
+ {"name": "Google News RSS",
605
+ "cmd": f"curl -sL --max-time 15 'https://news.google.com/rss/search?q={q}&hl=ar&gl=AR&ceid=AR:ar' | python3 -c \"import sys,re;x=sys.stdin.read();t=re.findall(r'<title><!\\[CDATA\\[(.*?)\\]\\]></title>|<title>(.*?)</title>',x);[(print(str(i+1)+'. '+(a or b).strip()[:160])) for i,(a,b) in enumerate(t[1:8]) if (a or b).strip()]\""},
606
+ {"name": "Wikipedia EN",
607
+ "cmd": f"curl -s --max-time 12 'https://en.wikipedia.org/api/rest_v1/page/summary/{q}' | python3 -c \"import sys,json;d=json.load(sys.stdin);print(d.get('title','')+'\\n'+d.get('extract','')[:1200])\""},
608
+ {"name": "DuckDuckGo HTML",
609
+ "cmd": f"curl -sL --max-time 15 -H 'User-Agent: Mozilla/5.0' 'https://html.duckduckgo.com/html/?q={q}' | python3 -c \"import sys,re;h=sys.stdin.read();s=re.findall(r'class=.result__snippet[^>]*>(.*?)</a>',h,re.DOTALL);clean=lambda x:re.sub('<[^>]+>','',x).strip();[print(str(i+1)+'. '+clean(x)[:200]) for i,x in enumerate(s[:7])]\""},
610
+ {"name": "HackerNews",
611
+ "cmd": f"curl -s --max-time 12 'https://hn.algolia.com/api/v1/search?query={q}&hitsPerPage=6&tags=story' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+h.get('title','')+' Pts:'+str(h.get('points',0))) for i,h in enumerate(d.get('hits',[])[:5])]\""},
612
+ {"name": "Reddit",
613
+ "cmd": f"curl -sL --max-time 12 -H 'Accept: application/json' 'https://www.reddit.com/search.json?q={q}&sort=new&limit=6' | python3 -c \"import sys,json;d=json.load(sys.stdin);[print(str(i+1)+'. '+p['data'].get('title','')[:160]) for i,p in enumerate(d.get('data',{{}}).get('children',[])[:5])]\""},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  ]
615
 
616
 
617
  def _result_empty(s: str) -> bool:
618
  if not s or len(s.strip()) < 15:
619
  return True
620
+ return "ans: none" in s.lower()
 
621
 
622
 
623
+ async def run_cmd_smart(cmd: str, display: str, timeout: int = 60) -> dict:
624
+ res = await run_cmd(cmd, display, timeout)
 
625
  if res["returncode"] == 0 and not _result_empty(res["stdout"]):
626
  return res
627
 
 
628
  is_search = "curl" in cmd and any(
629
  x in cmd for x in ["search", "duckduck", "bing", "google", "wikipedia", "reddit"]
630
  )
631
  if not is_search:
632
  return res
633
 
 
634
  m = re.search(r"[?&]q=([^&'\"\s]+)", cmd)
635
  query = urllib.parse.unquote(m.group(1).replace("+", " ")).strip() if m else ""
636
  if len(query) < 3:
 
639
  sources = _search_sources(query)
640
  results = []
641
  for src in sources:
642
+ r2 = await run_cmd(src["cmd"], display, 20)
643
  if not _result_empty(r2["stdout"]):
644
  results.append(f"[{src['name']}]\n{r2['stdout']}")
645
  if len(results) >= 2:
 
654
  # โ”€โ”€ FastAPI App โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
655
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
656
 
657
+ app = FastAPI(title="Z-Computer-Mode v11 โ€” Per-User Isolation")
658
  app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
659
  allow_methods=["*"], allow_headers=["*"])
660
 
 
 
661
 
662
  @app.get("/", response_class=HTMLResponse)
663
  async def root():
664
+ n = len(_user_displays)
665
+ users_info = ""
666
+ with _display_lock:
667
+ for uid, sess in _user_displays.items():
668
+ ws_status = "๐ŸŸข ู…ุชุตู„" if sess.get("active_ws") else "โšซ ุบูŠุฑ ู…ุชุตู„"
669
+ users_info += f"<tr><td>{uid[:30]}...</td><td>{sess['display']}</td><td>{ws_status}</td></tr>"
670
+
671
  return f"""<!DOCTYPE html><html><head><meta charset="utf-8">
672
+ <title>Z Computer Mode v11</title>
673
  <style>body{{background:#080808;color:#00d4aa;font-family:monospace;padding:40px}}
674
+ h1{{color:#4a90d9}}.ok{{color:#00ff88}}.info{{color:#888;font-size:13px}}
675
+ table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #333;padding:6px 10px;font-size:13px}}
676
+ th{{background:#1a1a2e;color:#4a90d9}}</style>
677
  </head><body>
678
+ <h1>๐Ÿ–ฅ๏ธ THE Z AI โ€” Computer Mode Server v11</h1>
679
+ <p class="ok">โœ… Server RUNNING โ€” Per-User Isolated Displays</p>
680
  <p>๐ŸŒ Browser: <b>{BROWSER}</b></p>
681
+ <p>๐Ÿ‘ฅ Active users: <b>{n}</b></p>
682
+ <p>๐Ÿ“‹ Display range: <b>:{DISPLAY_BASE} โ†’ :{DISPLAY_MAX}</b></p>
683
+ <hr style="border-color:#333">
684
+ <h3>๐Ÿ‘ค User Sessions</h3>
685
+ <table><tr><th>User ID</th><th>Display</th><th>Status</th></tr>
686
+ {users_info or '<tr><td colspan="3" style="text-align:center;color:#666">No active sessions</td></tr>'}
687
+ </table>
688
  <hr style="border-color:#333">
689
+ <p class="info">Endpoints: /health ยท /ws?user_id=email (WebSocket)</p>
690
  </body></html>"""
691
 
692
 
693
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
694
+ # โ”€โ”€ Action Handler (per-session) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
695
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
696
 
697
  async def handle_action(ws: WebSocket, msg: dict, sess: dict):
698
+ action = msg.get("action", "")
699
+ data = msg.get("data", {})
700
+ display = sess["display"]
701
 
702
  async def send(obj):
703
  try:
 
707
 
708
  async def shot_bg(label: str = "", delay: float = 0.5,
709
  force_mx: int | None = None, force_my: int | None = None):
710
+ """auto_shot ููŠ ุงู„ุฎู„ููŠุฉ โ€” ู…ุน rate limit + delta suppression."""
 
 
 
 
711
  await asyncio.sleep(delay)
 
 
712
  now = time.time()
713
  if now - sess.get("last_bg_shot_ts", 0) < 0.3:
714
  await asyncio.sleep(0.3 - (now - sess["last_bg_shot_ts"]))
715
 
716
  result = await asyncio.to_thread(
717
+ capture_with_grid, display, 0.65, 72, force_mx, force_my
718
  )
719
  sess["last_bg_shot_ts"] = time.time()
 
720
  if not result["data"]:
721
  return
722
 
 
723
  fh = _frame_hash(result["data"])
724
  if fh == sess.get("last_bg_hash", ""):
725
+ return
726
  sess["last_bg_hash"] = fh
727
 
728
  await send({
729
  "type": "screenshot",
730
  "data": result["data"],
731
  "ts": int(time.time() * 1000),
732
+ "auto": True, "label": label,
 
733
  "screen_width": result["width"],
734
  "screen_height": result["height"],
735
  "mouse_x": result["mouse_x"],
 
738
  })
739
 
740
  async def shot_explicit(label: str = ""):
741
+ """screenshot ุตุฑูŠุญ โ€” ูŠูุฑุณู„ ุฏุงุฆู…ุงู‹ ุจุฏูˆู† delta suppression."""
742
+ result = await asyncio.to_thread(capture_with_grid, display, 0.65, 75)
 
 
 
 
743
  if not result["data"]:
744
  return
 
745
  sess["last_bg_hash"] = _frame_hash(result["data"])
746
  await send({
747
  "type": "screenshot",
748
  "data": result["data"],
749
  "ts": int(time.time() * 1000),
750
+ "auto": False, "label": label,
 
751
  "screen_width": result["width"],
752
  "screen_height": result["height"],
753
  "mouse_x": result["mouse_x"],
 
755
  "has_grid": True,
756
  })
757
 
758
+ # โ”€โ”€ reset_computer: ุฅุนุงุฏุฉ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ ูƒุฃู†ู‡ ุฌุฏูŠุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
759
+ if action == "reset_computer":
760
+ user_id = sess["user_id"]
761
+ await send({"type": "ack", "action": "reset_computer", "status": "resetting"})
762
+ await reset_user_computer(user_id)
763
+ # ุฃุฑุณู„ ู„ู‚ุทุฉ ุดุงุดุฉ ุจุนุฏ ุงู„ุฑูŠุณุชุงุฑุช
764
+ await asyncio.sleep(2.0)
765
+ await shot_explicit("after reset")
766
+ await send({"type": "computer_reset", "msg": "โœ… ุชู… ุฅุนุงุฏุฉ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ โ€” ุงู„ุดุงุดุฉ ุฌุฏูŠุฏุฉ ุชู…ุงู…ุงู‹"})
767
+ return
768
+
769
+ # โ”€โ”€ screenshot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
770
  if action == "screenshot":
771
  await shot_explicit("explicit screenshot")
772
 
 
776
  if not cmd:
777
  await send({"type": "terminal_result", "stdout": "", "stderr": "no cmd", "returncode": -1})
778
  return
779
+ res = await run_cmd_smart(cmd, display, int(data.get("timeout", 60)))
780
  await send({
781
  "type": "terminal_result",
782
  "cmd": cmd,
 
784
  "stderr": res.get("stderr", ""),
785
  "returncode": res["returncode"],
786
  })
 
787
  asyncio.create_task(shot_explicit(f"after terminal"))
788
 
789
  # โ”€โ”€ mouse_move โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
790
  elif action == "mouse_move":
791
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
792
+ await xdo(["mousemove", "--sync", str(x), str(y)], display)
793
  await send({"type": "ack", "action": "mouse_move", "x": x, "y": y})
794
  asyncio.create_task(shot_bg(f"move ({x},{y})", delay=0.2, force_mx=x, force_my=y))
795
 
 
798
  x, y = int(data.get("x", 0)), int(data.get("y", 0))
799
  btn = {"left": "1", "middle": "2", "right": "3"}.get(data.get("button", "left"), "1")
800
  double = data.get("double", False)
801
+ await xdo(["mousemove", "--sync", str(x), str(y)], display)
802
  await asyncio.sleep(0.07)
803
  if double:
804
+ await xdo(["click", "--repeat", "2", "--delay", "100", btn], display)
805
  else:
806
+ await xdo(["click", btn], display)
807
  btn_name = {"1": "left", "2": "middle", "3": "right"}.get(btn, "left")
808
  await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn_name})
809
  asyncio.create_task(shot_bg(f"click {btn_name} ({x},{y})", delay=0.5, force_mx=x, force_my=y))
 
812
  elif action == "mouse_drag":
813
  x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
814
  x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
815
+ await xdo(["mousemove", str(x1), str(y1)], display)
816
+ await xdo(["mousedown", "1"], display)
817
  await asyncio.sleep(0.1)
818
+ await xdo(["mousemove", str(x2), str(y2)], display)
819
  await asyncio.sleep(0.1)
820
+ await xdo(["mouseup", "1"], display)
821
  await send({"type": "ack", "action": "mouse_drag"})
822
  asyncio.create_task(shot_bg("after drag", delay=0.4, force_mx=x2, force_my=y2))
823
 
 
825
  elif action == "keyboard_type":
826
  text = _safe_search(data.get("text", ""))
827
  if text:
828
+ res = await type_smart(text, display)
829
  await send({"type": "ack", "action": "keyboard_type", "method": res["method"]})
830
  asyncio.create_task(shot_bg("after type", delay=0.5))
831
 
 
833
  elif action == "keyboard_hotkey":
834
  keys = data.get("keys", [])
835
  if keys:
836
+ await xdo(["key", "--clearmodifiers", "+".join(keys)], display)
837
  await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys})
838
  asyncio.create_task(shot_bg("after hotkey", delay=0.4))
839
 
 
841
  elif action == "keyboard_press":
842
  key = data.get("key", "")
843
  if key:
844
+ await xdo(["key", "--clearmodifiers", key], display)
845
  await send({"type": "ack", "action": "keyboard_press"})
846
  asyncio.create_task(shot_bg("after key", delay=0.4))
847
 
 
850
  x, y = int(data.get("x", 960)), int(data.get("y", 540))
851
  clicks = max(-5, min(5, int(data.get("clicks", 3))))
852
  btn = "4" if clicks > 0 else "5"
853
+ await xdo(["mousemove", str(x), str(y)], display)
854
  for _ in range(abs(clicks)):
855
+ await xdo(["click", btn], display)
856
  await asyncio.sleep(0.025)
857
  await send({"type": "ack", "action": "scroll", "clicks": clicks})
858
  asyncio.create_task(shot_bg("after scroll", delay=0.4))
 
860
  # โ”€โ”€ clipboard_write โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
861
  elif action == "clipboard_write":
862
  text = data.get("text", "")
863
+ env = {**os.environ, "DISPLAY": display}
864
  def _clip():
865
  p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env)
866
  p.communicate(text.encode("utf-8"))
 
869
 
870
  # โ”€โ”€ clipboard_read โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
871
  elif action == "clipboard_read":
872
+ res = await run_cmd("xclip -selection clipboard -o", display, 5)
873
  await send({"type": "clipboard_content", "text": res["stdout"]})
874
 
875
  # โ”€โ”€ paste โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
876
  elif action == "paste":
877
  text = data.get("text", "")
878
  if text:
879
+ env = {**os.environ, "DISPLAY": display}
880
  def _clip2():
881
  p = subprocess.Popen(["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE, env=env)
882
  p.communicate(text.encode("utf-8"))
883
  await asyncio.to_thread(_clip2)
884
  await asyncio.sleep(0.1)
885
+ await xdo(["key", "--clearmodifiers", "ctrl+v"], display)
886
  await send({"type": "ack", "action": "paste"})
887
  asyncio.create_task(shot_bg("after paste", delay=0.5))
888
 
 
892
  if not cmd:
893
  await send({"type": "ack", "action": "open_app"})
894
  return
 
 
895
  if "firefox" in cmd.lower():
896
  await run_cmd(
897
  "pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
898
  "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
899
  "echo CLEANED",
900
+ display, timeout=8
901
  )
902
+ env = {**os.environ, "DISPLAY": display}
903
+ proc = subprocess.Popen(cmd, shell=True, env=env,
904
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 
 
 
905
  if any(b in cmd for b in ("firefox", "chromium", "chrome")):
906
+ with _display_lock:
907
+ if sess["user_id"] in _user_displays:
908
+ _user_displays[sess["user_id"]]["browser_proc"] = proc
909
  await send({"type": "ack", "action": "open_app", "cmd": cmd})
910
  async def _open_app_shots():
911
  await asyncio.sleep(3.0)
 
917
  # โ”€โ”€ open_browser โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
918
  elif action == "open_browser":
919
  url = _safe_search(data.get("url", "") or "about:blank")
920
+ # ู‚ุชู„ Firefox ุงู„ู‚ุฏูŠู… ุงู„ุฎุงุต ุจู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… ูู‚ุท
921
  await run_cmd(
922
  "pkill -f '[f]irefox' 2>/dev/null; sleep 0.5; "
923
  "rm -f ~/.mozilla/firefox/*/lock ~/.mozilla/firefox/*/.parentlock 2>/dev/null; "
924
  "echo CLEANED",
925
+ display, timeout=8
926
  )
927
+ env = {**os.environ, "DISPLAY": display}
928
+ proc = subprocess.Popen(
929
+ [BROWSER, "--no-remote", url], env=env,
930
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
931
+ )
932
+ with _display_lock:
933
+ if sess["user_id"] in _user_displays:
934
+ _user_displays[sess["user_id"]]["browser_proc"] = proc
935
  await send({"type": "ack", "action": "open_browser", "url": url})
936
  async def _open_browser_shots():
937
  await asyncio.sleep(3.0)
 
943
  # โ”€โ”€ open_tab โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
944
  elif action == "open_tab":
945
  url = _safe_search(data.get("url", "") or "about:blank")
946
+ await xdo(["key", "--clearmodifiers", "ctrl+t"], display)
947
  await asyncio.sleep(0.4)
948
+ await xdo(["key", "--clearmodifiers", "ctrl+l"], display)
949
  await asyncio.sleep(0.2)
950
+ await type_smart(url, display)
951
  await asyncio.sleep(0.2)
952
+ await xdo(["key", "--clearmodifiers", "Return"], display)
953
  await send({"type": "ack", "action": "open_tab", "url": url})
954
  asyncio.create_task(shot_bg("after open_tab", delay=1.5))
955
 
956
  # โ”€โ”€ close_tab โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
957
  elif action == "close_tab":
958
+ await xdo(["key", "--clearmodifiers", "ctrl+w"], display)
959
  await send({"type": "ack", "action": "close_tab"})
960
  asyncio.create_task(shot_bg("after close_tab", delay=0.5))
961
 
962
  # โ”€โ”€ browser_back โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
963
  elif action == "browser_back":
964
+ await xdo(["key", "--clearmodifiers", "alt+Left"], display)
965
  await send({"type": "ack", "action": "browser_back"})
966
  asyncio.create_task(shot_bg("after back", delay=0.8))
967
 
968
  # โ”€โ”€ browser_forward โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
969
  elif action == "browser_forward":
970
+ await xdo(["key", "--clearmodifiers", "alt+Right"], display)
971
  await send({"type": "ack", "action": "browser_forward"})
972
  asyncio.create_task(shot_bg("after forward", delay=0.8))
973
 
974
  # โ”€โ”€ browser_search โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
975
  elif action == "browser_search":
976
  url = _safe_search(data.get("url", "") or data.get("query", ""))
977
+ await xdo(["key", "--clearmodifiers", "ctrl+l"], display)
978
  await asyncio.sleep(0.2)
979
+ await type_smart(url, display)
980
  await asyncio.sleep(0.15)
981
+ await xdo(["key", "--clearmodifiers", "Return"], display)
982
  await send({"type": "ack", "action": "browser_search"})
983
  asyncio.create_task(shot_bg("after search", delay=1.5))
984
 
985
  # โ”€โ”€ screen_info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
986
  elif action == "screen_info":
987
+ w, h = await asyncio.to_thread(_get_screen_size, display)
988
+ mx, my = await asyncio.to_thread(_get_mouse_pos, display)
989
  await send({
990
  "type": "screen_info",
991
  "width": w, "height": h,
992
  "mouse_x": mx, "mouse_y": my,
993
  "browser": BROWSER,
994
+ "display": display,
995
  })
996
 
997
  # โ”€โ”€ unknown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
1000
 
1001
 
1002
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1003
+ # โ”€โ”€ WebSocket Endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1004
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1005
 
1006
  @app.websocket("/ws")
1007
+ async def websocket_endpoint(
1008
+ ws: WebSocket,
1009
+ user_id: str = Query(default="anonymous")
1010
+ ):
1011
  await ws.accept()
 
1012
 
1013
+ # ุงู„ุญุตูˆู„ ุนู„ู‰ session ุงู„ู…ุณุชุฎุฏู… ุฃูˆ ุฅู†ุดุงุก ูˆุงุญุฏุฉ ุฌุฏูŠุฏุฉ
1014
+ sess = await get_or_create_user_session(user_id, ws)
1015
+
1016
+ # ุชุฃูƒุฏ ุฃู† Xvfb ูŠุนู…ู„ ู„ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู…
1017
+ await ensure_xvfb_for_session(sess)
1018
+
1019
+ display = sess["display"]
1020
 
1021
  async def _heartbeat():
 
1022
  while True:
1023
  await asyncio.sleep(20)
1024
  try:
 
1029
  hb_task = asyncio.create_task(_heartbeat())
1030
 
1031
  try:
1032
+ w, h = await asyncio.to_thread(_get_screen_size, display)
1033
  await ws.send_text(json.dumps({
1034
  "type": "connected",
1035
  "screen_width": w, "screen_height": h,
1036
  "browser": BROWSER,
1037
+ "display": display,
1038
+ "user_id": user_id,
1039
  "session_id": id(ws),
1040
+ "msg": f"Z Computer Mode v11 | User: {user_id} | Display: {display} | Browser: {BROWSER} | Screen: {w}x{h}",
1041
  }, ensure_ascii=False))
1042
 
1043
+ # ู„ู‚ุทุฉ ุดุงุดุฉ ุฃูˆู„ูŠุฉ
1044
+ result = await asyncio.to_thread(capture_with_grid, display, 0.65, 72)
1045
  if result["data"]:
1046
  await ws.send_text(json.dumps({
1047
  "type": "screenshot",
 
1057
  sess["last_bg_hash"] = _frame_hash(result["data"])
1058
 
1059
  except Exception as e:
1060
+ print(f"[ws:{user_id}] init error: {e}")
1061
 
1062
  try:
1063
  while True:
1064
  raw = await ws.receive_text()
1065
  try:
1066
  msg = json.loads(raw)
 
1067
  if msg.get("type") == "pong":
1068
  continue
1069
  await handle_action(ws, msg, sess)
 
1072
  except WebSocketDisconnect:
1073
  pass
1074
  except Exception as e:
1075
+ print(f"[ws:{user_id}] error: {e}")
1076
  finally:
1077
  hb_task.cancel()
1078
+ await destroy_user_ws(user_id, ws)
 
 
1079
 
1080
 
1081
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
1083
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1084
 
1085
  @app.get("/screenshot")
1086
+ async def rest_screenshot(user_id: str = "anonymous"):
1087
+ with _display_lock:
1088
+ sess = _user_displays.get(user_id)
1089
+ display = sess["display"] if sess else f":{DISPLAY_BASE}"
1090
+ result = await asyncio.to_thread(capture_with_grid, display, 0.7, 75)
1091
  return JSONResponse({
1092
  "image": result["data"],
1093
  "ts": int(time.time() * 1000),
 
1096
  "mouse_x": result["mouse_x"],
1097
  "mouse_y": result["mouse_y"],
1098
  "has_grid": True,
1099
+ "display": display,
1100
+ "user_id": user_id,
1101
  })
1102
 
1103
  @app.post("/terminal")
1104
  async def rest_terminal(body: dict):
1105
+ user_id = body.get("user_id", "anonymous")
1106
+ with _display_lock:
1107
+ sess = _user_displays.get(user_id)
1108
+ display = sess["display"] if sess else f":{DISPLAY_BASE}"
1109
+ return JSONResponse(await run_cmd_smart(body.get("cmd", ""), display, body.get("timeout", 60)))
1110
 
1111
  @app.get("/health")
1112
  async def health():
1113
+ with _display_lock:
1114
+ n = len(_user_displays)
1115
+ users = [
1116
+ {"user_id": uid, "display": s["display"],
1117
+ "connected": bool(s.get("active_ws"))}
1118
+ for uid, s in _user_displays.items()
1119
+ ]
1120
  return {
1121
  "status": "ok",
1122
+ "version": "v11-per-user-isolation",
1123
  "browser": BROWSER,
1124
+ "active_users": n,
1125
+ "users": users,
 
 
1126
  }
1127
 
1128
 
 
1131
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1132
 
1133
  async def _cleanup_tmp():
1134
+ """ูŠู†ุธู‘ู /tmp ูƒู„ 5 ุฏู‚ุงุฆู‚."""
1135
  while True:
1136
  await asyncio.sleep(300)
1137
  try:
 
1143
  print(f"[cleanup] {e}")
1144
 
1145
 
1146
+ async def _cleanup_idle_sessions():
1147
+ """
1148
+ ูŠูุฒูŠู„ sessions ุงู„ู…ุณุชุฎุฏู…ูŠู† ุบูŠุฑ ุงู„ู†ุดุทูŠู† (ู„ุง ุงุชุตุงู„ ู…ู†ุฐ ุฃูƒุซุฑ ู…ู† ุณุงุนุฉ)
1149
+ ู„ุชุญุฑูŠุฑ ุงู„ู€ displays ูˆุงู„ุฐุงูƒุฑุฉ.
1150
+ """
1151
+ while True:
1152
+ await asyncio.sleep(1800) # ูƒู„ 30 ุฏู‚ูŠู‚ุฉ
1153
+ now = time.time()
1154
+ to_remove = []
1155
+ with _display_lock:
1156
+ for uid, sess in list(_user_displays.items()):
1157
+ if sess.get("active_ws"):
1158
+ continue # ู„ุง ุชุญุฐู sessions ุงู„ู†ุดุทุฉ
1159
+ if now - sess.get("created", now) > 3600: # ุฃูƒุซุฑ ู…ู† ุณุงุนุฉ
1160
+ to_remove.append((uid, sess))
1161
+
1162
+ for uid, sess in to_remove:
1163
+ print(f"[cleanup] ๐Ÿ—‘๏ธ Removing idle session for '{uid}' on {sess['display']}")
1164
+ _kill_proc(sess.get("browser_proc"))
1165
+ _kill_proc(sess.get("xvfb_proc"))
1166
+ _kill_display_processes(sess["display"])
1167
+ with _display_lock:
1168
+ _user_displays.pop(uid, None)
1169
+ try:
1170
+ _display_numbers.discard(int(sess["display"].lstrip(":")))
1171
+ except Exception:
1172
+ pass
1173
+
1174
+
1175
  @app.on_event("startup")
1176
  async def startup():
 
 
 
1177
  asyncio.create_task(_cleanup_tmp())
1178
+ asyncio.create_task(_cleanup_idle_sessions())
1179
+ print("โœ… Z Computer Mode v11 ready โ€” Per-User Isolated Displays")
1180
+ print(f" Display range: :{DISPLAY_BASE} โ†’ :{DISPLAY_MAX}")
1181
+ print(f" Connect: wss://your-space.hf.space/ws?user_id=EMAIL")
1182
 
1183
 
1184
  if __name__ == "__main__":