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); } /* ===================== Основной скрипт ===================== */ 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"); // Получение данных из CRM $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'); // Загрузка файлов в Vector Store и получение mapping (путь → file_id) $uploadResult = createVectorStoreAndUploadFiles($filePaths); if (!$uploadResult) { logMessage("Ошибка создания Vector Store или загрузки файлов"); die("Ошибка создания Vector Store или загрузки файлов"); } $vectorStoreId = $uploadResult['vectorStoreId']; $uploadedFileIds = $uploadResult['fileIds']; // Обновление ассистента с указанием векторного хранилища if (!updateAssistantWithVectorStore($vectorStoreId)) { logMessage("Ошибка обновления ассистента с Vector Store"); die("Ошибка обновления ассистента"); } // Анализ документов с учетом NSFW, OCR, Vision и знаний из базы $allResults = analyzeDocuments($documents, $uploadedFileIds); if (empty($allResults)) { logMessage("Ошибка: анализ документов не вернул результатов"); die("Ошибка: анализ документов не вернул результатов"); } // Формирование итогового отчета $report = generateReport($allResults); 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 = []; // mapping: путь → file_id 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: * – Проверяет документ на NSFW через Vision. * – Если NSFW найден, помечает для ручной модерации. * – Иначе пытается извлечь текст (сначала с помощью встроенного извлечения, затем через OCR). * – Если текста нет, вызывает Vision для описания изображения. * – Получает контекст из базы знаний и отправляет данные в OpenAI Assistants для финального анализа. */ 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; } // 1. Проверка NSFW через Vision API $isNSFW = checkNSFWWithVision($doc['filepath']); if ($isNSFW === null) { $isNSFW = checkNSFWLocally($doc['filepath']); } if ($isNSFW) { logMessage("NSFW обнаружен: " . $doc['filepath']); $results[] = [ 'document' => $doc['title'], 'status' => 'NSFW', 'message' => 'Файл содержит NSFW-контент и отправлен на ручную модерацию.' ]; continue; } // 2. Попытка извлечь текст напрямую (например, pdftotext) $extractedText = extractText($doc['filepath']); if (empty($extractedText)) { // Если прямое извлечение не дало результата, запускаем OCR $extractedText = doOCR($doc['filepath']); } // 3. Если текста все равно нет – используем Vision для описания изображения if (empty($extractedText)) { $extractedText = describeImageWithVision($doc['filepath']); } // 4. Получаем контекст из базы знаний (stub-функция) $knowledgeContext = getKnowledgeBaseContext($doc['filepath']); // 5. Объединяем извлеченный текст и контекст $finalContent = $extractedText . "\n" . $knowledgeContext; // 6. Получаем file_id для привязки (если имеется) $fileId = $uploadedFileIds[$doc['filepath']] ?? ''; // 7. Анализируем документ через ассистента $threadId = createThread(); if (!$threadId) { logMessage("Ошибка создания треда для " . $doc['filepath']); continue; } $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; } /** * Функция 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 || !file_exists($outputFile)) { logMessage("Ошибка извлечения текста из PDF: $filePath"); return ''; } $text = file_get_contents($outputFile); unlink($outputFile); return $text; } /** * Функция doOCR использует локальную OCR-систему (например, 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 || !file_exists($outputFile . ".txt")) { logMessage("Ошибка OCR для файла: $filePath"); return ''; } $text = file_get_contents($outputFile . ".txt"); unlink($outputFile . ".txt"); return $text; } /** * Функция describeImageWithVision вызывает OpenAI Vision для получения описания изображения. */ function describeImageWithVision($filePath) { logMessage("Запуск описания изображения через Vision для файла: $filePath"); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VISION_API . "/describe", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => [ 'file' => new CURLFile($filePath) ], CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: vision' ] ]); $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); return $decoded['description'] ?? ''; } /** * Функция checkNSFWWithVision использует OpenAI Vision для анализа NSFW-контента. */ function checkNSFWWithVision($filePath) { logMessage("NSFW-проверка через стандартный Vision endpoint для файла: $filePath"); $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_VISION_API . "/analyze", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => [ 'file' => new CURLFile($filePath) ], CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . OPENAI_API_KEY, 'OpenAI-Beta: vision' ] ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $curlError = curl_error($curl); curl_close($curl); if ($curlError) { logMessage("Ошибка cURL при проверке NSFW через Vision: " . $curlError); return null; } logMessage("Ответ Vision (анализ): HTTP $httpCode - " . $response); $decoded = json_decode($response, true); if ($httpCode === 200 && isset($decoded['nsfw'])) { return $decoded['nsfw']; // предполагается булевое значение } logMessage("Ошибка анализа NSFW через стандартный Vision endpoint: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return null; } function classifyImage($imagePath) { $absolutePath = realpath($imagePath); if (!$absolutePath) { logMessage("ERROR: Не удалось получить абсолютный путь для " . $imagePath); return []; } logMessage("DEBUG: Абсолютный путь для классификации: " . $absolutePath); $escapedPath = escapeshellarg($absolutePath); logMessage("DEBUG: Экранированный путь для классификации: " . $escapedPath); // Выполнение команды Python для запуска NudeClassifier $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); } function checkNSFWLocally($filePath) { logMessage("Запуск локальной проверки NSFW для файла: $filePath"); $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $imageToCheck = $filePath; if ($extension === 'pdf') { $outputImage = tempnam(sys_get_temp_dir(), 'pdf_img_') . '.png'; $command = "convert -density 150 " . escapeshellarg($filePath) . "[0] -quality 90 " . escapeshellarg($outputImage); exec($command, $output, $returnVar); if ($returnVar !== 0) { logMessage("Ошибка конвертации PDF в изображение для локальной NSFW проверки."); return null; } $imageToCheck = $outputImage; } // Используем функцию classifyImage для анализа изображения $classification = classifyImage($imageToCheck); if (empty($classification)) { logMessage("DEBUG: Нет данных проверки NSFW для изображения '$imageToCheck'."); return false; } $absImagePath = realpath($imageToCheck); if (isset($classification[$absImagePath])) { $unsafeProbability = $classification[$absImagePath]['unsafe'] ?? 0; logMessage("DEBUG: Для изображения '$absImagePath' получено unsafeProbability = " . $unsafeProbability); if ($unsafeProbability > 0.8) { logMessage("⚠️ Обнаружено NSFW-изображение: $absImagePath (unsafe = " . $unsafeProbability . ")"); return true; } else { logMessage("DEBUG: unsafeProbability для '$absImagePath' ниже порогового значения (0.8)."); } } else { logMessage("DEBUG: Классификатор не вернул данные для '$absImagePath'."); } return false; } /** * Stub-функция для получения контекста из базы знаний. */ function getKnowledgeBaseContext($filePath) { // Здесь можно реализовать поиск в базе знаний. // Пока возвращаем статическую информацию. return "Статическая информация: нормы и законы РФ, судебные прецеденты..."; } /** * Функция analyzeDocumentWithAssistant отправляет финальный запрос ассистенту. * Дополнительно передаёт извлечённый текст и контекст. */ function analyzeDocumentWithAssistant($threadId, $assistantId, $fileId, $content) { logMessage("Анализ документа: thread_id=$threadId, fileId=$fileId"); $messageContent = "Проанализируй документ"; if (!empty($fileId)) { $messageContent .= " (file_id: $fileId)"; } $messageContent .= ". Содержимое для анализа:\n" . $content; $messageData = [ 'role' => 'user', 'content' => $messageContent ]; // Отправка сообщения в тред $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/$threadId/messages", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($messageData), 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 null; } logMessage("Ответ (сообщение): HTTP $httpCode - " . $response); $decoded = json_decode($response, true); if ($httpCode !== 200 || !isset($decoded['id'])) { logMessage("Ошибка отправки сообщения: " . json_encode($decoded, JSON_UNESCAPED_UNICODE)); return null; } // Запуск ассистента $runData = ['assistant_id' => $assistantId]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/$threadId/runs", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($runData), 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 null; } logMessage("Ответ (запуск ассистента): HTTP $httpCode - " . $response); $decodedRun = json_decode($response, true); if ($httpCode !== 200 || !isset($decodedRun['id'])) { logMessage("Ошибка запуска ассистента: " . json_encode($decodedRun, JSON_UNESCAPED_UNICODE)); return null; } $runId = $decodedRun['id']; // Ожидание завершения обработки ассистентом с максимальным таймаутом $maxIterations = 30; // максимум 30 итераций (примерно 60 секунд) $iterations = 0; do { sleep(2); $iterations++; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/$threadId/runs/$runId", CURLOPT_RETURNTRANSFER => true, 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 null; } logMessage("Статус запуска (итерация $iterations): HTTP $httpCode - " . $response); $decodedStatus = json_decode($response, true); $status = $decodedStatus['status'] ?? null; if ($iterations >= $maxIterations) { logMessage("Превышен максимальный таймаут ожидания завершения работы ассистента."); return null; } } while ($status === 'queued' || $status === 'in_progress'); if ($status !== 'completed') { logMessage("Запуск ассистента завершился с ошибкой: $status"); return null; } // Получение результата $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => OPENAI_THREADS_API . "/$threadId/messages", CURLOPT_RETURNTRANSFER => true, 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 null; } logMessage("Ответ (сообщения): HTTP $httpCode - " . $response); $decodedMessages = json_decode($response, true); if ($httpCode !== 200 || !isset($decodedMessages['data'])) { logMessage("Ошибка получения сообщений: " . json_encode($decodedMessages, JSON_UNESCAPED_UNICODE)); return null; } logMessage("Результаты анализа: " . json_encode($decodedMessages['data'], JSON_UNESCAPED_UNICODE)); return $decodedMessages['data']; } /* ===================== Формирование отчета ===================== */ function generateReport($allResults) { if (empty($allResults)) { logMessage("Ошибка: Нет данных для отчета"); return "Ошибка: Нет данных для отчета"; } $report = "### Итоговый отчет по документам\n\n"; foreach ($allResults as $result) { $report .= "**Документ:** " . $result['document'] . "\n"; $report .= "**Статус:** " . $result['status'] . "\n"; if (isset($result['analysis'])) { $report .= "**Анализ:** " . json_encode($result['analysis'], JSON_UNESCAPED_UNICODE) . "\n"; } else { $report .= "**Сообщение:** " . $result['message'] . "\n"; } $report .= "\n"; } return $report; }