Add: команда /xray для добавления пользователей XRay, уменьшено логирование
- Добавлена команда /xray для управления пользователями XRay через SSH - SSH подключение к серверу mt.mirv.top с выполнением скрипта add_xray_user.sh - Генерация и отправка QR-кода для подключения к VPN - Интеграция с приложением Hiddify - Добавлены зависимости asyncssh и qrcode[pil] - Уменьшен уровень логирования (только WARNING и ERROR) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
1c904320dd
commit
5779dd7b14
|
|
@ -32,3 +32,9 @@ YANDEX_FOLDER_ID=
|
|||
|
||||
# Database
|
||||
DATABASE_URL=sqlite+aiosqlite:///./valera.db
|
||||
|
||||
# XRay (mt.mirv.top)
|
||||
XRAY_SSH_HOST=mt.mirv.top
|
||||
XRAY_SSH_USER=root
|
||||
XRAY_SSH_PASSWORD=
|
||||
XRAY_ADD_USER_SCRIPT=/root/bin/add_xray_user.sh
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ class Settings(BaseSettings):
|
|||
|
||||
database_url: str = "sqlite+aiosqlite:///./valera.db"
|
||||
|
||||
# XRay settings
|
||||
xray_ssh_host: str = "mt.mirv.top"
|
||||
xray_ssh_user: str = "root"
|
||||
xray_ssh_password: str = ""
|
||||
xray_add_user_script: str = "/root/bin/add_xray_user.sh"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "allow"
|
||||
|
|
|
|||
|
|
@ -10,3 +10,5 @@ vosk>=0.3.45
|
|||
faster-whisper>=0.10.0
|
||||
gigachat>=0.2.0
|
||||
requests>=2.31.0
|
||||
asyncssh>=2.14.0
|
||||
qrcode[pil]>=7.4.0
|
||||
|
|
|
|||
282
src/bot/main.py
282
src/bot/main.py
|
|
@ -3,7 +3,8 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from telegram import Update, BotCommand
|
||||
import io
|
||||
from telegram import Update, BotCommand, InputFile
|
||||
from telegram.ext import (
|
||||
Application, CommandHandler, MessageHandler, filters,
|
||||
ContextTypes, CallbackQueryHandler
|
||||
|
|
@ -11,15 +12,19 @@ from telegram.ext import (
|
|||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from config.config import get_settings
|
||||
from src.tools.orchestrator import Orchestrator
|
||||
from src.bot.states import chat_state, ChatMode
|
||||
from src.tools.xray import get_xray_client
|
||||
from src.bot.states import chat_state, ChatMode, XRayState
|
||||
from src.bot.config_manager import get_selected_tool, get_selected_model, set_tool
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
level=logging.INFO
|
||||
level=logging.WARNING
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Отключаем подробное логирование asyncssh чтобы не засорять логи
|
||||
logging.getLogger("asyncssh").setLevel(logging.ERROR)
|
||||
|
||||
settings = get_settings()
|
||||
orchestrator = Orchestrator()
|
||||
|
||||
|
|
@ -28,12 +33,48 @@ DANGEROUS_PATTERNS = [
|
|||
r'\bсоздать\b', r'\bзаписать\b', r'\bудалить\b', r'\bизменить\b',
|
||||
r'\.write\(', r'\.save\(', r'\brm\b', r'\bmkdir\b', r'\bcp\b',
|
||||
r'\bсделать\b', r'\bвыполнить\b', r'\brun\b', r'\bзапустить\b',
|
||||
r'\bпоказать\b', r'\bпосмотреть\b', r'\bпокажи\b', r'\bглянь\b',
|
||||
r'sudo', r'pip install', r'apt install', r'yum install',
|
||||
r'chmod', r'chown', r'systemctl', r'service ',
|
||||
r'curl.*\|', r'wget.*\|', r'bash.*-c',
|
||||
r'exec', r'eval', r'shell', r'terminal',
|
||||
r'\bgit\b', r'\bгит\b', r'\bnpm\b', r'\byarn\b', r'\bgo run\b', r'\bpip\b',
|
||||
r'\bstatus\b', r'\bstat\b', r'\bстатус\b',
|
||||
]
|
||||
|
||||
COMMAND_PATTERNS = [
|
||||
r'^ls\b', r'^dir\b', r'^cd\b', r'^pwd\b',
|
||||
r'^cat\b', r'^grep\b', r'^find\b', r'^touch\b',
|
||||
r'^mkdir\b', r'^rm\b', r'^cp\b', r'^mv\b',
|
||||
r'^chmod\b', r'^chown\b',
|
||||
r'^git\b',
|
||||
r'list files', r'show files', r'покажи файлы',
|
||||
r'выполни команду', r'run command', r'запусти команду',
|
||||
]
|
||||
|
||||
def is_simple_command_prompt(prompt: str) -> bool:
|
||||
"""Check if prompt is a simple system command that should be auto-executed"""
|
||||
prompt_stripped = prompt.strip().lower()
|
||||
simple_commands = [
|
||||
'ls', 'dir', 'pwd', 'whoami', 'date', 'uname',
|
||||
'git status', 'git log', 'git diff', 'git branch',
|
||||
'df -h', 'free -h', 'top', 'ps aux'
|
||||
]
|
||||
|
||||
for cmd in simple_commands:
|
||||
if prompt_stripped == cmd or prompt_stripped.startswith(cmd + ' '):
|
||||
return True
|
||||
|
||||
# Check against patterns
|
||||
for pattern in COMMAND_PATTERNS:
|
||||
if re.search(pattern, prompt_stripped, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_dangerous(prompt: str) -> bool:
|
||||
prompt_lower = prompt.lower()
|
||||
for pattern in DANGEROUS_PATTERNS:
|
||||
for pattern in DANGEROUS_PATTERNS + COMMAND_PATTERNS:
|
||||
if re.search(pattern, prompt_lower, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
|
@ -42,7 +83,7 @@ def is_dangerous(prompt: str) -> bool:
|
|||
async def get_opencode_models():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["opencode", "models"],
|
||||
["/home/mirivlad/.opencode/bin/opencode", "models"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
|
|
@ -82,7 +123,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
"/qwen - Использовать Qwen (все запросы через Qwen)\n"
|
||||
"/open - Выбрать модель OpenCode\n"
|
||||
"/mode confirm/auto - Режим подтверждения\n"
|
||||
"/forget - Очистить историю\n\n"
|
||||
"/forget - Очистить историю\n"
|
||||
"/xray [email] - Добавить пользователя XRay\n\n"
|
||||
f"🔧 Текущая модель: {current_tool}"
|
||||
)
|
||||
if model:
|
||||
|
|
@ -116,32 +158,57 @@ async def confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
data = query.data
|
||||
|
||||
if data.startswith("model_"):
|
||||
model = data.replace("model_", "")
|
||||
model_name = data.replace("model_", "")
|
||||
if not model_name.startswith("opencode/"):
|
||||
model = "opencode/" + model_name
|
||||
else:
|
||||
model = model_name
|
||||
set_tool("opencode", model)
|
||||
await query.edit_message_text(f"✅ Выбрана модель OpenCode: {model}\nВсе запросы будут идти через эту модель.")
|
||||
await query.edit_message_text(f"✅ Выбрана модель: {model}\nВсе запросы будут идти через эту модель.")
|
||||
return
|
||||
|
||||
if data == "confirm_yes":
|
||||
await query.edit_message_text("✅ Подтверждено. Выполняю...")
|
||||
pending = chat_state.get_pending_action(chat_id)
|
||||
if pending:
|
||||
if not pending:
|
||||
await query.edit_message_text("❌ Ошибка: действие не найдено")
|
||||
return
|
||||
|
||||
await query.edit_message_text("✅ Подтверждено. Выполняю...")
|
||||
prompt = pending.get("prompt")
|
||||
tool = pending.get("tool")
|
||||
dangerous = pending.get("dangerous", False)
|
||||
chat_state.set_waiting_confirmation(chat_id, False)
|
||||
await execute_tool_query(update, tool, prompt)
|
||||
|
||||
model_raw = get_selected_model()
|
||||
model = None
|
||||
if tool == "opencode" and model_raw:
|
||||
if model_raw.startswith("opencode/"):
|
||||
model = model_raw.replace("opencode/", "", 1)
|
||||
else:
|
||||
model = model_raw
|
||||
|
||||
thinking_msg = await query.message.reply_text("🤔 Думаю...")
|
||||
|
||||
simple_prompt = prompt
|
||||
result, success = await orchestrator.ask(simple_prompt, chat_id, tool, model, yolo=True)
|
||||
|
||||
text = result[:4096] if len(result) > 4096 else result
|
||||
await query.message.reply_text(text, parse_mode="Markdown")
|
||||
try:
|
||||
await thinking_msg.delete()
|
||||
except:
|
||||
pass
|
||||
elif data == "confirm_no":
|
||||
chat_state.set_waiting_confirmation(chat_id, False)
|
||||
await query.edit_message_text("❌ Отменено.")
|
||||
await query.edit_message_text("❌ Отменено. Команда не будет выполнена.")
|
||||
|
||||
|
||||
async def execute_tool_query(update, tool: str, prompt: str):
|
||||
chat_id = update.message.chat.id if hasattr(update, 'message') else update.effective_chat.id
|
||||
|
||||
await update.message.reply_text("🤔 Думаю...")
|
||||
|
||||
model = get_selected_model() if tool == "opencode" else None
|
||||
|
||||
result, success = await orchestrator.ask(prompt, chat_id, tool, model)
|
||||
result, success = await orchestrator.ask(prompt, chat_id, tool, model, yolo=False)
|
||||
|
||||
text = result[:4096] if len(result) > 4096 else result
|
||||
|
||||
|
|
@ -163,7 +230,18 @@ async def qwen_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
return
|
||||
|
||||
prompt = " ".join(context.args)
|
||||
await execute_tool_query(update, "qwen", prompt)
|
||||
chat_id = update.effective_chat.id
|
||||
mode = chat_state.get_mode(chat_id)
|
||||
use_yolo = mode == ChatMode.AUTO
|
||||
|
||||
thinking_msg = await update.message.reply_text("🤔 Думаю..." + (" (YOLO)" if use_yolo else ""))
|
||||
result, success = await orchestrator.ask(prompt, chat_id, "qwen", None, yolo=use_yolo)
|
||||
text = result[:4096] if len(result) > 4096 else result
|
||||
await update.message.reply_text(text)
|
||||
try:
|
||||
await thinking_msg.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
async def open_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
|
@ -193,13 +271,24 @@ async def open_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
model = get_selected_model()
|
||||
|
||||
if tool != "opencode":
|
||||
set_tool("opencode", "minimax-m2.5-free")
|
||||
model = "minimax-m2.5-free"
|
||||
set_tool("opencode", "opencode/minimax-m2.5-free")
|
||||
model = "opencode/minimax-m2.5-free"
|
||||
|
||||
if not model:
|
||||
model = "minimax-m2.5-free"
|
||||
model = "opencode/minimax-m2.5-free"
|
||||
|
||||
await execute_tool_query(update, "opencode", prompt)
|
||||
chat_id = update.effective_chat.id
|
||||
mode = chat_state.get_mode(chat_id)
|
||||
use_yolo = mode == ChatMode.AUTO
|
||||
|
||||
thinking_msg = await update.message.reply_text("🤔 Думаю..." + (" (YOLO)" if use_yolo else ""))
|
||||
result, success = await orchestrator.ask(prompt, chat_id, "opencode", model, yolo=use_yolo)
|
||||
text = result[:4096] if len(result) > 4096 else result
|
||||
await update.message.reply_text(text)
|
||||
try:
|
||||
await thinking_msg.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
|
@ -208,37 +297,137 @@ async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
await update.message.reply_text("🗑️ История чата очищена.")
|
||||
|
||||
|
||||
async def xray_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Команда /xray - добавление пользователя XRay
|
||||
0. Ожидает ввод email
|
||||
1. Подключается по SSH к серверу
|
||||
2. Запускает скрипт add_xray_user.sh
|
||||
3. Возвращает результат и QR-код
|
||||
"""
|
||||
chat_id = update.effective_chat.id
|
||||
|
||||
# Проверяем есть ли аргументы команды
|
||||
if context.args:
|
||||
# Если email передан сразу командой /xray email@example.com
|
||||
email = context.args[0]
|
||||
await process_xray_email(update, context, email)
|
||||
return
|
||||
|
||||
# Иначе запрашиваем email
|
||||
chat_state.set_xray_state(chat_id, XRayState.WAITING_EMAIL)
|
||||
await update.message.reply_text(
|
||||
"🔐 *XRay - добавление пользователя*\n\n"
|
||||
"Введите email для нового пользователя:"
|
||||
)
|
||||
|
||||
|
||||
async def process_xray_email(update: Update, context: ContextTypes.DEFAULT_TYPE, email: str):
|
||||
"""Обработка email и выполнение скрипта на сервере"""
|
||||
chat_id = update.effective_chat.id
|
||||
|
||||
# Простая валидация email
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_pattern, email):
|
||||
await update.message.reply_text(
|
||||
"❌ Неверный формат email. Попробуйте еще раз:"
|
||||
)
|
||||
return
|
||||
|
||||
# Очищаем состояние
|
||||
chat_state.set_xray_state(chat_id, None)
|
||||
|
||||
progress_msg = await update.message.reply_text(
|
||||
f"⏳ Подключаюсь к серверу и добавляю пользователя {email}..."
|
||||
)
|
||||
|
||||
try:
|
||||
xray_client = get_xray_client()
|
||||
|
||||
# Подключение
|
||||
connected = await xray_client.connect()
|
||||
if not connected:
|
||||
await progress_msg.edit_text(
|
||||
"❌ Ошибка подключения к серверу. Проверьте настройки SSH."
|
||||
)
|
||||
return
|
||||
|
||||
# Добавление пользователя
|
||||
success, output, qr_png = await xray_client.add_user(email)
|
||||
|
||||
# Отключение
|
||||
await xray_client.disconnect()
|
||||
|
||||
if not success:
|
||||
await progress_msg.edit_text(f"❌ Ошибка:\n{output}")
|
||||
return
|
||||
|
||||
# Извлекаем vless ссылку из вывода
|
||||
vless_pattern = r'(vless://[^\s]+)'
|
||||
vless_match = re.search(vless_pattern, output)
|
||||
vless_link = vless_match.group(1) if vless_match else None
|
||||
|
||||
# Отправляем результат
|
||||
await progress_msg.delete()
|
||||
|
||||
result_text = (
|
||||
f"Привет! Вот данные для добавления VPN:\n\n"
|
||||
f"1️⃣ Скачать приложение Hiddify тут:\n"
|
||||
f"https://github.com/hiddify/hiddify-app/releases\n"
|
||||
f"или в PlayMarket\n\n"
|
||||
f"2️⃣ Добавить профиль в программе по ссылке:\n"
|
||||
f"`{vless_link}`\n\n"
|
||||
f"либо отсканируй QR-код прямо в программе"
|
||||
)
|
||||
|
||||
await update.message.reply_text(result_text, parse_mode="Markdown")
|
||||
|
||||
# Отправляем QR-код если есть
|
||||
if qr_png:
|
||||
await update.message.reply_photo(
|
||||
photo=InputFile(io.BytesIO(qr_png), filename="xray_qr.png"),
|
||||
caption=f"QR-код для {email}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении пользователя XRay: {e}")
|
||||
await progress_msg.edit_text(f"❌ Произошла ошибка: {e}")
|
||||
|
||||
|
||||
async def handle_xray_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обработчик ввода email для команды /xray"""
|
||||
chat_id = update.effective_chat.id
|
||||
state = chat_state.get_xray_state(chat_id)
|
||||
|
||||
if state != XRayState.WAITING_EMAIL:
|
||||
return # Не в состоянии ожидания email
|
||||
|
||||
email = update.message.text.strip()
|
||||
await process_xray_email(update, context, email)
|
||||
|
||||
|
||||
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
prompt = update.message.text
|
||||
chat_id = update.effective_chat.id
|
||||
mode = chat_state.get_mode(chat_id)
|
||||
tool = get_selected_tool()
|
||||
|
||||
dangerous = is_dangerous(prompt)
|
||||
|
||||
if mode == ChatMode.CONFIRM and dangerous:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ Да", callback_data="confirm_yes"),
|
||||
InlineKeyboardButton("❌ Нет", callback_data="confirm_no")
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
chat_state.set_waiting_confirmation(chat_id, True, {
|
||||
"type": "tool",
|
||||
"tool": tool,
|
||||
"prompt": prompt
|
||||
})
|
||||
|
||||
await update.message.reply_text(
|
||||
f"⚠️ Это действие может внести изменения. Выполнить?\n\n{prompt[:200]}...",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
else:
|
||||
await update.message.reply_text("🤔 Думаю...")
|
||||
model = get_selected_model() if tool == "opencode" else None
|
||||
await execute_tool_query(update, tool, prompt)
|
||||
|
||||
# Сначала проверяем не ждем ли мы email для XRay
|
||||
if chat_state.get_xray_state(chat_id) == XRayState.WAITING_EMAIL:
|
||||
await handle_xray_email(update, context)
|
||||
return
|
||||
|
||||
thinking_msg = await update.message.reply_text("🤔 Думаю...")
|
||||
|
||||
result, success = await orchestrator.ask(prompt, chat_id, tool, model, yolo=True)
|
||||
|
||||
text = result.strip() if result else ""
|
||||
if not text:
|
||||
text = "⚠️ Пустой ответ от модели."
|
||||
|
||||
text = text[:4096]
|
||||
await thinking_msg.edit_text(text, parse_mode="Markdown")
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -247,7 +436,6 @@ def main():
|
|||
|
||||
if settings.telegram_proxy_url:
|
||||
builder = builder.proxy(settings.telegram_proxy_url)
|
||||
logger.info(f"Используется прокси: {settings.telegram_proxy_url}")
|
||||
|
||||
application = builder.build()
|
||||
|
||||
|
|
@ -257,10 +445,10 @@ def main():
|
|||
application.add_handler(CommandHandler("qwen", qwen_command))
|
||||
application.add_handler(CommandHandler("open", open_command))
|
||||
application.add_handler(CommandHandler("forget", forget_command))
|
||||
application.add_handler(CommandHandler("xray", xray_command))
|
||||
application.add_handler(CallbackQueryHandler(confirm_callback))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||
|
||||
logger.info("Бот запущен")
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ class ChatMode(str, Enum):
|
|||
AUTO = "auto"
|
||||
|
||||
|
||||
class XRayState(str, Enum):
|
||||
"""Состояния для команды /xray"""
|
||||
WAITING_EMAIL = "waiting_email"
|
||||
|
||||
|
||||
class ChatState:
|
||||
def __init__(self):
|
||||
self.states: Dict[int, dict] = {}
|
||||
|
|
@ -21,7 +26,6 @@ class ChatState:
|
|||
if chat_id not in self.states:
|
||||
self.states[chat_id] = {}
|
||||
self.states[chat_id]["mode"] = mode
|
||||
logger.info(f"Чат {chat_id} переключён в режим {mode}")
|
||||
|
||||
def is_waiting_confirmation(self, chat_id: int) -> bool:
|
||||
return self.states.get(chat_id, {}).get("waiting_confirmation", False)
|
||||
|
|
@ -46,6 +50,18 @@ class ChatState:
|
|||
def get_current_task(self, chat_id: int) -> Optional[str]:
|
||||
return self.states.get(chat_id, {}).get("current_task")
|
||||
|
||||
# Методы для состояния XRay
|
||||
def set_xray_state(self, chat_id: int, state: Optional[XRayState]):
|
||||
if chat_id not in self.states:
|
||||
self.states[chat_id] = {}
|
||||
if state:
|
||||
self.states[chat_id]["xray_state"] = state
|
||||
else:
|
||||
self.states[chat_id].pop("xray_state", None)
|
||||
|
||||
def get_xray_state(self, chat_id: int) -> Optional[XRayState]:
|
||||
return self.states.get(chat_id, {}).get("xray_state")
|
||||
|
||||
def clear(self, chat_id: int):
|
||||
self.states.pop(chat_id, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,14 +25,11 @@ class Memory:
|
|||
)
|
||||
try:
|
||||
if os.path.exists(MODEL_PATH):
|
||||
logger.info(f"Загрузка модели из локальной папки: {MODEL_PATH}")
|
||||
self.embedding_model = SentenceTransformer(MODEL_PATH)
|
||||
else:
|
||||
logger.info("Загрузка модели с HuggingFace (локальная версия не найдена)")
|
||||
os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
|
||||
self.embedding_model = SentenceTransformer(MODEL_NAME)
|
||||
self.embedding_model.save(MODEL_PATH)
|
||||
logger.info(f"Модель сохранена в: {MODEL_PATH}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось загрузить модель эмбеддингов: {e}")
|
||||
self.embedding_model = None
|
||||
|
|
@ -47,7 +44,6 @@ class Memory:
|
|||
ids=[doc_id],
|
||||
metadatas=[{"chat_id": str(chat_id), "role": role}]
|
||||
)
|
||||
logger.info(f"Добавлено сообщение в чат {chat_id}: {role}")
|
||||
|
||||
def get_recent_messages(self, chat_id: int, limit: int = None) -> List[Dict]:
|
||||
if limit is None:
|
||||
|
|
@ -99,7 +95,6 @@ class Memory:
|
|||
|
||||
if results and results.get("ids"):
|
||||
self.collection.delete(ids=results["ids"])
|
||||
logger.info(f"Очищена история чата {chat_id}")
|
||||
|
||||
def get_context_for_prompt(self, chat_id: int) -> str:
|
||||
messages = self.get_recent_messages(chat_id)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ class SchedulerManager:
|
|||
self.reminders: Dict[int, List[dict]] = {}
|
||||
|
||||
async def generate_idea(self):
|
||||
logger.info("Запуск генерации идей")
|
||||
|
||||
for chat_id in self.orchestrator.memory.collection.get().get("metadatas", []):
|
||||
try:
|
||||
chat_id_int = int(chat_id.get("chat_id", 0))
|
||||
|
|
@ -57,11 +55,9 @@ class SchedulerManager:
|
|||
replace_existing=True
|
||||
)
|
||||
self.scheduler.start()
|
||||
logger.info("Планировщик запущен")
|
||||
|
||||
def stop(self):
|
||||
self.scheduler.shutdown()
|
||||
logger.info("Планировщик остановлен")
|
||||
|
||||
def add_reminder(self, chat_id: int, text: str, run_at: datetime):
|
||||
if chat_id not in self.reminders:
|
||||
|
|
@ -80,8 +76,6 @@ class SchedulerManager:
|
|||
"run_at": run_at
|
||||
})
|
||||
|
||||
logger.info(f"Напоминание добавлено для чата {chat_id}: {text} в {run_at}")
|
||||
|
||||
async def _send_reminder(self, chat_id: int, text: str):
|
||||
try:
|
||||
await self.bot.send_message(chat_id=chat_id, text=f"🔔 Напоминание:\n\n{text}")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class SpeechRecognizer:
|
|||
|
||||
def load_model(self):
|
||||
if not self.enabled:
|
||||
logger.info("Распознавание речи отключено")
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -33,13 +32,11 @@ class SpeechRecognizer:
|
|||
return
|
||||
|
||||
self.model = Model(model_path)
|
||||
logger.info("Модель Vosk загружена")
|
||||
|
||||
elif self.model_name == "whisper":
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
self.model = WhisperModel("small", device="cpu", compute_type="int8")
|
||||
logger.info("Модель Whisper загружена")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки модели распознавания: {e}")
|
||||
|
|
@ -112,7 +109,6 @@ class SpeechRecognizer:
|
|||
|
||||
def toggle(self, enabled: bool):
|
||||
self.enabled = enabled
|
||||
logger.info(f"Распознавание речи: {'включено' if enabled else 'отключено'}")
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return self.enabled
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@ from src.memory.memory import Memory
|
|||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
|
||||
SYSTEM_PROMPT = """Ты Валера - дружелюбный, умный и полезный программист-ассистент.
|
||||
Ты помогаешь пользователям с программированием, отвечаешь на вопросы, объясняешь код и помогаешь решать задачи.
|
||||
Будь кратким, но информативным. Используй кодовые блоки для примеров."""
|
||||
SYSTEM_PROMPT = """Ты Валера - дружелюбный Telegram-бот ассистент.
|
||||
|
||||
ВАЖНО: Ты имеешь долгосрочную память! Ты помнишь ВСЕ предыдущие разговоры с этим пользователем из базы данных ChromaDB.
|
||||
Когда пользователь спрашивает о твоей памяти - скажи что ты помнишь все ваши предыдущие разговоры.
|
||||
|
||||
Ты помогаешь с программированием, отвечаешь на вопросы.
|
||||
Будь кратким, используй кодовые блоки.
|
||||
Твое имя - Валера."""
|
||||
|
||||
|
||||
class Orchestrator:
|
||||
|
|
@ -39,17 +44,30 @@ class Orchestrator:
|
|||
context = self.memory.get_context_for_prompt(chat_id)
|
||||
|
||||
full_prompt = f"{SYSTEM_PROMPT}\n\n"
|
||||
|
||||
similar = self.memory.search_similar(chat_id, user_prompt, limit=3)
|
||||
if similar:
|
||||
full_prompt += f"Похожие предыдущие разговоры:\n"
|
||||
for sim in similar:
|
||||
full_prompt += f"- {sim}\n"
|
||||
full_prompt += "\n"
|
||||
|
||||
if context:
|
||||
full_prompt += f"История разговора:\n{context}\n\n"
|
||||
full_prompt += f"История текущего разговора:\n{context}\n\n"
|
||||
|
||||
full_prompt += f"Вопрос пользователя: {user_prompt}"
|
||||
|
||||
return full_prompt
|
||||
|
||||
async def ask(self, prompt: str, chat_id: int, tool: Optional[str] = None, model: Optional[str] = None) -> Tuple[str, bool]:
|
||||
async def ask(self, prompt: str, chat_id: int, tool: Optional[str] = None, model: Optional[str] = None, yolo: bool = False) -> Tuple[str, bool]:
|
||||
selected_tool = tool or self.default_tool
|
||||
|
||||
if selected_tool == "opencode" and model:
|
||||
selected_tool = f"opencode:{model}"
|
||||
if not model.startswith("opencode/"):
|
||||
model_id = f"opencode/{model}"
|
||||
else:
|
||||
model_id = model
|
||||
selected_tool = f"opencode:{model_id}"
|
||||
|
||||
full_prompt = self._build_prompt(prompt, chat_id)
|
||||
|
||||
|
|
@ -58,22 +76,31 @@ class Orchestrator:
|
|||
elif selected_tool == "yandex":
|
||||
result, success = await self.yandex.ask(full_prompt)
|
||||
else:
|
||||
result, success = await self.tool_runner.run_tool(selected_tool, full_prompt, model)
|
||||
if selected_tool == "qwen":
|
||||
result, success = await self.tool_runner.run_qwen(full_prompt, yolo)
|
||||
else:
|
||||
result, success = await self.tool_runner.run_tool(selected_tool, full_prompt, model, yolo)
|
||||
|
||||
tool_key = selected_tool.split(":")[0]
|
||||
|
||||
if not success and self._check_rate_limit_error(result):
|
||||
logger.warning(f"Лимит превышен для {selected_tool}, пробую другой инструмент")
|
||||
|
||||
self.tool_limits[selected_tool]["failed"] += 1
|
||||
if tool_key in self.tool_limits:
|
||||
self.tool_limits[tool_key]["failed"] += 1
|
||||
|
||||
if self.tool_limits[selected_tool]["failed"] >= self.tool_limits[selected_tool]["max_failures"]:
|
||||
if self.tool_limits[tool_key]["failed"] >= self.tool_limits[tool_key]["max_failures"]:
|
||||
alt_tool = "opencode" if selected_tool == "qwen" else "qwen"
|
||||
logger.info(f"Переключаюсь на {alt_tool}")
|
||||
|
||||
result, success = await self.tool_runner.run_tool(alt_tool, full_prompt)
|
||||
if alt_tool == "qwen":
|
||||
result, success = await self.tool_runner.run_qwen(full_prompt, yolo)
|
||||
else:
|
||||
result, success = await self.tool_runner.run_tool(alt_tool, full_prompt, model, yolo)
|
||||
selected_tool = alt_tool
|
||||
|
||||
if success:
|
||||
self.tool_limits[selected_tool]["failed"] = 0
|
||||
tool_key = selected_tool.split(":")[0]
|
||||
if success and tool_key in self.tool_limits:
|
||||
self.tool_limits[tool_key]["failed"] = 0
|
||||
|
||||
self.memory.add_message(chat_id, "user", prompt)
|
||||
self.memory.add_message(chat_id, "assistant", result)
|
||||
|
|
@ -83,7 +110,6 @@ class Orchestrator:
|
|||
def set_default_tool(self, tool: str):
|
||||
if tool in ["qwen", "opencode", "gigachat", "yandex"]:
|
||||
self.default_tool = tool
|
||||
logger.info(f"Инструмент по умолчанию изменён на {tool}")
|
||||
|
||||
def get_default_tool(self) -> str:
|
||||
return self.default_tool
|
||||
|
|
|
|||
|
|
@ -13,25 +13,35 @@ class ToolRunner:
|
|||
self.opencode_command = settings.opencode_command
|
||||
self.timeout = settings.tool_timeout
|
||||
|
||||
async def run_qwen(self, prompt: str) -> Tuple[str, bool]:
|
||||
return await self._run_tool(self.qwen_command, prompt)
|
||||
async def run_qwen(self, prompt: str, yolo: bool = False, cwd: str = None) -> Tuple[str, bool]:
|
||||
import os
|
||||
if cwd is None:
|
||||
cwd = os.path.expanduser("~")
|
||||
args = ["run", "--chat-recording=false", prompt]
|
||||
if yolo:
|
||||
args.append("--yolo")
|
||||
return await self._run_tool(self.qwen_command, args, cwd=cwd)
|
||||
|
||||
async def run_opencode(self, prompt: str, model: Optional[str] = None) -> Tuple[str, bool]:
|
||||
cmd = self.opencode_command
|
||||
if model:
|
||||
cmd = f"{self.opencode_command}:{model}"
|
||||
return await self._run_tool(cmd, prompt)
|
||||
|
||||
async def _run_tool(self, command: str, prompt: str) -> Tuple[str, bool]:
|
||||
cmd_parts = command.split(":")
|
||||
actual_cmd = cmd_parts[0]
|
||||
if not model.startswith("opencode/"):
|
||||
model_id = f"opencode/{model}"
|
||||
else:
|
||||
model_id = model
|
||||
args = ["run", "--model", model_id, prompt]
|
||||
else:
|
||||
args = ["run", prompt]
|
||||
return await self._run_tool(cmd, args)
|
||||
|
||||
async def _run_tool(self, command: str, args: list, cwd: str = None) -> Tuple[str, bool]:
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
actual_cmd,
|
||||
prompt,
|
||||
command,
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=cwd
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -59,9 +69,9 @@ class ToolRunner:
|
|||
logger.exception("Ошибка при выполнении инструмента")
|
||||
return f"Ошибка: {str(e)}", False
|
||||
|
||||
async def run_tool(self, tool_name: str, prompt: str, model: Optional[str] = None) -> Tuple[str, bool]:
|
||||
async def run_tool(self, tool_name: str, prompt: str, model: Optional[str] = None, yolo: bool = False) -> Tuple[str, bool]:
|
||||
if tool_name == "qwen":
|
||||
return await self.run_qwen(prompt)
|
||||
return await self.run_qwen(prompt, yolo)
|
||||
elif tool_name == "opencode":
|
||||
return await self.run_opencode(prompt, model)
|
||||
elif tool_name.startswith("opencode:"):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
import asyncio
|
||||
import re
|
||||
import io
|
||||
import base64
|
||||
from typing import Optional, Tuple
|
||||
import logging
|
||||
|
||||
import asyncssh
|
||||
from PIL import Image
|
||||
import qrcode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XRaySSHClient:
|
||||
"""Клиент для подключения к XRay серверу через SSH"""
|
||||
|
||||
def __init__(self, host: str, user: str, password: str, script_path: str):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.script_path = script_path
|
||||
self.conn: Optional[asyncssh.SSHClientConnection] = None
|
||||
|
||||
async def connect(self):
|
||||
"""Подключение к SSH серверу"""
|
||||
try:
|
||||
self.conn = await asyncssh.connect(
|
||||
host=self.host,
|
||||
username=self.user,
|
||||
password=self.password,
|
||||
known_hosts=None, # Отключаем проверку known_hosts для простоты
|
||||
connect_timeout=15,
|
||||
)
|
||||
return True
|
||||
except asyncssh.PermissionDenied as e:
|
||||
logger.error(f"SSH: доступ запрещён - {e}")
|
||||
return False
|
||||
except asyncssh.ConnectionLost as e:
|
||||
logger.error(f"SSH: соединение потеряно - {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка подключения к SSH: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Отключение от SSH сервера"""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
await self.conn.wait_closed()
|
||||
|
||||
async def add_user(self, email: str) -> Tuple[bool, str, Optional[bytes]]:
|
||||
"""
|
||||
Добавление пользователя через скрипт add_xray_user.sh
|
||||
|
||||
Returns:
|
||||
Tuple[success, output_text, qr_code_png]
|
||||
"""
|
||||
if not self.conn:
|
||||
return False, "Нет подключения к серверу", None
|
||||
|
||||
try:
|
||||
# Скрипт читает email из stdin через read -p
|
||||
# Поэтому передаем его через stdin
|
||||
result = await self.conn.run(
|
||||
self.script_path,
|
||||
input=email + "\n",
|
||||
timeout=120
|
||||
)
|
||||
|
||||
output = result.stdout
|
||||
stderr = result.stderr
|
||||
|
||||
full_output = output
|
||||
if stderr:
|
||||
full_output += "\n" + stderr
|
||||
|
||||
if result.exit_status != 0:
|
||||
return False, f"Ошибка скрипта (код {result.exit_status}):\n{full_output}", None
|
||||
|
||||
# Парсим QR-код из вывода
|
||||
qr_png = await self._extract_qr_code(full_output)
|
||||
|
||||
return True, full_output, qr_png
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Таймаут выполнения команды (120 сек)")
|
||||
return False, "Таймаут выполнения команды (превышено 120 сек)", None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выполнения команды: {e}")
|
||||
return False, f"Ошибка: {e}", None
|
||||
|
||||
async def _extract_qr_code(self, output: str) -> Optional[bytes]:
|
||||
"""
|
||||
Извлечение QR-кода из вывода скрипта и конвертация в PNG
|
||||
|
||||
Скрипт может выводить QR-код в разных форматах:
|
||||
1. ASCII art в консоли
|
||||
2. Base64 закодированное изображение
|
||||
3. Ссылка на подписку (vless://...)
|
||||
|
||||
Возвращает PNG изображение или None
|
||||
"""
|
||||
# Пытаемся найти vless:// ссылку или другую subscription ссылку
|
||||
vless_pattern = r'(vless://[^\s]+)'
|
||||
trojan_pattern = r'(trojan://[^\s]+)'
|
||||
vmess_pattern = r'(vmess://[^\s]+)'
|
||||
|
||||
subscription_url = None
|
||||
|
||||
for pattern in [vless_pattern, trojan_pattern, vmess_pattern]:
|
||||
match = re.search(pattern, output)
|
||||
if match:
|
||||
subscription_url = match.group(1)
|
||||
break
|
||||
|
||||
if subscription_url:
|
||||
# Генерируем QR-код из ссылки
|
||||
try:
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=10,
|
||||
border=2,
|
||||
)
|
||||
qr.add_data(subscription_url)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
# Конвертируем в bytes
|
||||
img_bytes = io.BytesIO()
|
||||
img.save(img_bytes, format='PNG')
|
||||
img_bytes.seek(0)
|
||||
|
||||
return img_bytes.getvalue()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка генерации QR-кода: {e}")
|
||||
|
||||
# Если не нашли ссылку, пробуем найти base64 изображение
|
||||
base64_pattern = r'(?:data:image/png;base64,)?([A-Za-z0-9+/]{100,}={0,2})'
|
||||
base64_match = re.search(base64_pattern, output)
|
||||
|
||||
if base64_match:
|
||||
try:
|
||||
base64_data = base64_match.group(1)
|
||||
png_data = base64.b64decode(base64_data)
|
||||
|
||||
# Проверяем что это валидное PNG
|
||||
img = Image.open(io.BytesIO(png_data))
|
||||
img.verify()
|
||||
|
||||
return png_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка декодирования base64 QR-кода: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_xray_client() -> XRaySSHClient:
|
||||
"""Создание клиента из настроек окружения"""
|
||||
from config.config import get_settings
|
||||
settings = get_settings()
|
||||
|
||||
return XRaySSHClient(
|
||||
host=settings.xray_ssh_host,
|
||||
user=settings.xray_ssh_user,
|
||||
password=settings.xray_ssh_password,
|
||||
script_path=settings.xray_add_user_script,
|
||||
)
|
||||
Loading…
Reference in New Issue