PDO::ERRMODE_EXCEPTION]); logMessage("Подключение к БД успешно установлено."); } catch (PDOException $e) { logMessage("Ошибка подключения к БД: " . $e->getMessage()); die("Ошибка подключения к БД"); } function logMessage($message) { if (!is_dir('logs')) { mkdir('logs', 0777, true); } file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " - " . $message . "\n", FILE_APPEND | LOCK_EX); } function normalizeFilename($filename) { $filename = iconv('UTF-8', 'UTF-8//IGNORE', $filename); return preg_replace('/[^\w\.]+/u', '_', $filename); } /* ===== Новая функция для извлечения контента из PDF ===== */ function extractDocumentContent($filePath) { // 1. Попытка извлечь текст через pdftotext (простая версия, без дальнейшей обработки) $extractedText = extractText3($filePath); if (mb_strlen($extractedText, 'UTF-8') >= 200) { logMessage("pdftotext дал достаточное количество символов для $filePath."); // Режим "pdf": достаточный текст – передаем идентификатор файла return ['mode' => 'pdf', 'content' => '']; } else { logMessage("pdftotext дал недостаточно символов для $filePath. Переходим к обработке через изображения."); // 2. Конвертация PDF в изображения (JPG) $outputDir = sys_get_temp_dir() . '/pdf_images_' . md5($filePath); $images = convertPdfToImages($filePath, $outputDir); if (empty($images)) { logMessage("Ошибка: не удалось конвертировать PDF в изображения для $filePath"); return ['mode' => 'error', 'content' => '']; } // 3. NSFW-проверка изображений $allSafe = true; foreach ($images as $image) { $classification = classifyImage($image); $absImagePath = realpath($image); $unsafeProbability = isset($classification[$absImagePath]) ? $classification[$absImagePath]['unsafe'] ?? 0 : 0; logMessage("DEBUG: Для изображения '$absImagePath' получено unsafeProbability = " . $unsafeProbability); if ($unsafeProbability > 0.8) { logMessage("DEBUG: Изображение '$absImagePath' не прошло NSFW проверку."); $allSafe = false; break; } } if (!$allSafe) { return ['mode' => 'nsfw', 'content' => 'Файл содержит трешконтент.']; } else { // 4. Если все изображения безопасны – запускаем OCR $combinedOcrText = ""; foreach ($images as $image) { $ocrText = doOCR($image); if (!empty($ocrText)) { logMessage("DEBUG: OCR успешно извлёк текст для изображения: " . $image); $combinedOcrText .= $ocrText . "\n"; } else { logMessage("DEBUG: OCR не смог извлечь текст для изображения: " . $image); } } if (empty(trim($combinedOcrText))) { logMessage("Ошибка: не удалось извлечь текст посредством OCR для $filePath"); return ['mode' => 'error', 'content' => '']; } return ['mode' => 'ocr', 'content' => trim($combinedOcrText)]; } } } /* Функция extractText3 – простая реализация через pdftotext */ function extractText3($filePath) { $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); if ($extension !== 'pdf') { return ''; } $outputFile = tempnam(sys_get_temp_dir(), 'txt_') . '.txt'; $command = "pdftotext " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile); logMessage("Выполняем команду для извлечения текста: " . $command); exec($command, $output, $returnVar); if ($returnVar !== 0) { logMessage("Ошибка pdftotext: " . implode("\n", $output)); return ''; } if (!file_exists($outputFile)) { logMessage("Файл pdftotext не создан: $filePath"); return ''; } $text = file_get_contents($outputFile); unlink($outputFile); $cleanText = trim(preg_replace('/\pC+/u', '', $text)); return $cleanText; } /* ===================== Основной скрипт ===================== */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { logMessage("Получен POST-запрос."); $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? null; // Получаем динамический промпт из CRM (если передан) $dynamicPrompt = $input['prompt'] ?? ''; if (!$id) { logMessage("Ошибка: отсутствует ID документа"); die("Ошибка: отсутствует ID документа"); } logMessage("Начало обработки документа с ID: $id"); if (!empty($dynamicPrompt)) { logMessage("Получен динамический промпт: " . $dynamicPrompt); } $documents = fetchDocumentData($pdo, $id); if (empty($documents)) { logMessage("Документы не найдены для ID: $id"); die("Документы не найдены для ID: $id"); } logMessage("Документы получены из CRM: " . json_encode($documents, JSON_UNESCAPED_UNICODE)); $filePaths = array_column($documents, 'filepath'); $uploadResult = createVectorStoreAndUploadFiles($filePaths); if (!$uploadResult) { logMessage("Ошибка создания Vector Store или загрузки файлов"); die("Ошибка создания Vector Store или загрузки файлов"); } $vectorStoreId = $uploadResult['vectorStoreId']; $uploadedFileIds = $uploadResult['fileIds']; if (!updateAssistantWithVectorStore($vectorStoreId)) { logMessage("Ошибка обновления ассистента с Vector Store"); die("Ошибка обновления ассистента"); } // Формируем объединённый контент для анализа, используя extractDocumentContent для каждого файла $combinedMessages = []; foreach ($documents as $doc) { if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) { logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE)); continue; } $result = extractDocumentContent($doc['filepath']); $messageBlock = "Документ: " . $doc['title'] . "\n"; if ($result['mode'] === 'pdf') { // Если режим pdf, берем file id (то есть оригинальный PDF передается ассистенту) $fileId = $uploadedFileIds[$doc['filepath']] ?? ''; $messageBlock .= "Тип: PDF (достаточно текста, оригинальный файл передан).\nFileID: " . $fileId . "\n"; } elseif ($result['mode'] === 'ocr') { $messageBlock .= "OCR извлёк текст:\n" . $result['content'] . "\n"; } elseif ($result['mode'] === 'nsfw') { $messageBlock .= "Ошибка: Файл содержит трешконтент.\n"; } else { $messageBlock .= "Ошибка: Не удалось извлечь текст.\n"; } $combinedMessages[] = $messageBlock; } $combinedContent = implode("\n-----------------\n", $combinedMessages); if (!empty($dynamicPrompt)) { $combinedContent = "Динамический промпт из CRM:\n" . $dynamicPrompt . "\n\n" . $combinedContent; } logMessage("Собранный контент для анализа:\n" . $combinedContent); if (empty($combinedContent)) { logMessage("Ошибка: анализ документов не вернул результатов"); die("Ошибка: анализ документов не вернул результатов"); } $fileIdCombined = implode(',', array_values($uploadedFileIds)); logMessage("Объединённый список идентификаторов файлов: " . $fileIdCombined); $threadId = createThread(); if (!$threadId) { logMessage("Ошибка создания треда"); die("Ошибка создания треда"); } $analysis = analyzeDocumentWithAssistantStream($threadId, ASSISTANT_ID, $fileIdCombined, $combinedContent); if (!$analysis) { logMessage("Ошибка анализа совокупного запроса"); die("Ошибка анализа совокупного запроса"); } $report = generateReport([ [ 'document' => 'Объединенный анализ', 'status' => 'complete', 'analysis' => $analysis ] ]); $final_output = [ "status" => "complete", "content" => $report, "moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен' ]; logMessage("DEBUG: Извлеченный контент: " . $report); logMessage("DEBUG: Извлеченный вердикт модерации: " . ($analysis['moderationVerdict'] ?? 'Не определен')); echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); logMessage("Обработка всех документов завершена."); } else { logMessage("Ошибка: запрос должен быть POST"); die("Ошибка: запрос должен быть POST"); } /* ===================== Функции для работы с CRM и Vector Store ===================== */ function fetchDocumentData($pdo, $id) { logMessage("Получение данных документа из CRM по ID: $id"); $sql = " SELECT n.title, CASE WHEN a.storedname IS NOT NULL THEN CONCAT(a.path, a.attachmentsid, '_', a.storedname) ELSE CONCAT(a.path, a.attachmentsid, '_', a.name) END AS filepath FROM vtiger_senotesrel r LEFT JOIN vtiger_notes n ON n.notesid = r.notesid LEFT JOIN vtiger_crmentity e ON e.crmid = r.notesid LEFT JOIN vtiger_seattachmentsrel r2 ON r2.crmid = r.notesid LEFT JOIN vtiger_attachments a ON a.attachmentsid = r2.attachmentsid WHERE r.crmid = ? AND e.deleted = 0 AND (a.type = 'application/pdf' OR a.type = 'application/octet-stream') "; try { $stmt = $pdo->prepare($sql); $stmt->execute([$id]); $documents = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($documents as &$doc) { $doc['filepath'] = normalizeFilename($doc['filepath']); } logMessage("Документы получены из CRM: " . json_encode($documents, JSON_UNESCAPED_UNICODE)); return $documents; } catch (PDOException $e) { logMessage("Ошибка при выполнении запроса к CRM: " . $e->getMessage()); return []; } } function createVectorStoreAndUploadFiles($filePaths) { logMessage("Создание Vector Store и загрузка файлов..."); $vectorStoreId = createVectorStore(); if (!$vectorStoreId) return null; $uploadedFiles = []; foreach ($filePaths as $filePath) { logMessage("Загрузка файла: $filePath"); if (!file_exists($filePath)) { logMessage("Ошибка: Файл не существует: $filePath"); continue; } $fileId = uploadFileToOpenAI($filePath); if (!$fileId) { logMessage("Ошибка загрузки файла: $filePath"); continue; } if (!addFileToVectorStore($vectorStoreId, $fileId)) { logMessage("Ошибка добавления файла в Vector Store: $filePath"); } else { logMessage("Файл успешно добавлен в Vector Store: $filePath"); $uploadedFiles[$filePath] = $fileId; } } return ['vectorStoreId' => $vectorStoreId, 'fileIds' => $uploadedFiles]; } function createVectorStore() { $curl = curl_init(); $payload = json_encode(['name' => 'Vector Store']); logMessage("Создание Vector Store. Отправляем payload: " . $payload); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VECTOR_STORES_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); logMessage("Ответ OpenAI (создание Vector Store): HTTP $httpCode - " . $response); if ($curlError) { logMessage("Ошибка cURL при создании Vector Store: " . $curlError); return null; } $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка при создании Vector Store: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return null; } return $decoded['id']; } function uploadFileToOpenAI($filePath) { logMessage("Загрузка файла в OpenAI: $filePath"); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_FILES_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => [ 'file' => new CURLFile($filePath), 'purpose' => 'assistants' ], CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); logMessage("Ответ OpenAI (загрузка файла): HTTP $httpCode - " . $response); if ($curlError) { logMessage("Ошибка cURL при загрузке файла: " . $curlError); return null; } $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка при загрузке файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return null; } return $decoded['id']; } function addFileToVectorStore($vectorStoreId, $fileId) { $curl = curl_init(); $payload = json_encode(['file_id' => $fileId]); logMessage("Добавление файла в Vector Store. Payload: " . $payload); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VECTOR_STORES_API . "/$vectorStoreId/files", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); logMessage("Ответ OpenAI (добавление файла): HTTP $httpCode - " . $response); if ($curlError) { logMessage("Ошибка cURL при добавлении файла в Vector Store: " . $curlError); return false; } $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка добавления файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return false; } return true; } function updateAssistantWithVectorStore($vectorStoreId) { $data = [ 'tool_resources' => [ 'file_search' => [ 'vector_store_ids' => [$vectorStoreId] ] ] ]; $curl = curl_init(); $payload = json_encode($data); logMessage("Обновление ассистента. Payload: " . $payload); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_ASSISTANT_API . "/" . ASSISTANT_ID, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); logMessage("Ответ OpenAI (обновление ассистента): HTTP $httpCode - " . $response); if ($curlError) { logMessage("Ошибка обновления ассистента: " . $curlError); return false; } $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка обновления ассистента: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return false; } return true; } function analyzeDocumentWithAssistantStream($threadId, $assistantId, $fileId, $content) { logMessage("Анализ документа через ассистента (stream): thread_id=$threadId, fileId=$fileId"); $userMessage = "Проанализируй документ. Файлы: " . $fileId . "\nСодержимое для анализа:\n" . $content . "\nВыведи краткий сводный отчёт по загруженным файлам и содержимому, используя указанные идентификаторы."; logMessage("Формируем текст запроса для ассистента (stream):\n" . $userMessage); $userMessage = mb_convert_encoding($userMessage, 'UTF-8', 'auto'); $userMessage = iconv("UTF-8", "UTF-8//IGNORE//TRANSLIT", $userMessage); $payload = [ "assistant_id" => $assistantId, "thread" => [ "messages" => [ ["role" => "user", "content" => $userMessage] ] ], "temperature" => 0.7, "top_p" => 1.0, "stream" => true ]; $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE); logMessage("Отправляем запрос к ассистенту (stream). Payload:\n" . $payloadJson); $finalMessage = ""; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/runs", CURLOPT_RETURNTRANSFER => false, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payloadJson, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ], CURLOPT_WRITEFUNCTION => function($ch, $data) use (&$finalMessage) { $finalMessage .= $data; return strlen($data); } ]); curl_exec($curl); $curlError = curl_error($curl); if ($curlError) { logMessage("Ошибка cURL в analyzeDocumentWithAssistantStream: " . $curlError); curl_close($curl); return null; } curl_close($curl); logMessage("Сырой потоковый ответ (raw stream):\n" . $finalMessage); $parsedMessage = ""; $lines = explode("\n", $finalMessage); foreach ($lines as $line) { $line = trim($line); if (strpos($line, "data: ") === 0) { $dataPart = substr($line, 6); if ($dataPart === "[DONE]") { break; } $json = json_decode($dataPart, true); logMessage("DEBUG: Распарсенный фрагмент: " . print_r($json, true)); if (is_array($json) && isset($json['delta']['content'])) { $contentPiece = ""; foreach ($json['delta']['content'] as $segment) { if (isset($segment['text']['value'])) { $contentPiece .= $segment['text']['value']; } } $parsedMessage .= $contentPiece; } } } if (empty(trim($parsedMessage))) { logMessage("Парсинг не дал результата, используем сырой ответ."); $parsedMessage = $finalMessage; } logMessage("DEBUG: Итоговый ответ от ассистента (stream):\n" . $parsedMessage); $verdict = 'Вердикт не определён'; if (preg_match('/Вердикт:\s*(.+)$/mi', $parsedMessage, $matches)) { $extractedVerdict = trim($matches[1]); if (!empty($extractedVerdict)) { $verdict = $extractedVerdict; } } return [ "status" => "complete", "content" => $parsedMessage, "moderationVerdict" => $verdict ]; } function generateReport($allResults) { $report = "### Итоговый отчет\n\n"; foreach ($allResults as $result) { $report .= "**Результат анализа:**\n" . ($result['analysis']['content'] ?? 'Нет данных') . "\n"; $report .= "**Вердикт:** " . ($result['analysis']['moderationVerdict'] ?? 'Не определен') . "\n\n"; } return $report; } // возвращаем извлеченные данные в СРМ $report = generateReport([ [ 'document' => 'Объединенный анализ', 'status' => 'complete', 'analysis' => $analysis ] ]); $final_output = [ "status" => "complete", "content" => $report, "moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен' ]; logMessage("DEBUG: Извлеченный контент: " . $report); logMessage("DEBUG: Извлеченный вердикт модерации: " . ($analysis['moderationVerdict'] ?? 'Не определен')); echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);