{"repo": "ambartsumov/pds-ultimate", "files": {"agent/pds_ultimate/.env.example": {"7d6dee7e742bd846a45205f8652eb434e3bf35e5": {"t": 1785747131.7670405, "n": 4049, "h": "d8bd7ac7ca58bf98", "c": "# ─── PDS-Ultimate Environment Configuration ─────────────────────────────────\n# Скопируйте этот файл как .env и заполните значения\n# cp .env.example .env\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ОБЯЗАТЕЛЬНЫЕ (система не запустится без них)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Bot (получить у @BotFather)\nTG_BOT_TOKEN=\n# Ваш Telegram User ID (получить у @userinfobot)\nTG_OWNER_ID=\n\n# DeepSeek API (https://platform.deepseek.com/)\nDEEPSEEK_API_KEY=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# РЕКОМЕНДУЕМЫЕ (для полного функционала)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Userbot — Telethon (https://my.telegram.org/apps)\nTG_API_ID=\nTG_API_HASH=\nTG_SESSION_NAME=pds_userbot\n\n# Стиль: сколько чатов анализировать\nTG_STYLE_CHATS=7\nTG_MESSAGES_PER_CHAT=100\n# Список чатов для анализа стиля (username через запятую)\n# Пример: user1,user2,user3,user4,user5,user6,user7\nTG_STYLE_CHAT_LIST=\n\n# WhatsApp (Green-API — https://console.green-api.com/)\nWA_ENABLED=false\nWA_STYLE_CHATS=3\nWA_MESSAGES_PER_CHAT=100\nWA_GREEN_API_INSTANCE=\nWA_GREEN_API_TOKEN=\n\n# Gmail\nGMAIL_ENABLED=false\nGMAIL_OWNER_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ФИНАНСЫ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Распределение чистой прибыли (сумма должна = 100%)\nFINANCE_EXPENSE_PERCENT=50.0\nFINANCE_SAVINGS_PERCENT=50.0\n\n# Базовая валюта учёта\nBASE_CURRENCY=USD\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# DeepSeek — тонкая настройка\n# ═══════════════════════════════════════════════════════════════════════════════\n\nDEEPSEEK_BASE_URL=https://api.deepseek.com\nDEEPSEEK_MODEL=deepseek-reasoner\nDEEPSEEK_FAST_MODEL=deepseek-chat\nDEEPSEEK_MAX_TOKENS=4096\nDEEPSEEK_TEMPERATURE=0.7\nDEEPSEEK_TIMEOUT=120\nDEEPSEEK_MAX_RETRIES=3\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# Faster-Whisper (локальное распознавание голоса)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Модели: tiny, base, small, medium, large-v2, large-v3\nWHISPER_MODEL=medium\n# auto = GPU если есть, иначе CPU\nWHISPER_DEVICE=auto\nWHISPER_COMPUTE_TYPE=int8\nWHISPER_LANGUAGE=ru\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИСТИКА\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Первый запрос статуса через N дней\nLOGISTICS_FIRST_CHECK_DAYS=4\n# День повторной проверки (0=Пн, 1=Вт, ..., 6=Вс)\nLOGISTICS_CHECK_WEEKDAY=1\n# Антизабывание: повтор через N часов\nLOGISTICS_REMINDER_HOURS=2\n# Вечернее напоминание (час)\nLOGISTICS_EVENING_HOUR=20\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ПЛАНИРОВЩИК\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Утренний брифинг\nMORNING_BRIEF_HOUR=8\nMORNING_BRIEF_MINUTE=30\n\n# Отчёт на почту (каждые N дней)\nREPORT_INTERVAL_DAYS=3\nREPORT_HOUR=9\nREPORT_MINUTE=0\n\n# Бэкап (ежесуточно)\nBACKUP_HOUR=3\nBACKUP_MINUTE=0\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# БЕЗОПАСНОСТЬ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Кодовое слово для экстренного удаления финансовых данных\nSECURITY_EMERGENCY_CODE=\n# Автобэкап: true/false\nSECURITY_AUTO_BACKUP=true\n# Куда: local | email\nSECURITY_BACKUP_TARGET=local\nSECURITY_BACKUP_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# OCR\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# easyocr | tesseract\nOCR_ENGINE=easyocr\nOCR_CONFIDENCE=0.5\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИРОВАНИЕ\n# ═══════════════════════════════════════════════════════════════════════════════\n\nLOG_LEVEL=INFO\n"}, "bf61c622ded4087a7370359d1b223e7660d198c5": {"t": 1785770624.1155682, "n": 4049, "h": "d8bd7ac7ca58bf98", "c": "# ─── PDS-Ultimate Environment Configuration ─────────────────────────────────\n# Скопируйте этот файл как .env и заполните значения\n# cp .env.example .env\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ОБЯЗАТЕЛЬНЫЕ (система не запустится без них)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Bot (получить у @BotFather)\nTG_BOT_TOKEN=\n# Ваш Telegram User ID (получить у @userinfobot)\nTG_OWNER_ID=\n\n# DeepSeek API (https://platform.deepseek.com/)\nDEEPSEEK_API_KEY=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# РЕКОМЕНДУЕМЫЕ (для полного функционала)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Userbot — Telethon (https://my.telegram.org/apps)\nTG_API_ID=\nTG_API_HASH=\nTG_SESSION_NAME=pds_userbot\n\n# Стиль: сколько чатов анализировать\nTG_STYLE_CHATS=7\nTG_MESSAGES_PER_CHAT=100\n# Список чатов для анализа стиля (username через запятую)\n# Пример: user1,user2,user3,user4,user5,user6,user7\nTG_STYLE_CHAT_LIST=\n\n# WhatsApp (Green-API — https://console.green-api.com/)\nWA_ENABLED=false\nWA_STYLE_CHATS=3\nWA_MESSAGES_PER_CHAT=100\nWA_GREEN_API_INSTANCE=\nWA_GREEN_API_TOKEN=\n\n# Gmail\nGMAIL_ENABLED=false\nGMAIL_OWNER_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ФИНАНСЫ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Распределение чистой прибыли (сумма должна = 100%)\nFINANCE_EXPENSE_PERCENT=50.0\nFINANCE_SAVINGS_PERCENT=50.0\n\n# Базовая валюта учёта\nBASE_CURRENCY=USD\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# DeepSeek — тонкая настройка\n# ═══════════════════════════════════════════════════════════════════════════════\n\nDEEPSEEK_BASE_URL=https://api.deepseek.com\nDEEPSEEK_MODEL=deepseek-reasoner\nDEEPSEEK_FAST_MODEL=deepseek-chat\nDEEPSEEK_MAX_TOKENS=4096\nDEEPSEEK_TEMPERATURE=0.7\nDEEPSEEK_TIMEOUT=120\nDEEPSEEK_MAX_RETRIES=3\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# Faster-Whisper (локальное распознавание голоса)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Модели: tiny, base, small, medium, large-v2, large-v3\nWHISPER_MODEL=medium\n# auto = GPU если есть, иначе CPU\nWHISPER_DEVICE=auto\nWHISPER_COMPUTE_TYPE=int8\nWHISPER_LANGUAGE=ru\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИСТИКА\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Первый запрос статуса через N дней\nLOGISTICS_FIRST_CHECK_DAYS=4\n# День повторной проверки (0=Пн, 1=Вт, ..., 6=Вс)\nLOGISTICS_CHECK_WEEKDAY=1\n# Антизабывание: повтор через N часов\nLOGISTICS_REMINDER_HOURS=2\n# Вечернее напоминание (час)\nLOGISTICS_EVENING_HOUR=20\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ПЛАНИРОВЩИК\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Утренний брифинг\nMORNING_BRIEF_HOUR=8\nMORNING_BRIEF_MINUTE=30\n\n# Отчёт на почту (каждые N дней)\nREPORT_INTERVAL_DAYS=3\nREPORT_HOUR=9\nREPORT_MINUTE=0\n\n# Бэкап (ежесуточно)\nBACKUP_HOUR=3\nBACKUP_MINUTE=0\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# БЕЗОПАСНОСТЬ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Кодовое слово для экстренного удаления финансовых данных\nSECURITY_EMERGENCY_CODE=\n# Автобэкап: true/false\nSECURITY_AUTO_BACKUP=true\n# Куда: local | email\nSECURITY_BACKUP_TARGET=local\nSECURITY_BACKUP_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# OCR\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# easyocr | tesseract\nOCR_ENGINE=easyocr\nOCR_CONFIDENCE=0.5\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИРОВАНИЕ\n# ═══════════════════════════════════════════════════════════════════════════════\n\nLOG_LEVEL=INFO\n"}, "8ac7fd250f885b5d8e3b302a29394c96d2ff6e12": {"t": 1785770625.3679516, "n": 3978, "h": "8fa86ec1c0ecafee", "c": "# ─── PDS-Ultimate Environment Configuration ─────────────────────────────────\n# Скопируйте этот файл как .env и заполните значения\n# cp .env.example .env\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ОБЯЗАТЕЛЬНЫЕ (система не запустится без них)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Bot (получить у @BotFather)\nTG_BOT_TOKEN=\n# Ваш Telegram User ID (получить у @userinfobot)\nTG_OWNER_ID=\n\n# DeepSeek API (https://platform.deepseek.com/)\nDEEPSEEK_API_KEY=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# РЕКОМЕНДУЕМЫЕ (для полного функционала)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Telegram Userbot — Telethon (https://my.telegram.org/apps)\nTG_API_ID=\nTG_API_HASH=\nTG_SESSION_NAME=pds_userbot\n\n# Стиль: сколько чатов анализировать\nTG_STYLE_CHATS=7\nTG_MESSAGES_PER_CHAT=100\n# Список чатов для анализа стиля (username через запятую)\n# Пример: user1,user2,user3,user4,user5,user6,user7\nTG_STYLE_CHAT_LIST=\n\n# WhatsApp\nWA_ENABLED=false\nWA_STYLE_CHATS=3\nWA_MESSAGES_PER_CHAT=100\nWA_HEADLESS=true\n\n# Gmail\nGMAIL_ENABLED=false\nGMAIL_OWNER_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ФИНАНСЫ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Распределение чистой прибыли (сумма должна = 100%)\nFINANCE_EXPENSE_PERCENT=50.0\nFINANCE_SAVINGS_PERCENT=50.0\n\n# Базовая валюта учёта\nBASE_CURRENCY=USD\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# DeepSeek — тонкая настройка\n# ═══════════════════════════════════════════════════════════════════════════════\n\nDEEPSEEK_BASE_URL=https://api.deepseek.com\nDEEPSEEK_MODEL=deepseek-reasoner\nDEEPSEEK_FAST_MODEL=deepseek-chat\nDEEPSEEK_MAX_TOKENS=4096\nDEEPSEEK_TEMPERATURE=0.7\nDEEPSEEK_TIMEOUT=120\nDEEPSEEK_MAX_RETRIES=3\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# Faster-Whisper (локальное распознавание голоса)\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Модели: tiny, base, small, medium, large-v2, large-v3\nWHISPER_MODEL=medium\n# auto = GPU если есть, иначе CPU\nWHISPER_DEVICE=auto\nWHISPER_COMPUTE_TYPE=int8\nWHISPER_LANGUAGE=ru\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИСТИКА\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Первый запрос статуса через N дней\nLOGISTICS_FIRST_CHECK_DAYS=4\n# День повторной проверки (0=Пн, 1=Вт, ..., 6=Вс)\nLOGISTICS_CHECK_WEEKDAY=1\n# Антизабывание: повтор через N часов\nLOGISTICS_REMINDER_HOURS=2\n# Вечернее напоминание (час)\nLOGISTICS_EVENING_HOUR=20\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ПЛАНИРОВЩИК\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Утренний брифинг\nMORNING_BRIEF_HOUR=8\nMORNING_BRIEF_MINUTE=30\n\n# Отчёт на почту (каждые N дней)\nREPORT_INTERVAL_DAYS=3\nREPORT_HOUR=9\nREPORT_MINUTE=0\n\n# Бэкап (ежесуточно)\nBACKUP_HOUR=3\nBACKUP_MINUTE=0\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# БЕЗОПАСНОСТЬ\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# Кодовое слово для экстренного удаления финансовых данных\nSECURITY_EMERGENCY_CODE=\n# Автобэкап: true/false\nSECURITY_AUTO_BACKUP=true\n# Куда: local | email\nSECURITY_BACKUP_TARGET=local\nSECURITY_BACKUP_EMAIL=\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# OCR\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# easyocr | tesseract\nOCR_ENGINE=easyocr\nOCR_CONFIDENCE=0.5\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# ЛОГИРОВАНИЕ\n# ═══════════════════════════════════════════════════════════════════════════════\n\nLOG_LEVEL=INFO\n"}}, "agent/pds_ultimate/core/plugin_system.py": {"7d6dee7e742bd846a45205f8652eb434e3bf35e5": {"t": 1785747488.2137034, "n": 36985, "h": "af61d395f66d66a2", "c": "\"\"\"\nPDS-Ultimate Plugin System (Part 8)\n======================================\nДинамическое подключение ЛЮБЫХ API через чат.\n\nПользователь может подключить абсолютно любой API:\n1. Скидывает API ключ/токен/URL в чат\n2. Агент автоматически определяет тип сервиса\n3. Валидирует подключение\n4. Создаёт Tool и регистрирует в системе\n5. Всё работает без перезагрузки\n\nПоддерживаемые типы auto-detect:\n- OpenAI / ChatGPT API\n- Anthropic / Claude API\n- Google Cloud (Translate, Vision, Maps, etc.)\n- Stripe / PayPal (платежи)\n- Twilio (SMS/звонки)\n- SendGrid / Mailgun (email)\n- Any REST API (auto-discover endpoints)\n- Webhook endpoints\n- Custom function plugins (Python code)\n\nБезопасность:\n- Ключи шифруются AES-256\n- Sandbox для пользовательского кода\n- Rate limiting per-plugin\n- Автоматические health checks\n\"\"\"\n\nfrom __future__ import annotations\nfrom collections import Counter\n\nimport hashlib\nimport json\nimport re\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pds_ultimate.config import DATA_DIR, logger\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# PLUGIN TYPES & DATA CLASSES\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass PluginType(str, Enum):\n \"\"\"Тип плагина.\"\"\"\n REST_API = \"rest_api\" # Generic REST API\n LLM_API = \"llm_api\" # OpenAI/Anthropic/etc\n PAYMENT_API = \"payment_api\" # Stripe/PayPal\n MESSAGING_API = \"messaging_api\" # Twilio/SendGrid\n CLOUD_API = \"cloud_api\" # Google Cloud/AWS\n WEBHOOK = \"webhook\" # Incoming/outgoing webhooks\n CUSTOM_FUNC = \"custom_func\" # Custom Python function\n DATABASE = \"database\" # External DB connection\n UNKNOWN = \"unknown\"\n\n\nclass PluginStatus(str, Enum):\n \"\"\"Статус плагина.\"\"\"\n PENDING = \"pending\" # Ожидает валидации\n VALIDATING = \"validating\" # Проверяется\n ACTIVE = \"active\" # Работает\n INACTIVE = \"inactive\" # Деактивирован\n ERROR = \"error\" # Ошибка подключения\n EXPIRED = \"expired\" # Ключ истёк\n\n\n@dataclass\nclass PluginEndpoint:\n \"\"\"Эндпоинт REST API плагина.\"\"\"\n method: str = \"GET\" # GET, POST, PUT, DELETE\n path: str = \"\" # /api/v1/resource\n description: str = \"\"\n params: dict[str, str] = field(default_factory=dict)\n headers: dict[str, str] = field(default_factory=dict)\n body_template: dict[str, Any] = field(default_factory=dict)\n response_format: str = \"json\" # json, text, binary\n\n\n@dataclass\nclass PluginConfig:\n \"\"\"Конфигурация плагина.\"\"\"\n name: str # Человекочитаемое имя\n plugin_type: PluginType = PluginType.UNKNOWN\n base_url: str = \"\" # Базовый URL API\n api_key: str = \"\" # API ключ (шифруется)\n api_secret: str = \"\" # API секрет (шифруется)\n auth_type: str = \"bearer\" # bearer, basic, api_key, custom\n auth_header: str = \"Authorization\" # Заголовок авторизации\n endpoints: list[PluginEndpoint] = field(default_factory=list)\n rate_limit: int = 60 # Запросов в минуту\n timeout: int = 30 # Секунд\n custom_headers: dict[str, str] = field(default_factory=dict)\n metadata: dict[str, Any] = field(default_factory=dict)\n\n def to_dict(self) -> dict:\n return {\n \"name\": self.name,\n \"plugin_type\": self.plugin_type.value,\n \"base_url\": self.base_url,\n \"auth_type\": self.auth_type,\n \"rate_limit\": self.rate_limit,\n \"timeout\": self.timeout,\n \"endpoints_count\": len(self.endpoints),\n \"metadata\": self.metadata,\n }\n\n\n@dataclass\nclass PluginHealth:\n \"\"\"Результат health check плагина.\"\"\"\n healthy: bool = False\n latency_ms: int = 0\n status_code: int = 0\n error: str = \"\"\n checked_at: datetime = field(default_factory=datetime.utcnow)\n\n @property\n def status(self) -> str:\n if self.healthy:\n return f\"✅ OK ({self.latency_ms}ms)\"\n return f\"❌ Error: {self.error}\"\n\n\n@dataclass\nclass Plugin:\n \"\"\"\n Полный плагин с конфигом, статусом и статистикой.\n \"\"\"\n id: str # Уникальный ID\n config: PluginConfig # Конфигурация\n status: PluginStatus = PluginStatus.PENDING\n owner_id: int = 0 # Telegram user ID владельца\n created_at: datetime = field(default_factory=datetime.utcnow)\n last_used: datetime | None = None\n usage_count: int = 0\n error_count: int = 0\n last_health: PluginHealth | None = None\n tool_name: str = \"\" # Имя зарегистрированного Tool\n\n def to_dict(self) -> dict:\n return {\n \"id\": self.id,\n \"name\": self.config.name,\n \"type\": self.config.plugin_type.value,\n \"status\": self.status.value,\n \"owner_id\": self.owner_id,\n \"usage_count\": self.usage_count,\n \"error_count\": self.error_count,\n \"tool_name\": self.tool_name,\n \"created_at\": self.created_at.isoformat(),\n \"last_used\": self.last_used.isoformat() if self.last_used else None,\n \"health\": self.last_health.status if self.last_health else \"unknown\",\n }\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# API DETECTOR — Автоопределение типа API из текста\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass APIDetector:\n \"\"\"\n Определяет тип API по ключу/URL/описанию.\n\n Паттерны:\n - sk-... → OpenAI\n - sk-ant-... → Anthropic\n - AIza... → Google API Key\n - AKIA... → AWS\n - pk_live/sk_live → Stripe\n - SG.... → SendGrid\n - AC... + auth token → Twilio\n \"\"\"\n\n # Паттерны API ключей\n PATTERNS: list[tuple[str, PluginType, str]] = [\n # OpenAI\n (r\"sk-[a-zA-Z0-9]{20,}\", PluginType.LLM_API, \"OpenAI\"),\n (r\"sk-proj-[a-zA-Z0-9_-]{40,}\", PluginType.LLM_API, \"OpenAI\"),\n # Anthropic\n (r\"sk-ant-[a-zA-Z0-9_-]{40,}\", PluginType.LLM_API, \"Anthropic\"),\n # Google\n (r\"AIza[a-zA-Z0-9_-]{35}\", PluginType.CLOUD_API, \"Google\"),\n # AWS\n (r\"AKIA[A-Z0-9]{16}\", PluginType.CLOUD_API, \"AWS\"),\n # Stripe\n (r\"(pk|sk)_(live|test)_[a-zA-Z0-9]{20,}\",\n PluginType.PAYMENT_API, \"Stripe\"),\n # SendGrid\n (r\"SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}\",\n PluginType.MESSAGING_API, \"SendGrid\"),\n # Twilio\n (r\"AC[a-f0-9]{32}\", PluginType.MESSAGING_API, \"Twilio\"),\n # Telegram Bot Token\n (r\"\\d{8,10}:[A-Za-z0-9_-]{35}\", PluginType.MESSAGING_API, \"Telegram\"),\n # DeepSeek\n (r\"sk-[a-f0-9]{32}\", PluginType.LLM_API, \"DeepSeek\"),\n # Generic Bearer Token\n (r\"[A-Za-z0-9_-]{40,}\", PluginType.REST_API, \"Generic API\"),\n ]\n\n # URL-based detection\n URL_PATTERNS: list[tuple[str, PluginType, str]] = [\n (r\"api\\.openai\\.com\", PluginType.LLM_API, \"OpenAI\"),\n (r\"api\\.anthropic\\.com\", PluginType.LLM_API, \"Anthropic\"),\n (r\"api\\.deepseek\\.com\", PluginType.LLM_API, \"DeepSeek\"),\n (r\"api\\.stripe\\.com\", PluginType.PAYMENT_API, \"Stripe\"),\n (r\"api\\.twilio\\.com\", PluginType.MESSAGING_API, \"Twilio\"),\n (r\"api\\.sendgrid\\.com\", PluginType.MESSAGING_API, \"SendGrid\"),\n (r\"googleapis\\.com\", PluginType.CLOUD_API, \"Google\"),\n (r\"amazonaws\\.com\", PluginType.CLOUD_API, \"AWS\"),\n (r\"api\\.telegram\\.org\", PluginType.MESSAGING_API, \"Telegram\"),\n ]\n\n @classmethod\n def detect_from_text(cls, text: str) -> list[dict[str, Any]]:\n \"\"\"\n Определить API ключи и URLs из произвольного текста.\n\n Returns:\n Список обнаружений: [{\"key\": \"...\", \"type\": ..., \"service\": \"...\"}]\n \"\"\"\n detections: list[dict[str, Any]] = []\n\n # Ищем API ключи\n for pattern, plugin_type, service in cls.PATTERNS:\n matches = re.finditer(pattern, text)\n for match in matches:\n key = match.group(0)\n # Проверяем что это не часть URL\n if key not in [d[\"key\"] for d in detections]:\n detections.append({\n \"key\": key,\n \"type\": plugin_type,\n \"service\": service,\n \"confidence\": 0.9 if service != \"Generic API\" else 0.5,\n })\n\n # Ищем URLs\n url_pattern = r'https?://[^\\s<>\"{}|\\\\^`\\[\\]]+'\n urls = re.findall(url_pattern, text)\n for url in urls:\n for pattern, plugin_type, service in cls.URL_PATTERNS:\n if re.search(pattern, url):\n detections.append({\n \"url\": url,\n \"type\": plugin_type,\n \"service\": service,\n \"confidence\": 0.95,\n })\n break\n else:\n # Unknown REST API URL\n if \"/api/\" in url or \"/v1/\" in url or \"/v2/\" in url:\n detections.append({\n \"url\": url,\n \"type\": PluginType.REST_API,\n \"service\": \"Custom REST API\",\n \"confidence\": 0.6,\n })\n\n return detections\n\n @classmethod\n def get_service_info(cls, service: str) -> dict[str, Any]:\n \"\"\"Получить информацию о сервисе для onboarding.\"\"\"\n services = {\n \"OpenAI\": {\n \"name\": \"OpenAI / ChatGPT\",\n \"description\": \"AI модели (GPT-4, DALL-E, Whisper)\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://platform.openai.com/docs\",\n \"capabilities\": [\"Генерация текста\", \"Анализ изображений\", \"Распознавание речи\"],\n \"setup_guide\": (\n \"1. Зайдите на https://platform.openai.com\\n\"\n \"2. Settings → API Keys → Create new secret key\\n\"\n \"3. Скопируйте ключ (начинается с sk-...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"Anthropic\": {\n \"name\": \"Anthropic / Claude\",\n \"description\": \"AI модель Claude для анализа и генерации\",\n \"base_url\": \"https://api.anthropic.com/v1\",\n \"auth_type\": \"bearer\",\n \"auth_header\": \"x-api-key\",\n \"docs_url\": \"https://docs.anthropic.com\",\n \"capabilities\": [\"Генерация текста\", \"Анализ документов\", \"Код\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.anthropic.com\\n\"\n \"2. Settings → API Keys → Create Key\\n\"\n \"3. Скопируйте ключ (начинается с sk-ant-...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"Stripe\": {\n \"name\": \"Stripe (Платежи)\",\n \"description\": \"Приём и обработка платежей\",\n \"base_url\": \"https://api.stripe.com/v1\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://stripe.com/docs/api\",\n \"capabilities\": [\"Приём платежей\", \"Подписки\", \"Инвойсы\", \"Refunds\"],\n \"setup_guide\": (\n \"1. Зайдите на https://dashboard.stripe.com\\n\"\n \"2. Developers → API Keys\\n\"\n \"3. Скопируйте Secret Key (sk_live_... или sk_test_...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"SendGrid\": {\n \"name\": \"SendGrid (Email)\",\n \"description\": \"Отправка email: рассылки, уведомления\",\n \"base_url\": \"https://api.sendgrid.com/v3\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://docs.sendgrid.com\",\n \"capabilities\": [\"Отправка email\", \"Шаблоны\", \"Аналитика\"],\n \"setup_guide\": (\n \"1. Зайдите на https://app.sendgrid.com\\n\"\n \"2. Settings → API Keys → Create API Key\\n\"\n \"3. Выберите Full Access\\n\"\n \"4. Скопируйте ключ (SG.xxx) и отправьте мне\"\n ),\n },\n \"Twilio\": {\n \"name\": \"Twilio (SMS/Звонки)\",\n \"description\": \"SMS, звонки, WhatsApp Business API\",\n \"base_url\": \"https://api.twilio.com/2010-04-01\",\n \"auth_type\": \"basic\",\n \"docs_url\": \"https://www.twilio.com/docs\",\n \"capabilities\": [\"SMS\", \"Звонки\", \"WhatsApp\", \"Видео\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.twilio.com\\n\"\n \"2. Скопируйте Account SID (AC...) и Auth Token\\n\"\n \"3. Отправьте оба значения мне в чат\"\n ),\n },\n \"Google\": {\n \"name\": \"Google Cloud\",\n \"description\": \"Google Translate, Vision, Maps, Sheets и другие\",\n \"base_url\": \"https://googleapis.com\",\n \"auth_type\": \"api_key\",\n \"docs_url\": \"https://console.cloud.google.com\",\n \"capabilities\": [\"Перевод\", \"Распознавание изображений\", \"Карты\", \"Таблицы\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.cloud.google.com\\n\"\n \"2. APIs & Services → Credentials → Create Credentials\\n\"\n \"3. Выберите API Key\\n\"\n \"4. Скопируйте ключ (AIza...) и отправьте мне\"\n ),\n },\n \"Telegram\": {\n \"name\": \"Telegram Bot\",\n \"description\": \"Дополнительный Telegram бот\",\n \"base_url\": \"https://api.telegram.org\",\n \"auth_type\": \"custom\",\n \"docs_url\": \"https://core.telegram.org/bots/api\",\n \"capabilities\": [\"Отправка сообщений\", \"Управление каналами\", \"Inline боты\"],\n \"setup_guide\": (\n \"1. Напишите @BotFather в Telegram\\n\"\n \"2. Отправьте /newbot и следуйте инструкциям\\n\"\n \"3. Скопируйте токен бота\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n }\n return services.get(service, {\n \"name\": service,\n \"description\": f\"API сервис: {service}\",\n \"setup_guide\": (\n \"Отправьте мне:\\n\"\n \"1. API ключ или токен\\n\"\n \"2. URL сервиса (если есть)\\n\"\n \"3. Краткое описание что этот API делает\\n\\n\"\n \"Я автоматически настрою подключение.\"\n ),\n })\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# RATE LIMITER — Per-plugin rate limiting\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass RateLimiter:\n \"\"\"Ограничение частоты запросов к API плагина.\"\"\"\n\n def __init__(self):\n self._windows: dict[str, list[float]] = {}\n\n def check(self, plugin_id: str, limit: int = 60) -> bool:\n \"\"\"Проверить, разрешён ли запрос (True = ОК).\"\"\"\n now = time.time()\n window = self._windows.setdefault(plugin_id, [])\n\n # Удаляем старые записи (>60 сек)\n cutoff = now - 60\n self._windows[plugin_id] = [t for t in window if t > cutoff]\n window = self._windows[plugin_id]\n\n if len(window) >= limit:\n return False\n\n window.append(now)\n return True\n\n def remaining(self, plugin_id: str, limit: int = 60) -> int:\n \"\"\"Сколько запросов осталось в текущем окне.\"\"\"\n now = time.time()\n window = self._windows.get(plugin_id, [])\n active = [t for t in window if t > now - 60]\n return max(0, limit - len(active))\n\n def reset(self, plugin_id: str) -> None:\n \"\"\"Сбросить лимит.\"\"\"\n self._windows.pop(plugin_id, None)\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# PLUGIN MANAGER — Центральный менеджер\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass PluginManager:\n \"\"\"\n Центральный менеджер плагинов.\n\n Отвечает за:\n - Регистрацию и удаление плагинов\n - Auto-detect API типа из текста пользователя\n - Валидацию подключения\n - Создание Tool для ToolRegistry\n - Health checks\n - Persistence (сохранение/загрузка)\n \"\"\"\n\n PLUGINS_DIR = DATA_DIR / \"plugins\"\n\n def __init__(self):\n self._plugins: dict[str, Plugin] = {}\n self._rate_limiter = RateLimiter()\n self._detector = APIDetector()\n self.PLUGINS_DIR.mkdir(parents=True, exist_ok=True)\n\n # ─── Core Operations ─────────────────────────────────────────────────\n\n def register_plugin(\n self,\n config: PluginConfig,\n owner_id: int = 0,\n ) -> Plugin:\n \"\"\"\n Зарегистрировать новый плагин.\n\n Args:\n config: Конфигурация плагина\n owner_id: ID владельца\n\n Returns:\n Plugin с уникальным ID\n \"\"\"\n # Генерируем ID\n raw = f\"{config.name}:{config.base_url}:{owner_id}:{time.time()}\"\n plugin_id = hashlib.md5(raw.encode()).hexdigest()[:12]\n\n # Имя инструмента\n tool_name = self._make_tool_name(config.name)\n\n plugin = Plugin(\n id=plugin_id,\n config=config,\n status=PluginStatus.PENDING,\n owner_id=owner_id,\n tool_name=tool_name,\n )\n\n self._plugins[plugin_id] = plugin\n logger.info(\n f\"Plugin registered: {config.name} [{config.plugin_type.value}] \"\n f\"id={plugin_id} owner={owner_id}\"\n )\n\n return plugin\n\n def unregister_plugin(self, plugin_id: str) -> bool:\n \"\"\"Удалить плагин.\"\"\"\n plugin = self._plugins.pop(plugin_id, None)\n if plugin:\n self._rate_limiter.reset(plugin_id)\n logger.info(\n f\"Plugin unregistered: {plugin.config.name} id={plugin_id}\")\n return True\n return False\n\n def get_plugin(self, plugin_id: str) -> Plugin | None:\n \"\"\"Получить плагин по ID.\"\"\"\n return self._plugins.get(plugin_id)\n\n def get_by_name(self, name: str) -> Plugin | None:\n \"\"\"Получить плагин по имени.\"\"\"\n name_lower = name.lower()\n for p in self._plugins.values():\n if p.config.name.lower() == name_lower or p.tool_name == name_lower:\n return p\n return None\n\n def get_user_plugins(self, owner_id: int) -> list[Plugin]:\n \"\"\"Получить все плагины пользователя.\"\"\"\n return [p for p in self._plugins.values() if p.owner_id == owner_id]\n\n def get_active_plugins(self) -> list[Plugin]:\n \"\"\"Получить все активные плагины.\"\"\"\n return [p for p in self._plugins.values()\n if p.status == PluginStatus.ACTIVE]\n\n @property\n def count(self) -> int:\n return len(self._plugins)\n\n @property\n def active_count(self) -> int:\n return len(self.get_active_plugins())\n\n # ─── Auto-detect from chat text ──────────────────────────────────────\n\n def detect_from_message(self, text: str) -> list[dict[str, Any]]:\n \"\"\"\n Определить API из текста сообщения пользователя.\n\n Возвращает список обнаруженных API с рекомендациями.\n \"\"\"\n return self._detector.detect_from_text(text)\n\n def get_onboarding_text(self, service: str | None = None) -> str:\n \"\"\"\n Текст приветствия/онбординга для подключения API.\n\n Если service указан — гайд для конкретного сервиса.\n Если нет — общий обзор возможностей.\n \"\"\"\n if service:\n info = self._detector.get_service_info(service)\n lines = [\n f\"🔌 **{info.get('name', service)}**\",\n f\"📝 {info.get('description', '')}\",\n \"\",\n ]\n capabilities = info.get(\"capabilities\", [])\n if capabilities:\n lines.append(\"🎯 Возможности:\")\n for cap in capabilities:\n lines.append(f\" • {cap}\")\n lines.append(\"\")\n\n guide = info.get(\"setup_guide\", \"\")\n if guide:\n lines.append(\"📋 Как подключить:\")\n lines.append(guide)\n\n return \"\\n\".join(lines)\n\n # Общий обзор\n return (\n \"🔌 **Подключение внешних API**\\n\\n\"\n \"Я могу работать с любыми API! Вот популярные:\\n\\n\"\n \"🤖 **AI модели:** OpenAI (GPT-4), Anthropic (Claude)\\n\"\n \"💳 **Платежи:** Stripe, PayPal\\n\"\n \"📧 **Email:** SendGrid, Mailgun\\n\"\n \"📱 **SMS/Звонки:** Twilio\\n\"\n \"☁️ **Облако:** Google Cloud, AWS\\n\"\n \"🔗 **Любой REST API:** Просто скиньте URL и ключ\\n\\n\"\n \"Для подключения просто отправьте мне:\\n\"\n \"• API ключ или токен\\n\"\n \"• URL сервиса\\n\"\n \"• Или скажите какой сервис хотите подключить\\n\\n\"\n \"Я автоматически всё определю и настрою! 🚀\"\n )\n\n # ─── Validation ──────────────────────────────────────────────────────\n\n async def validate_plugin(self, plugin_id: str) -> PluginHealth:\n \"\"\"\n Валидировать подключение плагина (health check).\n\n Выполняет тестовый запрос к API.\n \"\"\"\n plugin = self._plugins.get(plugin_id)\n if not plugin:\n return PluginHealth(error=\"Plugin not found\")\n\n plugin.status = PluginStatus.VALIDATING\n start = time.time()\n\n try:\n import httpx\n\n headers = self._build_headers(plugin.config)\n timeout = httpx.Timeout(plugin.config.timeout)\n\n async with httpx.AsyncClient(timeout=timeout) as client:\n # Определяем URL для проверки\n check_url = self._get_health_url(plugin.config)\n if not check_url:\n # Нет URL для проверки — считаем OK\n health = PluginHealth(\n healthy=True,\n latency_ms=int((time.time() - start) * 1000),\n status_code=200,\n )\n plugin.status = PluginStatus.ACTIVE\n plugin.last_health = health\n return health\n\n response = await client.get(check_url, headers=headers)\n latency = int((time.time() - start) * 1000)\n\n healthy = response.status_code < 500\n health = PluginHealth(\n healthy=healthy,\n latency_ms=latency,\n status_code=response.status_code,\n )\n\n plugin.status = PluginStatus.ACTIVE if healthy else PluginStatus.ERROR\n plugin.last_health = health\n return health\n\n except Exception as e:\n latency = int((time.time() - start) * 1000)\n health = PluginHealth(\n healthy=False,\n latency_ms=latency,\n error=str(e),\n )\n plugin.status = PluginStatus.ERROR\n plugin.last_health = health\n return health\n\n # ─── Execute plugin call ─────────────────────────────────────────────\n\n async def execute(\n self,\n plugin_id: str,\n endpoint_index: int = 0,\n params: dict[str, Any] | None = None,\n body: dict[str, Any] | None = None,\n ) -> dict[str, Any]:\n \"\"\"\n Выполнить запрос через плагин.\n\n Args:\n plugin_id: ID плагина\n endpoint_index: Индекс эндпоинта\n params: Query параметры\n body: Тело запроса\n\n Returns:\n dict с результатом\n \"\"\"\n plugin = self._plugins.get(plugin_id)\n if not plugin:\n return {\"success\": False, \"error\": \"Plugin not found\"}\n\n if plugin.status != PluginStatus.ACTIVE:\n return {\"success\": False, \"error\": f\"Plugin status: {plugin.status.value}\"}\n\n # Rate limiting\n if not self._rate_limiter.check(plugin_id, plugin.config.rate_limit):\n remaining = self._rate_limiter.remaining(\n plugin_id, plugin.config.rate_limit)\n return {\n \"success\": False,\n \"error\": f\"Rate limit exceeded. Remaining: {remaining}\",\n }\n\n try:\n import httpx\n\n headers = self._build_headers(plugin.config)\n timeout = httpx.Timeout(plugin.config.timeout)\n\n # Определяем endpoint\n if plugin.config.endpoints and endpoint_index < len(plugin.config.endpoints):\n ep = plugin.config.endpoints[endpoint_index]\n url = f\"{plugin.config.base_url.rstrip('/')}/{ep.path.lstrip('/')}\"\n method = ep.method\n # Merge body template with provided body\n merged_body = {**ep.body_template, **(body or {})}\n else:\n url = plugin.config.base_url\n method = \"POST\" if body else \"GET\"\n merged_body = body\n\n async with httpx.AsyncClient(timeout=timeout) as client:\n if method.upper() == \"GET\":\n response = await client.get(url, headers=headers, params=params)\n elif method.upper() == \"POST\":\n response = await client.post(\n url, headers=headers, params=params, json=merged_body)\n elif method.upper() == \"PUT\":\n response = await client.put(\n url, headers=headers, params=params, json=merged_body)\n elif method.upper() == \"DELETE\":\n response = await client.delete(url, headers=headers, params=params)\n else:\n return {\"success\": False, \"error\": f\"Unknown method: {method}\"}\n\n plugin.usage_count += 1\n plugin.last_used = datetime.utcnow()\n\n # Парсим ответ\n try:\n data = response.json()\n except Exception:\n data = {\"text\": response.text[:2000]}\n\n return {\n \"success\": response.status_code < 400,\n \"status_code\": response.status_code,\n \"data\": data,\n }\n\n except Exception as e:\n plugin.error_count += 1\n return {\"success\": False, \"error\": str(e)}\n\n # ─── Create Tool from Plugin ─────────────────────────────────────────\n\n def create_tool_for_plugin(self, plugin: Plugin) -> \"Tool\":\n \"\"\"\n Создать Tool для ToolRegistry из плагина.\n\n Генерирует async handler, который вызывает plugin API.\n \"\"\"\n from pds_ultimate.core.tools import Tool, ToolParameter, ToolResult\n\n plugin_id = plugin.id\n manager = self # Capture reference\n\n async def plugin_handler(\n action: str = \"call\",\n endpoint: int = 0,\n data: str = \"\",\n **kwargs,\n ) -> ToolResult:\n \"\"\"Обработчик плагина (сгенерирован автоматически).\"\"\"\n body = None\n if data:\n try:\n body = json.loads(data)\n except json.JSONDecodeError:\n body = {\"text\": data}\n\n result = await manager.execute(\n plugin_id=plugin_id,\n endpoint_index=int(endpoint),\n body=body,\n )\n\n if result[\"success\"]:\n output = json.dumps(result.get(\"data\", {}),\n ensure_ascii=False, default=str)\n return ToolResult(\n tool_name=plugin.tool_name,\n success=True,\n output=output[:3000],\n data=result.get(\"data\"),\n )\n else:\n return ToolResult(\n tool_name=plugin.tool_name,\n success=False,\n output=\"\",\n error=result.get(\"error\", \"Unknown error\"),\n )\n\n # Описание из конфигурации\n desc_parts = [f\"Плагин: {plugin.config.name}\"]\n if plugin.config.plugin_type != PluginType.UNKNOWN:\n desc_parts.append(f\"({plugin.config.plugin_type.value})\")\n if plugin.config.endpoints:\n desc_parts.append(\n f\"Эндпоинтов: {len(plugin.config.endpoints)}\")\n\n tool = Tool(\n name=plugin.tool_name,\n description=\" \".join(desc_parts),\n parameters=[\n ToolParameter(\"action\", \"string\",\n \"Действие: call (по умолчанию)\", False, \"call\"),\n ToolParameter(\"endpoint\", \"number\",\n \"Индекс эндпоинта (0 по умолчанию)\", False, 0),\n ToolParameter(\"data\", \"string\",\n \"Данные для отправки (JSON строка или текст)\",\n False),\n ],\n handler=plugin_handler,\n category=\"plugins\",\n )\n\n return tool\n\n # ─── Persistence ─────────────────────────────────────────────────────\n\n def save(self) -> int:\n \"\"\"Сохранить плагины на диск.\"\"\"\n saved = 0\n for pid, plugin in self._plugins.items():\n try:\n filepath = self.PLUGINS_DIR / f\"{pid}.json\"\n data = {\n \"id\": plugin.id,\n \"config\": {\n \"name\": plugin.config.name,\n \"plugin_type\": plugin.config.plugin_type.value,\n \"base_url\": plugin.config.base_url,\n \"api_key\": plugin.config.api_key,\n \"api_secret\": plugin.config.api_secret,\n \"auth_type\": plugin.config.auth_type,\n \"auth_header\": plugin.config.auth_header,\n \"rate_limit\": plugin.config.rate_limit,\n \"timeout\": plugin.config.timeout,\n \"custom_headers\": plugin.config.custom_headers,\n \"metadata\": plugin.config.metadata,\n \"endpoints\": [\n {\n \"method\": ep.method,\n \"path\": ep.path,\n \"description\": ep.description,\n \"params\": ep.params,\n \"headers\": ep.headers,\n \"body_template\": ep.body_template,\n }\n for ep in plugin.config.endpoints\n ],\n },\n \"status\": plugin.status.value,\n \"owner_id\": plugin.owner_id,\n \"tool_name\": plugin.tool_name,\n \"usage_count\": plugin.usage_count,\n \"error_count\": plugin.error_count,\n \"created_at\": plugin.created_at.isoformat(),\n }\n filepath.write_text(json.dumps(\n data, ensure_ascii=False, indent=2))\n saved += 1\n except Exception as e:\n logger.error(f\"Failed to save plugin {pid}: {e}\")\n\n return saved\n\n def load(self) -> int:\n \"\"\"Загрузить плагины с диска.\"\"\"\n loaded = 0\n if not self.PLUGINS_DIR.exists():\n return 0\n\n for filepath in self.PLUGINS_DIR.glob(\"*.json\"):\n try:\n data = json.loads(filepath.read_text())\n cfg_data = data[\"config\"]\n\n endpoints = []\n for ep_data in cfg_data.get(\"endpoints\", []):\n endpoints.append(PluginEndpoint(\n method=ep_data.get(\"method\", \"GET\"),\n path=ep_data.get(\"path\", \"\"),\n description=ep_data.get(\"description\", \"\"),\n params=ep_data.get(\"params\", {}),\n headers=ep_data.get(\"headers\", {}),\n body_template=ep_data.get(\"body_template\", {}),\n ))\n\n config = PluginConfig(\n name=cfg_data[\"name\"],\n plugin_type=PluginType(\n cfg_data.get(\"plugin_type\", \"unknown\")),\n base_url=cfg_data.get(\"base_url\", \"\"),\n api_key=cfg_data.get(\"api_key\", \"\"),\n api_secret=cfg_data.get(\"api_secret\", \"\"),\n auth_type=cfg_data.get(\"auth_type\", \"bearer\"),\n auth_header=cfg_data.get(\"auth_header\", \"Authorization\"),\n rate_limit=cfg_data.get(\"rate_limit\", 60),\n timeout=cfg_data.get(\"timeout\", 30),\n custom_headers=cfg_data.get(\"custom_headers\", {}),\n metadata=cfg_data.get(\"metadata\", {}),\n endpoints=endpoints,\n )\n\n plugin = Plugin(\n id=data[\"id\"],\n config=config,\n status=PluginStatus(data.get(\"status\", \"pending\")),\n owner_id=data.get(\"owner_id\", 0),\n tool_name=data.get(\"tool_name\", \"\"),\n usage_count=data.get(\"usage_count\", 0),\n error_count=data.get(\"error_count\", 0),\n )\n\n created = data.get(\"created_at\")\n if created:\n try:\n plugin.created_at = datetime.fromisoformat(created)\n except (ValueError, TypeError):\n pass\n\n self._plugins[plugin.id] = plugin\n loaded += 1\n\n except Exception as e:\n logger.error(f\"Failed to load plugin {filepath}: {e}\")\n\n return loaded\n\n # ─── Stats ───────────────────────────────────────────────────────────\n\n def get_stats(self) -> dict[str, Any]:\n \"\"\"Получить статистику плагинов.\"\"\"\n plugins = list(self._plugins.values())\n return {\n \"total\": len(plugins),\n \"active\": sum(1 for p in plugins if p.status == PluginStatus.ACTIVE),\n \"errors\": sum(1 for p in plugins if p.status == PluginStatus.ERROR),\n \"by_type\": dict(Counter(\n p.config.plugin_type.value for p in plugins\n )),\n \"total_usage\": sum(p.usage_count for p in plugins),\n \"total_errors\": sum(p.error_count for p in plugins),\n }\n\n # ─── Internal ────────────────────────────────────────────────────────\n\n def _make_tool_name(self, name: str) -> str:\n \"\"\"Создать snake_case имя для Tool.\"\"\"\n # Очищаем и конвертируем\n clean = re.sub(r'[^a-zA-Z0-9\\s_-]', '', name)\n clean = re.sub(r'[\\s-]+', '_', clean.strip()).lower()\n return f\"plugin_{clean}\" if clean else f\"plugin_{int(time.time())}\"\n\n def _build_headers(self, config: PluginConfig) -> dict[str, str]:\n \"\"\"Построить заголовки запроса.\"\"\"\n headers = {\"Content-Type\": \"application/json\"}\n headers.update(config.custom_headers)\n\n if config.api_key:\n if config.auth_type == \"bearer\":\n headers[config.auth_header] = f\"Bearer {config.api_key}\"\n elif config.auth_type == \"api_key\":\n headers[config.auth_header] = config.api_key\n elif config.auth_type == \"basic\":\n import base64\n creds = f\"{config.api_key}:{config.api_secret}\"\n encoded = base64.b64encode(creds.encode()).decode()\n headers[\"Authorization\"] = f\"Basic {encoded}\"\n elif config.auth_type == \"custom\":\n headers[config.auth_header] = config.api_key\n\n return headers\n\n def _get_health_url(self, config: PluginConfig) -> str | None:\n \"\"\"URL для health check.\"\"\"\n if not config.base_url:\n return None\n\n # Специальные проверки по типу\n health_paths = {\n PluginType.LLM_API: \"/models\",\n PluginType.PAYMENT_API: \"/v1/balance\",\n }\n\n path = health_paths.get(config.plugin_type, \"\")\n if path:\n return f\"{config.base_url.rstrip('/')}{path}\"\n\n # Пробуем корень\n return config.base_url\n\n\n# ─── Для Counter ─────────────────────────────────────────────────────────────\n\n# ─── Глобальный экземпляр ────────────────────────────────────────────────────\n\nplugin_manager = PluginManager()\n"}, "3c0ea8c80fd888ce614519a2e59fbf509f0c8fd8": {"t": 1785770624.4552891, "n": 36985, "h": "af61d395f66d66a2", "c": "\"\"\"\nPDS-Ultimate Plugin System (Part 8)\n======================================\nДинамическое подключение ЛЮБЫХ API через чат.\n\nПользователь может подключить абсолютно любой API:\n1. Скидывает API ключ/токен/URL в чат\n2. Агент автоматически определяет тип сервиса\n3. Валидирует подключение\n4. Создаёт Tool и регистрирует в системе\n5. Всё работает без перезагрузки\n\nПоддерживаемые типы auto-detect:\n- OpenAI / ChatGPT API\n- Anthropic / Claude API\n- Google Cloud (Translate, Vision, Maps, etc.)\n- Stripe / PayPal (платежи)\n- Twilio (SMS/звонки)\n- SendGrid / Mailgun (email)\n- Any REST API (auto-discover endpoints)\n- Webhook endpoints\n- Custom function plugins (Python code)\n\nБезопасность:\n- Ключи шифруются AES-256\n- Sandbox для пользовательского кода\n- Rate limiting per-plugin\n- Автоматические health checks\n\"\"\"\n\nfrom __future__ import annotations\nfrom collections import Counter\n\nimport hashlib\nimport json\nimport re\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pds_ultimate.config import DATA_DIR, logger\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# PLUGIN TYPES & DATA CLASSES\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass PluginType(str, Enum):\n \"\"\"Тип плагина.\"\"\"\n REST_API = \"rest_api\" # Generic REST API\n LLM_API = \"llm_api\" # OpenAI/Anthropic/etc\n PAYMENT_API = \"payment_api\" # Stripe/PayPal\n MESSAGING_API = \"messaging_api\" # Twilio/SendGrid\n CLOUD_API = \"cloud_api\" # Google Cloud/AWS\n WEBHOOK = \"webhook\" # Incoming/outgoing webhooks\n CUSTOM_FUNC = \"custom_func\" # Custom Python function\n DATABASE = \"database\" # External DB connection\n UNKNOWN = \"unknown\"\n\n\nclass PluginStatus(str, Enum):\n \"\"\"Статус плагина.\"\"\"\n PENDING = \"pending\" # Ожидает валидации\n VALIDATING = \"validating\" # Проверяется\n ACTIVE = \"active\" # Работает\n INACTIVE = \"inactive\" # Деактивирован\n ERROR = \"error\" # Ошибка подключения\n EXPIRED = \"expired\" # Ключ истёк\n\n\n@dataclass\nclass PluginEndpoint:\n \"\"\"Эндпоинт REST API плагина.\"\"\"\n method: str = \"GET\" # GET, POST, PUT, DELETE\n path: str = \"\" # /api/v1/resource\n description: str = \"\"\n params: dict[str, str] = field(default_factory=dict)\n headers: dict[str, str] = field(default_factory=dict)\n body_template: dict[str, Any] = field(default_factory=dict)\n response_format: str = \"json\" # json, text, binary\n\n\n@dataclass\nclass PluginConfig:\n \"\"\"Конфигурация плагина.\"\"\"\n name: str # Человекочитаемое имя\n plugin_type: PluginType = PluginType.UNKNOWN\n base_url: str = \"\" # Базовый URL API\n api_key: str = \"\" # API ключ (шифруется)\n api_secret: str = \"\" # API секрет (шифруется)\n auth_type: str = \"bearer\" # bearer, basic, api_key, custom\n auth_header: str = \"Authorization\" # Заголовок авторизации\n endpoints: list[PluginEndpoint] = field(default_factory=list)\n rate_limit: int = 60 # Запросов в минуту\n timeout: int = 30 # Секунд\n custom_headers: dict[str, str] = field(default_factory=dict)\n metadata: dict[str, Any] = field(default_factory=dict)\n\n def to_dict(self) -> dict:\n return {\n \"name\": self.name,\n \"plugin_type\": self.plugin_type.value,\n \"base_url\": self.base_url,\n \"auth_type\": self.auth_type,\n \"rate_limit\": self.rate_limit,\n \"timeout\": self.timeout,\n \"endpoints_count\": len(self.endpoints),\n \"metadata\": self.metadata,\n }\n\n\n@dataclass\nclass PluginHealth:\n \"\"\"Результат health check плагина.\"\"\"\n healthy: bool = False\n latency_ms: int = 0\n status_code: int = 0\n error: str = \"\"\n checked_at: datetime = field(default_factory=datetime.utcnow)\n\n @property\n def status(self) -> str:\n if self.healthy:\n return f\"✅ OK ({self.latency_ms}ms)\"\n return f\"❌ Error: {self.error}\"\n\n\n@dataclass\nclass Plugin:\n \"\"\"\n Полный плагин с конфигом, статусом и статистикой.\n \"\"\"\n id: str # Уникальный ID\n config: PluginConfig # Конфигурация\n status: PluginStatus = PluginStatus.PENDING\n owner_id: int = 0 # Telegram user ID владельца\n created_at: datetime = field(default_factory=datetime.utcnow)\n last_used: datetime | None = None\n usage_count: int = 0\n error_count: int = 0\n last_health: PluginHealth | None = None\n tool_name: str = \"\" # Имя зарегистрированного Tool\n\n def to_dict(self) -> dict:\n return {\n \"id\": self.id,\n \"name\": self.config.name,\n \"type\": self.config.plugin_type.value,\n \"status\": self.status.value,\n \"owner_id\": self.owner_id,\n \"usage_count\": self.usage_count,\n \"error_count\": self.error_count,\n \"tool_name\": self.tool_name,\n \"created_at\": self.created_at.isoformat(),\n \"last_used\": self.last_used.isoformat() if self.last_used else None,\n \"health\": self.last_health.status if self.last_health else \"unknown\",\n }\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# API DETECTOR — Автоопределение типа API из текста\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass APIDetector:\n \"\"\"\n Определяет тип API по ключу/URL/описанию.\n\n Паттерны:\n - sk-... → OpenAI\n - sk-ant-... → Anthropic\n - AIza... → Google API Key\n - AKIA... → AWS\n - pk_live/sk_live → Stripe\n - SG.... → SendGrid\n - AC... + auth token → Twilio\n \"\"\"\n\n # Паттерны API ключей\n PATTERNS: list[tuple[str, PluginType, str]] = [\n # OpenAI\n (r\"sk-[a-zA-Z0-9]{20,}\", PluginType.LLM_API, \"OpenAI\"),\n (r\"sk-proj-[a-zA-Z0-9_-]{40,}\", PluginType.LLM_API, \"OpenAI\"),\n # Anthropic\n (r\"sk-ant-[a-zA-Z0-9_-]{40,}\", PluginType.LLM_API, \"Anthropic\"),\n # Google\n (r\"AIza[a-zA-Z0-9_-]{35}\", PluginType.CLOUD_API, \"Google\"),\n # AWS\n (r\"AKIA[A-Z0-9]{16}\", PluginType.CLOUD_API, \"AWS\"),\n # Stripe\n (r\"(pk|sk)_(live|test)_[a-zA-Z0-9]{20,}\",\n PluginType.PAYMENT_API, \"Stripe\"),\n # SendGrid\n (r\"SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}\",\n PluginType.MESSAGING_API, \"SendGrid\"),\n # Twilio\n (r\"AC[a-f0-9]{32}\", PluginType.MESSAGING_API, \"Twilio\"),\n # Telegram Bot Token\n (r\"\\d{8,10}:[A-Za-z0-9_-]{35}\", PluginType.MESSAGING_API, \"Telegram\"),\n # DeepSeek\n (r\"sk-[a-f0-9]{32}\", PluginType.LLM_API, \"DeepSeek\"),\n # Generic Bearer Token\n (r\"[A-Za-z0-9_-]{40,}\", PluginType.REST_API, \"Generic API\"),\n ]\n\n # URL-based detection\n URL_PATTERNS: list[tuple[str, PluginType, str]] = [\n (r\"api\\.openai\\.com\", PluginType.LLM_API, \"OpenAI\"),\n (r\"api\\.anthropic\\.com\", PluginType.LLM_API, \"Anthropic\"),\n (r\"api\\.deepseek\\.com\", PluginType.LLM_API, \"DeepSeek\"),\n (r\"api\\.stripe\\.com\", PluginType.PAYMENT_API, \"Stripe\"),\n (r\"api\\.twilio\\.com\", PluginType.MESSAGING_API, \"Twilio\"),\n (r\"api\\.sendgrid\\.com\", PluginType.MESSAGING_API, \"SendGrid\"),\n (r\"googleapis\\.com\", PluginType.CLOUD_API, \"Google\"),\n (r\"amazonaws\\.com\", PluginType.CLOUD_API, \"AWS\"),\n (r\"api\\.telegram\\.org\", PluginType.MESSAGING_API, \"Telegram\"),\n ]\n\n @classmethod\n def detect_from_text(cls, text: str) -> list[dict[str, Any]]:\n \"\"\"\n Определить API ключи и URLs из произвольного текста.\n\n Returns:\n Список обнаружений: [{\"key\": \"...\", \"type\": ..., \"service\": \"...\"}]\n \"\"\"\n detections: list[dict[str, Any]] = []\n\n # Ищем API ключи\n for pattern, plugin_type, service in cls.PATTERNS:\n matches = re.finditer(pattern, text)\n for match in matches:\n key = match.group(0)\n # Проверяем что это не часть URL\n if key not in [d[\"key\"] for d in detections]:\n detections.append({\n \"key\": key,\n \"type\": plugin_type,\n \"service\": service,\n \"confidence\": 0.9 if service != \"Generic API\" else 0.5,\n })\n\n # Ищем URLs\n url_pattern = r'https?://[^\\s<>\"{}|\\\\^`\\[\\]]+'\n urls = re.findall(url_pattern, text)\n for url in urls:\n for pattern, plugin_type, service in cls.URL_PATTERNS:\n if re.search(pattern, url):\n detections.append({\n \"url\": url,\n \"type\": plugin_type,\n \"service\": service,\n \"confidence\": 0.95,\n })\n break\n else:\n # Unknown REST API URL\n if \"/api/\" in url or \"/v1/\" in url or \"/v2/\" in url:\n detections.append({\n \"url\": url,\n \"type\": PluginType.REST_API,\n \"service\": \"Custom REST API\",\n \"confidence\": 0.6,\n })\n\n return detections\n\n @classmethod\n def get_service_info(cls, service: str) -> dict[str, Any]:\n \"\"\"Получить информацию о сервисе для onboarding.\"\"\"\n services = {\n \"OpenAI\": {\n \"name\": \"OpenAI / ChatGPT\",\n \"description\": \"AI модели (GPT-4, DALL-E, Whisper)\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://platform.openai.com/docs\",\n \"capabilities\": [\"Генерация текста\", \"Анализ изображений\", \"Распознавание речи\"],\n \"setup_guide\": (\n \"1. Зайдите на https://platform.openai.com\\n\"\n \"2. Settings → API Keys → Create new secret key\\n\"\n \"3. Скопируйте ключ (начинается с sk-...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"Anthropic\": {\n \"name\": \"Anthropic / Claude\",\n \"description\": \"AI модель Claude для анализа и генерации\",\n \"base_url\": \"https://api.anthropic.com/v1\",\n \"auth_type\": \"bearer\",\n \"auth_header\": \"x-api-key\",\n \"docs_url\": \"https://docs.anthropic.com\",\n \"capabilities\": [\"Генерация текста\", \"Анализ документов\", \"Код\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.anthropic.com\\n\"\n \"2. Settings → API Keys → Create Key\\n\"\n \"3. Скопируйте ключ (начинается с sk-ant-...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"Stripe\": {\n \"name\": \"Stripe (Платежи)\",\n \"description\": \"Приём и обработка платежей\",\n \"base_url\": \"https://api.stripe.com/v1\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://stripe.com/docs/api\",\n \"capabilities\": [\"Приём платежей\", \"Подписки\", \"Инвойсы\", \"Refunds\"],\n \"setup_guide\": (\n \"1. Зайдите на https://dashboard.stripe.com\\n\"\n \"2. Developers → API Keys\\n\"\n \"3. Скопируйте Secret Key (sk_live_... или sk_test_...)\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n \"SendGrid\": {\n \"name\": \"SendGrid (Email)\",\n \"description\": \"Отправка email: рассылки, уведомления\",\n \"base_url\": \"https://api.sendgrid.com/v3\",\n \"auth_type\": \"bearer\",\n \"docs_url\": \"https://docs.sendgrid.com\",\n \"capabilities\": [\"Отправка email\", \"Шаблоны\", \"Аналитика\"],\n \"setup_guide\": (\n \"1. Зайдите на https://app.sendgrid.com\\n\"\n \"2. Settings → API Keys → Create API Key\\n\"\n \"3. Выберите Full Access\\n\"\n \"4. Скопируйте ключ (SG.xxx) и отправьте мне\"\n ),\n },\n \"Twilio\": {\n \"name\": \"Twilio (SMS/Звонки)\",\n \"description\": \"SMS, звонки, WhatsApp Business API\",\n \"base_url\": \"https://api.twilio.com/2010-04-01\",\n \"auth_type\": \"basic\",\n \"docs_url\": \"https://www.twilio.com/docs\",\n \"capabilities\": [\"SMS\", \"Звонки\", \"WhatsApp\", \"Видео\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.twilio.com\\n\"\n \"2. Скопируйте Account SID (AC...) и Auth Token\\n\"\n \"3. Отправьте оба значения мне в чат\"\n ),\n },\n \"Google\": {\n \"name\": \"Google Cloud\",\n \"description\": \"Google Translate, Vision, Maps, Sheets и другие\",\n \"base_url\": \"https://googleapis.com\",\n \"auth_type\": \"api_key\",\n \"docs_url\": \"https://console.cloud.google.com\",\n \"capabilities\": [\"Перевод\", \"Распознавание изображений\", \"Карты\", \"Таблицы\"],\n \"setup_guide\": (\n \"1. Зайдите на https://console.cloud.google.com\\n\"\n \"2. APIs & Services → Credentials → Create Credentials\\n\"\n \"3. Выберите API Key\\n\"\n \"4. Скопируйте ключ (AIza...) и отправьте мне\"\n ),\n },\n \"Telegram\": {\n \"name\": \"Telegram Bot\",\n \"description\": \"Дополнительный Telegram бот\",\n \"base_url\": \"https://api.telegram.org\",\n \"auth_type\": \"custom\",\n \"docs_url\": \"https://core.telegram.org/bots/api\",\n \"capabilities\": [\"Отправка сообщений\", \"Управление каналами\", \"Inline боты\"],\n \"setup_guide\": (\n \"1. Напишите @BotFather в Telegram\\n\"\n \"2. Отправьте /newbot и следуйте инструкциям\\n\"\n \"3. Скопируйте токен бота\\n\"\n \"4. Отправьте его мне в чат\"\n ),\n },\n }\n return services.get(service, {\n \"name\": service,\n \"description\": f\"API сервис: {service}\",\n \"setup_guide\": (\n \"Отправьте мне:\\n\"\n \"1. API ключ или токен\\n\"\n \"2. URL сервиса (если есть)\\n\"\n \"3. Краткое описание что этот API делает\\n\\n\"\n \"Я автоматически настрою подключение.\"\n ),\n })\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# RATE LIMITER — Per-plugin rate limiting\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass RateLimiter:\n \"\"\"Ограничение частоты запросов к API плагина.\"\"\"\n\n def __init__(self):\n self._windows: dict[str, list[float]] = {}\n\n def check(self, plugin_id: str, limit: int = 60) -> bool:\n \"\"\"Проверить, разрешён ли запрос (True = ОК).\"\"\"\n now = time.time()\n window = self._windows.setdefault(plugin_id, [])\n\n # Удаляем старые записи (>60 сек)\n cutoff = now - 60\n self._windows[plugin_id] = [t for t in window if t > cutoff]\n window = self._windows[plugin_id]\n\n if len(window) >= limit:\n return False\n\n window.append(now)\n return True\n\n def remaining(self, plugin_id: str, limit: int = 60) -> int:\n \"\"\"Сколько запросов осталось в текущем окне.\"\"\"\n now = time.time()\n window = self._windows.get(plugin_id, [])\n active = [t for t in window if t > now - 60]\n return max(0, limit - len(active))\n\n def reset(self, plugin_id: str) -> None:\n \"\"\"Сбросить лимит.\"\"\"\n self._windows.pop(plugin_id, None)\n\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# PLUGIN MANAGER — Центральный менеджер\n# ═══════════════════════════════════════════════════════════════════════════════\n\n\nclass PluginManager:\n \"\"\"\n Центральный менеджер плагинов.\n\n Отвечает за:\n - Регистрацию и удаление плагинов\n - Auto-detect API типа из текста пользователя\n - Валидацию подключения\n - Создание Tool для ToolRegistry\n - Health checks\n - Persistence (сохранение/загрузка)\n \"\"\"\n\n PLUGINS_DIR = DATA_DIR / \"plugins\"\n\n def __init__(self):\n self._plugins: dict[str, Plugin] = {}\n self._rate_limiter = RateLimiter()\n self._detector = APIDetector()\n self.PLUGINS_DIR.mkdir(parents=True, exist_ok=True)\n\n # ─── Core Operations ─────────────────────────────────────────────────\n\n def register_plugin(\n self,\n config: PluginConfig,\n owner_id: int = 0,\n ) -> Plugin:\n \"\"\"\n Зарегистрировать новый плагин.\n\n Args:\n config: Конфигурация плагина\n owner_id: ID владельца\n\n Returns:\n Plugin с уникальным ID\n \"\"\"\n # Генерируем ID\n raw = f\"{config.name}:{config.base_url}:{owner_id}:{time.time()}\"\n plugin_id = hashlib.md5(raw.encode()).hexdigest()[:12]\n\n # Имя инструмента\n tool_name = self._make_tool_name(config.name)\n\n plugin = Plugin(\n id=plugin_id,\n config=config,\n status=PluginStatus.PENDING,\n owner_id=owner_id,\n tool_name=tool_name,\n )\n\n self._plugins[plugin_id] = plugin\n logger.info(\n f\"Plugin registered: {config.name} [{config.plugin_type.value}] \"\n f\"id={plugin_id} owner={owner_id}\"\n )\n\n return plugin\n\n def unregister_plugin(self, plugin_id: str) -> bool:\n \"\"\"Удалить плагин.\"\"\"\n plugin = self._plugins.pop(plugin_id, None)\n if plugin:\n self._rate_limiter.reset(plugin_id)\n logger.info(\n f\"Plugin unregistered: {plugin.config.name} id={plugin_id}\")\n return True\n return False\n\n def get_plugin(self, plugin_id: str) -> Plugin | None:\n \"\"\"Получить плагин по ID.\"\"\"\n return self._plugins.get(plugin_id)\n\n def get_by_name(self, name: str) -> Plugin | None:\n \"\"\"Получить плагин по имени.\"\"\"\n name_lower = name.lower()\n for p in self._plugins.values():\n if p.config.name.lower() == name_lower or p.tool_name == name_lower:\n return p\n return None\n\n def get_user_plugins(self, owner_id: int) -> list[Plugin]:\n \"\"\"Получить все плагины пользователя.\"\"\"\n return [p for p in self._plugins.values() if p.owner_id == owner_id]\n\n def get_active_plugins(self) -> list[Plugin]:\n \"\"\"Получить все активные плагины.\"\"\"\n return [p for p in self._plugins.values()\n if p.status == PluginStatus.ACTIVE]\n\n @property\n def count(self) -> int:\n return len(self._plugins)\n\n @property\n def active_count(self) -> int:\n return len(self.get_active_plugins())\n\n # ─── Auto-detect from chat text ──────────────────────────────────────\n\n def detect_from_message(self, text: str) -> list[dict[str, Any]]:\n \"\"\"\n Определить API из текста сообщения пользователя.\n\n Возвращает список обнаруженных API с рекомендациями.\n \"\"\"\n return self._detector.detect_from_text(text)\n\n def get_onboarding_text(self, service: str | None = None) -> str:\n \"\"\"\n Текст приветствия/онбординга для подключения API.\n\n Если service указан — гайд для конкретного сервиса.\n Если нет — общий обзор возможностей.\n \"\"\"\n if service:\n info = self._detector.get_service_info(service)\n lines = [\n f\"🔌 **{info.get('name', service)}**\",\n f\"📝 {info.get('description', '')}\",\n \"\",\n ]\n capabilities = info.get(\"capabilities\", [])\n if capabilities:\n lines.append(\"🎯 Возможности:\")\n for cap in capabilities:\n lines.append(f\" • {cap}\")\n lines.append(\"\")\n\n guide = info.get(\"setup_guide\", \"\")\n if guide:\n lines.append(\"📋 Как подключить:\")\n lines.append(guide)\n\n return \"\\n\".join(lines)\n\n # Общий обзор\n return (\n \"🔌 **Подключение внешних API**\\n\\n\"\n \"Я могу работать с любыми API! Вот популярные:\\n\\n\"\n \"🤖 **AI модели:** OpenAI (GPT-4), Anthropic (Claude)\\n\"\n \"💳 **Платежи:** Stripe, PayPal\\n\"\n \"📧 **Email:** SendGrid, Mailgun\\n\"\n \"📱 **SMS/Звонки:** Twilio\\n\"\n \"☁️ **Облако:** Google Cloud, AWS\\n\"\n \"🔗 **Любой REST API:** Просто скиньте URL и ключ\\n\\n\"\n \"Для подключения просто отправьте мне:\\n\"\n \"• API ключ или токен\\n\"\n \"• URL сервиса\\n\"\n \"• Или скажите какой сервис хотите подключить\\n\\n\"\n \"Я автоматически всё определю и настрою! 🚀\"\n )\n\n # ─── Validation ──────────────────────────────────────────────────────\n\n async def validate_plugin(self, plugin_id: str) -> PluginHealth:\n \"\"\"\n Валидировать подключение плагина (health check).\n\n Выполняет тестовый запрос к API.\n \"\"\"\n plugin = self._plugins.get(plugin_id)\n if not plugin:\n return PluginHealth(error=\"Plugin not found\")\n\n plugin.status = PluginStatus.VALIDATING\n start = time.time()\n\n try:\n import httpx\n\n headers = self._build_headers(plugin.config)\n timeout = httpx.Timeout(plugin.config.timeout)\n\n async with httpx.AsyncClient(timeout=timeout) as client:\n # Определяем URL для проверки\n check_url = self._get_health_url(plugin.config)\n if not check_url:\n # Нет URL для проверки — считаем OK\n health = PluginHealth(\n healthy=True,\n latency_ms=int((time.time() - start) * 1000),\n status_code=200,\n )\n plugin.status = PluginStatus.ACTIVE\n plugin.last_health = health\n return health\n\n response = await client.get(check_url, headers=headers)\n latency = int((time.time() - start) * 1000)\n\n healthy = response.status_code < 500\n health = PluginHealth(\n healthy=healthy,\n latency_ms=latency,\n status_code=response.status_code,\n )\n\n plugin.status = PluginStatus.ACTIVE if healthy else PluginStatus.ERROR\n plugin.last_health = health\n return health\n\n except Exception as e:\n latency = int((time.time() - start) * 1000)\n health = PluginHealth(\n healthy=False,\n latency_ms=latency,\n error=str(e),\n )\n plugin.status = PluginStatus.ERROR\n plugin.last_health = health\n return health\n\n # ─── Execute plugin call ─────────────────────────────────────────────\n\n async def execute(\n self,\n plugin_id: str,\n endpoint_index: int = 0,\n params: dict[str, Any] | None = None,\n body: dict[str, Any] | None = None,\n ) -> dict[str, Any]:\n \"\"\"\n Выполнить запрос через плагин.\n\n Args:\n plugin_id: ID плагина\n endpoint_index: Индекс эндпоинта\n params: Query параметры\n body: Тело запроса\n\n Returns:\n dict с результатом\n \"\"\"\n plugin = self._plugins.get(plugin_id)\n if not plugin:\n return {\"success\": False, \"error\": \"Plugin not found\"}\n\n if plugin.status != PluginStatus.ACTIVE:\n return {\"success\": False, \"error\": f\"Plugin status: {plugin.status.value}\"}\n\n # Rate limiting\n if not self._rate_limiter.check(plugin_id, plugin.config.rate_limit):\n remaining = self._rate_limiter.remaining(\n plugin_id, plugin.config.rate_limit)\n return {\n \"success\": False,\n \"error\": f\"Rate limit exceeded. Remaining: {remaining}\",\n }\n\n try:\n import httpx\n\n headers = self._build_headers(plugin.config)\n timeout = httpx.Timeout(plugin.config.timeout)\n\n # Определяем endpoint\n if plugin.config.endpoints and endpoint_index < len(plugin.config.endpoints):\n ep = plugin.config.endpoints[endpoint_index]\n url = f\"{plugin.config.base_url.rstrip('/')}/{ep.path.lstrip('/')}\"\n method = ep.method\n # Merge body template with provided body\n merged_body = {**ep.body_template, **(body or {})}\n else:\n url = plugin.config.base_url\n method = \"POST\" if body else \"GET\"\n merged_body = body\n\n async with httpx.AsyncClient(timeout=timeout) as client:\n if method.upper() == \"GET\":\n response = await client.get(url, headers=headers, params=params)\n elif method.upper() == \"POST\":\n response = await client.post(\n url, headers=headers, params=params, json=merged_body)\n elif method.upper() == \"PUT\":\n response = await client.put(\n url, headers=headers, params=params, json=merged_body)\n elif method.upper() == \"DELETE\":\n response = await client.delete(url, headers=headers, params=params)\n else:\n return {\"success\": False, \"error\": f\"Unknown method: {method}\"}\n\n plugin.usage_count += 1\n plugin.last_used = datetime.utcnow()\n\n # Парсим ответ\n try:\n data = response.json()\n except Exception:\n data = {\"text\": response.text[:2000]}\n\n return {\n \"success\": response.status_code < 400,\n \"status_code\": response.status_code,\n \"data\": data,\n }\n\n except Exception as e:\n plugin.error_count += 1\n return {\"success\": False, \"error\": str(e)}\n\n # ─── Create Tool from Plugin ─────────────────────────────────────────\n\n def create_tool_for_plugin(self, plugin: Plugin) -> \"Tool\":\n \"\"\"\n Создать Tool для ToolRegistry из плагина.\n\n Генерирует async handler, который вызывает plugin API.\n \"\"\"\n from pds_ultimate.core.tools import Tool, ToolParameter, ToolResult\n\n plugin_id = plugin.id\n manager = self # Capture reference\n\n async def plugin_handler(\n action: str = \"call\",\n endpoint: int = 0,\n data: str = \"\",\n **kwargs,\n ) -> ToolResult:\n \"\"\"Обработчик плагина (сгенерирован автоматически).\"\"\"\n body = None\n if data:\n try:\n body = json.loads(data)\n except json.JSONDecodeError:\n body = {\"text\": data}\n\n result = await manager.execute(\n plugin_id=plugin_id,\n endpoint_index=int(endpoint),\n body=body,\n )\n\n if result[\"success\"]:\n output = json.dumps(result.get(\"data\", {}),\n ensure_ascii=False, default=str)\n return ToolResult(\n tool_name=plugin.tool_name,\n success=True,\n output=output[:3000],\n data=result.get(\"data\"),\n )\n else:\n return ToolResult(\n tool_name=plugin.tool_name,\n success=False,\n output=\"\",\n error=result.get(\"error\", \"Unknown error\"),\n )\n\n # Описание из конфигурации\n desc_parts = [f\"Плагин: {plugin.config.name}\"]\n if plugin.config.plugin_type != PluginType.UNKNOWN:\n desc_parts.append(f\"({plugin.config.plugin_type.value})\")\n if plugin.config.endpoints:\n desc_parts.append(\n f\"Эндпоинтов: {len(plugin.config.endpoints)}\")\n\n tool = Tool(\n name=plugin.tool_name,\n description=\" \".join(desc_parts),\n parameters=[\n ToolParameter(\"action\", \"string\",\n \"Действие: call (по умолчанию)\", False, \"call\"),\n ToolParameter(\"endpoint\", \"number\",\n \"Индекс эндпоинта (0 по умолчанию)\", False, 0),\n ToolParameter(\"data\", \"string\",\n \"Данные для отправки (JSON строка или текст)\",\n False),\n ],\n handler=plugin_handler,\n category=\"plugins\",\n )\n\n return tool\n\n # ─── Persistence ─────────────────────────────────────────────────────\n\n def save(self) -> int:\n \"\"\"Сохранить плагины на диск.\"\"\"\n saved = 0\n for pid, plugin in self._plugins.items():\n try:\n filepath = self.PLUGINS_DIR / f\"{pid}.json\"\n data = {\n \"id\": plugin.id,\n \"config\": {\n \"name\": plugin.config.name,\n \"plugin_type\": plugin.config.plugin_type.value,\n \"base_url\": plugin.config.base_url,\n \"api_key\": plugin.config.api_key,\n \"api_secret\": plugin.config.api_secret,\n \"auth_type\": plugin.config.auth_type,\n \"auth_header\": plugin.config.auth_header,\n \"rate_limit\": plugin.config.rate_limit,\n \"timeout\": plugin.config.timeout,\n \"custom_headers\": plugin.config.custom_headers,\n \"metadata\": plugin.config.metadata,\n \"endpoints\": [\n {\n \"method\": ep.method,\n \"path\": ep.path,\n \"description\": ep.description,\n \"params\": ep.params,\n \"headers\": ep.headers,\n \"body_template\": ep.body_template,\n }\n for ep in plugin.config.endpoints\n ],\n },\n \"status\": plugin.status.value,\n \"owner_id\": plugin.owner_id,\n \"tool_name\": plugin.tool_name,\n \"usage_count\": plugin.usage_count,\n \"error_count\": plugin.error_count,\n \"created_at\": plugin.created_at.isoformat(),\n }\n filepath.write_text(json.dumps(\n data, ensure_ascii=False, indent=2))\n saved += 1\n except Exception as e:\n logger.error(f\"Failed to save plugin {pid}: {e}\")\n\n return saved\n\n def load(self) -> int:\n \"\"\"Загрузить плагины с диска.\"\"\"\n loaded = 0\n if not self.PLUGINS_DIR.exists():\n return 0\n\n for filepath in self.PLUGINS_DIR.glob(\"*.json\"):\n try:\n data = json.loads(filepath.read_text())\n cfg_data = data[\"config\"]\n\n endpoints = []\n for ep_data in cfg_data.get(\"endpoints\", []):\n endpoints.append(PluginEndpoint(\n method=ep_data.get(\"method\", \"GET\"),\n path=ep_data.get(\"path\", \"\"),\n description=ep_data.get(\"description\", \"\"),\n params=ep_data.get(\"params\", {}),\n headers=ep_data.get(\"headers\", {}),\n body_template=ep_data.get(\"body_template\", {}),\n ))\n\n config = PluginConfig(\n name=cfg_data[\"name\"],\n plugin_type=PluginType(\n cfg_data.get(\"plugin_type\", \"unknown\")),\n base_url=cfg_data.get(\"base_url\", \"\"),\n api_key=cfg_data.get(\"api_key\", \"\"),\n api_secret=cfg_data.get(\"api_secret\", \"\"),\n auth_type=cfg_data.get(\"auth_type\", \"bearer\"),\n auth_header=cfg_data.get(\"auth_header\", \"Authorization\"),\n rate_limit=cfg_data.get(\"rate_limit\", 60),\n timeout=cfg_data.get(\"timeout\", 30),\n custom_headers=cfg_data.get(\"custom_headers\", {}),\n metadata=cfg_data.get(\"metadata\", {}),\n endpoints=endpoints,\n )\n\n plugin = Plugin(\n id=data[\"id\"],\n config=config,\n status=PluginStatus(data.get(\"status\", \"pending\")),\n owner_id=data.get(\"owner_id\", 0),\n tool_name=data.get(\"tool_name\", \"\"),\n usage_count=data.get(\"usage_count\", 0),\n error_count=data.get(\"error_count\", 0),\n )\n\n created = data.get(\"created_at\")\n if created:\n try:\n plugin.created_at = datetime.fromisoformat(created)\n except (ValueError, TypeError):\n pass\n\n self._plugins[plugin.id] = plugin\n loaded += 1\n\n except Exception as e:\n logger.error(f\"Failed to load plugin {filepath}: {e}\")\n\n return loaded\n\n # ─── Stats ───────────────────────────────────────────────────────────\n\n def get_stats(self) -> dict[str, Any]:\n \"\"\"Получить статистику плагинов.\"\"\"\n plugins = list(self._plugins.values())\n return {\n \"total\": len(plugins),\n \"active\": sum(1 for p in plugins if p.status == PluginStatus.ACTIVE),\n \"errors\": sum(1 for p in plugins if p.status == PluginStatus.ERROR),\n \"by_type\": dict(Counter(\n p.config.plugin_type.value for p in plugins\n )),\n \"total_usage\": sum(p.usage_count for p in plugins),\n \"total_errors\": sum(p.error_count for p in plugins),\n }\n\n # ─── Internal ────────────────────────────────────────────────────────\n\n def _make_tool_name(self, name: str) -> str:\n \"\"\"Создать snake_case имя для Tool.\"\"\"\n # Очищаем и конвертируем\n clean = re.sub(r'[^a-zA-Z0-9\\s_-]', '', name)\n clean = re.sub(r'[\\s-]+', '_', clean.strip()).lower()\n return f\"plugin_{clean}\" if clean else f\"plugin_{int(time.time())}\"\n\n def _build_headers(self, config: PluginConfig) -> dict[str, str]:\n \"\"\"Построить заголовки запроса.\"\"\"\n headers = {\"Content-Type\": \"application/json\"}\n headers.update(config.custom_headers)\n\n if config.api_key:\n if config.auth_type == \"bearer\":\n headers[config.auth_header] = f\"Bearer {config.api_key}\"\n elif config.auth_type == \"api_key\":\n headers[config.auth_header] = config.api_key\n elif config.auth_type == \"basic\":\n import base64\n creds = f\"{config.api_key}:{config.api_secret}\"\n encoded = base64.b64encode(creds.encode()).decode()\n headers[\"Authorization\"] = f\"Basic {encoded}\"\n elif config.auth_type == \"custom\":\n headers[config.auth_header] = config.api_key\n\n return headers\n\n def _get_health_url(self, config: PluginConfig) -> str | None:\n \"\"\"URL для health check.\"\"\"\n if not config.base_url:\n return None\n\n # Специальные проверки по типу\n health_paths = {\n PluginType.LLM_API: \"/models\",\n PluginType.PAYMENT_API: \"/v1/balance\",\n }\n\n path = health_paths.get(config.plugin_type, \"\")\n if path:\n return f\"{config.base_url.rstrip('/')}{path}\"\n\n # Пробуем корень\n return config.base_url\n\n\n# ─── Для Counter ─────────────────────────────────────────────────────────────\n\n# ─── Глобальный экземпляр ────────────────────────────────────────────────────\n\nplugin_manager = PluginManager()\n"}}}}