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:
@@ -243,6 +243,24 @@ def _enviar_nuevo(chat_id, texto, teclado=None):
|
|||||||
_log(f"send error: {e}")
|
_log(f"send error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _cerrar_menu(chat_id, message_id):
|
||||||
|
"""Quita los botones del menu (lo deja cerrado como texto estatico)."""
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
requests.post(_api("editMessageReplyMarkup"),
|
||||||
|
data={"chat_id": chat_id, "message_id": message_id},
|
||||||
|
timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
_log(f"cerrar menu error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _disp(chat_id, message_id, comando):
|
||||||
|
"""Ejecuta un comando del bot y cierra el menu (opcion elegida)."""
|
||||||
|
import pi_bot
|
||||||
|
pi_bot.procesar(comando, chat_id)
|
||||||
|
_cerrar_menu(chat_id, message_id)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------- menu ----
|
# ----------------------------------------------------------------- menu ----
|
||||||
|
|
||||||
def enviar_menu(chat_id):
|
def enviar_menu(chat_id):
|
||||||
@@ -330,21 +348,23 @@ def procesar_callback(data, chat_id, message_id):
|
|||||||
}
|
}
|
||||||
_enviar_nuevo(chat_id, preguntas.get(accion, "Escribe el dato:"),
|
_enviar_nuevo(chat_id, preguntas.get(accion, "Escribe el dato:"),
|
||||||
teclado_cancelar())
|
teclado_cancelar())
|
||||||
|
_cerrar_menu(chat_id, message_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
if ruta == "pausa_min":
|
if ruta == "pausa_min":
|
||||||
nombre = _get_pausa_nombre(chat_id)
|
nombre = _get_pausa_nombre(chat_id)
|
||||||
if not nombre:
|
if not nombre:
|
||||||
_enviar_nuevo(chat_id, "Empieza de nuevo: /menu", teclado_cancelar())
|
_enviar_nuevo(chat_id, "Empieza de nuevo: /menu", teclado_cancelar())
|
||||||
|
_cerrar_menu(chat_id, message_id)
|
||||||
return
|
return
|
||||||
if partes[1] == "otro":
|
if partes[1] == "otro":
|
||||||
_guardar_pendiente(chat_id, "pausa_minutos")
|
_guardar_pendiente(chat_id, "pausa_minutos")
|
||||||
_enviar_nuevo(chat_id, "⏱️ ¿Cuántos minutos? (1-1440)", teclado_cancelar())
|
_enviar_nuevo(chat_id, "⏱️ ¿Cuántos minutos? (1-1440)", teclado_cancelar())
|
||||||
|
_cerrar_menu(chat_id, message_id)
|
||||||
return
|
return
|
||||||
minutos = partes[1]
|
minutos = partes[1]
|
||||||
import pi_bot
|
_disp(chat_id, message_id,
|
||||||
_enviar_nuevo(chat_id, f"⏸️ Pausa {nombre} {minutos} minutos…", None)
|
f"parental pausa {nombre} {minutos}")
|
||||||
pi_bot.procesar(f"parental pausa {nombre} {minutos}", chat_id)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if ruta == "acc":
|
if ruta == "acc":
|
||||||
@@ -383,8 +403,7 @@ def _ejecutar_parental(chat_id, message_id, accion, nombre):
|
|||||||
teclado_minutos_pausa(nombre))
|
teclado_minutos_pausa(nombre))
|
||||||
return
|
return
|
||||||
comando = f"parental {'desactivar' if accion == 'desactivar' else 'activar'} {nombre}"
|
comando = f"parental {'desactivar' if accion == 'desactivar' else 'activar'} {nombre}"
|
||||||
import pi_bot
|
_disp(chat_id, message_id, comando)
|
||||||
pi_bot.procesar(comando, chat_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _ejecutar_accion(chat_id, message_id, partes):
|
def _ejecutar_accion(chat_id, message_id, partes):
|
||||||
@@ -408,10 +427,10 @@ def _ejecutar_accion(chat_id, message_id, partes):
|
|||||||
"parental_listar": "parental listar",
|
"parental_listar": "parental listar",
|
||||||
}
|
}
|
||||||
if accion in directos:
|
if accion in directos:
|
||||||
pi_bot.procesar(directos[accion], chat_id)
|
_disp(chat_id, message_id, directos[accion])
|
||||||
return
|
return
|
||||||
if accion == "termo":
|
if accion == "termo":
|
||||||
pi_bot.procesar(f"termo {sub}", chat_id)
|
_disp(chat_id, message_id, f"termo {sub}")
|
||||||
return
|
return
|
||||||
if accion == "router":
|
if accion == "router":
|
||||||
mapeo = {
|
mapeo = {
|
||||||
@@ -423,12 +442,12 @@ def _ejecutar_accion(chat_id, message_id, partes):
|
|||||||
"reiniciar": "router reiniciar",
|
"reiniciar": "router reiniciar",
|
||||||
"reservar": "router reservar",
|
"reservar": "router reservar",
|
||||||
}
|
}
|
||||||
pi_bot.procesar(mapeo.get(sub, "router ayuda"), chat_id)
|
_disp(chat_id, message_id, mapeo.get(sub, "router ayuda"))
|
||||||
return
|
return
|
||||||
if accion in ("torrents", "bajar"):
|
if accion in ("torrents", "bajar"):
|
||||||
numero = int(sub) if sub.isdigit() else 0
|
numero = int(sub) if sub.isdigit() else 0
|
||||||
if numero:
|
if numero:
|
||||||
pi_bot.procesar(f"{accion} {numero}", chat_id)
|
_disp(chat_id, message_id, f"{accion} {numero}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,64 @@ def listar_descargas(hablar_solo_activas=True, session=None):
|
|||||||
return lineas, activas
|
return lineas, activas
|
||||||
|
|
||||||
|
|
||||||
|
def listar_tareas(session=None):
|
||||||
|
"""Devuelve todas las tareas de Download Station con id y estado,
|
||||||
|
para poder borrar una concreta desde Telegram."""
|
||||||
|
if not session:
|
||||||
|
session = login()
|
||||||
|
if not session:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
r = _api("/webapi/DownloadStation/task.cgi", {
|
||||||
|
"api": "SYNO.DownloadStation.Task",
|
||||||
|
"version": "3",
|
||||||
|
"method": "list",
|
||||||
|
"additional": "detail,transfer",
|
||||||
|
}, session=session)
|
||||||
|
if not r.get("success"):
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[NAS] Error listar_tareas: {e}")
|
||||||
|
return []
|
||||||
|
salida = []
|
||||||
|
for t in (r.get("data", {}).get("tasks", []) or []):
|
||||||
|
detail = (t.get("additional") or {}).get("detail") or {}
|
||||||
|
transfer = (t.get("additional") or {}).get("transfer") or {}
|
||||||
|
total = detail.get("total_pieces") or 0
|
||||||
|
done = transfer.get("downloaded_pieces") or 0
|
||||||
|
pct = (done * 100.0 / total) if total else 0.0
|
||||||
|
salida.append({
|
||||||
|
"id": t.get("id"),
|
||||||
|
"title": t.get("title") or "",
|
||||||
|
"status": t.get("status") or "?",
|
||||||
|
"pct": pct,
|
||||||
|
"size": transfer.get("size_downloaded") or 0,
|
||||||
|
})
|
||||||
|
return salida
|
||||||
|
|
||||||
|
|
||||||
|
def borrar_tarea(tid, session=None):
|
||||||
|
"""Borra una tarea de Download Station por su id.
|
||||||
|
Devuelve (ok, detalle)."""
|
||||||
|
if not session:
|
||||||
|
session = login()
|
||||||
|
if not session:
|
||||||
|
return False, "No se pudo autenticar con Download Station."
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
r = _api("/webapi/DownloadStation/task.cgi", {
|
||||||
|
"api": "SYNO.DownloadStation.Task",
|
||||||
|
"version": "3",
|
||||||
|
"method": "delete",
|
||||||
|
"id": _json.dumps([tid]),
|
||||||
|
}, session=session)
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
if r.get("success"):
|
||||||
|
return True, "ok"
|
||||||
|
return False, str(r.get("error"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ temporizadores_parental = {} # alias normalizado -> Timer de desbloqueo
|
|||||||
menu_pendiente = {} # chat_id -> accion de texto libre esperada (menu botones)
|
menu_pendiente = {} # chat_id -> accion de texto libre esperada (menu botones)
|
||||||
menu_pausa_nombre = {} # chat_id -> nombre elegido en flujo de pausa
|
menu_pausa_nombre = {} # chat_id -> nombre elegido en flujo de pausa
|
||||||
historial_chat = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar)
|
historial_chat = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar)
|
||||||
|
borrar_pend = {} # chat_id -> lista de tareas NAS esperando numero a borrar
|
||||||
|
|
||||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||||
LOG = os.path.join(BASE, "bot.log")
|
LOG = os.path.join(BASE, "bot.log")
|
||||||
@@ -119,6 +120,7 @@ def teclado_descargas():
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
{"text": "📁 Todas", "callback_data": "dl:all"},
|
{"text": "📁 Todas", "callback_data": "dl:all"},
|
||||||
|
{"text": "🗑️ Borrar…", "callback_data": "dl:del"},
|
||||||
{"text": "✖️ Cerrar", "callback_data": "dl:close"},
|
{"text": "✖️ Cerrar", "callback_data": "dl:close"},
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -245,6 +247,9 @@ def procesar_callback(callback):
|
|||||||
return
|
return
|
||||||
accion = data.split(":", 1)[-1]
|
accion = data.split(":", 1)[-1]
|
||||||
estado_panel = panel_descargas.get(chat_id)
|
estado_panel = panel_descargas.get(chat_id)
|
||||||
|
if accion == "del":
|
||||||
|
pedir_borrar(chat_id)
|
||||||
|
return
|
||||||
if not estado_panel:
|
if not estado_panel:
|
||||||
return
|
return
|
||||||
if accion == "close":
|
if accion == "close":
|
||||||
@@ -292,6 +297,57 @@ def limpiar_pantalla(chat_id):
|
|||||||
threading.Timer(6.0, borrar, args=(chat_id, aviso)).start()
|
threading.Timer(6.0, borrar, args=(chat_id, aviso)).start()
|
||||||
|
|
||||||
|
|
||||||
|
def pedir_borrar(chat_id):
|
||||||
|
"""Pide que se elija el numero de la tarea NAS a borrar."""
|
||||||
|
try:
|
||||||
|
import nas_dl
|
||||||
|
except Exception as e:
|
||||||
|
log(f"pedir_borrar import error: {e}")
|
||||||
|
enviar(f"❌ Error cargando el módulo del NAS: {e}", chat_id)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
tareas = nas_dl.listar_tareas()
|
||||||
|
except Exception as e:
|
||||||
|
log(f"listar tareas error: {e}")
|
||||||
|
enviar(f"❌ Error listando descargas: {e}", chat_id)
|
||||||
|
return
|
||||||
|
if not tareas:
|
||||||
|
enviar("No hay tareas en Download Station.", chat_id)
|
||||||
|
return
|
||||||
|
borrar_pend[chat_id] = tareas
|
||||||
|
iconos = {"downloading": "▶️", "waiting": "⏳", "error": "❌",
|
||||||
|
"finished": "✅", "paused": "⏸️", "seeding": "🌱"}
|
||||||
|
lineas = ["🗑️ <b>Responde el número</b> de la tarea a BORRAR "
|
||||||
|
"(o escribe <code>cancelar</code>):", ""]
|
||||||
|
for i, t in enumerate(tareas, start=1):
|
||||||
|
ic = iconos.get(t["status"], "•")
|
||||||
|
titulo = (t["title"] or "?").strip()
|
||||||
|
lineas.append(f"{i}. {ic} [{t['status']}] ({t['pct']:.0f}%) {titulo[:70]}")
|
||||||
|
enviar("\n".join(lineas), chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _ejecutar_borrado(chat_id, numero):
|
||||||
|
tareas = borrar_pend.pop(chat_id, None)
|
||||||
|
if not tareas:
|
||||||
|
return False
|
||||||
|
if numero < 1 or numero > len(tareas):
|
||||||
|
enviar("❌ Número no válido.", chat_id)
|
||||||
|
return True
|
||||||
|
tarea = tareas[numero - 1]
|
||||||
|
try:
|
||||||
|
import nas_dl
|
||||||
|
except Exception as e:
|
||||||
|
enviar(f"❌ Error cargando el módulo del NAS: {e}", chat_id)
|
||||||
|
return True
|
||||||
|
ok, detalle = nas_dl.borrar_tarea(tarea["id"])
|
||||||
|
titulo = (tarea["title"] or "?").strip()
|
||||||
|
if ok:
|
||||||
|
enviar(f"🗑️ Tarea borrada: <b>{titulo[:80]}</b>", chat_id)
|
||||||
|
else:
|
||||||
|
enviar(f"❌ No se pudo borrar <b>{titulo[:60]}</b>: {detalle}", chat_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def mandar_tv(mensaje, simple):
|
def mandar_tv(mensaje, simple):
|
||||||
try:
|
try:
|
||||||
url = f"{TV_API}?texto={urllib.parse.quote(mensaje)}"
|
url = f"{TV_API}?texto={urllib.parse.quote(mensaje)}"
|
||||||
@@ -340,6 +396,10 @@ def procesar(texto, chat_id):
|
|||||||
return
|
return
|
||||||
if cmd:
|
if cmd:
|
||||||
return procesar(cmd, chat_id)
|
return procesar(cmd, chat_id)
|
||||||
|
# Borrado de descargas pendiente (numero de tarea NAS)
|
||||||
|
if t.isdigit() and chat_id in borrar_pend:
|
||||||
|
_ejecutar_borrado(chat_id, int(t))
|
||||||
|
return
|
||||||
# Flujo interactivo por genero (espera numeros en pasos)
|
# Flujo interactivo por genero (espera numeros en pasos)
|
||||||
if flujo_genero.get(chat_id) and t.isdigit():
|
if flujo_genero.get(chat_id) and t.isdigit():
|
||||||
paso = flujo_genero[chat_id]
|
paso = flujo_genero[chat_id]
|
||||||
@@ -418,6 +478,10 @@ def procesar(texto, chat_id):
|
|||||||
else:
|
else:
|
||||||
ok = mandar_youtube(url)
|
ok = mandar_youtube(url)
|
||||||
enviar("🎬 YouTube lanzado en la TV ✅" if ok else "❌ No se pudo lanzar YouTube", chat_id)
|
enviar("🎬 YouTube lanzado en la TV ✅" if ok else "❌ No se pudo lanzar YouTube", chat_id)
|
||||||
|
elif comando == "borrar" and not argumento:
|
||||||
|
pedir_borrar(chat_id)
|
||||||
|
elif comando == "descargas" and argumento.lower() == "borrar":
|
||||||
|
pedir_borrar(chat_id)
|
||||||
elif t in ("descargas", "/descargas") or (comando == "descargar" and not argumento):
|
elif t in ("descargas", "/descargas") or (comando == "descargar" and not argumento):
|
||||||
abrir_panel_descargas(chat_id)
|
abrir_panel_descargas(chat_id)
|
||||||
elif comando == "descargar":
|
elif comando == "descargar":
|
||||||
@@ -716,7 +780,8 @@ def procesar(texto, chat_id):
|
|||||||
" ✏️ Ej: <code>/novedades</code>\n\n"
|
" ✏️ Ej: <code>/novedades</code>\n\n"
|
||||||
"━━━ 🖥️ NAS / DESCARGAS ━━━\n"
|
"━━━ 🖥️ NAS / DESCARGAS ━━━\n"
|
||||||
"<b>8.</b> <code>descargas</code> — panel con progreso y botones\n"
|
"<b>8.</b> <code>descargas</code> — panel con progreso y botones\n"
|
||||||
" ✏️ Ej: <code>/descargas</code> o <code>/descargar</code>\n"
|
" ▶️ En el panel: botón <b>🗑️ Borrar…</b> para eliminar una tarea\n"
|
||||||
|
" ✏️ Ej: <code>/descargas</code> o <code>/borrar</code>\n"
|
||||||
"<b>9.</b> <code>descargar <enlace></code> — añade a Download Station\n"
|
"<b>9.</b> <code>descargar <enlace></code> — añade a Download Station\n"
|
||||||
" ✏️ Ej: <code>/descargar magnet:?xt=urn:btih:...</code>\n"
|
" ✏️ Ej: <code>/descargar magnet:?xt=urn:btih:...</code>\n"
|
||||||
"<b>10.</b> <code>termo on|off|estado</code> — controla el enchufe Tapo\n"
|
"<b>10.</b> <code>termo on|off|estado</code> — controla el enchufe Tapo\n"
|
||||||
@@ -1091,6 +1156,23 @@ def guardar_offset(offset):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _atender_update(upd):
|
||||||
|
"""Procesa una update en su propio hilo (no bloquea el polling)."""
|
||||||
|
try:
|
||||||
|
if upd.get("callback_query"):
|
||||||
|
procesar_callback(upd["callback_query"])
|
||||||
|
return
|
||||||
|
msg = upd.get("message") or upd.get("channel_post")
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
texto = msg.get("text")
|
||||||
|
chat_id = msg["chat"]["id"]
|
||||||
|
if texto:
|
||||||
|
procesar(texto, chat_id)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"ERROR update: {e}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
print("Falta TELEGRAM_TOKEN en .env")
|
print("Falta TELEGRAM_TOKEN en .env")
|
||||||
@@ -1112,16 +1194,8 @@ def main():
|
|||||||
offset = upd["update_id"] + 1
|
offset = upd["update_id"] + 1
|
||||||
guardar_offset(offset)
|
guardar_offset(offset)
|
||||||
log(f"UPDATE {upd.get('update_id')}")
|
log(f"UPDATE {upd.get('update_id')}")
|
||||||
if upd.get("callback_query"):
|
threading.Thread(target=_atender_update, args=(upd,),
|
||||||
procesar_callback(upd["callback_query"])
|
daemon=True).start()
|
||||||
continue
|
|
||||||
msg = upd.get("message") or upd.get("channel_post")
|
|
||||||
if not msg:
|
|
||||||
continue
|
|
||||||
texto = msg.get("text")
|
|
||||||
chat_id = msg["chat"]["id"]
|
|
||||||
if texto:
|
|
||||||
procesar(texto, chat_id)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"ERROR {e}")
|
log(f"ERROR {e}")
|
||||||
print("Error:", e)
|
print("Error:", e)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ bloqueos = {} # chat_id -> True (procesando mensaje)
|
|||||||
sesiones = {} # chat_id -> {'repo': slug, 'session': id, 'model': obj|None}
|
sesiones = {} # chat_id -> {'repo': slug, 'session': id, 'model': obj|None}
|
||||||
repos_cache = [] # lista de {'slug','dir'}
|
repos_cache = [] # lista de {'slug','dir'}
|
||||||
sentidos = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar)
|
sentidos = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar)
|
||||||
|
menu_msg = {} # chat_id -> message_id del menu abierto
|
||||||
|
|
||||||
|
|
||||||
def log(linea):
|
def log(linea):
|
||||||
@@ -140,10 +141,14 @@ def teclado(filas):
|
|||||||
|
|
||||||
|
|
||||||
def tg_con_teclado(chat_id, texto, filas):
|
def tg_con_teclado(chat_id, texto, filas):
|
||||||
_registrar(chat_id, tg("sendMessage",
|
resp = tg("sendMessage",
|
||||||
{"chat_id": chat_id, "text": texto,
|
{"chat_id": chat_id, "text": texto, "parse_mode": "HTML",
|
||||||
"parse_mode": "HTML",
|
"reply_markup": json.dumps(teclado(filas))})
|
||||||
"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):
|
def editar(chat_id, mid, texto, filas=None):
|
||||||
@@ -443,6 +448,7 @@ def cmd_ayuda(chat):
|
|||||||
"<b>🤖 OpenCode por Telegram</b>\n"
|
"<b>🤖 OpenCode por Telegram</b>\n"
|
||||||
"Escribe cualquier cosa y lo hará el agente en el repo activo.\n\n"
|
"Escribe cualquier cosa y lo hará el agente en el repo activo.\n\n"
|
||||||
"<b>Comandos:</b>\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>/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>/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"
|
"<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)
|
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):
|
def responder_callback(callback):
|
||||||
cid = callback.get("id")
|
cid = callback.get("id")
|
||||||
data = callback.get("data") or ""
|
data = callback.get("data") or ""
|
||||||
@@ -572,6 +726,14 @@ def responder_callback(callback):
|
|||||||
chat = (msg.get("chat") or {}).get("id")
|
chat = (msg.get("chat") or {}).get("id")
|
||||||
mid = msg.get("message_id")
|
mid = msg.get("message_id")
|
||||||
tg("answerCallbackQuery", {"callback_query_id": cid})
|
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:"):
|
if not data.startswith("cb:"):
|
||||||
return
|
return
|
||||||
partes = data.split(":")
|
partes = data.split(":")
|
||||||
@@ -625,6 +787,8 @@ def procesar(texto, chat):
|
|||||||
resto = partes[1].strip() if len(partes) > 1 else ""
|
resto = partes[1].strip() if len(partes) > 1 else ""
|
||||||
if comando in ("/start", "/ayuda", "/help"):
|
if comando in ("/start", "/ayuda", "/help"):
|
||||||
cmd_ayuda(chat)
|
cmd_ayuda(chat)
|
||||||
|
elif comando == "/menu":
|
||||||
|
cmd_menu(chat)
|
||||||
elif comando in ("/nueva", "/nuevo"):
|
elif comando in ("/nueva", "/nuevo"):
|
||||||
cmd_nueva(chat, resto)
|
cmd_nueva(chat, resto)
|
||||||
elif comando in ("/salir", "/fin", "/exit"):
|
elif comando in ("/salir", "/fin", "/exit"):
|
||||||
@@ -660,7 +824,9 @@ def main():
|
|||||||
offset = upd["update_id"] + 1
|
offset = upd["update_id"] + 1
|
||||||
guardar_offset(offset)
|
guardar_offset(offset)
|
||||||
if upd.get("callback_query"):
|
if upd.get("callback_query"):
|
||||||
responder_callback(upd["callback_query"])
|
threading.Thread(target=responder_callback,
|
||||||
|
args=(upd["callback_query"],),
|
||||||
|
daemon=True).start()
|
||||||
continue
|
continue
|
||||||
msg = upd.get("message")
|
msg = upd.get("message")
|
||||||
if not msg:
|
if not msg:
|
||||||
|
|||||||
Reference in New Issue
Block a user