Files
crm.clientright.ru/iacode6.php
Fedor ac7467f0b4 Major CRM updates: AI Assistant, Court Status API, S3 integration improvements, and extensive file storage system
- Added comprehensive AI Assistant system (aiassist/ directory):
  * Vector search and embedding capabilities
  * Typebot proxy integration
  * Elastic search functionality
  * Message classification and chat history
  * MCP proxy for external integrations

- Implemented Court Status API (GetCourtStatus.php):
  * Real-time court document status checking
  * Integration with external court systems
  * Comprehensive error handling and logging

- Enhanced S3 integration:
  * Improved file backup system with metadata
  * Batch processing capabilities
  * Enhanced error logging and recovery
  * Copy operations with URL fixing

- Added Telegram contact creation API
- Improved error logging across all modules
- Enhanced callback system for AI responses
- Extensive backup file storage with timestamps
- Updated documentation and README files

- File storage improvements:
  * Thousands of backup files with proper metadata
  * Fix operations for broken file references
  * Project-specific backup and recovery systems
  * Comprehensive file integrity checking

Total: 26,461+ files added/modified including AWS SDK, vendor dependencies, and extensive backup system.
2025-10-16 11:17:21 +03:00

930 lines
44 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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');
// Подключаем модули
require_once 'elastic.php'; // Файл с функциями для работы с ElasticSearch
// Настройки OpenAI API и модерации
const OPENAI_API_KEY = 'sk-GS24OxHQYfq8ErW5CRLoN5F1CfJPxNsY';
const OPENAI_ASSISTANT_API = 'https://api.proxyapi.ru/openai/v1/assistants';
const OPENAI_FILES_API = 'https://api.proxyapi.ru/openai/v1/files';
const OPENAI_THREADS_API = 'https://api.proxyapi.ru/openai/v1/threads';
const OPENAI_VECTOR_STORES_API = 'https://api.proxyapi.ru/openai/v1/vector_stores';
const OPENAI_VISION_API = 'https://api.proxyapi.ru/openai/v1/chat/completions'; // для описания изображений
// URL для NSFW-модерации
const NSFW_MODERATION_API = 'https://api.proxyapi.ru/v1/moderation/nsfw';
const LOG_FILE = 'logs/scriptass1311.log';
// ID и имя ассистента
const ASSISTANT_ID = 'asst_suGt51aoepXUkJiC0t3vobeG';
const ASSISTANT_NAME = 'Clientright';
// Для корректной обработки кириллицы
setlocale(LC_ALL, 'ru_RU.UTF-8');
// Подключение к БД (Vtiger CRM)
$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("Ошибка подключения к БД");
}
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') {
// Получаем ID из $_POST или JSON-тела запроса
$id = $_POST['id'] ?? null;
if (!$id) {
$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."], JSON_UNESCAPED_UNICODE));
}
// Извлекаем документы из CRM через БД
$documents = fetchDocumentData($pdo, $id);
if (empty($documents)) {
logMessage("Документы не найдены для ID: $id");
die("Документы не найдены для ID: $id");
}
logMessage("Документы получены из БД: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
// Создаём массив строковых путей (извлекаем только 'filepath')
$filePathList = array_map(function($doc) {
return $doc['filepath'];
}, $documents);
// --- Проверка наличия ранее сохранённого анализа ---
/* $previousAnalysis = checkPreviousAnalysis($id, $documents);
if ($previousAnalysis) {
logMessage("✅ Используем сохранённый анализ из ElasticSearch.");
sendAnalysisToCRM($id, $previousAnalysis);
}
*/
$previousAnalysis = checkPreviousAnalysis($id, $filePathList);
logMessage("DEBUG: Значение предыдущего анализа: " . print_r($previousAnalysis, true));
if ($previousAnalysis) {
logMessage("Найден сохранённый анализ, отправляем его в CRM.");
sendAnalysisToCRM($id, $previousAnalysis);
exit;
} else {
logMessage("Сохранённый анализ не найден, продолжаем обработку.");
}
// --- Конец проверки ранее сохранённого анализа ---
// Подготовка документов и загрузка файлов
$uploadResult = createVectorStoreAndUploadFiles($filePathList);
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);
logMessage("Собранный контент для анализа:\n" . $combinedContent);
if (empty($combinedContent)) {
logMessage("Ошибка: анализ документов не вернул результатов");
die("Ошибка: анализ документов не вернул результатов");
}
$fileIdCombined = implode(',', array_values($uploadedFileIds));
logMessage("Объединённый список идентификаторов файлов: " . $fileIdCombined);
$threadId = createThread();
if (!$threadId) {
logMessage("Ошибка создания треда");
die("Ошибка создания треда");
}
// Запуск анализа документов через ассистента (stream)
$analysis = analyzeDocumentWithAssistantStream($threadId, ASSISTANT_ID, $fileIdCombined, $combinedContent);
if (!$analysis) {
logMessage("❌ Ошибка анализа совокупного запроса");
die("Ошибка анализа совокупного запроса");
}
// Сохранение результата анализа в ElasticSearch
$saveResult = saveAnalysisToElasticSearch($id, $analysis, $filePathList);
if ($saveResult) {
logMessage("✅ Анализ успешно сохранён в ElasticSearch.");
} else {
logMessage("❌ Ошибка при сохранении анализа в ElasticSearch.");
}
// Отправка результата в CRM через функцию sendAnalysisToCRM()
sendAnalysisToCRM($id, $analysis);
// Если функция sendAnalysisToCRM() корректно завершила работу, выполнение дальше не продолжается.
logMessage("Обработка всех документов завершена.");
} else {
logMessage("Ошибка: запрос должен быть POST");
die("Ошибка: запрос должен быть POST");
}
/* ===================== Функция отправки результата в CRM ===================== */
/*function sendAnalysisToCRM($caseId, $analysis) {
// Формируем финальный ответ
$final_output = [
"status" => $analysis['status'] ?? 'error',
"content" => $analysis['content'] ?? 'Анализ не выполнен',
"moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен'
];
// Логируем финальные данные перед отправкой
logMessage("DEBUG: Финальный массив данных: " . print_r($final_output, true));
// Кодируем в JSON
$json_output = json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// Проверка на ошибки кодирования
if ($json_output === false) {
$error = json_last_error_msg();
logMessage("ERROR: Ошибка кодирования JSON: $error");
die(json_encode(["status" => "error", "message" => "Ошибка формирования ответа"]));
}
// Логируем итоговый JSON
logMessage("DEBUG: Итоговый JSON для CRM: " . $json_output);
// Отправляем JSON-ответ
echo $json_output;
// Логируем завершение
logMessage("Обработка завершена. Ответ успешно отправлен в CRM");
exit;
}
*/
function sendAnalysisToCRM($caseId, $analysis) {
if (!isset($analysis['content'])) {
$analysis['content'] = $analysis['анализ_gpt'] ?? 'Анализ не выполнен';
}
if (!isset($analysis['moderationVerdict'])) {
$analysis['moderationVerdict'] = $analysis['вывод_gpt'] ?? 'Не определен';
}
if (!isset($analysis['status'])) {
$analysis['status'] = 'complete';
}
$final_output = [
"status" => $analysis['status'],
"content" => $analysis['content'],
"moderationVerdict" => $analysis['moderationVerdict']
];
// Кодируем в JSON
$json_output = json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// Проверка на ошибки кодирования
if ($json_output === false) {
$error = json_last_error_msg();
logMessage("ERROR: Ошибка кодирования JSON: $error");
die(json_encode(["status" => "error", "message" => "Ошибка формирования ответа"]));
}
// Логируем итоговый JSON
logMessage("DEBUG: Итоговый JSON для CRM: " . $json_output);
echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// Логируем завершение
logMessage("Обработка завершена. Ответ успешно отправлен в CRM");
exit;
}
/* ===================== Функции для работы с CRM и Vector Store ===================== */
/**
* Проверяет, является ли файл судебным решением, по ключевым словам в названии или содержимом.
*
* @param string $filename Название файла.
* @param string $content Извлечённый текст документа.
* @return bool
*/
function isCourtDecision($filename, $content) {
$keywords = ['решение суда', 'решение', 'судебное решение', 'постановление', 'определение', 'приказ'];
foreach ($keywords as $keyword) {
if (stripos($filename, $keyword) !== false || stripos($content, $keyword) !== false) {
return true;
}
}
return false;
}
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();
$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 extractDocumentContent($filePath) {
// 1. Попытка извлечь текст через pdftotext
$text = extractText($filePath);
if (mb_strlen($text, 'UTF-8') >= 200) {
logMessage("pdftotext дал достаточное количество символов.");
return ['mode' => 'pdf', 'content' => $text];
} else {
logMessage("pdftotext дал недостаточное количество символов. Переходим к обработке через изображения.");
// 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)];
}
}
}
/* СТАРАЯ Функция analyzeDocuments теперь использует extractDocumentContent() */
/*function analyzeDocuments($documents, $uploadedFileIds) {
$combinedMessages = [];
foreach ($documents as $doc) {
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
continue;
}
$normalizedPath = normalizeFilename($doc['filepath']);
$messageBlock = "Документ: " . $doc['title'] . "\n";
$extraction = extractDocumentContent($doc['filepath']);
if ($extraction['mode'] === 'pdf') {
logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Передаём PDF файл.");
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
$messageBlock .= "Тип: PDF (извлечение текста успешно, PDF передан для анализа).\nFileID: " . $fileId . "\n";
} elseif ($extraction['mode'] === 'ocr') {
logMessage("DEBUG: Извлечение текста посредством OCR для " . $doc['filepath']);
$messageBlock .= "OCR извлёк текст:\n" . $extraction['content'] . "\n";
} elseif ($extraction['mode'] === 'nsfw') {
logMessage("DEBUG: Файл " . $doc['filepath'] . " не прошёл NSFW проверку.");
$messageBlock .= "Ошибка: Файл не прошёл цензуру. Файл: " . $normalizedPath . "\n";
} else {
$messageBlock .= "Ошибка: Не удалось извлечь текст. Файл: " . $normalizedPath . "\n";
}
$combinedMessages[] = $messageBlock;
}
return implode("\n-----------------\n", $combinedMessages);
}
*/
//НОВАЯ analyzeDocuments
function analyzeDocuments($documents, $uploadedFileIds) {
$combinedMessages = [];
foreach ($documents as $doc) {
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
continue;
}
$normalizedPath = normalizeFilename($doc['filepath']);
$messageBlock = "Документ: " . $doc['title'] . "\n";
$extraction = extractDocumentContent($doc['filepath']);
// Если документ содержит ключевые слова, указывающие на судебное решение
if (isCourtDecision($doc['title'], $extraction['content'])) {
logMessage("📌 Найдено судебное решение: " . $doc['title']);
// Сохраняем судебное решение в отдельный индекс
saveCourtDecisionToElastic($GLOBALS['caseId'], $doc['title'], $extraction['content']);
$messageBlock .= "Тип: Судебное решение (сохранено отдельно).\n";
} else {
if ($extraction['mode'] === 'pdf') {
logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Передаём PDF файл.");
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
$messageBlock .= "Тип: PDF (извлечение текста успешно, PDF передан для анализа).\nFileID: " . $fileId . "\n";
} elseif ($extraction['mode'] === 'ocr') {
logMessage("DEBUG: Извлечение текста посредством OCR для " . $doc['filepath']);
$messageBlock .= "OCR извлёк текст:\n" . $extraction['content'] . "\n";
} elseif ($extraction['mode'] === 'nsfw') {
logMessage("DEBUG: Файл " . $doc['filepath'] . " не прошёл NSFW проверку.");
$messageBlock .= "Ошибка: Файл не прошёл цензуру. Файл: " . $normalizedPath . "\n";
} else {
$messageBlock .= "Ошибка: Не удалось извлечь текст. Файл: " . $normalizedPath . "\n";
}
}
$combinedMessages[] = $messageBlock;
}
return implode("\n-----------------\n", $combinedMessages);
}
function createThread() {
$curl = curl_init();
logMessage("Создание треда для анализа документа.");
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 ?: "не удалось создать") . ". Ответ: " . $response);
return $threadId;
}
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);
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));
if (mb_strlen($cleanText, 'UTF-8') < 200) {
logMessage("Извлечённый текст для $filePath слишком короткий или не содержит полезной информации. Запуск OCR.");
return '';
}
$snippet = mb_substr($cleanText, 0, 500, 'UTF-8');
logMessage("Извлечённый текст для $filePath (первые 500 символов): " . $snippet);
return $cleanText;
}
// Распознавание через OCR
function doOCR($filePath) {
logMessage("Запуск OCR для файла: $filePath");
$outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt';
// Изменённая команда: добавлены --psm 6 и --oem 1
$command = "tesseract " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile) . " -l rus --psm 6 --oem 1";
logMessage("Выполняем команду OCR: " . $command);
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;
}
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();
logMessage("Отправляем запрос на описание изображения. Payload: " . json_encode($data));
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);
logMessage("Ответ Vision (описание): HTTP $httpCode - " . $response);
if ($curlError) {
logMessage("Ошибка cURL в описании изображения: " . $curlError);
return '';
}
$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 '';
}
}
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);
logMessage("Ответ NSFW: HTTP $httpCode - " . $response);
if ($curlError) {
logMessage("Ошибка cURL при проверке NSFW: " . $curlError);
return null;
}
$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;
}
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);
}
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');
}
function getKnowledgeBaseContext($filePath) {
return "Статическая информация: нормы и законы РФ, судебные прецеденты...";
}
/**
* Функция analyzeDocumentWithAssistantStream отправляет запрос к ассистенту с параметром "stream": true,
* накапливает потоковый ответ и парсит его для получения итогового сообщения.
*/
function analyzeDocumentWithAssistantStream($threadId, $assistantId, $fileId, $content) {
logMessage("Анализ документа через ассистента (stream): thread_id=$threadId, fileId=$fileId");
$userMessage = "🔹 Отвечай по шаблону.\n\n" .
"Если в запросе присутствуют файлы (например, PDF или изображения), извлеки из них текст для анализа, даже если содержимое уже передано отдельно.\n\n" .
"**Задача**: \n" .
"🔹 Отвечай по шаблону. **Задача**:
Проанализируй загруженные документы, выполнив следующие действия:
1⃣ **Список файлов и проверка соответствия названий**
- Перечисли все загруженные файлы.
- Внимательно анализируй изображения.
- Если изображение содержит текст, сначала **извлеки текст** и **проанализируй его содержание**.
- Укажи, если текст плохо читается или частично распознан.
- Если изображение не содержит текста, опиши, что на нем изображено.
**Особое внимание NSFW контенту в тексте и изображениях**
- Обрати внимание на пометку `nsfw_alert`. Если она установлена в `true`, это значит, что файл содержит потенциально неприемлемый контент. Изучи его более внимательно. Отдельно укажи в отчете, что файл может содержать ТРЭШ-контент и требуется ручная проверка.
2⃣ **Краткий анализ спора**
- Определи **истца** (потребителя)(Указать **Истец:** Название или ФИО) и **ответчика** (компанию или ИП, на которую подана жалоба)(Указать **Ответчик:** название или ФИО).
- Опиши **суть спора** (что произошло и какая проблема заявлена).
- Укажи **основные аргументы сторон** (что заявляет потребитель и какие возражения возможны у компании).
3⃣ **Проверка на цензуру**
- Проверь документы на наличие **ненормативной лексики** и **нецензурных изображений**.
- Анализируй изображения на наличие нецензурного контента, запрещенной символики, сцен насилия и других нарушений. Если найдены такие элементы, укажи это в отчете.
- Если обнаружены изображения, укажи что именно и требуется ли их ручная проверка.
4⃣ **Выдача итогового вердикта**
- Если всё соответствует, укажи (строго): \"Вердикт: Прошло модерацию.\"
- Если имеются несоответствия или потенциальные проблемы, укажи, что требуется ручная проверка.
5⃣ **Характер спора**
- Дай краткую характеристику дела (например, \”Характер спора: некачественный товар\", \" Характер спора: товар не привезли\", \" Характер спора: возврат денег\", \" Характер спора: некачественная услуга\" и т. д.).
6. **Укажи вероятность разрешения спора в пользe потребителя в %** Отвечай в формате «Вероятность положительного решения спора: _____»
7. **Чего не хватает/запросить у Ответчика**
Укажи, каких документов не хватает для проведения полного анализа и проведения процедуры медиации. Что необходимо получить дополнительно от контрагента или третьих лиц. Отвечай в формате «Запросить: ____»
8. **Чего не хватает/запросить у Истца**
Укажи, каких документов не хватает для составления более полного отчета и определения вариантов решения спора в пользу потребителя. Что необходимо получить дополнительно от заявителя или третьих лиц. Отвечай в формате «Запросить: ____»
📌 **Важно**:
Отчет должен быть структурированным, четким и лаконичным. Укажи, какими нормами права РФ будет регулироваться рассмотрение данного спора. Если требуется ручная проверка, укажи приоритетные файлы для проверки." .
"Файлы: " . $fileId . "\n" .
"Содержимое для анализа:\n" . $content;
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]
]
],
"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);
// Парсинг ответа (ожидается SSE-формат)
$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;
}
?>