diff --git a/handlers/voice.py b/handlers/voice.py index ef6e18e..43033e0 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -4,6 +4,8 @@ import logging import asyncio import sqlite3 import subprocess +import urllib.request +import json from datetime import datetime from aiogram import Router, F, Bot from aiogram.types import Message, BufferedInputFile @@ -18,8 +20,55 @@ router = Router() WORKING_GEMINI_KEY = "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk" genai.configure(api_key=WORKING_GEMINI_KEY) +# Вебхук Битрикс24 холдинга (ecomsystems.bitrix24.ru) +B24_WEBHOOK = "https://ecomsystems.bitrix24.ru/rest/1424/yqt0a18d9qwf0l4r/" + +def query_bitrix24(action: str, task_id: int = 0, search_text: str = "") -> str: + """Инструмент для работы с Битрикс24 холдинга (ecomsystems.bitrix24.ru). + Александр Тырин: ID 1424, Илья Гавришин (Генеральный директор): ID 1. + Доступные действия (action): + - 'my_tasks': список активных задач Александра Тырина. + - 'search_tasks': поиск задач по ключевому слову search_text в названии. + - 'task_details': детальная информация по задаче с ID task_id. + - 'task_comments': все комментарии к задаче с ID task_id (включая комментарии гендира и юристов). + """ + logger.info(f"🏢 [Bitrix24 Query] action={action}, task_id={task_id}, search_text='{search_text}'") + try: + if action == "my_tasks": + url = f"{B24_WEBHOOK}tasks.task.list.json" + payload = json.dumps({ + "filter": {"RESPONSIBLE_ID": 1424, "!STATUS": 5}, + "select": ["ID", "TITLE", "STATUS", "CREATED_DATE", "DEADLINE"] + }).encode('utf-8') + elif action == "search_tasks": + url = f"{B24_WEBHOOK}tasks.task.list.json" + payload = json.dumps({ + "filter": {"%TITLE": search_text}, + "select": ["ID", "TITLE", "STATUS", "RESPONSIBLE_ID", "CREATED_BY"] + }).encode('utf-8') + elif action == "task_details": + url = f"{B24_WEBHOOK}tasks.task.get.json" + payload = json.dumps({ + "taskId": task_id, + "select": ["ID", "TITLE", "DESCRIPTION", "STATUS", "CREATED_BY", "RESPONSIBLE_ID"] + }).encode('utf-8') + elif action == "task_comments": + url = f"{B24_WEBHOOK}task.commentitem.getlist.json" + payload = json.dumps([task_id]).encode('utf-8') + else: + return f"Неизвестное действие: {action}" + + req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=15) as resp: + data = json.loads(resp.read().decode('utf-8')) + res = data.get("result", {}) + return json.dumps(res, ensure_ascii=False)[:3500] + except Exception as e: + logger.error(f"❌ Bitrix24 Error: {e}") + return f"Ошибка обращения к Битрикс24: {e}" + def execute_bash_command(command: str) -> str: - """Выполняет bash-команду на локальном Linux хосте (DockerServer) или по SSH на узлах сети (MacMini 192.168.10.167, Synology 192.168.10.105, MikroTik 192.168.10.215, NanoPi 192.168.10.1) и возвращает результат вывода.""" + """Выполняет bash-команду на хосте или по SSH и возвращает результат вывода.""" logger.info(f"⚡️ [Voice Agent Command] Executing bash: {command}") try: res = subprocess.run( @@ -65,21 +114,19 @@ def log_voice_to_db(user_id, username, msg_type, duration, text): async def text_to_speech_bytes(text: str) -> bytes: """Синтез качественного женского голосового ответа на русском через Edge TTS с глубоким тембром""" - # Если в тексте есть тег ..., озвучиваем именно его match = re.search(r"(.*?)", text, re.DOTALL | re.IGNORECASE) if match: speech_text = match.group(1).strip() else: - # Иначе берем выжимку до 350 символов speech_text = text[:350] if len(text) > 350 else text - # Очищаем от Markdown и спецсимволов для чистоты звучания + # Очищаем Markdown для чистого звука for ch in ["*", "#", "_", "`", "~", ">", "•", "🎯", "⚙️", "📋", "💡", "🗄️", "🚨", "⚠️", "✅", "❌"]: speech_text = speech_text.replace(ch, "") speech_text = re.sub(r"\[.*?\]\(.*?\)", "", speech_text).strip() if not speech_text: - speech_text = "Приняла, Александр. Всё структурировано и зафиксировано." + speech_text = "Приняла, Александр. Всё проанализировано и готово." communicate = edge_tts.Communicate(speech_text, "ru-RU-SvetlanaNeural", pitch="-8Hz", rate="-3%") out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3" @@ -92,7 +139,7 @@ async def text_to_speech_bytes(text: str) -> bytes: return audio_bytes def process_audio_with_gemini(audio_path: str) -> str: - """Прямое транскрибирование аудио через Gemini Multimodal API""" + """Прямое транскрибирование аудио через Gemini Multimodal Audio API""" try: logger.info(f"Выгрузка {audio_path} в Gemini Multimodal Audio API...") audio_file = genai.upload_file(path=audio_path) @@ -116,24 +163,31 @@ def process_audio_with_gemini(audio_path: str) -> str: return "" async def execute_antigravity_agent(user_text: str) -> str: - """Обработка команды и мыслей агентом Майа с систематизацией и правом исполнения bash-команд""" + """Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд""" system_instruction = ( - "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра. " + "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра с полными правами инженера. " "Александр обращается к тебе голосом в Телеграмме или надиктованной аудиозаписью с диктофона. " - "Твоя задача — быть идеальным личным инженером и аналитиком:\n\n" - "1. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи, мысли, список дел или сумбурный поток):\n" - " Разложи всё по четким блокам:\n" - " 🎯 Суть: краткая главная мысль в 1-2 предложениях.\n" - " 📋 Задачи к выполнению (Action Items): конкретные пункты с галочками [ ].\n" - " 💡 Идеи в копилку / Бэклог: новые задумки, архитектурные гипотезы.\n" - " 🗄️ Куда зафиксировать: в какую заметку Obsidian (HomeLab или Work) это стоит сохранить.\n\n" - "2. СИСТЕМНЫЕ КОМАНДЫ (если Александр просит проверить ресурсы, диски, сеть, контейнеры, запустить что-то):\n" - " ОБЯЗАТЕЛЬНО используй execute_bash_command! Выполни команду, проанализируй вывод и дай четкий отчет.\n" - " Узлы сети: NanoPi R3S (root@192.168.10.1), Synology (matrixhasyou@192.168.10.105), MacMini (matrixhasyou@192.168.10.167), MikroTik (matrixhasyou@192.168.10.215).\n\n" - "3. ГОЛОСОВОЙ ОТКЛИК ДЛЯ ОЗВУЧКИ:\n" + "Твои возможности:\n\n" + "1. РАБОЧИЕ ЗАДАЧИ В БИТРИКС24:\n" + " Если Александр просит проверить задачи, комментарии руководства, поручения в Битрикс24 — " + " ОБЯЗАТЕЛЬНО используй функцию query_bitrix24!\n" + " - Поиск задачи по ключевым словам: query_bitrix24(action='search_tasks', search_text='...')\n" + " - Список открытых задач Александра (ID 1424): query_bitrix24(action='my_tasks')\n" + " - Подробности задачи: query_bitrix24(action='task_details', task_id=...)\n" + " - Комментарии к задаче: query_bitrix24(action='task_comments', task_id=...)\n" + " Проанализируй суть, выдели позицию Ильи Гавришина (ID 1, гендир) и юристов, дай свой ясный вывод и предложи шаги.\n\n" + "2. СИСТЕМНЫЕ КОМАНДЫ (Docker, сеть, диски, серверы):\n" + " Используй execute_bash_command для выполнения консольных команд. Узлы: NanoPi (192.168.10.1), Synology (192.168.10.105), MacMini (192.168.10.167).\n\n" + "3. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи или планы):\n" + " Разложи всё по блокам:\n" + " 🎯 Суть: главная мысль в 1-2 предложениях.\n" + " 📋 Задачи (Action Items): конкретные пункты [ ].\n" + " 💡 Идеи в копилку: новые мысли и гипотезы.\n" + " 🗄️ Фиксация в Obsidian: куда сохранить (HomeLab или Work).\n\n" + "4. ГОЛОСОВОЙ ОТКЛИК ДЛЯ ОЗВУЧКИ:\n" " В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег ....\n" - " Внутри него напиши короткое, живое, теплое и емкое резюме от Майи (1-3 предложения), которое будет озвучено вслух! " - " Внутри пиши только чистый русский текст без звездочек, эмодзи, списков и Markdown." + " Внутри него напиши короткое, емкое, живое и теплое резюме от Майи (1-3 предложения), которое будет озвучено вслух! " + " Внутри пиши только чистый русский текст без звездочек, эмодзи и Markdown." ) models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite'] @@ -145,7 +199,7 @@ async def execute_antigravity_agent(user_text: str) -> str: model = genai.GenerativeModel( model_name=model_name, system_instruction=system_instruction, - tools=[execute_bash_command] + tools=[query_bitrix24, execute_bash_command] ) chat = model.start_chat(enable_automatic_function_calling=True) response = chat.send_message(user_text) @@ -164,7 +218,6 @@ async def handle_voice_message(message: Message, bot: Bot): if int(message.from_user.id) != int(ADMIN_USER_ID): return - # Определяем тип медиафайла msg_type = "VOICE" duration = 0 filename = "audio.ogg" @@ -192,7 +245,7 @@ async def handle_voice_message(message: Message, bot: Bot): mime = (doc.mime_type or "").lower() allowed_exts = ('.mp3', '.m4a', '.ogg', '.wav', '.aac', '.flac', '.3gp', '.oga', '.wma') if not (mime.startswith("audio/") or fn.endswith(allowed_exts)): - return # Не аудиозапись, пропускаем для других хэндлеров + return file_id = doc.file_id ext = os.path.splitext(doc.file_name or "rec.m4a")[1] or ".m4a" filename = f"doc_{file_id}{ext}" @@ -201,7 +254,7 @@ async def handle_voice_message(message: Message, bot: Bot): else: return - status_msg = await message.reply("🎙 Слушаю и анализирую аудиозапись...", parse_mode="HTML") + status_msg = await message.reply("🎙 Слушаю и выполняю команду...", parse_mode="HTML") await bot.send_chat_action(message.chat.id, "typing") try: @@ -218,24 +271,23 @@ async def handle_voice_message(message: Message, bot: Bot): if transcribed_text: log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text) - await status_msg.edit_text(f"🗣 Услышано: «{transcribed_text}»\n\n⚙️ Майа систематизирует и готовит ответ...", parse_mode="HTML") + await status_msg.edit_text(f"🗣 Команда принята: «{transcribed_text}»\n\n⚙️ Майа работает над задачей...", parse_mode="HTML") await bot.send_chat_action(message.chat.id, "record_voice") - # 2. Исполнение Maya Lead Agent (систематизация + команды хоста) + # 2. Исполнение Maya Lead Agent (Битрикс24, bash, анализ) agent_response = await execute_antigravity_agent(transcribed_text) - # 3. Подготовка текста для отправки (вырезаем тег из визуального сообщения) + # 3. Подготовка текста для отправки (вырезаем тег ) clean_text = re.sub(r".*?", "", agent_response, flags=re.DOTALL).strip() - formatted_text = f"🎯 Майа // Структурированный отклик:\n\n{clean_text}" + formatted_text = f"🎯 Майа // Отчет исполнения:\n\n{clean_text}" - # Разделяем длинный текст Telegram если нужно (TG limit 4096) if len(formatted_text) > 4000: await status_msg.edit_text(formatted_text[:4000], parse_mode="HTML") await message.reply(formatted_text[4000:], parse_mode="HTML") else: await status_msg.edit_text(formatted_text, parse_mode="HTML") - # 4. Синтез и отправка голосовой заметки (Voice Note) + # 4. Синтез и отправка голосового ответа try: voice_bytes = await text_to_speech_bytes(agent_response) if voice_bytes: