05-sep: [feat] menu wizard en bot OpenCode (/menu), menu IDD cierra al elegir + updates en hilos, borrar tarea NAS (0%) desde panel

This commit is contained in:
juanjo
2026-09-05 22:47:22 +02:00
parent 1c6bd113fd
commit 23fe44aecd
4 changed files with 342 additions and 25 deletions

View File

@@ -53,6 +53,7 @@ bloqueos = {} # chat_id -> True (procesando mensaje)
sesiones = {} # chat_id -> {'repo': slug, 'session': id, 'model': obj|None}
repos_cache = [] # lista de {'slug','dir'}
sentidos = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar)
menu_msg = {} # chat_id -> message_id del menu abierto
def log(linea):
@@ -140,10 +141,14 @@ def teclado(filas):
def tg_con_teclado(chat_id, texto, filas):
_registrar(chat_id, tg("sendMessage",
{"chat_id": chat_id, "text": texto,
"parse_mode": "HTML",
"reply_markup": json.dumps(teclado(filas))}))
resp = tg("sendMessage",
{"chat_id": chat_id, "text": texto, "parse_mode": "HTML",
"reply_markup": json.dumps(teclado(filas))})
_registrar(chat_id, resp)
try:
return (resp.get("result") or {}).get("message_id")
except Exception:
return None
def editar(chat_id, mid, texto, filas=None):
@@ -443,6 +448,7 @@ def cmd_ayuda(chat):
"<b>🤖 OpenCode por Telegram</b>\n"
"Escribe cualquier cosa y lo hará el agente en el repo activo.\n\n"
"<b>Comandos:</b>\n"
"<code>/menu</code> — menú interactivo (modelo, proyecto, cerrar)\n"
"<code>/nueva [texto]</code> — sesión nueva (olvida el contexto)\n"
"<code>/salir</code> — cerrar la sesión actual (la próxima arranca limpia)\n"
"<code>/proyecto</code> / <code>/defecto</code> — elegir repo (queda por defecto)\n"
@@ -565,6 +571,154 @@ def cmd_modelo(chat):
tg_con_teclado(chat, "🧠 Elige modelo (se aplica a partir del próximo mensaje):", filas)
# ------------------------------------------------------------ menu wizard --
def _wiz_resumen(chat):
slug, _ = repo_activo()
st = sesiones.get(str(chat)) or {}
modelo = (st.get("model") or {}).get("name") or "por defecto"
return (f"🧭 <b>Menú OpenCode</b>\n"
f"🧠 Modelo: <b>{modelo}</b>\n"
f"📂 Proyecto: <b>{slug}</b>\n\n"
"Configura y pulsa <b>Cerrar</b>.")
def _wiz_raiz_filas():
return [
[{"text": "🧠 Elegir modelo", "callback_data": "w:modelo"},
{"text": "📂 Elegir proyecto", "callback_data": "w:proyecto"}],
[{"text": "✅ Cerrar menú", "callback_data": "w:cerrar"},
{"text": "🚪 Salir", "callback_data": "w:salir"}],
]
def _quitar_teclado(chat, mid):
try:
requests.post(f"{API}/editMessageReplyMarkup",
data={"chat_id": chat, "message_id": mid}, timeout=10)
except Exception:
pass
def cmd_menu(chat):
mid = tg_con_teclado(chat, _wiz_resumen(chat), _wiz_raiz_filas())
if mid:
menu_msg[str(chat)] = mid
def _wiz_mostrar(chat, mid, texto, filas):
try:
requests.post(f"{API}/editMessageText",
data={"chat_id": chat, "message_id": mid, "text": texto,
"parse_mode": "HTML",
"reply_markup": json.dumps(teclado(filas))},
timeout=15)
except Exception as e:
log(f"wiz edit error: {e}")
def _cambiar_repo_silencioso(chat, slug):
ruta = os.path.join(GH, slug)
if not os.path.isdir(os.path.join(ruta, ".git")):
return "Repo no válido"
try:
with open(REPO_ENV, "w") as f:
f.write(f"REPO_DIR={ruta}\n")
subprocess.run(["systemctl", "restart", "opencode-serve"],
timeout=60, capture_output=True)
except Exception as e:
log(f"cambiar_repo_silencioso error: {e}")
return str(e)
for _ in range(25):
if health_ok():
break
time.sleep(1)
st = sesiones.get(str(chat)) or {}
sid, err = crear_sesion()
if sid:
sesiones[str(chat)] = {"repo": slug, "session": sid,
"model": st.get("model")}
guardar_estado()
return None
return str(err)
def manejar_wizard(data, chat, mid):
if data == "w:cerrar":
_quitar_teclado(chat, mid)
return True
if data == "w:salir":
_quitar_teclado(chat, mid)
return True
if data == "w:modelo":
modelos = listar_modelos()
if not modelos:
_wiz_mostrar(chat, mid, "No hay modelos disponibles.", _wiz_raiz_filas())
return True
st = sesiones.get(str(chat)) or {}
actual = (st.get("model") or {}).get("id")
filas = []
for m in modelos[:24]:
marca = " ✅" if m["id"] == actual else ""
filas.append([{"text": (m["name"] + marca)[:50],
"callback_data": f"wm:{m['providerID']}:{m['id']}"}])
filas.append([{"text": "↩️ Por defecto", "callback_data": "wm:def"},
{"text": "⬅️ Atrás", "callback_data": "w:root"}])
_wiz_mostrar(chat, mid, "🧠 Elige modelo:", filas)
return True
if data == "w:proyecto":
lista = listar_repos()
if not lista:
_wiz_mostrar(chat, mid, "No hay repos.", _wiz_raiz_filas())
return True
slug_act, _ = repo_activo()
filas = []
for i, r in enumerate(lista[:20]):
marca = " ✅" if r["slug"] == slug_act else ""
filas.append([{"text": r["slug"] + marca,
"callback_data": f"wr:{i}"}])
filas.append([{"text": "⬅️ Atrás", "callback_data": "w:root"}])
_wiz_mostrar(chat, mid, "📂 Elige proyecto (reinicia el serve):", filas)
return True
if data == "w:root":
_wiz_mostrar(chat, mid, _wiz_resumen(chat), _wiz_raiz_filas())
return True
if data.startswith("wm:"):
if data == "wm:def":
st = sesiones.get(str(chat)) or {}
st["model"] = None
sesiones[str(chat)] = st
guardar_estado()
else:
resto = data[3:]
provider, mid_modelo = resto.split(":", 1)
elegido = next((m for m in listar_modelos()
if m["providerID"] == provider
and m["id"] == mid_modelo), None)
if elegido:
st = sesiones.get(str(chat)) or {}
st["model"] = elegido
sesiones[str(chat)] = st
guardar_estado()
_wiz_mostrar(chat, mid, _wiz_resumen(chat), _wiz_raiz_filas())
return True
if data.startswith("wr:"):
try:
idx = int(data[3:])
r = repos_cache[idx]
except Exception:
return True
_wiz_mostrar(chat, mid, f"Cambiando a <b>{r['slug']}</b>…",
[[{"text": "⏳", "callback_data": "w:none"}]])
err = _cambiar_repo_silencioso(chat, r["slug"])
if err:
_wiz_mostrar(chat, mid, f"⚠️ {err}", _wiz_raiz_filas())
else:
_wiz_mostrar(chat, mid, _wiz_resumen(chat), _wiz_raiz_filas())
return True
return False
def responder_callback(callback):
cid = callback.get("id")
data = callback.get("data") or ""
@@ -572,6 +726,14 @@ def responder_callback(callback):
chat = (msg.get("chat") or {}).get("id")
mid = msg.get("message_id")
tg("answerCallbackQuery", {"callback_query_id": cid})
if ALLOWED and str(chat) not in ALLOWED.split(","):
return
if data.startswith(("w:", "wm:", "wr:")):
try:
manejar_wizard(data, chat, mid)
except Exception as e:
log(f"wizard error: {e}")
return
if not data.startswith("cb:"):
return
partes = data.split(":")
@@ -625,6 +787,8 @@ def procesar(texto, chat):
resto = partes[1].strip() if len(partes) > 1 else ""
if comando in ("/start", "/ayuda", "/help"):
cmd_ayuda(chat)
elif comando == "/menu":
cmd_menu(chat)
elif comando in ("/nueva", "/nuevo"):
cmd_nueva(chat, resto)
elif comando in ("/salir", "/fin", "/exit"):
@@ -660,7 +824,9 @@ def main():
offset = upd["update_id"] + 1
guardar_offset(offset)
if upd.get("callback_query"):
responder_callback(upd["callback_query"])
threading.Thread(target=responder_callback,
args=(upd["callback_query"],),
daemon=True).start()
continue
msg = upd.get("message")
if not msg: