PDO::ERRMODE_EXCEPTION]); } 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); } /* ===================== Основной скрипт ===================== */ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? null; if (!$id) { logMessage("Ошибка: отсутствует ID документа"); die("Ошибка: отсутствует ID документа"); } logMessage("Начало обработки документа с ID: $id"); $documents = fetchDocumentData($pdo, $id); if (empty($documents)) { logMessage("Документы не найдены для ID: $id"); die("Документы не найдены для ID: $id"); } logMessage("Документы получены из БД: " . 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("Ошибка обновления ассистента"); } // Собираем объединённое сообщение для всех документов $combinedContent = analyzeDocuments($documents, $uploadedFileIds); if (empty($combinedContent)) { logMessage("Ошибка: анализ документов не вернул результатов"); die("Ошибка: анализ документов не вернул результатов"); } // Создаем один тред (реальный вызов API) $threadId = createThread(); if (!$threadId) { logMessage("Ошибка создания треда"); die("Ошибка создания треда"); } // Отправляем объединённое сообщение в ассистента (fileId оставляем пустым для совокупного анализа) $analysis = analyzeDocumentWithAssistant($threadId, ASSISTANT_ID, '', $combinedContent); if (!$analysis) { logMessage("Ошибка анализа совокупного запроса"); die("Ошибка анализа совокупного запроса"); } // Формируем отчёт с объединённым анализом $report = generateReport([ [ 'document' => 'Объединенный анализ', 'status' => 'complete', 'analysis' => $analysis ] ]); logMessage("Итоговый отчет:\n" . $report); echo $report; 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); 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(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VECTOR_STORES_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['name' => 'Vector Store']), 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); if ($curlError) { logMessage("Ошибка cURL при создании Vector Store: " . $curlError); return null; } logMessage("Ответ OpenAI (создание Vector Store): HTTP $httpCode - " . $response); $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); if ($curlError) { logMessage("Ошибка cURL при загрузке файла: " . $curlError); return null; } logMessage("Ответ OpenAI (загрузка файла): HTTP $httpCode - " . $response); $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(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VECTOR_STORES_API . "/$vectorStoreId/files", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['file_id' => $fileId]), 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); if ($curlError) { logMessage("Ошибка cURL при добавлении файла в Vector Store: " . $curlError); return false; } logMessage("Ответ OpenAI (добавление файла): HTTP $httpCode - " . $response); $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(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_ASSISTANT_API . "/" . ASSISTANT_ID, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => json_encode($data), 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); if ($curlError) { logMessage("Ошибка обновления ассистента: " . $curlError); return false; } logMessage("Ответ OpenAI (обновление ассистента): HTTP $httpCode - " . $response); $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка обновления ассистента: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return false; } return true; } /* ===================== Логика анализа документов ===================== */ /** * Функция analyzeDocuments собирает информацию по всем документам и возвращает объединённый текст. */ function analyzeDocuments($documents, $uploadedFileIds) { $results = []; foreach ($documents as $doc) { if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) { logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE)); continue; } $finalContent = ""; // 1. Попытка извлечения текста напрямую $extractedText = extractText($doc['filepath']); if (!empty($extractedText)) { logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Отправляем PDF файл в ассистента."); // Если текст успешно извлечён, не передаём его в finalContent – будем использовать file_id } else { logMessage("DEBUG: Извлечение текста не удалось для " . $doc['filepath'] . ". Пытаемся обработать через изображения."); $extension = strtolower(pathinfo($doc['filepath'], PATHINFO_EXTENSION)); if ($extension === 'pdf') { $outputDir = sys_get_temp_dir() . '/pdf_images_' . md5($doc['filepath']); $images = convertPdfToImages($doc['filepath'], $outputDir); if (empty($images)) { logMessage("Ошибка: Не удалось конвертировать PDF в изображения для " . $doc['filepath']); $finalContent = "Не удалось извлечь текст: конвертация в изображение не выполнена."; } else { $allSafe = true; // Проверяем каждое изображение на цензуру foreach ($images as $image) { logMessage("DEBUG: Проверяем изображение: " . $image); $classification = classifyImage($image); $absImagePath = realpath($image); $unsafeProbability = 0; if (isset($classification[$absImagePath])) { $unsafeProbability = $classification[$absImagePath]['unsafe'] ?? 0; } logMessage("DEBUG: Для изображения '$absImagePath' получено unsafeProbability = " . $unsafeProbability); if ($unsafeProbability > 0.8) { logMessage("DEBUG: Изображение '$absImagePath' не прошло цензуру (unsafeProbability = $unsafeProbability)."); $allSafe = false; break; } } if (!$allSafe) { $finalContent = "Файл не прошёл цензуру."; } else { // Если все изображения безопасны, запускаем 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($combinedOcrText)) { $finalContent = $combinedOcrText . "\n" . getKnowledgeBaseContext($doc['filepath']); } else { $finalContent = "Не удалось извлечь текст посредством OCR."; } } } } else { $finalContent = "Не удалось извлечь текст: не поддерживаемый формат."; } } // 2. Получаем file_id для привязки (если имеется) $fileId = $uploadedFileIds[$doc['filepath']] ?? ''; logMessage("DEBUG: fileId для " . $doc['filepath'] . " = " . $fileId); // 3. Создаем тред для анализа $threadId = createThread(); if (!$threadId) { logMessage("Ошибка создания треда для " . $doc['filepath']); continue; } // 4. Отправляем итоговое содержимое ассистенту для анализа. // Если finalContent пустое, значит, текст извлечён успешно и мы передаем PDF через file_id. logMessage("DEBUG: Вызов analyzeDocumentWithAssistant с параметрами: threadId=$threadId, assistantId=" . ASSISTANT_ID . ", fileId=$fileId, finalContent=" . substr($finalContent, 0, 50)); $analysis = analyzeDocumentWithAssistant($threadId, ASSISTANT_ID, $fileId, $finalContent); if ($analysis) { logMessage("Анализ завершен: " . json_encode($analysis, JSON_UNESCAPED_UNICODE)); $results[] = [ 'document' => $doc['title'], 'status' => 'Анализ завершен', 'analysis' => $analysis ]; } else { logMessage("Ошибка анализа " . $doc['filepath']); $results[] = [ 'document' => $doc['title'], 'status' => 'Ошибка анализа', 'message' => 'Не удалось проанализировать документ.' ]; } } return $results; } /** * Функция createThread выполняет реальный вызов к API для создания треда. */ function createThread() { $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: assistants=v2' ] ]); $response = curl_exec($curl); $decoded = json_decode($response, true); $threadId = $decoded['id'] ?? null; logMessage("Создан тред с id: " . ($threadId ?: "не удалось создать")); return $threadId; } /** * Функция extractText пытается извлечь текст с помощью pdftotext. */ function extractText($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); 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); if (empty($text)) { logMessage("DEBUG: pdftotext вернул пустой результат для $filePath"); } unlink($outputFile); return $text; } /** * Функция doOCR использует Tesseract для извлечения текста. */ function doOCR($filePath) { logMessage("Запуск OCR для файла: $filePath"); $outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt'; $command = "tesseract " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile) . " -l rus"; exec($command, $output, $returnVar); if ($returnVar !== 0) { logMessage("Ошибка Tesseract: " . implode("\n", $output)); return ''; } if (!file_exists($outputFile . ".txt")) { logMessage("Файл OCR не создан: $filePath"); return ''; } $text = file_get_contents($outputFile . ".txt"); if (empty($text)) { logMessage("DEBUG: Tesseract вернул пустой результат для $filePath"); } unlink($outputFile . ".txt"); return $text; } /** * Функция describeImageWithVision вызывает API для получения описания изображения. */ function describeImageWithVision($filePath) { logMessage("Запуск описания изображения через Vision для файла: $filePath"); $imageData = base64_encode(file_get_contents($filePath)); $data = [ "model" => "gpt-4-vision-preview", "messages" => [ [ "role" => "user", "content" => [ [ "type" => "text", "text" => "Опиши это изображение подробно. Если это документ, прочитай и опиши его содержимое." ], [ "type" => "image_url", "image_url" => [ "url" => "data:image/jpeg;base64,$imageData" ] ] ] ] ], "max_tokens" => 500 ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VISION_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . OPENAI_API_KEY ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); if ($curlError) { logMessage("Ошибка cURL в описании изображения: " . $curlError); return ''; } logMessage("Ответ Vision (описание): HTTP $httpCode - " . $response); $decoded = json_decode($response, true); if (isset($decoded['choices'][0]['message']['content'])) { $desc = $decoded['choices'][0]['message']['content']; if (empty($desc)) { logMessage("DEBUG: Описание изображения пустое для $filePath"); } return $desc; } else { logMessage("Ошибка при получении описания изображения: " . json_encode($decoded)); return ''; } } /** * Функция checkNSFWWithVision использует корректный URL для NSFW-модерации. */ function checkNSFWWithVision($filePath) { logMessage("NSFW-проверка через URL " . NSFW_MODERATION_API для файла: $filePath"); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => NSFW_MODERATION_API, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => [ 'file' => new CURLFile($filePath) ], CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . OPENAI_API_KEY ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); if ($curlError) { logMessage("Ошибка cURL при проверке NSFW: " . $curlError); return null; } logMessage("Ответ NSFW: HTTP $httpCode - " . $response); $decoded = json_decode($response, true); if ($httpCode !== 200 || isset($decoded['detail'])) { logMessage("Ошибка анализа NSFW: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return null; } return $decoded['nsfw'] ?? null; } /** * Функция classifyImage использует NudeClassifier через Python. */ function classifyImage($imagePath) { $absolutePath = realpath($imagePath); if (!$absolutePath) { logMessage("ERROR: Не удалось получить абсолютный путь для " . $imagePath); return []; } logMessage("DEBUG: Абсолютный путь для классификации: " . $absolutePath); $escapedPath = escapeshellarg($absolutePath); logMessage("DEBUG: Экранированный путь для классификации: " . $escapedPath); $command = "python3 -c \"import json; from nudenet import NudeClassifier; classifier = NudeClassifier(); print(json.dumps(classifier.classify($escapedPath)))\""; logMessage("DEBUG: Выполнение команды: " . $command); $output = shell_exec($command); logMessage("DEBUG: Вывод команды: " . $output); if ($output === null) { logMessage("ERROR: shell_exec вернул null при выполнении NudeClassifier"); return []; } return json_decode(trim($output), true); } /** * Функция convertPdfToImages конвертирует PDF в изображения. */ function convertPdfToImages($pdfPath, $outputDir) { if (!file_exists($pdfPath)) { logMessage("Файл не существует: $pdfPath"); return []; } if (!file_exists($outputDir)) { mkdir($outputDir, 0777, true); logMessage("Создана директория для изображений: $outputDir"); } $imagePattern = $outputDir . '/page-%03d.jpg'; $command = "LC_ALL=en_US.UTF-8 convert -density 300 " . escapeshellarg($pdfPath) . " -quality 90 " . escapeshellarg($imagePattern); logMessage("Выполняем команду: " . $command); exec($command . " 2>&1", $output, $returnVar); logMessage("DEBUG: Вывод convert: " . implode("\n", $output)); if ($returnVar !== 0) { logMessage("Ошибка при конвертации PDF в изображения."); return []; } return glob($outputDir . '/*.jpg'); } /** * Stub-функция для получения контекста из базы знаний. */ function getKnowledgeBaseContext($filePath) { return "Статическая информация: нормы и законы РФ, судебные прецеденты..."; } /** * Функция analyzeDocumentWithAssistant отправляет реальный запрос к GPT. * * Здесь используется очистка строки, mb_substr, а данные формируются в ключе "messages". */ function analyzeDocumentWithAssistant($threadId, $assistantId, $fileId, $content) { logMessage("Анализ документа через ассистента: thread_id=$threadId, fileId=$fileId"); if (empty($content)) { $userMessage = "Проанализируй документ, предоставленный как PDF (file_id: $fileId)."; } else { $userMessage = "Проанализируй документ. Содержимое для анализа:\n" . $content; } // Очистка строки от битых символов $userMessage = mb_convert_encoding($userMessage, 'UTF-8', 'auto'); $userMessage = iconv("UTF-8", "UTF-8//IGNORE//TRANSLIT", $userMessage); // Формирование payload для вызова ассистента через threads/runs $payload = [ "assistant_id" => $assistantId, "thread" => [ // Можно указать идентификатор существующего треда, если API это поддерживает, // либо оставить сообщения, чтобы создать новый контекст в рамках треда "messages" => [ ["role" => "user", "content" => $userMessage] ] ], "temperature" => 0.7, "top_p" => 1.0, "stream" => false ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/runs", // новый endpoint для ассистента CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), 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); if ($curlError) { logMessage("Ошибка cURL: " . $curlError); return null; } if ($httpCode !== 200) { logMessage("Ошибка API: HTTP $httpCode - " . $response); return null; } $decoded = json_decode($response, true); // Предполагается, что в ответе содержится объект сообщения ассистента, // например, в ключе 'message' с полем 'content' $assistantMessage = $decoded['message']['content'] ?? ''; logMessage("DEBUG: Полученный ответ от ассистента: " . $assistantMessage); return [ "status" => "complete", "content" => $assistantMessage, "moderationVerdict" => "" ]; } /** * Функция generateReport формирует итоговый отчет. */ function generateReport($allResults) { $report = "### Итоговый отчет\n\n"; foreach ($allResults as $result) { $report .= "**Результат анализа:**\n" . ($result['analysis']['content'] ?? 'Нет данных') . "\n"; $report .= "**Вердикт:** " . ($result['analysis']['moderationVerdict'] ?? 'Не определен') . "\n\n"; } return $report; }