Files
crm.clientright.ru/iacode8.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

982 lines
46 KiB
PHP
Raw Permalink 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
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
ini_set('log_errors', 1);
ini_set('error_log', 'logs/php_errors.log');
// Подключаем модули
require_once 'aiassist/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/scriptass13112.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));
}
$GLOBALS['caseId'] = $id;
// Извлекаем документы из 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();
$threadId = createNewThread();
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, f.foldername AS folder_name, f.folderid AS folder_id
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_notescf ncf ON ncf.notesid = r.notesid
LEFT JOIN vtiger_seattachmentsrel r2 ON r2.crmid = r.notesid
LEFT JOIN vtiger_attachments a ON a.attachmentsid = r2.attachmentsid
LEFT JOIN vtiger_attachmentsfolder f ON f.folderid = n.folderid
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";
$messageBlock = "Документ: " . $doc['title'] . "папка" . $doc['folder_name'] ."\n";
$extraction = extractDocumentContent($doc['filepath']);
// Если документ содержит ключевые слова, указывающие на судебное решение
/*if (isCourtDecision($doc['title'], $extraction['content'])) {
logMessage("📌 Найдено судебное решение: " . $doc['title']);
Сохраняем судебное решение в отдельный индекс
saveCourtDecisionToElastic($GLOBALS['caseId'], $doc['title'], $extraction['content']);
$messageBlock .= "Тип: Судебное решение (сохранено отдельно).\n";
*/
// Проверяем, лежит ли документ в папке "судебные решения" или определён ли он как судебный акт
if (isset($doc['folder_name']) && mb_stripos($doc['folder_name'], 'судебные решения') !== false) {
logMessage("📌 Найдено судебное решение в папке '{$doc['folder_name']}': " . $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 createNewThread() {
$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);
curl_close($curl);
if (isset($decoded['id'])) {
logMessage("🔄 Создан новый thread_id: " . $decoded['id']);
return $decoded['id'];
} else {
logMessage("❌ Ошибка создания нового треда: " . json_encode($decoded));
return null;
}
}
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 = "🔹 Отвечай по шаблону:
Если в запросе присутствуют файлы (например, PDF или изображения), извлеки из них текст для анализа, даже если содержимое уже передано отдельно.
Задача:
Проанализируй загруженные документы, выполнив следующие действия:
1⃣ Список файлов и проверка соответствия названий
Перечисли все загруженные файлы, без указания технических идентификаторов (например, строк вида «file-…») и служебных слов (например, «папкаСуд») в названии.
Сравни название файлов с их содержимым. Если имеются расхождения (например, название указывает на договор, а текст не извлечён), укажи, что имеется в виду.
Если файл содержит изображение, сначала извлеки текст (если он присутствует) и проанализируй его содержание; если текст отсутствует, опиши, что изображено.
Особое внимание удели NSFW-контенту в тексте и изображениях:
Если присутствует пометка nsfw_alert со значением true, укажи, что файл может содержать неприемлемый контент и требует ручной проверки.
2⃣ Краткий анализ спора
Определи истца (потребителя) (укажи название или ФИО) и ответчика (компанию или ИП, на которую подана жалоба) (укажи название или ФИО).
Дополнительно: Проверь, совпадает ли лицо, отправившее сообщение (истец) с лицом, которое заключило договор (согласно содержимому файлов). Если лица не совпадают, добавь, что требуется ручная проверка и уточнение данной информации.
Опиши суть спора (что произошло и какая проблема заявлена).
Укажи основные аргументы сторон (что заявляет потребитель и какие возражения могут быть у компании).
3⃣ Проверка на цензуру
Проверь документы на наличие ненормативной лексики и нецензурных изображений.
Анализируй изображения на предмет нецензурного контента, запрещенной символики, сцен насилия и других нарушений. Если найдены такие элементы, укажи это в отчете, а также отметь, требуется ли ручная проверка.
4⃣ Выдача итогового вердикта
Если всё соответствует требованиям, укажи строго:
«Вердикт: Прошло модерацию.»
Важно: Если у файла не извлечён текст (то есть содержание недоступно для анализа) или выявлены несоответствия (например, отсутствие сопоставимости между названием и содержимым, либо расхождение между лицом-отправителем и лицом, заключившим договор), модерация не должна считаться пройденной необходимо отметить, что требуется ручная проверка.
5⃣ Характер спора
Дай краткую характеристику дела (например: «Характер спора: некачественная услуга», «Характер спора: невыполнение условий договора», «Характер спора: возврат денег» и т. д.).
6⃣ Вероятность положительного решения спора
Укажи вероятность положительного решения спора в пользу потребителя в формате:
«Вероятность положительного решения спора: _____ %».
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;
}
?>