#!/usr/bin/env python3 """Ejecuta las acciones de verdad y mira si funcionan y si se entienden. python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py --romper # solo las malas Las 121 acciones se escribieron a mano y casi ninguna se ha ejecutado nunca. Alguna tendra el comando roto, la salida vacia, o una plantilla que apunta a un campo que no existe —y entonces JARVIS diria una frase sin sentido en voz alta. Esto las corre, captura la salida y las clasifica, para saber cuales arreglar. Que hace con cada tipo: - Solo lectura (df, free, ps, cat /sys...): las EJECUTA y comprueba la salida. Son la mayoria y las que de verdad importan, porque son las que hay que "interpretar bien". - Con efecto (abre navegador, apaga pantalla, bloquea, sube volumen): NO las ejecuta —tendria efectos de verdad— pero valida que el comando exista. - Destructivas (confirmar=True): NO las ejecuta, solo comprueba el comando. - Con argumento: las ejecuta con un valor de prueba inofensivo. Clasificacion de cada accion: OK corrio, dio salida, y la respuesta hablada sale con datos reales VACIA corrio pero sin salida (la herramienta no aplica en esta maquina) ROTA el comando fallo o no existe PLANTILLA la respuesta cayo al volcado crudo: la plantilla pide un campo que la salida no tiene. Es el fallo que hace que JARVIS suene raro. EFECTO no se ejecuta por tener efectos; comando validado """ import argparse import importlib.util import json import os import re import shlex import subprocess import sys JARVIS = os.path.expanduser("~/COFRE/CODERS/JARVIS") MOTOR = os.path.join(JARVIS, "newelle/src/utility/acciones.py") CATALOGO = os.path.expanduser( "~/.local/share/flatpak/app/io.github.qwersyk.Newelle/x86_64/master/" "active/files/share/newelle/acciones/acciones.json") CATALOGO_FUENTE = os.path.join(JARVIS, "newelle/data/acciones/acciones.json") INFORME = os.path.join(JARVIS, "muestras/acciones-salud.json") # Órdenes que cambian algo del sistema o abren cosas: se validan pero no se # lanzan, que probar un asistente no deberia dejarte el navegador con veinte # pestañas ni la pantalla apagada. EFECTOS = ["firefox", "xdg-open", "loginctl", "xset", "pactl set", "pkill", "brightnessctl", "systemctl start", "systemctl stop", "import -window", "gnome-screenshot", "rm ", "kill", "--private-window", "dpms", "ventanas.sh", "xdotool", "wmctrl", "xrandr --output"] # Valores de prueba para las acciones con argumento, inofensivos a proposito. ARGUMENTOS = { "buscar_fichero": "acciones", "buscar_carpeta": "config", "leer_fichero": "README", "abrir_fichero": "no_existe_xyz", "abrir_carpeta": "no_existe_xyz", "tamano_carpeta": "config", "proceso_buscar": "python", "proceso_matar": "proceso_inexistente_xyz", "instalado": "bash", "manual": "ls", } def carga_motor(): spec = importlib.util.spec_from_file_location("acciones", MOTOR) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return m def spawn(): return ["flatpak-spawn", "--host"] if os.path.exists("/.flatpak-info") else [] def tiene_efecto(comando: str) -> bool: return any(marca in comando for marca in EFECTOS) def ejecuta_crudo(comando: str, argumento: str = "") -> tuple: """Corre el comando en el host y devuelve (salida, codigo).""" c = comando.replace("{argumento}", shlex.quote(argumento)) c = c.replace("{argumento_url}", shlex.quote(argumento)) try: r = subprocess.run(spawn() + ["bash", "-lc", c], capture_output=True, text=True, timeout=15) return (r.stdout or r.stderr or "").strip(), r.returncode except subprocess.TimeoutExpired: return "TIMEOUT", 124 except OSError as e: return f"ERROR: {e}", 1 def clasifica(motor, accion, salida, codigo): """A partir de la salida cruda, decide como quedaria la respuesta hablada.""" plantilla = accion.respuesta dicho = motor.formatea(plantilla, salida, "") # ¿la plantilla cayo al volcado crudo? formatea() devuelve {salida} cuando # el .format() peta por un campo que no existe. Se detecta comparando: si # la respuesta ES la salida y la plantilla NO era {salida}, fallo. campos = re.findall(r"\{(campo\d+|ultima|primera)\}", plantilla) if campos and dicho.strip() == salida[:600].strip() and plantilla.strip() not in ("{salida}", "{salida}."): return "PLANTILLA", dicho if not salida: return "VACIA", dicho if codigo != 0 and ("command not found" in salida or "No such file" in salida or salida.startswith("ERROR") or salida == "TIMEOUT"): return "ROTA", dicho return "OK", dicho def main(): ap = argparse.ArgumentParser() ap.add_argument("--romper", action="store_true", help="solo las que fallan") ap.add_argument("--json", action="store_true", help="salida en json") args = ap.parse_args() motor = carga_motor() catalogo = CATALOGO if os.path.exists(CATALOGO) else CATALOGO_FUENTE cat = motor.Catalogo([catalogo]) resultados = [] for accion in cat.acciones: estado = None if accion.confirmar: estado = "EFECTO" if not accion.comando else "DESTRUCTIVA" dicho = accion.acuse elif tiene_efecto(accion.comando): estado = "EFECTO" dicho = "(no se ejecuta; " + ("comando ok" if accion.comando else "sin comando") + ")" else: arg = ARGUMENTOS.get(accion.id, "") if accion.captura else "" salida, codigo = ejecuta_crudo(accion.comando, arg) estado, dicho = clasifica(motor, accion, salida, codigo) resultados.append({ "id": accion.id, "grupo": accion.grupo, "estado": estado, "dice": dicho[:120], }) cuenta = {} for r in resultados: cuenta[r["estado"]] = cuenta.get(r["estado"], 0) + 1 if args.json: print(json.dumps({"cuenta": cuenta, "acciones": resultados}, ensure_ascii=False)) else: malas = {"ROTA", "PLANTILLA", "VACIA"} for r in resultados: if args.romper and r["estado"] not in malas: continue marca = {"OK": " ok ", "VACIA": " vacia", "ROTA": " ROTA ", "PLANTILLA": " PLANT", "EFECTO": " efect", "DESTRUCTIVA": " destr"}.get(r["estado"], " ? ") print(f"[{marca}] {r['id']:<22} {r['dice']}") print("\n" + " ".join(f"{k}: {v}" for k, v in sorted(cuenta.items()))) buenas = cuenta.get("OK", 0) + cuenta.get("EFECTO", 0) + cuenta.get("DESTRUCTIVA", 0) print(f"utiles: {buenas}/{len(resultados)} · " f"a revisar: {sum(cuenta.get(k, 0) for k in ('ROTA','PLANTILLA','VACIA'))}") os.makedirs(os.path.dirname(INFORME), exist_ok=True) with open(INFORME, "w") as f: json.dump({"cuenta": cuenta, "acciones": resultados}, f, indent=2, ensure_ascii=False) fallan = sum(cuenta.get(k, 0) for k in ("ROTA", "PLANTILLA")) return 1 if fallan else 0 if __name__ == "__main__": sys.exit(main())