721 lines
31 KiB
PHP
721 lines
31 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
// Настройки 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/v1/chat/completions'; // для описания изображений
|
|||
|
|
// Для NSFW-модерации используем корректный URL:
|
|||
|
|
const NSFW_MODERATION_API = 'https://api.proxyapi.ru/v1/moderation/nsfw';
|
|||
|
|
|
|||
|
|
const LOG_FILE = 'logs/scriptDS3.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') {
|
|||
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|||
|
|
$id = $input['id'] ?? null;
|
|||
|
|
if (!$id) {
|
|||
|
|
logMessage("Ошибка: отсутствует ID документа");
|
|||
|
|
die("Ошибка: отсутствует ID документа");
|
|||
|
|
}
|
|||
|
|
logMessage("Начало обработки документа с ID: $id");
|
|||
|
|
|
|||
|
|
$documents = fetchDocumentData($pdo, $id);
|
|||
|
|
if (empty($documents)) {
|
|||
|
|
logMessage("Документы не найдены для ID: $id");
|
|||
|
|
die("Документы не найдены для ID: $id");
|
|||
|
|
}
|
|||
|
|
logMessage("Документы получены из БД: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
|
|||
|
|
|
|||
|
|
$filePaths = array_column($documents, 'filepath');
|
|||
|
|
$uploadResult = createVectorStoreAndUploadFiles($filePaths);
|
|||
|
|
if (!$uploadResult) {
|
|||
|
|
logMessage("Ошибка создания Vector Store или загрузки файлов");
|
|||
|
|
die("Ошибка создания Vector Store или загрузки файлов");
|
|||
|
|
}
|
|||
|
|
$vectorStoreId = $uploadResult['vectorStoreId'];
|
|||
|
|
$uploadedFileIds = $uploadResult['fileIds'];
|
|||
|
|
|
|||
|
|
if (!updateAssistantWithVectorStore($vectorStoreId)) {
|
|||
|
|
logMessage("Ошибка обновления ассистента с Vector Store");
|
|||
|
|
die("Ошибка обновления ассистента");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Собираем объединённое сообщение для всех документов
|
|||
|
|
$combinedContent = analyzeDocuments($documents, $uploadedFileIds);
|
|||
|
|
if (empty($combinedContent)) {
|
|||
|
|
logMessage("Ошибка: анализ документов не вернул результатов");
|
|||
|
|
die("Ошибка: анализ документов не вернул результатов");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем один тред (реальный вызов API)
|
|||
|
|
$threadId = createThread();
|
|||
|
|
if (!$threadId) {
|
|||
|
|
logMessage("Ошибка создания треда");
|
|||
|
|
die("Ошибка создания треда");
|
|||
|
|
}
|
|||
|
|
// Отправляем объединённое сообщение в ассистента (fileId оставляем пустым для совокупного анализа)
|
|||
|
|
$analysis = analyzeDocumentWithAssistant($threadId, ASSISTANT_ID, '', $combinedContent);
|
|||
|
|
if (!$analysis) {
|
|||
|
|
logMessage("Ошибка анализа совокупного запроса");
|
|||
|
|
die("Ошибка анализа совокупного запроса");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Формируем отчёт с объединённым анализом
|
|||
|
|
$report = generateReport([
|
|||
|
|
[
|
|||
|
|
'document' => 'Объединенный анализ',
|
|||
|
|
'status' => 'complete',
|
|||
|
|
'analysis' => $analysis
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
logMessage("Итоговый отчет:\n" . $report);
|
|||
|
|
echo $report;
|
|||
|
|
logMessage("Обработка всех документов завершена.");
|
|||
|
|
} else {
|
|||
|
|
logMessage("Ошибка: запрос должен быть POST");
|
|||
|
|
die("Ошибка: запрос должен быть POST");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ===================== Функции для работы с CRM и Vector Store ===================== */
|
|||
|
|
|
|||
|
|
function fetchDocumentData($pdo, $id) {
|
|||
|
|
logMessage("Получение данных документа из CRM по ID: $id");
|
|||
|
|
$sql = "
|
|||
|
|
SELECT
|
|||
|
|
n.title,
|
|||
|
|
CASE
|
|||
|
|
WHEN a.storedname IS NOT NULL
|
|||
|
|
THEN CONCAT(a.path, a.attachmentsid, '_', a.storedname)
|
|||
|
|
ELSE CONCAT(a.path, a.attachmentsid, '_', a.name)
|
|||
|
|
END AS filepath
|
|||
|
|
FROM
|
|||
|
|
vtiger_senotesrel r
|
|||
|
|
LEFT JOIN
|
|||
|
|
vtiger_notes n ON n.notesid = r.notesid
|
|||
|
|
LEFT JOIN
|
|||
|
|
vtiger_crmentity e ON e.crmid = r.notesid
|
|||
|
|
LEFT JOIN
|
|||
|
|
vtiger_seattachmentsrel r2 ON r2.crmid = r.notesid
|
|||
|
|
LEFT JOIN
|
|||
|
|
vtiger_attachments a ON a.attachmentsid = r2.attachmentsid
|
|||
|
|
WHERE
|
|||
|
|
r.crmid = ?
|
|||
|
|
AND e.deleted = 0
|
|||
|
|
AND (a.type = 'application/pdf' OR a.type = 'application/octet-stream')
|
|||
|
|
";
|
|||
|
|
try {
|
|||
|
|
$stmt = $pdo->prepare($sql);
|
|||
|
|
$stmt->execute([$id]);
|
|||
|
|
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
logMessage("Документы получены из CRM: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return $documents;
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
logMessage("Ошибка при выполнении запроса к CRM: " . $e->getMessage());
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createVectorStoreAndUploadFiles($filePaths) {
|
|||
|
|
logMessage("Создание Vector Store и загрузка файлов...");
|
|||
|
|
$vectorStoreId = createVectorStore();
|
|||
|
|
if (!$vectorStoreId) return null;
|
|||
|
|
$uploadedFiles = [];
|
|||
|
|
foreach ($filePaths as $filePath) {
|
|||
|
|
logMessage("Загрузка файла: $filePath");
|
|||
|
|
if (!file_exists($filePath)) {
|
|||
|
|
logMessage("Ошибка: Файл не существует: $filePath");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$fileId = uploadFileToOpenAI($filePath);
|
|||
|
|
if (!$fileId) {
|
|||
|
|
logMessage("Ошибка загрузки файла: $filePath");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (!addFileToVectorStore($vectorStoreId, $fileId)) {
|
|||
|
|
logMessage("Ошибка добавления файла в Vector Store: $filePath");
|
|||
|
|
} else {
|
|||
|
|
logMessage("Файл успешно добавлен в Vector Store: $filePath");
|
|||
|
|
$uploadedFiles[$filePath] = $fileId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ['vectorStoreId' => $vectorStoreId, 'fileIds' => $uploadedFiles];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function createVectorStore() {
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_VECTOR_STORES_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => json_encode(['name' => 'Vector Store']),
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при создании Vector Store: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ OpenAI (создание Vector Store): HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
|||
|
|
logMessage("Ошибка при создании Vector Store: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return $decoded['id'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function uploadFileToOpenAI($filePath) {
|
|||
|
|
logMessage("Загрузка файла в OpenAI: $filePath");
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_FILES_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => [
|
|||
|
|
'file' => new CURLFile($filePath),
|
|||
|
|
'purpose' => 'assistants'
|
|||
|
|
],
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при загрузке файла: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ OpenAI (загрузка файла): HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
|||
|
|
logMessage("Ошибка при загрузке файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return $decoded['id'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addFileToVectorStore($vectorStoreId, $fileId) {
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_VECTOR_STORES_API . "/$vectorStoreId/files",
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => json_encode(['file_id' => $fileId]),
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при добавлении файла в Vector Store: " . $curlError);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ OpenAI (добавление файла): HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
|||
|
|
logMessage("Ошибка добавления файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateAssistantWithVectorStore($vectorStoreId) {
|
|||
|
|
$data = [
|
|||
|
|
'tool_resources' => [
|
|||
|
|
'file_search' => [
|
|||
|
|
'vector_store_ids' => [$vectorStoreId]
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
];
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_ASSISTANT_API . "/" . ASSISTANT_ID,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_CUSTOMREQUEST => 'POST',
|
|||
|
|
CURLOPT_POSTFIELDS => json_encode($data),
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка обновления ассистента: " . $curlError);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ OpenAI (обновление ассистента): HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
|||
|
|
logMessage("Ошибка обновления ассистента: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ===================== Логика анализа документов ===================== */
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция analyzeDocuments собирает информацию по всем документам и возвращает объединённый текст.
|
|||
|
|
*/
|
|||
|
|
function analyzeDocuments($documents, $uploadedFileIds) {
|
|||
|
|
$results = [];
|
|||
|
|
foreach ($documents as $doc) {
|
|||
|
|
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
|
|||
|
|
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$finalContent = "";
|
|||
|
|
// 1. Попытка извлечения текста напрямую
|
|||
|
|
$extractedText = extractText($doc['filepath']);
|
|||
|
|
if (!empty($extractedText)) {
|
|||
|
|
logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Отправляем PDF файл в ассистента.");
|
|||
|
|
// Если текст успешно извлечён, не передаём его в finalContent – будем использовать file_id
|
|||
|
|
} else {
|
|||
|
|
logMessage("DEBUG: Извлечение текста не удалось для " . $doc['filepath'] . ". Пытаемся обработать через изображения.");
|
|||
|
|
$extension = strtolower(pathinfo($doc['filepath'], PATHINFO_EXTENSION));
|
|||
|
|
if ($extension === 'pdf') {
|
|||
|
|
$outputDir = sys_get_temp_dir() . '/pdf_images_' . md5($doc['filepath']);
|
|||
|
|
$images = convertPdfToImages($doc['filepath'], $outputDir);
|
|||
|
|
if (empty($images)) {
|
|||
|
|
logMessage("Ошибка: Не удалось конвертировать PDF в изображения для " . $doc['filepath']);
|
|||
|
|
$finalContent = "Не удалось извлечь текст: конвертация в изображение не выполнена.";
|
|||
|
|
} else {
|
|||
|
|
$allSafe = true;
|
|||
|
|
// Проверяем каждое изображение на цензуру
|
|||
|
|
foreach ($images as $image) {
|
|||
|
|
logMessage("DEBUG: Проверяем изображение: " . $image);
|
|||
|
|
$classification = classifyImage($image);
|
|||
|
|
$absImagePath = realpath($image);
|
|||
|
|
$unsafeProbability = 0;
|
|||
|
|
if (isset($classification[$absImagePath])) {
|
|||
|
|
$unsafeProbability = $classification[$absImagePath]['unsafe'] ?? 0;
|
|||
|
|
}
|
|||
|
|
logMessage("DEBUG: Для изображения '$absImagePath' получено unsafeProbability = " . $unsafeProbability);
|
|||
|
|
if ($unsafeProbability > 0.8) {
|
|||
|
|
logMessage("DEBUG: Изображение '$absImagePath' не прошло цензуру (unsafeProbability = $unsafeProbability).");
|
|||
|
|
$allSafe = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!$allSafe) {
|
|||
|
|
$finalContent = "Файл не прошёл цензуру.";
|
|||
|
|
} else {
|
|||
|
|
// Если все изображения безопасны, запускаем OCR для каждого и объединяем результаты
|
|||
|
|
$combinedOcrText = "";
|
|||
|
|
foreach ($images as $image) {
|
|||
|
|
$ocrText = doOCR($image);
|
|||
|
|
if (!empty($ocrText)) {
|
|||
|
|
logMessage("DEBUG: OCR успешно извлёк текст для изображения: " . $image);
|
|||
|
|
$combinedOcrText .= $ocrText . "\n";
|
|||
|
|
} else {
|
|||
|
|
logMessage("DEBUG: OCR не смог извлечь текст для изображения: " . $image);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!empty($combinedOcrText)) {
|
|||
|
|
$finalContent = $combinedOcrText . "\n" . getKnowledgeBaseContext($doc['filepath']);
|
|||
|
|
} else {
|
|||
|
|
$finalContent = "Не удалось извлечь текст посредством OCR.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$finalContent = "Не удалось извлечь текст: не поддерживаемый формат.";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Получаем file_id для привязки (если имеется)
|
|||
|
|
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
|
|||
|
|
logMessage("DEBUG: fileId для " . $doc['filepath'] . " = " . $fileId);
|
|||
|
|
|
|||
|
|
// 3. Создаем тред для анализа
|
|||
|
|
$threadId = createThread();
|
|||
|
|
if (!$threadId) {
|
|||
|
|
logMessage("Ошибка создания треда для " . $doc['filepath']);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. Отправляем итоговое содержимое ассистенту для анализа.
|
|||
|
|
// Если finalContent пустое, значит, текст извлечён успешно и мы передаем PDF через file_id.
|
|||
|
|
logMessage("DEBUG: Вызов analyzeDocumentWithAssistant с параметрами: threadId=$threadId, assistantId=" . ASSISTANT_ID . ", fileId=$fileId, finalContent=" . substr($finalContent, 0, 50));
|
|||
|
|
$analysis = analyzeDocumentWithAssistant($threadId, ASSISTANT_ID, $fileId, $finalContent);
|
|||
|
|
if ($analysis) {
|
|||
|
|
logMessage("Анализ завершен: " . json_encode($analysis, JSON_UNESCAPED_UNICODE));
|
|||
|
|
$results[] = [
|
|||
|
|
'document' => $doc['title'],
|
|||
|
|
'status' => 'Анализ завершен',
|
|||
|
|
'analysis' => $analysis
|
|||
|
|
];
|
|||
|
|
} else {
|
|||
|
|
logMessage("Ошибка анализа " . $doc['filepath']);
|
|||
|
|
$results[] = [
|
|||
|
|
'document' => $doc['title'],
|
|||
|
|
'status' => 'Ошибка анализа',
|
|||
|
|
'message' => 'Не удалось проанализировать документ.'
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return $results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция createThread выполняет реальный вызов к API для создания треда.
|
|||
|
|
*/
|
|||
|
|
function createThread() {
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_THREADS_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
$threadId = $decoded['id'] ?? null;
|
|||
|
|
logMessage("Создан тред с id: " . ($threadId ?: "не удалось создать"));
|
|||
|
|
return $threadId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция extractText пытается извлечь текст с помощью pdftotext.
|
|||
|
|
*/
|
|||
|
|
function extractText($filePath) {
|
|||
|
|
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
|||
|
|
if ($extension !== 'pdf') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$outputFile = tempnam(sys_get_temp_dir(), 'txt_') . '.txt';
|
|||
|
|
$command = "pdftotext " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile);
|
|||
|
|
exec($command, $output, $returnVar);
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("Ошибка pdftotext: " . implode("\n", $output));
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
if (!file_exists($outputFile)) {
|
|||
|
|
logMessage("Файл pdftotext не создан: $filePath");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$text = file_get_contents($outputFile);
|
|||
|
|
if (empty($text)) {
|
|||
|
|
logMessage("DEBUG: pdftotext вернул пустой результат для $filePath");
|
|||
|
|
}
|
|||
|
|
unlink($outputFile);
|
|||
|
|
return $text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция doOCR использует Tesseract для извлечения текста.
|
|||
|
|
*/
|
|||
|
|
function doOCR($filePath) {
|
|||
|
|
logMessage("Запуск OCR для файла: $filePath");
|
|||
|
|
$outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt';
|
|||
|
|
$command = "tesseract " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile) . " -l rus";
|
|||
|
|
exec($command, $output, $returnVar);
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("Ошибка Tesseract: " . implode("\n", $output));
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
if (!file_exists($outputFile . ".txt")) {
|
|||
|
|
logMessage("Файл OCR не создан: $filePath");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$text = file_get_contents($outputFile . ".txt");
|
|||
|
|
if (empty($text)) {
|
|||
|
|
logMessage("DEBUG: Tesseract вернул пустой результат для $filePath");
|
|||
|
|
}
|
|||
|
|
unlink($outputFile . ".txt");
|
|||
|
|
return $text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция describeImageWithVision вызывает API для получения описания изображения.
|
|||
|
|
*/
|
|||
|
|
function describeImageWithVision($filePath) {
|
|||
|
|
logMessage("Запуск описания изображения через Vision для файла: $filePath");
|
|||
|
|
$imageData = base64_encode(file_get_contents($filePath));
|
|||
|
|
$data = [
|
|||
|
|
"model" => "gpt-4-vision-preview",
|
|||
|
|
"messages" => [
|
|||
|
|
[
|
|||
|
|
"role" => "user",
|
|||
|
|
"content" => [
|
|||
|
|
[
|
|||
|
|
"type" => "text",
|
|||
|
|
"text" => "Опиши это изображение подробно. Если это документ, прочитай и опиши его содержимое."
|
|||
|
|
],
|
|||
|
|
[
|
|||
|
|
"type" => "image_url",
|
|||
|
|
"image_url" => [
|
|||
|
|
"url" => "data:image/jpeg;base64,$imageData"
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
],
|
|||
|
|
"max_tokens" => 500
|
|||
|
|
];
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_VISION_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => json_encode($data),
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL в описании изображения: " . $curlError);
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ Vision (описание): HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if (isset($decoded['choices'][0]['message']['content'])) {
|
|||
|
|
$desc = $decoded['choices'][0]['message']['content'];
|
|||
|
|
if (empty($desc)) {
|
|||
|
|
logMessage("DEBUG: Описание изображения пустое для $filePath");
|
|||
|
|
}
|
|||
|
|
return $desc;
|
|||
|
|
} else {
|
|||
|
|
logMessage("Ошибка при получении описания изображения: " . json_encode($decoded));
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция checkNSFWWithVision использует корректный URL для NSFW-модерации.
|
|||
|
|
*/
|
|||
|
|
function checkNSFWWithVision($filePath) {
|
|||
|
|
logMessage("NSFW-проверка через URL " . NSFW_MODERATION_API для файла: $filePath");
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => NSFW_MODERATION_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => [
|
|||
|
|
'file' => new CURLFile($filePath)
|
|||
|
|
],
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при проверке NSFW: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
logMessage("Ответ NSFW: HTTP $httpCode - " . $response);
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || isset($decoded['detail'])) {
|
|||
|
|
logMessage("Ошибка анализа NSFW: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return $decoded['nsfw'] ?? null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция classifyImage использует NudeClassifier через Python.
|
|||
|
|
*/
|
|||
|
|
function classifyImage($imagePath) {
|
|||
|
|
$absolutePath = realpath($imagePath);
|
|||
|
|
if (!$absolutePath) {
|
|||
|
|
logMessage("ERROR: Не удалось получить абсолютный путь для " . $imagePath);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
logMessage("DEBUG: Абсолютный путь для классификации: " . $absolutePath);
|
|||
|
|
$escapedPath = escapeshellarg($absolutePath);
|
|||
|
|
logMessage("DEBUG: Экранированный путь для классификации: " . $escapedPath);
|
|||
|
|
$command = "python3 -c \"import json; from nudenet import NudeClassifier; classifier = NudeClassifier(); print(json.dumps(classifier.classify($escapedPath)))\"";
|
|||
|
|
logMessage("DEBUG: Выполнение команды: " . $command);
|
|||
|
|
$output = shell_exec($command);
|
|||
|
|
logMessage("DEBUG: Вывод команды: " . $output);
|
|||
|
|
if ($output === null) {
|
|||
|
|
logMessage("ERROR: shell_exec вернул null при выполнении NudeClassifier");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
return json_decode(trim($output), true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция convertPdfToImages конвертирует PDF в изображения.
|
|||
|
|
*/
|
|||
|
|
function convertPdfToImages($pdfPath, $outputDir) {
|
|||
|
|
if (!file_exists($pdfPath)) {
|
|||
|
|
logMessage("Файл не существует: $pdfPath");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
if (!file_exists($outputDir)) {
|
|||
|
|
mkdir($outputDir, 0777, true);
|
|||
|
|
logMessage("Создана директория для изображений: $outputDir");
|
|||
|
|
}
|
|||
|
|
$imagePattern = $outputDir . '/page-%03d.jpg';
|
|||
|
|
$command = "LC_ALL=en_US.UTF-8 convert -density 300 " . escapeshellarg($pdfPath) . " -quality 90 " . escapeshellarg($imagePattern);
|
|||
|
|
logMessage("Выполняем команду: " . $command);
|
|||
|
|
exec($command . " 2>&1", $output, $returnVar);
|
|||
|
|
logMessage("DEBUG: Вывод convert: " . implode("\n", $output));
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("Ошибка при конвертации PDF в изображения.");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
return glob($outputDir . '/*.jpg');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Stub-функция для получения контекста из базы знаний.
|
|||
|
|
*/
|
|||
|
|
function getKnowledgeBaseContext($filePath) {
|
|||
|
|
return "Статическая информация: нормы и законы РФ, судебные прецеденты...";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция analyzeDocumentWithAssistant отправляет реальный запрос к GPT.
|
|||
|
|
*
|
|||
|
|
* Здесь используется очистка строки, mb_substr, а данные формируются в ключе "messages".
|
|||
|
|
*/
|
|||
|
|
function analyzeDocumentWithAssistant($threadId, $assistantId, $fileId, $content) {
|
|||
|
|
logMessage("Анализ документа через ассистента: thread_id=$threadId, fileId=$fileId");
|
|||
|
|
|
|||
|
|
if (empty($content)) {
|
|||
|
|
$userMessage = "Проанализируй документ, предоставленный как PDF (file_id: $fileId).";
|
|||
|
|
} else {
|
|||
|
|
$userMessage = "Проанализируй документ. Содержимое для анализа:\n" . $content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Очистка строки от битых символов
|
|||
|
|
$userMessage = mb_convert_encoding($userMessage, 'UTF-8', 'auto');
|
|||
|
|
$userMessage = iconv("UTF-8", "UTF-8//IGNORE//TRANSLIT", $userMessage);
|
|||
|
|
|
|||
|
|
// Формирование payload для вызова ассистента через threads/runs
|
|||
|
|
$payload = [
|
|||
|
|
"assistant_id" => $assistantId,
|
|||
|
|
"thread" => [
|
|||
|
|
// Можно указать идентификатор существующего треда, если API это поддерживает,
|
|||
|
|
// либо оставить сообщения, чтобы создать новый контекст в рамках треда
|
|||
|
|
"messages" => [
|
|||
|
|
["role" => "user", "content" => $userMessage]
|
|||
|
|
]
|
|||
|
|
],
|
|||
|
|
"temperature" => 0.7,
|
|||
|
|
"top_p" => 1.0,
|
|||
|
|
"stream" => false
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => OPENAI_THREADS_API . "/runs", // новый endpoint для ассистента
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Content-Type: application/json',
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY,
|
|||
|
|
'OpenAI-Beta: assistants=v2'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($httpCode !== 200) {
|
|||
|
|
logMessage("Ошибка API: HTTP $httpCode - " . $response);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
// Предполагается, что в ответе содержится объект сообщения ассистента,
|
|||
|
|
// например, в ключе 'message' с полем 'content'
|
|||
|
|
$assistantMessage = $decoded['message']['content'] ?? '';
|
|||
|
|
logMessage("DEBUG: Полученный ответ от ассистента: " . $assistantMessage);
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
"status" => "complete",
|
|||
|
|
"content" => $assistantMessage,
|
|||
|
|
"moderationVerdict" => ""
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Функция generateReport формирует итоговый отчет.
|
|||
|
|
*/
|
|||
|
|
function generateReport($allResults) {
|
|||
|
|
$report = "### Итоговый отчет\n\n";
|
|||
|
|
foreach ($allResults as $result) {
|
|||
|
|
$report .= "**Результат анализа:**\n" . ($result['analysis']['content'] ?? 'Нет данных') . "\n";
|
|||
|
|
$report .= "**Вердикт:** " . ($result['analysis']['moderationVerdict'] ?? 'Не определен') . "\n\n";
|
|||
|
|
}
|
|||
|
|
return $report;
|
|||
|
|
}
|