Pcservercomp / app.py
THEZYZSTUDIO's picture
Upload app.py
549573b verified
Raw
History Blame Contribute Delete
91.6 kB
"""
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
@property
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
@property
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=["*"])
@app.get("/", response_class=HTMLResponse)
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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@app.websocket("/ws")
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 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@app.get("/screenshot")
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,
})
@app.post("/terminal")
async def rest_terminal(body: dict):
return JSONResponse(await run_cmd_smart(body.get("cmd", ""), body.get("timeout", 60)))
@app.get("/health")
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)
@app.on_event("startup")
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")