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:
mirivlad 2026-02-24 04:09:25 +08:00
parent df8786bee2
commit 5d451ff870
2 changed files with 364 additions and 0 deletions

89
bot.py
View File

@ -24,6 +24,7 @@ from datetime import datetime, timedelta
import pexpect
import asyncssh
from qwen_integration import qwen_manager, QwenSessionState
from dotenv import load_dotenv
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
@ -2609,6 +2610,7 @@ async def post_init(application: Application):
BotCommand("help", "Справка"),
BotCommand("settings", "Настройки"),
BotCommand("stop", "Прервать SSH-сессию"),
BotCommand("ai", "Задача для Qwen Code AI"),
]
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():
"""Точка входа."""
# Чтение токена только из переменной окружения
@ -2667,6 +2755,7 @@ def main():
application.add_handler(CommandHandler("stop", stop_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("Запуск бота...")

275
qwen_integration.py Normal file
View File

@ -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()