va_daemon.py 4.81 KB
#!/data/data/com.termux/files/usr/bin/python3
"""VisualAgent Daemon — pure Python, no shell quoting issues.

Drop-in replacement for shell-based VisualAgent daemons. Eliminates all
shell-related problems: PATH resolution, JSON quoting, grep parsing,
su lifecycle, and code-version mismatch.

Start via Hermes background terminal (the ONLY reliable way on Android):
    terminal(background=true, command='su -c "exec python3 /path/to/va_daemon.py"')

Do NOT start via `su -c 'nohup ... &'` — the daemon will die when su exits.
"""
import json, os, time, shlex
from datetime import datetime

CMD_DIR     = "/sdcard/VisualAgent/commands"
RESULT_DIR  = "/sdcard/VisualAgent/results"
PROCESSED_DIR = "/sdcard/VisualAgent/processed"
SCREENSHOT_DIR = "/sdcard/VisualAgent/screenshots"
PIDFILE     = "/sdcard/VisualAgent/daemon.pid"
LOGFILE     = "/sdcard/VisualAgent/daemon.log"

os.makedirs(RESULT_DIR, exist_ok=True)
os.makedirs(PROCESSED_DIR, exist_ok=True)
os.makedirs(SCREENSHOT_DIR, exist_ok=True)

with open(PIDFILE, 'w') as f:
    f.write(str(os.getpid()))

def log(msg):
    ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    with open(LOGFILE, 'a') as f:
        f.write(f"[{ts}] {msg}\n")

log(f"VisualAgent Python daemon started (PID: {os.getpid()})")

def process_file(filepath):
    fname = os.path.basename(filepath)
    processed_path = os.path.join(PROCESSED_DIR, fname)

    if os.path.exists(processed_path):
        try: os.remove(filepath)
        except: pass
        return

    ts = int(time.time())

    try:
        with open(filepath) as f:
            data = json.load(f)
    except Exception as e:
        log(f"json parse error: {fname} — {e}")
        try: os.rename(filepath, processed_path)
        except: pass
        return

    cmd = data.get('command') or data.get('type') or ''
    result = {}

    try:
        if cmd == 'screenshot':
            ss = os.path.join(SCREENSHOT_DIR, f"screenshot_{ts}.png")
            rc = os.system(f"screencap -p {shlex.quote(ss)}")
            result = {"status": "success" if rc == 0 else "error", "type": "screenshot",
                      "path": ss, "timestamp": ts}
            log(f"screenshot -> {ss} (rc={rc})")

        elif cmd == 'tap':
            x, y = data.get('x', 0), data.get('y', 0)
            rc = os.system(f"input tap {x} {y}")
            result = {"status": "success" if rc == 0 else "error", "type": "tap",
                      "x": x, "y": y, "timestamp": ts}
            log(f"tap {x},{y} (rc={rc})")

        elif cmd == 'swipe':
            x1, y1 = data.get('x1', 0), data.get('y1', 0)
            x2, y2 = data.get('x2', 0), data.get('y2', 0)
            dur = data.get('duration', 300)
            rc = os.system(f"input swipe {x1} {y1} {x2} {y2} {dur}")
            result = {"status": "success" if rc == 0 else "error", "type": "swipe",
                      "timestamp": ts}
            log(f"swipe {x1},{y1}->{x2},{y2} dur={dur} (rc={rc})")

        elif cmd == 'input':
            text = data.get('text') or data.get('input') or ''
            if text:
                rc = os.system(f"input text {shlex.quote(text)}")
                result = {"status": "success" if rc == 0 else "error", "type": "input",
                          "timestamp": ts}
                log(f"input: {text[:40]} (rc={rc})")
            else:
                result = {"status": "error", "type": "input", "message": "no text"}

        elif cmd == 'start_app':
            pkg = data.get('package', '')
            if pkg:
                rc = os.system(f"monkey -p {pkg} -c android.intent.category.LAUNCHER 1")
                result = {"status": "success" if rc == 0 else "error", "type": "start_app",
                          "package": pkg, "timestamp": ts}
                log(f"start_app: {pkg} (rc={rc})")
            else:
                result = {"status": "error", "type": "start_app", "message": "no package"}
        else:
            result = {"status": "error", "type": "unknown", "message": f"unknown: {cmd}",
                      "timestamp": ts}
            log(f"unknown cmd: {cmd}")

    except Exception as e:
        result = {"status": "error", "type": "exception", "message": str(e), "timestamp": ts}
        log(f"exception in {fname}: {e}")

    # Write result
    result_file = os.path.join(RESULT_DIR, f"result_{fname.replace('.json','')}_{ts}.json")
    with open(result_file, 'w') as f:
        json.dump(result, f)

    # Move to processed
    try:
        os.rename(filepath, processed_path)
    except:
        pass

    log(f"done: {fname} ({result.get('status','?')})")

# Main loop
while True:
    try:
        entries = os.listdir(CMD_DIR)
    except:
        entries = []

    for entry in sorted(entries):
        if entry.endswith('.json'):
            filepath = os.path.join(CMD_DIR, entry)
            if os.path.isfile(filepath):
                process_file(filepath)

    time.sleep(5)