--- scripts/watcher.py 2026-10-05 15:36:41.413744391 +0000 +++ scripts/watcher.py 2026-10-05 15:36:41.413744391 +0000 @@ -49,6 +49,7 @@ BLUE_CLICK_Y = 425 MASTER_PROFILES_DIR = SYSTEM_DIR / "profiles" # 只讀 master LOGS_DIR = SYSTEM_DIR / "logs" +LOGS_DIR.mkdir(parents=True, exist_ok=True) # kit fix v1: 唔經 start-all.sh 起都唔會「第一個 log 就 crash」 RECALL_MSG = "Continue SESSION 2147483647" RECALL_BASE = 2147483647 # 固定起始數字(owner 2026-10-04 14:58 HKT:每次推減1,重啟重置) _recall_counter = RECALL_BASE # 內存計數器,重啟 watcher 即重置 @@ -63,6 +64,8 @@ _last_sent_num = num return f"Continue SESSION {num}" CHECK_INTERVAL = 30 # 秒 +# host 8568:DISPLAY 唔准寫死。由 env 攞;冇就 fallback ':99' 並一定 log(唔准靜靜 fallback)。 +DISPLAY = os.environ.get("DISPLAY") or ":99" def load_seats(): seats = {} @@ -72,7 +75,7 @@ if not line or line.startswith("#"): continue parts = line.split() - if len(parts) >= 6: + if len(parts) >= 5: # kit fix v1: 5 欄都收(原本 >=6 會靜默掉行) sid, profile, vpn, url, account = parts[0], parts[1], parts[2], parts[3], parts[4] status = parts[5] if len(parts) > 5 else "active" seats[sid] = { @@ -115,6 +118,11 @@ 'A8': 0, 'A9': 900, 'A10': 1800, } +# 窗名搜尋字串(自選 windowname 補丁):enforce_window_geometry 同 _get_browser_window_id 由原本 +# 兩套字串('Arena' vs 'Chrome')統一用同一個;可用 env W11_WINDOW_SEARCH 覆寫。 +# 另:設 W11_STRICT_PIXEL=1 會令像素關卡搵唔到窗時 fail-closed(預設唔設=維持原本 fail-open)。 +W11_WINDOW_SEARCH = os.environ.get("W11_WINDOW_SEARCH") or "Arena" + def enforce_window_geometry(sid): """強制 browser window 尺寸(2026-10-05 19:49 HKT owner 下令:硬性規定) 硬規則:如果唔係 1408x900,一律唔准運行,必須鎖死喺呢個尺寸。 @@ -126,9 +134,9 @@ import subprocess try: result = subprocess.run( - ['xdotool', 'search', '--onlyvisible', '--name', 'Arena'], + ['xdotool', 'search', '--onlyvisible', '--name', W11_WINDOW_SEARCH], capture_output=True, text=True, timeout=5, - env={**os.environ, 'DISPLAY': ':99'} + env={**os.environ, 'DISPLAY': DISPLAY} ) wids = result.stdout.strip().split() if not wids: @@ -139,20 +147,20 @@ geom = subprocess.run( ['xdotool', 'getwindowgeometry', wid], capture_output=True, text=True, timeout=5, - env={**os.environ, 'DISPLAY': ':99'} + env={**os.environ, 'DISPLAY': DISPLAY} ).stdout if '1408x900' not in geom: # 試較返 subprocess.run( ['xdotool', 'windowsize', wid, '1408', '900'], - timeout=5, env={**os.environ, 'DISPLAY': ':99'} + timeout=5, env={**os.environ, 'DISPLAY': DISPLAY} ) time.sleep(1) # 再驗 geom2 = subprocess.run( ['xdotool', 'getwindowgeometry', wid], capture_output=True, text=True, timeout=5, - env={**os.environ, 'DISPLAY': ':99'} + env={**os.environ, 'DISPLAY': DISPLAY} ).stdout if '1408x900' not in geom2: log(sid, f"🚨 window {wid} 較唔返 1408x900,呢個 cycle 唔運行") @@ -961,9 +969,9 @@ """攞 browser window ID(最大嗰個 Chrome window)""" import subprocess try: - out = subprocess.run(['xdotool', 'search', '--name', 'Chrome'], + out = subprocess.run(['xdotool', 'search', '--name', W11_WINDOW_SEARCH], capture_output=True, text=True, timeout=5, - env={**__import__('os').environ, 'DISPLAY': ':99'}) + env={**__import__('os').environ, 'DISPLAY': DISPLAY}) wids = [w for w in out.stdout.strip().split('\n') if w] # 搵最大嗰個(browser 主視窗) best, best_area = None, 0 @@ -971,7 +979,7 @@ try: g = subprocess.run(['xdotool', 'getwindowgeometry', wid], capture_output=True, text=True, timeout=3, - env={**__import__('os').environ, 'DISPLAY': ':99'}) + env={**__import__('os').environ, 'DISPLAY': DISPLAY}) for line in g.stdout.split('\n'): if 'Geometry:' in line: parts = line.split()[1].split('x') @@ -991,7 +999,7 @@ out = subprocess.run(['import', '-window', str(window_id), '-crop', f'1x1+{x}+{y}', '-depth', '8', 'txt:-'], capture_output=True, text=True, timeout=5, - env={**__import__('os').environ, 'DISPLAY': ':99'}) + env={**__import__('os').environ, 'DISPLAY': DISPLAY}) import re m = re.search(r'#([0-9a-fA-F]{6})', out.stdout) return '#' + m.group(1).lower() if m else None @@ -1027,7 +1035,7 @@ def _xdotool_click(window_id, x, y): """跟 CA:xdotool 三步點擊""" import subprocess, time - env = {**__import__('os').environ, 'DISPLAY': ':99'} + env = {**__import__('os').environ, 'DISPLAY': DISPLAY} try: subprocess.run(['xdotool', 'windowraise', str(window_id)], env=env, timeout=3, capture_output=True) time.sleep(0.2) @@ -1047,6 +1055,9 @@ try: wid = _get_browser_window_id() if not wid: + if os.environ.get("W11_STRICT_PIXEL"): + log(sid, "像素關卡搵唔到窗(W11_STRICT_PIXEL=1)→ fail-closed,唔當過") + return False return True # 第1套:紫色(優先) — check (715,415) #f3e9f7 → click (580,605) @@ -1155,7 +1166,7 @@ def ca_checkbox_click(window_id, x, y): """CA 式:用 xdotool 做 OS 層級滑鼠點擊(唔係 DOM click)""" import subprocess - env = {'DISPLAY': ':99'} + env = {'DISPLAY': DISPLAY} try: subprocess.run(['xdotool', 'windowraise', str(window_id)], env={**os.environ, **env}, timeout=3, capture_output=True) subprocess.run(['xdotool', 'windowactivate', '--sync', str(window_id)], env={**os.environ, **env}, timeout=3, capture_output=True) @@ -1401,11 +1412,14 @@ } ts = int(_t.time()) # 經 SSH 寫去 server bus(host inbox) - _sp.run([ + _r = _sp.run([ "/home/hatch/workspace/ssh-newserver.sh", f"cat > /root/muse-mesh/inbox/host/{ts}-watcher-{sid.lower()}.json && chmod 600 /root/muse-mesh/inbox/host/{ts}-watcher-{sid.lower()}.json" ], input=_json.dumps(msg).encode(), timeout=30, capture_output=True) - log(sid, "已寫 bus 訊息去 server:conversation_too_long(host 會掃到)") + if _r.returncode != 0: + log(sid, f"⚠️ bus 寫入失敗 rc={_r.returncode}:{( _r.stderr or b'')[:200]!r}(人手要知)") + else: + log(sid, "已寫 bus 訊息去 server:conversation_too_long(host 會掃到)") except Exception as e: log(sid, f"寫 bus 訊息出錯: {e}") # 停呢個 watcher,等 Muse AI 開新 URL