Версия 0.8.1 - Автоматическая OAuth авторизация Qwen Code
Основные изменения: - Добавлена автоматическая OAuth авторизация для Qwen Code - При первом запросе к Qwen бот отправляет ссылку на авторизацию - После авторизации токены сохраняются в ~/.qwen/oauth_creds.json - Добавлена команда /qwen_auth для явной авторизации - Ссылка на авторизацию кликабельная в Telegram Новые файлы: - bot/utils/qwen_oauth.py — OAuth 2.0 Device Flow клиент - authorize_qwen.sh — скрипт для ручной авторизации Изменения: - bot.py — проверка авторизации в handle_ai_task, qwen_auth_command - bot/models/user_state.py — поле waiting_for_qwen_oauth - qwen_integration.py — интеграция с OAuth модулем - README.md — версия 0.8.1 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
9854836b17
commit
769c662ab5
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Скрипт для авторизации Qwen Code на сервере
|
||||||
|
|
||||||
|
echo "🔐 Авторизация Qwen Code..."
|
||||||
|
echo ""
|
||||||
|
echo "Запускаю qwen-code в интерактивном режиме..."
|
||||||
|
echo "Следуй инструкциям в браузере."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd /home/mirivlad/telegram-bot
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Запускаем qwen-code с простой командой чтобы инициировать авторизацию
|
||||||
|
qwen -p "привет, это тест авторизации"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Если авторизация прошла успешно, токен сохранён в ~/.qwen/oauth_creds.json"
|
||||||
|
echo ""
|
||||||
|
echo "Проверка файла с токеном:"
|
||||||
|
ls -la ~/.qwen/oauth_creds.json 2>/dev/null || echo "❌ Файл с токеном не найден"
|
||||||
137
bot.py
137
bot.py
|
|
@ -138,6 +138,11 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
await handle_restart_password(update, text)
|
await handle_restart_password(update, text)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Проверка: не ждём ли завершения OAuth авторизации Qwen
|
||||||
|
if state.waiting_for_qwen_oauth:
|
||||||
|
await handle_qwen_oauth_completion(update, text)
|
||||||
|
return
|
||||||
|
|
||||||
# ПРОВЕРКА: режим чата с ИИ агентом
|
# ПРОВЕРКА: режим чата с ИИ агентом
|
||||||
if state.ai_chat_mode:
|
if state.ai_chat_mode:
|
||||||
logger.info(f"Пользователь {user_id} отправил задачу ИИ: {text}")
|
logger.info(f"Пользователь {user_id} отправил задачу ИИ: {text}")
|
||||||
|
|
@ -163,6 +168,40 @@ async def handle_ai_task(update: Update, text: str):
|
||||||
logger.info(f"Пользователь {user_id}: ИИ отключен, пропускаем обработку")
|
logger.info(f"Пользователь {user_id}: ИИ отключен, пропускаем обработку")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# === ПРОВЕРКА: Авторизация Qwen ===
|
||||||
|
from bot.utils.qwen_oauth import is_authorized, get_authorization_url
|
||||||
|
|
||||||
|
if not await is_authorized():
|
||||||
|
logger.info(f"Пользователь {user_id} не авторизован в Qwen, получаем OAuth URL")
|
||||||
|
oauth_url = await get_authorization_url()
|
||||||
|
|
||||||
|
if oauth_url:
|
||||||
|
# Устанавливаем флаг ожидания
|
||||||
|
state.waiting_for_qwen_oauth = True
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🔐 **Требуется авторизация Qwen Code**\n\n"
|
||||||
|
"Для работы с Qwen Code необходимо авторизоваться.\n\n"
|
||||||
|
f"🔗 **[Открыть ссылку для авторизации]({oauth_url})**\n\n"
|
||||||
|
"Или скопируй:\n"
|
||||||
|
f"`{oauth_url}`\n\n"
|
||||||
|
"📋 **Инструкция:**\n"
|
||||||
|
"1. Нажми на ссылку выше или скопируй её в браузер\n"
|
||||||
|
"2. Войди через Google или GitHub\n"
|
||||||
|
"3. Разрешите доступ\n"
|
||||||
|
"4. Вернись в Telegram и отправь любое сообщение\n\n"
|
||||||
|
"_Бот автоматически продолжит работу после авторизации._",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
disable_web_page_preview=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"❌ **Ошибка получения OAuth URL**\n\n"
|
||||||
|
"Не удалось получить ссылку для авторизации. Попробуйте позже или используйте /qwen_auth",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# === ПРОВЕРКА: Нужна ли компактификация? ===
|
# === ПРОВЕРКА: Нужна ли компактификация? ===
|
||||||
# Проверяем порог заполненности контекста
|
# Проверяем порог заполненности контекста
|
||||||
if compactor.check_compaction_needed():
|
if compactor.check_compaction_needed():
|
||||||
|
|
@ -241,8 +280,22 @@ async def handle_ai_task(update: Update, text: str):
|
||||||
"""Callback для накопления полного вывода (не используется для streaming)."""
|
"""Callback для накопления полного вывода (не используется для streaming)."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_oauth_url(url: str):
|
async def on_oauth_url(url: str):
|
||||||
pass
|
"""Callback для OAuth URL — отправляем ссылку в чат."""
|
||||||
|
try:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🔐 **Требуется авторизация Qwen Code**\n\n"
|
||||||
|
"Для работы с Qwen Code необходимо авторизоваться.\n\n"
|
||||||
|
"🔗 **Инструкция:**\n"
|
||||||
|
"1. Откройте ссылку: `{url}`\n"
|
||||||
|
"2. Войдите через Google/GitHub\n"
|
||||||
|
"3. После авторизации отправьте любое сообщение в чат\n\n"
|
||||||
|
"_Бот автоматически продолжит работу после авторизации._".format(url=url),
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
logger.info(f"Отправлен OAuth URL пользователю {user_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки OAuth URL: {e}")
|
||||||
|
|
||||||
def on_event(event):
|
def on_event(event):
|
||||||
"""Обработка событий stream-json для обновления статуса."""
|
"""Обработка событий stream-json для обновления статуса."""
|
||||||
|
|
@ -1712,6 +1765,7 @@ async def post_init(application: Application):
|
||||||
BotCommand("cron", "Управление задачами"),
|
BotCommand("cron", "Управление задачами"),
|
||||||
BotCommand("stop", "Прервать SSH-сессию"),
|
BotCommand("stop", "Прервать SSH-сессию"),
|
||||||
BotCommand("restart_bot", "Перезапустить бота"),
|
BotCommand("restart_bot", "Перезапустить бота"),
|
||||||
|
BotCommand("qwen_auth", "Авторизовать Qwen Code"),
|
||||||
BotCommand("ai_presets", "🎛️ Выбор AI-провайдера"),
|
BotCommand("ai_presets", "🎛️ Выбор AI-провайдера"),
|
||||||
BotCommand("ai_off", "⌨️ ИИ Отключен (CLI режим)"),
|
BotCommand("ai_off", "⌨️ ИИ Отключен (CLI режим)"),
|
||||||
BotCommand("ai_qwen", "💻 Qwen Code (бесплатно)"),
|
BotCommand("ai_qwen", "💻 Qwen Code (бесплатно)"),
|
||||||
|
|
@ -1861,6 +1915,84 @@ async def restart_bot_command(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# QWEN OAUTH АВТОРИЗАЦИЯ
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
@check_access
|
||||||
|
async def qwen_auth_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Обработка команды /qwen_auth - авторизация Qwen Code."""
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
state = state_manager.get(user_id)
|
||||||
|
|
||||||
|
from bot.utils.qwen_oauth import get_authorization_url, is_authorized
|
||||||
|
|
||||||
|
# Проверяем есть ли уже валидный токен
|
||||||
|
if await is_authorized():
|
||||||
|
await update.message.reply_text(
|
||||||
|
"✅ **Qwen уже авторизован!**\n\n"
|
||||||
|
"Токен действителен и готов к использованию.",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем OAuth URL
|
||||||
|
oauth_url = await get_authorization_url()
|
||||||
|
|
||||||
|
if not oauth_url:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"❌ **Ошибка получения OAuth URL**\n\n"
|
||||||
|
"Не удалось получить ссылку для авторизации. Попробуйте позже.",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Устанавливаем флаг ожидания
|
||||||
|
state.waiting_for_qwen_oauth = True
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🔐 **Авторизация Qwen Code**\n\n"
|
||||||
|
"Для работы с Qwen Code необходимо авторизоваться.\n\n"
|
||||||
|
"🔗 **Откройте ссылку:**\n"
|
||||||
|
f"`{oauth_url}`\n\n"
|
||||||
|
"📋 **Инструкция:**\n"
|
||||||
|
"1. Нажмите на ссылку или скопируйте её в браузер\n"
|
||||||
|
"2. Войдите через Google или GitHub\n"
|
||||||
|
"3. Разрешите доступ\n"
|
||||||
|
"4. Вернитесь в Telegram и отправьте любое сообщение\n\n"
|
||||||
|
"_Бот автоматически проверит завершение авторизации._",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_qwen_oauth_completion(update: Update, text: str):
|
||||||
|
"""Проверка завершения OAuth авторизации."""
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
state = state_manager.get(user_id)
|
||||||
|
|
||||||
|
from bot.utils.qwen_oauth import check_authorization_complete, is_authorized
|
||||||
|
|
||||||
|
# Проверяем завершение авторизации
|
||||||
|
if await check_authorization_complete():
|
||||||
|
state.waiting_for_qwen_oauth = False
|
||||||
|
|
||||||
|
if await is_authorized():
|
||||||
|
await update.message.reply_text(
|
||||||
|
"✅ **Авторизация успешна!**\n\n"
|
||||||
|
"Qwen Code готов к работе. Отправьте задачу.",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Если авторизация ещё не завершена — показываем статус
|
||||||
|
await update.message.reply_text(
|
||||||
|
"⏳ **Проверка авторизации...**\n\n"
|
||||||
|
"Если вы уже авторизовались, бот продолжит работу.\n"
|
||||||
|
"Если нет — откройте ссылку для авторизации.",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_restart_password(update: Update, text: str):
|
async def handle_restart_password(update: Update, text: str):
|
||||||
"""Обработка пароля для перезапуска бота."""
|
"""Обработка пароля для перезапуска бота."""
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
|
|
@ -2271,6 +2403,7 @@ def main():
|
||||||
application.add_handler(CommandHandler("menu", menu_command))
|
application.add_handler(CommandHandler("menu", menu_command))
|
||||||
application.add_handler(CommandHandler("stop", stop_command))
|
application.add_handler(CommandHandler("stop", stop_command))
|
||||||
application.add_handler(CommandHandler("restart_bot", restart_bot_command))
|
application.add_handler(CommandHandler("restart_bot", restart_bot_command))
|
||||||
|
application.add_handler(CommandHandler("qwen_auth", qwen_auth_command))
|
||||||
application.add_handler(CommandHandler("memory", memory_command))
|
application.add_handler(CommandHandler("memory", memory_command))
|
||||||
application.add_handler(CommandHandler("compact", compact_command))
|
application.add_handler(CommandHandler("compact", compact_command))
|
||||||
application.add_handler(CommandHandler("facts", facts_command))
|
application.add_handler(CommandHandler("facts", facts_command))
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ class UserState:
|
||||||
# Для команды /restart_bot
|
# Для команды /restart_bot
|
||||||
waiting_for_restart_password: bool = False # Ожидание пароля sudo для перезапуска
|
waiting_for_restart_password: bool = False # Ожидание пароля sudo для перезапуска
|
||||||
|
|
||||||
|
# Для OAuth авторизации Qwen
|
||||||
|
waiting_for_qwen_oauth: bool = False # Ожидание завершения OAuth авторизации
|
||||||
|
|
||||||
|
|
||||||
class StateManager:
|
class StateManager:
|
||||||
"""Управление состояниями пользователей."""
|
"""Управление состояниями пользователей."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,547 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Qwen OAuth 2.0 Device Flow клиент.
|
||||||
|
Реализует авторизацию через Device Authorization Grant (RFC 8628).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
import aiohttp
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Qwen OAuth константы
|
||||||
|
QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai'
|
||||||
|
QWEN_OAUTH_DEVICE_CODE_ENDPOINT = f'{QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code'
|
||||||
|
QWEN_OAUTH_TOKEN_ENDPOINT = f'{QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token'
|
||||||
|
QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56'
|
||||||
|
QWEN_OAUTH_SCOPE = 'openid profile email model.completion'
|
||||||
|
|
||||||
|
# Пути для хранения токенов (как в qwen-code CLI)
|
||||||
|
QWEN_CONFIG_DIR = Path.home() / '.qwen'
|
||||||
|
QWEN_CREDENTIALS_FILE = QWEN_CONFIG_DIR / 'oauth_creds.json'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QwenCredentials:
|
||||||
|
"""OAuth токены Qwen."""
|
||||||
|
access_token: str = ''
|
||||||
|
refresh_token: str = ''
|
||||||
|
token_type: str = 'Bearer'
|
||||||
|
expiry_date: int = 0 # Unix timestamp в миллисекундах
|
||||||
|
resource_url: str = 'portal.qwen.ai'
|
||||||
|
|
||||||
|
def is_expired(self, buffer_minutes: int = 5) -> bool:
|
||||||
|
"""Проверка истечения токена с буфером."""
|
||||||
|
if not self.expiry_date:
|
||||||
|
return True
|
||||||
|
expiry_ms = self.expiry_date - (buffer_minutes * 60 * 1000)
|
||||||
|
return int(datetime.now().timestamp() * 1000) >= expiry_ms
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceAuthorizationResponse:
|
||||||
|
"""Ответ устройства авторизации."""
|
||||||
|
device_code: str = ''
|
||||||
|
user_code: str = ''
|
||||||
|
verification_uri: str = ''
|
||||||
|
verification_uri_complete: str = ''
|
||||||
|
expires_in: int = 0
|
||||||
|
interval: int = 5 # Polling interval в секундах
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authorization_url(self) -> str:
|
||||||
|
"""Полная ссылка для авторизации."""
|
||||||
|
return self.verification_uri_complete
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
"""Истёк ли срок действия device code."""
|
||||||
|
return time.time() > (self.expires_in - 30) # 30 сек буфер
|
||||||
|
|
||||||
|
|
||||||
|
class QwenOAuthClient:
|
||||||
|
"""Qwen OAuth 2.0 клиент."""
|
||||||
|
|
||||||
|
def __init__(self, credentials_path: Optional[Path] = None):
|
||||||
|
self.credentials_path = credentials_path or QWEN_CREDENTIALS_FILE
|
||||||
|
self._credentials: Optional[QwenCredentials] = None
|
||||||
|
self._load_credentials()
|
||||||
|
|
||||||
|
def _load_credentials(self) -> None:
|
||||||
|
"""Загрузить токены из файла."""
|
||||||
|
if self.credentials_path.exists():
|
||||||
|
try:
|
||||||
|
with open(self.credentials_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self._credentials = QwenCredentials(**data)
|
||||||
|
logger.info(f"Токены загружены из {self.credentials_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка загрузки токенов: {e}")
|
||||||
|
self._credentials = None
|
||||||
|
else:
|
||||||
|
logger.debug("Файл с токенами не найден")
|
||||||
|
self._credentials = None
|
||||||
|
|
||||||
|
# Загружаем code_verifier из device_code.json если есть
|
||||||
|
device_code_file = QWEN_CONFIG_DIR / 'device_code.json'
|
||||||
|
if device_code_file.exists():
|
||||||
|
try:
|
||||||
|
with open(device_code_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
code_verifier = data.get('code_verifier', '')
|
||||||
|
if code_verifier:
|
||||||
|
self._code_verifier = code_verifier
|
||||||
|
logger.info(f"Code verifier загружен из {device_code_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Ошибка загрузки code_verifier: {e}")
|
||||||
|
|
||||||
|
def _save_credentials(self) -> None:
|
||||||
|
"""Сохранить токены в файл."""
|
||||||
|
if self._credentials:
|
||||||
|
self.credentials_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.credentials_path, 'w') as f:
|
||||||
|
json.dump(self._credentials.__dict__, f, indent=2)
|
||||||
|
# Устанавливаем права 600 (только владелец)
|
||||||
|
os.chmod(self.credentials_path, 0o600)
|
||||||
|
logger.info(f"Токены сохранены в {self.credentials_path}")
|
||||||
|
|
||||||
|
def has_valid_token(self) -> bool:
|
||||||
|
"""Проверка наличия валидного токена."""
|
||||||
|
return self._credentials is not None and not self._credentials.is_expired()
|
||||||
|
|
||||||
|
async def get_access_token(self) -> Optional[str]:
|
||||||
|
"""Получить access token (обновляет если истёк)."""
|
||||||
|
if self.has_valid_token():
|
||||||
|
return self._credentials.access_token
|
||||||
|
|
||||||
|
if self._credentials and self._credentials.refresh_token:
|
||||||
|
# Пробуем обновить токен
|
||||||
|
if await self._refresh_token():
|
||||||
|
return self._credentials.access_token
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def request_device_authorization(self) -> DeviceAuthorizationResponse:
|
||||||
|
"""
|
||||||
|
Запросить Device Authorization.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DeviceAuthorizationResponse с данными для авторизации
|
||||||
|
"""
|
||||||
|
# Проверяем есть ли сохранённый code_verifier
|
||||||
|
if not hasattr(self, '_code_verifier') or not self._code_verifier:
|
||||||
|
# Генерируем PKCE code verifier и challenge
|
||||||
|
self._code_verifier = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
code_challenge = hashlib.sha256(self._code_verifier.encode()).hexdigest()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID,
|
||||||
|
'scope': QWEN_OAUTH_SCOPE,
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'S256'
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'x-request-id': secrets.token_hex(16),
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
text = await resp.text()
|
||||||
|
raise Exception(f"Device authorization failed: {resp.status} - {text}")
|
||||||
|
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
return DeviceAuthorizationResponse(
|
||||||
|
device_code=data.get('device_code', ''),
|
||||||
|
user_code=data.get('user_code', ''),
|
||||||
|
verification_uri=data.get('verification_uri', ''),
|
||||||
|
verification_uri_complete=data.get('verification_uri_complete', ''),
|
||||||
|
expires_in=data.get('expires_in', 900),
|
||||||
|
interval=data.get('interval', 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def poll_for_token(self, device_code: str, timeout_seconds: int = 900) -> bool:
|
||||||
|
"""
|
||||||
|
Опрос сервера для получения токена после авторизации пользователем.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_code: Device code из request_device_authorization
|
||||||
|
timeout_seconds: Максимальное время ожидания
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если авторизация успешна
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_code_verifier'):
|
||||||
|
raise Exception("Code verifier not set. Call request_device_authorization first.")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
interval = 5 # Начальный интервал
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout_seconds:
|
||||||
|
payload = {
|
||||||
|
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
'device_code': device_code,
|
||||||
|
'code_verifier': self._code_verifier,
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
logger.debug(f"Polling payload: grant_type={payload['grant_type']}, device_code={device_code[:20]}..., code_verifier={self._code_verifier[:20]}...")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_TOKEN_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
logger.info(f"Polling response: status={resp.status}, data={data}")
|
||||||
|
|
||||||
|
if resp.status == 200:
|
||||||
|
# Успех! Сохраняем токены
|
||||||
|
self._credentials = QwenCredentials(
|
||||||
|
access_token=data.get('access_token', ''),
|
||||||
|
refresh_token=data.get('refresh_token', ''),
|
||||||
|
token_type=data.get('token_type', 'Bearer'),
|
||||||
|
expiry_date=int(datetime.now().timestamp() * 1000) + (data.get('expires_in', 3600) * 1000),
|
||||||
|
resource_url=data.get('resource_url', 'portal.qwen.ai')
|
||||||
|
)
|
||||||
|
self._save_credentials()
|
||||||
|
logger.info("Авторизация успешна!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
error = data.get('error', '')
|
||||||
|
|
||||||
|
if error == 'authorization_pending':
|
||||||
|
# Пользователь ещё не авторизовался
|
||||||
|
logger.debug("Ожидание авторизации пользователя...")
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif error == 'slow_down':
|
||||||
|
# Сервер просит увеличить интервал
|
||||||
|
interval += 5
|
||||||
|
logger.debug(f"Увеличиваем интервал до {interval} сек")
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif error == 'expired_token':
|
||||||
|
logger.error("Device code истёк")
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif error == 'access_denied':
|
||||||
|
logger.error("Пользователь отклонил авторизацию")
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"Неизвестная ошибка: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.error("Таймаут авторизации")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _refresh_token(self) -> bool:
|
||||||
|
"""Обновить access token используя refresh token."""
|
||||||
|
if not self._credentials or not self._credentials.refresh_token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': self._credentials.refresh_token,
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_TOKEN_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
self._credentials.access_token = data.get('access_token', '')
|
||||||
|
self._credentials.refresh_token = data.get('refresh_token', self._credentials.refresh_token)
|
||||||
|
self._credentials.expiry_date = int(datetime.now().timestamp() * 1000) + (data.get('expires_in', 3600) * 1000)
|
||||||
|
self._save_credentials()
|
||||||
|
logger.info("Токен обновлён")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка обновления токена: {resp.status}")
|
||||||
|
self._credentials = None # Очищаем неверные токены
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка обновления токена: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_credentials(self) -> None:
|
||||||
|
"""Очистить сохранённые токены."""
|
||||||
|
self._credentials = None
|
||||||
|
if self.credentials_path.exists():
|
||||||
|
self.credentials_path.unlink()
|
||||||
|
logger.info("Токены очищены")
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный клиент (singleton)
|
||||||
|
_oauth_client: Optional[QwenOAuthClient] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_oauth_client() -> QwenOAuthClient:
|
||||||
|
"""Получить OAuth клиент (singleton)."""
|
||||||
|
global _oauth_client
|
||||||
|
if _oauth_client is None:
|
||||||
|
_oauth_client = QwenOAuthClient()
|
||||||
|
return _oauth_client
|
||||||
|
|
||||||
|
|
||||||
|
async def get_authorization_url() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получить URL для авторизации.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
URL для авторизации или None если ошибка
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Проверяем есть ли активный device code
|
||||||
|
device_code_file = QWEN_CONFIG_DIR / 'device_code.json'
|
||||||
|
if device_code_file.exists():
|
||||||
|
with open(device_code_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
start_time = data.get('start_time', 0)
|
||||||
|
expires_in = data.get('expires_in', 900)
|
||||||
|
code_verifier = data.get('code_verifier', '')
|
||||||
|
auth_url = data.get('authorization_url', '')
|
||||||
|
device_code = data.get('device_code', '')
|
||||||
|
|
||||||
|
# Если device code ещё активен — используем его
|
||||||
|
if time.time() - start_time < expires_in - 60 and code_verifier and auth_url and device_code:
|
||||||
|
logger.info("Используем существующий device code")
|
||||||
|
return auth_url
|
||||||
|
|
||||||
|
# Генерируем PKCE пару как в qwen-code CLI
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip('=')
|
||||||
|
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode('utf-8').rstrip('=')
|
||||||
|
|
||||||
|
# Запрос device authorization
|
||||||
|
payload = {
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID,
|
||||||
|
'scope': QWEN_OAUTH_SCOPE,
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'S256'
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'x-request-id': secrets.token_hex(16),
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
text = await resp.text()
|
||||||
|
raise Exception(f"Device authorization failed: {resp.status} - {text}")
|
||||||
|
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
auth_url = data.get('verification_uri_complete', '')
|
||||||
|
device_code = data.get('device_code', '')
|
||||||
|
expires_in = data.get('expires_in', 900)
|
||||||
|
|
||||||
|
# Сохраняем device code и code verifier для polling
|
||||||
|
device_code_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(device_code_file, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'device_code': device_code,
|
||||||
|
'code_verifier': code_verifier, # Сохраняем тот же code_verifier!
|
||||||
|
'expires_in': expires_in,
|
||||||
|
'start_time': time.time(),
|
||||||
|
'authorization_url': auth_url
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
logger.info(f"Device code получен: {device_code[:20]}..., code_verifier сохранён")
|
||||||
|
return auth_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения URL авторизации: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def check_authorization_complete() -> bool:
|
||||||
|
"""
|
||||||
|
Проверить завершение авторизации (polling).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если авторизация завершена успешно
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Читаем device code
|
||||||
|
device_code_file = QWEN_CONFIG_DIR / 'device_code.json'
|
||||||
|
if not device_code_file.exists():
|
||||||
|
logger.debug("device_code.json не найден")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(device_code_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
device_code = data.get('device_code', '')
|
||||||
|
code_verifier = data.get('code_verifier', '') # Получаем code_verifier из файла
|
||||||
|
start_time = data.get('start_time', time.time())
|
||||||
|
expires_in = data.get('expires_in', 900)
|
||||||
|
|
||||||
|
logger.info(f"Device code: {device_code[:20]}..., code_verifier: {code_verifier[:20]}...")
|
||||||
|
|
||||||
|
# Проверяем не истёк ли timeout
|
||||||
|
if time.time() - start_time > expires_in:
|
||||||
|
logger.warning("Device code истёк")
|
||||||
|
device_code_file.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Polling с code_verifier из файла
|
||||||
|
logger.info("Запуск polling для получения токена...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
'device_code': device_code,
|
||||||
|
'code_verifier': code_verifier,
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_TOKEN_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
logger.info(f"Polling response: status={resp.status}, data={data}")
|
||||||
|
|
||||||
|
if resp.status == 200:
|
||||||
|
# Успех! Сохраняем токены
|
||||||
|
credentials = {
|
||||||
|
'access_token': data.get('access_token', ''),
|
||||||
|
'refresh_token': data.get('refresh_token', ''),
|
||||||
|
'token_type': data.get('token_type', 'Bearer'),
|
||||||
|
'expiry_date': int(time.time() * 1000) + (data.get('expires_in', 3600) * 1000),
|
||||||
|
'resource_url': data.get('resource_url', 'portal.qwen.ai')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохраняем токены в файл
|
||||||
|
QWEN_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(QWEN_CREDENTIALS_FILE, 'w') as f:
|
||||||
|
json.dump(credentials, f, indent=2)
|
||||||
|
os.chmod(QWEN_CREDENTIALS_FILE, 0o600)
|
||||||
|
|
||||||
|
device_code_file.unlink()
|
||||||
|
logger.info("Авторизация успешна! Токены сохранены.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
error = data.get('error', '')
|
||||||
|
|
||||||
|
if error == 'authorization_pending':
|
||||||
|
logger.debug("Ожидание авторизации пользователя...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif error == 'slow_down':
|
||||||
|
logger.debug("Сервер просит увеличить интервал")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif error == 'expired_token':
|
||||||
|
logger.error("Device code истёк")
|
||||||
|
device_code_file.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif error == 'access_denied':
|
||||||
|
logger.error("Пользователь отклонил авторизацию")
|
||||||
|
device_code_file.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif error == 'invalid_request':
|
||||||
|
error_desc = data.get('error_description', '')
|
||||||
|
logger.error(f"Invalid request: {error_desc}")
|
||||||
|
# Проверяем не истёк ли code_verifier
|
||||||
|
if 'code_verifier' in error_desc.lower():
|
||||||
|
logger.error("Code verifier не совпадает — удаляем device_code.json")
|
||||||
|
device_code_file.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"Неизвестная ошибка: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка проверки авторизации: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def is_authorized() -> bool:
|
||||||
|
"""Проверить авторизован ли пользователь."""
|
||||||
|
client = get_oauth_client()
|
||||||
|
return client.has_valid_token()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_access_token() -> Optional[str]:
|
||||||
|
"""Получить access token."""
|
||||||
|
client = get_oauth_client()
|
||||||
|
return await client.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_authorization() -> None:
|
||||||
|
"""Очистить авторизацию."""
|
||||||
|
client = get_oauth_client()
|
||||||
|
client.clear_credentials()
|
||||||
|
|
@ -18,6 +18,15 @@ from datetime import datetime, timedelta
|
||||||
from typing import Optional, Dict, Callable, Any, List, Union
|
from typing import Optional, Dict, Callable, Any, List, Union
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
# Импортируем OAuth модуль
|
||||||
|
from bot.utils.qwen_oauth import (
|
||||||
|
get_authorization_url,
|
||||||
|
check_authorization_complete,
|
||||||
|
is_authorized,
|
||||||
|
get_access_token,
|
||||||
|
clear_authorization
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,6 +68,7 @@ class QwenSession:
|
||||||
state: QwenSessionState = QwenSessionState.STARTING
|
state: QwenSessionState = QwenSessionState.STARTING
|
||||||
process: Optional[subprocess.Popen] = None
|
process: Optional[subprocess.Popen] = None
|
||||||
oauth_url: Optional[str] = None
|
oauth_url: Optional[str] = None
|
||||||
|
on_oauth_url: Optional[Callable] = None # Callback для OAuth URL
|
||||||
last_activity: datetime = field(default_factory=datetime.now)
|
last_activity: datetime = field(default_factory=datetime.now)
|
||||||
pending_task: Optional[str] = None
|
pending_task: Optional[str] = None
|
||||||
output_buffer: str = ""
|
output_buffer: str = ""
|
||||||
|
|
@ -114,6 +124,67 @@ class QwenCodeManager:
|
||||||
logger.info(f"Создана сессия Qwen Code для пользователя {user_id}")
|
logger.info(f"Создана сессия Qwen Code для пользователя {user_id}")
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
async def get_oauth_url(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получить OAuth ссылку для авторизации Qwen Code.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OAuth URL или None если не удалось получить
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
# Генерируем code verifier и challenge (упрощённо)
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
code_verifier = secrets.token_urlsafe(32)
|
||||||
|
code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest()
|
||||||
|
|
||||||
|
# Запрос на получение device code (form-urlencoded как в оригинале)
|
||||||
|
payload = {
|
||||||
|
'client_id': QWEN_OAUTH_CLIENT_ID,
|
||||||
|
'scope': 'openid profile email model.completion',
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'S256'
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'x-request-id': str(uuid.uuid4()),
|
||||||
|
'User-Agent': 'qwen-code-cli/0.11.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Формируем form-urlencoded тело
|
||||||
|
form_data = '&'.join(f'{k}={v}' for k, v in payload.items())
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
||||||
|
data=form_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
if resp.status == 200:
|
||||||
|
try:
|
||||||
|
data = await resp.json()
|
||||||
|
verification_uri = data.get('verification_uri_complete', '')
|
||||||
|
logger.info(f"Получен OAuth URL: {verification_uri}")
|
||||||
|
return verification_uri
|
||||||
|
except Exception as json_err:
|
||||||
|
logger.error(f"Ошибка парсинга JSON: {json_err}")
|
||||||
|
logger.debug(f"Ответ сервера: {text[:200]}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Ошибка получения OAuth: {resp.status}")
|
||||||
|
logger.debug(f"Ответ сервера: {text[:200]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения OAuth URL: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def close_session(self, user_id: int):
|
def close_session(self, user_id: int):
|
||||||
"""Закрыть сессию пользователя."""
|
"""Закрыть сессию пользователя."""
|
||||||
session = self._sessions.pop(user_id, None)
|
session = self._sessions.pop(user_id, None)
|
||||||
|
|
@ -155,6 +226,7 @@ class QwenCodeManager:
|
||||||
|
|
||||||
session.last_activity = datetime.now()
|
session.last_activity = datetime.now()
|
||||||
session.pending_task = task
|
session.pending_task = task
|
||||||
|
session.on_oauth_url = on_oauth_url # Сохраняем callback для OAuth
|
||||||
|
|
||||||
# Добавляем системный промпт если нужно
|
# Добавляем системный промпт если нужно
|
||||||
if use_system_prompt:
|
if use_system_prompt:
|
||||||
|
|
@ -185,6 +257,7 @@ class QwenCodeManager:
|
||||||
self._qwen_command,
|
self._qwen_command,
|
||||||
"--output-format", "stream-json",
|
"--output-format", "stream-json",
|
||||||
"--input-format", "text",
|
"--input-format", "text",
|
||||||
|
"--auth-type", "qwen-oauth", # Явное указание типа авторизации
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info(f"Запуск Qwen Code: {' '.join(cmd)}")
|
logger.info(f"Запуск Qwen Code: {' '.join(cmd)}")
|
||||||
|
|
@ -286,7 +359,8 @@ class QwenCodeManager:
|
||||||
self._qwen_command,
|
self._qwen_command,
|
||||||
"-p", task,
|
"-p", task,
|
||||||
"--output-format", "stream-json", # Правильный streaming формат
|
"--output-format", "stream-json", # Правильный streaming формат
|
||||||
"--yolo", # Авто-подтверждение
|
"--auth-type", "qwen-oauth", # Явное указание типа авторизации
|
||||||
|
# НЕ используем --yolo чтобы получить OAuth ссылку если нужно
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info(f"Выполнение задачи (stream-json): {' '.join(cmd)}")
|
logger.info(f"Выполнение задачи (stream-json): {' '.join(cmd)}")
|
||||||
|
|
@ -333,6 +407,17 @@ class QwenCodeManager:
|
||||||
session.output_buffer += line_str
|
session.output_buffer += line_str
|
||||||
last_chunk_time = datetime.now()
|
last_chunk_time = datetime.now()
|
||||||
|
|
||||||
|
# Проверяем на OAuth ссылку в текстовом выводе
|
||||||
|
oauth_match = re.search(
|
||||||
|
r'https://chat\.qwen\.ai/authorize\?user_code=([A-Za-z0-9_-]+)',
|
||||||
|
line_str
|
||||||
|
)
|
||||||
|
if oauth_match:
|
||||||
|
oauth_url = oauth_match.group(0)
|
||||||
|
logger.info(f"Обнаружена OAuth ссылка: {oauth_url}")
|
||||||
|
if session and session.on_oauth_url:
|
||||||
|
await session.on_oauth_url(oauth_url)
|
||||||
|
|
||||||
# Парсим JSON событие и извлекаем текст
|
# Парсим JSON событие и извлекаем текст
|
||||||
await self._process_stream_lines(
|
await self._process_stream_lines(
|
||||||
line_str, on_output, on_chunk, on_event, session
|
line_str, on_output, on_chunk, on_event, session
|
||||||
|
|
@ -437,8 +522,22 @@ class QwenCodeManager:
|
||||||
|
|
||||||
# Проверяем на ошибку
|
# Проверяем на ошибку
|
||||||
if event_data.get('is_error'):
|
if event_data.get('is_error'):
|
||||||
error_text = event_data.get('error', 'Неизвестная ошибка')
|
error_obj = event_data.get('error', {})
|
||||||
logger.error(f"Ошибка Qwen: {error_text}")
|
error_message = error_obj.get('message', 'Неизвестная ошибка') if isinstance(error_obj, dict) else str(error_obj)
|
||||||
|
logger.error(f"Ошибка Qwen: {error_message}")
|
||||||
|
|
||||||
|
# Проверяем ошибку авторизации — получаем OAuth URL
|
||||||
|
if 'No auth type' in error_message or 'auth type is not selected' in error_message:
|
||||||
|
# Получаем OAuth URL через API
|
||||||
|
oauth_url = await self.get_oauth_url()
|
||||||
|
if not oauth_url:
|
||||||
|
oauth_url = f"{QWEN_OAUTH_BASE_URL}/"
|
||||||
|
|
||||||
|
logger.info(f"Требуется OAuth: {oauth_url}")
|
||||||
|
|
||||||
|
# Вызываем on_oauth_url если есть в session
|
||||||
|
if session and hasattr(session, 'on_oauth_url') and session.on_oauth_url:
|
||||||
|
await session.on_oauth_url(oauth_url)
|
||||||
|
|
||||||
elif event_type == 'system':
|
elif event_type == 'system':
|
||||||
subtype = event_data.get('subtype', '')
|
subtype = event_data.get('subtype', '')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue