THEZYZSTUDIO commited on
Commit
113604f
ยท
verified ยท
1 Parent(s): 77fb3c5

Upload computer_mode_v20.py

Browse files
Files changed (1) hide show
  1. computer_mode_v20.py +1247 -0
computer_mode_v20.py ADDED
@@ -0,0 +1,1247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ THE Z AI โ€” Computer Mode Server v20 "Headless-Native"
3
+ =====================================================================================
4
+ ุฅุนุงุฏุฉ ูƒุชุงุจุฉ ูƒุงู…ู„ุฉ ู…ู† ุงู„ุตูุฑ โ€” ุจู†ูŠุฉ ู…ุฎุชู„ูุฉ ุชู…ุงู…ุงู‹ ุนู† v11-v13.
5
+
6
+ โ”€โ”€ ู„ู…ุงุฐุง ุฅุนุงุฏุฉ ูƒุชุงุจุฉ ูƒุงู…ู„ุฉุŸ โ”€โ”€
7
+ ุงู„ุจู†ูŠุฉ ุงู„ู‚ุฏูŠู…ุฉ (v11-v13) ุงุนุชู…ุฏุช ุนู„ู‰:
8
+ Xvfb (X server ูˆู‡ู…ูŠ ู…ู†ูุตู„) + Firefox/Chromium ุนุงุฏูŠ ูŠุฑุณู… ุนู„ูŠู‡ + ุงู„ุชู‚ุงุท
9
+ ุงู„ุดุงุดุฉ ุจุฃุฏุงุฉ ุฎุงุฑุฌูŠุฉ (import/ImageMagickุŒ ุฃูˆ python-xlibุŒ ุฃูˆ xwd) + xdotool
10
+ ู„ูƒู„ ุญุฑูƒุฉ ูุฃุฑุฉ/ูƒูŠุจูˆุฑุฏ.
11
+ ู‡ุฐุง ูŠุนู†ูŠ *ุนู…ู„ูŠุชูŠู† ู…ู†ูุตู„ุชูŠู†* ู„ูƒู„ ู…ุณุชุฎุฏู… (Xvfb + ุงู„ู…ุชุตูุญ) + ุนู…ู„ูŠุฉ subprocess
12
+ ุซุงู„ุซุฉ ู„ูƒู„ ู„ู‚ุทุฉ ุดุงุดุฉ ูˆุงุญุฏุฉุŒ ูˆูƒู„ู‡ุง ุชุชูˆุงุตู„ ุนุจุฑ ุจุฑูˆุชูˆูƒูˆู„ X11 ุงู„ุซู‚ูŠู„. ุนู„ู‰ ุณูŠุฑูุฑ
13
+ ู…ูˆุงุฑุฏ ู…ุญุฏูˆุฏุฉ (HF Free: 2 vCPU/16GB ู…ุดุชุฑูƒุฉ)ุŒ ู‡ุฐุง ูŠุนู†ูŠ ุงุณุชู‡ู„ุงูƒ ุฐุงูƒุฑุฉ/CPU ุฃุนู„ู‰
14
+ ุจูƒุซูŠุฑ ู…ู† ุงู„ู„ุงุฒู…ุŒ ูˆุฃูŠ ุจุทุก ุนุงุจุฑ ููŠ X11 ูŠููุณูŽู‘ุฑ ูƒู€"ูุดู„ ุงู„ุชู‚ุงุท" ููŠููุนู‘ู„ ุฑูŠุณุชุงุฑุช
15
+ ุชู„ู‚ุงุฆูŠ ู„ู„ุฌู„ุณุฉ ูƒุงู…ู„ุฉ โ€” ูˆู‡ุฐุง ุจุงู„ุถุจุท ู…ุง ูƒุงู† ูŠุณุจุจ ุงู„ุฑูŠุณุชุงุฑุช ุงู„ู…ุชูƒุฑุฑ ูˆุงู„ุฃุฎุทุงุก.
16
+
17
+ โ”€โ”€ ุงู„ุจู†ูŠุฉ ุงู„ุฌุฏูŠุฏุฉ v20 โ”€โ”€
18
+ Chromium ุจูˆุถุน "--headless=new" (ุงู„ูˆุถุน ุงู„ุฑุณู…ูŠ ุงู„ุญุฏูŠุซ ู…ู† Google) *ูู‚ุท*.
19
+ ุจุฏูˆู† Xvfb. ุจุฏูˆู† X server ู…ู† ุฃูŠ ู†ูˆุน. Chromium ูŠุฑู†ุฏุฑ ุงู„ุตูุญุฉ ุจุงู„ูƒุงู…ู„ ุฏุงุฎู„ูŠุงู‹
20
+ ููŠ ุงู„ุฐุงูƒุฑุฉ (ู†ูุณ ู…ุญุฑูƒ ุงู„ุฑู†ุฏุฑ ุงู„ุญู‚ูŠู‚ูŠุŒ ุจุฏูˆู† ู†ุงูุฐุฉ ู…ุฑุฆูŠุฉ)ุŒ ูˆู†ุชุญุฏุซ ู…ุนู‡ ุญุตุฑูŠุงู‹
21
+ ุนุจุฑ Chrome DevTools Protocol (CDP) ุนุจุฑ WebSocket ูˆุงุญุฏ ู„ูƒู„ ู…ุณุชุฎุฏู…:
22
+ - Page.captureScreenshot โ†’ ู„ู‚ุทุฉ ุงู„ุดุงุดุฉ (JPEG) ู…ุจุงุดุฑุฉ ู…ู† ุงู„ู…ุชุตูุญ ู†ูุณู‡ุŒ
23
+ ุจุฏูˆู† subprocess ุฎุงุฑุฌูŠุŒ ุจุฏูˆู† ูุญุต "ู‡ู„ ุงู„ุตูˆุฑุฉ
24
+ ุณูˆุฏุงุก" (headless ู„ุง ูŠูู†ุชุฌ ุดุงุดุงุช ุณูˆุฏุงุก ุฃุตู„ุงู‹
25
+ ู„ุฃู†ู‡ ู„ุง ูŠุนุชู…ุฏ ุนู„ู‰ X server ู‚ุฏ ูŠุชุนุทู„).
26
+ - Input.dispatchMouseEvent โ†’ ุญุฑูƒุฉ/ู†ู‚ุฑ/ุณุญุจ ุงู„ูุฃุฑุฉ (ุจุฏู„ xdotool mousemove/click)
27
+ - Input.dispatchKeyEvent โ†’ ูƒุชุงุจุฉ/ุงุฎุชุตุงุฑุงุช ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ (ุจุฏู„ xdotool key/type)
28
+ - Page.navigate โ†’ ูุชุญ ุฑุงุจุท/ุชุจูˆูŠุจ ุฌุฏูŠุฏ
29
+ - Runtime.evaluate โ†’ ู„ุตู‚ ุงู„ู†ุต ุงู„ุนุฑุจูŠ (clipboard ุจุฏูŠู„)ุŒ ุชู†ููŠุฐ ุณูƒุฑุจุช ู…ุณุงุนุฏ
30
+
31
+ ู†ุชูŠุฌุฉ ู‡ุฐุง ุงู„ุชุจุณูŠุท:
32
+ - ุนู…ู„ูŠุฉ ูˆุงุญุฏุฉ ูู‚ุท ู„ูƒู„ ู…ุณุชุฎุฏู… (Chromium) ุจุฏู„ ุนู…ู„ูŠุชูŠู† (Xvfb+ู…ุชุตูุญ).
33
+ - ุงุณุชู‡ู„ุงูƒ ุฐุงูƒุฑุฉ ุฃู‚ู„ ุจุดูƒู„ ู…ู„ุญูˆุธ (ู„ุง X server ู…ู†ูุตู„ ูŠูุจู‚ูŠ framebuffer ููŠ ุงู„ุฐุงูƒุฑุฉ).
34
+ - ู„ุง ูˆุฌูˆุฏ ู„ู…ูู‡ูˆู… "ูุดู„ ุงู„ุชู‚ุงุท ุงู„ุดุงุดุฉ" ุงู„ุฐูŠ ูƒุงู† ูŠููุนู‘ู„ ุฑูŠุณุชุงุฑุช โ€” ู„ูˆ ูุดู„ CDP
35
+ ูุงู„ู…ุดูƒู„ุฉ ุงู„ูˆุญูŠุฏุฉ ุงู„ู…ู…ูƒู†ุฉ ู‡ูŠ ุฃู† Chromium ู†ูุณู‡ ุชูˆู‚ูุŒ ูู†ูุนูŠุฏ ุชุดุบูŠู„ู‡ ู‡ูˆ ูู‚ุท
36
+ (ู„ุง ุญุงุฌุฉ ู„ุฃูŠ "reset" ุฃูˆุณุน ูŠู…ุณุญ ุจุฑูˆูุงูŠู„ุงุช ุฃูˆ Xvfb ู„ุฃู†ู‡ ุบูŠุฑ ู…ูˆุฌูˆุฏ ุฃุตู„ุงู‹).
37
+ - ุฃุฎู ุชุนู‚ูŠุฏุงู‹: ุฃุฏุงุฉ ุชุญูƒู… ูˆุงุญุฏุฉ (CDP) ุจุฏู„ ุฎู„ูŠุท (xdotool + import + xlib + xwd).
38
+
39
+ โ”€โ”€ ู…ุง ุงู„ุฐูŠ ุฃูุฒูŠู„ ู…ู† v13 ูˆู„ู…ุงุฐุง โ”€โ”€
40
+ ุฃุฏูˆุงุช "browser_get_elements / browser_click_element / browser_type_into_element /
41
+ browser_find_element / browser_get_page_text / browser_execute_js / browser_navigate
42
+ (CDP) / browser_scroll_to_element / browser_get_state" โ€” ู‡ุฐู‡ ู„ู… ุชูƒู† ุชูุณุชุฎุฏู… ุฅุทู„ุงู‚ุงู‹
43
+ ู…ู† ุงู„ูˆุงุฌู‡ุฉ ุงู„ุฃู…ุงู…ูŠุฉ (index.html) ุจุนุฏ ูุญุต ุงู„ูƒูˆุฏ ุงู„ูุนู„ูŠุ› ุงู„ูˆุงุฌู‡ุฉ ุชูุฑุณู„ ูู‚ุท:
44
+ screenshot, terminal, mouse_move, mouse_click, mouse_drag, keyboard_type,
45
+ keyboard_hotkey, keyboard_press, scroll, clipboard_write, clipboard_read, paste,
46
+ open_app, open_tab, close_tab, browser_back, browser_forward, browser_search,
47
+ screen_info, reset_computer. ุฅุฒุงู„ุฉ ุงู„ูƒูˆุฏ ุงู„ู…ูŠุช ู‚ู„ู‘ู„ุช ุงู„ุชุนู‚ูŠุฏ ูˆุงู„ู€ WebSocket
48
+ overhead ู„ุฃุฏูˆุงุช ู„ุง ุฃุญุฏ ูŠุณุชุฏุนูŠู‡ุง.
49
+
50
+ โ”€โ”€ ุงู„ุชูˆุงูู‚ ู…ุน ุงู„ูˆุงุฌู‡ุฉ ุงู„ุฃู…ุงู…ูŠุฉ โ”€โ”€
51
+ ุงู„ุจุฑูˆุชูˆูƒูˆู„ (ุฃุณู…ุงุก actionsุŒ ุฃุณู…ุงุก ุญู‚ูˆู„ ุฑุณุงุฆู„ screenshot/terminal_result/
52
+ computer_reset/error/connected/screen_info) ู…ุทุงุจู‚ 100% ู„ู…ุง ุชุชูˆู‚ุนู‡
53
+ index_plus_prompt_enhanced_v2-3.html โ€” ู„ุง ุญุงุฌุฉ ู„ุฃูŠ ุชุนุฏูŠู„ ููŠ ุงู„ูุฑูˆู†ุช ุฅู†ุฏ.
54
+ """
55
+
56
+ import asyncio
57
+ import base64
58
+ import hashlib
59
+ import json
60
+ import os
61
+ import re
62
+ import subprocess
63
+ import threading
64
+ import time
65
+ import urllib.parse
66
+ from typing import Optional
67
+
68
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
69
+ from fastapi.middleware.cors import CORSMiddleware
70
+ from fastapi.responses import JSONResponse, HTMLResponse
71
+ import uvicorn
72
+
73
+ try:
74
+ import websockets as ws_lib
75
+ _WS_LIB_AVAILABLE = True
76
+ except ImportError:
77
+ _WS_LIB_AVAILABLE = False
78
+ print("โš ๏ธ websockets ุบูŠุฑ ู…ุซุจู‘ุชุฉ โ€” pip install websockets (ู…ุทู„ูˆุจุฉ ู„ู‡ุฐุง ุงู„ุฅุตุฏุงุฑ)")
79
+
80
+
81
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
82
+ # โ”€โ”€ ุงู„ุฅุนุฏุงุฏุงุช ุงู„ุนุงู…ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
83
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
84
+
85
+ # ุฃู‚ุตู‰ ุนุฏุฏ ุฌู„ุณุงุช ู…ุชุฒุงู…ู†ุฉ. ุฑู‚ู… ูˆุงู‚ุนูŠ ู„ุณูŠุฑูุฑ ู…ูˆุงุฑุฏ ู…ุญุฏูˆุฏุฉ (HF Free CPU
86
+ # Basic: 2 vCPU/16GB ู…ุดุชุฑูƒุฉ) โ€” ูƒู„ ุฌู„ุณุฉ = ุนู…ู„ูŠุฉ Chromium ูˆุงุญุฏุฉ ูู‚ุท ุงู„ุขู†
87
+ # (ุจุฏู„ Xvfb+ู…ุชุตูุญ ุณุงุจู‚ุงู‹)ุŒ ูุงู„ุญู…ู„ ุงู„ูุนู„ูŠ ู„ูƒู„ ู…ุณุชุฎุฏู… ุฃุฎู ู…ู† ุงู„ู†ุตู ุชู‚ุฑูŠุจุงู‹
88
+ # ู…ู‚ุงุฑู†ุฉ ุจุงู„ุจู†ูŠุฉ ุงู„ู‚ุฏูŠู…ุฉุŒ ู„ูƒู† ู†ุจู‚ูŠ ุฑู‚ู…ุงู‹ ู…ุชุญูุธุงู‹ ุจุฏู„ ุงู„ุงูุชุฑุงุถ ุงู„ู…ุชูุงุฆู„.
89
+ MAX_SESSIONS = 40
90
+
91
+ CDP_PORT_BASE = 9300
92
+ SCREEN_W, SCREEN_H = 1280, 800
93
+
94
+ _sessions_lock = threading.Lock()
95
+ _cdp_port_pool_lock = threading.Lock()
96
+ _used_ports: set = set()
97
+
98
+
99
+ def _detect_chromium() -> str:
100
+ """ูŠูƒุชุดู ุฃูŠ ุชุณู…ูŠุฉ ู„ู„ู…ุชุตูุญ ู…ุชูˆูุฑุฉ ุนู„ู‰ ู‡ุฐุง ุงู„ุณูŠุฑูุฑ."""
101
+ for c in ["chromium-browser", "chromium", "google-chrome-stable", "google-chrome"]:
102
+ r = subprocess.run(["which", c], capture_output=True, text=True)
103
+ if r.returncode == 0 and r.stdout.strip():
104
+ print(f"๐ŸŒ Chromium binary: {c}")
105
+ return c
106
+ print("โŒ ู„ุง ูŠูˆุฌุฏ Chromium/Chrome ุนู„ู‰ ู‡ุฐุง ุงู„ุณูŠุฑูุฑ โ€” ุซุจู‘ุชู‡ ุนุจุฑ: apt-get install chromium")
107
+ return "chromium-browser"
108
+
109
+
110
+ BROWSER_BIN = _detect_chromium()
111
+
112
+
113
+ def _alloc_cdp_port() -> int:
114
+ """ูŠุฎุตุต ู…ู†ูุฐ CDP ุญุฑ ู„ู…ุณุชุฎุฏู… ุฌุฏูŠุฏ."""
115
+ with _cdp_port_pool_lock:
116
+ p = CDP_PORT_BASE
117
+ while p in _used_ports:
118
+ p += 1
119
+ _used_ports.add(p)
120
+ return p
121
+
122
+
123
+ def _release_cdp_port(port: int):
124
+ with _cdp_port_pool_lock:
125
+ _used_ports.discard(port)
126
+
127
+
128
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
129
+ # โ”€โ”€ CDP Session โ€” ุงู„ุงุชุตุงู„ ุงู„ู…ุจุงุดุฑ ุจู€ Chromium ุนุจุฑ DevTools Protocol โ”€โ”€
130
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
131
+
132
+ class CDPSession:
133
+ """
134
+ ุงุชุตุงู„ WebSocket ูˆุงุญุฏ ุจุฌู„ุณุฉ Chromium headless ูˆุงุญุฏุฉ. ูƒู„ ุงู„ุฃูˆุงู…ุฑ
135
+ (screenshot, mouse, keyboard, navigate) ุชู…ุฑ ู…ู† ู‡ู†ุง. ุงุชุตุงู„ ูˆุงุญุฏ ูŠูุนุงุฏ
136
+ ุงุณุชุฎุฏุงู…ู‡ ุทูˆุงู„ ุญูŠุงุฉ ุงู„ุฌู„ุณุฉ ุจุฏู„ ูุชุญ ุงุชุตุงู„ ุฌุฏูŠุฏ ู„ูƒู„ ุฃู…ุฑ.
137
+ """
138
+
139
+ def __init__(self, port: int):
140
+ self.port = port
141
+ self._ws = None
142
+ self._msg_id = 0
143
+ self._pending: dict = {}
144
+ self._recv_task: Optional[asyncio.Task] = None
145
+ self._send_lock = asyncio.Lock()
146
+ self._target_id = None
147
+
148
+ async def connect(self, timeout: float = 6.0) -> bool:
149
+ if not _WS_LIB_AVAILABLE:
150
+ return False
151
+ # ุฌุฑู‘ุจ ุนุฏุฉ ู…ุฑุงุช ู„ุฃู† Chromium ู‚ุฏ ูŠุญุชุงุฌ ู„ุญุธุฉ ุจุนุฏ ุงู„ุฅู‚ู„ุงุน ู‚ุจู„ ูุชุญ ู…ู†ูุฐ CDP
152
+ deadline = time.time() + timeout
153
+ tabs = None
154
+ while time.time() < deadline:
155
+ try:
156
+ import urllib.request
157
+ with urllib.request.urlopen(
158
+ f"http://127.0.0.1:{self.port}/json/list", timeout=1.5
159
+ ) as r:
160
+ tabs = json.loads(r.read())
161
+ break
162
+ except Exception:
163
+ await asyncio.sleep(0.25)
164
+ if tabs is None:
165
+ print(f"[CDP:{self.port}] โŒ Chromium ู„ุง ูŠุณุชุฌูŠุจ ุนู„ู‰ ู…ู†ูุฐ CDP")
166
+ return False
167
+
168
+ ws_url = None
169
+ for t in tabs:
170
+ if t.get("type") == "page":
171
+ ws_url = t.get("webSocketDebuggerUrl")
172
+ self._target_id = t.get("id")
173
+ break
174
+ if not ws_url:
175
+ print(f"[CDP:{self.port}] โŒ ู„ุง ูŠูˆุฌุฏ ุชุจูˆูŠุจ ุตูุญุฉ ู…ุชุงุญ")
176
+ return False
177
+
178
+ try:
179
+ self._ws = await asyncio.wait_for(
180
+ ws_lib.connect(ws_url, ping_interval=15, ping_timeout=10, max_size=64 * 1024 * 1024),
181
+ timeout=timeout,
182
+ )
183
+ except Exception as e:
184
+ print(f"[CDP:{self.port}] โŒ ูุดู„ ุงุชุตุงู„ WebSocket: {e}")
185
+ return False
186
+
187
+ self._recv_task = asyncio.create_task(self._recv_loop())
188
+
189
+ # ูุนู‘ู„ ุงู„ู†ุท๏ฟฝ๏ฟฝู‚ุงุช ุงู„ู…ุทู„ูˆุจุฉ
190
+ await self.send("Page.enable", timeout=5.0)
191
+ await self.send("Runtime.enable", timeout=5.0)
192
+ await self.send("DOM.enable", timeout=5.0)
193
+ # ุงุถุจุท ุญุฌู… ุงู„ุดุงุดุฉ ุงู„ุงูุชุฑุงุถูŠ ุนุจุฑ Emulation (headless ุจุฏูˆู† ู†ุงูุฐุฉ ูุนู„ูŠุฉุŒ
194
+ # ู‡ุฐุง ูŠุญุฏุฏ ุฃุจุนุงุฏ ุงู„ุฑู†ุฏุฑ ุงู„ุฏุงุฎู„ูŠ)
195
+ await self.send("Emulation.setDeviceMetricsOverride", {
196
+ "width": SCREEN_W, "height": SCREEN_H,
197
+ "deviceScaleFactor": 1, "mobile": False,
198
+ }, timeout=5.0)
199
+ print(f"[CDP:{self.port}] โœ… ู…ุชุตู„ ุจุฌู„ุณุฉ Chromium")
200
+ return True
201
+
202
+ async def _recv_loop(self):
203
+ try:
204
+ async for raw in self._ws:
205
+ try:
206
+ msg = json.loads(raw)
207
+ except Exception:
208
+ continue
209
+ mid = msg.get("id")
210
+ if mid is not None and mid in self._pending:
211
+ fut = self._pending.pop(mid)
212
+ if not fut.done():
213
+ fut.set_result(msg)
214
+ except Exception:
215
+ pass
216
+ finally:
217
+ for fut in self._pending.values():
218
+ if not fut.done():
219
+ fut.set_exception(ConnectionError("CDP disconnected"))
220
+ self._pending.clear()
221
+ self._ws = None
222
+
223
+ async def send(self, method: str, params: dict = None, timeout: float = 10.0):
224
+ if not self._ws:
225
+ return None
226
+ async with self._send_lock:
227
+ self._msg_id += 1
228
+ mid = self._msg_id
229
+ fut = asyncio.get_event_loop().create_future()
230
+ self._pending[mid] = fut
231
+ try:
232
+ await self._ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
233
+ return await asyncio.wait_for(fut, timeout=timeout)
234
+ except Exception:
235
+ self._pending.pop(mid, None)
236
+ return None
237
+
238
+ @property
239
+ def alive(self) -> bool:
240
+ return self._ws is not None
241
+
242
+ async def close(self):
243
+ if self._recv_task:
244
+ self._recv_task.cancel()
245
+ if self._ws:
246
+ try:
247
+ await self._ws.close()
248
+ except Exception:
249
+ pass
250
+ self._ws = None
251
+
252
+
253
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
254
+ # โ”€โ”€ ุชุดุบูŠู„/ุฅูŠู‚ุงู ุนู…ู„ูŠุฉ Chromium headless โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
255
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
256
+
257
+ def _profile_dir(user_id: str) -> str:
258
+ safe = re.sub(r"[^a-zA-Z0-9_.-]", "_", user_id) or "anon"
259
+ d = os.path.expanduser(f"~/.zai_chromium_profiles/{safe}")
260
+ os.makedirs(d, exist_ok=True)
261
+ return d
262
+
263
+
264
+ def _start_chromium(port: int, user_id: str) -> "subprocess.Popen | None":
265
+ """
266
+ ูŠูุดุบู‘ู„ Chromium ุจูˆุถุน headless=new ุนู„ู‰ ู…ู†ูุฐ CDP ู…ุฎุตุต. ู„ุง XvfbุŒ ู„ุง DISPLAYุŒ
267
+ ู„ุง ู†ุงูุฐุฉ โ€” ุงู„ุฑู†ุฏุฑ ุจุงู„ูƒุงู…ู„ ุฏุงุฎู„ูŠ (in-process) ู„ู…ุญุฑูƒ Chromium ู†ูุณู‡.
268
+ """
269
+ profile = _profile_dir(user_id)
270
+ # ู†ุธู‘ู ุฃูŠ Chromium ู‚ุฏูŠู… ู…ุชุนู„ู‚ ุจู†ูุณ ุงู„ู…ู†ูุฐ (ุฌู„ุณุฉ ุณุงุจู‚ุฉ ู„ู… ุชูุบู„ู‚ ู†ุธูŠูุงู‹)
271
+ try:
272
+ subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={port}"],
273
+ capture_output=True, timeout=3)
274
+ time.sleep(0.2)
275
+ except Exception:
276
+ pass
277
+
278
+ cmd = [
279
+ BROWSER_BIN,
280
+ "--headless=new", # ุงู„ูˆุถุน ุงู„ุฑุณู…ูŠ ุงู„ุญุฏูŠุซ โ€” ู„ุง ุญุงุฌุฉ ู„ุฃูŠ display
281
+ f"--remote-debugging-port={port}",
282
+ "--remote-debugging-address=127.0.0.1",
283
+ "--disable-gpu",
284
+ "--no-sandbox",
285
+ "--disable-dev-shm-usage",
286
+ "--disable-extensions",
287
+ "--disable-notifications",
288
+ "--disable-popup-blocking",
289
+ "--disable-translate",
290
+ "--disable-sync",
291
+ "--disable-background-timer-throttling",
292
+ "--disable-backgrounding-occluded-windows",
293
+ "--disable-renderer-backgrounding",
294
+ "--disable-features=Translate,BackForwardCache",
295
+ "--no-first-run",
296
+ "--no-default-browser-check",
297
+ "--mute-audio",
298
+ "--hide-scrollbars",
299
+ f"--window-size={SCREEN_W},{SCREEN_H}",
300
+ f"--user-data-dir={profile}",
301
+ "--js-flags=--max-old-space-size=256", # ุณู‚ู ุฐุงูƒุฑุฉ JS โ€” ูŠู…ู†ุน ุชุจูˆูŠุจ ุซู‚ูŠู„ ู…ู† ุงุณุชู‡ู„ุงูƒ ูƒู„ ุงู„ุฑุงู…
302
+ "about:blank",
303
+ ]
304
+ try:
305
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
306
+ print(f"[chromium] ๐Ÿš€ ุจุฏุฃ (PID {proc.pid}) ุนู„ู‰ ู…ู†ูุฐ CDP {port} โ€” headless=new")
307
+ return proc
308
+ except FileNotFoundError:
309
+ print(f"[chromium] โŒ ู„ู… ูŠูุนุซุฑ ุนู„ู‰ ุงู„ู…ู„ู ุงู„ุชู†ููŠุฐูŠ: {BROWSER_BIN}")
310
+ return None
311
+ except Exception as e:
312
+ print(f"[chromium] โŒ ูุดู„ ุงู„ุชุดุบูŠู„: {e}")
313
+ return None
314
+
315
+
316
+ def _kill_chromium(proc: "subprocess.Popen | None", port: int):
317
+ if proc:
318
+ try:
319
+ proc.kill()
320
+ proc.wait(timeout=3)
321
+ except Exception:
322
+ pass
323
+ try:
324
+ subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={port}"],
325
+ capture_output=True, timeout=3)
326
+ except Exception:
327
+ pass
328
+
329
+
330
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
331
+ # โ”€โ”€ ุฅุฏุงุฑุฉ ุงู„ุฌู„ุณุงุช (ู…ุณุชุฎุฏู… โ†’ CDPSession + Chromium process) โ”€โ”€โ”€โ”€โ”€
332
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
333
+
334
+ # user_id โ†’ {
335
+ # "port": int, "proc": Popen, "cdp": CDPSession|None,
336
+ # "active_ws": WebSocket|None, "created": float,
337
+ # "consecutive_failures": int, "last_frame_hash": str,
338
+ # "last_mouse_x": int, "last_mouse_y": int,
339
+ # }
340
+ _sessions: dict = {}
341
+
342
+
343
+ async def get_or_create_session(user_id: str, ws: WebSocket) -> dict:
344
+ with _sessions_lock:
345
+ sess = _sessions.get(user_id)
346
+ if sess:
347
+ sess["active_ws"] = ws
348
+ print(f"[session] ๐Ÿ” ุฅุนุงุฏุฉ ุงุชุตุงู„ '{user_id}' ุนู„ู‰ ู…ู†ูุฐ {sess['port']}")
349
+ return sess
350
+
351
+ if len(_sessions) >= MAX_SESSIONS:
352
+ # ุฃุนุฏ ุงุณุชุฎุฏุงู… ุฃู‚ุฏู… ุฌู„ุณุฉ ุบูŠุฑ ู…ุชุตู„ุฉ ุจุฏู„ ุฑูุถ ุงู„ู…ุณุชุฎุฏู… ุงู„ุฌุฏูŠุฏ
353
+ idle = [(uid, s) for uid, s in _sessions.items() if not s.get("active_ws")]
354
+ if idle:
355
+ oldest_uid = min(idle, key=lambda t: t[1]["created"])[0]
356
+ old = _sessions.pop(oldest_uid)
357
+ _kill_chromium(old.get("proc"), old["port"])
358
+ _release_cdp_port(old["port"])
359
+ print(f"[session] โ™ป๏ธ ุฃูุฒูŠู„ุช ุฌู„ุณุฉ ุฎุงู…ู„ุฉ ู‚ุฏูŠู…ุฉ '{oldest_uid}' ู„ุฅูุณุงุญ ุงู„ู…ุฌุงู„")
360
+
361
+ port = _alloc_cdp_port()
362
+ sess = {
363
+ "user_id": user_id,
364
+ "port": port,
365
+ "proc": None,
366
+ "cdp": None,
367
+ "active_ws": ws,
368
+ "created": time.time(),
369
+ "consecutive_failures": 0,
370
+ "last_frame_hash": "",
371
+ "last_mouse_x": SCREEN_W // 2,
372
+ "last_mouse_y": SCREEN_H // 2,
373
+ "shot_lock": asyncio.Lock(),
374
+ }
375
+ _sessions[user_id] = sess
376
+ print(f"[session] โœ… ู…ุณุชุฎุฏู… ุฌุฏูŠุฏ '{user_id}' โ†’ ู…ู†ูุฐ CDP {port}")
377
+ return sess
378
+
379
+
380
+ async def ensure_browser_running(sess: dict) -> bool:
381
+ """ูŠุชุฃูƒุฏ ุฃู† Chromium + ุงุชุตุงู„ CDP ูŠุนู…ู„ุงู† ู„ู‡ุฐู‡ ุงู„ุฌู„ุณุฉ. ูŠูุดุบู‘ู„ู‡ู…ุง ุฅุฐุง ู„ุฒู…."""
382
+ if sess.get("cdp") and sess["cdp"].alive:
383
+ return True
384
+
385
+ if not sess.get("proc") or sess["proc"].poll() is not None:
386
+ proc = await asyncio.to_thread(_start_chromium, sess["port"], sess["user_id"])
387
+ sess["proc"] = proc
388
+ if proc is None:
389
+ return False
390
+
391
+ cdp = CDPSession(sess["port"])
392
+ ok = await cdp.connect(timeout=8.0)
393
+ if ok:
394
+ sess["cdp"] = cdp
395
+ return True
396
+ return False
397
+
398
+
399
+ async def reset_session(user_id: str) -> bool:
400
+ """ูŠูุนูŠุฏ ุชุดุบูŠู„ Chromium ุจุงู„ูƒุงู…ู„ ู„ู‡ุฐุง ุงู„ู…ุณุชุฎุฏู… โ€” ุงู„ุฅุตู„ุงุญ ุงู„ุดุงู…ู„ ุงู„ูˆุญูŠุฏ ุงู„ู…ุทู„ูˆุจ ุงู„ุขู†ุŒ
401
+ ุจู…ุง ุฃู†ู‡ ู„ุง ูŠูˆุฌุฏ Xvfb ุฃูˆ ู…ู„ูุงุช ุฌู„ุณุฉ X ู…ู†ูุตู„ุฉ ูŠุฌุจ ุชู†ุธูŠูู‡ุง."""
402
+ with _sessions_lock:
403
+ sess = _sessions.get(user_id)
404
+ if not sess:
405
+ return False
406
+ proc, port = sess.get("proc"), sess["port"]
407
+ old_cdp = sess.get("cdp")
408
+
409
+ if old_cdp:
410
+ await old_cdp.close()
411
+ await asyncio.to_thread(_kill_chromium, proc, port)
412
+
413
+ new_proc = await asyncio.to_thread(_start_chromium, port, user_id)
414
+ new_cdp = CDPSession(port)
415
+ ok = await new_cdp.connect(timeout=8.0) if new_proc else False
416
+
417
+ with _sessions_lock:
418
+ if user_id in _sessions:
419
+ _sessions[user_id]["proc"] = new_proc
420
+ _sessions[user_id]["cdp"] = new_cdp if ok else None
421
+ _sessions[user_id]["consecutive_failures"] = 0
422
+ _sessions[user_id]["last_frame_hash"] = ""
423
+ print(f"[reset] {'โœ…' if ok else 'โŒ'} ุฅุนุงุฏุฉ ุถุจุท '{user_id}' ุนู„ู‰ ู…ู†ูุฐ {port}")
424
+ return ok
425
+
426
+
427
+ async def destroy_session_ws(user_id: str, ws: WebSocket):
428
+ """ุนู†ุฏ ุงู†ู‚ุทุงุน WebSocket โ€” ู†ูุจู‚ูŠ Chromium ุดุบุงู„ุงู‹ (ุงู„ุฌู„ุณุฉ ู…ุญููˆุธุฉ) ูˆูู‚ุท ู†ุฒูŠู„ ุงู„ุงุชุตุงู„."""
429
+ with _sessions_lock:
430
+ sess = _sessions.get(user_id)
431
+ if sess and sess.get("active_ws") is ws:
432
+ sess["active_ws"] = None
433
+ print(f"[session] ๐Ÿ“ด '{user_id}' ู‚ุทุน ุงู„ุงุชุตุงู„ (ุงู„ุฌู„ุณุฉ ู…ุญููˆุธุฉ)")
434
+
435
+
436
+ async def destroy_session_full(user_id: str):
437
+ with _sessions_lock:
438
+ sess = _sessions.pop(user_id, None)
439
+ if not sess:
440
+ return
441
+ if sess.get("cdp"):
442
+ await sess["cdp"].close()
443
+ await asyncio.to_thread(_kill_chromium, sess.get("proc"), sess["port"])
444
+ _release_cdp_port(sess["port"])
445
+
446
+
447
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
448
+ # โ”€โ”€ ู„ู‚ุทุฉ ุงู„ุดุงุดุฉ โ€” ุนุจุฑ CDP ู…ุจุงุดุฑุฉ (ุจุฏูˆู† subprocess ุฎุงุฑุฌูŠ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
449
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
450
+
451
+ async def cdp_screenshot(sess: dict, quality: int = 75) -> "tuple[str, int, int] | None":
452
+ """
453
+ ูŠู„ุชู‚ุท ู„ู‚ุทุฉ ุดุงุดุฉ ุนุจุฑ Page.captureScreenshot โ€” ุฃู…ุฑ CDP ูˆุงุญุฏ ูŠุฑุฌุน JPEG
454
+ base64 ุฌุงู‡ุฒ ู…ุจุงุดุฑุฉ ู…ู† ู…ุญุฑูƒ ุงู„ุฑู†ุฏุฑ ุงู„ุฏุงุฎู„ูŠ ู„ู€ Chromium. ู„ุง ูŠูˆุฌุฏ X server
455
+ ูŠู…ูƒู† ุฃู† "ูŠุชุฌู…ุฏ"ุŒ ู„ุง ุญุงุฌุฉ ู„ูุญุต "ู‡ู„ ุงู„ุตูˆุฑุฉ ุณูˆุฏุงุก"ุŒ ูˆู„ุง ุนู…ู„ูŠุฉ subprocess
456
+ ุฎุงุฑุฌูŠุฉ (import/xwd) ู‚ุฏ ุชูุดู„ ุจุณุจุจ ุฃุฏูˆุงุช ู…ูู‚ูˆุฏุฉ ุนู„ู‰ ุงู„ุณูŠุฑูุฑ.
457
+ ูŠูุนูŠุฏ (base64_jpeg, width, height) ุฃูˆ None ุนู†ุฏ ุงู„ูุดู„.
458
+ """
459
+ cdp = sess.get("cdp")
460
+ if not cdp or not cdp.alive:
461
+ return None
462
+ result = await cdp.send("Page.captureScreenshot", {
463
+ "format": "jpeg", "quality": quality, "fromSurface": True, "captureBeyondViewport": False,
464
+ }, timeout=8.0)
465
+ if not result:
466
+ return None
467
+ data = result.get("result", {}).get("data")
468
+ if not data:
469
+ return None
470
+ return data, SCREEN_W, SCREEN_H
471
+
472
+
473
+ def _draw_mouse_marker_on_jpeg(b64_jpeg: str, mx: int, my: int) -> str:
474
+ """ูŠุฑุณู… ุนู„ุงู…ุฉ ุตุบูŠุฑุฉ ู„ู…ูˆู‚ุน ุงู„ูุฃุฑุฉ ููˆู‚ ุงู„ุตูˆุฑุฉ (ู…ุทู„ูˆุจุฉ ุจุตุฑูŠุงู‹ ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ
475
+ ู„ูŠุฑู‰ ุฃูŠู† ุงู„ู…ุคุดุฑ). ุงู„ุนู…ู„ูŠุฉ ุฎููŠูุฉ (ุฑุณู… ูู‚ุทุŒ ุจุฏูˆู† resize) ูุชูุจู‚ูŠ ุฒู…ู† ูƒู„ ู„ู‚ุทุฉ ู…ู†ุฎูุถุงู‹."""
476
+ try:
477
+ from PIL import Image, ImageDraw
478
+ import io as _io
479
+ raw = base64.b64decode(b64_jpeg)
480
+ img = Image.open(_io.BytesIO(raw)).convert("RGB")
481
+ draw = ImageDraw.Draw(img)
482
+ r = 9
483
+ draw.ellipse([(mx - r, my - r), (mx + r, my + r)], outline=(255, 60, 60), width=2)
484
+ draw.line([(mx - 14, my), (mx + 14, my)], fill=(255, 60, 60), width=1)
485
+ draw.line([(mx, my - 14), (mx, my + 14)], fill=(255, 60, 60), width=1)
486
+ buf = _io.BytesIO()
487
+ img.save(buf, format="JPEG", quality=80, optimize=True)
488
+ return base64.b64encode(buf.getvalue()).decode()
489
+ except Exception:
490
+ return b64_jpeg # ู„ูˆ ูุดู„ ุงู„ุฑุณู… ู„ุฃูŠ ุณุจุจุŒ ุฃุนุฏ ุงู„ุตูˆุฑุฉ ุงู„ุฃุตู„ูŠุฉ ุจุฏู„ ุงู„ูุดู„ ุงู„ูƒุงู…ู„
491
+
492
+
493
+ def _grid_variant(b64_jpeg: str, grid_step: int, color: tuple, label_every: int) -> str:
494
+ """
495
+ ูŠู†ุชุฌ ู†ุณุฎุฉ ู…ู† ุงู„ุตูˆุฑุฉ ู…ุน ุดุจูƒุฉ ุฅุญุฏุงุซูŠุงุช ู…ุฑุณูˆู…ุฉ ููˆู‚ู‡ุง โ€” ุชุณุงุนุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ
496
+ ุนู„ู‰ ุชุญุฏูŠุฏ ุฅุญุฏุงุซูŠุงุช ุฏู‚ูŠู‚ุฉ ู„ู„ู†ู‚ุฑ. ู†ูุณ ููƒุฑุฉ v13 ู„ูƒู† ู…ุจู†ูŠุฉ ููˆู‚ JPEG ุงู„ุฌุงู‡ุฒ ู…ู†
497
+ CDP ู…ุจุงุดุฑุฉ ุจุฏู„ ุฅุนุงุฏุฉ ุงู„ุฑุณู… ู…ู† raw pixels.
498
+ """
499
+ try:
500
+ from PIL import Image, ImageDraw
501
+ import io as _io
502
+ raw = base64.b64decode(b64_jpeg)
503
+ img = Image.open(_io.BytesIO(raw)).convert("RGB")
504
+ draw = ImageDraw.Draw(img, "RGBA")
505
+ w, h = img.size
506
+ major = (*color, 130)
507
+ minor = (*color, 40)
508
+ x = grid_step
509
+ while x < w:
510
+ is_major = x % label_every == 0
511
+ draw.line([(x, 0), (x, h)], fill=major if is_major else minor, width=1)
512
+ if is_major:
513
+ draw.rectangle([(x + 1, 2), (x + 34, 15)], fill=(0, 0, 0, 175))
514
+ draw.text((x + 2, 3), str(x), fill=color)
515
+ x += grid_step
516
+ y = grid_step
517
+ while y < h:
518
+ is_major = y % label_every == 0
519
+ draw.line([(0, y), (w, y)], fill=major if is_major else minor, width=1)
520
+ if is_major:
521
+ draw.rectangle([(2, y + 1), (38, y + 14)], fill=(0, 0, 0, 175))
522
+ draw.text((3, y + 2), str(y), fill=color)
523
+ y += grid_step
524
+ final = img.convert("RGB")
525
+ buf = _io.BytesIO()
526
+ final.save(buf, format="JPEG", quality=78, optimize=True)
527
+ return base64.b64encode(buf.getvalue()).decode()
528
+ except Exception:
529
+ return b64_jpeg
530
+
531
+
532
+ async def capture_full(sess: dict, quality: int = 75) -> dict:
533
+ """
534
+ ูŠู„ุชู‚ุท ู„ู‚ุทุฉ ูˆุงุญุฏุฉ ูˆูŠู†ุชุฌ ู…ู†ู‡ุง: ู†ุณุฎุฉ ู†ุธูŠูุฉ + ู†ุณุฎุฉ ุจุนู„ุงู…ุฉ ุงู„ูุฃุฑุฉ + ุดุจูƒุชุงู†
535
+ ุฅุญุฏุงุซูŠุงุช (ุนุงุฏูŠุฉ ูˆ๏ฟฝ๏ฟฝู‚ูŠู‚ุฉ) โ€” ุจู†ูุณ ุชู†ุณูŠู‚ ุญู‚ูˆู„ ุงู„ุฑุฏ ุงู„ุฐูŠ ุชุชูˆู‚ุนู‡ ุงู„ูˆุงุฌู‡ุฉ ุงู„ุฃู…ุงู…ูŠุฉ
536
+ (data, data_grid, data_grid2, data_grid3, width, height, mouse_x, mouse_y).
537
+ """
538
+ shot = await cdp_screenshot(sess, quality)
539
+ if not shot:
540
+ return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "",
541
+ "width": SCREEN_W, "height": SCREEN_H,
542
+ "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0),
543
+ "is_placeholder": True}
544
+
545
+ raw_b64, w, h = shot
546
+ mx, my = sess.get("last_mouse_x", w // 2), sess.get("last_mouse_y", h // 2)
547
+
548
+ def _build():
549
+ clean = _draw_mouse_marker_on_jpeg(raw_b64, mx, my)
550
+ grid1 = _grid_variant(clean, 50, (0, 255, 180), 100)
551
+ grid2 = _grid_variant(clean, 20, (255, 60, 60), 60)
552
+ return clean, grid1, grid2
553
+
554
+ clean, grid1, grid2 = await asyncio.to_thread(_build)
555
+ return {
556
+ "data": clean, "data_grid": grid1, "data_grid2": grid2, "data_grid3": grid2,
557
+ "width": w, "height": h, "mouse_x": mx, "mouse_y": my,
558
+ "is_placeholder": False,
559
+ }
560
+
561
+
562
+ def _frame_hash(data: str) -> str:
563
+ return hashlib.md5(data.encode()).hexdigest()
564
+
565
+
566
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
567
+ # โ”€โ”€ ูุฃุฑุฉ/ูƒูŠุจูˆุฑุฏ โ€” ุนุจุฑ CDP Input domain (ุจุฏูˆู† xdotool/X server) โ”€โ”€
568
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
569
+
570
+ async def cdp_mouse_move(sess: dict, x: int, y: int):
571
+ cdp = sess.get("cdp")
572
+ if not cdp or not cdp.alive:
573
+ return
574
+ await cdp.send("Input.dispatchMouseEvent", {
575
+ "type": "mouseMoved", "x": x, "y": y,
576
+ }, timeout=4.0)
577
+ sess["last_mouse_x"], sess["last_mouse_y"] = x, y
578
+
579
+
580
+ async def cdp_mouse_click(sess: dict, x: int, y: int, button: str = "left", double: bool = False):
581
+ cdp = sess.get("cdp")
582
+ if not cdp or not cdp.alive:
583
+ return
584
+ await cdp_mouse_move(sess, x, y)
585
+ click_count = 2 if double else 1
586
+ await cdp.send("Input.dispatchMouseEvent", {
587
+ "type": "mousePressed", "x": x, "y": y, "button": button, "clickCount": click_count,
588
+ }, timeout=4.0)
589
+ await asyncio.sleep(0.04)
590
+ await cdp.send("Input.dispatchMouseEvent", {
591
+ "type": "mouseReleased", "x": x, "y": y, "button": button, "clickCount": click_count,
592
+ }, timeout=4.0)
593
+
594
+
595
+ async def cdp_mouse_drag(sess: dict, x1: int, y1: int, x2: int, y2: int, steps: int = 8):
596
+ cdp = sess.get("cdp")
597
+ if not cdp or not cdp.alive:
598
+ return
599
+ await cdp_mouse_move(sess, x1, y1)
600
+ await cdp.send("Input.dispatchMouseEvent", {
601
+ "type": "mousePressed", "x": x1, "y": y1, "button": "left", "clickCount": 1,
602
+ }, timeout=4.0)
603
+ for i in range(1, steps + 1):
604
+ ix = x1 + (x2 - x1) * i // steps
605
+ iy = y1 + (y2 - y1) * i // steps
606
+ await cdp.send("Input.dispatchMouseEvent", {
607
+ "type": "mouseMoved", "x": ix, "y": iy, "button": "left",
608
+ }, timeout=3.0)
609
+ await asyncio.sleep(0.02)
610
+ await cdp.send("Input.dispatchMouseEvent", {
611
+ "type": "mouseReleased", "x": x2, "y": y2, "button": "left", "clickCount": 1,
612
+ }, timeout=4.0)
613
+ sess["last_mouse_x"], sess["last_mouse_y"] = x2, y2
614
+
615
+
616
+ async def cdp_scroll(sess: dict, x: int, y: int, delta_y: int):
617
+ cdp = sess.get("cdp")
618
+ if not cdp or not cdp.alive:
619
+ return
620
+ await cdp.send("Input.dispatchMouseEvent", {
621
+ "type": "mouseWheel", "x": x, "y": y, "deltaX": 0, "deltaY": delta_y,
622
+ }, timeout=4.0)
623
+
624
+
625
+ # ุฎุฑูŠุทุฉ ุฃุณู…ุงุก ู…ูุงุชูŠุญ ุดุงุฆุนุฉ โ†’ ุฃูƒูˆุงุฏ CDP (windowsVirtualKeyCode + key)
626
+ _KEY_MAP = {
627
+ "Return": (13, "Enter"), "Enter": (13, "Enter"),
628
+ "Tab": (9, "Tab"), "Escape": (27, "Escape"), "BackSpace": (8, "Backspace"),
629
+ "Delete": (46, "Delete"), "Left": (37, "ArrowLeft"), "Right": (39, "ArrowRight"),
630
+ "Up": (38, "ArrowUp"), "Down": (40, "ArrowDown"), "Home": (36, "Home"), "End": (35, "End"),
631
+ "space": (32, " "), "F5": (116, "F5"),
632
+ }
633
+
634
+
635
+ async def cdp_key_press(sess: dict, key_name: str):
636
+ cdp = sess.get("cdp")
637
+ if not cdp or not cdp.alive:
638
+ return
639
+ code, key = _KEY_MAP.get(key_name, (0, key_name))
640
+ params = {"type": "keyDown", "key": key}
641
+ if code:
642
+ params["windowsVirtualKeyCode"] = code
643
+ params["nativeVirtualKeyCode"] = code
644
+ await cdp.send("Input.dispatchKeyEvent", params, timeout=4.0)
645
+ params["type"] = "keyUp"
646
+ await cdp.send("Input.dispatchKeyEvent", params, timeout=4.0)
647
+
648
+
649
+ async def cdp_hotkey(sess: dict, keys: list):
650
+ """ุงุฎุชุตุงุฑ ู…ุชุนุฏุฏ ุงู„ู…ูุงุชูŠุญ ู…ุซู„ ['ctrl','l'] ุฃูˆ ['alt','Left']."""
651
+ cdp = sess.get("cdp")
652
+ if not cdp or not cdp.alive:
653
+ return
654
+ modifiers = 0
655
+ mod_map = {"alt": 1, "ctrl": 2, "control": 2, "meta": 4, "shift": 8}
656
+ plain_keys = []
657
+ for k in keys:
658
+ kl = k.lower()
659
+ if kl in mod_map:
660
+ modifiers |= mod_map[kl]
661
+ else:
662
+ plain_keys.append(k)
663
+ for k in plain_keys:
664
+ code, key = _KEY_MAP.get(k, (ord(k[0].upper()) if len(k) == 1 else 0, k))
665
+ await cdp.send("Input.dispatchKeyEvent", {
666
+ "type": "rawKeyDown", "key": key, "modifiers": modifiers,
667
+ "windowsVirtualKeyCode": code, "nativeVirtualKeyCode": code,
668
+ }, timeout=4.0)
669
+ await cdp.send("Input.dispatchKeyEvent", {
670
+ "type": "keyUp", "key": key, "modifiers": modifiers,
671
+ "windowsVirtualKeyCode": code, "nativeVirtualKeyCode": code,
672
+ }, timeout=4.0)
673
+
674
+
675
+ async def cdp_type_text(sess: dict, text: str):
676
+ """
677
+ ูŠูƒุชุจ ู†ุตุงู‹ ุนุจุฑ Input.insertText โ€” ูŠุฏุนู… ูƒู„ ุงู„ู„ุบุงุช (ุนุฑุจูŠ/ุฅู†ุฌู„ูŠุฒูŠ/ูุฑู†ุณูŠ)
678
+ ุจุดูƒู„ ุฃุตู„ูŠ ุนุจุฑ CDP ู†ูุณู‡ุŒ ุจุฏูˆู† ุญุงุฌุฉ ู„ู€ xclip ุฃูˆ ุญูŠู„ุฉ clipboard+paste
679
+ ุงู„ุชูŠ ูƒุงู†ุช ุถุฑูˆุฑูŠุฉ ุณุงุจู‚ุงู‹ ุจุณุจุจ ู‚ูŠูˆุฏ xdotool ู…ุน ุงู„ู†ุตูˆุต ุบูŠุฑ ุงู„ู„ุงุชูŠู†ูŠุฉ.
680
+ """
681
+ cdp = sess.get("cdp")
682
+ if not cdp or not cdp.alive:
683
+ return
684
+ await cdp.send("Input.insertText", {"text": text}, timeout=6.0)
685
+
686
+
687
+ async def cdp_navigate(sess: dict, url: str) -> bool:
688
+ cdp = sess.get("cdp")
689
+ if not cdp or not cdp.alive:
690
+ return False
691
+ result = await cdp.send("Page.navigate", {"url": url}, timeout=15.0)
692
+ return bool(result and "error" not in result.get("result", {}))
693
+
694
+
695
+ async def cdp_clipboard_write(sess: dict, text: str):
696
+ """ูŠู†ุณุฎ ู†ุตุงู‹ ู„ุญุงูุธุฉ afterward-paste ุฏุงุฎู„ูŠุฉ ุนุจุฑ ู…ุชุบูŠู‘ุฑ JS ุจุณูŠุท (ุจุฏูŠู„ ุฎููŠู
697
+ ู„ู€ xclipุŒ ู„ุฃู†ู‡ ู„ุง ูŠูˆุฌุฏ X server ู‡ู†ุง ุฃุตู„ุงู‹ ุชูุจู†ู‰ ุนู„ูŠู‡ ุญุงูุธุฉ ู†ุธุงู… ุญู‚ูŠู‚ูŠุฉ)."""
698
+ cdp = sess.get("cdp")
699
+ if not cdp or not cdp.alive:
700
+ return
701
+ js = f"window.__zclip = {json.dumps(text)};"
702
+ await cdp.send("Runtime.evaluate", {"expression": js}, timeout=4.0)
703
+
704
+
705
+ async def cdp_clipboard_read(sess: dict) -> str:
706
+ cdp = sess.get("cdp")
707
+ if not cdp or not cdp.alive:
708
+ return ""
709
+ result = await cdp.send("Runtime.evaluate", {
710
+ "expression": "window.__zclip || ''", "returnByValue": True,
711
+ }, timeout=4.0)
712
+ if not result:
713
+ return ""
714
+ return result.get("result", {}).get("result", {}).get("value", "") or ""
715
+
716
+
717
+ async def cdp_get_screen_info(sess: dict) -> dict:
718
+ return {"width": SCREEN_W, "height": SCREEN_H,
719
+ "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0)}
720
+
721
+
722
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
723
+ # โ”€โ”€ SafeSearch โ€” ู†ูุณ ู…ู†ุทู‚ v13 ู…ุญููˆุธุงู‹ ูƒู…ุง ู‡ูˆ (ูŠุนู…ู„ ุจุดูƒู„ ุฌูŠุฏ) โ”€โ”€โ”€โ”€
724
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
725
+
726
+ _BING_RE = re.compile(r"https?://(?:www\.)?bing\.com[^\s'\"]*")
727
+ _DDG_RE = re.compile(r"https?://(?:www\.)?duckduckgo\.com[^\s'\"]*")
728
+ _GOOG_RE = re.compile(r"https?://(?:www\.)?google\.[a-z.]+[^\s'\"]*")
729
+
730
+
731
+ def _safe_search(text: str) -> str:
732
+ def _bing(m):
733
+ u = m.group(0)
734
+ return re.sub(r"adlt=\w+", "adlt=strict", u) if "adlt=" in u else u + ("&" if "?" in u else "?") + "adlt=strict"
735
+
736
+ def _ddg(m):
737
+ u = m.group(0)
738
+ return re.sub(r"kp=\d", "kp=1", u) if "kp=" in u else u + ("&" if "?" in u else "?") + "kp=1"
739
+
740
+ def _goog(m):
741
+ u = m.group(0)
742
+ return re.sub(r"safe=\w+", "safe=strict", u) if "safe=" in u else u + ("&" if "?" in u else "?") + "safe=strict"
743
+
744
+ return _GOOG_RE.sub(_goog, _DDG_RE.sub(_ddg, _BING_RE.sub(_bing, text)))
745
+
746
+
747
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
748
+ # โ”€โ”€ Terminal โ€” ุชู†ููŠุฐ ุฃูˆุงู…ุฑ bash (ู…ู†ูุตู„ ุชู…ุงู…ุงู‹ ุนู† Chromium) โ”€โ”€โ”€โ”€โ”€โ”€
749
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
750
+
751
+ _terminal_sem = asyncio.Semaphore(6)
752
+ _PKILL_RE = re.compile(r"\b(pkill|killall)\s+(-9\s+)?-f\s+(['\"]?)([a-zA-Z0-9_./-]+)\3")
753
+
754
+
755
+ def _sanitize_pkill(cmd: str) -> str:
756
+ def _fix(m):
757
+ tool, d9, _q, p = m.group(1), m.group(2) or "", m.group(3), m.group(4)
758
+ sp = f"[{p[0]}]{p[1:]}" if len(p) > 1 else p
759
+ return f"{tool} {d9}-f '{sp}'"
760
+ return _PKILL_RE.sub(_fix, cmd)
761
+
762
+
763
+ async def run_cmd(cmd: str, timeout: int = 60) -> dict:
764
+ cmd = _sanitize_pkill(_safe_search(cmd))
765
+ async with _terminal_sem:
766
+ env = {**os.environ, "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"}
767
+
768
+ def _exec():
769
+ try:
770
+ r = subprocess.run(cmd, shell=True, capture_output=True, text=True,
771
+ timeout=timeout, env=env, executable="/bin/bash")
772
+ return {"stdout": r.stdout[-15000:], "stderr": r.stderr[-3000:], "returncode": r.returncode}
773
+ except subprocess.TimeoutExpired:
774
+ return {"stdout": "", "stderr": f"โฑ๏ธ Timeout {timeout}s", "returncode": -1}
775
+ except Exception as e:
776
+ return {"stdout": "", "stderr": str(e), "returncode": -1}
777
+
778
+ return await asyncio.to_thread(_exec)
779
+
780
+
781
+ def _search_sources(query: str) -> list:
782
+ q = urllib.parse.quote_plus(query)
783
+ return [
784
+ {"name": "DuckDuckGo Instant",
785
+ "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)]\""},
786
+ {"name": "Wikipedia EN",
787
+ "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])\""},
788
+ {"name": "DuckDuckGo HTML",
789
+ "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])]\""},
790
+ {"name": "HackerNews",
791
+ "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])]\""},
792
+ {"name": "Reddit",
793
+ "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])]\""},
794
+ ]
795
+
796
+
797
+ def _result_empty(s: str) -> bool:
798
+ if not s or len(s.strip()) < 15:
799
+ return True
800
+ return "ans: none" in s.lower()
801
+
802
+
803
+ async def run_cmd_smart(cmd: str, timeout: int = 60) -> dict:
804
+ res = await run_cmd(cmd, timeout)
805
+ if res["returncode"] == 0 and not _result_empty(res["stdout"]):
806
+ return res
807
+ is_search = "curl" in cmd and any(x in cmd for x in ["search", "duckduck", "bing", "google", "wikipedia", "reddit"])
808
+ if not is_search:
809
+ return res
810
+ m = re.search(r"[?&]q=([^&'\"\s]+)", cmd)
811
+ query = urllib.parse.unquote(m.group(1).replace("+", " ")).strip() if m else ""
812
+ if len(query) < 3:
813
+ return res
814
+ results = []
815
+ for src in _search_sources(query):
816
+ r2 = await run_cmd(src["cmd"], 20)
817
+ if not _result_empty(r2["stdout"]):
818
+ results.append(f"[{src['name']}]\n{r2['stdout']}")
819
+ if len(results) >= 2:
820
+ break
821
+ if results:
822
+ return {"stdout": "\n\n".join(results)[:15000], "stderr": "", "returncode": 0}
823
+ return res
824
+
825
+
826
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
827
+ # โ”€โ”€ FastAPI App โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
828
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
829
+
830
+ app = FastAPI(title="Z-Computer-Mode v20 โ€” Headless-Native")
831
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
832
+ allow_methods=["*"], allow_headers=["*"])
833
+
834
+
835
+ @app.get("/", response_class=HTMLResponse)
836
+ async def root():
837
+ with _sessions_lock:
838
+ n = len(_sessions)
839
+ rows = "".join(
840
+ f"<tr><td>{uid[:30]}</td><td>{s['port']}</td>"
841
+ f"<td>{'๐ŸŸข ู…ุชุตู„' if s.get('active_ws') else 'โšซ ุบูŠุฑ ู…ุชุตู„'}</td></tr>"
842
+ for uid, s in _sessions.items()
843
+ )
844
+ return f"""<!DOCTYPE html><html><head><meta charset="utf-8">
845
+ <title>Z Computer Mode v20</title>
846
+ <style>body{{background:#080808;color:#00d4aa;font-family:monospace;padding:40px}}
847
+ h1{{color:#4a90d9}}.ok{{color:#00ff88}}.info{{color:#888;font-size:13px}}
848
+ table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #333;padding:6px 10px;font-size:13px}}
849
+ th{{background:#1a1a2e;color:#4a90d9}}</style></head><body>
850
+ <h1>๐Ÿ–ฅ๏ธ THE Z AI โ€” Computer Mode Server v20 (Headless-Native)</h1>
851
+ <p class="ok">โœ… ูŠุนู…ู„ โ€” ุจุฏูˆู† XvfbุŒ Chromium headless=new ูู‚ุท</p>
852
+ <p>๐ŸŒ ุงู„ู…ุชุตูุญ: <b>{BROWSER_BIN}</b></p>
853
+ <p>๐Ÿ‘ฅ ุงู„ุฌู„ุณุงุช ุงู„ู†ุดุทุฉ: <b>{n}</b> / {MAX_SESSIONS}</p>
854
+ <hr style="border-color:#333"><h3>๐Ÿ‘ค ุงู„ุฌู„ุณุงุช</h3>
855
+ <table><tr><th>User ID</th><th>CDP Port</th><th>ุงู„ุญุงู„ุฉ</th></tr>
856
+ {rows or '<tr><td colspan="3" style="text-align:center;color:#666">ู„ุง ุชูˆุฌุฏ ุฌู„ุณุงุช</td></tr>'}
857
+ </table>
858
+ <hr style="border-color:#333">
859
+ <p class="info">Endpoints: /health ยท /ws?user_id=email (WebSocket)</p>
860
+ </body></html>"""
861
+
862
+
863
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
864
+ # โ”€โ”€ ู…ุนุงู„ุฌ ุงู„ุฃูˆุงู…ุฑ โ€” ุจุฑูˆุชูˆูƒูˆู„ ู…ุทุงุจู‚ 100% ู„ู„ูˆุงุฌู‡ุฉ ุงู„ุฃู…ุงู…ูŠุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
865
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
866
+ # ุงู„ุฃูˆุงู…ุฑ ุงู„ู…ุฏุนูˆู…ุฉ (ูƒู„ ู…ุง ุชุฑุณู„ู‡ index_plus_prompt_enhanced_v2-3.html ูุนู„ูŠุงู‹):
867
+ # screenshot, terminal, mouse_move, mouse_click, mouse_drag, keyboard_type,
868
+ # keyboard_hotkey, keyboard_press, scroll, clipboard_write, clipboard_read,
869
+ # paste, open_app, open_tab, close_tab, browser_back, browser_forward,
870
+ # browser_search, screen_info, reset_computer
871
+
872
+ # ุนุชุจุฉ ุฃุจุณุท ุจูƒุซูŠุฑ ู…ู† v13: ุจู…ุง ุฃู† ู„ุง ูŠูˆุฌุฏ X server ู…ู†ูุตู„ ูŠู…ูƒู† ุฃู† "ูŠุชุฌู…ุฏ" ุฏูˆู†
873
+ # ุฃู† ูŠุชูˆู‚ู Chromium ู†ูุณู‡ุŒ ุฃูŠ ูุดู„ ุงู„ุชู‚ุงุท ุงู„ุขู† ูŠุนู†ูŠ ุนู…ู„ูŠุงู‹ ุฃู† Chromium ุชูˆู‚ู ุฃูˆ
874
+ # ุงุชุตุงู„ CDP ุงู†ู‚ุทุน โ€” ูุดู„ุฉ ูˆุงุญุฏุฉ ูƒุงููŠุฉ ู„ู„ุชุฃูƒุฏ ูˆู…ุญุงูˆู„ุฉ ุฅุนุงุฏุฉ ุงู„ุงุชุตุงู„ ู…ุจุงุดุฑุฉ ู‚ุจู„
875
+ # ุงู„ู„ุฌูˆุก ู„ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุงู„ุนู…ู„ูŠุฉ ุจุงู„ูƒุงู…ู„.
876
+ _RECONNECT_THRESHOLD = 1
877
+ _FULL_RESET_THRESHOLD = 3
878
+ _MAX_ROUNDS = 4
879
+
880
+
881
+ async def handle_action(ws: WebSocket, msg: dict, sess: dict):
882
+ action = msg.get("action", "")
883
+ data = msg.get("data", {})
884
+
885
+ async def send(obj):
886
+ try:
887
+ await ws.send_text(json.dumps(obj, ensure_ascii=False))
888
+ except Exception:
889
+ pass
890
+
891
+ async def do_screenshot(label: str = "", auto: bool = False):
892
+ """
893
+ ูŠู„ุชู‚ุท ูˆูŠุฑุณู„ ู„ู‚ุทุฉ ุดุงุดุฉุŒ ู…ุน ุชุนุงููŠ ุชู„ู‚ุงุฆูŠ ุจุณูŠุท:
894
+ ูุดู„ุฉ 1 โ†’ ุฃุนุฏ ุงู„ุงุชุตุงู„ ุจู€ CDP ูู‚ุท (Chromium ุบุงู„ุจุงู‹ ุญูŠุŒ ุงู„ุงุชุตุงู„ ูู‚ุท ุณู‚ุท)
895
+ ูุดู„ุฉ 3 โ†’ ุฃุนุฏ ุชุดุบูŠู„ Chromium ุจุงู„ูƒุงู…ู„ (reset_session)
896
+ ุจุนุฏ ุฐู„ูƒ โ†’ ุฃุฑุณู„ computer_reset ุตุฑุงุญุฉ ู„ู„ุนู…ูŠู„ ุจุฏู„ ุงู„ุตู…ุช
897
+ """
898
+ for _round in range(_MAX_ROUNDS):
899
+ ok_browser = await ensure_browser_running(sess)
900
+ if not ok_browser:
901
+ sess["consecutive_failures"] += 1
902
+ else:
903
+ async with sess["shot_lock"]:
904
+ result = await capture_full(sess, quality=75)
905
+ if result.get("data") and not result.get("is_placeholder"):
906
+ sess["consecutive_failures"] = 0
907
+ fh = _frame_hash(result["data"])
908
+ if not auto or fh != sess.get("last_frame_hash", ""):
909
+ sess["last_frame_hash"] = fh
910
+ await send({
911
+ "type": "screenshot",
912
+ "data": result["data"],
913
+ "data_grid": result.get("data_grid", ""),
914
+ "data_grid2": result.get("data_grid2", ""),
915
+ "data_grid3": result.get("data_grid3", ""),
916
+ "ts": int(time.time() * 1000),
917
+ "auto": auto, "label": label,
918
+ "screen_width": result["width"], "screen_height": result["height"],
919
+ "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"],
920
+ "has_grid": True,
921
+ })
922
+ return
923
+ sess["consecutive_failures"] += 1
924
+
925
+ fails = sess["consecutive_failures"]
926
+ if fails >= _FULL_RESET_THRESHOLD:
927
+ await send({"type": "computer_reset",
928
+ "msg": "๐Ÿ”„ ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุชู„ู‚ุงุฆูŠุฉ ู„ู„ูƒู…ุจูŠูˆุชุฑ ุจุนุฏ ูุดู„ ู…ุชูƒุฑุฑ โ€” ู„ุญุธุงุช ูˆูŠุนูˆุฏ ุทุจูŠุนูŠุงู‹",
929
+ "auto": True})
930
+ await reset_session(sess["user_id"])
931
+ await asyncio.sleep(1.0)
932
+ continue
933
+ elif fails >= _RECONNECT_THRESHOLD:
934
+ # ุฃุนุฏ ุงู„ุงุชุตุงู„ ุจู€ CDP ูู‚ุท (ุฃุฎู ุจูƒุซูŠุฑ ู…ู† ุฅุนุงุฏุฉ ุชุดุบูŠู„ Chromium ุจุงู„ูƒุงู…ู„)
935
+ sess["cdp"] = None
936
+ await asyncio.sleep(0.3)
937
+ continue
938
+ await asyncio.sleep(0.3)
939
+
940
+ # ุงุณุชู†ูุฏู†ุง ูƒู„ ุงู„ู…ุญุงูˆู„ุงุช โ€” ุฃุนู„ู… ุงู„ุนู…ูŠู„ ุตุฑุงุญุฉ ุจุฏู„ ุงู„ุตู…ุช ุงู„ุฃุจุฏูŠ
941
+ await send({"type": "computer_reset",
942
+ "msg": "โš ๏ธ ุงุณุชู…ุฑุช ู…ุดูƒู„ุฉ ุงู„ุงู„ุชู‚ุงุท โ€” ุงู„ุฑุฌุงุก ุทู„ุจ ู„ู‚ุทุฉ ุดุงุดุฉ ุฌุฏูŠุฏุฉ",
943
+ "auto": True})
944
+
945
+ def bg_shot(label: str = "", delay: float = 0.5):
946
+ async def _run():
947
+ await asyncio.sleep(delay)
948
+ await do_screenshot(label, auto=True)
949
+ asyncio.create_task(_run())
950
+
951
+ # โ”€โ”€ reset_computer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
952
+ if action == "reset_computer":
953
+ await send({"type": "ack", "action": "reset_computer", "status": "resetting"})
954
+ await reset_session(sess["user_id"])
955
+ await asyncio.sleep(1.0)
956
+ await do_screenshot("after reset")
957
+ await send({"type": "computer_reset", "msg": "โœ… ุชู… ุฅุนุงุฏุฉ ุถุจุท ุงู„ูƒู…ุจูŠูˆุชุฑ โ€” ุงู„ุดุงุดุฉ ุฌุฏูŠุฏุฉ ุชู…ุงู…ุงู‹"})
958
+ return
959
+
960
+ # โ”€โ”€ screenshot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
961
+ if action == "screenshot":
962
+ _task = asyncio.create_task(do_screenshot("explicit screenshot"))
963
+ def _on_done(t: asyncio.Task):
964
+ exc = t.exception() if not t.cancelled() else None
965
+ if exc:
966
+ print(f"[screenshot-task:{sess['user_id']}] โŒ {exc}")
967
+ _task.add_done_callback(_on_done)
968
+ return
969
+
970
+ # โ”€โ”€ terminal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
971
+ if action == "terminal":
972
+ cmd = data.get("cmd", "")
973
+ if not cmd:
974
+ await send({"type": "terminal_result", "stdout": "", "stderr": "no cmd", "returncode": -1})
975
+ return
976
+ res = await run_cmd_smart(cmd, int(data.get("timeout", 60)))
977
+ await send({"type": "terminal_result", "cmd": cmd, "stdout": res["stdout"],
978
+ "stderr": res.get("stderr", ""), "returncode": res["returncode"]})
979
+ return
980
+
981
+ # โ”€โ”€ ูŠุญุชุงุฌ ุงู„ู…ุชุตูุญ ุดุบุงู„ุงู‹ุŸ ุชุฃูƒุฏ ุฃูˆู„ุงู‹ ู„ุจู‚ูŠุฉ ุงู„ุฃูˆุงู…ุฑ โ”€โ”€
982
+ await ensure_browser_running(sess)
983
+
984
+ if action == "mouse_move":
985
+ x, y = int(data.get("x", 0)), int(data.get("y", 0))
986
+ await cdp_mouse_move(sess, x, y)
987
+ await send({"type": "ack", "action": "mouse_move", "x": x, "y": y})
988
+
989
+ elif action == "mouse_click":
990
+ x, y = int(data.get("x", 0)), int(data.get("y", 0))
991
+ btn = {"left": "left", "middle": "middle", "right": "right"}.get(data.get("button", "left"), "left")
992
+ double = bool(data.get("double", False))
993
+ await cdp_mouse_click(sess, x, y, btn, double)
994
+ await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn})
995
+ bg_shot("after click", delay=0.5)
996
+
997
+ elif action == "mouse_drag":
998
+ x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0))
999
+ x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0))
1000
+ await cdp_mouse_drag(sess, x1, y1, x2, y2)
1001
+ await send({"type": "ack", "action": "mouse_drag"})
1002
+ bg_shot("after drag", delay=0.4)
1003
+
1004
+ elif action == "keyboard_type":
1005
+ text = _safe_search(data.get("text", ""))
1006
+ if text:
1007
+ await cdp_type_text(sess, text)
1008
+ await send({"type": "ack", "action": "keyboard_type", "method": "cdp_input"})
1009
+
1010
+ elif action == "keyboard_hotkey":
1011
+ keys = data.get("keys", [])
1012
+ if keys:
1013
+ await cdp_hotkey(sess, keys)
1014
+ await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys})
1015
+
1016
+ elif action == "keyboard_press":
1017
+ key = data.get("key", "")
1018
+ if key:
1019
+ await cdp_key_press(sess, key)
1020
+ await send({"type": "ack", "action": "keyboard_press"})
1021
+
1022
+ elif action == "scroll":
1023
+ x, y = int(data.get("x", SCREEN_W // 2)), int(data.get("y", SCREEN_H // 2))
1024
+ clicks = max(-5, min(5, int(data.get("clicks", 3))))
1025
+ await cdp_scroll(sess, x, y, delta_y=-clicks * 100)
1026
+ await send({"type": "ack", "action": "scroll", "clicks": clicks})
1027
+
1028
+ elif action == "clipboard_write":
1029
+ text = data.get("text", "")
1030
+ await cdp_clipboard_write(sess, text)
1031
+ await send({"type": "ack", "action": "clipboard_write", "length": len(text)})
1032
+
1033
+ elif action == "clipboard_read":
1034
+ text = await cdp_clipboard_read(sess)
1035
+ await send({"type": "clipboard_content", "text": text})
1036
+
1037
+ elif action == "paste":
1038
+ text = data.get("text", "")
1039
+ if text:
1040
+ await cdp_clipboard_write(sess, text)
1041
+ await cdp_type_text(sess, text)
1042
+ await send({"type": "ack", "action": "paste"})
1043
+
1044
+ elif action == "open_app":
1045
+ # ููŠ ู‡ุฐู‡ ุงู„ุจู†ูŠุฉ Chromium ู†ูุณู‡ ู‡ูˆ "ุงู„ุชุทุจูŠู‚" ุงู„ูˆุญูŠุฏุ› ุฃูŠ ุฃู…ุฑ ูŠุญูˆูŠ
1046
+ # firefox/chromium/chrome ูŠููู‡ู… ูƒุทู„ุจ ูุชุญ ุตูุญุฉ ูุงุฑุบุฉ ุฌุฏูŠุฏุฉ ููŠ ู†ูุณ ุงู„ู…ุชุตูุญ.
1047
+ cmd = _safe_search(data.get("cmd", ""))
1048
+ url_match = re.search(r"https?://\S+", cmd)
1049
+ target = url_match.group(0) if url_match else "about:blank"
1050
+ await cdp_navigate(sess, target)
1051
+ await send({"type": "ack", "action": "open_app", "cmd": cmd})
1052
+ bg_shot("after open_app", delay=1.0)
1053
+
1054
+ elif action == "open_tab":
1055
+ url = _safe_search(data.get("url", "") or "about:blank")
1056
+ await cdp_navigate(sess, url)
1057
+ await send({"type": "ack", "action": "open_tab", "url": url})
1058
+ bg_shot("after open_tab", delay=1.2)
1059
+
1060
+ elif action == "close_tab":
1061
+ await cdp_navigate(sess, "about:blank")
1062
+ await send({"type": "ack", "action": "close_tab"})
1063
+ bg_shot("after close_tab", delay=0.4)
1064
+
1065
+ elif action == "browser_back":
1066
+ cdp = sess.get("cdp")
1067
+ if cdp and cdp.alive:
1068
+ hist = await cdp.send("Page.getNavigationHistory", timeout=4.0)
1069
+ if hist:
1070
+ entries = hist.get("result", {}).get("entries", [])
1071
+ idx = hist.get("result", {}).get("currentIndex", 0)
1072
+ if idx > 0:
1073
+ await cdp.send("Page.navigateToHistoryEntry", {"entryId": entries[idx - 1]["id"]}, timeout=5.0)
1074
+ await send({"type": "ack", "action": "browser_back"})
1075
+ bg_shot("after back", delay=0.6)
1076
+
1077
+ elif action == "browser_forward":
1078
+ cdp = sess.get("cdp")
1079
+ if cdp and cdp.alive:
1080
+ hist = await cdp.send("Page.getNavigationHistory", timeout=4.0)
1081
+ if hist:
1082
+ entries = hist.get("result", {}).get("entries", [])
1083
+ idx = hist.get("result", {}).get("currentIndex", 0)
1084
+ if idx < len(entries) - 1:
1085
+ await cdp.send("Page.navigateToHistoryEntry", {"entryId": entries[idx + 1]["id"]}, timeout=5.0)
1086
+ await send({"type": "ack", "action": "browser_forward"})
1087
+ bg_shot("after forward", delay=0.6)
1088
+
1089
+ elif action == "browser_search":
1090
+ url = _safe_search(data.get("url", "") or data.get("query", ""))
1091
+ if not re.match(r"^https?://", url):
1092
+ url = "https://duckduckgo.com/?q=" + urllib.parse.quote_plus(url)
1093
+ await cdp_navigate(sess, url)
1094
+ await send({"type": "ack", "action": "browser_search"})
1095
+ bg_shot("after search", delay=1.0)
1096
+
1097
+ elif action == "screen_info":
1098
+ info = await cdp_get_screen_info(sess)
1099
+ await send({"type": "screen_info", "width": info["width"], "height": info["height"],
1100
+ "mouse_x": info["mouse_x"], "mouse_y": info["mouse_y"], "browser": BROWSER_BIN})
1101
+
1102
+ else:
1103
+ await send({"type": "error", "msg": f"Unknown action: '{action}'"})
1104
+
1105
+
1106
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1107
+ # โ”€โ”€ WebSocket Endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1108
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1109
+
1110
+ @app.websocket("/ws")
1111
+ async def websocket_endpoint(ws: WebSocket, user_id: str = Query(default="anonymous")):
1112
+ await ws.accept()
1113
+ sess = await get_or_create_session(user_id, ws)
1114
+ await ensure_browser_running(sess)
1115
+
1116
+ async def _heartbeat():
1117
+ while True:
1118
+ await asyncio.sleep(20)
1119
+ try:
1120
+ await ws.send_text(json.dumps({"type": "ping", "ts": int(time.time() * 1000)}))
1121
+ except Exception:
1122
+ break
1123
+
1124
+ hb_task = asyncio.create_task(_heartbeat())
1125
+
1126
+ try:
1127
+ info = await cdp_get_screen_info(sess)
1128
+ await ws.send_text(json.dumps({
1129
+ "type": "connected",
1130
+ "screen_width": info["width"], "screen_height": info["height"],
1131
+ "browser": BROWSER_BIN, "display": f"headless:{sess['port']}",
1132
+ "user_id": user_id, "session_id": id(ws),
1133
+ "msg": f"Z Computer Mode v20 | User: {user_id} | Port: {sess['port']} | "
1134
+ f"Browser: {BROWSER_BIN} (headless=new) | Screen: {info['width']}x{info['height']}",
1135
+ }, ensure_ascii=False))
1136
+
1137
+ # ู„ู‚ุทุฉ ุดุงุดุฉ ุฃูˆู„ูŠุฉ
1138
+ result = await capture_full(sess, quality=72)
1139
+ if result.get("data"):
1140
+ await ws.send_text(json.dumps({
1141
+ "type": "screenshot", "data": result["data"],
1142
+ "data_grid": result.get("data_grid", ""), "data_grid2": result.get("data_grid2", ""),
1143
+ "data_grid3": result.get("data_grid3", ""), "ts": int(time.time() * 1000),
1144
+ "label": "Initial screen", "screen_width": result["width"], "screen_height": result["height"],
1145
+ "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"], "has_grid": True,
1146
+ }, ensure_ascii=False))
1147
+ sess["last_frame_hash"] = _frame_hash(result["data"])
1148
+ except Exception as e:
1149
+ print(f"[ws:{user_id}] ุฎุทุฃ ุนู†ุฏ ุงู„ุจุฏุก: {e}")
1150
+
1151
+ try:
1152
+ while True:
1153
+ raw = await ws.receive_text()
1154
+ try:
1155
+ msg = json.loads(raw)
1156
+ if msg.get("type") == "pong":
1157
+ continue
1158
+ await handle_action(ws, msg, sess)
1159
+ except json.JSONDecodeError:
1160
+ pass
1161
+ except WebSocketDisconnect:
1162
+ pass
1163
+ except Exception as e:
1164
+ print(f"[ws:{user_id}] ุฎุทุฃ: {e}")
1165
+ finally:
1166
+ hb_task.cancel()
1167
+ await destroy_session_ws(user_id, ws)
1168
+
1169
+
1170
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1171
+ # โ”€โ”€ REST Endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1172
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1173
+
1174
+ @app.get("/screenshot")
1175
+ async def rest_screenshot(user_id: str = "anonymous"):
1176
+ with _sessions_lock:
1177
+ sess = _sessions.get(user_id)
1178
+ if not sess:
1179
+ return JSONResponse({"error": "no session for this user_id"}, status_code=404)
1180
+ await ensure_browser_running(sess)
1181
+ result = await capture_full(sess, quality=75)
1182
+ return JSONResponse({
1183
+ "image": result["data"], "image_grid": result.get("data_grid", ""),
1184
+ "image_grid2": result.get("data_grid2", ""), "image_grid3": result.get("data_grid3", ""),
1185
+ "ts": int(time.time() * 1000), "screen_width": result["width"], "screen_height": result["height"],
1186
+ "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"], "has_grid": True,
1187
+ "user_id": user_id,
1188
+ })
1189
+
1190
+
1191
+ @app.post("/terminal")
1192
+ async def rest_terminal(body: dict):
1193
+ return JSONResponse(await run_cmd_smart(body.get("cmd", ""), body.get("timeout", 60)))
1194
+
1195
+
1196
+ @app.get("/health")
1197
+ async def health():
1198
+ with _sessions_lock:
1199
+ n = len(_sessions)
1200
+ users = [
1201
+ {"user_id": uid, "cdp_port": s["port"], "connected": bool(s.get("active_ws")),
1202
+ "browser_alive": bool(s.get("cdp") and s["cdp"].alive)}
1203
+ for uid, s in _sessions.items()
1204
+ ]
1205
+ return {"status": "ok", "version": "v20-headless-native", "browser": BROWSER_BIN,
1206
+ "active_sessions": n, "max_sessions": MAX_SESSIONS, "users": users}
1207
+
1208
+
1209
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1210
+ # โ”€โ”€ Background Cleanup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1211
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1212
+
1213
+ async def _cleanup_idle_sessions():
1214
+ """ูŠูุฒูŠู„ ุฌู„ุณุงุช ุงู„ู…ุณุชุฎุฏู…ูŠู† ุบูŠุฑ ุงู„ู†ุดุทูŠู† (ุจุฏูˆู† ุงุชุตุงู„ WebSocket ู…ู†ุฐ ุฃูƒุซุฑ ู…ู†
1215
+ ุณุงุนุฉ) โ€” ูŠูˆู‚ู Chromium ุงู„ุฎุงุต ุจู‡ู… ูุนู„ูŠุงู‹ ู„ุชุญุฑูŠุฑ ุงู„ุฐุงูƒุฑุฉ."""
1216
+ while True:
1217
+ await asyncio.sleep(1800)
1218
+ now = time.time()
1219
+ with _sessions_lock:
1220
+ idle_uids = [uid for uid, s in _sessions.items()
1221
+ if not s.get("active_ws") and now - s.get("created", now) > 3600]
1222
+ for uid in idle_uids:
1223
+ print(f"[cleanup] ๐Ÿ—‘๏ธ ุฅุฒุงู„ุฉ ุฌู„ุณุฉ ุฎุงู…ู„ุฉ '{uid}'")
1224
+ await destroy_session_full(uid)
1225
+
1226
+
1227
+ @app.on_event("startup")
1228
+ async def startup():
1229
+ asyncio.create_task(_cleanup_idle_sessions())
1230
+ print("โœ… Z Computer Mode v20 ุฌุงู‡ุฒ โ€” Headless-Native (ุจุฏูˆู† Xvfb)")
1231
+ print(f" ุงู„ู…ุชุตูุญ: {BROWSER_BIN} (--headless=new)")
1232
+ print(f" ุฃู‚ุตู‰ ุฌู„ุณุงุช ู…ุชุฒุงู…ู†ุฉ: {MAX_SESSIONS}")
1233
+ print(f" ู†ุทุงู‚ ู…ู†ุงูุฐ CDP: {CDP_PORT_BASE}+")
1234
+ print(f" websockets lib: {'โœ…' if _WS_LIB_AVAILABLE else 'โŒ (pip install websockets)'}")
1235
+ print(" Connect: wss://your-space.hf.space/ws?user_id=EMAIL")
1236
+
1237
+
1238
+ if __name__ == "__main__":
1239
+ port = int(os.environ.get("PORT", 7860))
1240
+ uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info")
1241
+
1242
+
1243
+
1244
+
1245
+
1246
+
1247
+