v0.8.0: Исправление OAuth + память файлов + совместимость PTB 20.7+
Основные изменения: - Исправлена рекурсия в qwen_oauth.py (RecursionError при проверке токенов) - Добавлена проверка force=True в _load_credentials() - Бот теперь запоминает загруженные файлы в памяти ИИ (ai_chat_history + vector_memory) - Сохранение полного абсолютного пути к файлу для корректной работы ИИ - Исправлена совместимость с python-telegram-bot 20.7+: - MAX_FILE_SIZE_DOWNLOAD → локальная константа - filters.STICKER → filters.Sticker.ALL - Обновлена версия бота до 0.8.0 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
c767f3d50b
commit
09fb020745
6
bot.py
6
bot.py
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Telegram CLI Bot - бот для выполнения CLI команд с многоуровневым меню.
|
||||
Версия: 0.7.4 (Универсальный интерфейс AI-провайдеров с поддержкой инструментов)
|
||||
Версия: 0.8.0 (Исправление OAuth + память файлов + совместимость PTB 20.7+)
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -2413,6 +2413,10 @@ def main():
|
|||
from bot.handlers.ai_presets import register_ai_preset_handlers
|
||||
register_ai_preset_handlers(application)
|
||||
|
||||
# Обработчики файлов
|
||||
from bot.handlers.files import register_file_handlers
|
||||
register_file_handlers(application)
|
||||
|
||||
application.add_handler(CallbackQueryHandler(menu_callback))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message))
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,507 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Обработчики файлов для Telegram бота.
|
||||
|
||||
Функционал:
|
||||
- Прием файлов от пользователя с сохранением в uploads/YYYY-MM-DD/filename
|
||||
- Команда /get filename — отправка файла пользователю
|
||||
- Команда /files [date] — список файлов за дату (сегодня по умолчанию)
|
||||
- Проверка размера файлов (max 20MB)
|
||||
- Логирование операций в uploads/files.log
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import CommandHandler, MessageHandler, filters, ContextTypes
|
||||
|
||||
from bot.config import state_manager
|
||||
from bot.utils.decorators import check_access
|
||||
from vector_memory import save_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ============================================================================
|
||||
# КОНСТАНТЫ
|
||||
# ============================================================================
|
||||
|
||||
# Максимальный размер файла: 20MB (ограничение Telegram)
|
||||
MAX_FILE_SIZE_DOWNLOAD = 20 * 1024 * 1024 # 20 MB в байтах
|
||||
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB в байтах
|
||||
|
||||
# Базовая директория для загрузок
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
UPLOADS_DIR = BASE_DIR / "uploads"
|
||||
|
||||
# Файл лога операций
|
||||
LOG_FILE = UPLOADS_DIR / "files.log"
|
||||
|
||||
# ============================================================================
|
||||
# ЛОГИРОВАНИЕ
|
||||
# ============================================================================
|
||||
|
||||
def log_operation(operation: str, user_id: int, username: str, details: str = ""):
|
||||
"""Логирование операций с файлами."""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_entry = f"[{timestamp}] {operation} | user_id={user_id} | username={username}"
|
||||
if details:
|
||||
log_entry += f" | {details}"
|
||||
|
||||
try:
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
f.write(log_entry + "\n")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка записи в лог файлов: {e}")
|
||||
|
||||
|
||||
def setup_file_logging():
|
||||
"""Настройка отдельного логгера для операций с файлами."""
|
||||
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
"[%(asctime)s] %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
))
|
||||
|
||||
file_logger = logging.getLogger("files")
|
||||
file_logger.addHandler(file_handler)
|
||||
file_logger.setLevel(logging.INFO)
|
||||
|
||||
return file_logger
|
||||
|
||||
|
||||
file_logger = setup_file_logging()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
|
||||
# ============================================================================
|
||||
|
||||
def get_date_dir(date: Optional[datetime] = None) -> Path:
|
||||
"""Получить директорию для указанной даты."""
|
||||
if date is None:
|
||||
date = datetime.now()
|
||||
|
||||
date_str = date.strftime("%Y-%m-%d")
|
||||
date_dir = UPLOADS_DIR / date_str
|
||||
|
||||
# Создаем директорию если нет
|
||||
date_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return date_dir
|
||||
|
||||
|
||||
def get_file_path(filename: str, date: Optional[datetime] = None) -> Path:
|
||||
"""Получить полный путь к файлу."""
|
||||
date_dir = get_date_dir(date)
|
||||
return date_dir / filename
|
||||
|
||||
|
||||
def file_exists(filename: str, date: Optional[datetime] = None) -> bool:
|
||||
"""Проверить существование файла."""
|
||||
file_path = get_file_path(filename, date)
|
||||
return file_path.exists()
|
||||
|
||||
|
||||
def list_files_for_date(date: Optional[datetime] = None) -> list:
|
||||
"""Получить список файлов за указанную дату."""
|
||||
date_dir = get_date_dir(date)
|
||||
|
||||
if not date_dir.exists():
|
||||
return []
|
||||
|
||||
files = []
|
||||
for item in date_dir.iterdir():
|
||||
if item.is_file() and item.name != "files.log":
|
||||
stat = item.stat()
|
||||
files.append({
|
||||
"name": item.name,
|
||||
"size": stat.st_size,
|
||||
"path": str(item),
|
||||
"modified": datetime.fromtimestamp(stat.st_mtime)
|
||||
})
|
||||
|
||||
# Сортируем по имени
|
||||
files.sort(key=lambda x: x["name"])
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""Форматировать размер файла в человекочитаемом виде."""
|
||||
for unit in ["B", "KB", "MB", "GB"]:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes:.1f} {unit}"
|
||||
size_bytes /= 1024
|
||||
return f"{size_bytes:.1f} TB"
|
||||
|
||||
|
||||
def parse_date_string(date_str: Optional[str]) -> Optional[datetime]:
|
||||
"""Распарсить строку даты."""
|
||||
if not date_str:
|
||||
return None
|
||||
|
||||
# Поддерживаемые форматы
|
||||
formats = [
|
||||
"%Y-%m-%d", # 2024-01-15
|
||||
"%d.%m.%Y", # 15.01.2024
|
||||
"%d-%m-%Y", # 15-01-2024
|
||||
"%Y%m%d", # 20240115
|
||||
]
|
||||
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(date_str, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Относительные даты
|
||||
if date_str.lower() in ["yesterday", "вчера"]:
|
||||
return datetime.now() - timedelta(days=1)
|
||||
elif date_str.lower() in ["today", "сегодня"]:
|
||||
return datetime.now()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ОБРАБОТЧИК ФАЙЛОВ
|
||||
# ============================================================================
|
||||
|
||||
@check_access
|
||||
async def handle_file_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обработка входящих файлов от пользователя."""
|
||||
user_id = update.effective_user.id
|
||||
username = update.effective_user.username or str(user_id)
|
||||
|
||||
# Определяем тип файла и получаем файл
|
||||
file = None
|
||||
file_type = None
|
||||
|
||||
if update.message.document:
|
||||
file = update.message.document
|
||||
file_type = "document"
|
||||
elif update.message.photo:
|
||||
# Берем фото в максимальном разрешении (последнее в списке)
|
||||
file = update.message.photo[-1]
|
||||
file_type = "photo"
|
||||
elif update.message.audio:
|
||||
file = update.message.audio
|
||||
file_type = "audio"
|
||||
elif update.message.voice:
|
||||
file = update.message.voice
|
||||
file_type = "voice"
|
||||
elif update.message.video:
|
||||
file = update.message.video
|
||||
file_type = "video"
|
||||
elif update.message.video_note:
|
||||
file = update.message.video_note
|
||||
file_type = "video_note"
|
||||
elif update.message.sticker:
|
||||
file = update.message.sticker
|
||||
file_type = "sticker"
|
||||
elif update.message.animation:
|
||||
file = update.message.animation
|
||||
file_type = "animation"
|
||||
|
||||
if not file:
|
||||
return
|
||||
|
||||
# Получаем информацию о файле
|
||||
file_name = file.file_name or f"file_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
file_size = file.file_size or 0
|
||||
|
||||
logger.info(f"Получен файл: {file_name}, размер: {file_size}, тип: {file_type}")
|
||||
|
||||
# Проверка размера файла
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
log_operation("REJECTED_SIZE", user_id, username, f"file={file_name}, size={file_size}")
|
||||
await update.message.reply_text(
|
||||
f"❌ **Файл слишком большой**\n\n"
|
||||
f"Максимальный размер: {format_file_size(MAX_FILE_SIZE)}\n"
|
||||
f"Размер файла: {format_file_size(file_size)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
# Создаем директорию для сегодняшней даты
|
||||
date_dir = get_date_dir()
|
||||
file_path = date_dir / file_name
|
||||
|
||||
# Обработка дубликатов имен
|
||||
if file_path.exists():
|
||||
base, ext = os.path.splitext(file_name)
|
||||
counter = 1
|
||||
while file_path.exists():
|
||||
new_name = f"{base}_{counter}{ext}"
|
||||
file_path = date_dir / new_name
|
||||
counter += 1
|
||||
file_name = file_path.name
|
||||
|
||||
logger.info(f"Сохранение файла: {file_path}")
|
||||
|
||||
# Отправляем статус
|
||||
status_msg = await update.message.reply_text(
|
||||
f"⏳ **Загрузка файла...**\n\n"
|
||||
f"📁 {file_name}\n"
|
||||
f"📊 Размер: {format_file_size(file_size)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
try:
|
||||
# Скачиваем файл
|
||||
telegram_file = await context.bot.get_file(file.file_id)
|
||||
await telegram_file.download_to_drive(file_path)
|
||||
|
||||
# Логирование
|
||||
log_operation("UPLOAD", user_id, username, f"file={file_name}, size={file_size}, path={file_path}")
|
||||
file_logger.info(f"UPLOAD: user={username}, file={file_name}, size={file_size}")
|
||||
|
||||
# === СОХРАНЕНИЕ В ПАМЯТЬ ИИ ===
|
||||
# Добавляем информацию о файле в историю диалога и векторную память
|
||||
state = state_manager.get(user_id)
|
||||
if state:
|
||||
# Формируем сообщение о файле с ПОЛНЫМ абсолютным путём!
|
||||
# Это важно чтобы ИИ правильно понимал где файл
|
||||
absolute_path = str(file_path.absolute())
|
||||
file_info = f"Пользователь загрузил файл: {file_name} (тип: {file_type}, размер: {format_file_size(file_size)}, полный путь: {absolute_path})"
|
||||
|
||||
# Добавляем в историю диалога
|
||||
state.ai_chat_history.append(f"User: {file_info}")
|
||||
|
||||
# Сохраняем в векторную память
|
||||
save_message(user_id, "user", file_info)
|
||||
|
||||
logger.info(f"Информация о файле сохранена в памяти ИИ: {file_name}, путь: {absolute_path}")
|
||||
# ===============================
|
||||
|
||||
# Обновляем статус
|
||||
await status_msg.edit_text(
|
||||
f"✅ **Файл сохранен!**\n\n"
|
||||
f"📁 **Имя:** `{file_name}`\n"
|
||||
f"📊 **Размер:** {format_file_size(file_size)}\n"
|
||||
f"📂 **Директория:** `{date_dir.name}/`\n"
|
||||
f"📅 **Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
|
||||
f"Используйте `/files` для просмотра всех файлов за сегодня.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Ошибка сохранения файла: {e}")
|
||||
log_operation("UPLOAD_ERROR", user_id, username, f"file={file_name}, error={str(e)}")
|
||||
|
||||
await status_msg.edit_text(
|
||||
f"❌ **Ошибка сохранения файла**\n\n"
|
||||
f"Файл: `{file_name}`\n"
|
||||
f"Ошибка: `{str(e)}`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# КОМАНДА /GET - ПОЛУЧЕНИЕ ФАЙЛА
|
||||
# ============================================================================
|
||||
|
||||
@check_access
|
||||
async def get_file_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Команда /get filename - отправка файла пользователю.
|
||||
|
||||
Использование:
|
||||
/get filename.txt - получить файл за сегодня
|
||||
/get filename.txt 2024-01-15 - получить файл за указанную дату
|
||||
"""
|
||||
user_id = update.effective_user.id
|
||||
username = update.effective_user.username or str(user_id)
|
||||
|
||||
if not context.args:
|
||||
await update.message.reply_text(
|
||||
"❌ **Использование:**\n\n"
|
||||
"`/get <имя_файла>` - получить файл за сегодня\n"
|
||||
"`/get <имя_файла> <дата>` - получить файл за указанную дату\n\n"
|
||||
"**Примеры:**\n"
|
||||
"`/get document.pdf`\n"
|
||||
"`/get photo.jpg 2024-01-15`\n"
|
||||
"`/get data.csv вчера`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
# Парсим аргументы
|
||||
filename = context.args[0]
|
||||
date_arg = context.args[1] if len(context.args) > 1 else None
|
||||
file_date = parse_date_string(date_arg)
|
||||
|
||||
# Проверяем существование файла
|
||||
file_path = get_file_path(filename, file_date)
|
||||
|
||||
if not file_path.exists():
|
||||
# Ищем файл во всех директориях за сегодня
|
||||
date_dir = get_date_dir(file_date)
|
||||
await update.message.reply_text(
|
||||
f"❌ **Файл не найден**\n\n"
|
||||
f"Файл: `{filename}`\n"
|
||||
f"Директория: `{date_dir.name}/`\n\n"
|
||||
f"Используйте `/files` для просмотра доступных файлов.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
log_operation("GET_NOT_FOUND", user_id, username, f"file={filename}, date={file_date}")
|
||||
return
|
||||
|
||||
# Проверка размера перед отправкой
|
||||
file_size = file_path.stat().st_size
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
await update.message.reply_text(
|
||||
f"❌ **Файл слишком большой для отправки**\n\n"
|
||||
f"Максимальный размер: {format_file_size(MAX_FILE_SIZE)}\n"
|
||||
f"Размер файла: {format_file_size(file_size)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
log_operation("GET_TOO_LARGE", user_id, username, f"file={filename}, size={file_size}")
|
||||
return
|
||||
|
||||
# Отправляем статус
|
||||
status_msg = await update.message.reply_text(
|
||||
f"⏳ **Отправка файла...**\n\n"
|
||||
f"📁 `{filename}`\n"
|
||||
f"📊 {format_file_size(file_size)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
try:
|
||||
# Отправляем файл
|
||||
with open(file_path, "rb") as f:
|
||||
await update.message.reply_document(
|
||||
document=f,
|
||||
filename=filename,
|
||||
caption=f"📁 **{filename}**\n📊 {format_file_size(file_size)}",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
# Логирование
|
||||
log_operation("GET", user_id, username, f"file={filename}, size={file_size}")
|
||||
file_logger.info(f"GET: user={username}, file={filename}, size={file_size}")
|
||||
|
||||
# Удаляем статус
|
||||
await status_msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Ошибка отправки файла: {e}")
|
||||
log_operation("GET_ERROR", user_id, username, f"file={filename}, error={str(e)}")
|
||||
|
||||
await status_msg.edit_text(
|
||||
f"❌ **Ошибка отправки файла**\n\n"
|
||||
f"Файл: `{filename}`\n"
|
||||
f"Ошибка: `{str(e)}`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# КОМАНДА /FILES - СПИСОК ФАЙЛОВ
|
||||
# ============================================================================
|
||||
|
||||
@check_access
|
||||
async def list_files_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Команда /files [date] - список файлов за указанную дату.
|
||||
|
||||
Использование:
|
||||
/files - список файлов за сегодня
|
||||
/files 2024-01-15 - список файлов за указанную дату
|
||||
/files вчера - список файлов за вчера
|
||||
"""
|
||||
user_id = update.effective_user.id
|
||||
username = update.effective_user.username or str(user_id)
|
||||
|
||||
# Парсим дату из аргументов
|
||||
date_arg = context.args[0] if context.args else None
|
||||
file_date = parse_date_string(date_arg)
|
||||
|
||||
if file_date is None and date_arg:
|
||||
await update.message.reply_text(
|
||||
f"❌ **Неверный формат даты:** `{date_arg}`\n\n"
|
||||
f"**Поддерживаемые форматы:**\n"
|
||||
f"• `2024-01-15` (YYYY-MM-DD)\n"
|
||||
f"• `15.01.2024` (DD.MM.YYYY)\n"
|
||||
f"• `сегодня` / `today`\n"
|
||||
f"• `вчера` / `yesterday`",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем список файлов
|
||||
files = list_files_for_date(file_date)
|
||||
|
||||
date_str = (file_date or datetime.now()).strftime("%Y-%m-%d")
|
||||
date_dir = get_date_dir(file_date)
|
||||
|
||||
if not files:
|
||||
await update.message.reply_text(
|
||||
f"📭 **Нет файлов за {date_str}**\n\n"
|
||||
f"Директория: `{date_dir.name}/`\n\n"
|
||||
f"Отправьте файл в чат чтобы сохранить его.",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
log_operation("LIST_EMPTY", user_id, username, f"date={date_str}")
|
||||
return
|
||||
|
||||
# Формируем вывод
|
||||
total_size = sum(f["size"] for f in files)
|
||||
|
||||
output = f"📁 **Файлы за {date_str}**\n\n"
|
||||
output += f"📂 Директория: `{date_dir.name}/`\n"
|
||||
output += f"📊 Всего файлов: {len(files)}\n"
|
||||
output += f"💾 Общий размер: {format_file_size(total_size)}\n\n"
|
||||
|
||||
for i, file_info in enumerate(files, 1):
|
||||
name = file_info["name"]
|
||||
size = format_file_size(file_info["size"])
|
||||
modified = file_info["modified"].strftime("%H:%M")
|
||||
|
||||
# Обрезаем длинные имена
|
||||
if len(name) > 40:
|
||||
display_name = name[:37] + "..."
|
||||
else:
|
||||
display_name = name
|
||||
|
||||
output += f"{i}. `{display_name}` - {size} ({modified})\n"
|
||||
|
||||
output += f"\n💡 **Использование:**\n"
|
||||
output += f"`/get <имя_файла>` - скачать файл\n"
|
||||
output += f"`/get <имя_файла> {date_str}` - скачать файл за дату"
|
||||
|
||||
await update.message.reply_text(output, parse_mode="Markdown")
|
||||
log_operation("LIST", user_id, username, f"date={date_str}, count={len(files)}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# РЕГИСТРАЦИЯ ХЕНДЛЕРОВ
|
||||
# ============================================================================
|
||||
|
||||
def register_file_handlers(application):
|
||||
"""Регистрация обработчиков файлов в приложении."""
|
||||
# Обработчик входящих файлов (документы, фото, аудио, видео и т.д.)
|
||||
application.add_handler(MessageHandler(
|
||||
filters.Document.ALL |
|
||||
filters.PHOTO |
|
||||
filters.AUDIO |
|
||||
filters.VOICE |
|
||||
filters.VIDEO |
|
||||
filters.VIDEO_NOTE |
|
||||
filters.Sticker.ALL |
|
||||
filters.ANIMATION,
|
||||
handle_file_message
|
||||
))
|
||||
|
||||
# Команда /get - получение файла
|
||||
application.add_handler(CommandHandler("get", get_file_command))
|
||||
|
||||
# Команда /files - список файлов
|
||||
application.add_handler(CommandHandler("files", list_files_command))
|
||||
|
||||
logger.info("Обработчики файлов зарегистрированы")
|
||||
|
|
@ -71,25 +71,67 @@ class DeviceAuthorizationResponse:
|
|||
class QwenOAuthClient:
|
||||
"""Qwen OAuth 2.0 клиент."""
|
||||
|
||||
# Как в qwen-code: 30 секунд буфер + 5 секунд интервал проверки
|
||||
TOKEN_REFRESH_BUFFER_MS = 30 * 1000
|
||||
CACHE_CHECK_INTERVAL_MS = 5 * 1000
|
||||
|
||||
def __init__(self, credentials_path: Optional[Path] = None):
|
||||
self.credentials_path = credentials_path or QWEN_CREDENTIALS_FILE
|
||||
self._credentials: Optional[QwenCredentials] = None
|
||||
self._file_mod_time: float = 0 # Время последней модификации файла
|
||||
self._last_check_time: float = 0 # Время последней проверки
|
||||
self._load_credentials()
|
||||
|
||||
def _load_credentials(self) -> None:
|
||||
"""Загрузить токены из файла."""
|
||||
def _get_file_mod_time(self) -> float:
|
||||
"""Получить время модификации файла токенов."""
|
||||
try:
|
||||
if self.credentials_path.exists():
|
||||
return self.credentials_path.stat().st_mtime
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _load_credentials(self, force: bool = False) -> None:
|
||||
"""
|
||||
Загрузить токены из файла.
|
||||
|
||||
Args:
|
||||
force: Принудительная перезагрузка даже если файл не изменился
|
||||
"""
|
||||
current_mod_time = self._get_file_mod_time()
|
||||
now = time.time()
|
||||
|
||||
# Если force=True — пропускаем все проверки и загружаем всегда
|
||||
if not force:
|
||||
# Проверяем не слишком ли часто проверяем (как в qwen-code)
|
||||
if (now - self._last_check_time) < (self.CACHE_CHECK_INTERVAL_MS / 1000):
|
||||
return
|
||||
|
||||
# Проверяем изменился ли файл (как в qwen-code)
|
||||
if current_mod_time <= self._file_mod_time:
|
||||
# Файл не изменился — используем кэш
|
||||
return
|
||||
|
||||
self._last_check_time = now
|
||||
|
||||
logger.debug(f"Загрузка токенов из {self.credentials_path} (mod_time={current_mod_time}, force={force})")
|
||||
if self.credentials_path.exists():
|
||||
try:
|
||||
with open(self.credentials_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
self._credentials = QwenCredentials(**data)
|
||||
self._file_mod_time = current_mod_time
|
||||
logger.info(f"Токены загружены из {self.credentials_path}")
|
||||
logger.debug(f"Access token: {self._credentials.access_token[:20] if self._credentials.access_token else 'None'}..., expiry: {self._credentials.expiry_date}")
|
||||
# НЕ вызываем has_valid_token() здесь — это вызывает рекурсию!
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки токенов: {e}")
|
||||
self._credentials = None
|
||||
self._file_mod_time = 0
|
||||
else:
|
||||
logger.debug("Файл с токенами не найден")
|
||||
self._credentials = None
|
||||
self._file_mod_time = 0
|
||||
|
||||
# Загружаем code_verifier из device_code.json если есть
|
||||
device_code_file = QWEN_CONFIG_DIR / 'device_code.json'
|
||||
|
|
@ -116,6 +158,9 @@ class QwenOAuthClient:
|
|||
|
||||
def has_valid_token(self) -> bool:
|
||||
"""Проверка наличия валидного токена."""
|
||||
# Принудительно перезагружаем токены из файла
|
||||
# Это нужно потому что токены могут быть обновлены другим процессом (qwen-code CLI)
|
||||
self._load_credentials(force=True)
|
||||
return self._credentials is not None and not self._credentials.is_expired()
|
||||
|
||||
async def get_access_token(self) -> Optional[str]:
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ async def read_ssh_output(process: asyncssh.SSHClientProcess, timeout: float = 2
|
|||
is_done = False
|
||||
|
||||
try:
|
||||
# Используем readany() для чтения доступных данных
|
||||
# Используем read() для чтения доступных данных
|
||||
while True:
|
||||
try:
|
||||
# readany() читает любые доступные данные
|
||||
data = await asyncio.wait_for(process.stdout.readany(), timeout=timeout)
|
||||
# read() читает данные до EOF
|
||||
data = await asyncio.wait_for(process.stdout.read(), timeout=timeout)
|
||||
if data:
|
||||
if isinstance(data, bytes):
|
||||
output += data.decode('utf-8', errors='replace')
|
||||
|
|
@ -92,7 +92,7 @@ async def read_ssh_output(process: asyncssh.SSHClientProcess, timeout: float = 2
|
|||
try:
|
||||
while True:
|
||||
try:
|
||||
data = await asyncio.wait_for(process.stderr.readany(), timeout=0.5)
|
||||
data = await asyncio.wait_for(process.stderr.read(), timeout=0.5)
|
||||
if data:
|
||||
if isinstance(data, bytes):
|
||||
error_output += data.decode('utf-8', errors='replace')
|
||||
|
|
|
|||
|
|
@ -18,13 +18,17 @@ from datetime import datetime, timedelta
|
|||
from typing import Optional, Dict, Callable, Any, List, Union
|
||||
from enum import Enum
|
||||
|
||||
# Импортируем OAuth модуль
|
||||
# Импортируем OAuth модуль и константы
|
||||
from bot.utils.qwen_oauth import (
|
||||
get_authorization_url,
|
||||
check_authorization_complete,
|
||||
is_authorized,
|
||||
get_access_token,
|
||||
clear_authorization
|
||||
clear_authorization,
|
||||
QWEN_OAUTH_CLIENT_ID,
|
||||
QWEN_OAUTH_BASE_URL,
|
||||
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
||||
QWEN_OAUTH_TOKEN_ENDPOINT
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -352,6 +356,15 @@ class QwenCodeManager:
|
|||
session.output_buffer = ""
|
||||
|
||||
try:
|
||||
# ПРОВЕРКА: Проверяем авторизацию ПЕРЕД запуском qwen-code
|
||||
# Это предотвращает ошибку "No auth type is selected"
|
||||
if not is_authorized():
|
||||
logger.warning("Пользователь не авторизован в Qwen, получаем OAuth URL")
|
||||
oauth_url = await get_authorization_url()
|
||||
if oauth_url and session.on_oauth_url:
|
||||
await session.on_oauth_url(oauth_url)
|
||||
return "🔐 Требуется авторизация Qwen. Пожалуйста, пройдите по ссылке и вернитесь."
|
||||
|
||||
env = os.environ.copy()
|
||||
env["FORCE_COLOR"] = "0"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue