2366 lines
101 KiB
Python
2366 lines
101 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Telegram CLI Bot - бот для выполнения CLI команд с многоуровневым меню.
|
||
Легкое добавление новых команд через регистрацию хендлеров.
|
||
|
||
Версия: 0.5.0 (рефакторинг)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import asyncio
|
||
import subprocess
|
||
import logging
|
||
import pty
|
||
import select
|
||
import fcntl
|
||
from pathlib import Path
|
||
from typing import Optional, Callable, Dict, Any, List, Tuple
|
||
from datetime import datetime, timedelta
|
||
|
||
import pexpect
|
||
import asyncssh
|
||
from qwen_integration import qwen_manager, QwenSessionState
|
||
|
||
# Подавляем логи sentence-transformers и huggingface
|
||
logging.getLogger("sentence_transformers").setLevel(logging.WARNING)
|
||
logging.getLogger("huggingface_hub").setLevel(logging.WARNING)
|
||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||
|
||
from vector_memory import (
|
||
hybrid_memory_manager,
|
||
save_message,
|
||
get_context,
|
||
get_profile,
|
||
get_memory_stats
|
||
)
|
||
|
||
from dotenv import load_dotenv
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
CallbackQueryHandler,
|
||
MessageHandler,
|
||
ContextTypes,
|
||
filters,
|
||
)
|
||
|
||
# Загрузка переменных окружения из .env
|
||
load_dotenv()
|
||
|
||
# --- Конфигурация ---
|
||
BASE_DIR = Path(__file__).parent
|
||
|
||
logging.basicConfig(
|
||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||
level=logging.INFO,
|
||
handlers=[
|
||
logging.FileHandler(BASE_DIR / "bot.log"),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ============================================================================
|
||
# ИМПОРТЫ ИЗ bot/ - новая модульная структура
|
||
# ============================================================================
|
||
from bot.config import config, state_manager, menu_builder, command_registry, server_manager
|
||
from bot.models.server import Server
|
||
from bot.models.session import SSHSession, SSHSessionManager, LocalSession, LocalSessionManager, INPUT_PATTERNS
|
||
from bot.utils.cleaners import clean_ansi_codes, normalize_output
|
||
from bot.utils.formatters import escape_markdown, split_message, send_long_message, format_long_output, MAX_MESSAGE_LENGTH
|
||
from bot.utils.ssh_readers import detect_input_type, read_ssh_output, read_pty_output
|
||
from bot.utils.decorators import check_access
|
||
from bot.keyboards.menus import MenuItem, init_menus
|
||
|
||
# Глобальные менеджеры сессий
|
||
ssh_session_manager = SSHSessionManager()
|
||
local_session_manager = LocalSessionManager()
|
||
def init_menus():
|
||
"""Инициализация структуры меню."""
|
||
|
||
# Главное меню
|
||
main_menu = [
|
||
MenuItem("🖥️ Выбор сервера", "server_menu", icon="🖥️"),
|
||
MenuItem("📋 Предустановленные команды", "preset_menu", icon="📋"),
|
||
MenuItem("💬 Чат с ИИ агентом", "toggle_ai_chat", icon="💬"),
|
||
MenuItem("⚙️ Настройки бота", "settings_menu", icon="⚙️"),
|
||
MenuItem("ℹ️ О боте", "about", icon="ℹ️"),
|
||
]
|
||
menu_builder.add_menu("main", main_menu)
|
||
|
||
# Меню серверов
|
||
server_menu = [
|
||
MenuItem("💻 local (localhost)", "server_select_local", icon="💻"),
|
||
MenuItem("➕ Добавить сервер", "server_add", icon="➕"),
|
||
MenuItem("⬅️ Назад", "main", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("server", server_menu)
|
||
|
||
# Меню предустановленных команд
|
||
preset_menu = [
|
||
MenuItem("📁 Файловая система", "fs_menu", icon="📁"),
|
||
MenuItem("🔍 Поиск", "search_menu", icon="🔍"),
|
||
MenuItem("📊 Система", "system_menu", icon="📊"),
|
||
MenuItem("🌐 Сеть", "network_menu", icon="🌐"),
|
||
MenuItem("⬅️ Назад", "main", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("preset", preset_menu)
|
||
|
||
# Файловая система
|
||
fs_menu = [
|
||
MenuItem("ls -la", "cmd_ls_la", command="ls -la", icon="📄"),
|
||
MenuItem("pwd", "cmd_pwd", command="pwd", icon="📍"),
|
||
MenuItem("df -h", "cmd_df", command="df -h", icon="💾"),
|
||
MenuItem("du -sh *", "cmd_du", command="du -sh * 2>/dev/null | sort -hr | head -20", icon="📊"),
|
||
MenuItem("⬅️ Назад", "preset", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("fs", fs_menu)
|
||
|
||
# Поиск
|
||
search_menu = [
|
||
MenuItem("find . -name", "cmd_find_name", command="find . -maxdepth 3 -name '*.txt' 2>/dev/null", icon="🔎"),
|
||
MenuItem("grep пример", "cmd_grep", command="grep -r 'example' . 2>/dev/null | head -20", icon="🔍"),
|
||
MenuItem("which command", "cmd_which", command="which python3 bash git", icon="📍"),
|
||
MenuItem("⬅️ Назад", "preset", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("search", search_menu)
|
||
|
||
# Система
|
||
system_menu = [
|
||
MenuItem("top -n 1", "cmd_top", command="top -bn1 | head -20", icon="📈"),
|
||
MenuItem("ps aux", "cmd_ps", command="ps aux | head -20", icon="🔄"),
|
||
MenuItem("free -h", "cmd_free", command="free -h", icon="💾"),
|
||
MenuItem("uname -a", "cmd_uname", command="uname -a", icon="ℹ️"),
|
||
MenuItem("uptime", "cmd_uptime", command="uptime", icon="⏱️"),
|
||
MenuItem("⬅️ Назад", "preset", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("system", system_menu)
|
||
|
||
# Сеть
|
||
network_menu = [
|
||
MenuItem("ip addr", "cmd_ip", command="ip addr 2>/dev/null || ifconfig 2>/dev/null", icon="🌐"),
|
||
MenuItem("ping google", "cmd_ping", command="ping -c 4 google.com 2>&1 | head -10", icon="📡"),
|
||
MenuItem("netstat", "cmd_netstat", command="ss -tuln 2>/dev/null || netstat -tuln 2>/dev/null | head -20", icon="🔌"),
|
||
MenuItem("curl ifconfig.me", "cmd_curl_ip", command="curl -s ifconfig.me 2>&1", icon="📍"),
|
||
MenuItem("⬅️ Назад", "preset", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("network", network_menu)
|
||
|
||
# Настройки
|
||
settings_menu = [
|
||
MenuItem("📝 Изменить имя бота", "set_name", icon="📝"),
|
||
MenuItem("📄 Изменить описание", "set_description", icon="📄"),
|
||
MenuItem("🎨 Изменить иконку", "set_icon", icon="🎨"),
|
||
MenuItem("👥 Управление доступом", "access_menu", icon="👥"),
|
||
MenuItem("🧠 Память ИИ", "memory_menu", icon="🧠"),
|
||
MenuItem("⬅️ Назад", "main", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("settings", settings_menu)
|
||
|
||
# Память ИИ
|
||
memory_menu = [
|
||
MenuItem("📋 Мой профиль", "memory_profile", icon="📋"),
|
||
MenuItem("📊 Статистика", "memory_stats", icon="📊"),
|
||
MenuItem("🗑️ Очистить историю", "memory_clear", icon="🗑️"),
|
||
MenuItem("⬅️ Назад", "settings", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("memory", memory_menu)
|
||
|
||
# Доступ
|
||
access_menu = [
|
||
MenuItem("📋 Показать разрешённых", "show_access", icon="📋"),
|
||
MenuItem("➕ Добавить пользователя", "add_access", icon="➕"),
|
||
MenuItem("➖ Удалить пользователя", "remove_access", icon="➖"),
|
||
MenuItem("⬅️ Назад", "settings", icon="⬅️"),
|
||
]
|
||
menu_builder.add_menu("access", access_menu)
|
||
|
||
|
||
# --- Хендлеры ---
|
||
|
||
@check_access
|
||
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /start."""
|
||
user = update.effective_user
|
||
logger.info(f"Пользователь {user.username} ({user.id}) запустил бота")
|
||
|
||
state_manager.reset(user.id)
|
||
|
||
# Показать текущую директорию и сервер
|
||
working_dir = config.working_directory
|
||
server = server_manager.get("local")
|
||
server_desc = server.description if server else "localhost"
|
||
|
||
await update.message.reply_text(
|
||
f"👋 Привет, {user.first_name}!\n\n"
|
||
f"{config.icon} *{config.name}*\n"
|
||
f"_{config.description}_\n\n"
|
||
f"*Просто отправьте CLI команду в чат* — я её выполню!\n\n"
|
||
f"🖥️ *Текущий сервер:* `{server_desc}`\n"
|
||
f"📁 *Рабочая директория:* `{working_dir}`\n\n"
|
||
f"Используйте `cd путь` для смены директории.\n"
|
||
f"Или выберите сервер в меню.\n"
|
||
f"Команда /help покажет справку.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=update.effective_user.id, state=state)
|
||
)
|
||
|
||
|
||
@check_access
|
||
async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /menu - показывает главное меню."""
|
||
user = update.effective_user
|
||
state = state_manager.get(user.id)
|
||
|
||
# Не сбрасываем состояние - сохраняем ai_chat_mode и другие настройки
|
||
state.current_menu = "main"
|
||
|
||
# Показать текущую директорию и сервер
|
||
working_dir = state.working_directory or config.working_directory
|
||
server = server_manager.get(state.current_server)
|
||
server_desc = server.description if server else state.current_server
|
||
|
||
await update.message.reply_text(
|
||
f"🏠 *Главное меню*\n\n"
|
||
f"🖥️ *Сервер:* `{server_desc}`\n"
|
||
f"📁 *Директория:* `{working_dir}`\n\n"
|
||
f"Выберите действие:",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=update.effective_user.id, state=state)
|
||
)
|
||
|
||
|
||
@check_access
|
||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /help."""
|
||
help_text = f"""
|
||
📖 *Справка по боту {config.name}*
|
||
|
||
*Как использовать:*
|
||
Просто отправьте любую CLI команду в чат — бот выполнит её!
|
||
|
||
*Примеры:*
|
||
• `ls -la` — список файлов
|
||
• `pwd` — текущая директория
|
||
• `df -h` — свободное место на диске
|
||
• `git status` — статус git
|
||
|
||
*Навигация по директориям:*
|
||
• `cd путь` — сменить директорию (например, `cd git/project`)
|
||
• `cd ..` — на уровень вверх
|
||
• `cd ~` — в домашнюю директорию
|
||
• `pwd` — показать текущую директорию
|
||
|
||
*Кнопки меню:*
|
||
• 📋 Предустановленные команды — быстрые команды по категориям
|
||
• ⚙️ Настройки бота — изменение имени, описания, иконки
|
||
• ℹ️ О боте — информация
|
||
|
||
*Команды управления:*
|
||
/start — Запустить бота, главное меню
|
||
/menu — Показать главное меню с кнопками
|
||
/help — Эта справка
|
||
/settings — Настройки
|
||
|
||
*Безопасность:*
|
||
Команды выполняются от вашего имени.
|
||
Будьте осторожны с деструктивными командами!
|
||
"""
|
||
await update.message.reply_text(help_text, parse_mode="Markdown")
|
||
|
||
|
||
@check_access
|
||
async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /settings."""
|
||
state = state_manager.get(update.effective_user.id)
|
||
state.current_menu = "settings"
|
||
|
||
await update.message.reply_text(
|
||
"⚙️ *Настройки бота*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("settings")
|
||
)
|
||
|
||
|
||
async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка нажатий на кнопки меню."""
|
||
query = update.callback_query
|
||
user_id = query.from_user.id
|
||
state = state_manager.get(user_id)
|
||
|
||
await query.answer()
|
||
|
||
callback = query.data
|
||
logger.info(f"Callback: {callback} от пользователя {user_id}")
|
||
|
||
# Обработка навигации
|
||
if callback == "main":
|
||
state.current_menu = "main"
|
||
|
||
# Проверяем режим чата с ИИ для обновления текста кнопки
|
||
ai_status = "✅ ВКЛ" if state.ai_chat_mode else "❌ ВЫКЛ"
|
||
await query.edit_message_text(
|
||
f"🏠 *Главное меню*\n\n"
|
||
f"💬 *Чат с ИИ:* {ai_status}",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
|
||
)
|
||
|
||
elif callback == "preset_menu":
|
||
state.current_menu = "preset"
|
||
await query.edit_message_text(
|
||
"📋 *Предустановленные команды*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("preset")
|
||
)
|
||
|
||
elif callback == "fs_menu":
|
||
await query.edit_message_text(
|
||
"📁 *Файловая система*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("fs")
|
||
)
|
||
|
||
elif callback == "search_menu":
|
||
await query.edit_message_text(
|
||
"🔍 *Поиск*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("search")
|
||
)
|
||
|
||
elif callback == "system_menu":
|
||
await query.edit_message_text(
|
||
"📊 *Система*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("system")
|
||
)
|
||
|
||
elif callback == "network_menu":
|
||
await query.edit_message_text(
|
||
"🌐 *Сеть*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("network")
|
||
)
|
||
|
||
elif callback == "server_menu":
|
||
# Динамическое обновление меню серверов с кнопками управления
|
||
servers = server_manager.list_servers()
|
||
keyboard = []
|
||
|
||
for srv in servers:
|
||
# Кнопка выбора сервера + кнопка управления (для не-local)
|
||
row = [InlineKeyboardButton(
|
||
srv.display_name,
|
||
callback_data=f"server_select_{srv.name}"
|
||
)]
|
||
if srv.name != "local":
|
||
row.append(InlineKeyboardButton(
|
||
"⚙️",
|
||
callback_data=f"server_manage_{srv.name}"
|
||
))
|
||
keyboard.append(row)
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("➕ Добавить", callback_data="server_add"),
|
||
InlineKeyboardButton("⬅️ Назад", callback_data="main")
|
||
])
|
||
|
||
state.current_menu = "server"
|
||
await query.edit_message_text(
|
||
"🖥️ *Управление серверами*\n\n"
|
||
"Выберите сервер для подключения или добавьте новый.\n"
|
||
"⚙️ — редактировать/удалить сервер",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
elif callback == "server_add":
|
||
state.waiting_for_input = True
|
||
state.input_type = "add_server_name"
|
||
state.context["new_server"] = {}
|
||
await query.edit_message_text(
|
||
"➕ *Добавление сервера*\n\n"
|
||
"Введите *имя сервера* (латиница, без пробелов):\n"
|
||
"Пример: `web-prod`, `db-backup`",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
|
||
elif callback.startswith("server_manage_"):
|
||
server_name = callback.replace("server_manage_", "")
|
||
server = server_manager.get(server_name)
|
||
|
||
if server and server_name != "local":
|
||
state.editing_server = server_name
|
||
await query.edit_message_text(
|
||
f"⚙️ *Управление сервером*\n\n"
|
||
f"{server.display_name}\n"
|
||
f"📍 `{server.description}`\n"
|
||
f"🏷️ Теги: `{','.join(server.tags) if server.tags else 'нет'}`\n\n"
|
||
f"Выберите действие:",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("✏️ Редактировать", callback_data=f"server_edit_{server_name}")],
|
||
[InlineKeyboardButton("🗑️ Удалить", callback_data=f"server_delete_{server_name}")],
|
||
[InlineKeyboardButton("⬅️ Назад", callback_data="server_menu")]
|
||
])
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
f"❌ *Сервер не найден*\n\n"
|
||
f"Сервер `{server_name}` отсутствует в конфигурации.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
elif callback.startswith("server_edit_"):
|
||
server_name = callback.replace("server_edit_", "")
|
||
server = server_manager.get(server_name)
|
||
|
||
if server and server_name != "local":
|
||
state.editing_server = server_name
|
||
state.waiting_for_input = True
|
||
state.input_type = "edit_server_field"
|
||
password_status = "установлен" if server.password else "не установлен"
|
||
await query.edit_message_text(
|
||
f"✏️ *Редактирование сервера: {server_name}*\n\n"
|
||
f"Текущие значения:\n"
|
||
f"• Host: `{server.host}`\n"
|
||
f"• Port: `{server.port}`\n"
|
||
f"• User: `{server.user}`\n"
|
||
f"• Tags: `{','.join(server.tags) if server.tags else 'нет'}`\n"
|
||
f"• Password: {password_status}\n\n"
|
||
f"Введите номер поля для изменения:\n"
|
||
f"1 — Host\n"
|
||
f"2 — Port\n"
|
||
f"3 — User\n"
|
||
f"4 — Tags\n"
|
||
f"5 — Password",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка: сервер не найден",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
elif callback.startswith("server_delete_"):
|
||
server_name = callback.replace("server_delete_", "")
|
||
server = server_manager.get(server_name)
|
||
|
||
if server and server_name != "local":
|
||
# Удаляем сразу с подтверждением
|
||
if server_manager.delete_server(server_name):
|
||
await query.edit_message_text(
|
||
f"🗑️ *Сервер удалён*\n\n"
|
||
f"Сервер `{server_name}` успешно удалён из конфигурации.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка при удалении сервера",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"❌ Нельзя удалить local сервер",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
elif callback == "srv_skip_password":
|
||
# Пропуск пароля при добавлении сервера
|
||
user_id = query.from_user.id
|
||
state = state_manager.get(user_id)
|
||
|
||
state.context["new_server"]["password"] = ""
|
||
state.input_type = "add_server_tags"
|
||
await query.edit_message_text(
|
||
"✅ Пароль пропущен (будет использоваться только ключ)\n\n"
|
||
"Введите *теги* через запятую (или нажмите Пропустить):\n"
|
||
"Пример: `web,prod`, `db,backup`\n\n"
|
||
"Теги помогают группировать серверы.",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("⏭️ Пропустить", callback_data="srv_skip_tags")],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="server_menu")]
|
||
])
|
||
)
|
||
|
||
elif callback == "srv_skip_tags":
|
||
# Пропуск тегов при добавлении сервера
|
||
user_id = query.from_user.id
|
||
state = state_manager.get(user_id)
|
||
|
||
new_server = state.context.get("new_server", {})
|
||
if new_server.get("name") and new_server.get("host") and new_server.get("port") and new_server.get("user"):
|
||
if server_manager.add_server(
|
||
name=new_server["name"],
|
||
host=new_server["host"],
|
||
port=new_server["port"],
|
||
user=new_server["user"],
|
||
tags=[],
|
||
password=new_server.get("password", "")
|
||
):
|
||
await query.edit_message_text(
|
||
"✅ *Сервер добавлен*\n\n"
|
||
f"Имя: `{new_server['name']}`\n"
|
||
f"Host: `{new_server['host']}`\n"
|
||
f"Port: `{new_server['port']}`\n"
|
||
f"User: `{new_server['user']}`\n"
|
||
f"Tags: нет\n"
|
||
f"Password: {'установлен' if new_server.get('password') else 'не установлен'}\n\n"
|
||
f"Сервер сохранён в `.env` и доступен для выбора.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка: сервер с таким именем уже существует",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await query.edit_message_text(
|
||
"❌ Ошибка: неполные данные сервера",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
state.waiting_for_input = False
|
||
state.input_type = None
|
||
state.context.clear()
|
||
|
||
elif callback.startswith("server_select_"):
|
||
server_name = callback.replace("server_select_", "")
|
||
server = server_manager.get(server_name)
|
||
|
||
if server:
|
||
state.current_server = server_name
|
||
# Сброс рабочей директории при смене сервера
|
||
state.working_directory = None
|
||
|
||
await query.edit_message_text(
|
||
f"✅ *Сервер изменён*\n\n"
|
||
f"{server.display_name}\n"
|
||
f"📍 `{server.description}`\n\n"
|
||
f"Теперь команды выполняются на этом сервере.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
|
||
)
|
||
state.current_menu = "main"
|
||
else:
|
||
await query.edit_message_text(
|
||
f"❌ *Сервер не найден*\n\n"
|
||
f"Сервер `{server_name}` отсутствует в конфигурации.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
elif callback == "settings_menu":
|
||
state.current_menu = "settings"
|
||
await query.edit_message_text(
|
||
"⚙️ *Настройки бота*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("settings")
|
||
)
|
||
|
||
elif callback == "access_menu":
|
||
await query.edit_message_text(
|
||
"👥 *Управление доступом*",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("access")
|
||
)
|
||
|
||
# Обработка команд выполнения
|
||
elif callback.startswith("cmd_"):
|
||
# Поиск команды в меню
|
||
command = None
|
||
for menu_items in menu_builder._menus.values():
|
||
for item in menu_items:
|
||
if item.callback == callback and item.command:
|
||
command = item.command
|
||
break
|
||
|
||
if command:
|
||
await execute_cli_command(query, command)
|
||
else:
|
||
await query.edit_message_text("❌ Команда не найдена")
|
||
|
||
# Настройки бота - только просмотр, изменение через .env
|
||
elif callback == "set_name":
|
||
await query.edit_message_text(
|
||
"📝 *Изменение имени бота*\n\n"
|
||
f"Текущее имя: `{config.name}`\n\n"
|
||
"Для изменения отредактируйте `.env`:\n"
|
||
"```\nBOT_NAME=Ваше имя\n```",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("settings")
|
||
)
|
||
|
||
elif callback == "set_description":
|
||
await query.edit_message_text(
|
||
"📄 *Изменение описания бота*\n\n"
|
||
f"Текущее описание: `{config.description}`\n\n"
|
||
"Для изменения отредактируйте `.env`:\n"
|
||
"```\nBOT_DESCRIPTION=Ваше описание\n```",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("settings")
|
||
)
|
||
|
||
elif callback == "set_icon":
|
||
await query.edit_message_text(
|
||
"🎨 *Изменение иконки бота*\n\n"
|
||
f"Текущая иконка: `{config.icon}`\n\n"
|
||
"Для изменения отредактируйте `.env`:\n"
|
||
"```\nBOT_ICON_EMOJI=🤖\n```",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("settings")
|
||
)
|
||
|
||
elif callback == "show_access":
|
||
if config.allowed_users:
|
||
text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
|
||
else:
|
||
text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)"
|
||
await query.edit_message_text(text, parse_mode="Markdown")
|
||
|
||
elif callback == "add_access":
|
||
await query.edit_message_text(
|
||
"➕ *Добавление пользователя*\n\n"
|
||
"Для добавления пользователя отредактируйте `.env`:\n"
|
||
"```\nALLOWED_USERS=123456789,987654321\n```\n"
|
||
"Ваш ID можно узнать через @userinfobot",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
elif callback == "remove_access":
|
||
if config.allowed_users:
|
||
text = "➖ *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
|
||
text += "\n\nУдалите ID из `.env` чтобы убрать доступ"
|
||
else:
|
||
text = "➖ Список пуст, некого удалять"
|
||
await query.edit_message_text(text, parse_mode="Markdown")
|
||
|
||
elif callback == "about":
|
||
await query.edit_message_text(
|
||
f"ℹ️ *О боте*\n\n"
|
||
f"*{config.icon} {config.name}*\n"
|
||
f"_{config.description}_\n\n"
|
||
f"*Версия:* `2.1.0`\n\n"
|
||
f"*Возможности:*\n"
|
||
f"• Выполнение CLI команд через Telegram\n"
|
||
f"• Поддержка локальных команд и SSH\n"
|
||
f"• Интерактивный ввод пароля (sudo)\n"
|
||
f"• Предустановленные команды\n"
|
||
f"• Управление серверами\n"
|
||
f"• Очистка ANSI-кодов и прогресс-баров\n"
|
||
f"• Форматирование длинного вывода\n"
|
||
f"• 💬 Чат с ИИ агентом (Qwen Code)\n\n"
|
||
f"*Рабочая директория:*\n"
|
||
f"`{config.working_directory}`\n\n"
|
||
f"Бот позволяет безопасно выполнять команды\n"
|
||
f"на вашем сервере через интерфейс Telegram.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
|
||
)
|
||
state.current_menu = "main"
|
||
|
||
elif callback in ["toggle_ai_chat", "toggle_ai_chat_on", "toggle_ai_chat_off"]:
|
||
# Переключаем режим чата с ИИ
|
||
state.ai_chat_mode = not state.ai_chat_mode
|
||
logger.info(f"toggle_ai_chat: user_id={user_id}, new_mode={state.ai_chat_mode}")
|
||
|
||
ai_status = "✅ ВКЛЮЧЕН" if state.ai_chat_mode else "❌ ВЫКЛЮЧЕН"
|
||
action = "включён" if state.ai_chat_mode else "выключен"
|
||
|
||
await query.edit_message_text(
|
||
f"🏠 *Главное меню*\n\n"
|
||
f"💬 *Чат с ИИ:* {ai_status}\n\n"
|
||
f"Режим чата с агентом {action}.\n"
|
||
f"Теперь все сообщения будут отправляться в Qwen Code.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
|
||
)
|
||
state.current_menu = "main"
|
||
|
||
# --- Обработчики меню памяти ---
|
||
elif callback == "memory_menu":
|
||
state.current_menu = "memory"
|
||
await query.edit_message_text(
|
||
"🧠 *Память ИИ*\n\n"
|
||
"Управление памятью чата с ИИ:\n"
|
||
"• Профиль — факты о вас, которые запомнил ИИ\n"
|
||
"• Статистика — количество сообщений и сессий\n"
|
||
"• Очистить — удалить историю переписки",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("memory")
|
||
)
|
||
|
||
elif callback == "memory_profile":
|
||
profile_summary = get_user_profile_summary(user_id)
|
||
if not profile_summary:
|
||
profile_summary = "📭 Профиль пуст\n\nФакты ещё не извлечены.\nНачните общаться с ИИ в чате."
|
||
|
||
await query.edit_message_text(
|
||
f"📋 *Ваш профиль*\n\n{profile_summary}",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("memory")
|
||
)
|
||
|
||
elif callback == "memory_stats":
|
||
stats = memory_manager.get_stats(user_id)
|
||
await query.edit_message_text(
|
||
f"📊 *Статистика памяти*\n\n"
|
||
f"• Сессий: `{stats['total_sessions']}`\n"
|
||
f"• Сообщений: `{stats['total_messages']}`\n"
|
||
f"• Фактов: `{stats['total_facts']}`",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("memory")
|
||
)
|
||
|
||
elif callback == "memory_clear":
|
||
# Показываем подтверждение
|
||
await query.edit_message_text(
|
||
"🗑️ *Очистка истории*\n\n"
|
||
"Вы уверены?\n"
|
||
"Это удалит всю историю сообщений.\n"
|
||
"Факты останутся (их можно удалить отдельно).",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🗑️ Да, очистить", callback_data="memory_clear_confirm")],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="memory_menu")]
|
||
])
|
||
)
|
||
|
||
elif callback == "memory_clear_confirm":
|
||
# Очищаем историю сообщений (в будущем можно добавить метод в memory_manager)
|
||
from memory_system import MemoryStorage
|
||
# Пока просто уведомляем
|
||
await query.edit_message_text(
|
||
"✅ *История очищена*\n\n"
|
||
"Функция полной очистки будет добавлена в следующей версии.\n"
|
||
"Пока очищается только история сессии в памяти бота.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("memory")
|
||
)
|
||
# Сбрасываем историю чата в состоянии
|
||
state.ai_chat_history = []
|
||
|
||
|
||
async def execute_cli_command(query, command: str):
|
||
"""Выполнение CLI команды из кнопки меню."""
|
||
user_id = query.from_user.id
|
||
state = state_manager.get(user_id)
|
||
server_name = state.current_server
|
||
server = server_manager.get(server_name)
|
||
|
||
# Определяем рабочую директорию
|
||
working_dir = state.working_directory or config.working_directory
|
||
|
||
logger.info(f"Выполнение команды: {command} на сервере: {server_name}, в директории: {working_dir}")
|
||
|
||
# Если локальный сервер — выполняем локально
|
||
if server_name == "local" or server is None:
|
||
await _execute_local_command(query, command, working_dir)
|
||
else:
|
||
# Выполняем через SSH
|
||
await _execute_ssh_command(query, command, server, working_dir)
|
||
|
||
|
||
async def _execute_local_command(query, command: str, working_dir: str):
|
||
"""Выполнение локальной команды через PTY."""
|
||
user_id = query.from_user.id
|
||
|
||
try:
|
||
logger.info(f"Создание PTY для команды: {command}")
|
||
# Создаём PTY
|
||
master_fd, slave_fd = pty.openpty()
|
||
logger.info(f"PTY создан: master_fd={master_fd}")
|
||
|
||
# Запускаем процесс в PTY
|
||
pid = os.fork()
|
||
if pid == 0:
|
||
# Дочерний процесс
|
||
os.close(master_fd)
|
||
os.setsid()
|
||
os.dup2(slave_fd, 0) # stdin
|
||
os.dup2(slave_fd, 1) # stdout
|
||
os.dup2(slave_fd, 2) # stderr
|
||
os.close(slave_fd)
|
||
|
||
os.chdir(working_dir)
|
||
os.execvp("/bin/bash", ["/bin/bash", "-c", command])
|
||
else:
|
||
# Родительский процесс
|
||
os.close(slave_fd)
|
||
logger.info(f"Процесс запущен: pid={pid}")
|
||
|
||
# Создаём сессию
|
||
session = local_session_manager.create_session(
|
||
user_id=user_id,
|
||
command=command,
|
||
master_fd=master_fd,
|
||
pid=pid
|
||
)
|
||
|
||
# Читаем начальный вывод
|
||
logger.info("Чтение вывода из PTY...")
|
||
output, is_done = read_pty_output(master_fd, timeout=3.0)
|
||
logger.info(f"Прочитано: {len(output)} байт, is_done={is_done}")
|
||
logger.debug(f"Вывод: {output[:500] if output else '(пусто)'}")
|
||
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Проверяем тип ввода
|
||
input_type = detect_input_type(output)
|
||
logger.info(f"Тип ввода: {input_type}")
|
||
|
||
if input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await query.edit_message_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await query.edit_message_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif is_done:
|
||
local_session_manager.close_session(user_id)
|
||
await _show_result(query, command, output.encode(), b"", 0)
|
||
return
|
||
else:
|
||
# Команда ещё выполняется
|
||
await query.edit_message_text(
|
||
f"⏳ *Выполнение...*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"```\n{output.strip()[-500:] if output else 'Выполняется...'}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
while not is_done:
|
||
more_output, is_done = read_pty_output(master_fd, timeout=5.0)
|
||
output += more_output
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
input_type = detect_input_type(output)
|
||
if input_type in ("password", "confirm"):
|
||
session.waiting_for_input = True
|
||
session.input_type = input_type
|
||
await query.edit_message_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"{'🔐 *Запрошен пароль*' if input_type == 'password' else '❓ *Требуется подтверждение'}\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"{'Отправьте пароль в чат:' if input_type == 'password' else 'Отправьте `y` (да) или `n` (нет):'}",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
local_session_manager.close_session(user_id)
|
||
await _show_result(query, command, output.encode(), b"", 0)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка выполнения команды: {e}")
|
||
local_session_manager.close_session(user_id)
|
||
await query.edit_message_text(
|
||
f"❌ *Ошибка:*\n```\n{str(e)}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
async def _execute_ssh_command(query, command: str, server: Server, working_dir: str):
|
||
"""Выполнение команды через SSH с интерактивной сессией."""
|
||
user_id = query.from_user.id
|
||
|
||
try:
|
||
# Подготовка SSH ключа
|
||
client_keys = [server_manager.ssh_key_path] if server_manager.ssh_key_path else None
|
||
|
||
# Подготовка параметров подключения
|
||
connect_kwargs = {
|
||
"host": server.host,
|
||
"port": server.port,
|
||
"username": server.user,
|
||
"client_host_keys": None,
|
||
"known_hosts": None
|
||
}
|
||
|
||
# Добавляем ключ или пароль
|
||
if client_keys:
|
||
connect_kwargs["client_keys"] = client_keys
|
||
if server.password:
|
||
connect_kwargs["password"] = server.password
|
||
|
||
logger.info(f"SSH подключение к {server.host}:{server.port} как {server.user}")
|
||
|
||
# Подключение к серверу
|
||
conn = await asyncssh.connect(**connect_kwargs)
|
||
|
||
# Выполнение команды с cd в рабочую директорию
|
||
full_command = f"cd {working_dir} && {command}" if working_dir else command
|
||
|
||
# Создаем интерактивный процесс с PTY для поддержки ввода
|
||
# TERM环境变量设置 для корректной кодировки
|
||
process = await conn.create_process(
|
||
full_command,
|
||
term_type='xterm-256color',
|
||
env={'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'}
|
||
)
|
||
|
||
# Создаём сессию
|
||
session = ssh_session_manager.create_session(
|
||
user_id=user_id,
|
||
server=server,
|
||
working_dir=working_dir,
|
||
conn=conn,
|
||
process=process,
|
||
command=command
|
||
)
|
||
|
||
# Читаем начальный вывод
|
||
output, is_done = await read_ssh_output(process, timeout=3.0)
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Читаем пока процесс не завершится
|
||
while not is_done:
|
||
more_output, is_done = await read_ssh_output(process, timeout=2.0)
|
||
output += more_output
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Проверяем тип ввода
|
||
input_type = detect_input_type(output)
|
||
|
||
if input_type == "password":
|
||
# Запрос пароля
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await query.edit_message_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif input_type == "confirm":
|
||
# Запрос подтверждения
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await query.edit_message_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
else:
|
||
# Команда завершена, показываем результат
|
||
ssh_session_manager.close_session(user_id)
|
||
await _show_result(query, command, output.encode(), "", 0)
|
||
return
|
||
|
||
except asyncssh.Error as e:
|
||
logger.error(f"SSH ошибка: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await query.edit_message_text(
|
||
f"❌ *SSH ошибка:*\n```\n{str(e)}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
except asyncio.TimeoutError:
|
||
logger.error("Таймаут SSH подключения")
|
||
ssh_session_manager.close_session(user_id)
|
||
await query.edit_message_text(
|
||
"❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд и была прервана.",
|
||
parse_mode="Markdown"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка выполнения команды: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await query.edit_message_text(
|
||
f"❌ *Ошибка:*\n```\n{str(e)}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
async def _show_result(query, command: str, stdout: bytes, stderr: bytes, returncode: int):
|
||
"""Показ результата выполнения команды."""
|
||
output = clean_ansi_codes(stdout.decode("utf-8", errors="replace"))
|
||
output = normalize_output(output)
|
||
error = clean_ansi_codes(stderr.decode("utf-8", errors="replace"))
|
||
|
||
result = f"✅ *Результат:*\n\n"
|
||
|
||
if output:
|
||
# Форматируем длинный вывод
|
||
output = format_long_output(output)
|
||
result += f"```\n{output}\n```\n"
|
||
|
||
if error:
|
||
result += f"*Ошибки:*\n```\n{error}\n```\n"
|
||
|
||
result += f"\n*Код возврата:* `{returncode}`"
|
||
|
||
# Экранируем backticks
|
||
result = escape_markdown(result)
|
||
|
||
# Отправляем с разбивкой на части если нужно
|
||
await send_long_message(query, result, parse_mode="Markdown")
|
||
|
||
|
||
@check_access
|
||
async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка текстовых сообщений как CLI команд."""
|
||
user_id = update.effective_user.id
|
||
text = update.message.text.strip()
|
||
state = state_manager.get(user_id)
|
||
|
||
logger.info(f"handle_text_message: user_id={user_id}, ai_chat_mode={state.ai_chat_mode}, text={text[:50]}")
|
||
|
||
# Проверка: не в режиме ввода данных сервера ли мы
|
||
if state.waiting_for_input:
|
||
await handle_server_input(update, text)
|
||
return
|
||
|
||
# Проверка: не активная ли SSH-сессия ожидает ввода
|
||
ssh_session = ssh_session_manager.get_session(user_id)
|
||
if ssh_session and ssh_session.waiting_for_input:
|
||
await handle_ssh_session_input(update, text, ssh_session)
|
||
return
|
||
|
||
# Проверка: не активная ли локальная сессия ожидает ввода
|
||
local_session = local_session_manager.get_session(user_id)
|
||
if local_session and local_session.waiting_for_input:
|
||
await handle_local_session_input(update, text, local_session)
|
||
return
|
||
|
||
# ПРОВЕРКА: режим чата с ИИ агентом
|
||
if state.ai_chat_mode:
|
||
logger.info(f"Пользователь {user_id} отправил задачу ИИ: {text}")
|
||
await handle_ai_task(update, text)
|
||
return
|
||
|
||
# Любое текстовое сообщение = CLI команда
|
||
logger.info(f"Пользователь {user_id} отправил команду: {text}")
|
||
|
||
await execute_cli_command_from_message(update, text)
|
||
|
||
|
||
async def handle_ai_task(update: Update, text: str):
|
||
"""Обработка задачи для ИИ агента с использованием системы памяти."""
|
||
user_id = update.effective_user.id
|
||
state = state_manager.get(user_id)
|
||
|
||
# Сохраняем сообщение пользователя в памяти
|
||
save_message(user_id, "user", text)
|
||
|
||
# Добавляем сообщение пользователя в историю сессии
|
||
state.ai_chat_history.append(f"User: {text}")
|
||
|
||
# Ограничиваем историю последними 20 сообщениями
|
||
if len(state.ai_chat_history) > 20:
|
||
state.ai_chat_history = state.ai_chat_history[-20:]
|
||
|
||
# Отправляем статус
|
||
status_msg = await update.message.reply_text("⏳ 🤖 Думаю...")
|
||
|
||
output_buffer = []
|
||
|
||
def on_output(text: str):
|
||
output_buffer.append(text)
|
||
|
||
def on_oauth_url(url: str):
|
||
pass # OAuth обрабатывается автоматически
|
||
|
||
# Формируем контекст с историей + памятью
|
||
history_context = "\n".join(state.ai_chat_history)
|
||
|
||
# Получаем контекст из системы памяти (профиль + релевантные факты)
|
||
memory_context = get_context(user_id, query=text)
|
||
|
||
# Считаем токены в контексте (примерно: 1 слово ≈ 1.3 токена)
|
||
context_words = len((memory_context + "\n" + history_context).split())
|
||
context_tokens = int(context_words * 1.3)
|
||
|
||
# Максимальный контекст модели (Qwen поддерживает до 256K токенов)
|
||
# Для безопасности берём 200K
|
||
MAX_CONTEXT_TOKENS = 200_000
|
||
context_percent = round((context_tokens / MAX_CONTEXT_TOKENS) * 100, 1)
|
||
|
||
# Собираем полный промпт
|
||
full_task = (
|
||
f"{memory_context}\n\n"
|
||
f"Previous conversation:\n{history_context}\n\n"
|
||
f"Current request: {text}"
|
||
)
|
||
|
||
# Выполняем задачу
|
||
result = await qwen_manager.run_task(user_id, full_task, on_output, on_oauth_url)
|
||
|
||
# Показываем результат
|
||
full_output = "".join(output_buffer).strip()
|
||
|
||
if not full_output:
|
||
full_output = result
|
||
|
||
# Добавляем ответ ИИ в историю и память
|
||
if full_output:
|
||
state.ai_chat_history.append(f"Assistant: {full_output[:500]}")
|
||
save_message(user_id, "assistant", full_output)
|
||
|
||
# Обрезаем если слишком длинный (с запасом на контекст)
|
||
if len(full_output) > 3500:
|
||
full_output = full_output[:3500] + "\n... (вывод обрезан)"
|
||
|
||
# Автоматическое извлечение фактов каждые 5 сообщений
|
||
state.messages_since_fact_extract += 1
|
||
if state.messages_since_fact_extract >= 5:
|
||
logger.info(f"Запуск извлечения фактов через ИИ для пользователя {user_id}")
|
||
dialog_context = "\n".join(state.ai_chat_history[-10:]) # Последние 10 сообщений
|
||
asyncio.create_task(hybrid_memory_manager.extract_facts_with_ai(user_id, dialog_context))
|
||
state.messages_since_fact_extract = 0
|
||
|
||
# Формируем сообщение с информацией о контексте (как в qwen-code)
|
||
context_info = f"📊 Контекст: {context_percent}%"
|
||
response_text = f"{full_output}\n\n_{context_info}_"
|
||
|
||
# Отправляем ответ с разбивкой на части если нужно
|
||
await send_long_message(update, response_text, parse_mode="Markdown")
|
||
|
||
|
||
async def handle_ssh_session_input(update: Update, text: str, session: SSHSession):
|
||
"""Обработка ввода пользователя в активную SSH-сессию."""
|
||
user_id = update.effective_user.id
|
||
input_type = session.input_type
|
||
|
||
logger.info(f"Пользователь {user_id} ввёл '{text}' в SSH-сессию (тип: {input_type})")
|
||
|
||
try:
|
||
# Отправляем ввод в SSH-процесс
|
||
if input_type == "password":
|
||
# Пароль отправляем с newline
|
||
session.process.stdin.write(text + "\n")
|
||
elif input_type == "confirm":
|
||
# Подтверждение - y или n
|
||
answer = "y" if text.lower() in ("y", "yes", "да", "д") else "n"
|
||
session.process.stdin.write(answer + "\n")
|
||
else:
|
||
# Обычный ввод
|
||
session.process.stdin.write(text + "\n")
|
||
|
||
await session.process.stdin.drain()
|
||
session.last_activity = datetime.now()
|
||
|
||
# Читаем ответ
|
||
output, is_done = await read_ssh_output(session.process, timeout=3.0)
|
||
session.output_buffer += output
|
||
|
||
# Проверяем тип ввода
|
||
new_input_type = detect_input_type(output)
|
||
|
||
if new_input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:] if output else 'Ожидание...'}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif new_input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:] if output else 'Ожидание...'}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif is_done or new_input_type == "prompt":
|
||
# Команда завершена
|
||
await update.message.reply_text(
|
||
f"✅ *Результат:*\n\n"
|
||
f"```\n{session.command}\n```\n\n"
|
||
f"```\n{session.output_buffer.strip()[-4000:]}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
ssh_session_manager.close_session(user_id)
|
||
return
|
||
else:
|
||
# Команда ещё выполняется
|
||
await update.message.reply_text(
|
||
f"⏳ *Выполнение...*\n\n"
|
||
f"```\n{output.strip()[-500:] if output else 'Выполняется...'}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
# Читаем остаток
|
||
while not is_done:
|
||
more_output, is_done = await read_ssh_output(session.process, timeout=5.0)
|
||
output += more_output
|
||
session.output_buffer += output
|
||
session.last_activity = datetime.now()
|
||
|
||
new_input_type = detect_input_type(output)
|
||
if new_input_type in ("password", "confirm"):
|
||
session.waiting_for_input = True
|
||
session.input_type = new_input_type
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"{'🔐 *Запрошен пароль*' if new_input_type == 'password' else '❓ *Требуется подтверждение'}\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"{'Отправьте пароль в чат:' if new_input_type == 'password' else 'Отправьте `y` (да) или `n` (нет):'}",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Завершено
|
||
await update.message.reply_text(
|
||
f"✅ *Результат:*\n\n"
|
||
f"```\n{session.command}\n```\n\n"
|
||
f"```\n{session.output_buffer.strip()[-4000:]}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
ssh_session_manager.close_session(user_id)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка ввода в SSH-сессию: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(
|
||
f"❌ *Ошибка:*\n```\n{str(e)}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
async def handle_local_session_input(update: Update, text: str, session: LocalSession):
|
||
"""Обработка ввода пользователя в локальную сессию."""
|
||
user_id = update.effective_user.id
|
||
input_type = session.input_type
|
||
|
||
logger.info(f"Пользователь {user_id} ввёл '{text}' в локальную сессию (тип: {input_type})")
|
||
|
||
try:
|
||
child = session.context.get('child')
|
||
if not child:
|
||
raise Exception("Сессия не содержит child объект")
|
||
|
||
# Отправляем ввод
|
||
if input_type == "password":
|
||
child.sendline(text)
|
||
elif input_type == "confirm":
|
||
answer = "y" if text.lower() in ("y", "yes", "да", "д") else "n"
|
||
child.sendline(answer)
|
||
else:
|
||
child.sendline(text)
|
||
|
||
session.last_activity = datetime.now()
|
||
|
||
# Читаем ответ
|
||
logger.info("Чтение ответа...")
|
||
output = ""
|
||
|
||
try:
|
||
while True:
|
||
line = child.read_nonblocking(size=4096, timeout=5.0)
|
||
if not line:
|
||
break
|
||
output += line
|
||
logger.debug(f"Прочитано: {len(line)} символов")
|
||
|
||
# Проверяем запрос ввода
|
||
if detect_input_type(output):
|
||
break
|
||
|
||
except pexpect.TIMEOUT:
|
||
pass
|
||
except pexpect.EOF:
|
||
pass
|
||
|
||
logger.info(f"После ввода прочитано: {len(output)} символов")
|
||
session.output_buffer += output
|
||
|
||
# Проверяем тип ввода
|
||
new_input_type = detect_input_type(output)
|
||
|
||
if new_input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:] if output else 'Ожидание...'}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif new_input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:] if output else 'Ожидание...'}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
else:
|
||
# Команда завершена
|
||
# Очищаем ANSI-коды и нормализуем вывод
|
||
cleaned_output = clean_ansi_codes(session.output_buffer)
|
||
cleaned_output = normalize_output(cleaned_output)
|
||
|
||
# Форматируем длинный вывод: первые 5 и последние 10 строк
|
||
formatted_output = format_long_output(cleaned_output.strip(), max_lines=15, head_lines=5, tail_lines=10)
|
||
if len(formatted_output) > 4000:
|
||
formatted_output = formatted_output[:4000] + "\n... (вывод обрезан)"
|
||
|
||
await update.message.reply_text(
|
||
f"✅ *Результат:*\n\n"
|
||
f"```\n{session.command}\n```\n\n"
|
||
f"```\n{formatted_output}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
local_session_manager.close_session(user_id)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка ввода в локальную сессию: {e}")
|
||
local_session_manager.close_session(user_id)
|
||
await update.message.reply_text(
|
||
f"❌ *Ошибка:*\n```\n{str(e)}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
async def handle_server_input(update: Update, text: str):
|
||
"""Обработка ввода данных для CRUD операций с серверами."""
|
||
user_id = update.effective_user.id
|
||
state = state_manager.get(user_id)
|
||
input_type = state.input_type
|
||
|
||
if input_type == "add_server_name":
|
||
# Проверка имени
|
||
if not text.replace("-", "").replace("_", "").isalnum():
|
||
await update.message.reply_text(
|
||
"❌ Неверный формат имени.\n\n"
|
||
"Используйте только латиницу, дефисы и подчёркивания.\n"
|
||
"Пример: `web-prod`, `db_backup`",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
return
|
||
|
||
state.context["new_server"]["name"] = text
|
||
state.input_type = "add_server_host"
|
||
await update.message.reply_text(
|
||
f"✅ Имя: `{text}`\n\n"
|
||
"Введите *host* (IP или домен):\n"
|
||
"Пример: `192.168.1.10`, `example.com`",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
|
||
elif input_type == "add_server_host":
|
||
state.context["new_server"]["host"] = text
|
||
state.input_type = "add_server_port"
|
||
await update.message.reply_text(
|
||
f"✅ Host: `{text}`\n\n"
|
||
"Введите *SSH порт* (обычно 22):",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
|
||
elif input_type == "add_server_port":
|
||
try:
|
||
port = int(text)
|
||
if port < 1 or port > 65535:
|
||
raise ValueError()
|
||
state.context["new_server"]["port"] = port
|
||
state.input_type = "add_server_user"
|
||
await update.message.reply_text(
|
||
f"✅ Port: `{port}`\n\n"
|
||
"Введите *SSH пользователя*:\n"
|
||
"Пример: `root`, `admin`, `ubuntu`",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
except ValueError:
|
||
await update.message.reply_text(
|
||
"❌ Неверный формат порта.\n\n"
|
||
"Введите число от 1 до 65535:",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
|
||
elif input_type == "add_server_user":
|
||
state.context["new_server"]["user"] = text
|
||
state.input_type = "add_server_password"
|
||
await update.message.reply_text(
|
||
f"✅ User: `{text}`\n\n"
|
||
"Введите *SSH пароль* (или нажмите Пропустить для подключения только по ключу):\n"
|
||
"⚠️ Пароль будет сохранён в .env файл в открытом виде!",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("⏭️ Пропустить", callback_data="srv_skip_password")],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="server_menu")]
|
||
])
|
||
)
|
||
|
||
elif input_type == "add_server_password":
|
||
state.context["new_server"]["password"] = text
|
||
state.input_type = "add_server_tags"
|
||
await update.message.reply_text(
|
||
"✅ Пароль сохранён\n\n"
|
||
"Введите *теги* через запятую (или нажмите Пропустить):\n"
|
||
"Пример: `web,prod`, `db,backup`\n\n"
|
||
"Теги помогают группировать серверы.",
|
||
parse_mode="Markdown",
|
||
reply_markup=InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("⏭️ Пропустить", callback_data="srv_skip_tags")],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="server_menu")]
|
||
])
|
||
)
|
||
|
||
elif input_type == "add_server_tags":
|
||
# Обработка ввода тегов (если пользователь ввёл текстом, а не нажал кнопку)
|
||
tags = [t.strip() for t in text.split(",") if t.strip()]
|
||
state.context["new_server"]["tags"] = tags
|
||
|
||
# Завершение добавления
|
||
new_server = state.context.get("new_server", {})
|
||
if server_manager.add_server(
|
||
name=new_server["name"],
|
||
host=new_server["host"],
|
||
port=new_server["port"],
|
||
user=new_server["user"],
|
||
tags=tags,
|
||
password=new_server.get("password", "")
|
||
):
|
||
await update.message.reply_text(
|
||
"✅ *Сервер добавлен*\n\n"
|
||
f"Имя: `{new_server['name']}`\n"
|
||
f"Host: `{new_server['host']}`\n"
|
||
f"Port: `{new_server['port']}`\n"
|
||
f"User: `{new_server['user']}`\n"
|
||
f"Tags: `{','.join(tags)}`\n"
|
||
f"Password: {'установлен' if new_server.get('password') else 'не установлен'}\n\n"
|
||
f"Сервер сохранён в `.env` и доступен для выбора.",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
"❌ Ошибка: сервер с таким именем уже существует",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
state.waiting_for_input = False
|
||
state.input_type = None
|
||
state.context.clear()
|
||
|
||
elif input_type == "edit_server_field":
|
||
# Выбор поля для редактирования
|
||
if text == "1":
|
||
state.input_type = "edit_server_host"
|
||
await update.message.reply_text(
|
||
"Введите новый *host*:",
|
||
parse_mode="Markdown"
|
||
)
|
||
elif text == "2":
|
||
state.input_type = "edit_server_port"
|
||
await update.message.reply_text(
|
||
"Введите новый *port*:",
|
||
parse_mode="Markdown"
|
||
)
|
||
elif text == "3":
|
||
state.input_type = "edit_server_user"
|
||
await update.message.reply_text(
|
||
"Введите нового *user*:",
|
||
parse_mode="Markdown"
|
||
)
|
||
elif text == "4":
|
||
state.input_type = "edit_server_tags"
|
||
await update.message.reply_text(
|
||
"Введите новые *теги* через запятую:",
|
||
parse_mode="Markdown"
|
||
)
|
||
elif text == "5":
|
||
state.input_type = "edit_server_password"
|
||
await update.message.reply_text(
|
||
"Введите новый *password* (или оставьте пустым для подключения только по ключу):\n"
|
||
"⚠️ Пароль будет сохранён в .env файл в открытом виде!",
|
||
parse_mode="Markdown"
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
"❌ Введите номер поля (1-5):",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
|
||
]])
|
||
)
|
||
return
|
||
|
||
elif input_type == "edit_server_host":
|
||
server_manager.update_server(state.editing_server, host=text)
|
||
await finish_edit_server(update, state)
|
||
|
||
elif input_type == "edit_server_port":
|
||
try:
|
||
port = int(text)
|
||
server_manager.update_server(state.editing_server, port=port)
|
||
await finish_edit_server(update, state)
|
||
except ValueError:
|
||
await update.message.reply_text("❌ Неверный формат порта")
|
||
return
|
||
|
||
elif input_type == "edit_server_user":
|
||
server_manager.update_server(state.editing_server, user=text)
|
||
await finish_edit_server(update, state)
|
||
|
||
elif input_type == "edit_server_tags":
|
||
tags = [t.strip() for t in text.split(",") if t.strip()]
|
||
server_manager.update_server(state.editing_server, tags=tags)
|
||
await finish_edit_server(update, state)
|
||
|
||
elif input_type == "edit_server_password":
|
||
server_manager.update_server(state.editing_server, password=text)
|
||
await finish_edit_server(update, state)
|
||
|
||
else:
|
||
# Неизвестный тип ввода - выполняем команду
|
||
await execute_cli_command_from_message(update, text)
|
||
return
|
||
|
||
# Сброс состояния после завершения
|
||
if not state.waiting_for_input or input_type.startswith("add_server_tags"):
|
||
state.waiting_for_input = False
|
||
state.input_type = None
|
||
state.context.clear()
|
||
|
||
|
||
async def finish_edit_server(update: Update, state):
|
||
"""Завершение редактирования сервера."""
|
||
server_name = state.editing_server
|
||
state.waiting_for_input = False
|
||
state.input_type = None
|
||
state.editing_server = None
|
||
|
||
server = server_manager.get(server_name)
|
||
if server:
|
||
await update.message.reply_text(
|
||
"✅ *Сервер обновлён*\n\n"
|
||
f"{server.display_name}\n"
|
||
f"📍 `{server.description}`",
|
||
parse_mode="Markdown",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
"❌ Ошибка при обновлении сервера",
|
||
reply_markup=menu_builder.get_keyboard("server")
|
||
)
|
||
|
||
|
||
async def execute_cli_command_from_message(update: Update, command: str):
|
||
"""Выполнение CLI команды из сообщения."""
|
||
user_id = update.effective_user.id
|
||
state = state_manager.get(user_id)
|
||
server_name = state.current_server
|
||
server = server_manager.get(server_name)
|
||
|
||
# Определяем рабочую директорию
|
||
working_dir = state.working_directory or config.working_directory
|
||
|
||
# Обработка команды cd - меняем директорию пользователя
|
||
# Работает только с простыми командами cd, не с составными
|
||
cmd_stripped = command.strip()
|
||
if cmd_stripped.startswith("cd ") and "&&" not in cmd_stripped and ";" not in cmd_stripped and "|" not in cmd_stripped:
|
||
parts = cmd_stripped.split(maxsplit=1)
|
||
if len(parts) == 2:
|
||
target_dir = parts[1]
|
||
|
||
# Обработка ~ и относительных путей
|
||
if target_dir.startswith("~"):
|
||
target_dir = str(Path.home()) + target_dir[1:]
|
||
elif not target_dir.startswith("/"):
|
||
target_dir = str(Path(working_dir) / target_dir)
|
||
|
||
# Проверка существования директории
|
||
if Path(target_dir).is_dir():
|
||
state.working_directory = target_dir
|
||
await update.message.reply_text(
|
||
f"📁 *Директория изменена:*\n`{target_dir}`\n"
|
||
f"🖥️ Сервер: `{server_name}`",
|
||
parse_mode="Markdown"
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
f"❌ *Директория не найдена:*\n`{target_dir}`",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Для составных команд с cd — выполняем через SSH или локально
|
||
if "cd " in cmd_stripped and ("&&" in cmd_stripped or ";" in cmd_stripped):
|
||
if server_name == "local" or server is None:
|
||
await _execute_composite_command_local(update, cmd_stripped, working_dir)
|
||
else:
|
||
await _execute_composite_command_ssh(update, cmd_stripped, server, working_dir)
|
||
return
|
||
|
||
# Обычное выполнение
|
||
if server_name == "local" or server is None:
|
||
await _execute_local_command_message(update, cmd_stripped, working_dir)
|
||
else:
|
||
await _execute_ssh_command_message(update, cmd_stripped, server, working_dir)
|
||
|
||
|
||
async def _execute_composite_command_local(update: Update, command: str, working_dir: str):
|
||
"""Выполнение составной команды локально."""
|
||
command_with_pwd = f"{command} && pwd"
|
||
logger.info(f"Выполнение составной команды с cd: {command_with_pwd} в директории: {working_dir}")
|
||
|
||
try:
|
||
process = await asyncio.create_subprocess_shell(
|
||
command_with_pwd,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
cwd=working_dir
|
||
)
|
||
|
||
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30)
|
||
output = stdout.decode("utf-8", errors="replace").strip()
|
||
error = stderr.decode("utf-8", errors="replace")
|
||
|
||
# Последняя строка - это pwd
|
||
if output and process.returncode == 0:
|
||
lines = output.split('\n')
|
||
final_dir = lines[-1].strip()
|
||
if Path(final_dir).is_dir():
|
||
state_manager.get(update.effective_user.id).working_directory = final_dir
|
||
output = '\n'.join(lines[:-1])
|
||
|
||
await _show_result_message(update, command, output, error, process.returncode)
|
||
|
||
except asyncio.TimeoutError:
|
||
await update.message.reply_text("❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд.", parse_mode="Markdown")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка: {e}")
|
||
await update.message.reply_text(f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
|
||
|
||
async def _execute_composite_command_ssh(update: Update, command: str, server: Server, working_dir: str):
|
||
"""Выполнение составной команды через SSH с интерактивной сессией."""
|
||
user_id = update.effective_user.id
|
||
command_with_pwd = f"{command} && pwd"
|
||
|
||
try:
|
||
client_keys = [server_manager.ssh_key_path] if server_manager.ssh_key_path else None
|
||
|
||
# Подготовка параметров подключения
|
||
connect_kwargs = {
|
||
"host": server.host,
|
||
"port": server.port,
|
||
"username": server.user,
|
||
"client_host_keys": None,
|
||
"known_hosts": None
|
||
}
|
||
|
||
if client_keys:
|
||
connect_kwargs["client_keys"] = client_keys
|
||
if server.password:
|
||
connect_kwargs["password"] = server.password
|
||
|
||
logger.info(f"SSH подключение к {server.host}:{server.port} как {server.user}")
|
||
|
||
conn = await asyncssh.connect(**connect_kwargs)
|
||
|
||
# Выполнение команды с cd в рабочую директорию
|
||
full_command = f"cd {working_dir} && {command_with_pwd}" if working_dir else command_with_pwd
|
||
|
||
# Создаем интерактивный процесс с PTY для поддержки ввода
|
||
# TERM环境变量设置 для корректной кодировки
|
||
process = await conn.create_process(
|
||
full_command,
|
||
term_type='xterm-256color',
|
||
env={'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'}
|
||
)
|
||
|
||
# Создаём сессию
|
||
session = ssh_session_manager.create_session(
|
||
user_id=user_id,
|
||
server=server,
|
||
working_dir=working_dir,
|
||
conn=conn,
|
||
process=process,
|
||
command=command
|
||
)
|
||
|
||
# Читаем начальный вывод
|
||
output, is_done = await read_ssh_output(process, timeout=3.0)
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Читаем пока процесс не завершится
|
||
while not is_done:
|
||
more_output, is_done = await read_ssh_output(process, timeout=2.0)
|
||
output += more_output
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Проверяем тип ввода
|
||
input_type = detect_input_type(output)
|
||
|
||
if input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
else:
|
||
# Обработка pwd для смены директории
|
||
if output:
|
||
lines = output.strip().split('\n')
|
||
final_dir = lines[-1].strip()
|
||
if final_dir.startswith('/'):
|
||
state_manager.get(user_id).working_directory = final_dir
|
||
output = '\n'.join(lines[:-1])
|
||
|
||
ssh_session_manager.close_session(user_id)
|
||
await _show_result_message(update, command, output, "", 0)
|
||
return
|
||
|
||
except asyncssh.Error as e:
|
||
logger.error(f"SSH ошибка: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(f"❌ *SSH ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
except asyncio.TimeoutError:
|
||
logger.error("Таймаут SSH подключения")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text("❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд.", parse_mode="Markdown")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
|
||
|
||
async def _execute_local_command_message(update: Update, command: str, working_dir: str):
|
||
"""Выполнение локальной команды из сообщения через pexpect."""
|
||
user_id = update.effective_user.id
|
||
|
||
try:
|
||
logger.info(f"Запуск команды через pexpect: {command}")
|
||
|
||
# Создаём интерактивный процесс
|
||
child = pexpect.spawn(
|
||
'/bin/bash',
|
||
['-c', command],
|
||
cwd=working_dir,
|
||
encoding='utf-8',
|
||
codec_errors='replace',
|
||
echo=False,
|
||
timeout=30
|
||
)
|
||
|
||
# Создаём сессию (используем child вместо master_fd)
|
||
session = local_session_manager.create_session(
|
||
user_id=user_id,
|
||
command=command,
|
||
master_fd=child.child_fd,
|
||
pid=child.pid
|
||
)
|
||
session.context = {'child': child} # Сохраняем child объект
|
||
|
||
# Читаем начальный вывод
|
||
logger.info("Чтение вывода...")
|
||
output = ""
|
||
|
||
try:
|
||
# Пробуем прочитать с таймаутом
|
||
while True:
|
||
line = child.read_nonblocking(size=4096, timeout=2.0)
|
||
if not line:
|
||
break
|
||
output += line
|
||
logger.debug(f"Прочитано: {len(line)} символов")
|
||
|
||
# Проверяем запрос ввода
|
||
if detect_input_type(output):
|
||
break
|
||
|
||
except pexpect.TIMEOUT:
|
||
pass
|
||
except pexpect.EOF:
|
||
pass
|
||
|
||
logger.info(f"Прочитано: {len(output)} символов")
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Проверяем тип ввода
|
||
input_type = detect_input_type(output)
|
||
logger.info(f"Тип ввода: {input_type}")
|
||
|
||
if input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
else:
|
||
# Команда завершена
|
||
local_session_manager.close_session(user_id)
|
||
await _show_result_message(update, command, output, "", 0)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка выполнения команды: {e}")
|
||
local_session_manager.close_session(user_id)
|
||
await update.message.reply_text(f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
|
||
|
||
async def _execute_ssh_command_message(update: Update, command: str, server: Server, working_dir: str):
|
||
"""Выполнение команды через SSH из сообщения с интерактивной сессией."""
|
||
user_id = update.effective_user.id
|
||
|
||
try:
|
||
client_keys = [server_manager.ssh_key_path] if server_manager.ssh_key_path else None
|
||
|
||
# Подготовка параметров подключения
|
||
connect_kwargs = {
|
||
"host": server.host,
|
||
"port": server.port,
|
||
"username": server.user,
|
||
"client_host_keys": None,
|
||
"known_hosts": None
|
||
}
|
||
|
||
if client_keys:
|
||
connect_kwargs["client_keys"] = client_keys
|
||
if server.password:
|
||
connect_kwargs["password"] = server.password
|
||
|
||
logger.info(f"SSH подключение к {server.host}:{server.port} как {server.user}")
|
||
|
||
conn = await asyncssh.connect(**connect_kwargs)
|
||
|
||
# Выполнение команды с cd в рабочую директорию
|
||
full_command = f"cd {working_dir} && {command}" if working_dir else command
|
||
|
||
# Создаем интерактивный процесс с PTY для поддержки ввода
|
||
# TERM环境变量设置 для корректной кодировки
|
||
process = await conn.create_process(
|
||
full_command,
|
||
term_type='xterm-256color',
|
||
env={'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'}
|
||
)
|
||
|
||
# Создаём сессию
|
||
session = ssh_session_manager.create_session(
|
||
user_id=user_id,
|
||
server=server,
|
||
working_dir=working_dir,
|
||
conn=conn,
|
||
process=process,
|
||
command=command
|
||
)
|
||
|
||
# Читаем начальный вывод
|
||
output, is_done = await read_ssh_output(process, timeout=3.0)
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Читаем пока процесс не завершится
|
||
while not is_done:
|
||
more_output, is_done = await read_ssh_output(process, timeout=2.0)
|
||
output += more_output
|
||
session.output_buffer = output
|
||
session.last_activity = datetime.now()
|
||
|
||
# Проверяем тип ввода
|
||
input_type = detect_input_type(output)
|
||
|
||
if input_type == "password":
|
||
session.waiting_for_input = True
|
||
session.input_type = "password"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"🔐 *Запрошен пароль*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте пароль в чат:",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
elif input_type == "confirm":
|
||
session.waiting_for_input = True
|
||
session.input_type = "confirm"
|
||
await update.message.reply_text(
|
||
f"⏳ *Требуется ввод*\n\n"
|
||
f"Команда: `{command}`\n\n"
|
||
f"❓ *Требуется подтверждение*\n\n"
|
||
f"```\n{output.strip()[-200:]}\n```\n\n"
|
||
f"Отправьте `y` (да) или `n` (нет):",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
else:
|
||
# Команда завершена, показываем результат
|
||
ssh_session_manager.close_session(user_id)
|
||
await _show_result_message(update, command, output, "", 0)
|
||
return
|
||
|
||
except asyncssh.Error as e:
|
||
logger.error(f"SSH ошибка: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(f"❌ *SSH ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
except asyncio.TimeoutError:
|
||
logger.error("Таймаут SSH подключения")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text("❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд.", parse_mode="Markdown")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка: {e}")
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown")
|
||
|
||
|
||
async def _show_result_message(update: Update, command: str, output: str, error: str, returncode: int):
|
||
"""Показ результата выполнения команды."""
|
||
# Очистка ANSI-кодов и нормализация
|
||
output = normalize_output(clean_ansi_codes(output)) if output else ""
|
||
error = clean_ansi_codes(error) if error else ""
|
||
|
||
result = f"✅ *Результат:*\n\n"
|
||
|
||
if output:
|
||
# Форматируем длинный вывод: первые 5 и последние 10 строк
|
||
output = format_long_output(output, max_lines=15, head_lines=5, tail_lines=10)
|
||
result += f"```\n{output}\n```\n"
|
||
|
||
if error:
|
||
result += f"*Ошибки:*\n```\n{error}\n```\n"
|
||
|
||
result += f"\n*Код возврата:* `{returncode}`"
|
||
|
||
# Экранируем backticks и отправляем с разбивкой
|
||
result = escape_markdown(result)
|
||
await send_long_message(update, result, parse_mode="Markdown")
|
||
|
||
|
||
async def post_init(application: Application):
|
||
"""Инициализация после запуска бота."""
|
||
# Установка команд бота
|
||
commands = [
|
||
BotCommand("start", "Запустить бота"),
|
||
BotCommand("menu", "Главное меню с кнопками"),
|
||
BotCommand("help", "Справка"),
|
||
BotCommand("settings", "Настройки"),
|
||
BotCommand("stop", "Прервать SSH-сессию"),
|
||
BotCommand("ai", "Задача для Qwen Code AI"),
|
||
BotCommand("memory", "Статистика памяти ИИ"),
|
||
BotCommand("facts", "Показать сохранённые факты"),
|
||
BotCommand("forget", "Удалить факт по номеру"),
|
||
]
|
||
await application.bot.set_my_commands(commands)
|
||
|
||
logger.info("Бот инициализирован")
|
||
|
||
|
||
@check_access
|
||
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /stop - прерывание активной SSH-сессии."""
|
||
user_id = update.effective_user.id
|
||
|
||
session = ssh_session_manager.get_session(user_id)
|
||
if session:
|
||
ssh_session_manager.close_session(user_id)
|
||
await update.message.reply_text(
|
||
"❌ *SSH-сессия прервана*\n\n"
|
||
f"Команда `{session.command}` была остановлена.",
|
||
parse_mode="Markdown"
|
||
)
|
||
else:
|
||
await update.message.reply_text(
|
||
"ℹ️ *Нет активных SSH-сессий*\n\n"
|
||
"У вас нет выполняющихся команд.",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
|
||
|
||
# ============================================
|
||
# КОМАНДЫ ДЛЯ РАБОТЫ С QWEN CODE (ИИ)
|
||
# ============================================
|
||
|
||
@check_access
|
||
async def ai_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /ai - выполнение задачи через Qwen Code."""
|
||
user_id = update.effective_user.id
|
||
task = " ".join(context.args).strip()
|
||
|
||
if not task:
|
||
await update.message.reply_text(
|
||
"🤖 *Qwen Code AI*\n\n"
|
||
"Использование:\n"
|
||
"`/ai <задача>`\n\n"
|
||
"Примеры:\n"
|
||
"`/ai создай функцию Python для сортировки списка`\n"
|
||
"`/ai объясни код в файле main.py`\n"
|
||
"`/ai найди баги в этом коде`\n\n"
|
||
"Команды:\n"
|
||
"`/ai status` — статус сессии\n"
|
||
"`/ai stop` — завершить сессию\n"
|
||
"`/ai clear` — очистить историю диалога",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Специальные команды
|
||
if task == "status":
|
||
session = qwen_manager.get_session(user_id)
|
||
if session:
|
||
await update.message.reply_text(
|
||
f"🤖 *Статус сессии Qwen Code*\n\n"
|
||
f"Состояние: `{session.state.value}`\n"
|
||
f"Последняя активность: {session.last_activity.strftime('%H:%M:%S')}\n"
|
||
f"Задача: `{session.pending_task or 'Нет'}`",
|
||
parse_mode="Markdown"
|
||
)
|
||
else:
|
||
await update.message.reply_text("ℹ️ Активных сессий нет")
|
||
return
|
||
|
||
if task == "stop":
|
||
qwen_manager.close_session(user_id)
|
||
await update.message.reply_text("✅ Сессия Qwen Code завершена")
|
||
return
|
||
|
||
if task == "clear":
|
||
state = state_manager.get(user_id)
|
||
state.ai_chat_history.clear()
|
||
await update.message.reply_text("✅ История диалога с ИИ очищена")
|
||
return
|
||
|
||
# Отправляем задачу в ИИ
|
||
status_msg = await update.message.reply_text("⏳ 🤖 Думаю...", parse_mode="Markdown")
|
||
|
||
output_buffer = []
|
||
|
||
def on_output(text: str):
|
||
output_buffer.append(text)
|
||
|
||
def on_oauth_url(url: str):
|
||
pass # OAuth обрабатывается автоматически при первом запуске
|
||
|
||
# Выполняем задачу
|
||
result = await qwen_manager.run_task(user_id, task, on_output, on_oauth_url)
|
||
|
||
# Показываем результат
|
||
full_output = "".join(output_buffer).strip()
|
||
|
||
if not full_output:
|
||
full_output = result
|
||
|
||
if len(full_output) > 4000:
|
||
full_output = full_output[:4000] + "\n... (вывод обрезан)"
|
||
|
||
await status_msg.edit_text(
|
||
f"🤖 *Результат:*\n\n"
|
||
f"```\n{full_output}\n```",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
@check_access
|
||
async def memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /memory — статистика памяти ИИ."""
|
||
user_id = update.effective_user.id
|
||
|
||
stats = get_memory_stats(user_id)
|
||
|
||
if not stats:
|
||
await update.message.reply_text("ℹ️ Память не инициализирована")
|
||
return
|
||
|
||
# Форматируем статистику
|
||
total_messages = stats.get("total_messages", 0)
|
||
total_facts = stats.get("total_facts", 0)
|
||
total_sessions = stats.get("total_sessions", 0)
|
||
vector_docs = stats.get("vector_documents", "N/A")
|
||
vector_model = stats.get("vector_model", "N/A")
|
||
hybrid_mode = stats.get("hybrid_mode", False)
|
||
|
||
text = (
|
||
"🧠 *Статистика памяти:*\n\n"
|
||
f"📊 Сообщений: `{total_messages}`\n"
|
||
f"📌 Фактов: `{total_facts}`\n"
|
||
f"📁 Сессий: `{total_sessions}`\n"
|
||
)
|
||
|
||
if hybrid_mode:
|
||
text += (
|
||
f"\n🔮 *Векторная память:*\n"
|
||
f" Документы: `{vector_docs}`\n"
|
||
f" Модель: `{vector_model}`\n"
|
||
)
|
||
|
||
text += "\n_Память использует SQLite + ChromaDB с семантическим поиском._"
|
||
|
||
await update.message.reply_text(text, parse_mode="Markdown")
|
||
|
||
|
||
@check_access
|
||
async def facts_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /facts — показать сохранённые факты."""
|
||
user_id = update.effective_user.id
|
||
|
||
# Получаем факты из SQLite
|
||
from memory_system import memory_manager
|
||
facts = memory_manager.storage.get_facts(user_id)
|
||
|
||
if not facts:
|
||
await update.message.reply_text(
|
||
"📋 *Ваши факты*\n\n"
|
||
"Пока нет сохранённых фактов.\n"
|
||
"Общайтесь с ИИ в чате — он автоматически запомнит важное!",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Группируем по типам
|
||
from memory_system import FactType
|
||
grouped = {}
|
||
for fact in facts:
|
||
type_name = fact.fact_type.value
|
||
if type_name not in grouped:
|
||
grouped[type_name] = []
|
||
grouped[type_name].append(fact)
|
||
|
||
# Формируем сообщение
|
||
type_names_ru = {
|
||
"personal": "👤 Личное",
|
||
"technical": "💻 Технологии",
|
||
"project": "📁 Проекты",
|
||
"preference": "⭐ Предпочтения",
|
||
"other": "📌 Другое"
|
||
}
|
||
|
||
text = "📋 *Ваши сохранённые факты:*\n\n"
|
||
|
||
for type_name, type_facts in grouped.items():
|
||
type_title = type_names_ru.get(type_name, type_name)
|
||
text += f"*{type_title}* ({len(type_facts)}):\n"
|
||
|
||
for i, fact in enumerate(type_facts, 1):
|
||
# Обрезаем длинные факты
|
||
content = fact.content
|
||
if len(content) > 100:
|
||
content = content[:100] + "..."
|
||
text += f" {i}. {content}\n"
|
||
|
||
text += "\n"
|
||
|
||
text += f"_Всего: {len(facts)} фактов_\n"
|
||
text += "_Для удаления факта используйте `/forget <номер>`_"
|
||
|
||
await update.message.reply_text(text, parse_mode="Markdown")
|
||
|
||
|
||
@check_access
|
||
async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Обработка команды /forget — удалить факт."""
|
||
user_id = update.effective_user.id
|
||
|
||
if not context.args or not context.args[0].isdigit():
|
||
await update.message.reply_text(
|
||
"❌ *Использование:*\n"
|
||
"`/forget <номер>`\n\n"
|
||
"Сначала вызовите `/facts` чтобы увидеть список.",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Получаем факты
|
||
from memory_system import memory_manager
|
||
facts = memory_manager.storage.get_facts(user_id)
|
||
|
||
fact_index = int(context.args[0]) - 1
|
||
|
||
if fact_index < 0 or fact_index >= len(facts):
|
||
await update.message.reply_text(
|
||
f"❌ Факт с номером {fact_index + 1} не найден.\n"
|
||
f"Всего фактов: {len(facts)}",
|
||
parse_mode="Markdown"
|
||
)
|
||
return
|
||
|
||
# Удаляем факт
|
||
fact_to_delete = facts[fact_index]
|
||
memory_manager.storage.update_fact(fact_to_delete.id, is_active=False)
|
||
|
||
await update.message.reply_text(
|
||
f"✅ Факт удалён:\n_{fact_to_delete.content}_",
|
||
parse_mode="Markdown"
|
||
)
|
||
|
||
|
||
def main():
|
||
"""Точка входа."""
|
||
# Чтение токена только из переменной окружения
|
||
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||
|
||
if not token:
|
||
print("❌ Ошибка: не установлен TELEGRAM_BOT_TOKEN")
|
||
print("\nСпособы установки токена:")
|
||
print(" 1. Создайте файл .env по примеру .env.example")
|
||
print(" 2. Или задайте переменную окружения:")
|
||
print(" export TELEGRAM_BOT_TOKEN='your_token_here'")
|
||
print("\nИли запустите ./run.sh для интерактивной настройки")
|
||
sys.exit(1)
|
||
|
||
# Проверка настроек прокси
|
||
use_proxy = os.getenv("USE_PROXY", "false").lower() == "true"
|
||
proxy_url = None
|
||
|
||
if use_proxy:
|
||
proxy_host = os.getenv("PROXY_HOST", "127.0.0.1")
|
||
proxy_port = os.getenv("PROXY_PORT", "1080")
|
||
proxy_username = os.getenv("PROXY_USERNAME", "")
|
||
proxy_password = os.getenv("PROXY_PASSWORD", "")
|
||
|
||
# Формируем URL прокси: socks5://user:pass@host:port
|
||
if proxy_username and proxy_password:
|
||
proxy_url = f"socks5://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
|
||
else:
|
||
proxy_url = f"socks5://{proxy_host}:{proxy_port}"
|
||
|
||
print(f"✅ Прокси включён: {proxy_url.split('@')[0]}@{proxy_host}:{proxy_port}")
|
||
|
||
# Загрузка серверов из env
|
||
server_manager.load_from_env()
|
||
|
||
# Инициализация меню
|
||
init_menus()
|
||
|
||
# Создание приложения с таймаутами и прокси
|
||
builder = (
|
||
Application.builder()
|
||
.token(token)
|
||
.post_init(post_init)
|
||
.read_timeout(30)
|
||
.write_timeout(30)
|
||
.connect_timeout(30)
|
||
.pool_timeout(30)
|
||
)
|
||
|
||
# Добавляем прокси если включён
|
||
if use_proxy and proxy_url:
|
||
builder = builder.proxy_url(proxy_url)
|
||
logger.info(f"Используется SOCKS5 прокси: {proxy_host}:{proxy_port}")
|
||
|
||
application = builder.build()
|
||
|
||
# Регистрация хендлеров
|
||
application.add_handler(CommandHandler("start", start_command))
|
||
application.add_handler(CommandHandler("help", help_command))
|
||
application.add_handler(CommandHandler("settings", settings_command))
|
||
application.add_handler(CommandHandler("menu", menu_command))
|
||
application.add_handler(CommandHandler("stop", stop_command))
|
||
application.add_handler(CommandHandler("memory", memory_command))
|
||
application.add_handler(CommandHandler("facts", facts_command))
|
||
application.add_handler(CommandHandler("forget", forget_command))
|
||
application.add_handler(CallbackQueryHandler(menu_callback))
|
||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message))
|
||
application.add_handler(CommandHandler("ai", ai_command))
|
||
|
||
# Запуск
|
||
logger.info("Запуск бота...")
|
||
print(f"🤖 {config.name} запущен!")
|
||
print(f"📝 Описание: {config.description}")
|
||
print(f"🎨 Иконка: {config.icon}")
|
||
print("\nОстановка: Ctrl+C")
|
||
|
||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|