Files
fckbot/bots/handlers/message_handler.py
Markov Andrey 63900feece Update 86 files
- /rag/commands/expert.py
- /core/services/__init__.py
- /core/services/embedding_service.py
- /core/services/file_service.py
- /core/services/giga_client.py
- /core/services/kb_service.py
- /core/services/postgres_service.py
- /core/services/qdrant_service.py
- /core/services/reranker_service.py
- /core/functions/__init__.py
- /core/functions/check_consistency.py
- /core/functions/check_spelling.py
- /core/functions/critique_answer.py
- /core/functions/expand_query.py
- /core/functions/extract_metrics.py
- /core/functions/file_processor.py
- /core/functions/generate_document.py
- /core/functions/intent_classify.py
- /core/functions/rerank_context.py
- /core/functions/summarize_document.py
- /core/utils/__init__.py
- /core/utils/arg_parser.py
- /core/utils/config_loader.py
- /core/utils/layout_converter.py
- /core/utils/logger.py
- /core/utils/text_utils.py
- /core/utils/web_utils.py
- /bots/commands/__init__.py
- /bots/commands/base.py
- /bots/commands/create.py
- /core/commands/global_remove.py
- /core/commands/help.py
- /core/commands/info.py
- /core/commands/kb.py
- /core/commands/learn.py
- /core/commands/other.py
- /core/commands/registry.py
- /core/commands/stats.py
- /core/commands/template.py
- /core/handlers/__init__.py
- /core/handlers/message_handler.py
- /core/workers/__init__.py
- /core/workers/indexing_worker.py
- /core/xmpp/__init__.py
- /core/xmpp/client.py
- /bots/commands/global_remove.py
- /bots/commands/help.py
- /bots/commands/info.py
- /bots/commands/kb.py
- /bots/commands/registry.py
- /bots/commands/other.py
- /bots/commands/template.py
- /bots/commands/learn.py
- /bots/commands/stats.py
- /bots/handlers/message_handler.py
- /bots/handlers/__init__.py
- /bots/workers/__init__.py
- /bots/workers/indexing_worker.py
- /bots/xmpp/__init__.py
- /bots/xmpp/client.py
- /rag/services/qdrant_service.py
- /rag/services/giga_client.py
- /rag/services/embedding_service.py
- /rag/services/reranker_service.py
- /rag/services/__init__.py
- /rag/services/file_service.py
- /rag/services/postgres_service.py
- /rag/services/kb_service.py
- /rag/functions/check_spelling.py
- /rag/functions/__init__.py
- /rag/functions/extract_metrics.py
- /rag/functions/generate_document.py
- /rag/functions/expand_query.py
- /rag/functions/critique_answer.py
- /rag/functions/file_processor.py
- /rag/functions/check_consistency.py
- /rag/functions/summarize_document.py
- /rag/functions/rerank_context.py
- /rag/functions/intent_classify.py
- /rag/utils/__init__.py
- /rag/utils/layout_converter.py
- /rag/utils/text_utils.py
- /rag/utils/arg_parser.py
- /rag/utils/config_loader.py
- /rag/utils/logger.py
- /rag/utils/web_utils.py
2026-06-30 10:33:28 +00:00

241 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Обработчик входящих XMPP сообщений (личных и групповых).
Использует RAGClient для отправки запросов на RAG-сервер.
ИСТОРИЯ НЕ ПЕРЕДАЁТСЯ сервер хранит её самостоятельно.
"""
import logging
import re
import asyncio
from core.utils.layout_converter import convert_layout
from core.commands.registry import suggest_command
logger = logging.getLogger(__name__)
class MessageHandler:
"""
Хендлер, привязанный к Bot. Вызывается при событии "message".
"""
def __init__(self, bot):
self.bot = bot
async def _wait_for_queue(self):
"""Ожидает создания очереди индексации (защита от гонки)."""
while self.bot.indexing_queue is None:
logger.debug("Очередь индексации ещё не создана, ожидание...")
await asyncio.sleep(0.5)
async def handle(self, msg):
"""
Основная точка входа для всех сообщений.
"""
# Определяем тип: личный чат (chat/normal) или групповой (groupchat)
if msg['type'] not in ('chat', 'normal', 'groupchat'):
return
user_jid = msg['from'].bare
# Не обрабатываем сообщения от самого бота
if msg['type'] == 'groupchat':
if msg['from'].resource == self.bot.boundjid.user:
return
else:
if msg['from'] == self.bot.boundjid:
return
# Извлекаем room_jid для групповых чатов (None для личных)
room_jid = msg['from'].bare if msg['type'] == 'groupchat' else None
# Проверка OMEMO (сквозное шифрование не поддерживается)
if self.bot.is_message_encrypted(msg):
reply = msg.reply(
"🔒 Я получил зашифрованное сообщение (OMEMO), но не могу его расшифровать. "
"Пожалуйста, отключите сквозное шифрование."
)
if reply:
reply.send()
return
# Извлечение тела сообщения и URL файла (из OOB)
raw_body = msg['body'].strip() if msg['body'] else ""
file_url = msg['oob']['url'] if msg['oob'] else None
# Если OOB отсутствует, пытаемся извлечь URL из тела сообщения
if not file_url and raw_body:
url_match = re.search(r'https?://[^\s<>"\'\)\]]+', raw_body)
if url_match:
file_url = url_match.group(0)
logger.info(f"Извлечена ссылка из тела сообщения (fallback OOB): {file_url}")
if not raw_body and not file_url:
return
# Для групповых чатов проверяем, упомянут ли бот
if msg['type'] == 'groupchat':
is_command = raw_body.startswith(('!', '/'))
has_file = file_url is not None
if not (is_command or has_file):
if not self._is_mentioned(msg, raw_body):
return
# Исправление раскладки клавиатуры (русская ↔ английская)
body = convert_layout(raw_body)
if body != raw_body:
reply = msg.reply(f"🔄 Исправлено: {body}")
if reply:
reply.send()
# Метрика: инкремент счётчика сообщений
self.bot.metrics_handler.increment("messages_total")
logger.info(f"Сообщение от {user_jid} (room={room_jid}): {body[:50]}...")
# ---- ИСТОРИЯ БОЛЬШЕ НЕ СОХРАНЯЕТСЯ ЛОКАЛЬНО ----
# Вся история хранится на сервере. Клиент не занимается этим.
# Обработка команд (начинаются с ! или /)
if body.startswith(('!', '/')):
await self._handle_command(msg, body, user_jid, room_jid)
return
# Режим обучения (текст без файла) только в личных чатах
mode_info = self.bot.learning_mode.get(user_jid)
if mode_info and not file_url and room_jid is None:
if isinstance(mode_info, tuple):
mode, saved_room = mode_info
else:
mode = mode_info
saved_room = None
await self._handle_learning_text(msg, user_jid, mode, body, room_jid)
return
# Обработка файла (если есть URL)
is_command = body.startswith(('!', '/'))
is_surgical_keyword = any(kw in body.lower() for kw in self.bot.config.surgical_keywords)
if file_url and not (is_command or is_surgical_keyword):
await self.bot.file_handler.handle_file(msg, file_url, user_jid, room_jid)
return
# ---- RAG-ЗАПРОС (БЕЗ ИСТОРИИ) ----
# История НЕ ПЕРЕДАЁТСЯ в запросе. Сервер сам её получит из БД.
key = (user_jid, room_jid) if room_jid is not None else user_jid
result = await self.bot.rag_client.query(
query=body,
user_jid=user_jid,
room_jid=room_jid,
prompts=self.bot.prompts,
last_file_path=self.bot._last_file_path.get(key),
last_file_text=self.bot.last_file_texts.get(user_jid)
)
answer = result.get('answer', '⚠️ Не удалось получить ответ.')
# Если в ответе есть файловые теги, обрабатываем их
if self.bot.config.surgical_replace and any(tag in answer for tag in ("[SURGICAL_REPLACE]", "[REWRITE]", "[FILE]")):
await self.bot.handle_ai_file_request(msg, answer, user_jid, room_jid, body=body)
else:
reply = msg.reply(answer)
if reply:
reply.send()
else:
logger.error("Не удалось отправить ответ RAG (без тегов)")
# ---- ИСТОРИЯ БОЛЬШЕ НЕ СОХРАНЯЕТСЯ ЛОКАЛЬНО ----
# await self.bot.db.add_history(...) # УБРАНО!
# ==========================================================
# Вспомогательные методы
# ==========================================================
def _is_mentioned(self, msg, body: str) -> bool:
"""
Проверяет, упомянут ли бот в групповом чате.
"""
my_nick = self.bot.plugin['xep_0045'].our_nicks.get(msg['from'].bare, self.bot.boundjid.user)
mention_jid = self.bot.boundjid.user.lower()
mention_word = self.bot.config.mention_keyword.lower()
return (mention_jid in body.lower() or
my_nick.lower() in body.lower() or
mention_word in body.lower()) or body.startswith(('!', '/'))
async def _handle_command(self, msg, body: str, user_jid: str, room_jid: str):
"""Обрабатывает команды (начинаются с ! или /)."""
cmd_name = body.split()[0].lower()
if cmd_name in self.bot.commands:
await self.bot.commands[cmd_name].execute(msg, body, user_jid, room_jid)
else:
suggestion = suggest_command(cmd_name, self.bot._command_names)
if suggestion:
reply = msg.reply(
f"❓ Неизвестная команда. Может быть, вы имели в виду `{suggestion}`? "
f"Напишите `!help` для списка команд."
)
if reply:
reply.send()
else:
reply = msg.reply("❓ Неизвестная команда. Напишите `!help` для списка команд.")
if reply:
reply.send()
async def _handle_learning_text(self, msg, user_jid: str, mode: str, body: str, room_jid: str):
"""
Обрабатывает текстовые сообщения в режиме обучения (только в личных чатах).
Поддерживает ссылки (URL) с флагами --recursive, --depth, --max.
"""
is_global = (mode == 'global')
title = None
# Парсим --title "Название" (если есть)
match = re.search(r'--title "([^"]+)"', body)
if match:
title = match.group(1)
body = re.sub(r'--title "[^"]+"', '', body).strip()
# Проверяем, является ли тело ссылкой
if body.startswith(('http://', 'https://')):
parts = body.split(maxsplit=1)
url = parts[0]
flags_str = parts[1] if len(parts) > 1 else ""
recursive = '--recursive' in flags_str or '-r' in flags_str
depth = 1
max_pages = self.bot.config.ws_max_pages
depth_match = re.search(r'--depth\s+(\d+)', flags_str)
if depth_match:
depth = int(depth_match.group(1))
max_match = re.search(r'--max\s+(\d+)', flags_str)
if max_match:
max_pages = int(max_match.group(1))
target_room = None
if mode == 'room' and room_jid is not None:
target_room = room_jid
# Создаём задачу для фонового индексатора
task = {
"type": "url",
"user_jid": user_jid,
"room_jid": target_room,
"url": url,
"title": title,
"is_global": is_global,
"recursive": recursive,
"depth": depth,
"max_pages": max_pages,
}
await self._wait_for_queue()
await self.bot.indexing_queue.put(task)
scope = "глобальную" if is_global else ("комнатную" if target_room else "личную")
reply = msg.reply(f"🌐 Ссылка '{url}' принята в обработку для {scope} БЗ.")
if reply:
reply.send()
return
# Если не ссылка сообщаем, что текст не индексируется
reply = msg.reply("В режиме обучения можно отправлять файлы или ссылки. Текст не индексируется.")
if reply:
reply.send()