""" Hermes PC Executor — läuft auf dem Windows PC. Empfängt Tool-Befehle vom MCP-Server der AI Box und führt sie lokal aus. """ import base64 import io import os import socket import subprocess import webbrowser from urllib.parse import quote import uvicorn from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="Hermes PC Executor") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) MY_PORT = int(os.environ.get("HERMES_PC_PORT", "7777")) # ── Shell ────────────────────────────────────────────────────────────────── @app.post("/shell") async def run_shell(req: dict): cmd = req.get("command", "") try: result = subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd], capture_output=True, text=True, timeout=60, encoding="utf-8", errors="replace", ) return { "stdout": result.stdout.strip(), "stderr": result.stderr.strip(), "returncode": result.returncode, } except subprocess.TimeoutExpired: raise HTTPException(408, "Timeout nach 60s") except Exception as e: raise HTTPException(500, str(e)) # ── Screen ───────────────────────────────────────────────────────────────── @app.post("/screenshot") async def take_screenshot(): try: import mss from PIL import Image with mss.mss() as sct: monitor = sct.monitors[1] raw = sct.grab(monitor) img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX") img.thumbnail((1280, 720)) buf = io.BytesIO() img.save(buf, format="JPEG", quality=75) return {"screenshot": base64.b64encode(buf.getvalue()).decode()} except Exception as e: raise HTTPException(500, str(e)) # ── Input ────────────────────────────────────────────────────────────────── @app.post("/type") async def type_text(req: dict): text = req.get("text", "") try: import pyautogui pyautogui.write(text, interval=0.03) return {"ok": True} except Exception as e: raise HTTPException(500, str(e)) @app.post("/key") async def press_keys(req: dict): keys = req.get("keys", "") try: import pyautogui parts = [k.strip() for k in keys.split("+")] pyautogui.hotkey(*parts) return {"ok": True} except Exception as e: raise HTTPException(500, str(e)) # ── Apps / Browser ───────────────────────────────────────────────────────── @app.post("/open") async def open_target(req: dict): target = req.get("target", "") try: if target.startswith("http://") or target.startswith("https://"): webbrowser.open(target) else: os.startfile(target) return {"ok": True} except Exception as e: raise HTTPException(500, str(e)) @app.post("/search") async def search_web(req: dict): query = req.get("query", "") url = f"https://www.google.com/search?q={quote(query)}" webbrowser.open(url) return {"ok": True, "url": url} # ── Health ───────────────────────────────────────────────────────────────── @app.get("/health") async def health(): return {"status": "ok", "host": socket.gethostname()} def _get_local_ip() -> str: try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(("8.8.8.8", 80)) return s.getsockname()[0] except Exception: return socket.gethostbyname(socket.gethostname()) if __name__ == "__main__": my_ip = _get_local_ip() print(f"Hermes PC Executor läuft auf http://{my_ip}:{MY_PORT}") uvicorn.run(app, host="0.0.0.0", port=MY_PORT)