diff --git a/core/functions/file_processor.py b/core/functions/file_processor.py new file mode 100644 index 0000000..df4da93 --- /dev/null +++ b/core/functions/file_processor.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +""" +Обработка файлов: отправка пользователю через HTTP Upload и хирургическая замена в DOCX. +Функции: send_file_to_user, handle_ai_file_request. + +АДАПТАЦИЯ: эти функции уже работают через HTTP (загружают файлы в ejabberd). +Они не требуют изменений, так как работают с локальными файлами. +Но я добавлю комментарии для ясности. +""" + +import asyncio +import os +import re +import shutil +import time +import random +import string +import logging + +logger = logging.getLogger(__name__) + + +async def send_file_to_user(bot, msg, file_path, prefix="", suffix=None): + """ + Отправляет файл пользователю через HTTP Upload (XEP-0363). + + Аргументы: + bot: экземпляр бота + msg: объект XMPP-сообщения + file_path: путь к файлу на диске + prefix: текст перед ссылкой + suffix: суффикс для имени файла + + Возвращает: + bool: True при успехе, False при ошибке + """ + if not os.path.exists(file_path): + logger.error(f"Файл не найден: {file_path}") + return False + + original_name = os.path.basename(file_path) + name, ext = os.path.splitext(original_name) + + # Санитизация суффикса (только буквы, цифры, подчёркивание, дефис) + if suffix: + suffix = re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9_-]', '', suffix) + suffix = suffix.strip('.') + if not suffix or suffix == '_': + suffix = '_sanitized' + if not suffix.startswith('_'): + suffix = '_' + suffix + new_name = f"{name}{suffix}{ext}" + new_path = os.path.join(os.path.dirname(file_path), new_name) + os.rename(file_path, new_path) + file_path = new_path + + try: + # Загружаем файл через XEP-0363 (HTTP Upload) + result = await bot.plugin['xep_0363'].upload_file(file_path) + if isinstance(result, dict): + file_url = result.get('get_url', result.get('url', str(result))) + else: + file_url = str(result) + except Exception as e: + logger.error(f"Ошибка загрузки файла через XEP-0363: {e}", exc_info=True) + error_reply = msg.reply("❌ Не удалось загрузить файл. Проверьте настройки HTTP Upload.") + if error_reply: + await error_reply.send() + return False + + reply = msg.reply(f"{prefix}\n📄 {file_url}") + if reply: + reply.send() + else: + logger.error("Не удалось создать reply для ссылки на файл") + return True + + +async def handle_ai_file_request(bot, msg, ai_response, user_jid, room_jid=None, body=None, suffix=None): + """ + Обрабатывает теги [SURGICAL_REPLACE], [REWRITE], [FILE] из ответа AI. + + Поддерживает три режима: + - SURGICAL: замена текста в существующем DOCX + - REWRITE: создание нового DOCX из текста + - FILE: создание нового DOCX (общий случай) + """ + key = (user_jid, room_jid) if room_jid is not None else user_jid + + surgical_match = re.search(r'\[SURGICAL_REPLACE\](.*?)\[/SURGICAL_REPLACE\]', ai_response, re.DOTALL) + rewrite_match = re.search(r'\[REWRITE\](.*?)\[/REWRITE\]', ai_response, re.DOTALL) + file_match = re.search(r'\[FILE\](.*?)\[/FILE\]', ai_response, re.DOTALL) + + # ---- SURGICAL_REPLACE ---- + if surgical_match: + content = surgical_match.group(1).strip() + replacements = {} + for line in content.split('\n'): + line = line.strip() + if '|||' in line: + old, new = line.split('|||', 1) + replacements[old.strip()] = new.strip() + if not replacements: + reply = msg.reply("⚠️ Не найдено пар 'старое ||| новое' в теге SURGICAL_REPLACE.") + if reply: + reply.send() + return + + original_path = bot._last_file_path.get(key) + if not original_path or not os.path.exists(original_path): + reply = msg.reply("❌ Нет загруженного документа для замены. Сначала отправьте файл (без !learn).") + if reply: + reply.send() + return + + new_path = await asyncio.to_thread(bot.files.surgical_replace, original_path, replacements) + if new_path: + await send_file_to_user(bot, msg, new_path, prefix="✅ Замена выполнена:", suffix="surgical") + try: + os.unlink(new_path) + except Exception as e: + logger.warning(f"Не удалось удалить временный файл {new_path}: {e}") + else: + reply = msg.reply("❌ Ошибка при хирургической замене. Проверьте формат документа.") + if reply: + reply.send() + return + + # ---- REWRITE ---- + if rewrite_match: + content = rewrite_match.group(1).strip() + title = "Сгенерированный документ" + title_match = re.search(r'\[TITLE\](.*?)\[/TITLE\]', ai_response, re.DOTALL) + if title_match: + title = title_match.group(1).strip() + new_path = await asyncio.to_thread(bot.files.create_docx, content, title, user_jid, mode="REWRITE") + if new_path: + await send_file_to_user(bot, msg, new_path, prefix="📄 Документ создан:", suffix="rewrite") + try: + os.unlink(new_path) + except Exception as e: + logger.warning(f"Не удалось удалить временный файл {new_path}: {e}") + else: + reply = msg.reply("❌ Не удалось создать документ.") + if reply: + reply.send() + return + + # ---- FILE ---- + if file_match: + content = file_match.group(1).strip() + title = "Файл" + title_match = re.search(r'\[TITLE\](.*?)\[/TITLE\]', ai_response, re.DOTALL) + if title_match: + title = title_match.group(1).strip() + new_path = await asyncio.to_thread(bot.files.create_docx, content, title, user_jid, mode="FILE") + if new_path: + await send_file_to_user(bot, msg, new_path, prefix="📄 Файл создан:", suffix="file") + try: + os.unlink(new_path) + except Exception as e: + logger.warning(f"Не удалось удалить временный файл {new_path}: {e}") + else: + reply = msg.reply("❌ Не удалось создать файл.") + if reply: + reply.send() + return + + logger.debug(f"Ответ AI не содержит тегов файлов: {ai_response[:200]}") \ No newline at end of file