config = config('parser'); } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- /** * Главная точка входа. * 1. Обходит активные порталы из конфига * 2. Вызывает parse() на каждом → данные оседают в tmp * 3. Читает результаты через getResult() * 4. Отправляет HTML в ChatGPT и получает JSON * 5. Возвращает массив готовых данных для создания Car */ public function run(): array { $cars = []; foreach ($this->getActivePortals() as $portalInstance) { // Шаг 1: загружаем страницы и сохраняем HTML во временные файлы $portalInstance->parse(); // Шаг 2: читаем HTML из tmp $results = $portalInstance->getResult(); // Шаг 3: каждый HTML → ChatGPT → массив полей Car foreach ($results as $item) { try { $carData = $this->processWithGpt($item['html']); if ($carData !== null) { $cars[] = $carData; } } catch (Exception $e) { error_log('[Parser] GPT error for file ' . ($item['file'] ?? '?') . ': ' . $e->getMessage()); } } // clearData() — не вызываем пока, но метод готов // $portalInstance->clearData(); } return $cars; } // ------------------------------------------------------------------------- // Portal loading // ------------------------------------------------------------------------- /** * Возвращает массив инстансов активных порталов из конфига. * Пример конфига: * 'portals' => ['Encar' => true, 'Drom' => false] * * @return object[] */ private function getActivePortals(): array { $portals = $this->config['portals'] ?? []; $instances = []; foreach ($portals as $name => $enabled) { if (! $enabled) { continue; } $class = self::PORTALS_NAMESPACE . $name; if (! class_exists($class)) { error_log("[Parser] Класс портала не найден: $class"); continue; } $instance = new $class(); // Убеждаемся что у класса есть нужные методы if (! method_exists($instance, 'parse') || ! method_exists($instance, 'getResult')) { error_log("[Parser] Портал $name не реализует parse() / getResult()"); continue; } $instances[] = $instance; } return $instances; } // ------------------------------------------------------------------------- // GPT integration // ------------------------------------------------------------------------- /** * Отправляет HTML в ChatGPT, получает JSON, декодирует в массив. * Возвращает null если GPT вернул невалидный JSON. */ private function processWithGpt(string $html): ?array { $prompt = $this->buildPrompt($html); $raw = $this->callChatGpt($prompt); // Убираем возможные markdown-обёртки ```json … ``` $json = preg_replace('/^```(?:json)?\s*/i', '', trim($raw)); $json = preg_replace('/\s*```$/', '', $json); $data = json_decode($json, true); if (! is_array($data)) { error_log('[Parser] GPT вернул невалидный JSON: ' . substr($raw, 0, 200)); return null; } return $data; } /** * Подставляет HTML в промт из конфига (плейсхолдер {html}) */ private function buildPrompt(string $html): string { $template = $this->config['gpt_prompt'] ?? '{html}'; // Обрезаем HTML чтобы не превысить контекст модели (~100k символов достаточно) $trimmedHtml = mb_substr($html, 0, 80000); return str_replace('{html}', $trimmedHtml, $template); } /** * Отправляет запрос к OpenAI Chat Completions API и возвращает текст ответа. * * @throws Exception при сетевых ошибках или ошибках API */ private function callChatGpt(string $prompt): string { $cfg = $this->config['openai'] ?? []; $apiKey = $cfg['api_key'] ?? config('services.openai.key', ''); $model = $cfg['model'] ?? 'gpt-4o'; $maxTok = $cfg['max_tokens'] ?? 2000; $temp = $cfg['temperature'] ?? 0; if (empty($apiKey)) { throw new Exception('OPENAI_API_KEY не задан в конфиге или .env'); } $response = Http::withToken($apiKey) ->timeout(60) ->post('https://api.openai.com/v1/chat/completions', [ 'model' => $model, 'messages' => [ ['role' => 'user', 'content' => $prompt], ], ]); if ($response->failed()) { $error = $response->json('error.message', 'unknown'); throw new Exception("OpenAI API error [{$response->status()}]: {$error}"); } return $response->json('choices.0.message.content', ''); } }