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 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("Запуск бота...")
|
||||
|
|
|
|||
|
|
@ -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