This commit is contained in:
F4ilji
2026-03-20 21:47:02 +05:00
parent ae7b42acbe
commit a3948c872e
1926 changed files with 2630 additions and 270 deletions
@@ -0,0 +1,93 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CallAiServiceForFileSelectionTask
{
/**
* Отправляет фрагменты файлов в AI для определения основного файла
*
* @param array $fragments Массив фрагментов [['filename' => '...', 'text' => '...'], ...]
* @return array|null Ответ AI с названием основного файла
*/
public function run(array $fragments): ?array
{
Log::info('[CallAiServiceForFileSelectionTask] Начало LLM запроса', [
'fragments_count' => count($fragments),
]);
$prompt = $this->buildPrompt($fragments);
Log::info('[CallAiServiceForFileSelectionTask] Отправка запроса к AI');
try {
$response = Http::timeout(30) // Таймаут 30 секунд
->withHeaders([
'Authorization' => 'Bearer ' . env('QWEN_API_KEY'),
'Content-Type' => 'application/json',
])->post('https://routerai.ru/api/v1/chat/completions', [
'model' => 'qwen/qwen3.5-flash-02-23',
'messages' => [
['role' => 'user', 'content' => $prompt],
],
'response_format' => ['type' => 'json_object'],
'temperature' => 0,
'max_tokens' => 500,
]);
Log::info('[CallAiServiceForFileSelectionTask] Ответ от AI', [
'status' => $response->status(),
'successful' => $response->successful(),
]);
if ($response->successful()) {
$result = $response->json();
$llmResponse = json_decode($result['choices'][0]['message']['content'], true);
Log::info('[CallAiServiceForFileSelectionTask] Распознан ответ', [
'main_file' => $llmResponse['main_file'] ?? null,
]);
return $llmResponse;
} else {
Log::error('[CallAiServiceForFileSelectionTask] Ошибка AI запроса', [
'status' => $response->status(),
'body' => $response->body(),
]);
}
} catch (\Exception $e) {
Log::error('[CallAiServiceForFileSelectionTask] Исключение при запросе к AI', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
return null;
}
/**
* Формирует промпт для AI
*/
private function buildPrompt(array $fragments): string
{
$filesInfo = "";
foreach ($fragments as $index => $fragment) {
$filesInfo .= "Файл #{$index}: {$fragment['filename']}\n";
$filesInfo .= "Текст: {$fragment['text']}\n\n";
}
return "Ты — ассистент для определения основного файла с новостью. " .
"Тебе предоставлены фрагменты текста из нескольких DOCX файлов.\n\n" .
"СПИСОК ФАЙЛОВ:\n{$filesInfo}\n" .
"ЗАДАЧА: Определи, какой файл содержит основной текст новости (не приложение, не документ, а именно новость/статью).\n\n" .
"ВЕРНИ СТРОГО JSON:\n" .
"{\n" .
' "main_file": "точное название файла с расширением .docx",' . "\n" .
' "reason": "краткое объяснение выбора"' . "\n" .
"}\n\n" .
"Если не можешь определить — верни первый файл из списка.";
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
class CallAiServiceTask
{
/**
* Отправляет текст в AI сервис для извлечения структурированных данных
*
* @param string $text Исходный текст новости
* @param Collection $categories Коллекция категорий для выбора
* @return array|null Распознанные данные (title, body, authors, tags, category_id)
*/
public function run(string $text, Collection $categories): ?array
{
$categoryList = collect($categories)
->map(fn($cat) => "{$cat->id}: {$cat->title}")
->implode("\n");
$systemPrompt = $this->buildSystemPrompt($categoryList);
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . env('QWEN_API_KEY'),
'Content-Type' => 'application/json',
])->post('https://routerai.ru/api/v1/chat/completions', [
'model' => 'qwen/qwen3.5-flash-02-23',
'reasoning' => ['enabled' => false],
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $text],
],
'response_format' => ['type' => 'json_object'],
'temperature' => 0,
'max_tokens' => 2000,
]);
if ($response->successful()) {
$result = $response->json();
return json_decode($result['choices'][0]['message']['content'], true);
}
return null;
}
/**
* Формирует системный промпт для AI
*/
private function buildSystemPrompt(string $categoryList): string
{
return "Ты — строгий агент по извлечению данных. Твоя задача — преобразовать входящий текст новости в JSON.\n\n"
. "СПИСОК КАТЕГОРИЙ (ID: Название):\n"
. $categoryList
. "\n\nПРАВИЛА:\n"
. "1. Поля \"title\" и \"body\" копируй ТОЧЬ-В-ТОЧЬ из источника. Запрещено исправлять ошибки, менять пунктуацию или добавлять Markdown-разметку. Только чистый текст.\n"
. "2. В конце новости извлеки имя автора(-ов) и их должности, это должен быть массив \"authors\". Если оно не указано явно — верни null.\n"
. "3. \"tags\" извлекай ТОЛЬКО слова, начинающиеся с символа решетки (#). Если таких слов в тексте нет — верни null. Не создавай свои теги.\n"
. "4. В поле \"category_id\" запиши идентификатор из предоставленного списка выше.\n"
. "5. Полностью игнорируй комментарии пользователей и рекламный мусор, не включая их в \"body\".\n\n"
. "ОТВЕТ ДОЛЖЕН БЫТЬ СТРОГО В JSON:\n"
. "{\n"
. ' "title": "string",' . "\n"
. ' "body": "string",' . "\n"
. ' "authors": ["ФИО и должность"] или null,' . "\n"
. ' "tags": ["#тег1"] или null,' . "\n"
. ' "category_id": number/string' . "\n"
. "}";
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
class ConvertDocToDocxTask
{
/**
* Конвертирует .doc файл в .docx с помощью LibreOffice
*
* @param UploadedFile $file Исходный .doc файл
* @return string|null Путь к конвертированному .docx файлу или null при ошибке
*/
public function run(UploadedFile $file): ?string
{
Log::info('[ConvertDocToDocxTask] Начало конвертации', [
'file' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'extension' => $file->getClientOriginalExtension(),
]);
// Проверяем расширение
if (strtolower($file->getClientOriginalExtension()) !== 'doc') {
Log::warning('[ConvertDocToDocxTask] Неверное расширение', [
'extension' => $file->getClientOriginalExtension(),
]);
return null;
}
// Сохраняем исходный файл во временную директорию
$originalPath = $file->storeAs('temp/conversion', 'original_' . time() . '_' . Str::random(10) . '.doc', 'local');
$absoluteOriginalPath = Storage::disk('local')->path($originalPath);
Log::info('[ConvertDocToDocxTask] Файл сохранен', [
'original_path' => $originalPath,
'absolute_path' => $absoluteOriginalPath,
'file_exists' => file_exists($absoluteOriginalPath),
]);
// Проверяем существование файла
if (!file_exists($absoluteOriginalPath)) {
Log::error('[ConvertDocToDocxTask] Файл не сохранен', [
'path' => $absoluteOriginalPath,
]);
return null;
}
// Директория для вывода
$outputDir = dirname($absoluteOriginalPath);
// Конвертируем через LibreOffice
$command = sprintf(
'libreoffice --headless --convert-to docx --outdir %s %s 2>&1',
escapeshellarg($outputDir),
escapeshellarg($absoluteOriginalPath)
);
Log::info('[ConvertDocToDocxTask] Выполнение команды', [
'command' => $command,
]);
$output = shell_exec($command);
Log::info('[ConvertDocToDocxTask] Результат конвертации', [
'output' => $output,
]);
// Ищем конвертированный файл
$docxPath = preg_replace('/\.doc$/i', '.docx', $absoluteOriginalPath);
Log::info('[ConvertDocToDocxTask] Проверка результата', [
'expected_path' => $docxPath,
'file_exists' => file_exists($docxPath),
]);
if (!file_exists($docxPath)) {
// Логируем ошибку
Log::error('[ConvertDocToDocxTask] Конвертация не удалась', [
'file' => $file->getClientOriginalName(),
'output' => $output,
'expected_path' => $docxPath,
]);
// Удаляем исходный файл
Storage::disk('local')->delete($originalPath);
return null;
}
// Удаляем исходный .doc файл
Storage::disk('local')->delete($originalPath);
// Возвращаем относительный путь к .docx файлу
$relativeDocxPath = preg_replace('/\.doc$/i', '.docx', $originalPath);
Log::info('[ConvertDocToDocxTask] Конвертация успешна', [
'result_path' => $relativeDocxPath,
]);
return $relativeDocxPath;
}
}
@@ -0,0 +1,129 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\PostDataProcessor;
use Illuminate\Support\Str;
class CreatePostFromAiDataTask
{
/**
* Создаёт пост из данных, полученных от AI
*
* @param array $newsData Данные от AI (title, body, authors, tags, category_id)
* @param string|null $documentPath Путь к основному документу
* @param array $mediaPaths Пути к медиафайлам
* @param array $attachedFiles Прикреплённые файлы (для блока files)
*/
public function run(array $newsData, ?string $documentPath, array $mediaPaths, array $attachedFiles): Post
{
// Формируем контент в формате редактора
$content = $this->buildContent($newsData, $attachedFiles);
// Обрабатываем изображения
['preview' => $previewPath, 'images' => $imagesPaths] = $this->processImages($mediaPaths);
// Подготавливаем данные для создания поста
$postData = $this->preparePostData($newsData, $content, $previewPath, $imagesPaths);
// Обрабатываем через PostDataProcessor (как в админке)
$processedData = (new PostDataProcessor())->processCreate($postData);
// Создаём пост
$post = Post::create($processedData);
// Сохраняем теги
$this->syncTags($post, $newsData);
return $post;
}
/**
* Формирует контент в формате редактора
*/
private function buildContent(array $newsData, array $attachedFiles): array
{
$content = [
[
'type' => 'paragraph',
'data' => [
'seo_active' => true,
'content' => $newsData['body'] ?? '',
],
],
];
if (!empty($attachedFiles)) {
$content[] = [
'type' => 'files',
'data' => [
'file' => $attachedFiles,
],
];
}
return $content;
}
/**
* Обрабатывает изображения: все изображения идут в gallery, первое — preview
*/
private function processImages(array $mediaPaths): array
{
$imagesPaths = [];
$imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'svg', 'tiff'];
foreach ($mediaPaths as $path) {
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($extension, $imageExtensions)) {
$imagesPaths[] = $path;
}
}
return [
'preview' => !empty($imagesPaths) ? reset($imagesPaths) : null,
'images' => $imagesPaths,
];
}
/**
* Подготавливает данные для создания поста
*/
private function preparePostData(array $newsData, array $content, ?string $previewPath, array $imagesPaths): array
{
return [
'title' => $newsData['title'] ?? 'Без названия',
'slug' => Str::slug($newsData['title'] ?? '') . '-' . time(),
'content' => $content,
'authors' => $newsData['authors'] ?? [],
'category_id' => $newsData['category_id'] ?? null,
'status' => PostStatus::VERIFICATION,
'user_id' => auth()->id(),
'preview' => $previewPath,
'images' => !empty($imagesPaths) ? $imagesPaths : null,
'publish_setting' => [
'publish_after' => false,
'publish_at' => null,
],
'publication' => [],
];
}
/**
* Сохраняет теги поста
*/
private function syncTags(Post $post, array $newsData): void
{
if (!empty($newsData['tags'])) {
$tagNames = array_map(
fn($tag) => str_replace('#', '', $tag),
$newsData['tags']
);
$post->syncTags($tagNames);
}
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class ExtractTextFragmentTask
{
/**
* Извлекает фрагмент текста (первые N символов) из DOCX или DOC файла
*/
public function run(UploadedFile $file, int $maxLength = 300): string
{
Log::info('[ExtractTextFragmentTask] Начало извлечения фрагмента', [
'file' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
]);
$extension = strtolower($file->getClientOriginalExtension());
// Для DOCX — парсим напрямую
if ($extension === 'docx') {
return $this->extractFromDocx($file, $maxLength);
}
// Для DOC — конвертируем и парсим
if ($extension === 'doc') {
return $this->extractFromDoc($file, $maxLength);
}
Log::warning('[ExtractTextFragmentTask] Неизвестный формат', [
'extension' => $extension,
]);
return '(неподдерживаемый формат)';
}
/**
* Извлекает фрагмент из DOCX
*/
private function extractFromDocx(UploadedFile $file, int $maxLength): string
{
// Сохраняем во временную директорию
$tempPath = $file->storeAs('temp', 'temp_fragment_' . time() . '.docx', 'local');
$absoluteTempPath = Storage::disk('local')->path($tempPath);
// Парсим файл через ParseDocxTask
$fullText = app(ParseDocxTask::class)->run($absoluteTempPath);
// Удаляем временный файл
Storage::disk('local')->delete($tempPath);
return $this->truncateText($fullText, $maxLength);
}
/**
* Извлекает фрагмент из DOC (через конвертацию)
*/
private function extractFromDoc(UploadedFile $file, int $maxLength): string
{
// Используем ExtractTextFromDocumentTask для конвертации и извлечения
$extractor = app(ExtractTextFromDocumentTask::class);
$fullText = $extractor->run($file);
return $this->truncateText($fullText, $maxLength);
}
/**
* Обрезает текст до нужной длины
*/
private function truncateText(?string $text, int $maxLength): string
{
if (empty($text)) {
return '(пустой файл)';
}
if (strlen($text) > $maxLength) {
return substr($text, 0, $maxLength) . '...';
}
return $text;
}
}
@@ -0,0 +1,116 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use App\Containers\Dashboard\Tasks\ParseDocxTask;
use App\Containers\Dashboard\Tasks\ConvertDocToDocxTask;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class ExtractTextFromDocumentTask
{
public function __construct(
private readonly ParseDocxTask $parseDocxTask,
private readonly ConvertDocToDocxTask $convertDocToDocxTask,
) {}
/**
* Извлекает текст из DOCX или DOC файла
* Автоматически определяет формат и конвертирует при необходимости
*/
public function run(UploadedFile $file): ?string
{
$extension = strtolower($file->getClientOriginalExtension());
Log::info('[ExtractTextFromDocumentTask] Начало извлечения текста', [
'file' => $file->getClientOriginalName(),
'extension' => $extension,
'size' => $file->getSize(),
]);
// Если DOCX — парсим напрямую
if ($extension === 'docx') {
Log::info('[ExtractTextFromDocumentTask] Обработка DOCX файла');
return $this->extractFromDocx($file);
}
// Если DOC — конвертируем в DOCX, затем парсим
if ($extension === 'doc') {
Log::info('[ExtractTextFromDocumentTask] Обработка DOC файла (требуется конвертация)');
return $this->extractFromDoc($file);
}
Log::warning('[ExtractTextFromDocumentTask] Неизвестный формат файла', [
'extension' => $extension,
]);
return null;
}
/**
* Извлекает текст из DOCX файла
*/
private function extractFromDocx(UploadedFile $file): ?string
{
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Начало');
// Сохраняем во временную директорию
$tempPath = $file->storeAs('temp', 'temp_doc_' . time() . '.docx', 'local');
$absoluteTempPath = Storage::disk('local')->path($tempPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Файл сохранен', [
'path' => $tempPath,
'absolute_path' => $absoluteTempPath,
]);
// Парсим файл
$extractedText = $this->parseDocxTask->run($absoluteTempPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Результат парсинга', [
'text_length' => strlen($extractedText ?? ''),
]);
// Удаляем временный файл
Storage::disk('local')->delete($tempPath);
return $extractedText;
}
/**
* Извлекает текст из DOC файла (через конвертацию)
*/
private function extractFromDoc(UploadedFile $file): ?string
{
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Начало конвертации');
// Конвертируем DOC → DOCX
$docxPath = $this->convertDocToDocxTask->run($file);
if (!$docxPath) {
Log::error('[ExtractTextFromDocumentTask:extractFromDoc] Не удалось конвертировать DOC файл', [
'file' => $file->getClientOriginalName(),
]);
return null;
}
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Конвертация успешна', [
'docx_path' => $docxPath,
]);
// Получаем абсолютный путь
$absoluteDocxPath = Storage::disk('local')->path($docxPath);
// Парсим конвертированный файл
$extractedText = $this->parseDocxTask->run($absoluteDocxPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Результат парсинга', [
'text_length' => strlen($extractedText ?? ''),
]);
// Удаляем конвертированный файл
Storage::disk('local')->delete($docxPath);
return $extractedText;
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class FindMainNewsFileTask
{
/**
* Ключевые слова для определения файла с новостью
*/
private const NEWS_KEYWORDS = [
'новость', 'новости', 'заметка', 'заметки', 'статья', 'статьи',
'материал', 'материалы', 'текст', 'контент', 'публикация',
'news', 'article', 'post', 'material', 'text',
'релиз', 'пресс', 'отчет', 'отчёт', 'доклад'
];
/**
* Находит основной файл с текстом новости среди всех загруженных файлов
*
* @param Collection<UploadedFile> $files Все загруженные файлы
* @return UploadedFile|null Основной файл или null если не найден
*/
public function run(Collection $files): ?UploadedFile
{
Log::info('[FindMainNewsFileTask] Начало поиска основного файла', [
'files_count' => $files->count(),
'files' => $files->map(function($f) {
return $f->getClientOriginalName() . ' (' . $f->getClientOriginalExtension() . ')';
})->toArray(),
]);
// Фильтруем DOC и DOCX файлы (поддерживаем оба формата)
$docFiles = $files->filter(function($file) {
$ext = strtolower($file->getClientOriginalExtension());
return in_array($ext, ['doc', 'docx']);
});
Log::info('[FindMainNewsFileTask] Найдено DOC/DOCX файлов', [
'count' => $docFiles->count(),
'files' => $docFiles->map(fn($f) => $f->getClientOriginalName())->toArray(),
]);
if ($docFiles->isEmpty()) {
Log::warning('[FindMainNewsFileTask] Не найдено DOC/DOCX файлов');
return null;
}
// Если только один файл — используем его
if ($docFiles->count() === 1) {
Log::info('[FindMainNewsFileTask] Найден один файл, используем его', [
'file' => $docFiles->first()->getClientOriginalName(),
]);
return $docFiles->first();
}
// Ищем файл с ключевыми словами в названии
foreach ($docFiles as $file) {
$filename = mb_strtolower($file->getClientOriginalName());
foreach (self::NEWS_KEYWORDS as $keyword) {
if (str_contains($filename, $keyword)) {
Log::info('[FindMainNewsFileTask] Найден файл по ключевому слову', [
'file' => $file->getClientOriginalName(),
'keyword' => $keyword,
]);
return $file;
}
}
}
Log::info('[FindMainNewsFileTask] Не найдено файлов по ключевым словам, используем LLM');
// Если не нашли по ключевым словам — используем LLM
return $this->findViaLLM($docFiles);
}
/**
* Определяет основной файл через LLM
*/
private function findViaLLM(Collection $docFiles): ?UploadedFile
{
Log::info('[FindMainNewsFileTask:findViaLLM] Начало LLM определения', [
'files_count' => $docFiles->count(),
]);
if ($docFiles->isEmpty()) {
return null;
}
if ($docFiles->count() === 1) {
return $docFiles->first();
}
// Собираем фрагменты текста из каждого файла
$fragments = [];
foreach ($docFiles as $file) {
Log::info('[FindMainNewsFileTask:findViaLLM] Извлечение фрагмента', [
'file' => $file->getClientOriginalName(),
]);
$fragment = app(ExtractTextFragmentTask::class)->run($file, 300);
$fragments[] = [
'filename' => $file->getClientOriginalName(),
'text' => $fragment,
];
}
// Отправляем запрос к LLM
Log::info('[FindMainNewsFileTask:findViaLLM] Отправка запроса к LLM');
$llmResponse = app(CallAiServiceForFileSelectionTask::class)->run($fragments);
if ($llmResponse && isset($llmResponse['main_file'])) {
Log::info('[FindMainNewsFileTask:findViaLLM] LLM вернула результат', [
'main_file' => $llmResponse['main_file'],
]);
$mainFilename = $llmResponse['main_file'];
foreach ($docFiles as $file) {
if ($file->getClientOriginalName() === $mainFilename) {
return $file;
}
}
}
Log::warning('[FindMainNewsFileTask:findViaLLM] LLM не помогла, используем первый файл');
// Fallback — первый файл
return $docFiles->first();
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use App\Containers\Article\Models\Post;
use Illuminate\Database\Eloquent\Collection;
class GetDraftPostsTask
{
public function run(): Collection
{
return Post::query()
->with(['category', 'author'])
->whereNull('publish_at')
->orderBy('created_at', 'desc')
->get([
'id',
'title',
'slug',
'preview_text',
'content',
'status',
'authors',
'preview',
'images',
'reading_time',
'category_id',
'created_at',
'updated_at',
]);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Containers\Dashboard\Tasks;
use ZipArchive;
use Illuminate\Support\Facades\Log;
class ParseDocxTask
{
/**
* Извлекает текст из .docx файла с подробным логированием ошибок
*/
public function run(string $filePath): ?string
{
// 1. Проверяем, существует ли физически файл по этому пути
if (!file_exists($filePath)) {
Log::error("[ParseDocxTask] Файл не найден по пути: {$filePath}");
return "ОШИБКА ДЕБАГА: Файл не найден на сервере.";
}
$zip = new ZipArchive();
// 2. Пытаемся открыть архив
$res = $zip->open($filePath);
// Если не true, значит произошла ошибка
if ($res !== true) {
$errorMsg = $this->getZipError($res);
Log::error("[ParseDocxTask] Ошибка ZipArchive: {$errorMsg} | Путь: {$filePath}");
// Возвращаем текст ошибки прямо на фронт, чтобы сразу увидеть причину!
return "ОШИБКА ДЕБАГА ZipArchive: {$errorMsg}";
}
// 3. Пытаемся достать нужный XML файл
$xmlString = $zip->getFromName('word/document.xml');
// Обязательно закрываем архив, чтобы не было утечек памяти
$zip->close();
if ($xmlString === false) {
Log::error("[ParseDocxTask] Файл word/document.xml не найден внутри архива. Возможно, это не настоящий DOCX.");
return "ОШИБКА ДЕБАГА: Внутри архива нет word/document.xml";
}
// 4. Парсим XML
try {
$text = str_replace('</w:p>', " \n", $xmlString);
$text = strip_tags($text);
return trim($text);
} catch (\Exception $e) {
Log::error("[ParseDocxTask] Ошибка очистки тегов: " . $e->getMessage());
return "ОШИБКА ДЕБАГА при очистке текста.";
}
}
/**
* Превращает числовой код ошибки ZipArchive в текст
*/
private function getZipError(int $code): string
{
$errors = [
ZipArchive::ER_EXISTS => 'Файл уже существует.',
ZipArchive::ER_INCONS => 'Несовместимый ZIP-архив.',
ZipArchive::ER_INVAL => 'Недопустимый аргумент.',
ZipArchive::ER_MEMORY => 'Ошибка выделения памяти (Malloc).',
ZipArchive::ER_NOENT => 'Нет такого файла.',
ZipArchive::ER_NOZIP => 'Это НЕ ZIP-архив (Скорее всего файл переименован или битый).',
ZipArchive::ER_OPEN => 'Невозможно открыть файл (Проблема с правами).',
ZipArchive::ER_READ => 'Ошибка чтения.',
ZipArchive::ER_SEEK => 'Ошибка поиска (Seek error).',
];
return $errors[$code] ?? "Неизвестная ошибка с кодом: {$code}";
}
}