788 lines
34 KiB
PHP
788 lines
34 KiB
PHP
|
|
<?php
|
|||
|
|
ini_set('display_errors', 1);
|
|||
|
|
ini_set('display_startup_errors', 1);
|
|||
|
|
error_reporting(E_ALL);
|
|||
|
|
ini_set('log_errors', 1);
|
|||
|
|
ini_set('error_log', 'logs/php_errors.log');
|
|||
|
|
|
|||
|
|
define('LOG_FILE', 'logs/HybridPDFProcessor.log');
|
|||
|
|
define('OPENAI_API_KEY', 'sk-GS24OxHQYfq8ErW5CRLoN5F1CfJPxNsY');
|
|||
|
|
define('OPENAI_API_URL', 'https://api.proxyapi.ru/openai/v1/chat/completions');
|
|||
|
|
|
|||
|
|
define('OPENAI_ASSISTANT_API', 'https://api.proxyapi.ru/openai/v1/assistants');
|
|||
|
|
define('OPENAI_FILES_API', 'https://api.proxyapi.ru/openai/v1/files');
|
|||
|
|
define('OPENAI_THREADS_API', 'https://api.proxyapi.ru/openai/v1/threads');
|
|||
|
|
define('OPENAI_VECTOR_STORES_API', 'https://api.proxyapi.ru/openai/v1/vector_stores');
|
|||
|
|
// Для Vision используем тот же endpoint через ProxyAPI, как в code4
|
|||
|
|
define('OPENAI_VISION_API', 'https://api.proxyapi.ru/v1/chat/completions');
|
|||
|
|
|
|||
|
|
define('ASSISTANT_ID', 'asst_suGt51aoepXUkJiC0t3vobeG');
|
|||
|
|
define('ASSISTANT_NAME', 'Clientright');
|
|||
|
|
|
|||
|
|
// Подключение к БД
|
|||
|
|
$dsn = 'mysql:host=localhost;port=3306;dbname=ci20465_72new;charset=utf8mb4';
|
|||
|
|
$user = 'ci20465_72new';
|
|||
|
|
$password = 'EcY979Rn';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
logMessage("Ошибка подключения к БД: " . $e->getMessage());
|
|||
|
|
die(json_encode(['status' => 'error', 'message' => 'Database connection error']));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Основная логика обработки POST запроса
|
|||
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|||
|
|
$id = $input['id'] ?? null;
|
|||
|
|
|
|||
|
|
if (!$id || !is_numeric($id)) {
|
|||
|
|
logMessage("Ошибка: Некорректный ID");
|
|||
|
|
die(json_encode(['status' => 'error', 'message' => 'Invalid ID']));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Получаем данные документов
|
|||
|
|
$documents = fetchDocumentData($pdo, $id);
|
|||
|
|
if (!$documents) {
|
|||
|
|
throw new Exception("Documents not found");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем массив для хранения результатов обработки
|
|||
|
|
$processedDocuments = [];
|
|||
|
|
|
|||
|
|
// Обрабатываем каждый документ
|
|||
|
|
foreach ($documents as $document) {
|
|||
|
|
logMessage("Обработка документа: " . $document['title']);
|
|||
|
|
|
|||
|
|
// Создаем тред для документа
|
|||
|
|
$threadId = createThread();
|
|||
|
|
if (!$threadId) {
|
|||
|
|
logMessage("Ошибка создания треда для документа: " . $document['title']);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Загружаем файл в OpenAI
|
|||
|
|
$fileId = uploadFileToOpenAI($document['filepath']);
|
|||
|
|
if (!$fileId) {
|
|||
|
|
logMessage("Ошибка загрузки файла для документа: " . $document['title']);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Анализируем документ
|
|||
|
|
$analysis = analyzeDocumentWithAssistant($threadId, ASSISTANT_ID, $fileId, $document['content']);
|
|||
|
|
if ($analysis) {
|
|||
|
|
$processedDocuments[] = [
|
|||
|
|
'title' => $document['title'],
|
|||
|
|
'analysis' => $analysis
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Формируем итоговый ответ
|
|||
|
|
echo json_encode([
|
|||
|
|
'status' => 'success',
|
|||
|
|
'documents' => $processedDocuments
|
|||
|
|
], JSON_UNESCAPED_UNICODE);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
logMessage("Ошибка: " . $e->getMessage());
|
|||
|
|
die(json_encode(['status' => 'error', 'message' => $e->getMessage()]));
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
die(json_encode(['status' => 'error', 'message' => 'Method not allowed']));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function logMessage($message) {
|
|||
|
|
if (!is_dir('logs')) {
|
|||
|
|
mkdir('logs', 0777, true);
|
|||
|
|
}
|
|||
|
|
$message = mb_convert_encoding($message, 'UTF-8', 'auto');
|
|||
|
|
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " - " . $message . "\n", FILE_APPEND | LOCK_EX);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Функция транслитерации (для переименования файлов)
|
|||
|
|
function transliterate($text) {
|
|||
|
|
$cyr = ['а','б','в','г','д','е','ё','ж','з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ц','ч','ш','щ','ъ','ы','ь','э','ю','я'];
|
|||
|
|
$lat = ['a','b','v','g','d','e','yo','zh','z','i','y','k','l','m','n','o','p','r','s','t','u','f','h','ts','ch','sh','shch','','y','','e','yu','ya'];
|
|||
|
|
return str_replace($cyr, $lat, mb_strtolower($text));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renameFileForProcessing($filePath, $targetFolder = "scanpdf") {
|
|||
|
|
$pathInfo = pathinfo($filePath);
|
|||
|
|
$newName = transliterate($pathInfo['filename']) . "_" . uniqid() . "." . $pathInfo['extension'];
|
|||
|
|
$newPath = rtrim($targetFolder, "/") . "/" . $newName;
|
|||
|
|
if (copy($filePath, $newPath)) {
|
|||
|
|
logMessage("Файл переименован: $filePath -> $newPath");
|
|||
|
|
return $newPath;
|
|||
|
|
} else {
|
|||
|
|
logMessage("Ошибка при копировании файла: $filePath");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Извлечение текста из PDF с помощью pdftotext
|
|||
|
|
function extractTextFromPDF($pdfPath) {
|
|||
|
|
$text = shell_exec("pdftotext -layout -enc UTF-8 " . escapeshellarg($pdfPath) . " -");
|
|||
|
|
$text = mb_convert_encoding($text, 'UTF-8', 'auto');
|
|||
|
|
logMessage("DEBUG: Извлеченный текст (начало): " . substr($text, 0, 500));
|
|||
|
|
return trim($text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Конвертация PDF в изображения (ImageMagick convert)
|
|||
|
|
function convertPdfToImages($pdfPath, $outputDir) {
|
|||
|
|
if (!file_exists($outputDir)) {
|
|||
|
|
mkdir($outputDir, 0777, true);
|
|||
|
|
logMessage("Создана директория для изображений: $outputDir");
|
|||
|
|
}
|
|||
|
|
$imagePattern = $outputDir . '/page-%03d.jpg';
|
|||
|
|
$command = "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');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Удаление рабочей папки и ее содержимого
|
|||
|
|
function deleteFolderAndContents($folderPath) {
|
|||
|
|
if (is_dir($folderPath)) {
|
|||
|
|
$files = array_diff(scandir($folderPath), array('.', '..'));
|
|||
|
|
foreach ($files as $file) {
|
|||
|
|
$filePath = $folderPath . DIRECTORY_SEPARATOR . $file;
|
|||
|
|
if (is_dir($filePath)) {
|
|||
|
|
deleteFolderAndContents($filePath);
|
|||
|
|
} else {
|
|||
|
|
unlink($filePath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
rmdir($folderPath);
|
|||
|
|
logMessage("Удалена директория: $folderPath");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Запуск OCR с Tesseract
|
|||
|
|
function doOCR($filePath) {
|
|||
|
|
logMessage("Запуск OCR для файла: $filePath");
|
|||
|
|
$outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt';
|
|||
|
|
$command = "tesseract " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile) . " -l rus+eng";
|
|||
|
|
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 trim($text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Запуск NudeNet для NSFW-проверки изображения
|
|||
|
|
function classifyImage($imagePath) {
|
|||
|
|
$absolutePath = realpath($imagePath);
|
|||
|
|
if (!$absolutePath) {
|
|||
|
|
logMessage("ERROR: Не удалось получить абсолютный путь для " . $imagePath);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
logMessage("DEBUG: Абсолютный путь для классификации: " . $absolutePath);
|
|||
|
|
$escapedPath = escapeshellarg($absolutePath);
|
|||
|
|
$command = "python3 -c \"import json; from nudenet import NudeClassifier; classifier = NudeClassifier(); print(json.dumps(classifier.classify($escapedPath)))\"";
|
|||
|
|
logMessage("DEBUG: Выполнение команды NudeNet: " . $command);
|
|||
|
|
$output = shell_exec($command);
|
|||
|
|
logMessage("DEBUG: Вывод NudeNet: " . $output);
|
|||
|
|
if ($output === null) {
|
|||
|
|
logMessage("ERROR: shell_exec вернул null при выполнении NudeClassifier");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
return json_decode(trim($output), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Локальная NSFW-проверка с использованием NudeNet
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
$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: unsafeProbability для '$absImagePath' = " . $unsafeProbability);
|
|||
|
|
return $unsafeProbability > 0.8;
|
|||
|
|
}
|
|||
|
|
logMessage("DEBUG: Классификатор не вернул данные для '$absImagePath'.");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stub для получения контекста из базы знаний
|
|||
|
|
function getKnowledgeBaseContext() {
|
|||
|
|
return "Статическая информация: нормы и законы РФ, судебные прецеденты...";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Функции для работы с OpenAI Assistants (как в code4)
|
|||
|
|
function uploadFileToOpenAI($filePath) {
|
|||
|
|
logMessage("Загрузка файла в OpenAI: $filePath");
|
|||
|
|
$ch = curl_init(OPENAI_FILES_API);
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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 createVectorStore() {
|
|||
|
|
$ch = curl_init();
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($ch);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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 addFileToVectorStore($vectorStoreId, $fileId) {
|
|||
|
|
$ch = curl_init(OPENAI_VECTOR_STORES_API . "/$vectorStoreId/files");
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
logMessage("Ответ OpenAI (добавление файла в Vector Store): 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]
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
];
|
|||
|
|
$ch = curl_init();
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($ch);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
];
|
|||
|
|
// Отправляем сообщение в тред
|
|||
|
|
$ch = curl_init(OPENAI_THREADS_API . "/$threadId/messages");
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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];
|
|||
|
|
$ch = curl_init(OPENAI_THREADS_API . "/$threadId/runs");
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
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($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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;
|
|||
|
|
$iterations = 0;
|
|||
|
|
do {
|
|||
|
|
sleep(2);
|
|||
|
|
$iterations++;
|
|||
|
|
$ch = curl_init(OPENAI_THREADS_API . "/$threadId/runs/$runId");
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
// Получаем результат
|
|||
|
|
$ch = curl_init(OPENAI_THREADS_API . "/$threadId/messages");
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($ch);
|
|||
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Основной процесс: получение ID из POST-запроса, выбор файлов из БД и обработка
|
|||
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|||
|
|
$id = $input['id'] ?? null;
|
|||
|
|
if (!$id || !is_numeric($id)) {
|
|||
|
|
logMessage("Ошибка: Некорректный ID.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Некорректный ID."]));
|
|||
|
|
}
|
|||
|
|
/* $id = $_POST['id'] ?? null;
|
|||
|
|
if (!$id || !is_numeric($id)) {
|
|||
|
|
logMessage("Ошибка: Некорректный ID.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Некорректный ID."]));
|
|||
|
|
} */
|
|||
|
|
|
|||
|
|
$dbconfig = [
|
|||
|
|
'db_server' => 'localhost',
|
|||
|
|
'db_port' => '3306',
|
|||
|
|
'db_username' => 'ci20465_72new',
|
|||
|
|
'db_password' => 'EcY979Rn',
|
|||
|
|
'db_name' => 'ci20465_72new'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$conn = new mysqli($dbconfig['db_server'], $dbconfig['db_username'], $dbconfig['db_password'], $dbconfig['db_name'], $dbconfig['db_port']);
|
|||
|
|
if ($conn->connect_error) {
|
|||
|
|
logMessage("Ошибка подключения к БД: " . $conn->connect_error);
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка подключения к БД."]));
|
|||
|
|
}
|
|||
|
|
$conn->set_charset("utf8mb4");
|
|||
|
|
|
|||
|
|
$stmt = $conn->prepare("
|
|||
|
|
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')
|
|||
|
|
");
|
|||
|
|
$stmt->bind_param("i", $id);
|
|||
|
|
$stmt->execute();
|
|||
|
|
$result = $stmt->get_result();
|
|||
|
|
if ($result->num_rows == 0) {
|
|||
|
|
logMessage("Ошибка: Нет данных в БД.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Нет данных."]));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$uniqueFolder = "scanpdf/" . uniqid('run_', true);
|
|||
|
|
if (!file_exists($uniqueFolder)) {
|
|||
|
|
mkdir($uniqueFolder, 0777, true);
|
|||
|
|
logMessage("Создана рабочая папка: " . $uniqueFolder);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$files_data = [];
|
|||
|
|
while ($row = $result->fetch_assoc()) {
|
|||
|
|
$title = $row["title"];
|
|||
|
|
$filePath = $row["filepath"];
|
|||
|
|
logMessage("Обрабатываем файл: " . $filePath);
|
|||
|
|
$tempFilePath = renameFileForProcessing($filePath, $uniqueFolder);
|
|||
|
|
if (!$tempFilePath) continue;
|
|||
|
|
|
|||
|
|
$data = ["title" => $title];
|
|||
|
|
$text = extractTextFromPDF($tempFilePath);
|
|||
|
|
if (!empty(trim($text)) && mb_strlen(trim($text), 'UTF-8') >= 100) {
|
|||
|
|
logMessage("Текст извлечён из '$title'.");
|
|||
|
|
$data["text"] = $text;
|
|||
|
|
} else {
|
|||
|
|
logMessage("Текст не извлечён из '$title', конвертируем в изображения.");
|
|||
|
|
$outputDir = $uniqueFolder . "/pdf_images_" . uniqid();
|
|||
|
|
$images = convertPdfToImages($tempFilePath, $outputDir);
|
|||
|
|
if (!empty($images)) {
|
|||
|
|
$recognizedText = "";
|
|||
|
|
$data["nsfw_alert"] = false;
|
|||
|
|
foreach ($images as $img) {
|
|||
|
|
if (checkNSFWLocally($img)) {
|
|||
|
|
$data["nsfw_alert"] = true;
|
|||
|
|
logMessage("NSFW обнаружен в изображении: " . $img);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
$ocrText = doOCR($img);
|
|||
|
|
if (!empty(trim($ocrText))) {
|
|||
|
|
$recognizedText .= $ocrText . "\n";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (isset($data["nsfw_alert"]) && $data["nsfw_alert"] === true) {
|
|||
|
|
unset($data["text"]);
|
|||
|
|
$data["images"] = $images;
|
|||
|
|
} else {
|
|||
|
|
if (!empty(trim($recognizedText)) && mb_strlen(trim($recognizedText), 'UTF-8') >= 100) {
|
|||
|
|
$data["text"] = $recognizedText;
|
|||
|
|
} else {
|
|||
|
|
$data["images"] = $images;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
logMessage("Ошибка: Изображения не созданы для '$title'.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$data["moderation_passed"] = isset($data["nsfw_alert"]) && $data["nsfw_alert"] === true ? false : (isset($data["text"]) && !empty(trim($data["text"])) ? true : false);
|
|||
|
|
$files_data[] = $data;
|
|||
|
|
}
|
|||
|
|
$stmt->close();
|
|||
|
|
$conn->close();
|
|||
|
|
|
|||
|
|
logMessage("Данные для анализа: " . json_encode($files_data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|||
|
|
|
|||
|
|
// Загрузка файлов в Vector Store (используем методы из code4)
|
|||
|
|
function createVectorStoreAndUploadFiles($files_data) {
|
|||
|
|
$vectorStoreId = createVectorStore();
|
|||
|
|
if (!$vectorStoreId) return null;
|
|||
|
|
$uploadedFiles = [];
|
|||
|
|
foreach ($files_data as $data) {
|
|||
|
|
if (!isset($data["filepath"])) continue;
|
|||
|
|
$filePath = $data["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];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$uploadResult = createVectorStoreAndUploadFiles($files_data);
|
|||
|
|
if (!$uploadResult) {
|
|||
|
|
logMessage("Ошибка создания Vector Store или загрузки файлов");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка создания Vector Store или загрузки файлов"]));
|
|||
|
|
}
|
|||
|
|
$vectorStoreId = $uploadResult['vectorStoreId'];
|
|||
|
|
$uploadedFileIds = $uploadResult['fileIds'];
|
|||
|
|
|
|||
|
|
if (!updateAssistantWithVectorStore($vectorStoreId)) {
|
|||
|
|
logMessage("Ошибка обновления ассистента с Vector Store");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка обновления ассистента"]));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Формирование итогового запроса для OpenAI
|
|||
|
|
$task = "🔹 Отвечай по шаблону. **Задача**:
|
|||
|
|
Проанализируй загруженные документы, выполнив следующие действия:
|
|||
|
|
|
|||
|
|
1️⃣ **Список файлов и проверка соответствия названий**
|
|||
|
|
- Перечисли все загруженные файлы.
|
|||
|
|
- Внимательно анализируй изображения.
|
|||
|
|
- Если изображение содержит текст, сначала извлеки текст и проанализируй его содержание.
|
|||
|
|
- Укажи, если текст плохо читается или частично распознан.
|
|||
|
|
- Если изображение не содержит текста, опиши, что на нем изображено.
|
|||
|
|
- Обрати внимание на пометку nsfw_alert. Если она true, файл требует ручной проверки.
|
|||
|
|
|
|||
|
|
2️⃣ **Краткий анализ спора**
|
|||
|
|
- Определи истца (потребителя) и ответчика (компанию, на которую подана жалоба).
|
|||
|
|
- Опиши суть спора и основные аргументы сторон.
|
|||
|
|
|
|||
|
|
3️⃣ **Проверка на цензуру**
|
|||
|
|
- Проверь документы на наличие ненормативной лексики и нецензурных изображений.
|
|||
|
|
|
|||
|
|
4️⃣ **Выдача итогового вердикта**
|
|||
|
|
- Если всё соответствует, укажи: \"Вердикт: Прошло модерацию.\"
|
|||
|
|
- Если есть проблемы, укажи, что требуется ручная проверка.
|
|||
|
|
|
|||
|
|
5️⃣ **Характер спора**
|
|||
|
|
- Дай краткую характеристику дела (например, \"возврат денег\", \"некачественная услуга\" и т.д.).
|
|||
|
|
|
|||
|
|
6️⃣ **Вероятность положительного решения спора**
|
|||
|
|
- Укажи вероятность в процентах.
|
|||
|
|
|
|||
|
|
7️⃣ **Чего не хватает**
|
|||
|
|
- Запроси дополнительные документы, если необходимо.
|
|||
|
|
|
|||
|
|
📌 **Важно**: Отчет должен быть структурированным, четким и лаконичным, с указанием норм права РФ, регулирующих данный спор, и приоритетных файлов для ручной проверки.";
|
|||
|
|
|
|||
|
|
$finalContent = "";
|
|||
|
|
foreach ($files_data as $file) {
|
|||
|
|
$finalContent .= json_encode($file, JSON_UNESCAPED_UNICODE) . "\n";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$payload = [
|
|||
|
|
"model" => "o1",
|
|||
|
|
"messages" => [
|
|||
|
|
["role" => "system", "content" => "Ты юридический аналитик. Проанализируй материалы согласно инструкции."],
|
|||
|
|
["role" => "user", "content" => $task],
|
|||
|
|
["role" => "user", "content" => $finalContent]
|
|||
|
|
],
|
|||
|
|
"max_completion_tokens" => 4000
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$payload_json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|||
|
|
logMessage("Отправка запроса в OpenAI: " . $payload_json);
|
|||
|
|
|
|||
|
|
$ch = curl_init(OPENAI_API_URL);
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => $payload_json,
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($ch);
|
|||
|
|
curl_close($ch);
|
|||
|
|
|
|||
|
|
logMessage("Ответ от OpenAI: " . $response);
|
|||
|
|
$gptAnalysis = json_decode($response, true);
|
|||
|
|
if (!is_array($gptAnalysis)) {
|
|||
|
|
logMessage("Ошибка: JSON-декодирование не удалось.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка: JSON-декодирование не удалось."]));
|
|||
|
|
}
|
|||
|
|
if (!isset($gptAnalysis['choices']) || empty($gptAnalysis['choices'])) {
|
|||
|
|
logMessage("Ошибка: в JSON-ответе отсутствует ключ 'choices'.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка: в JSON-ответе отсутствует ключ 'choices'."]));
|
|||
|
|
}
|
|||
|
|
$content = $gptAnalysis['choices'][0]['message']['content'] ?? null;
|
|||
|
|
if (!$content) {
|
|||
|
|
logMessage("Ошибка: контент не найден в ответе от GPT.");
|
|||
|
|
die(json_encode(["status" => "error", "message" => "Ошибка: контент не найден в ответе от GPT."]));
|
|||
|
|
}
|
|||
|
|
if (preg_match('/Вердикт:\s*(Прошло модерацию|Не прошло модерацию)/ui', $content, $matches)) {
|
|||
|
|
$moderationVerdict = trim($matches[1]);
|
|||
|
|
} else {
|
|||
|
|
$moderationVerdict = "";
|
|||
|
|
}
|
|||
|
|
logMessage("Извлеченный вердикт модерации: " . ($moderationVerdict ?: "Не найден"));
|
|||
|
|
|
|||
|
|
$final_output = [
|
|||
|
|
"status" => "complete",
|
|||
|
|
"analysis" => $content,
|
|||
|
|
"moderationVerdict" => $moderationVerdict
|
|||
|
|
// "files_data" => $files_data
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
logMessage("Финальный вывод: " . json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|||
|
|
echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|||
|
|
|
|||
|
|
// Очистка: удаление рабочей папки
|
|||
|
|
deleteFolderAndContents($uniqueFolder);
|
|||
|
|
|
|||
|
|
function fetchDocumentData($pdo, $id) {
|
|||
|
|
try {
|
|||
|
|
// Изменяем SQL-запрос для корректного получения данных
|
|||
|
|
$stmt = $pdo->prepare("
|
|||
|
|
SELECT DISTINCT
|
|||
|
|
n.title,
|
|||
|
|
n.content,
|
|||
|
|
CONCAT(a.path, a.attachmentsid, '_', COALESCE(a.storedname, a.name)) AS filepath
|
|||
|
|
FROM vtiger_notes n
|
|||
|
|
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
|
|||
|
|
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
|
|||
|
|
LEFT JOIN vtiger_seattachmentsrel sar ON sar.crmid = n.notesid
|
|||
|
|
LEFT JOIN vtiger_attachments a ON a.attachmentsid = sar.attachmentsid
|
|||
|
|
WHERE snr.crmid = ?
|
|||
|
|
AND e.deleted = 0
|
|||
|
|
");
|
|||
|
|
|
|||
|
|
// Логируем SQL запрос
|
|||
|
|
logMessage("SQL запрос: " . $stmt->queryString);
|
|||
|
|
|
|||
|
|
$stmt->execute([$id]);
|
|||
|
|
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// Логируем результат запроса
|
|||
|
|
logMessage("Результат запроса: " . print_r($documents, true));
|
|||
|
|
|
|||
|
|
if (empty($documents)) {
|
|||
|
|
logMessage("Документы не найдены для ID = $id");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем наличие файлов
|
|||
|
|
foreach ($documents as $key => $doc) {
|
|||
|
|
if (isset($doc['filepath']) && !file_exists($doc['filepath'])) {
|
|||
|
|
logMessage("Предупреждение: Файл не существует: " . $doc['filepath']);
|
|||
|
|
// Попробуем альтернативный путь
|
|||
|
|
$alternativePath = "storage/" . basename($doc['filepath']);
|
|||
|
|
if (file_exists($alternativePath)) {
|
|||
|
|
$documents[$key]['filepath'] = $alternativePath;
|
|||
|
|
logMessage("Найден альтернативный путь: " . $alternativePath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Логируем финальный результат
|
|||
|
|
logMessage("Обработанные документы: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
|
|||
|
|
|
|||
|
|
return $documents;
|
|||
|
|
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
logMessage("Ошибка SQL: " . $e->getMessage());
|
|||
|
|
logMessage("SQL State: " . $e->getCode());
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
?>
|