diff --git "a/server.py" "b/server.py" deleted file mode 100644--- "a/server.py" +++ /dev/null @@ -1,2490 +0,0 @@ -# ============================================================================ -# 🧠 PINKSKY v6.0 — ПОЛНЫЙ ДОСТУП В ИНТЕРНЕТ ДЛЯ ВСЕХ РЕЖИМОВ -# ============================================================================ - -# === БАЗОВЫЕ ИМПОРТЫ === -import os -import json -import urllib.request -import sys -import time -import ssl -import requests -import threading -import socket -import re -import io -import signal -import weakref -import fnmatch -import hashlib -from http.server import BaseHTTPRequestHandler, HTTPServer -from typing import Dict, List, Optional, Any, Tuple, Callable -from dataclasses import dataclass, field -from datetime import datetime, timedelta -from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError -from collections import Counter -from functools import wraps - -# ============================================================================ -# 📦 ПРОВЕРКА ЗАВИСИМОСТЕЙ -# ============================================================================ - -try: - from interpreter import interpreter -except ImportError: - print("❌ open-interpreter не найден. Установи: pip install open-interpreter") - sys.exit(1) - -try: - from bs4 import BeautifulSoup -except ImportError: - print("⚠️ BeautifulSoup не найден. Установи: pip install beautifulsoup4") - # Создаём заглушку - class BeautifulSoup: - def __init__(self, *args, **kwargs): - pass - -# ============================================================================ -# 🔧 CUSTOM DNS RESOLVER -# ============================================================================ - -HF_DOMAIN = "api-inference.huggingface.co" -resolved_hf_ip = None - -def get_hf_ip_via_google(): - global resolved_hf_ip - if resolved_hf_ip: - return resolved_hf_ip - try: - req = urllib.request.Request(f"https://dns.google/resolve?name={HF_DOMAIN}&type=A") - with urllib.request.urlopen(req, timeout=5) as response: - data = json.loads(response.read().decode('utf-8')) - for answer in data.get("Answer", []): - if answer.get("type") == 1: - resolved_hf_ip = answer.get("data") - print(f"🔍 [DNS] HF IP через Google: {resolved_hf_ip}") - return resolved_hf_ip - except Exception as e: - print(f"⚠️ [DNS] Google DoH ошибка: {e}") - return None - -original_getaddrinfo = socket.getaddrinfo - -def custom_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): - if host == HF_DOMAIN: - ip = get_hf_ip_via_google() - if ip: - return [(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, '', (ip, port))] - return original_getaddrinfo(host, port, family, type, proto, flags) - -socket.getaddrinfo = custom_getaddrinfo - -# ============================================================================ -# 🔐 ENVIRONMENT CONFIG -# ============================================================================ - -PORT = 7860 -TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip() -ALLOWED_USER = os.environ.get("TELEGRAM_ALLOWED_USERS", "").strip() -CF_URL = os.environ.get("CF_WORKER_URL", "").rstrip('/') - -API_KEY = os.environ.get("NVIDIA_API_KEY", os.environ.get("OPENAI_API_KEY", "")).strip() -API_BASE = os.environ.get("NVIDIA_API_BASE", os.environ.get("OPENAI_API_BASE", "")).strip().rstrip('/') -if API_BASE and not API_BASE.endswith("/v1"): - API_BASE += "/v1" - -HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() -HF_FALLBACK_MODEL = "Qwen/Qwen2.5-72B-Instruct" -# ============================================================================ -# 📁 FOLDERS -# ============================================================================ - -for folder in ["skills", "projects", "downloads", "prompts", "build_modes"]: - os.makedirs(folder, exist_ok=True) - -ROLES_FILE = "prompts/roles.json" -MODELS_FILE = "prompts/models.json" -CONDUCTORS_FILE = "prompts/conductors.json" -HISTORY_FILE = "prompts/history.json" -BUILD_MODES_FILE = "build_modes/build_modes.json" - -# ============================================================================ -# 🏗️ DATACLASSES -# ============================================================================ - -@dataclass -class ModelConfig: - name: str - provider: str - endpoint: str - api_key_env: str - context_window: int = 32000 - max_tokens: int = 8000 - cost_per_1k_input: float = 0.0 - cost_per_1k_output: float = 0.0 - coding_rank: int = 50 - speed_rank: int = 50 - reasoning_rank: int = 50 - tags: List[str] = field(default_factory=list) - -@dataclass -class Role: - name: str - prompt: str - description: str - preferred_models: List[str] = field(default_factory=list) - complexity: str = "medium" - tags: List[str] = field(default_factory=list) - tools: List[str] = field(default_factory=list) # 🔥 Список доступных инструментов - -@dataclass -class Conductor: - name: str - prompt: str - description: str - strategy: str = "parallel" - max_agents: int = 3 - cost_aware: bool = True - auto_rank_by: str = "coding" - -# ============================================================================ -# 🛑 PROCESS MANAGER -# ============================================================================ - -class ProcessManager: - def __init__(self): - self.active_threads: List[threading.Thread] = [] - self.cancel_flags: Dict[int, bool] = {} - self.lock = threading.Lock() - self.executor = ThreadPoolExecutor(max_workers=10) - self.futures = [] - - def register_thread(self, thread: threading.Thread) -> None: - with self.lock: - self.active_threads.append(thread) - self.cancel_flags[thread.ident] = False - - def register_future(self, future) -> None: - with self.lock: - self.futures.append(future) - - def cancel_all(self) -> str: - with self.lock: - for future in self.futures: - if not future.done(): - future.cancel() - self.futures.clear() - for thread_id in self.cancel_flags: - self.cancel_flags[thread_id] = True - for thread in self.active_threads: - if thread.is_alive(): - try: - thread.join(timeout=0.5) - except Exception: - pass - self.active_threads.clear() - self.cancel_flags.clear() - - try: - if hasattr(interpreter, 'cancel'): - interpreter.cancel() - except Exception: - pass - - STATE.cancel_flag = True - STATE.current_mode = "paused" - return "✅ Все процессы генерации остановлены!" - - def is_cancelled(self, thread_id: int = None) -> bool: - if thread_id is None: - thread_id = threading.current_thread().ident - with self.lock: - return self.cancel_flags.get(thread_id, False) - - def clear(self) -> None: - with self.lock: - self.active_threads = [t for t in self.active_threads if t.is_alive()] - - def get_active_count(self) -> int: - with self.lock: - return len([t for t in self.active_threads if t.is_alive()]) - -PROCESS_MANAGER = ProcessManager() -# ============================================================================ -# 🥇 AUTO-RANKING MODELS -# ============================================================================ - -MODEL_RANKING = { - "deepseek-v4-pro": { - "endpoint": "deepseek-ai/deepseek-v4-pro", - "context_window": 64000, "max_tokens": 8000, - "coding_rank": 1, "speed_rank": 15, "reasoning_rank": 1, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005, - "tags": ["elite", "coding", "reasoning", "math", "cheap"] - }, - "kimi-k2.6": { - "endpoint": "moonshotai/kimi-k2.6", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 2, "speed_rank": 12, "reasoning_rank": 2, - "cost_per_1k_input": 0.003, "cost_per_1k_output": 0.015, - "tags": ["elite", "coding", "reasoning", "long_context"] - }, - "qwen3.5-397b": { - "endpoint": "qwen/qwen3.5-397b-a17b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 3, "speed_rank": 18, "reasoning_rank": 3, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["elite", "coding", "long_context", "chinese"] - }, - "mistral-large-3": { - "endpoint": "mistralai/mistral-large-3-675b-instruct-2512", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 4, "speed_rank": 14, "reasoning_rank": 4, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.010, - "tags": ["elite", "coding", "multilingual", "long_context"] - }, - "gpt-oss-120b": { - "endpoint": "openai/gpt-oss-120b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 5, "speed_rank": 20, "reasoning_rank": 5, - "cost_per_1k_input": 0.003, "cost_per_1k_output": 0.012, - "tags": ["elite", "coding", "reasoning", "openai"] - }, - "deepseek-v4-flash": { - "endpoint": "deepseek-ai/deepseek-v4-flash", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 6, "speed_rank": 8, "reasoning_rank": 8, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "fast", "cheap"] - }, - "llama-4-maverick": { - "endpoint": "meta/llama-4-maverick-17b-128e-instruct", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 7, "speed_rank": 10, "reasoning_rank": 7, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["strong", "coding", "meta", "long_context"] - }, - "nemotron-3-super": { - "endpoint": "nvidia/nemotron-3-super-120b-a12b", - "context_window": 128000, "max_tokens": 8000, - "coding_rank": 8, "speed_rank": 16, "reasoning_rank": 6, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["strong", "coding", "nvidia", "reasoning"] - }, - "mistral-medium-3.5": { - "endpoint": "mistralai/mistral-medium-3.5-128b", - "context_window": 64000, "max_tokens": 8000, - "coding_rank": 9, "speed_rank": 11, "reasoning_rank": 10, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.005, - "tags": ["strong", "coding", "mistral", "balanced"] - }, - "dracarys-llama-70b": { - "endpoint": "abacusai/dracarys-llama-3.1-70b-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 10, "speed_rank": 13, "reasoning_rank": 11, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["strong", "coding", "roleplay", "creative"] - }, - "llama-3.3-70b": { - "endpoint": "meta/llama-3.3-70b-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 11, "speed_rank": 9, "reasoning_rank": 12, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "meta", "fast", "cheap"] - }, - "nemotron-super-49b": { - "endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1.5", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 12, "speed_rank": 7, "reasoning_rank": 13, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["strong", "coding", "nvidia", "fast", "cheap"] - }, - "step-3.7-flash": { - "endpoint": "stepfun-ai/step-3.7-flash", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 13, "speed_rank": 6, "reasoning_rank": 14, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "chinese", "cheap"] - }, - "mistral-small-4": { - "endpoint": "mistralai/mistral-small-4-119b-2603", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 14, "speed_rank": 5, "reasoning_rank": 15, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "mistral", "cheap"] - }, - "minimax-m2.7": { - "endpoint": "minimaxai/minimax-m2.7", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 15, "speed_rank": 4, "reasoning_rank": 16, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "fast", "chinese", "cheap"] - }, - "nemotron-super-49b-v1": { - "endpoint": "nvidia/llama-3.3-nemotron-super-49b-v1", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 16, "speed_rank": 17, "reasoning_rank": 17, - "cost_per_1k_input": 0.0005, "cost_per_1k_output": 0.002, - "tags": ["good", "coding", "nvidia", "cheap"] - }, - "llama-3.2-90b-vision": { - "endpoint": "meta/llama-3.2-90b-vision-instruct", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 17, "speed_rank": 19, "reasoning_rank": 18, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["good", "coding", "vision", "multimodal", "meta"] - }, - "nemotron-nano-12b": { - "endpoint": "nvidia/nemotron-nano-12b-v2-vl", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 18, "speed_rank": 2, "reasoning_rank": 22, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fast", "vision", "nvidia", "cheap"] - }, - "nemotron-3-nano-30b": { - "endpoint": "nvidia/nemotron-3-nano-30b-a3b", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 19, "speed_rank": 3, "reasoning_rank": 19, - "cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001, - "tags": ["light", "fast", "nvidia", "cheap"] - }, - "nemotron-nano-9b": { - "endpoint": "nvidia/nvidia-nemotron-nano-9b-v2", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 20, "speed_rank": 1, "reasoning_rank": 23, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fastest", "nvidia", "cheap"] - }, - "nemotron-content-safety": { - "endpoint": "nvidia/nemotron-content-safety-reasoning-4b", - "context_window": 8000, "max_tokens": 2000, - "coding_rank": 21, "speed_rank": 1, "reasoning_rank": 24, - "cost_per_1k_input": 0.0001, "cost_per_1k_output": 0.0005, - "tags": ["light", "fastest", "safety", "nvidia", "cheap"] - }, - "nemotron-3-nano-omni": { - "endpoint": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 22, "speed_rank": 5, "reasoning_rank": 20, - "cost_per_1k_input": 0.0002, "cost_per_1k_output": 0.001, - "tags": ["specialized", "omni", "multimodal", "reasoning", "nvidia", "cheap"] - }, - "diffusiongemma": { - "endpoint": "google/diffusiongemma-26b-a4b-it", - "context_window": 16000, "max_tokens": 4000, - "coding_rank": 23, "speed_rank": 10, "reasoning_rank": 25, - "cost_per_1k_input": 0.001, "cost_per_1k_output": 0.004, - "tags": ["specialized", "image", "diffusion", "google"] - }, - "glm": { - "endpoint": "z-ai/glm-5.1", - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 9, "speed_rank": 8, "reasoning_rank": 9, - "cost_per_1k_input": 0.002, "cost_per_1k_output": 0.008, - "tags": ["legacy", "coding", "fast", "chinese"] - }, - "hf_fallback": { - "endpoint": HF_FALLBACK_MODEL, - "context_window": 32000, "max_tokens": 8000, - "coding_rank": 50, "speed_rank": 50, "reasoning_rank": 50, - "cost_per_1k_input": 0, "cost_per_1k_output": 0, - "tags": ["fallback", "free", "hf"] - }, -} -# ============================================================================ -# 📁 FILE MANAGER -# ============================================================================ - -class FileManager: - SUPPORTED_EXTENSIONS = { - '.txt': 'text', '.py': 'python', '.js': 'javascript', - '.html': 'html', '.css': 'css', '.json': 'json', - '.yaml': 'yaml', '.yml': 'yaml', '.md': 'markdown', - '.csv': 'csv', '.xml': 'xml', '.log': 'log', - '.sql': 'sql', '.sh': 'bash', '.bat': 'batch', - '.ps1': 'powershell', '.ipynb': 'jupyter', - } - - def __init__(self, base_dir: str = "."): - self.base_dir = base_dir - - def read_file(self, file_path: str, max_size: int = 100000) -> Tuple[str, str]: - full_path = os.path.join(self.base_dir, file_path) - if not os.path.exists(full_path): - return "", "not_found" - ext = os.path.splitext(file_path)[1].lower() - file_type = self.SUPPORTED_EXTENSIONS.get(ext, 'binary') - size = os.path.getsize(full_path) - if size > max_size: - return f"⚠️ Файл слишком большой ({size} байт). Показано {max_size} байт.", file_type - try: - with open(full_path, 'r', encoding='utf-8') as f: - content = f.read(max_size) - return content, file_type - except UnicodeDecodeError: - with open(full_path, 'rb') as f: - content = f.read(max_size) - return f"📎 Бинарный файл ({size} байт)", 'binary' - except Exception as e: - return f"❌ Ошибка чтения: {e}", 'error' - - def read_multiple_files(self, file_paths: List[str], max_total: int = 50000) -> Dict[str, Tuple[str, str]]: - results = {} - total_size = 0 - for path in file_paths: - content, file_type = self.read_file(path) - if total_size + len(content) > max_total: - results[path] = (f"⚠️ Превышен лимит ({max_total} байт)", file_type) - else: - results[path] = (content, file_type) - total_size += len(content) - return results - - def list_files(self, directory: str = ".", pattern: str = "*", recursive: bool = False) -> List[str]: - full_path = os.path.join(self.base_dir, directory) - if not os.path.exists(full_path): - return [] - files = [] - if recursive: - for root, _, filenames in os.walk(full_path): - for filename in filenames: - if self._match_pattern(filename, pattern): - rel_path = os.path.relpath(os.path.join(root, filename), self.base_dir) - files.append(rel_path) - else: - for filename in os.listdir(full_path): - if os.path.isfile(os.path.join(full_path, filename)): - if self._match_pattern(filename, pattern): - files.append(os.path.join(directory, filename)) - return files - - def save_file(self, file_path: str, content: str) -> str: - full_path = os.path.join(self.base_dir, file_path) - os.makedirs(os.path.dirname(full_path), exist_ok=True) - try: - with open(full_path, 'w', encoding='utf-8') as f: - f.write(content) - return f"✅ Файл сохранён: {file_path}" - except Exception as e: - return f"❌ Ошибка сохранения: {e}" - - def analyze_file(self, file_path: str) -> Dict[str, Any]: - full_path = os.path.join(self.base_dir, file_path) - if not os.path.exists(full_path): - return {"error": "Файл не найден"} - stat = os.stat(full_path) - ext = os.path.splitext(file_path)[1].lower() - result = { - "name": os.path.basename(file_path), - "path": file_path, - "size_bytes": stat.st_size, - "type": self.SUPPORTED_EXTENSIONS.get(ext, 'unknown'), - "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), - "created": datetime.fromtimestamp(stat.st_ctime).isoformat(), - } - if result["type"] != 'binary' and result["type"] != 'unknown': - content, _ = self.read_file(file_path, max_size=100000) - if content: - lines = content.split('\n') - result["lines"] = len(lines) - result["chars"] = len(content) - result["words"] = len(content.split()) - return result - - def _match_pattern(self, filename: str, pattern: str) -> bool: - if pattern == "*": - return True - return fnmatch.fnmatch(filename, pattern) - -FILE_MANAGER = FileManager() -# ============================================================================ -# 🌐 ПОЛНОСТЬЮ БЕСПЛАТНЫЙ ИНТЕРНЕТ-АГЕНТ -# ============================================================================ - -class FreeInternetAgent: - """Интернет-агент с бесплатными поисковыми системами""" - - def __init__(self, cache_ttl: int = 3600): - self.session = requests.Session() - self.session.headers.update({ - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - }) - self.cache: Dict[str, Tuple[Any, datetime]] = {} - self.cache_ttl = cache_ttl - self._has_bs4 = 'BeautifulSoup' in globals() - - # SearXNG инстансы (можно добавить свои) - self.searxng_instances = [ - "https://searx.be", - "https://search.bus-hit.me", - "https://searx.nixnet.xyz", - "https://searx.tuxcloud.net", - "https://searx.moe", - ] - - def search_web(self, query: str, num_results: int = 5) -> List[Dict[str, str]]: - """Умный поиск с несколькими источниками""" - - cache_key = self._get_cache_key('search', query, num_results) - cached = self._get_from_cache(cache_key) - if cached is not None: - return cached - - results = [] - - # 🥇 SearXNG - results = self._search_searxng(query, num_results) - - # 🥈 DuckDuckGo - if not results: - results = self._search_duckduckgo(query, num_results) - - # 🥉 Google - if not results: - results = self._search_google(query, num_results) - - # 🏁 Яндекс (для русского) - if not results and any(ord(c) > 1024 for c in query): - results = self._search_yandex(query, num_results) - - self._save_to_cache(cache_key, results) - return results - - def _search_searxng(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через SearXNG (мета-поиск)""" - results = [] - - for instance in self.searxng_instances: - try: - url = f"{instance}/search" - params = { - "q": query, - "format": "json", - "categories": "general", - "engines": "google,bing,duckduckgo,startpage", - "language": "en", - "pageno": 1 - } - - response = self.session.get(url, params=params, timeout=20) - response.raise_for_status() - data = response.json() - - if 'results' in data: - for item in data['results'][:num_results]: - results.append({ - 'title': item.get('title', '')[:100], - 'url': item.get('url', ''), - 'snippet': item.get('content', '')[:200], - 'source': 'searxng', - 'engine': item.get('engine', '') - }) - - if results: - print(f"🔍 SearXNG: {len(results)} результатов") - break - - except Exception as e: - continue - - return results - - def _search_duckduckgo(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через DuckDuckGo API""" - results = [] - try: - url = f"https://api.duckduckgo.com/?q={query}&format=json&no_html=1&skip_disambig=1" - response = self.session.get(url, timeout=15) - data = response.json() - - if 'RelatedTopics' in data: - for item in data['RelatedTopics'][:num_results]: - if 'Text' in item and 'FirstURL' in item: - results.append({ - 'title': item['Text'][:100], - 'url': item['FirstURL'], - 'snippet': item.get('Text', '')[:200], - 'source': 'duckduckgo' - }) - - print(f"🦆 DuckDuckGo: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ DuckDuckGo ошибка: {e}") - - return results - - def _search_google(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через Google (парсинг)""" - results = [] - if not self._has_bs4: - return results - - try: - from bs4 import BeautifulSoup - - url = f"https://www.google.com/search?q={query}&num={num_results * 2}" - response = self.session.get(url, timeout=20) - soup = BeautifulSoup(response.text, 'html.parser') - - for g in soup.find_all('div', class_='g'): - title_elem = g.find('h3') - link_elem = g.find('a') - snippet_elem = g.find('div', class_='VwiC3b') - - if title_elem and link_elem: - title = title_elem.get_text() - link = link_elem.get('href', '') - snippet = snippet_elem.get_text() if snippet_elem else '' - - if link.startswith('/url?q='): - link = link.split('/url?q=')[1].split('&')[0] - - if link.startswith('http'): - results.append({ - 'title': title[:100], - 'url': link, - 'snippet': snippet[:200], - 'source': 'google' - }) - - if len(results) >= num_results: - break - - print(f"🔍 Google: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ Google ошибка: {e}") - - return results - - def _search_yandex(self, query: str, num_results: int) -> List[Dict[str, str]]: - """Поиск через Яндекс (парсинг)""" - results = [] - if not self._has_bs4: - return results - - try: - from bs4 import BeautifulSoup - - url = f"https://yandex.ru/search/?text={query}&numdoc={num_results}" - response = self.session.get(url, timeout=20) - soup = BeautifulSoup(response.text, 'html.parser') - - for item in soup.find_all('li', class_='serp-item'): - link_elem = item.find('a', class_='link') - snippet_elem = item.find('div', class_='text-container') - - if link_elem: - title = link_elem.get_text() - link = link_elem.get('href', '') - snippet = snippet_elem.get_text() if snippet_elem else '' - - if link.startswith('http'): - results.append({ - 'title': title[:100], - 'url': link, - 'snippet': snippet[:200], - 'source': 'yandex' - }) - - if len(results) >= num_results: - break - - print(f"🔍 Яндекс: {len(results)} результатов") - - except Exception as e: - print(f"⚠️ Яндекс ошибка: {e}") - - return results - - # ... остальные методы (fetch_page, analyze_website и т.д.) такие же ... - - # Извлекаем ссылки и изображения (с BeautifulSoup если доступен) - if self._has_bs4 and 'content' in page: - soup = BeautifulSoup(content, 'html.parser') - links = [a.get('href') for a in soup.find_all('a') if a.get('href') and a.get('href').startswith('http')] - images = [img.get('src') for img in soup.find_all('img') if img.get('src')] - else: - links = re.findall(r'href=["\'](https?://[^"\']+)["\']', content, re.IGNORECASE)[:20] - images = re.findall(r'src=["\'](https?://[^"\']+\.(jpg|jpeg|png|gif|svg))["\']', content, re.IGNORECASE) - images = [img[0] if isinstance(img, tuple) else img for img in images] - - result = { - 'url': url, - 'title': page.get('title', ''), - 'content_length': len(content), - 'word_count': len(content.split()), - 'links': links[:20], - 'images': images[:10], - 'keywords': self._extract_keywords(content), - 'language': self._detect_language(content), - 'has_form': '