Files

1 line
111 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}