feat: интеграция с Qwen Code AI
Новые возможности: - Команда /ai для выполнения задач через Qwen Code - Автоматический запуск сессии при первой задаче - Обработка OAuth авторизации (ссылка отправляется в чат) - Команды /ai status и /ai stop для управления сессией - Таймаут неактивности 30 минут - Буферизация вывода Файлы: - qwen_integration.py — менеджер сессий Qwen Code - bot.py — команда /ai и хендлеры Пример использования: /ai создай функцию Python для сортировки списка /ai status /ai stop Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
df8786bee2
commit
5d451ff870
89
bot.py
89
bot.py
|
|
@ -24,6 +24,7 @@ from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pexpect
|
import pexpect
|
||||||
import asyncssh
|
import asyncssh
|
||||||
|
from qwen_integration import qwen_manager, QwenSessionState
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
|
||||||
|
|
@ -2609,6 +2610,7 @@ async def post_init(application: Application):
|
||||||
BotCommand("help", "Справка"),
|
BotCommand("help", "Справка"),
|
||||||
BotCommand("settings", "Настройки"),
|
BotCommand("settings", "Настройки"),
|
||||||
BotCommand("stop", "Прервать SSH-сессию"),
|
BotCommand("stop", "Прервать SSH-сессию"),
|
||||||
|
BotCommand("ai", "Задача для Qwen Code AI"),
|
||||||
]
|
]
|
||||||
await application.bot.set_my_commands(commands)
|
await application.bot.set_my_commands(commands)
|
||||||
|
|
||||||
|
|
@ -2636,6 +2638,92 @@ async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# КОМАНДЫ ДЛЯ РАБОТЫ С 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` — завершить сессию",
|
||||||
|
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
|
||||||
|
|
||||||
|
# Отправляем задачу в ИИ
|
||||||
|
await update.message.reply_text("⏳ 🤖 Думаю...", parse_mode="Markdown")
|
||||||
|
|
||||||
|
output_buffer = []
|
||||||
|
oauth_url_sent = False
|
||||||
|
|
||||||
|
def on_output(text: str):
|
||||||
|
output_buffer.append(text)
|
||||||
|
|
||||||
|
def on_oauth_url(url: str):
|
||||||
|
nonlocal oauth_url_sent
|
||||||
|
if not oauth_url_sent:
|
||||||
|
oauth_url_sent = True
|
||||||
|
asyncio.create_task(update.message.reply_text(
|
||||||
|
f"🔐 *Требуется авторизация Qwen Code*\n\n"
|
||||||
|
f"Откройте ссылку для авторизации:\n"
|
||||||
|
f"{url}\n\n"
|
||||||
|
f"После авторизации отправьте команду снова.",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Выполняем задачу
|
||||||
|
result = await qwen_manager.run_task(user_id, task, on_output, on_oauth_url)
|
||||||
|
|
||||||
|
# Если это не OAuth — показываем результат
|
||||||
|
if not oauth_url_sent:
|
||||||
|
full_output = "".join(output_buffer)
|
||||||
|
|
||||||
|
if len(full_output) > 4000:
|
||||||
|
full_output = full_output[:4000] + "\n... (вывод обрезан)"
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"🤖 *Результат:*\n\n"
|
||||||
|
f"```\n{full_output if full_output else result}\n```",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Точка входа."""
|
"""Точка входа."""
|
||||||
# Чтение токена только из переменной окружения
|
# Чтение токена только из переменной окружения
|
||||||
|
|
@ -2667,6 +2755,7 @@ def main():
|
||||||
application.add_handler(CommandHandler("stop", stop_command))
|
application.add_handler(CommandHandler("stop", stop_command))
|
||||||
application.add_handler(CallbackQueryHandler(menu_callback))
|
application.add_handler(CallbackQueryHandler(menu_callback))
|
||||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message))
|
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message))
|
||||||
|
application.add_handler(CommandHandler("ai", ai_command))
|
||||||
|
|
||||||
# Запуск
|
# Запуск
|
||||||
logger.info("Запуск бота...")
|
logger.info("Запуск бота...")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Интеграция с Qwen Code CLI.
|
||||||
|
Запуск, управление сессиями, обработка OAuth.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, Callable, Any
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class QwenSessionState(Enum):
|
||||||
|
"""Состояние сессии Qwen Code."""
|
||||||
|
STARTING = "starting"
|
||||||
|
WAITING_FOR_OAUTH = "waiting_for_oauth"
|
||||||
|
READY = "ready"
|
||||||
|
BUSY = "busy"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QwenSession:
|
||||||
|
"""Сессия Qwen Code."""
|
||||||
|
user_id: int
|
||||||
|
state: QwenSessionState = QwenSessionState.STARTING
|
||||||
|
process: Optional[subprocess.Popen] = None
|
||||||
|
oauth_url: Optional[str] = None
|
||||||
|
last_activity: datetime = field(default_factory=datetime.now)
|
||||||
|
pending_task: Optional[str] = None
|
||||||
|
output_buffer: str = ""
|
||||||
|
|
||||||
|
SESSION_TIMEOUT = timedelta(minutes=30) # Таймаут неактивности
|
||||||
|
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
return datetime.now() - self.last_activity > self.SESSION_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
class QwenCodeManager:
|
||||||
|
"""Менеджер сессий Qwen Code."""
|
||||||
|
|
||||||
|
def __init__(self, working_dir: str = None):
|
||||||
|
self._sessions: Dict[int, QwenSession] = {}
|
||||||
|
self._working_dir = working_dir or str(Path.home())
|
||||||
|
self._qwen_command = "qwen"
|
||||||
|
|
||||||
|
def get_session(self, user_id: int) -> Optional[QwenSession]:
|
||||||
|
"""Получить сессию пользователя."""
|
||||||
|
session = self._sessions.get(user_id)
|
||||||
|
if session and session.is_expired():
|
||||||
|
self.close_session(user_id)
|
||||||
|
return None
|
||||||
|
return session
|
||||||
|
|
||||||
|
def create_session(self, user_id: int) -> QwenSession:
|
||||||
|
"""Создать новую сессию."""
|
||||||
|
session = QwenSession(user_id=user_id)
|
||||||
|
self._sessions[user_id] = session
|
||||||
|
logger.info(f"Создана сессия Qwen Code для пользователя {user_id}")
|
||||||
|
return session
|
||||||
|
|
||||||
|
def close_session(self, user_id: int):
|
||||||
|
"""Закрыть сессию пользователя."""
|
||||||
|
session = self._sessions.pop(user_id, None)
|
||||||
|
if session and session.process:
|
||||||
|
try:
|
||||||
|
session.process.terminate()
|
||||||
|
session.process.wait(timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Ошибка при закрытии сессии Qwen: {e}")
|
||||||
|
logger.info(f"Закрыта сессия Qwen Code для пользователя {user_id}")
|
||||||
|
|
||||||
|
def has_active_session(self, user_id: int) -> bool:
|
||||||
|
"""Проверка наличия активной сессии."""
|
||||||
|
session = self.get_session(user_id)
|
||||||
|
return session is not None and session.state != QwenSessionState.ERROR
|
||||||
|
|
||||||
|
async def run_task(self, user_id: int, task: str,
|
||||||
|
on_output: Callable[[str], Any],
|
||||||
|
on_oauth_url: Callable[[str], Any]) -> str:
|
||||||
|
"""
|
||||||
|
Выполнить задачу в Qwen Code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
task: Задача для выполнения
|
||||||
|
on_output: Callback для вывода (вызывается при появлении вывода)
|
||||||
|
on_oauth_url: Callback для OAuth URL (вызывается если нужна авторизация)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Результат выполнения
|
||||||
|
"""
|
||||||
|
session = self.get_session(user_id)
|
||||||
|
|
||||||
|
# Если сессии нет или она в ошибке — создаём новую
|
||||||
|
if not session or session.state == QwenSessionState.ERROR:
|
||||||
|
session = self.create_session(user_id)
|
||||||
|
|
||||||
|
session.last_activity = datetime.now()
|
||||||
|
session.pending_task = task
|
||||||
|
|
||||||
|
# Если сессия ещё не готова (ожидает OAuth или запуска)
|
||||||
|
if session.state in [QwenSessionState.STARTING, QwenSessionState.WAITING_FOR_OAUTH]:
|
||||||
|
return await self._start_session(session, on_output, on_oauth_url, task)
|
||||||
|
|
||||||
|
# Если сессия готова — выполняем задачу
|
||||||
|
return await self._execute_task(session, task, on_output)
|
||||||
|
|
||||||
|
async def _start_session(self, session: QwenSession,
|
||||||
|
on_output: Callable[[str], Any],
|
||||||
|
on_oauth_url: Callable[[str], Any],
|
||||||
|
pending_task: str = None) -> str:
|
||||||
|
"""Запустить сессию Qwen Code."""
|
||||||
|
session.state = QwenSessionState.STARTING
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Запускаем qwen в интерактивном режиме с JSON выводом
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["FORCE_COLOR"] = "0" # Отключаем цвета для парсинга
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
self._qwen_command,
|
||||||
|
"--output-format", "stream-json",
|
||||||
|
"--input-format", "text",
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Запуск Qwen Code: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
session.process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
cwd=self._working_dir,
|
||||||
|
env=env,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Читаем вывод пока не поймём состояние
|
||||||
|
output = ""
|
||||||
|
oauth_detected = False
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = session.process.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
output += line
|
||||||
|
on_output(line)
|
||||||
|
|
||||||
|
# Проверяем на OAuth URL
|
||||||
|
oauth_match = re.search(
|
||||||
|
r'https://oauth\.qwen\.ai/[^>\s]+|'
|
||||||
|
r'https://[^>\s]*qwen[^>\s]*/oauth[^>\s]*|'
|
||||||
|
r'Authorize.*?https?://[^\s]+',
|
||||||
|
line,
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if oauth_match:
|
||||||
|
oauth_url = oauth_match.group(0)
|
||||||
|
session.oauth_url = oauth_url
|
||||||
|
session.state = QwenSessionState.WAITING_FOR_OAUTH
|
||||||
|
on_oauth_url(oauth_url)
|
||||||
|
oauth_detected = True
|
||||||
|
logger.info(f"Обнаружен OAuth URL: {oauth_url}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Проверяем на готовность
|
||||||
|
if "ready" in line.lower() or "assistant" in line.lower():
|
||||||
|
session.state = QwenSessionState.READY
|
||||||
|
logger.info("Сессия Qwen Code готова")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Таймаут запуска
|
||||||
|
if session.process.poll() is not None:
|
||||||
|
session.state = QwenSessionState.ERROR
|
||||||
|
return f"❌ Ошибка запуска Qwen Code: {output}"
|
||||||
|
|
||||||
|
# Если после запуска есть отложенная задача — выполняем
|
||||||
|
if pending_task and session.state == QwenSessionState.READY:
|
||||||
|
return await self._execute_task(session, pending_task, on_output)
|
||||||
|
|
||||||
|
if oauth_detected:
|
||||||
|
return "⏳ Ожидание авторизации..."
|
||||||
|
|
||||||
|
return "✅ Сессия запущена"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.state = QwenSessionState.ERROR
|
||||||
|
logger.error(f"Ошибка запуска сессии Qwen: {e}")
|
||||||
|
return f"❌ Ошибка: {str(e)}"
|
||||||
|
|
||||||
|
async def _execute_task(self, session: QwenSession,
|
||||||
|
task: str,
|
||||||
|
on_output: Callable[[str], Any]) -> str:
|
||||||
|
"""Выполнить задачу в активной сессии."""
|
||||||
|
session.state = QwenSessionState.BUSY
|
||||||
|
session.output_buffer = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Отправляем задачу
|
||||||
|
session.process.stdin.write(task + "\n")
|
||||||
|
session.process.stdin.flush()
|
||||||
|
|
||||||
|
# Читаем ответ
|
||||||
|
output = ""
|
||||||
|
timeout = 300 # 5 минут на выполнение
|
||||||
|
|
||||||
|
start_time = datetime.now()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Проверяем таймаут
|
||||||
|
if (datetime.now() - start_time).total_seconds() > timeout:
|
||||||
|
output += "\n\n⚠️ Таймаут выполнения (5 минут)"
|
||||||
|
break
|
||||||
|
|
||||||
|
# Проверяем процесс
|
||||||
|
if session.process.poll() is not None:
|
||||||
|
# Процесс завершился
|
||||||
|
remaining = session.process.stdout.read()
|
||||||
|
if remaining:
|
||||||
|
output += remaining
|
||||||
|
on_output(remaining)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Читаем вывод
|
||||||
|
line = session.process.stdout.readline()
|
||||||
|
if line:
|
||||||
|
output += line
|
||||||
|
session.output_buffer += line
|
||||||
|
on_output(line)
|
||||||
|
|
||||||
|
# Небольшая пауза чтобы не блокировать
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
session.state = QwenSessionState.READY
|
||||||
|
session.last_activity = datetime.now()
|
||||||
|
|
||||||
|
return self._parse_output(output)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
session.state = QwenSessionState.ERROR
|
||||||
|
logger.error(f"Ошибка выполнения задачи: {e}")
|
||||||
|
return f"❌ Ошибка: {str(e)}"
|
||||||
|
|
||||||
|
def _parse_output(self, output: str) -> str:
|
||||||
|
"""
|
||||||
|
Распарсить JSON вывод qwen-code.
|
||||||
|
Если вывод не JSON — вернуть как есть.
|
||||||
|
"""
|
||||||
|
# Пока просто возвращаем очищенный вывод
|
||||||
|
# В будущем можно парсить JSON stream-format
|
||||||
|
lines = output.split('\n')
|
||||||
|
cleaned = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# Убираем служебные сообщения
|
||||||
|
if line.strip() and not line.startswith('{'):
|
||||||
|
cleaned.append(line)
|
||||||
|
|
||||||
|
return '\n'.join(cleaned) if cleaned else output
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный менеджер
|
||||||
|
qwen_manager = QwenCodeManager()
|
||||||
Loading…
Reference in New Issue