Files
crm.clientright.ru/iacode.php

554 lines
24 KiB
PHP
Raw Permalink Normal View History

<?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/openai/v1/chat/completions';
const NSFW_MODERATION_API = 'https://api.proxyapi.ru/v1/moderation/nsfw';
const LOG_FILE = 'logs/scriptAIas21.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]);
logMessage("Подключение к БД успешно установлено.");
} 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);
}
/* ===== Новая функция для извлечения контента из PDF ===== */
function extractDocumentContent($filePath) {
// 1. Попытка извлечь текст через pdftotext (простая версия, без дальнейшей обработки)
$extractedText = extractText3($filePath);
if (mb_strlen($extractedText, 'UTF-8') >= 200) {
logMessage("pdftotext дал достаточное количество символов для $filePath.");
// Режим "pdf": достаточный текст передаем идентификатор файла
return ['mode' => 'pdf', 'content' => ''];
} else {
logMessage("pdftotext дал недостаточно символов для $filePath. Переходим к обработке через изображения.");
// 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)];
}
}
}
/* Функция extractText3 простая реализация через pdftotext */
function extractText3($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));
return $cleanText;
}
/* ===================== Основной скрипт ===================== */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
logMessage("Получен POST-запрос.");
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? null;
// Получаем динамический промпт из CRM (если передан)
$dynamicPrompt = $input['prompt'] ?? '';
if (!$id) {
logMessage("Ошибка: отсутствует ID документа");
die("Ошибка: отсутствует ID документа");
}
logMessage("Начало обработки документа с ID: $id");
if (!empty($dynamicPrompt)) {
logMessage("Получен динамический промпт: " . $dynamicPrompt);
}
$documents = fetchDocumentData($pdo, $id);
if (empty($documents)) {
logMessage("Документы не найдены для ID: $id");
die("Документы не найдены для ID: $id");
}
logMessage("Документы получены из CRM: " . 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("Ошибка обновления ассистента");
}
// Формируем объединённый контент для анализа, используя extractDocumentContent для каждого файла
$combinedMessages = [];
foreach ($documents as $doc) {
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
continue;
}
$result = extractDocumentContent($doc['filepath']);
$messageBlock = "Документ: " . $doc['title'] . "\n";
if ($result['mode'] === 'pdf') {
// Если режим pdf, берем file id (то есть оригинальный PDF передается ассистенту)
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
$messageBlock .= "Тип: PDF (достаточно текста, оригинальный файл передан).\nFileID: " . $fileId . "\n";
} elseif ($result['mode'] === 'ocr') {
$messageBlock .= "OCR извлёк текст:\n" . $result['content'] . "\n";
} elseif ($result['mode'] === 'nsfw') {
$messageBlock .= "Ошибка: Файл содержит трешконтент.\n";
} else {
$messageBlock .= "Ошибка: Не удалось извлечь текст.\n";
}
$combinedMessages[] = $messageBlock;
}
$combinedContent = implode("\n-----------------\n", $combinedMessages);
if (!empty($dynamicPrompt)) {
$combinedContent = "Динамический промпт из CRM:\n" . $dynamicPrompt . "\n\n" . $combinedContent;
}
logMessage("Собранный контент для анализа:\n" . $combinedContent);
if (empty($combinedContent)) {
logMessage("Ошибка: анализ документов не вернул результатов");
die("Ошибка: анализ документов не вернул результатов");
}
$fileIdCombined = implode(',', array_values($uploadedFileIds));
logMessage("Объединённый список идентификаторов файлов: " . $fileIdCombined);
$threadId = createThread();
if (!$threadId) {
logMessage("Ошибка создания треда");
die("Ошибка создания треда");
}
$analysis = analyzeDocumentWithAssistantStream($threadId, ASSISTANT_ID, $fileIdCombined, $combinedContent);
if (!$analysis) {
logMessage("Ошибка анализа совокупного запроса");
die("Ошибка анализа совокупного запроса");
}
$report = generateReport([
[
'document' => 'Объединенный анализ',
'status' => 'complete',
'analysis' => $analysis
]
]);
$final_output = [
"status" => "complete",
"content" => $report,
"moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен'
];
logMessage("DEBUG: Извлеченный контент: " . $report);
logMessage("DEBUG: Извлеченный вердикт модерации: " . ($analysis['moderationVerdict'] ?? 'Не определен'));
echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
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);
foreach ($documents as &$doc) {
$doc['filepath'] = normalizeFilename($doc['filepath']);
}
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 analyzeDocumentWithAssistantStream($threadId, $assistantId, $fileId, $content) {
logMessage("Анализ документа через ассистента (stream): thread_id=$threadId, fileId=$fileId");
$userMessage = "Проанализируй документ. Файлы: " . $fileId . "\nСодержимое для анализа:\n" . $content . "\nВыведи краткий сводный отчёт по загруженным файлам и содержимому, используя указанные идентификаторы.";
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]
]
],
"temperature" => 0.7,
"top_p" => 1.0,
"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);
$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;
}
// возвращаем извлеченные данные в СРМ
$report = generateReport([
[
'document' => 'Объединенный анализ',
'status' => 'complete',
'analysis' => $analysis
]
]);
$final_output = [
"status" => "complete",
"content" => $report,
"moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен'
];
logMessage("DEBUG: Извлеченный контент: " . $report);
logMessage("DEBUG: Извлеченный вердикт модерации: " . ($analysis['moderationVerdict'] ?? 'Не определен'));
echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);