"""Обработчики команд Telegram""" import json import threading from datetime import datetime from .config import ALLOWED_USER from .state import STATE from .process_manager import PROCESS_MANAGER from . import INTERNET_AGENT from .file_manager import FILE_MANAGER from .notification_system import NOTIFICATIONS from .conductor_engine import ConductorEngine from .skill_orchestrator import SkillOrchestrator from .build_mode_editor import BuildModeEditor from .mode_handlers import run_chat_mode, run_skill_mode, run_build_mode from .telegram_utils import send_tg, download_tg_file, send_tg_file from .helpers import parse_skill_agents, parse_build_args, get_current_mode_info class BotHandler: HELP_TEXT = """PinkSky v7.0 — МОДУЛЬНАЯ АРХИТЕКТУРА 🎛️ Режимы: Chat — отправь сообщение /skill [задача] — Open Interpreter /build [спек] — сборка через кондуктор 🎭 Роли: /role [guru|hacker|architect|principal|evangelist|techlead|qa|sdet|qe|researcher|critic|universal] /role list — список ролей 🧠 Кондукторы: /conductor [default|strict|creative|economy|review|build] /conductor list — список 🤖 Модели: /model [имя] — переключить модель /model list — список моделей 📁 Файлы: /get [путь] — скачать файл /stop — остановить все процессы 🌐 Интернет: /search [запрос] — поиск в интернете /cache — статистика кэша /cache clear — очистить кэш 📊 Система: /status — текущий статус /history — история /export [json|md] — экспорт истории /mode — текущий режим""" def __init__(self, *args, **kwargs): # Принимаем args/kwargs для совместимости с BaseHTTPRequestHandler.__init__ # BotHandler не использует их напрямую self.mode_editor = BuildModeEditor(STATE) self.skill_orchestrator = SkillOrchestrator(STATE) def handle_message(self, data: dict): if "message" not in data: return message = data["message"] chat_id = str(message.get("chat", {}).get("id", "")) if ALLOWED_USER and chat_id != ALLOWED_USER: return NOTIFICATIONS.set_chat_id(chat_id) text = message.get("text", message.get("caption", "")).strip() file_id, file_name = None, None target_msg = message if "reply_to_message" in message: if "document" in message["reply_to_message"] or "photo" in message["reply_to_message"]: target_msg = message["reply_to_message"] if "document" in target_msg: file_id = target_msg["document"]["file_id"] file_name = target_msg["document"].get("file_name", "document.file") elif "photo" in target_msg: file_id = target_msg["photo"][-1]["file_id"] file_name = "photo.jpg" file_context = "" if file_id: dl_path = download_tg_file(file_id, file_name) if dl_path: file_context = f"\n\n[SYSTEM: User attached file: ./{dl_path}]" if not text: send_tg(chat_id, f"📁 Файл `{file_name}` сохранён. Ответь командой, например: `/build Что в этом файле?`") return if not text: return parts = text.split(maxsplit=1) command = parts[0].lower() args = parts[1].strip() if len(parts) > 1 else "" # === HELP === if command in ["/help", "/start"]: send_tg(chat_id, self.HELP_TEXT) return # === STOP === if command == "/stop": result = PROCESS_MANAGER.cancel_all() send_tg(chat_id, f"{result}\n\n{self.HELP_TEXT}") return # === STATUS === if command == "/status": info = get_current_mode_info() stats = f"""{info} 📊 Моделей: {len(STATE.models)} 📊 Ролей: {len(STATE.roles)} 📊 Кондукторов: {len(STATE.conductors)} 🌐 Интернет: {'✅' if STATE.build_context.get('internet_access', True) else '❌'} 🗑️ Кэш: {INTERNET_AGENT.get_cache_stats()} 🧵 Активных потоков: {PROCESS_MANAGER.get_active_count()}""" send_tg(chat_id, stats) return # === MODE === if command == "/mode": send_tg(chat_id, get_current_mode_info()) return # === CONDUCTOR === if command == "/conductor": if args == "list": lines = ["🧠 Доступные кондукторы:"] for name, cond in STATE.conductors.items(): marker = " [ACTIVE]" if name == STATE.current_conductor else "" lines.append(f"- {name}{marker}: {cond.description} [rank_by={cond.auto_rank_by}]") send_tg(chat_id, "\n".join(lines)) return if args in STATE.conductors: STATE.current_conductor = args send_tg(chat_id, f"🧠 Кондуктор: {args.upper()}\n{STATE.conductors[args].description}") else: available = ", ".join(STATE.conductors.keys()) send_tg(chat_id, f"Текущий: {STATE.current_conductor}\nДоступные: {available}") return # === ROLE === if command == "/role": role_parts = args.split(maxsplit=1) subcmd = role_parts[0].lower() if role_parts else "" subargs = role_parts[1] if len(role_parts) > 1 else "" if subcmd == "list": lines = ["🎭 Доступные роли:"] for name, role in STATE.roles.items(): marker = " [ACTIVE]" if name == STATE.current_role else "" lines.append(f"- {name}{marker}: {role.description} (complexity: {role.complexity})") send_tg(chat_id, "\n".join(lines)) return if subcmd == "info" and subargs: role = STATE.roles.get(subargs) if role: send_tg(chat_id, f"🎭 Роль {subargs}:\n\n{role.description}\n\nComplexity: {role.complexity}\nPreferred: {', '.join(role.preferred_models)}\n\nPrompt:\n```\n{role.prompt[:500]}...\n```") else: send_tg(chat_id, f"❌ Роль {subargs} не найдена") return if subcmd == "add" and subargs: add_parts = subargs.split(maxsplit=1) if len(add_parts) < 2: send_tg(chat_id, "Формат: /role add ") return new_name, new_prompt = add_parts[0], add_parts[1] from .models import Role STATE.roles[new_name] = Role( name=new_name, prompt=new_prompt, description=f"Custom role (added {datetime.now().strftime('%Y-%m-%d')})", complexity="medium" ) STATE.save_roles() send_tg(chat_id, f"✅ Роль {new_name} добавлена!") return if subcmd in STATE.roles: STATE.current_role = subcmd send_tg(chat_id, f"🎭 Роль: {subcmd.upper()}\n{STATE.roles[subcmd].description}") else: available = ", ".join(STATE.roles.keys()) send_tg(chat_id, f"Текущая: {STATE.current_role}\nДоступные: {available}") return # === MODEL === if command == "/model": model_parts = args.split(maxsplit=1) subcmd = model_parts[0].lower() if model_parts else "" subargs = model_parts[1] if len(model_parts) > 1 else "" if subcmd == "list": lines = ["🤖 Модели (по coding rank):"] for name, model in sorted(STATE.models.items(), key=lambda x: x[1].coding_rank): if name == "hf_fallback": continue marker = " [ACTIVE]" if name == STATE.current_model else "" tier = 1 if model.coding_rank > 5: tier = 2 if model.coding_rank > 12: tier = 3 if model.coding_rank > 18: tier = 4 if model.coding_rank > 24: tier = 5 cost = f"${model.cost_per_1k_output}/1k" if model.cost_per_1k_output > 0 else "free" lines.append(f"- {name}{marker} -- TIER {tier} | coding={model.coding_rank} speed={model.speed_rank} reasoning={model.reasoning_rank} | {cost}") send_tg(chat_id, "\n".join(lines)) return if subcmd == "add" and subargs: add_parts = subargs.split() if len(add_parts) < 2: send_tg(chat_id, "Формат: /model add ") return new_name, new_endpoint = add_parts[0], add_parts[1] from .models import ModelConfig STATE.models[new_name] = ModelConfig( name=new_name, provider="openai", endpoint=new_endpoint, api_key_env="NVIDIA_API_KEY" ) STATE.save_models() send_tg(chat_id, f"✅ Модель {new_name} добавлена!") return if subcmd in STATE.models: STATE.current_model = subcmd send_tg(chat_id, f"🤖 Модель: {subcmd.upper()}\n{STATE.models[subcmd].endpoint}") else: available = ", ".join([k for k in STATE.models.keys() if k != "hf_fallback"]) send_tg(chat_id, f"Текущая: {STATE.current_model}\nДоступные: {available}") return # === GET FILE === if command == "/get": if not args: send_tg(chat_id, "Использование: /get ./projects/test.py") else: send_tg_file(chat_id, args) return # === SEARCH === if command == "/search": if not args: send_tg(chat_id, "Использование: /search запрос") return results = INTERNET_AGENT.search_web(args) if results: lines = [f"🔍 Результаты поиска: {args}\n"] for i, r in enumerate(results[:5], 1): lines.append(f"{i}. [{r['title']}]({r['url']})\n{r['snippet'][:150]}...\n") send_tg(chat_id, "\n".join(lines)) else: send_tg(chat_id, "❌ Ничего не найдено") return # === CACHE === if command == "/cache": if args == "clear": result = INTERNET_AGENT.clear_cache() send_tg(chat_id, result) else: send_tg(chat_id, INTERNET_AGENT.get_cache_stats()) return # === HISTORY === if command == "/history": mode = args if args in ("chat", "skill", "build") else "chat" history = getattr(STATE, f"{mode}_history", []) if not history: send_tg(chat_id, f"📋 История {mode} пуста") return lines = [f"📋 История {mode} (последние 5):"] for entry in history[-5:]: ts = entry.get("timestamp", "unknown") role = entry.get("role", "unknown") content = entry.get("content", "")[:200] lines.append(f"\n[{ts}] {role}:\n{content}...") send_tg(chat_id, "\n".join(lines)) return # === EXPORT === if command == "/export": fmt = args if args in ("json", "md") else "json" if fmt == "json": data = STATE.export_history_json() FILE_MANAGER.save_file("exports/history.json", data) send_tg_file(chat_id, "exports/history.json") else: data = STATE.export_history_md() FILE_MANAGER.save_file("exports/history.md", data) send_tg_file(chat_id, "exports/history.md") return # === BUILD MODE === if command == "/build": if not args: send_tg(chat_id, "Укажите спецификацию проекта.") return params, clean_args = parse_build_args(args) run_build_mode(chat_id, clean_args, file_context, params) return # === SKILL MODE === if command == "/skill": if not args: send_tg(chat_id, "Укажите задачу.") return run_skill_mode(chat_id, args, file_context) return # === DEFAULT: CHAT MODE === run_chat_mode(chat_id, text, file_context)