===================== 新功能 (2026-04) ===================== 本頁說明 2026 年 4 月加入的新功能。每一項新功能都同時提供 **無 GUI 的 Python API** 與 **對應的 GUI 介面**,並已連接到 executor,可從 JSON 腳 本、socket server、REST API 及 CLI 直接使用,無需撰寫額外 Python 程式 碼。 .. contents:: :local: :depth: 2 剪貼簿 (Clipboard) ================== 程式碼呼叫:: import je_auto_control as ac ac.set_clipboard("hello") text = ac.get_clipboard() Action-JSON 指令:: [["AC_clipboard_set", {"text": "hello"}]] [["AC_clipboard_get", {}]] 平台後端:Windows (ctypes + Win32)、macOS (``pbcopy`` / ``pbpaste``)、 Linux (``xclip`` 或 ``xsel``)。若無可用後端會拋出 ``RuntimeError``。 試跑 / 逐步除錯 (Dry-run / Step Debug) ====================================== 無副作用執行 action 列表,方便驗證 JSON 腳本:: from je_auto_control.utils.executor.action_executor import executor record = executor.execute_action(actions, dry_run=True) ``step_callback`` 可在每一條 action 執行前收到通知:: executor.execute_action(actions, step_callback=lambda a: print(a)) CLI:: python -m je_auto_control.cli run script.json --dry-run 全域熱鍵 (Windows) ================== 綁定系統級熱鍵到 action-JSON 腳本:: from je_auto_control import default_hotkey_daemon default_hotkey_daemon.bind("ctrl+alt+1", "scripts/greet.json") default_hotkey_daemon.start() 支援的修飾鍵:``ctrl``、``alt``、``shift``、``win`` / ``super`` / ``meta``。按鍵:英文字母、數字、``f1``–``f12``、方向鍵、``space``、 ``enter``、``tab``、``escape`` 等。 macOS 與 Linux 目前在 ``start()`` 會拋出 ``NotImplementedError`` ── 採 Strategy pattern,未來可擴充對應後端。 GUI:**全域熱鍵** 分頁。 事件觸發器 (Triggers) ===================== 輪詢式觸發器,偵測到畫面或狀態變化時執行腳本:: from je_auto_control import default_trigger_engine, ImageAppearsTrigger default_trigger_engine.add(ImageAppearsTrigger( trigger_id="", script_path="scripts/click_ok.json", image_path="templates/ok_button.png", threshold=0.85, repeat=True, )) default_trigger_engine.start() 可用類型: - ``ImageAppearsTrigger`` — 螢幕上偵測模板影像 - ``WindowAppearsTrigger`` — 視窗標題包含子字串 - ``PixelColorTrigger`` — 某座標像素顏色在容差內 - ``FilePathTrigger`` — 監看檔案 mtime 變化 GUI:**事件觸發器** 分頁。 Cron 排程 ========= 五欄位 cron (``minute hour day-of-month month day-of-week``),支援 ``*``、逗號列表、``*/step`` 步進、``start-stop`` 範圍:: from je_auto_control import default_scheduler job = default_scheduler.add_cron_job( script_path="scripts/daily.json", cron_expression="0 9 * * 1-5", # 週一到週五 09:00 ) default_scheduler.start() interval 與 cron 排程可同時存在,可由 ``job.is_cron`` 判斷類型。 GUI:**排程器** 分頁新增 cron/interval 切換。 外掛載入器 (Plugin Loader) ========================== 外掛檔案為任何定義頂層 ``AC_*`` callable 的 ``.py``,每個都會成為新的 executor 指令:: # my_plugins/greeting.py def AC_greet(args=None): return f"hello, {args['name']}" :: from je_auto_control import ( load_plugin_directory, register_plugin_commands, ) commands = load_plugin_directory("my_plugins/") register_plugin_commands(commands) GUI:**外掛** 分頁,可選擇目錄一鍵載入。 .. warning:: 外掛檔案會直接執行任意 Python 程式。請務必只載入自己信任的目錄。 REST API 伺服器 =============== 純 stdlib HTTP server,公開 executor 與 scheduler:: from je_auto_control import start_rest_api_server server = start_rest_api_server(host="127.0.0.1", port=9939) 端點: - ``GET /health`` - ``GET /jobs`` - ``POST /execute`` (body: ``{"actions": [...]}``) GUI:**Socket 伺服器** 分頁新增獨立的 REST 區塊。 .. note:: 預設綁定 ``127.0.0.1`` (符合 CLAUDE.md 規範)。只有在網路邊界已做好 身分驗證時才綁定 ``0.0.0.0``。 CLI 子指令介面 ============== 以 headless API 為基礎的輕量 CLI:: python -m je_auto_control.cli run script.json python -m je_auto_control.cli run script.json --var name=alice --dry-run python -m je_auto_control.cli list-jobs python -m je_auto_control.cli start-server --port 9938 python -m je_auto_control.cli start-rest --port 9939 ``--var name=value`` 優先以 JSON 解析 (``count=10`` 會變 int),失敗時 當作字串。 GUI 多語系 ========== 可透過 **Language** 選單即時切換。內建語言包: - English - 繁體中文 (Traditional Chinese) - 简体中文 (Simplified Chinese) - 日本語 (Japanese) 執行期可註冊額外語言:: from je_auto_control.gui.language_wrapper.multi_language_wrapper import ( language_wrapper, ) language_wrapper.register_language("French", {"menu_file": "Fichier", ...}) 缺少的 key 會退回英文,讓新功能未翻譯前仍可正常顯示。 可關閉分頁 + 選單列 =================== 主視窗改為 ``QMainWindow`` + 選單列: - **File** → 開啟腳本 / 結束 - **View → Tabs** → 每個分頁可勾選顯示或隱藏 - **Tools** → 啟動熱鍵 / 排程 / 觸發器服務 - **Language** → 切換語言 - **Help** → 關於 點擊分頁的 ✕ 即可關閉,之後可從 *View → Tabs* 恢復。 OCR 螢幕文字辨識 ================ 以 Tesseract 為後端的文字定位。適用於沒有穩定 Accessibility 名稱、也 不方便擷取模板影像的按鈕或標籤:: import je_auto_control as ac matches = ac.find_text_matches("Submit") cx, cy = ac.locate_text_center("Submit") ac.click_text("Submit") ac.wait_for_text("載入完成", timeout=15.0) 若 Tesseract 不在 ``PATH`` 中:: ac.set_tesseract_cmd(r"C:\Program Files\Tesseract-OCR\tesseract.exe") Action-JSON 指令:``AC_locate_text``、``AC_click_text``、 ``AC_wait_text``。 Accessibility 元件搜尋 ====================== 透過作業系統無障礙樹查詢控制項(Windows UIA 透過 ``uiautomation``; macOS AX),支援依名稱 / 角色 / 應用程式過濾:: import je_auto_control as ac elements = ac.list_accessibility_elements(app_name="Calculator") ok = ac.find_accessibility_element(name="OK", role="Button") ac.click_accessibility_element(name="OK", app_name="Calculator") 當前平台若沒有可用後端會拋出 ``AccessibilityNotAvailableError``。 Action-JSON 指令:``AC_a11y_list``、``AC_a11y_find``、 ``AC_a11y_click``。GUI:**Accessibility** 分頁。 VLM(AI)元件定位 ================= 當模板匹配與 Accessibility 都無法找到目標時,可用自然語言描述元件, 交給視覺語言模型回傳像素座標:: import je_auto_control as ac x, y = ac.locate_by_description("綠色的 Submit 按鈕") ac.click_by_description( "Cookie 橫幅中的『全部接受』按鈕", screen_region=[0, 800, 1920, 1080], # 可選:只在此區域搜尋 ) 後端(延遲載入,import ``je_auto_control`` 時不會引入): - Anthropic (``anthropic`` SDK,``ANTHROPIC_API_KEY``) - OpenAI (``openai`` SDK,``OPENAI_API_KEY``) 環境變數(金鑰不會被記錄或寫入磁碟): - ``ANTHROPIC_API_KEY`` / ``OPENAI_API_KEY`` - ``AUTOCONTROL_VLM_BACKEND=anthropic|openai`` - ``AUTOCONTROL_VLM_MODEL=`` Action-JSON 指令:``AC_vlm_locate``、``AC_vlm_click``。GUI: **AI Locator** 分頁。 執行歷史 + 錯誤截圖附件 ======================= 排程器、觸發器、熱鍵守護程序、REST API 與 GUI 手動回放的每一次執行 都會被寫入 ``~/.je_auto_control/history.db``(SQLite)。失敗時會自動 擷取螢幕截圖並附到該筆紀錄上:: from je_auto_control import default_history_store for run in default_history_store.list_runs(limit=20): print(run.id, run.source, run.status, run.artifact_path) 截圖檔存於 ``~/.je_auto_control/artifacts/``,相關紀錄被 prune 或整個 歷史被清除時會一併刪除。GUI:**Run History** 分頁 — 雙擊截圖欄位可開 啟 OS 預覽。 OCR — 區域 dump 與 regex 搜尋 ============================= 原本 OCR 模組只支援字串/精確比對,新增兩個 API 補強其他常見場景:: import je_auto_control as ac # 把區域(或整個螢幕)內辨識到的所有文字傾倒出來 for match in ac.read_text_in_region(region=[0, 0, 800, 600]): print(match.text, match.center, match.confidence) # Regex 搜尋 — 適合內容會變的文字(訂單編號、錯誤代碼) for match in ac.find_text_regex(r"Order#\d+"): print(match.text, match.center) # 也接受 compiled pattern 與 flags import re ac.find_text_regex(re.compile(r"foo", re.IGNORECASE)) Action-JSON 指令:: [["AC_read_text_in_region", {"region": [0, 0, 800, 600]}]] [["AC_find_text_regex", {"pattern": "Order#\\d+"}]] GUI:**OCR Reader** 分頁。可用既有的選取 overlay 圈出區域(留空則整螢幕), 設定語言/最低信心度後按 *抓取區域全部文字* 或 *用 regex 搜尋*。結果 以 JSON 列出,含文字、邊界框、信心度。 執行期變數與資料驅動流程控制 ============================ 過去 :mod:`script_vars.interpolate` 只能在執行前一次性把 ``${var}`` 取代成靜態 mapping 中的值,腳本沒辦法在執行時修改狀態。``VariableScope`` 是 executor 暴露給流程控制指令的執行期 mapping,讓它們能讀寫與 runtime interpolator 相同的容器。 executor 現在改成「每次呼叫」才解析 ``${var}`` placeholder(不會 事先攤平),所以巢狀的 ``body`` / ``then`` / ``else`` 清單會保留 placeholder,每次重複執行時重新繫結 — 因此 ``AC_for_each`` 走訪 list 時,body 內看到的就是當前的元素。 :: import je_auto_control as ac from je_auto_control.utils.executor.action_executor import executor executor.execute_action([ ["AC_set_var", {"name": "items", "value": ["alpha", "beta"]}], ["AC_set_var", {"name": "i", "value": 0}], ["AC_for_each", { "items": "${items}", "as": "name", "body": [ ["AC_inc_var", {"name": "i"}], ["AC_if_var", { "name": "i", "op": "ge", "value": 2, "then": [["AC_break"]], "else": [], }], ], }], ]) ``AC_if_var``(與 ``AC_while_var``)的比較運算子:``eq``、``ne``、``lt``、 ``le``、``gt``、``ge``、``contains``、``startswith``、``endswith``。 ``AC_while_var`` 在變數比較成立時持續執行 ``body``。每輪迭代前都會對當前 scope 重新判斷條件,因此會變動該變數的 body(例如 ``AC_inc_var``)能讓迴圈 終止;``max_iter``(預設 1000)為條件永不為假時的安全上限。``AC_break`` / ``AC_continue`` 與一般迴圈相同:: executor.execute_action([ ["AC_set_var", {"name": "i", "value": 0}], ["AC_while_var", { "name": "i", "op": "lt", "value": 5, "body": [["AC_inc_var", {"name": "i"}]], }], ]) ``AC_try`` 提供 try / catch / finally。``body`` 失敗時改走 ``catch`` 分支而 非中止腳本;``finally`` 一律執行(成功、捕捉到錯誤,或在 ``reraise`` / 迴圈 break、continue 穿透時皆然)。錯誤文字會暴露到 ``error_var`` 供 ``catch`` 分支檢視,``reraise=true`` 會在清理後重新拋出:: executor.execute_action([ ["AC_try", { "body": [["AC_click_image", {"image": "dialog_ok.png"}]], "catch": [["AC_set_var", {"name": "dismissed", "value": False}]], "finally": [["AC_screenshot", {"file_path": "after.png"}]], "error_var": "err", }], ]) Action-JSON 指令:``AC_set_var``、``AC_get_var``、``AC_inc_var``、 ``AC_if_var``、``AC_for_each``、``AC_while_var``、``AC_try``。 GUI:**Variables** 分頁 — 即時檢視 ``executor.variables``,可單筆設 定、JSON 整批 seed、清空,反映 ``AC_set_var`` / ``AC_for_each`` 在執 行期的變動。 LLM 動作規劃器 ============== 把一段中/英文描述交給 LLM(預設 Anthropic Claude),生成驗證過的 ``AC_*`` 動作清單。輸出採寬鬆解析(會剝 code fence、從散文中抽出 第一個 JSON array),再用 executor 同樣的 schema 驗證,所以結果可 以直接餵給 ``execute_action``:: import je_auto_control as ac from je_auto_control.utils.executor.action_executor import executor actions = ac.plan_actions( "點擊 Submit 按鈕,然後輸入 'done' 並儲存", known_commands=executor.known_commands(), ) executor.execute_action(actions) # 或者一行做完: ac.run_from_description("開記事本,輸入 hello", executor=executor) 後端選擇對齊 :mod:`vision.backends`: - Anthropic(``anthropic`` SDK,``ANTHROPIC_API_KEY``)— 預設 - 用 ``AUTOCONTROL_LLM_BACKEND``、``AUTOCONTROL_LLM_MODEL`` 覆寫 Action-JSON 指令:``AC_llm_plan``、``AC_llm_run``。 GUI:**LLM Planner** 分頁。描述輸入框、``QThread`` 背景執行的 *Plan* 按鈕、預覽指令清單、以及 *Run plan* 按鈕 — 長時間呼叫不會卡 UI。 遠端桌面(Host + Viewer) ========================= 把本機畫面串流給別人看/控制,**或** 觀看並控制別人的機器 — 雙向都有 headless API 與 GUI 分頁。 協定是 raw TCP 上的長度前綴框架(沒有額外相依),先做一輪 HMAC-SHA256 challenge/response 認證;認證失敗的 viewer 在看到任何畫面前就被踢掉。 JPEG frame 依設定的 FPS/品質產生,透過共享 latest-frame slot 廣播給 通過認證的 viewers,慢的 viewer 只會掉 frame 而不會卡其他人。Viewer 輸入訊息是 JSON,host 端用允許清單驗證後才透過既有 mouse/keyboard wrapper 派送。 Headless host(被別人遠端):: from je_auto_control import RemoteDesktopHost host = RemoteDesktopHost( token="hunter2", # 共用密鑰(HMAC key) bind="127.0.0.1", # 預設值;要對外請走 SSH tunnel # 或可信的 VPN port=0, # 0 = 自動指派 fps=10, quality=70, ) host.start() print("listening on", host.port, "viewers:", host.connected_clients) # ... host.stop() Headless viewer(控制別人):: from je_auto_control import RemoteDesktopViewer viewer = RemoteDesktopViewer( host="10.0.0.5", port=51234, token="hunter2", on_frame=lambda jpeg_bytes: ..., # 顯示或存檔 ) viewer.connect() viewer.send_input({"action": "mouse_move", "x": 100, "y": 200}) viewer.send_input({"action": "type", "text": "hello"}) viewer.disconnect() 輸入訊息允許清單(host 派送前驗證): - ``mouse_move`` ``{x, y}`` - ``mouse_click`` ``{x?, y?, button}`` - ``mouse_press`` / ``mouse_release`` ``{button}`` - ``mouse_scroll`` ``{x?, y?, amount}`` - ``key_press`` / ``key_release`` ``{keycode}`` - ``type`` ``{text}`` - ``ping`` Action-JSON 指令(使用 :mod:`utils.remote_desktop.registry` 的單例):: AC_start_remote_host # token, bind, port, fps, quality, region AC_stop_remote_host AC_remote_host_status # → {running, port, connected_clients} AC_remote_connect # host, port, token, timeout AC_remote_disconnect AC_remote_viewer_status # → {connected} AC_remote_send_input # action: {...} GUI:\ **Remote Desktop**\ 分頁預設打開的是 **快速連線** (AnyDesk 風格)— 一邊是超大本機 Host ID,另一邊一個輸入框接受 ``host:port``、 ``ws://``、``wss://`` 或 9 位數 Host ID,搭配 *連線* 與 *開始被遠端* 兩個主要按鈕。近期連線會跨 session 記住。進階的逐傳輸子分頁(既有 TCP / WS host + viewer、WebRTC host + viewer 含手動 SDP / 自訂編碼器 / TLS pinning)仍只差一個 click。WebRTC 子分頁採延遲載入,沒裝 ``[webrtc]`` extra 也能正常開啟整個分頁。 .. warning:: 取得 host:port 與 token 的人,等同擁有本機完整滑鼠/鍵盤控制權。 預設只綁 ``127.0.0.1``;要對外暴露請務必搭配 SSH tunnel 或 TLS 前端。Token 是唯一防線 — 請當作密碼來保管。 遠端桌面 — 快速連線 + Phase 4/5 強化 ====================================== 快速連線的 headless API ------------------------ 撐起 GUI 輸入框的 transport coordinator 也對外開放,腳本可以走同樣 的解析路徑:: from je_auto_control import parse_remote_desktop_target parse_remote_desktop_target("192.168.1.10:5555") # ConnectTarget(kind='tcp', host='192.168.1.10', port=5555, ...) parse_remote_desktop_target("ws://hub:8765/desk") # ConnectTarget(kind='ws', host='hub', port=8765, path='/desk') parse_remote_desktop_target("123-456-789") # ConnectTarget(kind='webrtc_id', host_id='123456789') 連線審批 + 僅檢視模式 ---------------------- 可選的 callback 守住每一個 incoming session,AnyDesk 風格。回傳 ``"view_only"`` admit 但丟掉 viewer 的 ``INPUT``;回傳 falsy(或 raise)就送 ``AUTH_FAIL "rejected by host"``:: from je_auto_control import RemoteDesktopHost, PendingViewer def gate(p: PendingViewer) -> str: if p.address[0].startswith("10."): return "view_only" return "full" # 或 True host = RemoteDesktopHost(token="tok", on_pending_viewer=gate) IP 白名單(CIDR + 單一 IP) ---------------------------- 在 TLS / auth 之前就拒絕範圍外的對端,攻擊者連探測都不行:: host = RemoteDesktopHost( token="tok", ip_allowlist=["10.0.0.0/8", "192.168.1.100"], ) 一次性分享碼 ------------ 額外的 token,認證成功一次後自毀;客服支援流程很好用:: host = RemoteDesktopHost(token="tok", single_use_tokens=["abc123"]) host.add_single_use_token("9k4ndx") # 運行時加 host.revoke_single_use_token("abc123") # 還沒被用就先撤銷 TOTP 2FA(RFC 6238,純 stdlib) -------------------------------- 在 token 之上加一層 6 位數 OTP;host 接受 ±1 時間步的 clock drift:: from je_auto_control.utils.remote_desktop.totp import ( generate_secret, generate_code, provisioning_uri, ) secret = generate_secret() # 給 Google Authenticator / Authy / 1Password QR code 用的 otpauth:// URI print(provisioning_uri(secret, account="alice")) host = RemoteDesktopHost(token="tok", totp_secret=secret) viewer = RemoteDesktopViewer( host=..., token="tok", totp_code=generate_code(secret), ) 多螢幕選擇 ---------- 指定某一個螢幕擷取,而非合併虛擬桌面:: from je_auto_control import list_host_monitors, RemoteDesktopHost print(list_host_monitors()) # [{'index': 0, 'is_combined': True, ...}, # {'index': 1, 'left': 0, 'top': 0, ...}, # {'index': 2, 'left': 1920, ...}] host = RemoteDesktopHost(token="tok", monitor_index=1) 遠端游標 overlay ---------------- host 每秒 30 Hz 廣播 cursor 位置(靜止桌面去重);viewer 的彈出視窗 會在 JPEG 串流上疊一個箭頭,看得到 host 滑鼠位置。可用 ``enable_cursor_broadcast=False`` 關掉。 多 viewer 協作游標 + 文字 chat ------------------------------- 兩個新 message type(``CHAT`` 與 ``CURSOR`` 帶 ``viewer_id``)。搭配 ``MultiViewerHost`` 把一個 viewer 的指標 echo 給其他人;chat channel 給操作者之間臨時對話用:: host = RemoteDesktopHost( token="tok", on_chat=lambda sender, text: print(sender, ":", text), ) host.broadcast_chat("session starts in 30s") host.broadcast_viewer_cursor("alice", 200, 300) viewer = RemoteDesktopViewer( host=..., on_chat=lambda s, t: ..., on_viewer_cursor=lambda vid, x, y: ..., ) viewer.send_chat("ack") 相對滑鼠模式(FPS / CAD) -------------------------- 新輸入 action 送 delta 而非絕對座標:: viewer.send_input( {"action": "mouse_move_relative", "dx": 5, "dy": -3}, ) 動態擷取 -------- capture loop 會 hash 每張編碼後的 JPEG;重複 frame 直接跳過,所以 靜止桌面幾乎零頻寬。新 viewer 在 auth 後立即拿到最新 frame,不會 看到一片黑。 即時統計 -------- FPS / kbps / 累計 — 3 秒滑動視窗:: viewer.stats() # {'fps': 24.3, 'kbps': 4801.2, 'frames': 720.0, # 'bytes': 1.8e7, 'uptime': 30.2} JPEG 序列錄影(不需要 PyAV) ----------------------------- TCP path 的 session 錄影:每張 frame 寫到磁碟,再加一份 ``manifest.json`` 讓播放器可以原速重放:: from je_auto_control.utils.remote_desktop.jpeg_recorder import ( JpegSequenceRecorder, ) rec = JpegSequenceRecorder("~/recordings/2026-05-23") rec.start() viewer = RemoteDesktopViewer(host=..., on_frame=rec.record_frame) # ... session ... rec.stop() # 在 .jpg 旁邊寫出 manifest.json TCP relay(WebRTC fallback) ----------------------------- 當 P2P 失敗(嚴格 NAT、行動電信 CGNAT、旅館 Wi-Fi)兩端都向 relay 主動連線、交換一個 32-byte session ID,relay 在中間互轉 bytes。 同一模組附 ``encode_handshake(role, session_id)`` 給 client 用:: from je_auto_control.utils.remote_desktop.relay import RelayServer relay = RelayServer(bind="0.0.0.0", port=9000) relay.start() 服務安裝器(無人值守 host) ---------------------------- ``python -m je_auto_control.utils.remote_desktop.host_service ...`` 提供 ``configure`` / ``init`` / ``run``,以及每個平台的安裝指令: ``install-windows-service`` / ``uninstall-windows-service``(需 pywin32)、``generate-launchd`` / ``uninstall-launchd``、 ``generate-systemd`` / ``uninstall-systemd``。 遠端桌面 — 加密傳輸、音訊、剪貼簿、檔案傳輸 ============================================ Host ID 握手 ------------ 每台 host 現在都有一個穩定的 9 位數字 ID,存在 ``~/.je_auto_control/remote_host_id``,重啟後仍是同一個。ID 在 ``AUTH_OK`` 訊息內回傳(只有通過認證的 viewer 才看得到),viewer 可 以指定 ``expected_host_id`` 驗證,避免「同樣位址但是別的程序」的 冒充攻擊:: from je_auto_control import RemoteDesktopHost, RemoteDesktopViewer host = RemoteDesktopHost(token="tok") print(host.host_id) # 例如 "123456789" viewer = RemoteDesktopViewer( host="10.0.0.5", port=51234, token="tok", expected_host_id="123456789", ) viewer.connect() # 不一致就拋 AuthenticationError 另外提供 ``format_host_id("123456789") == "123 456 789"`` 與 ``parse_host_id("123 456 789") == "123456789"`` 助手。GUI 會顯示分組 過的 ID 並有 *複製* 按鈕;viewer 端的輸入欄接受常見的空白/破折號。 TLS --- ``RemoteDesktopHost`` 與 ``RemoteDesktopViewer`` 都接受 ``ssl.SSLContext`` 參數。設定後,host 會把每條接受的連線在伺服器側 套上 TLS;viewer 在客戶端側套上。失敗的握手會被記錄並關閉,不會 進到 connected client 計數:: import ssl ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.load_cert_chain("cert.pem", "key.pem") host = RemoteDesktopHost(token="tok", ssl_context=ctx) client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) client_ctx.load_verify_locations("cert.pem") viewer = RemoteDesktopViewer(host=..., ssl_context=client_ctx) 自簽憑證 loopback 測試時,把 ``ctx.check_hostname = False`` 與 ``ctx.verify_mode = ssl.CERT_NONE`` 設在 client context 上。GUI host 分頁有 TLS 憑證/私鑰的檔案選擇器;viewer 分頁有 *忽略憑證驗證* 的 checkbox 配自簽用。 WebSocket 傳輸 -------------- 新增 ``WebSocketDesktopHost`` / ``WebSocketDesktopViewer``,用 RFC 6455 BINARY frame 傳同樣的 typed message。實作放在 in-tree(沒有 額外相依);每個 application message 對應一個完整的 WS frame,所以 不需要重組機制。同一個 ``ssl_context`` 也是 ``wss://`` 的開關:: from je_auto_control import ( WebSocketDesktopHost, WebSocketDesktopViewer, ) host = WebSocketDesktopHost(token="tok", ssl_context=ctx) # wss:// viewer = WebSocketDesktopViewer( host="example.com", port=443, token="tok", ssl_context=client_ctx, path="/rd", ) 為什麼用 WS:穿牆友善、容易接反向代理、跟瀏覽器 viewer 相容。GUI viewer 的傳輸下拉(*TCP* / *WebSocket* / *TLS* / *WSS*)會自動選對 應的 class。 音訊串流 -------- 新增 ``AUDIO`` 訊息類型,攜帶 16-bit signed PCM 區塊(預設 16 kHz mono,每塊 50 ms / 1600 bytes)。``sounddevice`` 為 optional 相依, 延遲載入;沒裝就 host 端音訊回報停用且整個 host 仍能運作:: from je_auto_control.utils.remote_desktop import AudioCaptureConfig host = RemoteDesktopHost( token="tok", audio_config=AudioCaptureConfig( enabled=True, device=None, # 預設 mic sample_rate=16000, channels=1, ), ) from je_auto_control.utils.remote_desktop import AudioPlayer player = AudioPlayer(); player.start() viewer = RemoteDesktopViewer(host=..., on_audio=player.play) Host 把每塊抓到的音訊透過每個 client 一個有上限的 deque(~2.5 秒緩 衝)廣播出去;慢的 viewer 只會丟掉舊的音訊塊,不會卡到大家的擷取 執行緒。如果要抓系統聲音(而非 mic),用 device index 指定 — Win 是 WASAPI loopback、Linux 是 PulseAudio monitor source、macOS 要 BlackHole 之類。GUI:Host 分頁的 *串流系統音訊*,Viewer 分頁的 *播 放接收的音訊*。 剪貼簿同步(文字 + 圖片) ------------------------- 新增 ``CLIPBOARD`` 訊息類型,payload 是 JSON envelope,方便日後加新 類別不用動到 framing: * ``{"kind": "text", "text": "..."}`` * ``{"kind": "image", "format": "png", "data_b64": "..."}`` ``utils/clipboard/clipboard.py`` 補上 ``get_clipboard_image`` / ``set_clipboard_image``;Windows 用 ctypes 寫 CF_DIB(Pillow 把 PNG 轉成 BMP 再去掉 14 byte file header 變成 DIB),Linux 走 ``xclip -t image/png``,macOS get 走 Pillow ImageGrab、set 暫時拋 NotImplemented 等 PyObjC backend。同步是「明確呼叫」的(避免雙向 auto-poll 造成 paste 迴圈):: # Viewer 把本機剪貼簿送到 host viewer.send_clipboard_text("hello") viewer.send_clipboard_image(open("logo.png", "rb").read()) # Host 把本機剪貼簿送到所有 viewers host.broadcast_clipboard_text("greetings") host.broadcast_clipboard_image(png_bytes) # Viewer 接收回 callback,自己決定要不要 paste viewer = RemoteDesktopViewer( host=..., on_clipboard=lambda kind, data: ..., ) GUI:Viewer 分頁有 *把本機剪貼簿文字送到 Host* 按鈕;host 收到後 透過上述 helpers 套用到本機剪貼簿。 檔案傳輸 + 進度 --------------- 三個新訊息組成一次傳輸: * ``FILE_BEGIN`` — JSON ``{transfer_id, dest_path, size}`` * ``FILE_CHUNK`` — 36-byte ASCII transfer id + 原始 payload * ``FILE_END`` — JSON ``{transfer_id, status, error?}`` 雙向、分塊(256 KiB / chunk)、**沒有總大小上限**、**沒有目的路徑 限制**(拿到 token 就視為信任使用者)。進度由兩端各自本地計算,不 需要額外的 wire 訊息:: from je_auto_control.utils.remote_desktop import ( FileReceiver, RemoteDesktopHost, RemoteDesktopViewer, send_file, ) # Viewer 上傳到 host viewer.send_file("local.bin", "/tmp/uploaded.bin", on_progress=lambda tid, done, total: print(done, total)) # Host 下發到所有 viewer(viewer 需要設一個 FileReceiver 來收) viewer.set_file_receiver(FileReceiver( on_progress=..., on_complete=..., )) host.send_file_to_viewers("local.bin", "/tmp/from_host.bin") GUI:*傳送檔案...* 按鈕開啟檔案選擇器 + 目的路徑提示,上傳跑在 ``QThread`` 上,底下 ``QProgressBar`` 綁到 sender 的 progress 事 件。Frame display widget 也接受 dragEnter/drop 拖放本機檔案,丟進 去就走同一個流程上傳。 .. warning:: 路徑無限制、大小無上限。任何拿到 token 的人都能把任意檔案寫到 任意位置(覆蓋 ``C:\\Windows\\System32\\*.dll`` 都可能),也能 塞滿磁碟。Token 持有者必須等同信任使用者;要更嚴格的話請自行 繼承 ``FileReceiver`` 在 ``handle_begin`` 內驗證 dest_path。 遠端桌面 — AnyDesk 風格彈出視窗 ================================ Viewer 分頁不再把遠端畫面內嵌在面板裡 — viewer 認證成功後,會 另外開啟一個獨立的 :class:`RemoteScreenWindow` 顯示遠端桌面, 面板本身只剩下連線卡片 + 控制元件。關閉 popup 視窗的 ✕ 按鈕 會自動斷線,跟 AnyDesk 的 session 視窗體驗一致。 * 新增模組:``je_auto_control/gui/remote_desktop/remote_screen_window.py`` * 內部包一個 ``_FrameDisplay`` 並重新發送其 mouse / keyboard / drag-and-drop / annotation signals,所以面板仍然只需要訂閱 單一 signal source。 * 視窗底部保留檔案傳輸進度條 / 標籤,沒有傳輸時隱藏。 * TCP ``_ViewerPanel`` 與 WebRTC ``_WebRTCViewerPanel`` 都會在 connect / auth_ok 時開啟此視窗,在 disconnect / stop 時關閉。 設計動機 原先的版面在垂直方向擠得很滿:畫面顯示 + 連線卡 + 折疊區 + action row + stats + sparkline + 傳輸進度 + 狀態列全部 往下堆。把遠端畫面拉到獨立視窗後,操作者多了一個真正的工作 區,控制面板也不用再跟畫面爭空間。 遠端桌面 — 自適應的子分頁尺寸 ============================== 每一個 Remote Desktop 子分頁外面都改包了一層 ``QScrollArea`` 並設 ``setWidgetResizable(True)``。包裝邏輯 放在 ``gui/remote_desktop/tab.py``(``_wrap_in_scroll_area`` helper)。 * 視窗縮小時:出現垂直捲軸,WebRTC 那種密集分頁不會被切到。 * 視窗放大(4K)時:內部 panel 會跟著 viewport 橫向延展,連線 卡與 session 表格會撐滿到右邊緣,不再縮成左上角一坨。 * 各 panel 底部仍有 ``addStretch(1)``,額外空間時內容會被推到 上方,版面不會下垂。 WebRTC viewer 分頁裡比較少用的群組(Manual SDP、Remote Files、 Sync)也透過新的 ``_wrap_collapsed`` 包成預設摺疊的 ``_CollapsibleSection``,初次顯示高度大約砍半。 WebRTC host 的 session 表格原本固定為 ``setMaximumHeight(140)`` ,改成 ``setMinimumHeight(140)`` — 維持原本 140 px 的起始高度, 但在大螢幕上不再被卡住。 遠端桌面 — MCP 工具 ==================== MCP server 現在把 GUI 用的 process-global remote-desktop registry 包成工具,工廠函式為 ``je_auto_control/utils/mcp_server/tools/_factories.py`` 內的 ``remote_desktop_tools()``: ``ac_remote_host_start`` 啟動(或重啟)singleton TCP host,參數 ``token``、 ``bind``、``port``、``fps``、``quality``、``max_clients``、 ``host_id``,回傳 ``{running, port, host_id, connected_clients}``。 ``ac_remote_host_stop`` 關閉 host(沒在跑時為 no-op)。 ``ac_remote_host_status`` 唯讀的 host 狀態快照,在 ``--readonly`` 模式下仍然可用。 ``ac_remote_viewer_connect`` 把 singleton viewer 連到遠端 host,可選 ``expected_host_id`` 驗證 9 位數 ID。 ``ac_remote_viewer_disconnect`` / ``ac_remote_viewer_status`` 關閉 / 觀察 viewer(status 為唯讀)。 ``ac_remote_viewer_send_input`` 透過已連線的 viewer 把輸入動作 dict(``mouse_move``、 ``mouse_press``、``mouse_release``、``mouse_scroll``、 ``key_press``、``key_release``、``type``、``hotkey``)轉送到 遠端 host。屬於 destructive,在 ``--readonly`` 模式下會被剔 除。 這樣一來模型就能在不開 GUI 的情況下完成完整的遠端控制流程: .. code-block:: text ac_remote_host_start(token="tok", bind="127.0.0.1", port=0) → {"running": true, "port": 51234, "host_id": "123456789", "connected_clients": 0} # … 切到另一台機器 … ac_remote_viewer_connect(host="10.0.0.5", port=51234, token="tok", expected_host_id="123456789") → {"connected": true, "host_id": "123456789"} ac_remote_viewer_send_input(action={ "action": "mouse_move", "x": 100, "y": 200, }) ac_remote_viewer_send_input(action={ "action": "type", "text": "hello", }) 狀態類工具(``ac_remote_host_status``、 ``ac_remote_viewer_status``)為唯讀,可以通過 MCP server 的 ``--readonly`` 過濾;會修改狀態的工具都正確帶上 ``destructiveHint: true``,MCP client 端可以據此跳出使用者確認。 驅動層輸入後端 — 驅動不接受 SendInput / XTest 的遊戲 ===================================================== 預設的 Windows(SendInput)與 Linux(XTest)輸入路徑落在 user-mode / X-server 那一層;會用 ``GetRawInputData``(Win)或 ``evdev`` (Linux)直接讀 raw input 的遊戲會跳過這些層,完全忽略合成事件。 新增三個可選的後端可以解決這個問題。 Interception(Windows) ------------------------ Oblita 的 WHQL-signed Interception driver (https://github.com/oblitum/Interception)在 HID 層注入鍵鼠事件, OS 看到的就是「真實裝置」事件。 * 新增子套件:``je_auto_control/windows/interception/`` (``_dll.py`` ctypes bindings + ``keyboard.py`` + ``mouse.py``)。 * 與 ``win32_ctype_keyboard_control`` / ``win32_ctype_mouse_control`` 公開介面完全一致 — wrapper 在啟動 時直接換模組,呼叫端不需要任何修改。 * 透過 ``JE_AUTOCONTROL_WIN32_BACKEND=interception`` 啟用;若 driver 沒裝,wrapper 會打 warning 並回到 SendInput,所以可以分 階段佈署。 * 用 ``JE_AUTOCONTROL_INTERCEPTION_KEYBOARD`` / ``JE_AUTOCONTROL_INTERCEPTION_MOUSE`` 覆寫 device id(預設 ``1`` / ``11``)。 操作步驟:: # 1. 以系統管理員身份安裝 driver(一次性,需要重開機) install-interception.exe /install # 2. 告訴 AutoControl 走這條路 setx JE_AUTOCONTROL_WIN32_BACKEND interception uinput(Linux) ---------------- kernel 自帶的合成輸入閘道。透過 ``/dev/uinput`` 送出的事件會被 建立成一個全新的 HID 裝置,任何讀 ``evdev`` 的程式(包含大部分 遊戲與 SDL2 app)都會視為真實輸入。 * 新增子套件:``je_auto_control/linux_with_x11/uinput/`` (``_device.py`` 直接用 ctypes + ioctl 包 ``/dev/uinput`` + ``keyboard.py`` + ``mouse.py``)。 * 無第三方依賴 — 全程 ctypes + ioctl。 * 透過 ``JE_AUTOCONTROL_LINUX_BACKEND=uinput`` 啟用;若 ``/dev/uinput`` 沒寫入權限,會 warning 後回退到 XTest。 操作步驟:: # 載入 kernel module sudo modprobe uinput # 一次性測試:直接放寬權限 sudo chmod 666 /dev/uinput # 持續性權限,寫一個 udev rule: echo 'KERNEL=="uinput", GROUP="input", MODE="0660"' \ | sudo tee /etc/udev/rules.d/99-autocontrol-uinput.rules sudo udevadm control --reload && sudo udevadm trigger sudo usermod -aG input $USER # 重新登入後生效 # 啟用後端 export JE_AUTOCONTROL_LINUX_BACKEND=uinput ViGEm 虛擬手把(Windows) ------------------------- 針對「完全不吃鍵鼠、只認手把」的遊戲,可以用 ViGEmBus 建立一個虛 擬 Xbox 360 / DualShock 4 控制器;AutoControl 透過第三方 ``vgamepad`` 套件來驅動它。 * 新增模組:``je_auto_control/utils/gamepad/`` 提供友善的 ``VirtualGamepad`` API(字串名稱的 button / dpad / stick / trigger,支援 context manager)。 * Headless:: from je_auto_control import VirtualGamepad with VirtualGamepad() as pad: pad.click_button("a") # A 鍵 pad.set_left_stick(16000, 0) # int16 stick 偏移 pad.set_right_trigger(255) # 0..255 力度 pad.set_dpad("up") # 按住方向鍵上 pad.update() # 把狀態 flush 給 driver * Executor 指令:``AC_gamepad_press``、``AC_gamepad_release``、 ``AC_gamepad_click``、``AC_gamepad_dpad``、 ``AC_gamepad_left_stick`` / ``_right_stick``、 ``AC_gamepad_left_trigger`` / ``_right_trigger``,以及 ``AC_gamepad_reset``。 * MCP 工具:同名加上 ``ac_`` 前綴(``ac_gamepad_press``、 ``ac_gamepad_left_stick`` …),所以模型可以透過 MCP 玩只支援 手把的遊戲。 操作步驟:: # 1. 安裝 ViGEmBus driver(一次性,需要重開機) # https://github.com/nefarius/ViGEmBus/releases # 2. 安裝 Python wrapper: pip install vgamepad 反作弊注意事項 --------------- 驅動層注入比 SendInput / XTest 更難偵測,但帶 kernel-mode driver 的反作弊(Vanguard、有 kernel module 的 Easy Anti-Cheat、 BattlEye)依然可以列舉 Interception / ViGEmBus / 新建立的 uinput 裝置然後拒絕啟動。 這三個後端針對的是合法用途 — 輔助科技、遊戲 GUI 測試、從 headless 環境控制執行遊戲的遠端機器 — **不是** 通用反作弊繞過工具。 效能分析器(Profiler) ==================== 針對每個 ``AC_*`` 動作記錄執行時間,讓你不用外部工具就能回答 「哪一步是腳本的瓶頸?」分析是 opt-in 的,關閉時 executor 包裝層 零開銷:: import je_auto_control as ac ac.default_profiler.enable() ac.execute_action([["AC_locate_image_center", {"image": "btn.png"}], ["AC_click_mouse"]]) for row in ac.default_profiler.hot_spots(limit=5): print(row.name, row.calls, row.average_seconds) Action JSON 指令:: [["AC_profiler_enable"]] [["AC_profiler_stats", {"limit": 10}]] [["AC_profiler_hot_spots", {"limit": 5}]] [["AC_profiler_reset"]] [["AC_profiler_disable"]] GUI: **Profiler** 分頁 — 每秒重新整理熱點表(次數 / 總時間 / 平均 / 最短 / 最長 / 佔比),支援開關記錄、清除統計,或透過 headless API 匯出 快照。 執行紀錄時間軸 + 失敗截圖 ========================= Run History 分頁在篩選列下方多了一條 Gantt 風格的時間軸:每筆 scheduler / trigger / hotkey / webhook / email 觸發都繪成橫向時間 軸上的色條(綠 = ok、紅 = error、琥珀 = 進行中)。點選色條會同步 表格列,右側預覽面板顯示由 artifact manager 已捕捉的失敗截圖。 Headless 端讀取相同資料用既有的 run history store:: import je_auto_control as ac for row in ac.default_history_store.list_runs(limit=20): print(row.id, row.status, row.duration_seconds, row.artifact_path) 沒有新增指令 — store API 維持不變,GUI 只是 ``runs`` 表的薄殼可視化。 密鑰管理器(Secret Manager) ========================== 需要 API token、IMAP 密碼等敏感資訊的腳本,絕不該明文嵌入。新的 密鑰庫把 Fernet 加密過的條目存在 ``~/.je_auto_control/secrets/vault.json``; 通行碼透過 PBKDF2-HMAC-SHA256(60 萬次迭代,16 byte 鹽值)推導出金鑰:: import je_auto_control as ac ac.default_secret_manager.initialize("my-vault-passphrase") ac.default_secret_manager.set("github_token", "ghp_xxxxx") ac.default_secret_manager.lock() # 之後 — 在同一個程序或新的 run: ac.default_secret_manager.unlock("my-vault-passphrase") Action JSON 指令:: [["AC_secret_init", {"passphrase": "..."}]] [["AC_secret_unlock", {"passphrase": "..."}]] [["AC_secret_set", {"name": "github_token", "value": "ghp_xxx"}]] [["AC_secret_list"]] [["AC_secret_remove", {"name": "github_token"}]] [["AC_secret_lock"]] [["AC_secret_status"]] 腳本透過 ``${secrets.NAME}`` 佔位符引用 vault 條目。插補器會把 ``secrets.`` 命名空間導向 vault,而不是普通變數作用域,所以明文值 永遠不會進到變數袋裡:: [["AC_shell_command", {"command": "curl -H \"Authorization: Bearer ${secrets.github_token}\" ..."}]] GUI: **Secrets** 分頁 — 建立 vault、解鎖、新增 / 移除條目、變更 通行碼。POSIX 系統上 vault 檔以 0o600 建立;Windows 預設 ACL 已限制 只有擁有者能讀取。 Webhook(HTTP push)觸發 ======================= 內建的 :mod:`http.server` dispatcher 在外部服務 POST 到註冊路徑時 觸發腳本。可設定路徑、允許的方法、可選 bearer token;請求方法、 路徑、query、headers、原始 body、解析後 JSON 都會種到變數作用域:: import je_auto_control as ac ac.default_webhook_server.add( path="/jobs/build", script_path="hooks/on_build.json", methods=["POST"], token="topsecret", ) host, port = ac.default_webhook_server.start("127.0.0.1", 0) print("listening on", host, port) 綁定的腳本透過 ``${webhook.*}`` 佔位符讀取請求:: [ ["AC_set_var", {"name": "branch", "value": "${webhook.query.ref}"}], ["AC_shell_command", {"command": "echo received build for ${webhook.body}"}] ] Action JSON 指令:: [["AC_webhook_start", {"host": "127.0.0.1", "port": 8765}]] [["AC_webhook_add", {"path": "/jobs", "script_path": "...", "methods": ["POST"], "token": "..."}]] [["AC_webhook_list"]] [["AC_webhook_remove", {"webhook_id": "abcd1234"}]] [["AC_webhook_status"]] [["AC_webhook_stop"]] 每次觸發以 ``trigger`` 來源寫入 run history,source id 為 ``webhook:``,讓 dashboard 把 webhook 活動和其他 trigger 並排 顯示。Body 上限 1 MiB,bearer token 比對用 :func:`hmac.compare_digest`。除非你真的需要從網路其他地方連入, 否則綁定 ``127.0.0.1``。 GUI: **Webhooks** 分頁 — 啟動 / 停止伺服器、註冊路徑、檢視每條 路由的觸發次數與驗證狀態。 IMAP Email 觸發 =============== 輪詢式 watcher,依設定週期登入信箱,每封符合條件的郵件執行一次 腳本:: import je_auto_control as ac ac.default_email_trigger_watcher.add( host="imap.gmail.com", username="user@example.com", password="app-specific-password", script_path="hooks/on_alert.json", mailbox="INBOX", search_criteria='UNSEEN FROM "alerts@..."', poll_seconds=120, mark_seen=True, ) ac.default_email_trigger_watcher.start() 腳本透過 ``${email.*}`` 看到郵件中繼資料:: [ ["AC_if_var", { "name": "email.subject", "op": "contains", "value": "CRITICAL", "then": [["AC_hotkey", {"keys": ["ctrl", "alt", "p"]}]] }] ] 每次觸發種入的變數: ``email.uid``、``email.from``、``email.to``、 ``email.subject``、``email.message_id``、``email.date``、``email.body``。 Action JSON 指令:: [["AC_email_trigger_add", {"host": "...", "username": "...", "password": "${secrets.imap_pw}", "script_path": "...", "mailbox": "INBOX", "search_criteria": "UNSEEN", "poll_seconds": 120, "mark_seen": true, "use_ssl": true}]] [["AC_email_trigger_start"]] [["AC_email_trigger_poll_once"]] [["AC_email_trigger_list"]] [["AC_email_trigger_remove", {"trigger_id": "abcd1234"}]] [["AC_email_trigger_stop"]] Watcher 在 process 記憶體中追蹤已觸發的 UID,可選擇把訊息標為 ``\\Seen`` 確保跨重啟不會重複觸發。TLS 強制最低 1.2 版。把 ``AC_email_trigger_add`` 跟 ``${secrets.NAME}`` 搭配使用, 密碼就不會出現在 JSON 裡。 GUI: **Email Triggers** 分頁 — 註冊 IMAP 觸發、啟動 / 停止 watcher、 手動觸發一次輪詢、檢視最近錯誤與觸發次數。