Spaces:
Paused
Paused
| """ | |
| THE Z AI โ Computer Mode Server v20 "Headless-Native" | |
| ===================================================================================== | |
| ุฅุนุงุฏุฉ ูุชุงุจุฉ ูุงู ูุฉ ู ู ุงูุตูุฑ โ ุจููุฉ ู ุฎุชููุฉ ุชู ุงู ุงู ุนู v11-v13. | |
| โโ ูู ุงุฐุง ุฅุนุงุฏุฉ ูุชุงุจุฉ ูุงู ูุฉุ โโ | |
| ุงูุจููุฉ ุงููุฏูู ุฉ (v11-v13) ุงุนุชู ุฏุช ุนูู: | |
| Xvfb (X server ููู ู ู ููุตู) + Firefox/Chromium ุนุงุฏู ูุฑุณู ุนููู + ุงูุชูุงุท | |
| ุงูุดุงุดุฉ ุจุฃุฏุงุฉ ุฎุงุฑุฌูุฉ (import/ImageMagickุ ุฃู python-xlibุ ุฃู xwd) + xdotool | |
| ููู ุญุฑูุฉ ูุฃุฑุฉ/ููุจูุฑุฏ. | |
| ูุฐุง ูุนูู *ุนู ููุชูู ู ููุตูุชูู* ููู ู ุณุชุฎุฏู (Xvfb + ุงูู ุชุตูุญ) + ุนู ููุฉ subprocess | |
| ุซุงูุซุฉ ููู ููุทุฉ ุดุงุดุฉ ูุงุญุฏุฉุ ููููุง ุชุชูุงุตู ุนุจุฑ ุจุฑูุชูููู X11 ุงูุซููู. ุนูู ุณูุฑูุฑ | |
| ู ูุงุฑุฏ ู ุญุฏูุฏุฉ (HF Free: 2 vCPU/16GB ู ุดุชุฑูุฉ)ุ ูุฐุง ูุนูู ุงุณุชููุงู ุฐุงูุฑุฉ/CPU ุฃุนูู | |
| ุจูุซูุฑ ู ู ุงููุงุฒู ุ ูุฃู ุจุทุก ุนุงุจุฑ ูู X11 ูููุณููุฑ ูู"ูุดู ุงูุชูุงุท" ููููุนูู ุฑูุณุชุงุฑุช | |
| ุชููุงุฆู ููุฌูุณุฉ ูุงู ูุฉ โ ููุฐุง ุจุงูุถุจุท ู ุง ูุงู ูุณุจุจ ุงูุฑูุณุชุงุฑุช ุงูู ุชูุฑุฑ ูุงูุฃุฎุทุงุก. | |
| โโ ุงูุจููุฉ ุงูุฌุฏูุฏุฉ v20 โโ | |
| Chromium ุจูุถุน "--headless=new" (ุงููุถุน ุงูุฑุณู ู ุงูุญุฏูุซ ู ู Google) *ููุท*. | |
| ุจุฏูู Xvfb. ุจุฏูู X server ู ู ุฃู ููุน. Chromium ูุฑูุฏุฑ ุงูุตูุญุฉ ุจุงููุงู ู ุฏุงุฎููุงู | |
| ูู ุงูุฐุงูุฑุฉ (ููุณ ู ุญุฑู ุงูุฑูุฏุฑ ุงูุญููููุ ุจุฏูู ูุงูุฐุฉ ู ุฑุฆูุฉ)ุ ููุชุญุฏุซ ู ุนู ุญุตุฑูุงู | |
| ุนุจุฑ Chrome DevTools Protocol (CDP) ุนุจุฑ WebSocket ูุงุญุฏ ููู ู ุณุชุฎุฏู : | |
| - Page.captureScreenshot โ ููุทุฉ ุงูุดุงุดุฉ (JPEG) ู ุจุงุดุฑุฉ ู ู ุงูู ุชุตูุญ ููุณูุ | |
| ุจุฏูู subprocess ุฎุงุฑุฌูุ ุจุฏูู ูุญุต "ูู ุงูุตูุฑุฉ | |
| ุณูุฏุงุก" (headless ูุง ูููุชุฌ ุดุงุดุงุช ุณูุฏุงุก ุฃุตูุงู | |
| ูุฃูู ูุง ูุนุชู ุฏ ุนูู X server ูุฏ ูุชุนุทู). | |
| - Input.dispatchMouseEvent โ ุญุฑูุฉ/ููุฑ/ุณุญุจ ุงููุฃุฑุฉ (ุจุฏู xdotool mousemove/click) | |
| - Input.dispatchKeyEvent โ ูุชุงุจุฉ/ุงุฎุชุตุงุฑุงุช ููุญุฉ ุงูู ูุงุชูุญ (ุจุฏู xdotool key/type) | |
| - Page.navigate โ ูุชุญ ุฑุงุจุท/ุชุจููุจ ุฌุฏูุฏ | |
| - Runtime.evaluate โ ูุตู ุงููุต ุงูุนุฑุจู (clipboard ุจุฏูู)ุ ุชูููุฐ ุณูุฑุจุช ู ุณุงุนุฏ | |
| ูุชูุฌุฉ ูุฐุง ุงูุชุจุณูุท: | |
| - ุนู ููุฉ ูุงุญุฏุฉ ููุท ููู ู ุณุชุฎุฏู (Chromium) ุจุฏู ุนู ููุชูู (Xvfb+ู ุชุตูุญ). | |
| - ุงุณุชููุงู ุฐุงูุฑุฉ ุฃูู ุจุดูู ู ูุญูุธ (ูุง X server ู ููุตู ููุจูู framebuffer ูู ุงูุฐุงูุฑุฉ). | |
| - ูุง ูุฌูุฏ ูู ูููู "ูุดู ุงูุชูุงุท ุงูุดุงุดุฉ" ุงูุฐู ูุงู ูููุนูู ุฑูุณุชุงุฑุช โ ูู ูุดู CDP | |
| ูุงูู ุดููุฉ ุงููุญูุฏุฉ ุงูู ู ููุฉ ูู ุฃู Chromium ููุณู ุชูููุ ูููุนูุฏ ุชุดุบููู ูู ููุท | |
| (ูุง ุญุงุฌุฉ ูุฃู "reset" ุฃูุณุน ูู ุณุญ ุจุฑููุงููุงุช ุฃู Xvfb ูุฃูู ุบูุฑ ู ูุฌูุฏ ุฃุตูุงู). | |
| - ุฃุฎู ุชุนููุฏุงู: ุฃุฏุงุฉ ุชุญูู ูุงุญุฏุฉ (CDP) ุจุฏู ุฎููุท (xdotool + import + xlib + xwd). | |
| โโ ู ุง ุงูุฐู ุฃูุฒูู ู ู v13 ููู ุงุฐุง โโ | |
| ุฃุฏูุงุช "browser_get_elements / browser_click_element / browser_type_into_element / | |
| browser_find_element / browser_get_page_text / browser_execute_js / browser_navigate | |
| (CDP) / browser_scroll_to_element / browser_get_state" โ ูุฐู ูู ุชูู ุชูุณุชุฎุฏู ุฅุทูุงูุงู | |
| ู ู ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ (index.html) ุจุนุฏ ูุญุต ุงูููุฏ ุงููุนููุ ุงููุงุฌูุฉ ุชูุฑุณู ููุท: | |
| screenshot, terminal, mouse_move, mouse_click, mouse_drag, keyboard_type, | |
| keyboard_hotkey, keyboard_press, scroll, clipboard_write, clipboard_read, paste, | |
| open_app, open_tab, close_tab, browser_back, browser_forward, browser_search, | |
| screen_info, reset_computer. ุฅุฒุงูุฉ ุงูููุฏ ุงูู ูุช ููููุช ุงูุชุนููุฏ ูุงูู WebSocket | |
| overhead ูุฃุฏูุงุช ูุง ุฃุญุฏ ูุณุชุฏุนููุง. | |
| โโ ุงูุชูุงูู ู ุน ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ โโ | |
| ุงูุจุฑูุชูููู (ุฃุณู ุงุก actionsุ ุฃุณู ุงุก ุญููู ุฑุณุงุฆู screenshot/terminal_result/ | |
| computer_reset/error/connected/screen_info) ู ุทุงุจู 100% ูู ุง ุชุชููุนู | |
| index_plus_prompt_enhanced_v2-3.html โ ูุง ุญุงุฌุฉ ูุฃู ุชุนุฏูู ูู ุงููุฑููุช ุฅูุฏ. | |
| โโ v20.1 โ ุฅุถุงูุงุช: ุชุนุฏุฏ ุงูู ุชุตูุญุงุช + ุจุญุซ ุฑุณู ู ุจุฏูู ูุงุจุชุดุง โโ | |
| 1) ุชุนุฏุฏ ุงูู ุชุตูุญุงุช (Multi-Browser Fallback): | |
| ุนูุฏ ุฅููุงุน ุงูุณูุฑูุฑุ ูููุญุต ูู ู ุฑุดุญู ุงูู ุชุตูุญ ุงูู ุนุฑูููู (Chromium ุจุฃุณู ุงุฆู | |
| ุงูู ุฎุชููุฉ ุญุณุจ ุงูุชูุฒูุนุฉุ Chromeุ Edgeุ Brave...) ุจุชุดุบูู ุญูููู ูุนูู (ู ู ู ุฌุฑุฏ | |
| "ูู ู ูุฌูุฏ ุจุงูู ุณุงุฑ") ูุงุฎุชูุงุฑ ุฃูู ูุงุญุฏ ูุณุชุฌูุจ ูุนูุงู ุนูู CDP. ูู ุงูู ุชุตูุญ | |
| ุงูู ุนุชู ุฏ ูุดู ูุงุญูุงู ุฃุซูุงุก ุชุดุบูู ุฌูุณุฉ ู ุณุชุฎุฏู ุญููููุฉ (ู ุซูุงู ุชุญุฏูุซ ูุณุฑ ุงูุซูุงุฆู)ุ | |
| ุงูุณูุฑูุฑ ูุฌุฑุจ ุจููุฉ ุงูู ุฑุดุญูู ุชููุงุฆูุงู ููุนุชู ุฏ ุฃูู ุจุฏูู ูุงุฌุญ ูุจููุฉ ุงูุฌูุณุงุช โ | |
| ุจุฏูู ุฃู ุชุฏุฎู ูุฏูู ุฃู ุฅุนุงุฏุฉ ูุดุฑ. | |
| 2) Brave Search API โ ููุงุฉ ุจุญุซ ุฑุณู ูุฉ ุจุฏูู ูุงุจุชุดุง ุฅุทูุงูุงู: | |
| ุงูู ุดููุฉ ุงูุฃุตููุฉ: ูุชุญ duckduckgo.com/google.com ูู ููุน ูุนูู ุนุจุฑ ุงูู ุชุตูุญ | |
| ูุฃุฌู "ุงูุจุญุซ ุนู ู ุนููู ุฉ" ููุงุฌู ูุงุจุชุดุง ุญุชู ูุงูุ ูุฃู ูุฐู ุงูู ุญุฑูุงุช ู ุตู ู ุฉ ูู ูุน | |
| ุงูุทูุจุงุช ุงูุขููุฉ ุชุญุฏูุฏุงู. ุงูุญู: ุจุฏู ุฒูุงุฑุฉ ุงูู ููุนุ ุงูุฐูุงุก ุงูุงุตุทูุงุนู ูุฑุณู ุฃู ุฑ | |
| terminal ุจุตูุบุฉ search: ูุต ุงูุงุณุชุนูุงู (ุจุฏู curl ู ุนูุฏ) ูููุณุชุฏุนู Brave | |
| Search API ู ุจุงุดุฑุฉ (ุจุฏูู ูุชุญ ุฃู ู ุชุตูุญุ ุจุฏูู ูุงุจุชุดุงุ ูุฃูู ู ุฑุฎูุต ููุฐุง ุงูุบุฑุถ | |
| ุชุญุฏูุฏุงู ู ู ู ุฒููุฏู). ูุชุญ ู ุชุตูุญ Computer Mode ุงููุนูู ูุจูู ู ุญููุธุงู ููุท ูู ุง | |
| ูุญุชุงุฌ ุงูู ุณุชุฎุฏู "ุชุตูุญ ู ููุน ุจุนููู ุจุตุฑูุงู" ุฃู ุงูุชูุงุนู ู ุน ุตูุญุฉ ู ุนููุฉ โ ุจุงูุถุจุท | |
| ููุณ ููุณูุฉ ุนู ู ูููุงุก ู ุซู Manus: ุจุญุซ ูุธูู ุนุจุฑ API ูุณุทุฑ ุงูุฃูุงู ุฑ ุฃููุงูุ ูู ุชุตูุญ | |
| ุญูููู ููุท ููุชุตูุญ ุงููุนูู. | |
| ุงูุชูุนูู: ุนููู ู ุชุบููุฑ ุจูุฆุฉ BRAVE_API_KEY (ู ู | |
| https://api-dashboard.search.brave.com โ ููู ุจุงูุฉ ู ุฌุงููุฉ 2000 ุงุณุชุนูุงู /ุดูุฑ) | |
| ุนูู ู ูุตุฉ ุงููุดุฑ (Render/HF Spaces โ Settings โ Environment Variables). | |
| ุจุฏูู ุงูู ูุชุงุญุ ูุณุชู ุฑ ุงูุณูุฑูุฑ ุจุงูุนู ู ุนุจุฑ ู ุตุงุฏุฑ API ุงูู ุฌุงููุฉ ุงูุจุฏููุฉ | |
| (DuckDuckGo Instant Answer, Wikipedia, HackerNews, Reddit) ุชููุงุฆูุงู. | |
| โโ v20.2 โ Camoufox: ู ุญุฑู ู ุชุตูุญ ุซุงูู ู ุถุงุฏ ูููุดู โโ | |
| ุงูู ุดููุฉ: ุญุชู ู ุน Brave Search APIุ ุฃุญูุงูุงู ุงูู ุณุชุฎุฏู ูุทูุจ ูุนููุงู "ุชุตููุญ | |
| ู ููุน ู ุนููู ุจุตุฑูุงู" (ู ู ุจุญุซ)ุ ูุจุนุถ ุงูู ูุงูุน (ุฎุตูุตุงู ุงูู ุญู ูุฉ ุจู Cloudflare/ | |
| DataDome) ุชูุชุดู Chromium ุงูุนุงุฏู ููุฑุงู ุญุชู ูู headlessุ ูุฃูู ูุณุฑูุจ ุนูุงู ุงุช | |
| ุฃุชู ุชุฉ ูุงุถุญุฉ (navigator.webdriver=trueุ ุจุตู ุฉ toString ู ูุดููุฉุ ุฅูุฎ). | |
| ุงูุญู: ู ุญุฑู ุจุฏูู ุงุฎุชูุงุฑู (Camoufox โ Firefox ู ุนุฏููู ู ู ู ุณุชูู C++ุ ู ูุชูุญ | |
| ุงูู ุตุฏุฑ ูู ุฌุงูู ุจุงููุงู ู) ููุฎูู ูุฐู ุงูุนูุงู ุงุช ูููุชุฌ ุจุตู ุฉ ู ุชุตูุญ ูุงูุนูุฉ ุฅุญุตุงุฆูุงู. | |
| ูููุนููู ุนุจุฑ ุฃู ุฑ ุฌุฏูุฏ ูุฑุณูู ุงููุฑููุช ุฅูุฏ: | |
| {"action": "switch_engine", "data": {"engine": "camoufox"}} | |
| ูููุฑุฌูุน ููู ุญุฑู ุงูุงูุชุฑุงุถู ุงูุณุฑูุน: | |
| {"action": "switch_engine", "data": {"engine": "chromium"}} | |
| ุจููุฉ ูู ุงูุฃูุงู ุฑ (screenshot, mouse_click, keyboard_type...) ุชุจูู ุจููุณ | |
| ุงูุตูุบุฉ ูุงูุจุฑูุชูููู ุชู ุงู ุงู โ ููุท ุชูููููุฐ ุนุจุฑ ุงูู ุญุฑู ุงููุดุท ููู ุณุชุฎุฏู ุ ุจุฏูู | |
| ุฃู ุชุนุฏูู ู ุทููุจ ุนูู ุงููุฑููุช ุฅูุฏ ูุชูุฌูู ุงูุฃูุงู ุฑ ุจููุณูุง. | |
| ุชูุจูู ูุงูุนู ู ูู : Camoufox ูููู ุธููุฑ ุงููุงุจุชุดุง ุจุดูู ู ูุญูุธุ ูููู **ููุณ ุญูุงู | |
| ุณุญุฑูุงู 100%** โ ู ูุงุซูู Camoufox ููุณูุง ุชุฐูุฑ ุฃู ู ูุงูุน ู ุชูุฏู ุฉ ุฌุฏุงู ูุฏ ุชูุชุดูู | |
| ุฃุญูุงูุงู ุนุจุฑ ุชุญููู ุจุตู ุงุช ู ุชูุฏู . ูู ุง ุฃู ุณู ุนุฉ IP ุงูุณูุฑูุฑ (Render/HF) ุชุจูู | |
| ุนุงู ูุงู ู ููุตูุงูุ Camoufox ูุนุงูุฌ ุจุตู ุฉ *ุงูู ุชุตูุญ*ุ ูุง ุจุตู ุฉ *ุงูุดุจูุฉ/IP*. | |
| ุงูู ุชุทูุจุงุช: pip install camoufox[geoip] playwrightุ ุซู | |
| playwright install firefox (ุฃู camoufox fetch ูุชูุฒูู ุงูุซูุงุฆู ุงูู ุนุฏููู | |
| ุชููุงุฆูุงู ุนูุฏ ุฃูู ุงุณุชุฎุฏุงู ุ ~150-300MB ูู ุฑุฉ ูุงุญุฏุฉ). ุจุฏูู ูุฐู ุงูุญุฒู ุ ุฃูุงู ุฑ | |
| switch_engine ุฅูู camoufox ุชูุดู ุจุฃู ุงู ูุชูุจูู ุงูุฌูุณุฉ ุนูู Chromium. | |
| """ | |
| import asyncio | |
| import base64 | |
| import hashlib | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import threading | |
| import time | |
| import urllib.parse | |
| from typing import Optional | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, HTMLResponse | |
| import uvicorn | |
| try: | |
| import websockets as ws_lib | |
| _WS_LIB_AVAILABLE = True | |
| except ImportError: | |
| _WS_LIB_AVAILABLE = False | |
| print("โ ๏ธ websockets ุบูุฑ ู ุซุจูุชุฉ โ pip install websockets (ู ุทููุจุฉ ููุฐุง ุงูุฅุตุฏุงุฑ)") | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ุงูุฅุนุฏุงุฏุงุช ุงูุนุงู ุฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุฃูุตู ุนุฏุฏ ุฌูุณุงุช ู ุชุฒุงู ูุฉ. ุฑูู ูุงูุนู ูุณูุฑูุฑ ู ูุงุฑุฏ ู ุญุฏูุฏุฉ (HF Free CPU | |
| # Basic: 2 vCPU/16GB ู ุดุชุฑูุฉ) โ ูู ุฌูุณุฉ = ุนู ููุฉ Chromium ูุงุญุฏุฉ ููุท ุงูุขู | |
| # (ุจุฏู Xvfb+ู ุชุตูุญ ุณุงุจูุงู)ุ ูุงูุญู ู ุงููุนูู ููู ู ุณุชุฎุฏู ุฃุฎู ู ู ุงููุตู ุชูุฑูุจุงู | |
| # ู ูุงุฑูุฉ ุจุงูุจููุฉ ุงููุฏูู ุฉุ ููู ูุจูู ุฑูู ุงู ู ุชุญูุธุงู ุจุฏู ุงูุงูุชุฑุงุถ ุงูู ุชูุงุฆู. | |
| MAX_SESSIONS = 40 | |
| CDP_PORT_BASE = 9300 | |
| SCREEN_W, SCREEN_H = 1280, 800 | |
| _sessions_lock = threading.Lock() | |
| _cdp_port_pool_lock = threading.Lock() | |
| _used_ports: set = set() | |
| def _detect_chromium() -> str: | |
| """ | |
| ููุชุดู ุฃู ู ุชุตูุญ ู ุชููุฑ ุนูู ูุฐุง ุงูุณูุฑูุฑ โ ูุฌุฑุจ ูู ุงูุฃุณู ุงุก ุงูุดุงุฆุนุฉ (Chromium, | |
| Chrome, Chromium ุงูุฑุณู ู ุจุฃุดูุงูู ุงูู ุฎุชููุฉ ุญุณุจ ุชูุฒูุนุฉ Linux)ุ ู ู ุงุณู ูุงุญุฏ ุจุณ. | |
| """ | |
| return _pick_working_browser() | |
| # ูู ุงูุฃุณู ุงุก ุงูุดุงุฆุนุฉ ูุซูุงุฆูุงุช ุงูู ุชุตูุญ ุนูู ุชูุฒูุนุงุช Linux ุงูู ุฎุชููุฉ (Debian/Ubuntuุ | |
| # Alpineุ RPM-based) โ ุงูุฐูุงุก ุงูุงุตุทูุงุนู (ุนุจุฑ ูุฐู ุงูุฏุงูุฉ) ูุฌุฑุจ ูู ูุงุญุฏ ุจุงูุชุฑุชูุจ | |
| # ููุณุชุฎุฏู ุฃูู ูุงุญุฏ ูุนู ู ูุนููุงูุ ุจุฏู ุงูุงุนุชู ุงุฏ ุนูู ุงุณู ูุงุญุฏ ูุฏ ูุง ููุฌุฏ ุนูู ุตูุฑุฉ | |
| # ุงูุญุงููุฉ ุงูู ุณุชุฎุฏู ุฉ. | |
| _BROWSER_CANDIDATES = [ | |
| "chromium-browser", # Debian/Ubuntu | |
| "chromium", # Alpine/Arch/Fedora | |
| "google-chrome-stable", # Google Chrome ุงูุฑุณู ู | |
| "google-chrome", | |
| "chromium-freeworld", # ุจุนุถ ุชูุฒูุนุงุช RPM | |
| "ungoogled-chromium", # ูุณุฎุฉ ุฎุตูุตูุฉ ู ู Chromium | |
| "microsoft-edge-stable", # Edge (ู ุจูู ุนูู Chromiumุ ูุฏุนู ููุณ CDP) | |
| "brave-browser", # Brave (ู ุจูู ุนูู Chromiumุ ูุฏุนู ููุณ CDP) | |
| ] | |
| def _binary_exists(name: str) -> bool: | |
| r = subprocess.run(["which", name], capture_output=True, text=True) | |
| return r.returncode == 0 and bool(r.stdout.strip()) | |
| def _test_browser_launch(binary: str, timeout: float = 6.0) -> bool: | |
| """ | |
| ุงุฎุชุจุงุฑ ุญูููู: ูุดุบูู ุงูู ุชุตูุญ ูุนููุงู ุจูุถุน headless ุนูู ู ููุฐ CDP ู ุคูุช | |
| ููุชุฃูุฏ ุฃูู ูุณุชุฌูุจ โ ู ู ู ุฌุฑุฏ ูุญุต "ู ูุฌูุฏ ุจุงูู ุณุงุฑ"ุ ูุฃู ุฃุญูุงูุงู ุงูุซูุงุฆู | |
| ู ูุฌูุฏ ููู ู ูุณูุฑ (ู ูุชุจุงุช ูุงูุตุฉุ ุฅูุฎ). | |
| """ | |
| test_port = CDP_PORT_BASE + 9999 # ู ููุฐ ู ุฎุตุต ููุงุฎุชุจุงุฑ ููุทุ ูุง ูุชุนุงุฑุถ ู ุน ุงูุฌูุณุงุช | |
| try: | |
| subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={test_port}"], | |
| capture_output=True, timeout=3) | |
| except Exception: | |
| pass | |
| try: | |
| proc = subprocess.Popen([ | |
| binary, "--headless=new", f"--remote-debugging-port={test_port}", | |
| "--remote-debugging-address=127.0.0.1", "--disable-gpu", "--no-sandbox", | |
| "--disable-dev-shm-usage", "--no-first-run", "about:blank", | |
| ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| except Exception: | |
| return False | |
| ok = False | |
| deadline = time.time() + timeout | |
| try: | |
| import urllib.request | |
| while time.time() < deadline: | |
| try: | |
| with urllib.request.urlopen(f"http://127.0.0.1:{test_port}/json/version", timeout=1.0) as r: | |
| if r.status == 200: | |
| ok = True | |
| break | |
| except Exception: | |
| time.sleep(0.3) | |
| finally: | |
| try: | |
| proc.kill() | |
| proc.wait(timeout=3) | |
| except Exception: | |
| pass | |
| try: | |
| subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={test_port}"], | |
| capture_output=True, timeout=3) | |
| except Exception: | |
| pass | |
| return ok | |
| def _pick_working_browser() -> str: | |
| """ | |
| ูุฌุฑุจ ูู ู ุฑุดุญู ุงูู ุชุตูุญ ุจุงูุชุฑุชูุจ: ุฃููุงู ูุชุฃูุฏ ุฃู ุงูุซูุงุฆู ู ูุฌูุฏ ุนูู ุงููุฑุต | |
| (ุณุฑูุน)ุ ูุฅุฐุง ูุฌุฏุ ูุฎุชุจุฑู ูุนููุงู ุจุชุดุบููู ูุงูุชุฃูุฏ ุฃูู ูุณุชุฌูุจ ุนูู CDP | |
| (ุงุฎุชุจุงุฑ ุญููููุ ูุง ุงูุชุฑุงุถ). ุฃูู ู ุชุตูุญ "ุญู" ูุนูุงู ููุนุชู ุฏ ูุจููุฉ ุนู ุฑ ุงูุณูุฑูุฑ. | |
| """ | |
| found_but_untested = [] | |
| for name in _BROWSER_CANDIDATES: | |
| if _binary_exists(name): | |
| found_but_untested.append(name) | |
| if not found_but_untested: | |
| print("โ ูุง ููุฌุฏ ุฃู ู ุชุตูุญ ู ุซุจูุช ุนูู ูุฐุง ุงูุณูุฑูุฑ ู ู ุจูู ุงูู ุฑุดุญูู ุงูู ุนุฑูููู:") | |
| print(f" {', '.join(_BROWSER_CANDIDATES)}") | |
| print(" ุซุจูุช ูุงุญุฏุงู ุนุจุฑ: apt-get install chromium") | |
| return "chromium-browser" # ุงูุชุฑุงุถูุ ุญุชู ูู ุบูุฑ ู ูุฌูุฏุ ููููู ุงุณู ูุงุถุญ ุจุงูุณุฌูุงุช | |
| print(f"๐ ู ุชุตูุญุงุช ู ูุฌูุฏุฉ ุนูู ุงููุฑุต: {', '.join(found_but_untested)} โ ูุฌุฑู ุงุฎุชุจุงุฑ ูู ูุงุญุฏ ูุนููุงู...") | |
| for name in found_but_untested: | |
| print(f" ๐งช ุงุฎุชุจุงุฑ {name} ...") | |
| if _test_browser_launch(name): | |
| print(f" โ {name} ูุนู ู ูุนููุงู โ ุณููุนุชู ุฏ ููู ุงูุฌูุณุงุช") | |
| return name | |
| else: | |
| print(f" โ {name} ู ูุฌูุฏ ููู ูุดู ุจุงูุชุดุบูู ุงููุนูู (ู ูุชุจุงุช ูุงูุตุฉ ุฃู ุซูุงุฆู ู ุนุทูุจ)") | |
| # ูู ุดูุก ููุฌุฏ ููู ูุดู ุงูุงุฎุชุจุงุฑ ุงูุญู โ ุงุณุชุฎุฏู ุฃูู ู ูุฌูุฏ ูุฎูุงุฑ ุฃุฎูุฑ ู ุน ุชุญุฐูุฑ ูุงุถุญ | |
| print(f"โ ๏ธ ูู ุงูู ุชุตูุญุงุช ุงูู ูุฌูุฏุฉ ูุดูุช ุจุงุฎุชุจุงุฑ ุงูุชุดุบูู ุงูุญู. ุณููุณุชุฎุฏู '{found_but_untested[0]}' " | |
| f"ูุฎูุงุฑ ุงูุชุฑุงุถู ุฃุฎูุฑุ ููู ูููุตุญ ุจูุญุต ุชุซุจูุช ุงูู ุชุตูุญ ุนูู ูุฐุง ุงูุณูุฑูุฑ.") | |
| return found_but_untested[0] | |
| BROWSER_BIN = _detect_chromium() | |
| def _alloc_cdp_port() -> int: | |
| """ูุฎุตุต ู ููุฐ CDP ุญุฑ ูู ุณุชุฎุฏู ุฌุฏูุฏ.""" | |
| with _cdp_port_pool_lock: | |
| p = CDP_PORT_BASE | |
| while p in _used_ports: | |
| p += 1 | |
| _used_ports.add(p) | |
| return p | |
| def _release_cdp_port(port: int): | |
| with _cdp_port_pool_lock: | |
| _used_ports.discard(port) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Camoufox โ ู ุญุฑู ู ุชุตูุญ ุจุฏูู ู ุถุงุฏ ูููุดู (ุงุฎุชูุงุฑูุ ุจุฌุงูุจ Chromium) โโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ูู ุงุฐุง ู ุญุฑู ุซุงูู ู ููุตูุ Camoufox ูู Firefox ู ุนุฏููู ู ู ู ุณุชูู C++ (ู ู ุญูู | |
| # JavaScript ูุงุจู ูููุดู) ููุฎูู ุนูุงู ุงุช ุงูุฃุชู ุชุฉ (navigator.webdriver ูุบูุฑูุง) | |
| # ูููููุฏ ุจุตู ุฉ ู ุชุตูุญ ู ุชูุณูุฉ ููุงูุนูุฉ ุฅุญุตุงุฆูุงู (ูุธุงู ุชุดุบููุ ุฏูุฉ ุดุงุดุฉุ ุฎุทูุทุ | |
| # WebGL...) ุนุจุฑ ู ูุชุจุฉ BrowserForge. ูุฐุง ูููู ุธููุฑ ูุงุจุชุดุง ุงูู ูุงูุน ุงูู ุญู ูุฉ | |
| # ุจุดุฏุฉ (Cloudflare, DataDome) ุจุดูู ู ูุญูุธ โ ูููู **ููุณ ุถู ุงูุงู ู ุทููุงู** (ุญุชู | |
| # ู ูุซูู Camoufox ููุณู ูุฐูุฑ ุฃู ู ูุงูุน ู ุชูุฏู ุฉ ูุฏ ุชูุชุดูู ุฃุญูุงูุงู). | |
| # | |
| # ุงููุฑู ุงูุฌููุฑู ุนู Chromium/CDP: ูุงุฌูุฉ Camoufox ุงูุฑุณู ูุฉ ูู Python+Playwright | |
| # (ู ู CDP ุฎุงู )ุ ูุชุญูู ูุง ููู ูู ุฑ ุนุจุฑ Playwright API (page.mouse, page.keyboard, | |
| # page.screenshot) ุจุฏู ุฑุณุงุฆู CDP JSON ุงูู ุจุงุดุฑุฉ ุงูู ุณุชุฎุฏู ุฉ ู ุน Chromium. ูุฐูู | |
| # ุจูููุงู ูู"ู ุญุฑู ุจุฏูู" ู ุณุชูู ุจุฌุงูุจ CDPSessionุ ุจููุณ ุจุฑูุชูููู ุงูุฃูุดูุงุช ุงูุฎุงุฑุฌู | |
| # (screenshot/mouse_click/keyboard_type...) ุญุชู ูุง ูุชุฃุซุฑ ุงููุฑููุช ุฅูุฏ ุฅุทูุงูุงู. | |
| # | |
| # ุงูุชูุนูู: ุชููุงุฆู ุจุงููุงู ู โ ูุง ุญุงุฌุฉ ูุฃู ุฅุนุฏุงุฏ. Camoufox ุญุฒู ุฉ ุจุงูุซูู ุนุงุฏูุฉ | |
| # (pip install camoufox[geoip] + playwright)ุ ุฃูู ุงุณุชุฎุฏุงู ูููุฒูู ุซูุงุฆู | |
| # Firefox ุงูู ุนุฏููู ุชููุงุฆูุงู (~150-300MB ู ุฑุฉ ูุงุญุฏุฉ ููุทุ ููุฎุฒููู ูููุนุงุฏ ุงุณุชุฎุฏุงู ู). | |
| try: | |
| from camoufox.async_api import AsyncCamoufox | |
| _CAMOUFOX_LIB_AVAILABLE = True | |
| except ImportError: | |
| _CAMOUFOX_LIB_AVAILABLE = False | |
| print("โน๏ธ ู ูุชุจุฉ camoufox ุบูุฑ ู ุซุจูุชุฉ โ pip install camoufox playwright ูุชูุนูู ุงูู ุญุฑู ุงูู ุถุงุฏ ูููุดู") | |
| CAMOUFOX_ENABLED = os.environ.get("CAMOUFOX_ENABLED", "1").strip() not in ("0", "false", "no") | |
| class CamoufoxSession: | |
| """ | |
| ุฌูุณุฉ Camoufox ูุงุญุฏุฉ ููู ู ุณุชุฎุฏู โ ุจุฏูู ูู CDPSession ููู ู ุจูู ููู | |
| Playwright ุจุฏู CDP ุฎุงู . ูููุฑ ููุณ ููุน ุงูุนู ููุงุช (screenshot, mouse, | |
| keyboard, navigate) ุจูุงุฌูุฉ ู ูุญูุฏุฉ (dict ูุชุงุฆุฌ) ูุชูุณุชููู ู ู ููุณ ู ุนุงูุฌ | |
| ุงูุฃูุดูุงุช (handle_action) ุฏูู ุชุบููุฑ ุจุฑูุชูููู ุงููุฑููุช ุฅูุฏ. | |
| """ | |
| def __init__(self, user_id: str): | |
| self.user_id = user_id | |
| self._cm = None # AsyncCamoufox context manager instance | |
| self._browser = None # Playwright Browser | |
| self._context = None # Playwright BrowserContext | |
| self.page = None # Playwright Page ุงูุญุงููุฉ | |
| self._mouse_x = SCREEN_W // 2 | |
| self._mouse_y = SCREEN_H // 2 | |
| def alive(self) -> bool: | |
| return self.page is not None and not self.page.is_closed() | |
| async def start(self) -> bool: | |
| if not _CAMOUFOX_LIB_AVAILABLE: | |
| print("[camoufox] โ ุงูู ูุชุจุฉ ุบูุฑ ู ุซุจููุชุฉ ุนูู ูุฐุง ุงูุณูุฑูุฑ") | |
| return False | |
| try: | |
| profile = _profile_dir("camoufox_" + self.user_id) | |
| self._cm = AsyncCamoufox( | |
| headless=True, # Camoufox ูุฏุนู headless ุญูููู ุจุฃู ุงู (ููุฎูู ุนูุงู ุงุช ุงููุดู ุญุชู ูู ูุฐุง ุงููุถุน) | |
| humanize=True, # ุญุฑูุฉ ูุฃุฑุฉ ุดุจููุฉ ุจุงูุจุดุฑ (ู ูุญููุงุช Bezier ุจุฏู ุฎุทูุท ู ุณุชููู ุฉ) | |
| geoip=False, # ูุง ูุญุชุงุฌ ุถุจุท ุฌุบุฑุงูู ุชููุงุฆู ุนุจุฑ ุจุฑููุณู ููุง | |
| persistent_context=True, | |
| user_data_dir=profile, | |
| window=(SCREEN_W, SCREEN_H), | |
| ) | |
| self._context = await self._cm.__aenter__() | |
| # ูู persistent_context=Trueุ ุงููุงุฆู ุงูู ูุนุงุฏ ูู BrowserContext ู ุจุงุดุฑุฉ | |
| pages = self._context.pages | |
| self.page = pages[0] if pages else await self._context.new_page() | |
| await self.page.set_viewport_size({"width": SCREEN_W, "height": SCREEN_H}) | |
| print(f"[camoufox] โ ุฌูุณุฉ ุฌุฏูุฏุฉ ูู '{self.user_id}' ุฌุงูุฒุฉ") | |
| return True | |
| except Exception as e: | |
| print(f"[camoufox] โ ูุดู ุจุฏุก ุงูุฌูุณุฉ: {e}") | |
| await self.close() | |
| return False | |
| async def close(self): | |
| try: | |
| if self._cm: | |
| await self._cm.__aexit__(None, None, None) | |
| except Exception: | |
| pass | |
| self._cm = None | |
| self._context = None | |
| self.page = None | |
| async def screenshot_b64(self) -> "str | None": | |
| if not self.alive: | |
| return None | |
| try: | |
| raw = await self.page.screenshot(type="jpeg", quality=75) | |
| return base64.b64encode(raw).decode() | |
| except Exception as e: | |
| print(f"[camoufox] โ ูุดู ุงูุชูุงุท ุงูุดุงุดุฉ: {e}") | |
| return None | |
| async def mouse_move(self, x: int, y: int): | |
| if self.alive: | |
| await self.page.mouse.move(x, y) | |
| self._mouse_x, self._mouse_y = x, y | |
| async def mouse_click(self, x: int, y: int, button: str = "left", double: bool = False): | |
| if not self.alive: | |
| return | |
| await self.page.mouse.move(x, y) | |
| if double: | |
| await self.page.mouse.dblclick(x, y, button=button) | |
| else: | |
| await self.page.mouse.click(x, y, button=button) | |
| self._mouse_x, self._mouse_y = x, y | |
| async def mouse_drag(self, x1: int, y1: int, x2: int, y2: int): | |
| if not self.alive: | |
| return | |
| await self.page.mouse.move(x1, y1) | |
| await self.page.mouse.down() | |
| await self.page.mouse.move(x2, y2, steps=10) | |
| await self.page.mouse.up() | |
| self._mouse_x, self._mouse_y = x2, y2 | |
| async def scroll(self, x: int, y: int, delta_y: int): | |
| if not self.alive: | |
| return | |
| await self.page.mouse.move(x, y) | |
| await self.page.mouse.wheel(0, delta_y) | |
| async def type_text(self, text: str): | |
| if self.alive: | |
| await self.page.keyboard.type(text, delay=25) # ุชุฃุฎูุฑ ุฎููู ุจูู ุงูุญุฑูู โ ุฃูุซุฑ ูุงูุนูุฉ ุจุดุฑูุงู | |
| async def key_press(self, key_name: str): | |
| if self.alive: | |
| await self.page.keyboard.press(key_name) | |
| async def hotkey(self, keys: list): | |
| if not self.alive: | |
| return | |
| combo = "+".join(keys) | |
| await self.page.keyboard.press(combo) | |
| async def navigate(self, url: str) -> bool: | |
| if not self.alive: | |
| return False | |
| try: | |
| await self.page.goto(url, timeout=20000, wait_until="domcontentloaded") | |
| return True | |
| except Exception as e: | |
| print(f"[camoufox] โ ๏ธ ูุดู ุงูุชูููู ุฅูู {url}: {e}") | |
| return False | |
| async def go_back(self): | |
| if self.alive: | |
| try: | |
| await self.page.go_back(timeout=8000) | |
| except Exception: | |
| pass | |
| async def go_forward(self): | |
| if self.alive: | |
| try: | |
| await self.page.go_forward(timeout=8000) | |
| except Exception: | |
| pass | |
| # ุฌูุณุงุช Camoufox ู ููุตูุฉ ุนู ุฌูุณุงุช Chromium/CDP โ ู ูุชุงุญูุง user_id ุฃูุถุงูุ ููู | |
| # ูุงู ูุณ ู ุณุชูู ูุฃู ู ุญุฑู ุงูู ุณุชุฎุฏู ูุฏ ูุชุจุฏูู ุจูู ุงูุงุซููู ุฃุซูุงุก ุงูุฌูุณุฉ ููุณูุง. | |
| _camoufox_sessions: dict = {} | |
| _camoufox_lock = threading.Lock() | |
| async def get_camoufox_session(user_id: str) -> "CamoufoxSession | None": | |
| """ููุฑุฌุน ุฌูุณุฉ Camoufox ู ูุฌูุฏุฉ ุฃู ููุดุฆ ูุงุญุฏุฉ ุฌุฏูุฏุฉ ุนูุฏ ุงูุญุงุฌุฉ.""" | |
| if not CAMOUFOX_ENABLED or not _CAMOUFOX_LIB_AVAILABLE: | |
| return None | |
| with _camoufox_lock: | |
| sess = _camoufox_sessions.get(user_id) | |
| if sess and sess.alive: | |
| return sess | |
| sess = CamoufoxSession(user_id) | |
| ok = await sess.start() | |
| if not ok: | |
| return None | |
| with _camoufox_lock: | |
| _camoufox_sessions[user_id] = sess | |
| return sess | |
| async def destroy_camoufox_session(user_id: str): | |
| with _camoufox_lock: | |
| sess = _camoufox_sessions.pop(user_id, None) | |
| if sess: | |
| await sess.close() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ CDP Session โ ุงูุงุชุตุงู ุงูู ุจุงุดุฑ ุจู Chromium ุนุจุฑ DevTools Protocol โโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class CDPSession: | |
| """ | |
| ุงุชุตุงู WebSocket ูุงุญุฏ ุจุฌูุณุฉ Chromium headless ูุงุญุฏุฉ. ูู ุงูุฃูุงู ุฑ | |
| (screenshot, mouse, keyboard, navigate) ุชู ุฑ ู ู ููุง. ุงุชุตุงู ูุงุญุฏ ููุนุงุฏ | |
| ุงุณุชุฎุฏุงู ู ุทูุงู ุญูุงุฉ ุงูุฌูุณุฉ ุจุฏู ูุชุญ ุงุชุตุงู ุฌุฏูุฏ ููู ุฃู ุฑ. | |
| """ | |
| def __init__(self, port: int): | |
| self.port = port | |
| self._ws = None | |
| self._msg_id = 0 | |
| self._pending: dict = {} | |
| self._recv_task: Optional[asyncio.Task] = None | |
| self._send_lock = asyncio.Lock() | |
| self._target_id = None | |
| async def connect(self, timeout: float = 6.0) -> bool: | |
| if not _WS_LIB_AVAILABLE: | |
| return False | |
| # ุฌุฑูุจ ุนุฏุฉ ู ุฑุงุช ูุฃู Chromium ูุฏ ูุญุชุงุฌ ูุญุธุฉ ุจุนุฏ ุงูุฅููุงุน ูุจู ูุชุญ ู ููุฐ CDP | |
| deadline = time.time() + timeout | |
| tabs = None | |
| while time.time() < deadline: | |
| try: | |
| import urllib.request | |
| with urllib.request.urlopen( | |
| f"http://127.0.0.1:{self.port}/json/list", timeout=1.5 | |
| ) as r: | |
| tabs = json.loads(r.read()) | |
| break | |
| except Exception: | |
| await asyncio.sleep(0.25) | |
| if tabs is None: | |
| print(f"[CDP:{self.port}] โ Chromium ูุง ูุณุชุฌูุจ ุนูู ู ููุฐ CDP") | |
| return False | |
| ws_url = None | |
| for t in tabs: | |
| if t.get("type") == "page": | |
| ws_url = t.get("webSocketDebuggerUrl") | |
| self._target_id = t.get("id") | |
| break | |
| if not ws_url: | |
| print(f"[CDP:{self.port}] โ ูุง ููุฌุฏ ุชุจููุจ ุตูุญุฉ ู ุชุงุญ") | |
| return False | |
| try: | |
| self._ws = await asyncio.wait_for( | |
| ws_lib.connect(ws_url, ping_interval=15, ping_timeout=10, max_size=64 * 1024 * 1024), | |
| timeout=timeout, | |
| ) | |
| except Exception as e: | |
| print(f"[CDP:{self.port}] โ ูุดู ุงุชุตุงู WebSocket: {e}") | |
| return False | |
| self._recv_task = asyncio.create_task(self._recv_loop()) | |
| # ูุนูู ุงููุทุงูุงุช ุงูู ุทููุจุฉ | |
| await self.send("Page.enable", timeout=5.0) | |
| await self.send("Runtime.enable", timeout=5.0) | |
| await self.send("DOM.enable", timeout=5.0) | |
| # ุงุถุจุท ุญุฌู ุงูุดุงุดุฉ ุงูุงูุชุฑุงุถู ุนุจุฑ Emulation (headless ุจุฏูู ูุงูุฐุฉ ูุนููุฉุ | |
| # ูุฐุง ูุญุฏุฏ ุฃุจุนุงุฏ ุงูุฑูุฏุฑ ุงูุฏุงุฎูู) | |
| await self.send("Emulation.setDeviceMetricsOverride", { | |
| "width": SCREEN_W, "height": SCREEN_H, | |
| "deviceScaleFactor": 1, "mobile": False, | |
| }, timeout=5.0) | |
| print(f"[CDP:{self.port}] โ ู ุชุตู ุจุฌูุณุฉ Chromium") | |
| return True | |
| async def _recv_loop(self): | |
| try: | |
| async for raw in self._ws: | |
| try: | |
| msg = json.loads(raw) | |
| except Exception: | |
| continue | |
| mid = msg.get("id") | |
| if mid is not None and mid in self._pending: | |
| fut = self._pending.pop(mid) | |
| if not fut.done(): | |
| fut.set_result(msg) | |
| except Exception: | |
| pass | |
| finally: | |
| for fut in self._pending.values(): | |
| if not fut.done(): | |
| fut.set_exception(ConnectionError("CDP disconnected")) | |
| self._pending.clear() | |
| self._ws = None | |
| async def send(self, method: str, params: dict = None, timeout: float = 10.0): | |
| if not self._ws: | |
| return None | |
| async with self._send_lock: | |
| self._msg_id += 1 | |
| mid = self._msg_id | |
| fut = asyncio.get_event_loop().create_future() | |
| self._pending[mid] = fut | |
| try: | |
| await self._ws.send(json.dumps({"id": mid, "method": method, "params": params or {}})) | |
| return await asyncio.wait_for(fut, timeout=timeout) | |
| except Exception: | |
| self._pending.pop(mid, None) | |
| return None | |
| def alive(self) -> bool: | |
| return self._ws is not None | |
| async def close(self): | |
| if self._recv_task: | |
| self._recv_task.cancel() | |
| if self._ws: | |
| try: | |
| await self._ws.close() | |
| except Exception: | |
| pass | |
| self._ws = None | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ุชุดุบูู/ุฅููุงู ุนู ููุฉ Chromium headless โโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _profile_dir(user_id: str) -> str: | |
| safe = re.sub(r"[^a-zA-Z0-9_.-]", "_", user_id) or "anon" | |
| d = os.path.expanduser(f"~/.zai_chromium_profiles/{safe}") | |
| os.makedirs(d, exist_ok=True) | |
| return d | |
| def _launch_browser_process(binary: str, port: int, profile: str) -> "subprocess.Popen | None": | |
| cmd = [ | |
| binary, | |
| "--headless=new", # ุงููุถุน ุงูุฑุณู ู ุงูุญุฏูุซ โ ูุง ุญุงุฌุฉ ูุฃู display | |
| f"--remote-debugging-port={port}", | |
| "--remote-debugging-address=127.0.0.1", | |
| "--disable-gpu", | |
| "--no-sandbox", | |
| "--disable-dev-shm-usage", | |
| "--disable-extensions", | |
| "--disable-notifications", | |
| "--disable-popup-blocking", | |
| "--disable-translate", | |
| "--disable-sync", | |
| "--disable-background-timer-throttling", | |
| "--disable-backgrounding-occluded-windows", | |
| "--disable-renderer-backgrounding", | |
| "--disable-features=Translate,BackForwardCache", | |
| "--no-first-run", | |
| "--no-default-browser-check", | |
| "--mute-audio", | |
| "--hide-scrollbars", | |
| f"--window-size={SCREEN_W},{SCREEN_H}", | |
| f"--user-data-dir={profile}", | |
| "--js-flags=--max-old-space-size=256", # ุณูู ุฐุงูุฑุฉ JS โ ูู ูุน ุชุจููุจ ุซููู ู ู ุงุณุชููุงู ูู ุงูุฑุงู | |
| "about:blank", | |
| ] | |
| try: | |
| proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| return proc | |
| except FileNotFoundError: | |
| return None | |
| except Exception as e: | |
| print(f"[chromium] โ ูุดู ุชุดุบูู {binary}: {e}") | |
| return None | |
| def _start_chromium(port: int, user_id: str) -> "subprocess.Popen | None": | |
| """ | |
| ููุดุบูู ู ุชุตูุญุงู ุจูุถุน headless=new ุนูู ู ููุฐ CDP ู ุฎุตุต. ูุง Xvfbุ ูุง DISPLAYุ | |
| ูุง ูุงูุฐุฉ โ ุงูุฑูุฏุฑ ุจุงููุงู ู ุฏุงุฎูู (in-process) ูู ุญุฑู ุงูู ุชุตูุญ ููุณู. | |
| ุฐูุงุก ุฅุถุงูู: ูู ุงูู ุชุตูุญ ุงูุญุงูู (BROWSER_BIN) ูุดู ุจุงูุฅููุงุน ูุนููุงู ููุช ุงูุชุดุบูู | |
| ุงูุญูููู (ู ู ููุช ุงููุญุต ุงูุฃููู)ุ ูุฌุฑูุจ ุจููุฉ ุงูู ุชุตูุญุงุช ุงูู ุฑุดุญุฉ ุชููุงุฆูุงู ุจุฏู | |
| ุงููุดู ุงููุงู ูุ ููุญุฏูุซ BROWSER_BIN ููู ุชุตูุญ ุงููู ูุฌุญ ูููุณุชุฎุฏู ู ุจุงุดุฑุฉ ุจุงูุฌูุณุงุช | |
| ุงููุงุฏู ุฉ (ุจุฏูู ุฅุนุงุฏุฉ ุชุฌุฑุจุฉ ูู ุงูู ุชุตูุญุงุช ุงููุงุดูุฉ ู ู ุฌุฏูุฏ ูู ู ุฑุฉ). | |
| """ | |
| global BROWSER_BIN | |
| profile = _profile_dir(user_id) | |
| try: | |
| subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={port}"], | |
| capture_output=True, timeout=3) | |
| time.sleep(0.2) | |
| except Exception: | |
| pass | |
| proc = _launch_browser_process(BROWSER_BIN, port, profile) | |
| if proc is not None: | |
| print(f"[chromium] ๐ ุจุฏุฃ '{BROWSER_BIN}' (PID {proc.pid}) ุนูู ู ููุฐ CDP {port} โ headless=new") | |
| return proc | |
| print(f"[chromium] โ ๏ธ ูุดู '{BROWSER_BIN}' ูุนููุงู โ ุฃุฌุฑุจ ุจููุฉ ุงูู ุชุตูุญุงุช ุงูู ุนุฑููุฉ ุชููุงุฆูุงู...") | |
| for alt in _BROWSER_CANDIDATES: | |
| if alt == BROWSER_BIN or not _binary_exists(alt): | |
| continue | |
| proc = _launch_browser_process(alt, port, profile) | |
| if proc is not None: | |
| print(f"[chromium] โ ูุฌุญ ุงูุจุฏูู '{alt}' โ ุณููุนุชู ุฏ ูุจููุฉ ุงูุฌูุณุงุช ู ู ุงูุขู") | |
| BROWSER_BIN = alt | |
| return proc | |
| print(f"[chromium] โ ูุดูุช ูู ุงูู ุชุตูุญุงุช ุงูู ุชุงุญุฉ ุนูู ูุฐุง ุงูุณูุฑูุฑ") | |
| return None | |
| def _kill_chromium(proc: "subprocess.Popen | None", port: int): | |
| if proc: | |
| try: | |
| proc.kill() | |
| proc.wait(timeout=3) | |
| except Exception: | |
| pass | |
| try: | |
| subprocess.run(["pkill", "-9", "-f", f"[r]emote-debugging-port={port}"], | |
| capture_output=True, timeout=3) | |
| except Exception: | |
| pass | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ุฅุฏุงุฑุฉ ุงูุฌูุณุงุช (ู ุณุชุฎุฏู โ CDPSession + Chromium process) โโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # user_id โ { | |
| # "port": int, "proc": Popen, "cdp": CDPSession|None, | |
| # "active_ws": WebSocket|None, "created": float, | |
| # "consecutive_failures": int, "last_frame_hash": str, | |
| # "last_mouse_x": int, "last_mouse_y": int, | |
| # } | |
| _sessions: dict = {} | |
| async def get_or_create_session(user_id: str, ws: WebSocket) -> dict: | |
| with _sessions_lock: | |
| sess = _sessions.get(user_id) | |
| if sess: | |
| sess["active_ws"] = ws | |
| print(f"[session] ๐ ุฅุนุงุฏุฉ ุงุชุตุงู '{user_id}' ุนูู ู ููุฐ {sess['port']}") | |
| return sess | |
| if len(_sessions) >= MAX_SESSIONS: | |
| # ุฃุนุฏ ุงุณุชุฎุฏุงู ุฃูุฏู ุฌูุณุฉ ุบูุฑ ู ุชุตูุฉ ุจุฏู ุฑูุถ ุงูู ุณุชุฎุฏู ุงูุฌุฏูุฏ | |
| idle = [(uid, s) for uid, s in _sessions.items() if not s.get("active_ws")] | |
| if idle: | |
| oldest_uid = min(idle, key=lambda t: t[1]["created"])[0] | |
| old = _sessions.pop(oldest_uid) | |
| _kill_chromium(old.get("proc"), old["port"]) | |
| _release_cdp_port(old["port"]) | |
| print(f"[session] โป๏ธ ุฃูุฒููุช ุฌูุณุฉ ุฎุงู ูุฉ ูุฏูู ุฉ '{oldest_uid}' ูุฅูุณุงุญ ุงูู ุฌุงู") | |
| port = _alloc_cdp_port() | |
| sess = { | |
| "user_id": user_id, | |
| "port": port, | |
| "proc": None, | |
| "cdp": None, | |
| "active_ws": ws, | |
| "created": time.time(), | |
| "consecutive_failures": 0, | |
| "last_frame_hash": "", | |
| "last_mouse_x": SCREEN_W // 2, | |
| "last_mouse_y": SCREEN_H // 2, | |
| "shot_lock": asyncio.Lock(), | |
| "engine": "chromium", # "chromium" (ุงูุชุฑุงุถูุ ุนุจุฑ CDP) ุฃู "camoufox" (ู ุถุงุฏ ูููุดูุ ุนุจุฑ Playwright) | |
| } | |
| _sessions[user_id] = sess | |
| print(f"[session] โ ู ุณุชุฎุฏู ุฌุฏูุฏ '{user_id}' โ ู ููุฐ CDP {port}") | |
| return sess | |
| async def ensure_browser_running(sess: dict) -> bool: | |
| """ูุชุฃูุฏ ุฃู Chromium + ุงุชุตุงู CDP ูุนู ูุงู ููุฐู ุงูุฌูุณุฉ. ููุดุบูููู ุง ุฅุฐุง ูุฒู .""" | |
| if sess.get("cdp") and sess["cdp"].alive: | |
| return True | |
| if not sess.get("proc") or sess["proc"].poll() is not None: | |
| proc = await asyncio.to_thread(_start_chromium, sess["port"], sess["user_id"]) | |
| sess["proc"] = proc | |
| if proc is None: | |
| return False | |
| cdp = CDPSession(sess["port"]) | |
| ok = await cdp.connect(timeout=8.0) | |
| if ok: | |
| sess["cdp"] = cdp | |
| return True | |
| return False | |
| async def reset_session(user_id: str) -> bool: | |
| """ููุนูุฏ ุชุดุบูู Chromium ุจุงููุงู ู ููุฐุง ุงูู ุณุชุฎุฏู โ ุงูุฅุตูุงุญ ุงูุดุงู ู ุงููุญูุฏ ุงูู ุทููุจ ุงูุขูุ | |
| ุจู ุง ุฃูู ูุง ููุฌุฏ Xvfb ุฃู ู ููุงุช ุฌูุณุฉ X ู ููุตูุฉ ูุฌุจ ุชูุธูููุง.""" | |
| with _sessions_lock: | |
| sess = _sessions.get(user_id) | |
| if not sess: | |
| return False | |
| proc, port = sess.get("proc"), sess["port"] | |
| old_cdp = sess.get("cdp") | |
| if old_cdp: | |
| await old_cdp.close() | |
| await asyncio.to_thread(_kill_chromium, proc, port) | |
| new_proc = await asyncio.to_thread(_start_chromium, port, user_id) | |
| new_cdp = CDPSession(port) | |
| ok = await new_cdp.connect(timeout=8.0) if new_proc else False | |
| with _sessions_lock: | |
| if user_id in _sessions: | |
| _sessions[user_id]["proc"] = new_proc | |
| _sessions[user_id]["cdp"] = new_cdp if ok else None | |
| _sessions[user_id]["consecutive_failures"] = 0 | |
| _sessions[user_id]["last_frame_hash"] = "" | |
| print(f"[reset] {'โ ' if ok else 'โ'} ุฅุนุงุฏุฉ ุถุจุท '{user_id}' ุนูู ู ููุฐ {port}") | |
| return ok | |
| async def destroy_session_ws(user_id: str, ws: WebSocket): | |
| """ุนูุฏ ุงููุทุงุน WebSocket โ ููุจูู Chromium ุดุบุงูุงู (ุงูุฌูุณุฉ ู ุญููุธุฉ) ูููุท ูุฒูู ุงูุงุชุตุงู.""" | |
| with _sessions_lock: | |
| sess = _sessions.get(user_id) | |
| if sess and sess.get("active_ws") is ws: | |
| sess["active_ws"] = None | |
| print(f"[session] ๐ด '{user_id}' ูุทุน ุงูุงุชุตุงู (ุงูุฌูุณุฉ ู ุญููุธุฉ)") | |
| async def destroy_session_full(user_id: str): | |
| with _sessions_lock: | |
| sess = _sessions.pop(user_id, None) | |
| if sess: | |
| if sess.get("cdp"): | |
| await sess["cdp"].close() | |
| await asyncio.to_thread(_kill_chromium, sess.get("proc"), sess["port"]) | |
| _release_cdp_port(sess["port"]) | |
| await destroy_camoufox_session(user_id) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ููุทุฉ ุงูุดุงุดุฉ โ ุนุจุฑ CDP ู ุจุงุดุฑุฉ (ุจุฏูู subprocess ุฎุงุฑุฌู) โโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def cdp_screenshot(sess: dict, quality: int = 75) -> "tuple[str, int, int] | None": | |
| """ | |
| ููุชูุท ููุทุฉ ุดุงุดุฉ ุนุจุฑ Page.captureScreenshot โ ุฃู ุฑ CDP ูุงุญุฏ ูุฑุฌุน JPEG | |
| base64 ุฌุงูุฒ ู ุจุงุดุฑุฉ ู ู ู ุญุฑู ุงูุฑูุฏุฑ ุงูุฏุงุฎูู ูู Chromium. ูุง ููุฌุฏ X server | |
| ูู ูู ุฃู "ูุชุฌู ุฏ"ุ ูุง ุญุงุฌุฉ ููุญุต "ูู ุงูุตูุฑุฉ ุณูุฏุงุก"ุ ููุง ุนู ููุฉ subprocess | |
| ุฎุงุฑุฌูุฉ (import/xwd) ูุฏ ุชูุดู ุจุณุจุจ ุฃุฏูุงุช ู ูููุฏุฉ ุนูู ุงูุณูุฑูุฑ. | |
| ููุนูุฏ (base64_jpeg, width, height) ุฃู None ุนูุฏ ุงููุดู. | |
| """ | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return None | |
| result = await cdp.send("Page.captureScreenshot", { | |
| "format": "jpeg", "quality": quality, "fromSurface": True, "captureBeyondViewport": False, | |
| }, timeout=8.0) | |
| if not result: | |
| return None | |
| data = result.get("result", {}).get("data") | |
| if not data: | |
| return None | |
| return data, SCREEN_W, SCREEN_H | |
| def _draw_mouse_marker_on_jpeg(b64_jpeg: str, mx: int, my: int) -> str: | |
| """ูุฑุณู ุนูุงู ุฉ ุตุบูุฑุฉ ูู ููุน ุงููุฃุฑุฉ ููู ุงูุตูุฑุฉ (ู ุทููุจุฉ ุจุตุฑูุงู ููุฐูุงุก ุงูุงุตุทูุงุนู | |
| ููุฑู ุฃูู ุงูู ุคุดุฑ). ุงูุนู ููุฉ ุฎูููุฉ (ุฑุณู ููุทุ ุจุฏูู resize) ูุชูุจูู ุฒู ู ูู ููุทุฉ ู ูุฎูุถุงู.""" | |
| try: | |
| from PIL import Image, ImageDraw | |
| import io as _io | |
| raw = base64.b64decode(b64_jpeg) | |
| img = Image.open(_io.BytesIO(raw)).convert("RGB") | |
| draw = ImageDraw.Draw(img) | |
| r = 9 | |
| draw.ellipse([(mx - r, my - r), (mx + r, my + r)], outline=(255, 60, 60), width=2) | |
| draw.line([(mx - 14, my), (mx + 14, my)], fill=(255, 60, 60), width=1) | |
| draw.line([(mx, my - 14), (mx, my + 14)], fill=(255, 60, 60), width=1) | |
| buf = _io.BytesIO() | |
| img.save(buf, format="JPEG", quality=80, optimize=True) | |
| return base64.b64encode(buf.getvalue()).decode() | |
| except Exception: | |
| return b64_jpeg # ูู ูุดู ุงูุฑุณู ูุฃู ุณุจุจุ ุฃุนุฏ ุงูุตูุฑุฉ ุงูุฃุตููุฉ ุจุฏู ุงููุดู ุงููุงู ู | |
| def _grid_variant(b64_jpeg: str, grid_step: int, color: tuple, label_every: int) -> str: | |
| """ | |
| ููุชุฌ ูุณุฎุฉ ู ู ุงูุตูุฑุฉ ู ุน ุดุจูุฉ ุฅุญุฏุงุซูุงุช ู ุฑุณูู ุฉ ููููุง โ ุชุณุงุนุฏ ุงูุฐูุงุก ุงูุงุตุทูุงุนู | |
| ุนูู ุชุญุฏูุฏ ุฅุญุฏุงุซูุงุช ุฏูููุฉ ููููุฑ. ููุณ ููุฑุฉ v13 ููู ู ุจููุฉ ููู JPEG ุงูุฌุงูุฒ ู ู | |
| CDP ู ุจุงุดุฑุฉ ุจุฏู ุฅุนุงุฏุฉ ุงูุฑุณู ู ู raw pixels. | |
| """ | |
| try: | |
| from PIL import Image, ImageDraw | |
| import io as _io | |
| raw = base64.b64decode(b64_jpeg) | |
| img = Image.open(_io.BytesIO(raw)).convert("RGB") | |
| draw = ImageDraw.Draw(img, "RGBA") | |
| w, h = img.size | |
| major = (*color, 130) | |
| minor = (*color, 40) | |
| x = grid_step | |
| while x < w: | |
| is_major = x % label_every == 0 | |
| draw.line([(x, 0), (x, h)], fill=major if is_major else minor, width=1) | |
| if is_major: | |
| draw.rectangle([(x + 1, 2), (x + 34, 15)], fill=(0, 0, 0, 175)) | |
| draw.text((x + 2, 3), str(x), fill=color) | |
| x += grid_step | |
| y = grid_step | |
| while y < h: | |
| is_major = y % label_every == 0 | |
| draw.line([(0, y), (w, y)], fill=major if is_major else minor, width=1) | |
| if is_major: | |
| draw.rectangle([(2, y + 1), (38, y + 14)], fill=(0, 0, 0, 175)) | |
| draw.text((3, y + 2), str(y), fill=color) | |
| y += grid_step | |
| final = img.convert("RGB") | |
| buf = _io.BytesIO() | |
| final.save(buf, format="JPEG", quality=78, optimize=True) | |
| return base64.b64encode(buf.getvalue()).decode() | |
| except Exception: | |
| return b64_jpeg | |
| async def capture_full(sess: dict, quality: int = 75) -> dict: | |
| """ | |
| ููุชูุท ููุทุฉ ูุงุญุฏุฉ ูููุชุฌ ู ููุง: ูุณุฎุฉ ูุธููุฉ + ูุณุฎุฉ ุจุนูุงู ุฉ ุงููุฃุฑุฉ + ุดุจูุชุงู | |
| ุฅุญุฏุงุซูุงุช (ุนุงุฏูุฉ ูุฏูููุฉ) โ ุจููุณ ุชูุณูู ุญููู ุงูุฑุฏ ุงูุฐู ุชุชููุนู ุงููุงุฌูุฉ ุงูุฃู ุงู ูุฉ | |
| (data, data_grid, data_grid2, data_grid3, width, height, mouse_x, mouse_y). | |
| ูุนู ู ู ุน ููุง ุงูู ุญุฑููู ุญุณุจ sess["engine"]: | |
| "chromium" (ุงูุชุฑุงุถู) โ ุนุจุฑ CDP ู ุจุงุดุฑุฉ (cdp_screenshot) | |
| "camoufox" โ ุนุจุฑ Playwright screenshot (ุฃุจุทุฃ ููููุงู ููู ู ุถุงุฏ ูููุดู) | |
| """ | |
| if sess.get("engine") == "camoufox": | |
| cam = await get_camoufox_session(sess["user_id"]) | |
| if not cam: | |
| return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "", | |
| "width": SCREEN_W, "height": SCREEN_H, | |
| "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0), | |
| "is_placeholder": True} | |
| raw_b64 = await cam.screenshot_b64() | |
| if not raw_b64: | |
| return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "", | |
| "width": SCREEN_W, "height": SCREEN_H, | |
| "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0), | |
| "is_placeholder": True} | |
| w, h = SCREEN_W, SCREEN_H | |
| mx, my = sess.get("last_mouse_x", w // 2), sess.get("last_mouse_y", h // 2) | |
| def _build_cam(): | |
| clean = _draw_mouse_marker_on_jpeg(raw_b64, mx, my) | |
| grid1 = _grid_variant(clean, 50, (0, 255, 180), 100) | |
| grid2 = _grid_variant(clean, 20, (255, 60, 60), 60) | |
| return clean, grid1, grid2 | |
| clean, grid1, grid2 = await asyncio.to_thread(_build_cam) | |
| return {"data": clean, "data_grid": grid1, "data_grid2": grid2, "data_grid3": grid2, | |
| "width": w, "height": h, "mouse_x": mx, "mouse_y": my, "is_placeholder": False} | |
| shot = await cdp_screenshot(sess, quality) | |
| if not shot: | |
| return {"data": "", "data_grid": "", "data_grid2": "", "data_grid3": "", | |
| "width": SCREEN_W, "height": SCREEN_H, | |
| "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0), | |
| "is_placeholder": True} | |
| raw_b64, w, h = shot | |
| mx, my = sess.get("last_mouse_x", w // 2), sess.get("last_mouse_y", h // 2) | |
| def _build(): | |
| clean = _draw_mouse_marker_on_jpeg(raw_b64, mx, my) | |
| grid1 = _grid_variant(clean, 50, (0, 255, 180), 100) | |
| grid2 = _grid_variant(clean, 20, (255, 60, 60), 60) | |
| return clean, grid1, grid2 | |
| clean, grid1, grid2 = await asyncio.to_thread(_build) | |
| return { | |
| "data": clean, "data_grid": grid1, "data_grid2": grid2, "data_grid3": grid2, | |
| "width": w, "height": h, "mouse_x": mx, "mouse_y": my, | |
| "is_placeholder": False, | |
| } | |
| def _frame_hash(data: str) -> str: | |
| return hashlib.md5(data.encode()).hexdigest() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ูุฃุฑุฉ/ููุจูุฑุฏ โ ุนุจุฑ CDP Input domain (ุจุฏูู xdotool/X server) โโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def cdp_mouse_move(sess: dict, x: int, y: int): | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mouseMoved", "x": x, "y": y, | |
| }, timeout=4.0) | |
| sess["last_mouse_x"], sess["last_mouse_y"] = x, y | |
| async def cdp_mouse_click(sess: dict, x: int, y: int, button: str = "left", double: bool = False): | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| await cdp_mouse_move(sess, x, y) | |
| click_count = 2 if double else 1 | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mousePressed", "x": x, "y": y, "button": button, "clickCount": click_count, | |
| }, timeout=4.0) | |
| await asyncio.sleep(0.04) | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mouseReleased", "x": x, "y": y, "button": button, "clickCount": click_count, | |
| }, timeout=4.0) | |
| async def cdp_mouse_drag(sess: dict, x1: int, y1: int, x2: int, y2: int, steps: int = 8): | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| await cdp_mouse_move(sess, x1, y1) | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mousePressed", "x": x1, "y": y1, "button": "left", "clickCount": 1, | |
| }, timeout=4.0) | |
| for i in range(1, steps + 1): | |
| ix = x1 + (x2 - x1) * i // steps | |
| iy = y1 + (y2 - y1) * i // steps | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mouseMoved", "x": ix, "y": iy, "button": "left", | |
| }, timeout=3.0) | |
| await asyncio.sleep(0.02) | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mouseReleased", "x": x2, "y": y2, "button": "left", "clickCount": 1, | |
| }, timeout=4.0) | |
| sess["last_mouse_x"], sess["last_mouse_y"] = x2, y2 | |
| async def cdp_scroll(sess: dict, x: int, y: int, delta_y: int): | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| await cdp.send("Input.dispatchMouseEvent", { | |
| "type": "mouseWheel", "x": x, "y": y, "deltaX": 0, "deltaY": delta_y, | |
| }, timeout=4.0) | |
| # ุฎุฑูุทุฉ ุฃุณู ุงุก ู ูุงุชูุญ ุดุงุฆุนุฉ โ ุฃููุงุฏ CDP (windowsVirtualKeyCode + key) | |
| _KEY_MAP = { | |
| "Return": (13, "Enter"), "Enter": (13, "Enter"), | |
| "Tab": (9, "Tab"), "Escape": (27, "Escape"), "BackSpace": (8, "Backspace"), | |
| "Delete": (46, "Delete"), "Left": (37, "ArrowLeft"), "Right": (39, "ArrowRight"), | |
| "Up": (38, "ArrowUp"), "Down": (40, "ArrowDown"), "Home": (36, "Home"), "End": (35, "End"), | |
| "space": (32, " "), "F5": (116, "F5"), | |
| } | |
| async def cdp_key_press(sess: dict, key_name: str): | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| code, key = _KEY_MAP.get(key_name, (0, key_name)) | |
| params = {"type": "keyDown", "key": key} | |
| if code: | |
| params["windowsVirtualKeyCode"] = code | |
| params["nativeVirtualKeyCode"] = code | |
| await cdp.send("Input.dispatchKeyEvent", params, timeout=4.0) | |
| params["type"] = "keyUp" | |
| await cdp.send("Input.dispatchKeyEvent", params, timeout=4.0) | |
| async def cdp_hotkey(sess: dict, keys: list): | |
| """ุงุฎุชุตุงุฑ ู ุชุนุฏุฏ ุงูู ูุงุชูุญ ู ุซู ['ctrl','l'] ุฃู ['alt','Left'].""" | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| modifiers = 0 | |
| mod_map = {"alt": 1, "ctrl": 2, "control": 2, "meta": 4, "shift": 8} | |
| plain_keys = [] | |
| for k in keys: | |
| kl = k.lower() | |
| if kl in mod_map: | |
| modifiers |= mod_map[kl] | |
| else: | |
| plain_keys.append(k) | |
| for k in plain_keys: | |
| code, key = _KEY_MAP.get(k, (ord(k[0].upper()) if len(k) == 1 else 0, k)) | |
| await cdp.send("Input.dispatchKeyEvent", { | |
| "type": "rawKeyDown", "key": key, "modifiers": modifiers, | |
| "windowsVirtualKeyCode": code, "nativeVirtualKeyCode": code, | |
| }, timeout=4.0) | |
| await cdp.send("Input.dispatchKeyEvent", { | |
| "type": "keyUp", "key": key, "modifiers": modifiers, | |
| "windowsVirtualKeyCode": code, "nativeVirtualKeyCode": code, | |
| }, timeout=4.0) | |
| async def cdp_type_text(sess: dict, text: str): | |
| """ | |
| ููุชุจ ูุตุงู ุนุจุฑ Input.insertText โ ูุฏุนู ูู ุงููุบุงุช (ุนุฑุจู/ุฅูุฌููุฒู/ูุฑูุณู) | |
| ุจุดูู ุฃุตูู ุนุจุฑ CDP ููุณูุ ุจุฏูู ุญุงุฌุฉ ูู xclip ุฃู ุญููุฉ clipboard+paste | |
| ุงูุชู ูุงูุช ุถุฑูุฑูุฉ ุณุงุจูุงู ุจุณุจุจ ูููุฏ xdotool ู ุน ุงููุตูุต ุบูุฑ ุงููุงุชูููุฉ. | |
| """ | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| await cdp.send("Input.insertText", {"text": text}, timeout=6.0) | |
| async def cdp_navigate(sess: dict, url: str) -> bool: | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return False | |
| result = await cdp.send("Page.navigate", {"url": url}, timeout=15.0) | |
| return bool(result and "error" not in result.get("result", {})) | |
| async def cdp_clipboard_write(sess: dict, text: str): | |
| """ููุณุฎ ูุตุงู ูุญุงูุธุฉ afterward-paste ุฏุงุฎููุฉ ุนุจุฑ ู ุชุบููุฑ JS ุจุณูุท (ุจุฏูู ุฎููู | |
| ูู xclipุ ูุฃูู ูุง ููุฌุฏ X server ููุง ุฃุตูุงู ุชูุจูู ุนููู ุญุงูุธุฉ ูุธุงู ุญููููุฉ).""" | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return | |
| js = f"window.__zclip = {json.dumps(text)};" | |
| await cdp.send("Runtime.evaluate", {"expression": js}, timeout=4.0) | |
| async def cdp_clipboard_read(sess: dict) -> str: | |
| cdp = sess.get("cdp") | |
| if not cdp or not cdp.alive: | |
| return "" | |
| result = await cdp.send("Runtime.evaluate", { | |
| "expression": "window.__zclip || ''", "returnByValue": True, | |
| }, timeout=4.0) | |
| if not result: | |
| return "" | |
| return result.get("result", {}).get("result", {}).get("value", "") or "" | |
| async def cdp_get_screen_info(sess: dict) -> dict: | |
| return {"width": SCREEN_W, "height": SCREEN_H, | |
| "mouse_x": sess.get("last_mouse_x", 0), "mouse_y": sess.get("last_mouse_y", 0)} | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ SafeSearch โ ููุณ ู ูุทู v13 ู ุญููุธุงู ูู ุง ูู (ูุนู ู ุจุดูู ุฌูุฏ) โโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _BING_RE = re.compile(r"https?://(?:www\.)?bing\.com[^\s'\"]*") | |
| _DDG_RE = re.compile(r"https?://(?:www\.)?duckduckgo\.com[^\s'\"]*") | |
| _GOOG_RE = re.compile(r"https?://(?:www\.)?google\.[a-z.]+[^\s'\"]*") | |
| def _safe_search(text: str) -> str: | |
| def _bing(m): | |
| u = m.group(0) | |
| return re.sub(r"adlt=\w+", "adlt=strict", u) if "adlt=" in u else u + ("&" if "?" in u else "?") + "adlt=strict" | |
| def _ddg(m): | |
| u = m.group(0) | |
| return re.sub(r"kp=\d", "kp=1", u) if "kp=" in u else u + ("&" if "?" in u else "?") + "kp=1" | |
| def _goog(m): | |
| u = m.group(0) | |
| return re.sub(r"safe=\w+", "safe=strict", u) if "safe=" in u else u + ("&" if "?" in u else "?") + "safe=strict" | |
| return _GOOG_RE.sub(_goog, _DDG_RE.sub(_ddg, _BING_RE.sub(_bing, text))) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Terminal โ ุชูููุฐ ุฃูุงู ุฑ bash (ู ููุตู ุชู ุงู ุงู ุนู Chromium) โโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _terminal_sem = asyncio.Semaphore(6) | |
| _PKILL_RE = re.compile(r"\b(pkill|killall)\s+(-9\s+)?-f\s+(['\"]?)([a-zA-Z0-9_./-]+)\3") | |
| def _sanitize_pkill(cmd: str) -> str: | |
| def _fix(m): | |
| tool, d9, _q, p = m.group(1), m.group(2) or "", m.group(3), m.group(4) | |
| sp = f"[{p[0]}]{p[1:]}" if len(p) > 1 else p | |
| return f"{tool} {d9}-f '{sp}'" | |
| return _PKILL_RE.sub(_fix, cmd) | |
| async def run_cmd(cmd: str, timeout: int = 60) -> dict: | |
| cmd = _sanitize_pkill(_safe_search(cmd)) | |
| async with _terminal_sem: | |
| env = {**os.environ, "PYTHONIOENCODING": "utf-8", "LANG": "en_US.UTF-8"} | |
| def _exec(): | |
| try: | |
| r = subprocess.run(cmd, shell=True, capture_output=True, text=True, | |
| timeout=timeout, env=env, executable="/bin/bash") | |
| return {"stdout": r.stdout[-15000:], "stderr": r.stderr[-3000:], "returncode": r.returncode} | |
| except subprocess.TimeoutExpired: | |
| return {"stdout": "", "stderr": f"โฑ๏ธ Timeout {timeout}s", "returncode": -1} | |
| except Exception as e: | |
| return {"stdout": "", "stderr": str(e), "returncode": -1} | |
| return await asyncio.to_thread(_exec) | |
| def _search_sources(query: str) -> list: | |
| q = urllib.parse.quote_plus(query) | |
| return [ | |
| {"name": "DuckDuckGo Instant", | |
| "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)]\""}, | |
| {"name": "Wikipedia EN", | |
| "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])\""}, | |
| {"name": "DuckDuckGo HTML", | |
| "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])]\""}, | |
| {"name": "HackerNews", | |
| "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])]\""}, | |
| {"name": "Reddit", | |
| "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])]\""}, | |
| ] | |
| def _result_empty(s: str) -> bool: | |
| if not s or len(s.strip()) < 15: | |
| return True | |
| return "ans: none" in s.lower() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Brave Search API โ ููุงุฉ ุงูุจุญุซ ุงูุฑุณู ูุฉ ุงูู ุตุฑููุญ ุจูุง โโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุนูู ุนูุณ ุฒูุงุฑุฉ duckduckgo.com/google.com ูู ููุน ูุนูู ุนุจุฑ ุงูู ุชุตูุญ (ุงููู ููุงุฌู | |
| # ูุงุจุชุดุง ูุฃู ูุฐู ุงูู ุญุฑูุงุช ุชูุฑู ุงูุทูุจุงุช ุงูุขููุฉ)ุ Brave Search API ู ุตู ู ุฃุตูุงู | |
| # ูููุณุชุฎุฏู ุจุฑู ุฌูุงู โ ุจุฏูู ูุงุจุชุดุง ุฅุทูุงูุงูุ ูุฃูู ู ุฑุฎูุต ููุฐุง ุงูุบุฑุถ ุชุญุฏูุฏุงู. | |
| # ูุฐุง ุจุงูุถุจุท ุฃุณููุจ Manus: ุจุญุซ ูุธูู ุนุจุฑ API/ุณุทุฑ ุฃูุงู ุฑ ูุฑุฌุน ูุชุงุฆุฌ ูุตูุฉ ู ุจุงุดุฑุฉุ | |
| # ูุชุญ ู ุชุตูุญ ูุนูู ููุญูุธ ููุท ูู ุง ูุญุชุงุฌ ุงูู ุณุชุฎุฏู "ุชุตูุญ ู ููุน ุจุนููู ุจุตุฑูุงู". | |
| # | |
| # ุงูุชูุนูู: ุนููู ู ุชุบููุฑ ุงูุจูุฆุฉ BRAVE_API_KEY ุจู ูุชุงุญู ู ู | |
| # https://api-dashboard.search.brave.com (ููู tier ู ุฌุงูู โ 2000 ุงุณุชุนูุงู /ุดูุฑ). | |
| BRAVE_API_KEY = os.environ.get("BRAVE_API_KEY", "").strip() | |
| BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search" | |
| async def brave_search(query: str, count: int = 8) -> dict: | |
| """ | |
| ูุณุชุฏุนู Brave Search API ู ุจุงุดุฑุฉ (ุจุฏูู ู ุชุตูุญุ ุจุฏูู ูุงุจุชุดุง). ููุนูุฏ ูุชุงุฆุฌ | |
| ู ูุณููุฉ ูุตูุงู ุจููุณ ุดูู ู ุฎุฑุฌุงุช run_cmd ุงูุนุงุฏูุฉุ ุนุดุงู ุชูุฏู ุฌ ุจุณูุงุณุฉ ู ุน ุจููุฉ | |
| ู ูุทู ุงูุชุฑู ูุงู ูุงููุฑููุช ุฅูุฏ ุจุฏูู ุฃู ุชุนุฏูู ุฅุถุงูู ู ุทููุจ ููุงู. | |
| """ | |
| if not BRAVE_API_KEY: | |
| return {"stdout": "", "stderr": "BRAVE_API_KEY ุบูุฑ ู ูุนุฑููู โ ุฑุงุฌุน ุงูุฅุนุฏุงุฏุงุช", "returncode": -1} | |
| def _fetch(): | |
| import urllib.request | |
| import urllib.error | |
| url = f"{BRAVE_ENDPOINT}?q={urllib.parse.quote_plus(query)}&count={min(count, 20)}&safesearch=moderate" | |
| req = urllib.request.Request(url, headers={ | |
| "Accept": "application/json", | |
| "Accept-Encoding": "gzip", | |
| "X-Subscription-Token": BRAVE_API_KEY, | |
| }) | |
| try: | |
| with urllib.request.urlopen(req, timeout=15) as r: | |
| raw = r.read() | |
| if r.info().get("Content-Encoding") == "gzip": | |
| import gzip | |
| raw = gzip.decompress(raw) | |
| return json.loads(raw), None | |
| except urllib.error.HTTPError as e: | |
| body = e.read().decode(errors="ignore")[:300] | |
| return None, f"HTTP {e.code}: {body}" | |
| except Exception as e: | |
| return None, str(e) | |
| data, err = await asyncio.to_thread(_fetch) | |
| if err: | |
| return {"stdout": "", "stderr": f"Brave Search error: {err}", "returncode": -1} | |
| lines = [] | |
| # ุงูุฅุฌุงุจุฉ ุงูุณุฑูุนุฉ (infobox) ุฅู ููุฌุฏุช | |
| infobox = data.get("infobox", {}) or {} | |
| if infobox.get("long_desc") or infobox.get("description"): | |
| lines.append("ANS: " + (infobox.get("long_desc") or infobox.get("description", ""))[:400]) | |
| web_results = (data.get("web", {}) or {}).get("results", []) or [] | |
| for i, r in enumerate(web_results[:count], 1): | |
| title = r.get("title", "") | |
| desc = re.sub(r"<[^>]+>", "", r.get("description", "")) | |
| url = r.get("url", "") | |
| lines.append(f"{i}. {title}\n {desc[:220]}\n {url}") | |
| if not lines: | |
| return {"stdout": "", "stderr": "ูุง ุชูุฌุฏ ูุชุงุฆุฌ ู ู Brave Search ููุฐุง ุงูุงุณุชุนูุงู ", "returncode": 0} | |
| return {"stdout": "\n".join(lines)[:15000], "stderr": "", "returncode": 0} | |
| # ูู ุท ููุชูุท ุฃู ุฑ ุจุญุซ "ุฒุงุฆู" ู ุฎุตุต ููุฑุณูู ุงูุฐูุงุก ุงูุงุตุทูุงุนู ุนุจุฑ ุฃุฏุงุฉ terminal | |
| # ุจุตูุบุฉ ุจุณูุทุฉ: search: ูุต ุงูุงุณุชุนูุงู (ุฃู search "ูุต ุงูุงุณุชุนูุงู ") | |
| _SEARCH_CMD_RE = re.compile(r"^\s*search\s*:?\s*[\"']?(.+?)[\"']?\s*$", re.IGNORECASE) | |
| async def run_cmd_smart(cmd: str, timeout: int = 60) -> dict: | |
| # โโ ุฃููููุฉ ูุตูู: ุฃู ุฑ ุจุญุซ ุตุฑูุญ "search: ..." โ Brave Search API ู ุจุงุดุฑุฉ โโ | |
| m_search = _SEARCH_CMD_RE.match(cmd) | |
| if m_search and "curl" not in cmd and "http" not in cmd: | |
| query = m_search.group(1).strip() | |
| if BRAVE_API_KEY: | |
| res = await brave_search(query) | |
| if res["returncode"] == 0 and not _result_empty(res["stdout"]): | |
| return res | |
| # ูุดู Brave (ู ูุชุงุญ ุฎุงุทุฆ/ุญุตุฉ ู ูุชููุฉ) โ ุงุณุชู ุฑ ุจุงูุฎุทุฉ ุงูุจุฏููุฉ ุฃุฏูุงู | |
| # ูุง ููุฌุฏ ู ูุชุงุญ Brave ุฃู ูุดู ุงูุทูุจ โ ุงุณุชุฎุฏู ู ุตุงุฏุฑ API ุงูู ุฌุงููุฉ ุงูุฃุฎุฑู | |
| return await _fallback_search_sources(query) | |
| res = await run_cmd(cmd, timeout) | |
| if res["returncode"] == 0 and not _result_empty(res["stdout"]): | |
| return res | |
| is_search = "curl" in cmd and any(x in cmd for x in ["search", "duckduck", "bing", "google", "wikipedia", "reddit"]) | |
| if not is_search: | |
| return res | |
| m = re.search(r"[?&]q=([^&'\"\s]+)", cmd) | |
| query = urllib.parse.unquote(m.group(1).replace("+", " ")).strip() if m else "" | |
| if len(query) < 3: | |
| return res | |
| return await _fallback_search_sources(query, prior_result=res) | |
| async def _fallback_search_sources(query: str, prior_result: dict = None) -> dict: | |
| """ูุฌุฑุจ Brave ุฃููุงู (ูู ู ุชุงุญ)ุ ุซู ู ุตุงุฏุฑ API ุงูู ุฌุงููุฉ ุงูุฃุฎุฑู (DuckDuckGo | |
| Instant Answer, Wikipedia, HackerNews, Reddit) โ ูู ูุฐู APIs ุฑุณู ูุฉ | |
| ูู ุตุฑููุญ ุจุงุณุชุฎุฏุงู ูุง ุขููุงูุ ูุง ุชุญุชุงุฌ ูุชุญ ู ุชุตูุญ ููุง ุชูุงุฌู ูุงุจุชุดุง ุฅุทูุงูุงู.""" | |
| if BRAVE_API_KEY: | |
| res = await brave_search(query) | |
| if res["returncode"] == 0 and not _result_empty(res["stdout"]): | |
| return res | |
| results = [] | |
| for src in _search_sources(query): | |
| r2 = await run_cmd(src["cmd"], 20) | |
| if not _result_empty(r2["stdout"]): | |
| results.append(f"[{src['name']}]\n{r2['stdout']}") | |
| if len(results) >= 2: | |
| break | |
| if results: | |
| return {"stdout": "\n\n".join(results)[:15000], "stderr": "", "returncode": 0} | |
| return prior_result or {"stdout": "", "stderr": "ูู ููุนุซุฑ ุนูู ูุชุงุฆุฌ ู ู ุฃู ู ุตุฏุฑ", "returncode": -1} | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ FastAPI App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = FastAPI(title="Z-Computer-Mode v20 โ Headless-Native") | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, | |
| allow_methods=["*"], allow_headers=["*"]) | |
| async def root(): | |
| with _sessions_lock: | |
| n = len(_sessions) | |
| rows = "".join( | |
| f"<tr><td>{uid[:30]}</td><td>{s['port']}</td>" | |
| f"<td>{'๐ข ู ุชุตู' if s.get('active_ws') else 'โซ ุบูุฑ ู ุชุตู'}</td></tr>" | |
| for uid, s in _sessions.items() | |
| ) | |
| return f"""<!DOCTYPE html><html><head><meta charset="utf-8"> | |
| <title>Z Computer Mode v20</title> | |
| <style>body{{background:#080808;color:#00d4aa;font-family:monospace;padding:40px}} | |
| h1{{color:#4a90d9}}.ok{{color:#00ff88}}.info{{color:#888;font-size:13px}} | |
| table{{border-collapse:collapse;width:100%}}td,th{{border:1px solid #333;padding:6px 10px;font-size:13px}} | |
| th{{background:#1a1a2e;color:#4a90d9}}</style></head><body> | |
| <h1>๐ฅ๏ธ THE Z AI โ Computer Mode Server v20 (Headless-Native)</h1> | |
| <p class="ok">โ ูุนู ู โ ุจุฏูู Xvfbุ Chromium headless=new ููุท</p> | |
| <p>๐ ุงูู ุชุตูุญ ุงูู ุนุชู ุฏ: <b>{BROWSER_BIN}</b></p> | |
| <p>๐ Brave Search API: <b>{'โ ู ูุนูู' if BRAVE_API_KEY else 'โ ุบูุฑ ู ูุนุฑููู (ุนููู BRAVE_API_KEY)'}</b></p> | |
| <p>๐ฆ Camoufox (ู ุถุงุฏ ูููุดู): <b>{'โ ู ุชุงุญ โ ุฅุฑุณุงู action: switch_engine ููุชูุนูู' if _CAMOUFOX_LIB_AVAILABLE and CAMOUFOX_ENABLED else 'โ ุบูุฑ ู ุชุงุญ'}</b></p> | |
| <p>๐ฅ ุงูุฌูุณุงุช ุงููุดุทุฉ: <b>{n}</b> / {MAX_SESSIONS}</p> | |
| <hr style="border-color:#333"><h3>๐ค ุงูุฌูุณุงุช</h3> | |
| <table><tr><th>User ID</th><th>CDP Port</th><th>ุงูุญุงูุฉ</th></tr> | |
| {rows or '<tr><td colspan="3" style="text-align:center;color:#666">ูุง ุชูุฌุฏ ุฌูุณุงุช</td></tr>'} | |
| </table> | |
| <hr style="border-color:#333"> | |
| <p class="info">Endpoints: /health ยท /ws?user_id=email (WebSocket)</p> | |
| </body></html>""" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ู ุนุงูุฌ ุงูุฃูุงู ุฑ โ ุจุฑูุชูููู ู ุทุงุจู 100% ูููุงุฌูุฉ ุงูุฃู ุงู ูุฉ โโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ุงูุฃูุงู ุฑ ุงูู ุฏุนูู ุฉ (ูู ู ุง ุชุฑุณูู index_plus_prompt_enhanced_v2-3.html ูุนููุงู): | |
| # screenshot, terminal, mouse_move, mouse_click, mouse_drag, keyboard_type, | |
| # keyboard_hotkey, keyboard_press, scroll, clipboard_write, clipboard_read, | |
| # paste, open_app, open_tab, close_tab, browser_back, browser_forward, | |
| # browser_search, screen_info, reset_computer | |
| # ุนุชุจุฉ ุฃุจุณุท ุจูุซูุฑ ู ู v13: ุจู ุง ุฃู ูุง ููุฌุฏ X server ู ููุตู ูู ูู ุฃู "ูุชุฌู ุฏ" ุฏูู | |
| # ุฃู ูุชููู Chromium ููุณูุ ุฃู ูุดู ุงูุชูุงุท ุงูุขู ูุนูู ุนู ููุงู ุฃู Chromium ุชููู ุฃู | |
| # ุงุชุตุงู CDP ุงููุทุน โ ูุดูุฉ ูุงุญุฏุฉ ูุงููุฉ ููุชุฃูุฏ ูู ุญุงููุฉ ุฅุนุงุฏุฉ ุงูุงุชุตุงู ู ุจุงุดุฑุฉ ูุจู | |
| # ุงููุฌูุก ูุฅุนุงุฏุฉ ุชุดุบูู ุงูุนู ููุฉ ุจุงููุงู ู. | |
| _RECONNECT_THRESHOLD = 1 | |
| _FULL_RESET_THRESHOLD = 3 | |
| _MAX_ROUNDS = 4 | |
| async def handle_action(ws: WebSocket, msg: dict, sess: dict): | |
| action = msg.get("action", "") | |
| data = msg.get("data", {}) | |
| async def send(obj): | |
| try: | |
| await ws.send_text(json.dumps(obj, ensure_ascii=False)) | |
| except Exception: | |
| pass | |
| async def do_screenshot(label: str = "", auto: bool = False): | |
| """ | |
| ููุชูุท ููุฑุณู ููุทุฉ ุดุงุดุฉุ ู ุน ุชุนุงูู ุชููุงุฆู ุจุณูุท: | |
| ูุดูุฉ 1 โ ุฃุนุฏ ุงูุงุชุตุงู ุจู CDP ููุท (Chromium ุบุงูุจุงู ุญูุ ุงูุงุชุตุงู ููุท ุณูุท) | |
| ูุดูุฉ 3 โ ุฃุนุฏ ุชุดุบูู Chromium ุจุงููุงู ู (reset_session) | |
| ุจุนุฏ ุฐูู โ ุฃุฑุณู computer_reset ุตุฑุงุญุฉ ููุนู ูู ุจุฏู ุงูุตู ุช | |
| """ | |
| for _round in range(_MAX_ROUNDS): | |
| ok_browser = await ensure_browser_running(sess) | |
| if not ok_browser: | |
| sess["consecutive_failures"] += 1 | |
| else: | |
| async with sess["shot_lock"]: | |
| result = await capture_full(sess, quality=75) | |
| if result.get("data") and not result.get("is_placeholder"): | |
| sess["consecutive_failures"] = 0 | |
| fh = _frame_hash(result["data"]) | |
| if not auto or fh != sess.get("last_frame_hash", ""): | |
| sess["last_frame_hash"] = fh | |
| await send({ | |
| "type": "screenshot", | |
| "data": result["data"], | |
| "data_grid": result.get("data_grid", ""), | |
| "data_grid2": result.get("data_grid2", ""), | |
| "data_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), | |
| "auto": auto, "label": label, | |
| "screen_width": result["width"], "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"], | |
| "has_grid": True, | |
| }) | |
| return | |
| sess["consecutive_failures"] += 1 | |
| fails = sess["consecutive_failures"] | |
| if fails >= _FULL_RESET_THRESHOLD: | |
| await send({"type": "computer_reset", | |
| "msg": "๐ ุฅุนุงุฏุฉ ุชุดุบูู ุชููุงุฆูุฉ ูููู ุจููุชุฑ ุจุนุฏ ูุดู ู ุชูุฑุฑ โ ูุญุธุงุช ููุนูุฏ ุทุจูุนูุงู", | |
| "auto": True}) | |
| await reset_session(sess["user_id"]) | |
| await asyncio.sleep(1.0) | |
| continue | |
| elif fails >= _RECONNECT_THRESHOLD: | |
| # ุฃุนุฏ ุงูุงุชุตุงู ุจู CDP ููุท (ุฃุฎู ุจูุซูุฑ ู ู ุฅุนุงุฏุฉ ุชุดุบูู Chromium ุจุงููุงู ู) | |
| sess["cdp"] = None | |
| await asyncio.sleep(0.3) | |
| continue | |
| await asyncio.sleep(0.3) | |
| # ุงุณุชููุฏูุง ูู ุงูู ุญุงููุงุช โ ุฃุนูู ุงูุนู ูู ุตุฑุงุญุฉ ุจุฏู ุงูุตู ุช ุงูุฃุจุฏู | |
| await send({"type": "computer_reset", | |
| "msg": "โ ๏ธ ุงุณุชู ุฑุช ู ุดููุฉ ุงูุงูุชูุงุท โ ุงูุฑุฌุงุก ุทูุจ ููุทุฉ ุดุงุดุฉ ุฌุฏูุฏุฉ", | |
| "auto": True}) | |
| def bg_shot(label: str = "", delay: float = 0.5): | |
| async def _run(): | |
| await asyncio.sleep(delay) | |
| await do_screenshot(label, auto=True) | |
| asyncio.create_task(_run()) | |
| # โโ reset_computer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if action == "reset_computer": | |
| await send({"type": "ack", "action": "reset_computer", "status": "resetting"}) | |
| await reset_session(sess["user_id"]) | |
| await asyncio.sleep(1.0) | |
| await do_screenshot("after reset") | |
| await send({"type": "computer_reset", "msg": "โ ุชู ุฅุนุงุฏุฉ ุถุจุท ุงููู ุจููุชุฑ โ ุงูุดุงุดุฉ ุฌุฏูุฏุฉ ุชู ุงู ุงู"}) | |
| return | |
| # โโ screenshot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if action == "screenshot": | |
| _task = asyncio.create_task(do_screenshot("explicit screenshot")) | |
| def _on_done(t: asyncio.Task): | |
| exc = t.exception() if not t.cancelled() else None | |
| if exc: | |
| print(f"[screenshot-task:{sess['user_id']}] โ {exc}") | |
| _task.add_done_callback(_on_done) | |
| return | |
| # โโ terminal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if action == "terminal": | |
| cmd = data.get("cmd", "") | |
| if not cmd: | |
| await send({"type": "terminal_result", "stdout": "", "stderr": "no cmd", "returncode": -1}) | |
| return | |
| res = await run_cmd_smart(cmd, int(data.get("timeout", 60))) | |
| await send({"type": "terminal_result", "cmd": cmd, "stdout": res["stdout"], | |
| "stderr": res.get("stderr", ""), "returncode": res["returncode"]}) | |
| return | |
| # โโ ูุญุชุงุฌ ุงูู ุชุตูุญ ุดุบุงูุงูุ ุชุฃูุฏ ุฃููุงู ูุจููุฉ ุงูุฃูุงู ุฑ โโ | |
| engine = sess.get("engine", "chromium") | |
| if engine == "chromium": | |
| await ensure_browser_running(sess) | |
| else: | |
| cam = await get_camoufox_session(sess["user_id"]) | |
| if not cam: | |
| # ูุดู ุจุฏุก Camoufox (ู ูุชุจุฉ ุบูุฑ ู ุซุจููุชุฉ/ูุดู ุฅููุงุน) โ ุงุฑุฌุน ุชููุงุฆูุงู ูู Chromium | |
| print(f"[engine] โ ๏ธ ูุดู Camoufox ูู '{sess['user_id']}' โ ุฑุฌูุน ุชููุงุฆู ูู Chromium") | |
| sess["engine"] = "chromium" | |
| engine = "chromium" | |
| await ensure_browser_running(sess) | |
| # โโ switch_engine โ ุชุจุฏูู ู ุญุฑู ุงูู ุชุตูุญ ููู ุณุชุฎุฏู (chromium โ camoufox) โโ | |
| if action == "switch_engine": | |
| target = data.get("engine", "chromium") | |
| if target not in ("chromium", "camoufox"): | |
| await send({"type": "error", "msg": f"Unknown engine: '{target}'"}) | |
| return | |
| if target == "camoufox" and not _CAMOUFOX_LIB_AVAILABLE: | |
| await send({"type": "error", "msg": "Camoufox ุบูุฑ ู ุซุจููุช ุนูู ูุฐุง ุงูุณูุฑูุฑ"}) | |
| return | |
| sess["engine"] = target | |
| sess["last_frame_hash"] = "" | |
| await send({"type": "ack", "action": "switch_engine", "engine": target, | |
| "msg": f"โ ุชู ุงูุชุจุฏูู ูู ุญุฑู '{target}'" | |
| + (" โ ู ุชุตูุญ ู ุถุงุฏ ูููุดูุ ุฃุจุทุฃ ููููุงู ููู ูุชุฌูุจ ูุงุจุชุดุง ุงูู ูุงูุน ุงูู ุญู ูุฉ ุจุดุฏุฉ" | |
| if target == "camoufox" else " โ ุงูู ุญุฑู ุงูุงูุชุฑุงุถู ุงูุณุฑูุน")}) | |
| bg_shot(f"after switch to {target}", delay=0.8) | |
| return | |
| if action == "mouse_move": | |
| x, y = int(data.get("x", 0)), int(data.get("y", 0)) | |
| if engine == "camoufox": | |
| await cam.mouse_move(x, y) | |
| else: | |
| await cdp_mouse_move(sess, x, y) | |
| await send({"type": "ack", "action": "mouse_move", "x": x, "y": y}) | |
| elif action == "mouse_click": | |
| x, y = int(data.get("x", 0)), int(data.get("y", 0)) | |
| btn = {"left": "left", "middle": "middle", "right": "right"}.get(data.get("button", "left"), "left") | |
| double = bool(data.get("double", False)) | |
| if engine == "camoufox": | |
| await cam.mouse_click(x, y, btn, double) | |
| else: | |
| await cdp_mouse_click(sess, x, y, btn, double) | |
| await send({"type": "ack", "action": "mouse_click", "x": x, "y": y, "button": btn}) | |
| bg_shot("after click", delay=0.5) | |
| elif action == "mouse_drag": | |
| x1, y1 = int(data.get("x1", 0)), int(data.get("y1", 0)) | |
| x2, y2 = int(data.get("x2", 0)), int(data.get("y2", 0)) | |
| if engine == "camoufox": | |
| await cam.mouse_drag(x1, y1, x2, y2) | |
| else: | |
| await cdp_mouse_drag(sess, x1, y1, x2, y2) | |
| await send({"type": "ack", "action": "mouse_drag"}) | |
| bg_shot("after drag", delay=0.4) | |
| elif action == "keyboard_type": | |
| text = _safe_search(data.get("text", "")) | |
| if text: | |
| if engine == "camoufox": | |
| await cam.type_text(text) | |
| else: | |
| await cdp_type_text(sess, text) | |
| await send({"type": "ack", "action": "keyboard_type", "method": engine}) | |
| elif action == "keyboard_hotkey": | |
| keys = data.get("keys", []) | |
| if keys: | |
| if engine == "camoufox": | |
| await cam.hotkey(keys) | |
| else: | |
| await cdp_hotkey(sess, keys) | |
| await send({"type": "ack", "action": "keyboard_hotkey", "keys": keys}) | |
| elif action == "keyboard_press": | |
| key = data.get("key", "") | |
| if key: | |
| if engine == "camoufox": | |
| await cam.key_press(key) | |
| else: | |
| await cdp_key_press(sess, key) | |
| await send({"type": "ack", "action": "keyboard_press"}) | |
| elif action == "scroll": | |
| x, y = int(data.get("x", SCREEN_W // 2)), int(data.get("y", SCREEN_H // 2)) | |
| clicks = max(-5, min(5, int(data.get("clicks", 3)))) | |
| if engine == "camoufox": | |
| await cam.scroll(x, y, delta_y=-clicks * 100) | |
| else: | |
| await cdp_scroll(sess, x, y, delta_y=-clicks * 100) | |
| await send({"type": "ack", "action": "scroll", "clicks": clicks}) | |
| elif action == "clipboard_write": | |
| text = data.get("text", "") | |
| if engine != "camoufox": | |
| await cdp_clipboard_write(sess, text) | |
| # ูู Camoufoxุ ุงููุณุฎ ุงููุนูู ููุญุงูู ุนุจุฑ paste ู ุจุงุดุฑุฉ (ูุง ุญุงุฌุฉ ูุชุฎุฒูู ู ููุตู) | |
| await send({"type": "ack", "action": "clipboard_write", "length": len(text)}) | |
| elif action == "clipboard_read": | |
| text = await cdp_clipboard_read(sess) if engine != "camoufox" else "" | |
| await send({"type": "clipboard_content", "text": text}) | |
| elif action == "paste": | |
| text = data.get("text", "") | |
| if text: | |
| if engine == "camoufox": | |
| await cam.type_text(text) | |
| else: | |
| await cdp_clipboard_write(sess, text) | |
| await cdp_type_text(sess, text) | |
| await send({"type": "ack", "action": "paste"}) | |
| elif action == "open_app": | |
| # ูู ูุฐู ุงูุจููุฉ Chromium/Camoufox ูู "ุงูุชุทุจูู" ุงููุญูุฏุ ุฃู ุฃู ุฑ ูุญูู | |
| # firefox/chromium/chrome ููููู ูุทูุจ ูุชุญ ุตูุญุฉ ูุงุฑุบุฉ ุฌุฏูุฏุฉ ูู ููุณ ุงูู ุชุตูุญ. | |
| cmd = _safe_search(data.get("cmd", "")) | |
| url_match = re.search(r"https?://\S+", cmd) | |
| target_url = url_match.group(0) if url_match else "about:blank" | |
| if engine == "camoufox": | |
| await cam.navigate(target_url) | |
| else: | |
| await cdp_navigate(sess, target_url) | |
| await send({"type": "ack", "action": "open_app", "cmd": cmd}) | |
| bg_shot("after open_app", delay=1.0) | |
| elif action == "open_tab": | |
| url = _safe_search(data.get("url", "") or "about:blank") | |
| if engine == "camoufox": | |
| await cam.navigate(url) | |
| else: | |
| await cdp_navigate(sess, url) | |
| await send({"type": "ack", "action": "open_tab", "url": url}) | |
| bg_shot("after open_tab", delay=1.2) | |
| elif action == "close_tab": | |
| if engine == "camoufox": | |
| await cam.navigate("about:blank") | |
| else: | |
| await cdp_navigate(sess, "about:blank") | |
| await send({"type": "ack", "action": "close_tab"}) | |
| bg_shot("after close_tab", delay=0.4) | |
| elif action == "browser_back": | |
| if engine == "camoufox": | |
| await cam.go_back() | |
| else: | |
| cdp = sess.get("cdp") | |
| if cdp and cdp.alive: | |
| hist = await cdp.send("Page.getNavigationHistory", timeout=4.0) | |
| if hist: | |
| entries = hist.get("result", {}).get("entries", []) | |
| idx = hist.get("result", {}).get("currentIndex", 0) | |
| if idx > 0: | |
| await cdp.send("Page.navigateToHistoryEntry", {"entryId": entries[idx - 1]["id"]}, timeout=5.0) | |
| await send({"type": "ack", "action": "browser_back"}) | |
| bg_shot("after back", delay=0.6) | |
| elif action == "browser_forward": | |
| if engine == "camoufox": | |
| await cam.go_forward() | |
| else: | |
| cdp = sess.get("cdp") | |
| if cdp and cdp.alive: | |
| hist = await cdp.send("Page.getNavigationHistory", timeout=4.0) | |
| if hist: | |
| entries = hist.get("result", {}).get("entries", []) | |
| idx = hist.get("result", {}).get("currentIndex", 0) | |
| if idx < len(entries) - 1: | |
| await cdp.send("Page.navigateToHistoryEntry", {"entryId": entries[idx + 1]["id"]}, timeout=5.0) | |
| await send({"type": "ack", "action": "browser_forward"}) | |
| bg_shot("after forward", delay=0.6) | |
| elif action == "browser_search": | |
| url = _safe_search(data.get("url", "") or data.get("query", "")) | |
| if not re.match(r"^https?://", url): | |
| url = "https://duckduckgo.com/?q=" + urllib.parse.quote_plus(url) | |
| if engine == "camoufox": | |
| await cam.navigate(url) | |
| else: | |
| await cdp_navigate(sess, url) | |
| await send({"type": "ack", "action": "browser_search"}) | |
| bg_shot("after search", delay=1.0) | |
| elif action == "screen_info": | |
| info = await cdp_get_screen_info(sess) | |
| await send({"type": "screen_info", "width": info["width"], "height": info["height"], | |
| "mouse_x": info["mouse_x"], "mouse_y": info["mouse_y"], | |
| "browser": BROWSER_BIN if engine == "chromium" else "camoufox (Firefox anti-detect)", | |
| "engine": engine}) | |
| else: | |
| await send({"type": "error", "msg": f"Unknown action: '{action}'"}) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ WebSocket Endpoint โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def websocket_endpoint(ws: WebSocket, user_id: str = Query(default="anonymous")): | |
| await ws.accept() | |
| sess = await get_or_create_session(user_id, ws) | |
| await ensure_browser_running(sess) | |
| async def _heartbeat(): | |
| while True: | |
| await asyncio.sleep(20) | |
| try: | |
| await ws.send_text(json.dumps({"type": "ping", "ts": int(time.time() * 1000)})) | |
| except Exception: | |
| break | |
| hb_task = asyncio.create_task(_heartbeat()) | |
| try: | |
| info = await cdp_get_screen_info(sess) | |
| await ws.send_text(json.dumps({ | |
| "type": "connected", | |
| "screen_width": info["width"], "screen_height": info["height"], | |
| "browser": BROWSER_BIN, "display": f"headless:{sess['port']}", | |
| "user_id": user_id, "session_id": id(ws), | |
| "msg": f"Z Computer Mode v20 | User: {user_id} | Port: {sess['port']} | " | |
| f"Browser: {BROWSER_BIN} (headless=new) | Screen: {info['width']}x{info['height']}", | |
| }, ensure_ascii=False)) | |
| # ููุทุฉ ุดุงุดุฉ ุฃูููุฉ | |
| result = await capture_full(sess, quality=72) | |
| if result.get("data"): | |
| await ws.send_text(json.dumps({ | |
| "type": "screenshot", "data": result["data"], | |
| "data_grid": result.get("data_grid", ""), "data_grid2": result.get("data_grid2", ""), | |
| "data_grid3": result.get("data_grid3", ""), "ts": int(time.time() * 1000), | |
| "label": "Initial screen", "screen_width": result["width"], "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"], "has_grid": True, | |
| }, ensure_ascii=False)) | |
| sess["last_frame_hash"] = _frame_hash(result["data"]) | |
| except Exception as e: | |
| print(f"[ws:{user_id}] ุฎุทุฃ ุนูุฏ ุงูุจุฏุก: {e}") | |
| try: | |
| while True: | |
| raw = await ws.receive_text() | |
| try: | |
| msg = json.loads(raw) | |
| if msg.get("type") == "pong": | |
| continue | |
| await handle_action(ws, msg, sess) | |
| except json.JSONDecodeError: | |
| pass | |
| except WebSocketDisconnect: | |
| pass | |
| except Exception as e: | |
| print(f"[ws:{user_id}] ุฎุทุฃ: {e}") | |
| finally: | |
| hb_task.cancel() | |
| await destroy_session_ws(user_id, ws) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ REST Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def rest_screenshot(user_id: str = "anonymous"): | |
| with _sessions_lock: | |
| sess = _sessions.get(user_id) | |
| if not sess: | |
| return JSONResponse({"error": "no session for this user_id"}, status_code=404) | |
| await ensure_browser_running(sess) | |
| result = await capture_full(sess, quality=75) | |
| return JSONResponse({ | |
| "image": result["data"], "image_grid": result.get("data_grid", ""), | |
| "image_grid2": result.get("data_grid2", ""), "image_grid3": result.get("data_grid3", ""), | |
| "ts": int(time.time() * 1000), "screen_width": result["width"], "screen_height": result["height"], | |
| "mouse_x": result["mouse_x"], "mouse_y": result["mouse_y"], "has_grid": True, | |
| "user_id": user_id, | |
| }) | |
| async def rest_terminal(body: dict): | |
| return JSONResponse(await run_cmd_smart(body.get("cmd", ""), body.get("timeout", 60))) | |
| async def health(): | |
| with _sessions_lock: | |
| n = len(_sessions) | |
| users = [ | |
| {"user_id": uid, "cdp_port": s["port"], "connected": bool(s.get("active_ws")), | |
| "browser_alive": bool(s.get("cdp") and s["cdp"].alive), | |
| "engine": s.get("engine", "chromium")} | |
| for uid, s in _sessions.items() | |
| ] | |
| return {"status": "ok", "version": "v20.1-headless-native+camoufox", "browser": BROWSER_BIN, | |
| "brave_search_enabled": bool(BRAVE_API_KEY), | |
| "camoufox_available": bool(_CAMOUFOX_LIB_AVAILABLE and CAMOUFOX_ENABLED), | |
| "active_sessions": n, "max_sessions": MAX_SESSIONS, "users": users} | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ Background Cleanup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def _cleanup_idle_sessions(): | |
| """ููุฒูู ุฌูุณุงุช ุงูู ุณุชุฎุฏู ูู ุบูุฑ ุงููุดุทูู (ุจุฏูู ุงุชุตุงู WebSocket ู ูุฐ ุฃูุซุฑ ู ู | |
| ุณุงุนุฉ) โ ูููู Chromium ุงูุฎุงุต ุจูู ูุนููุงู ูุชุญุฑูุฑ ุงูุฐุงูุฑุฉ.""" | |
| while True: | |
| await asyncio.sleep(1800) | |
| now = time.time() | |
| with _sessions_lock: | |
| idle_uids = [uid for uid, s in _sessions.items() | |
| if not s.get("active_ws") and now - s.get("created", now) > 3600] | |
| for uid in idle_uids: | |
| print(f"[cleanup] ๐๏ธ ุฅุฒุงูุฉ ุฌูุณุฉ ุฎุงู ูุฉ '{uid}'") | |
| await destroy_session_full(uid) | |
| async def startup(): | |
| asyncio.create_task(_cleanup_idle_sessions()) | |
| print("โ Z Computer Mode v20 ุฌุงูุฒ โ Headless-Native (ุจุฏูู Xvfb)") | |
| print(f" ุงูู ุชุตูุญ ุงูู ุนุชู ุฏ: {BROWSER_BIN} (--headless=new)") | |
| print(f" ู ุฑุดุญู ุงูู ุชุตูุญ ุงูู ูุญูุตูู: {', '.join(_BROWSER_CANDIDATES)}") | |
| print(f" ุฃูุตู ุฌูุณุงุช ู ุชุฒุงู ูุฉ: {MAX_SESSIONS}") | |
| print(f" ูุทุงู ู ูุงูุฐ CDP: {CDP_PORT_BASE}+") | |
| print(f" websockets lib: {'โ ' if _WS_LIB_AVAILABLE else 'โ (pip install websockets)'}") | |
| if BRAVE_API_KEY: | |
| print(" ๐ Brave Search API: โ ู ูุนูู โ ุงูุจุญุซ ุณูุชู ุนุจุฑู ุฃููุงู (ุจุฏูู ูุงุจุชุดุง)") | |
| else: | |
| print(" ๐ Brave Search API: โ ุบูุฑ ู ูุนุฑููู โ ุนููู BRAVE_API_KEY ูุชูุนููู") | |
| print(" (ุจุฏูููุ ุงูุจุญุซ ุณูุนุชู ุฏ ุนูู DuckDuckGo/Wikipedia APIs ุงูู ุฌุงููุฉ ูุจุฏูู)") | |
| if _CAMOUFOX_LIB_AVAILABLE and CAMOUFOX_ENABLED: | |
| print(" ๐ฆ Camoufox (ู ุชุตูุญ ู ุถุงุฏ ูููุดู): โ ู ุชุงุญ โ ุนุจุฑ action: switch_engine {engine: camoufox}") | |
| else: | |
| print(" ๐ฆ Camoufox: โ ุบูุฑ ู ุชุงุญ โ pip install camoufox playwright && playwright install firefox") | |
| print(" Connect: wss://your-space.hf.space/ws?user_id=EMAIL") | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info") | |