Распределить товары по разделам каталога с помощью ИИ

Внимание! Все сообщения на форуме проходят модерацию. Ваше сообщение появится после проверки.
Привет всем.
Просто оставлю это тут - проверено на нескольких каталогах.
Ц меня API сваливает все товары в раздел ТОВАРЫ - а потом ИИшка распеределят по нормальным разделам, если надо создает.
После распределния API уже не меняет раздел, только обновляет товар
Код
<?php
/**
 * Скрипт автоматического распределения товаров по разделам каталога
 * с помощью DeepSeek AI

 * Обрабатывает по 10 товаров из раздела "Товары" (ID: 254) за один запуск
 * и распределяет их по подходящим разделам на основе названий
 */

define("NO_KEEP_STATISTIC", true);
define("NOT_CHECK_PERMISSIONS", true);
define('NO_AGENT_STATISTIC', true);
define('DisableEventsCheck', true);
define('STOP_STATISTICS', true);

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use \Bitrix\Main\Loader;

Loader::includeModule('iblock');

// Настройки
const DEEPSEEK_API_KEY = 'sk-XXXXXXXXXXXXXXXXXXXX'; // TODO: Заменить на реальный ключ
const DEEPSEEK_API_URL = 'https://api.deepseek.com';
const IBLOCK_ID = 4;
const SOURCE_SECTION_ID = 254; // Раздел "Товары"
const BATCH_SIZE = 10; // Количество товаров за один батч
const AUTO_RELOAD_DELAY = 1; // Задержка перезапуска в секундах

/**
 * Класс для работы с DeepSeek API
 */
class DeepSeekAPI {
    private $apiKey;
    private $apiUrl;

    public function __construct($apiKey, $apiUrl) {
        $this->apiKey = $apiKey;
        $this->apiUrl = $apiUrl;
    }

    /**
     * Отправка запроса к DeepSeek API
     */
    public function chat($messages, $model = 'deepseek-chat') {
        $data = [
            'model' => $model,
            'messages' => $messages,
            'temperature' => 0.7,
            'max_tokens' => 2000
        ];

        $ch = curl_init($this->apiUrl . '/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey
            ],
            CURLOPT_POSTFIELDS => json_encode($data),
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new Exception("DeepSeek API Error: " . $error);
        }

        $result = json_decode($response, true);

        if (isset($result['error'])) {
            throw new Exception("DeepSeek API Error: " . $result['error']['message']);
        }

        return $result['choices'][0]['message']['content'] ?? '';
    }
}

/**
 * Класс для работы с каталогом товаров
 */
class CatalogResort {
    private $deepseek;
    private $iblockId;
    private $sourceSectionId;
    private $sections = [];
    private $newSections = [];

    public function __construct(DeepSeekAPI $deepseek, $iblockId, $sourceSectionId) {
        $this->deepseek = $deepseek;
        $this->iblockId = $iblockId;
        $this->sourceSectionId = $sourceSectionId;
    }

    /**
     * Загрузка всех разделов каталога
     */
    public function loadSections() {
        $arFilter = ['IBLOCK_ID' => $this->iblockId, 'ACTIVE' => 'Y'];
        $arSelect = ['ID', 'NAME', 'CODE', 'DEPTH_LEVEL', 'IBLOCK_SECTION_ID'];

        $dbSections = CIBlockSection::GetList(
            ['LEFT_MARGIN' => 'ASC'],
            $arFilter,
            false,
            $arSelect
        );

        while ($section = $dbSections->Fetch()) {
            $this->sections[$section['ID']] = [
                'ID' => $section['ID'],
                'NAME' => $section['NAME'],
                'CODE' => $section['CODE'],
                'DEPTH_LEVEL' => $section['DEPTH_LEVEL'],
                'PARENT_ID' => $section['IBLOCK_SECTION_ID']
            ];
        }

        return $this->sections;
    }

    /**
     * Получение списка товаров из раздела "Товары"
     */
    public function getProductsFromSource($limit = BATCH_SIZE) {
        $arFilter = [
            'IBLOCK_ID' => $this->iblockId,
            'IBLOCK_SECTION_ID' => $this->sourceSectionId,
            'ACTIVE' => 'Y'
        ];

        $arSelect = ['ID', 'NAME', 'IBLOCK_SECTION_ID'];

        $dbElements = CIBlockElement::GetList(
            ['ID' => 'ASC'],
            $arFilter,
            false,
            ['nTopCount' => $limit],
            $arSelect
        );

        $products = [];
        while ($element = $dbElements->Fetch()) {
            $products[] = [
                'ID' => $element['ID'],
                'NAME' => $element['NAME']
            ];
        }

        return $products;
    }

    /**
     * Создание нового раздела
     */
    public function createSection($name, $parentId = null) {
        // Генерируем символьный код
        $code = $this->generateSectionCode($name);

        // Проверяем, не создан ли уже раздел с таким кодом
        foreach ($this->sections as $section) {
            if ($section['CODE'] === $code) {
                return $section['ID'];
            }
        }

        // Проверяем в новых созданных разделах
        if (isset($this->newSections[$code])) {
            return $this->newSections[$code];
        }

        $bs = new CIBlockSection;
        $arFields = [
            'IBLOCK_ID' => $this->iblockId,
            'NAME' => $name,
            'CODE' => $code,
            'ACTIVE' => 'Y',
            'IBLOCK_SECTION_ID' => $parentId
        ];

        $sectionId = $bs->Add($arFields);

        if ($sectionId) {
            $this->sections[$sectionId] = [
                'ID' => $sectionId,
                'NAME' => $name,
                'CODE' => $code,
                'DEPTH_LEVEL' => $parentId ? ($this->sections[$parentId]['DEPTH_LEVEL'] + 1) : 1,
                'PARENT_ID' => $parentId
            ];

            $this->newSections[$code] = $sectionId;

            return $sectionId;
        } else {
            throw new Exception("Ошибка создания раздела: " . $bs->LAST_ERROR);
        }
    }

    /**
     * Генерация символьного кода раздела
     */
    private function generateSectionCode($name) {
        $code = mb_strtolower($name);

        // Транслитерация
        $arTranslit = [
            'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
            'е' => 'e', 'ё' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
            'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
            'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
            'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
            'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
            'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
        ];

        $code = strtr($code, $arTranslit);

        // Заменяем все небуквенно-цифровые символы на дефис
        $code = preg_replace('/[^a-z0-9]+/', '-', $code);

        // Убираем дефисы в начале и конце
        $code = trim($code, '-');

        return $code;
    }

    /**
     * Определение раздела для товара с помощью DeepSeek
     */
    public function determineSectionForProduct($productName) {
        // Формируем список доступных разделов для AI
        $sectionsText = "Существующие разделы каталога:\n";
        foreach ($this->sections as $section) {
            if ($section['ID'] != $this->sourceSectionId) {
                $indent = str_repeat('  ', $section['DEPTH_LEVEL'] - 1);
                $sectionsText .= $indent . "- {$section['NAME']} (ID: {$section['ID']})\n";
            }
        }

        $prompt = <<<PROMPT
Ты помогаешь распределять товары аптеки по разделам каталога.

$sectionsText

Товар: "$productName"

Инструкции:
1. Определи наиболее подходящий раздел для этого товара из существующих
2. Если подходящего раздела нет - предложи название нового раздела на русском языке
3. Ответ дай СТРОГО в формате JSON без дополнительного текста:

Если раздел существует:
{"action": "move", "section_id": 123, "section_name": "Название раздела"}

Если нужно создать новый раздел:
{"action": "create", "section_name": "Название нового раздела", "parent_id": null}

Если нужно создать подраздел в существующем разделе:
{"action": "create", "section_name": "Название нового подраздела", "parent_id": 123}

Примеры категоризации:
- Парацетамол, Аспирин → Обезболивающие и жаропонижающие
- Витамин C, Омега-3 → Витамины и БАДы
- Зубная паста, щетка → Гигиена полости рта
- Йод, бинт → Перевязочные материалы
- Тонометр → Медицинская техника

Отвечай только JSON, без пояснений!
PROMPT;

        try {
            $messages = [
                ['role' => 'user', 'content' => $prompt]
            ];

            $response = $this->deepseek->chat($messages);

            // Извлекаем JSON из ответа (может быть в markdown блоке)
            if (preg_match('/```json\s*(.*?)\s*```/s', $response, $matches)) {
                $jsonStr = $matches[1];
            } else {
                $jsonStr = $response;
            }

            $result = json_decode(trim($jsonStr), true);

            if (!$result || !isset($result['action'])) {
                throw new Exception("Некорректный ответ от AI: " . $response);
            }

            return $result;

        } catch (Exception $e) {
            // Если AI не сработал, возвращаем дефолтное действие
            return [
                'action' => 'skip',
                'error' => $e->getMessage()
            ];
        }
    }

    /**
     * Перемещение товара в раздел
     */
    public function moveProductToSection($productId, $sectionId) {
        $el = new CIBlockElement;
        $result = $el->SetElementSection($productId, $sectionId);

        if (!$result) {
            throw new Exception("Ошибка перемещения товара ID $productId: " . $el->LAST_ERROR);
        }

        return true;
    }

    /**
     * Основной процесс распределения товаров
     */
    public function process($totalProcessed = 0) {
        $log = [];
        $log[] = "=== Запуск распределения товаров " . date('Y-m-d H:i:s') . " ===\n";
        $log[] = "Обработано всего: $totalProcessed товаров\n";

        // Загружаем разделы
        $this->loadSections();
        $log[] = "Загружено разделов: " . count($this->sections) . "\n";

        // Получаем товары из раздела "Товары"
        $products = $this->getProductsFromSource();
        $log[] = "Найдено товаров для обработки: " . count($products) . "\n\n";

        if (empty($products)) {
            $log[] = "Нет товаров для обработки. Все товары распределены!\n";
            return [
                'log' => implode('', $log),
                'processed' => 0,
                'continue' => false
            ];
        }

        $stats = [
            'moved' => 0,
            'created_sections' => 0,
            'skipped' => 0,
            'errors' => 0
        ];

        foreach ($products as $product) {
            $log[] = "Товар #{$product['ID']}: {$product['NAME']}\n";

            try {
                // Определяем раздел с помощью AI
                $decision = $this->determineSectionForProduct($product['NAME']);

                if ($decision['action'] === 'move') {
                    // Перемещаем в существующий раздел
                    $this->moveProductToSection($product['ID'], $decision['section_id']);
                    $log[] = "  ✓ Перемещен в раздел: {$decision['section_name']} (ID: {$decision['section_id']})\n";
                    $stats['moved']++;

                } elseif ($decision['action'] === 'create') {
                    // Создаем новый раздел и перемещаем
                    $parentId = $decision['parent_id'] ?? null;
                    $newSectionId = $this->createSection($decision['section_name'], $parentId);
                    $this->moveProductToSection($product['ID'], $newSectionId);
                    $log[] = "  ✓ Создан раздел: {$decision['section_name']} (ID: $newSectionId)\n";
                    $log[] = "  ✓ Товар перемещен в новый раздел\n";
                    $stats['created_sections']++;
                    $stats['moved']++;

                } elseif ($decision['action'] === 'skip') {
                    $log[] = "  ⊘ Пропущен: " . ($decision['error'] ?? 'Не удалось определить раздел') . "\n";
                    $stats['skipped']++;
                }

            } catch (Exception $e) {
                $log[] = "  ✗ Ошибка: " . $e->getMessage() . "\n";
                $stats['errors']++;
            }

            $log[] = "\n";
        }

        // Итоговая статистика
        $log[] = "=== Статистика батча ===\n";
        $log[] = "Перемещено товаров: {$stats['moved']}\n";
        $log[] = "Создано разделов: {$stats['created_sections']}\n";
        $log[] = "Пропущено: {$stats['skipped']}\n";
        $log[] = "Ошибок: {$stats['errors']}\n";

        $processedInBatch = $stats['moved'] + $stats['skipped'];
        $newTotal = $totalProcessed + $processedInBatch;

        $remainingProducts = count($this->getProductsFromSource(1));
        $shouldContinue = ($remainingProducts > 0); // Продолжаем пока есть товары

        if ($remainingProducts > 0) {
            $log[] = "\n⏳ Осталось товаров в разделе 'Товары': продолжаем обработку...\n";
        } else {
            $log[] = "\n✓ Все товары из раздела 'Товары' распределены!\n";
        }

        return [
            'log' => implode('', $log),
            'processed' => $processedInBatch,
            'continue' => $shouldContinue,
            'total' => $newTotal
        ];
    }
}

// Функция для вывода с HTML-форматированием
function output($text) {
    echo nl2br(htmlspecialchars($text));
}

// Основной блок выполнения
try {
    // Получаем текущий счетчик из параметра URL
    $totalProcessed = isset($_GET['processed']) ? (int)$_GET['processed'] : 0;

    echo "<!DOCTYPE html>
<html>
<head>
    <meta charset='UTF-8'>
    <title>Распределение товаров по разделам</title>
    <style>
        body { font-family: monospace; padding: 20px; background: #f5f5f5; }
        .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        h1 { color: #333; }
        .success { color: green; }
        .error { color: red; }
        .warning { color: orange; }
        .info { color: blue; font-weight: bold; }
        #auto-reload { 
            background: #fff3cd; 
            border: 2px solid #ffc107; 
            padding: 15px; 
            margin: 20px 0; 
            border-radius: 5px;
            text-align: center;
        }
        .stats-box {
            background: #e3f2fd;
            border: 2px solid #2196F3;
            padding: 15px;
            margin: 15px 0;
            border-radius: 5px;
        }
        .stats-box h3 {
            margin: 0 0 10px 0;
            color: #1976D2;
        }
        .stats-item {
            font-size: 18px;
            margin: 8px 0;
        }
        .stats-item strong {
            color: #1565C0;
        }
        .spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(0,0,0,.1);
            border-radius: 50%;
            border-top-color: #ffc107;
            animation: spin 1s ease-in-out infinite;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class='container'>
        <h1>:f09fa496: Распределение товаров по разделам с помощью AI</h1>
        <hr>";

    // Создаем экземпляры классов
    $deepseek = new DeepSeekAPI(DEEPSEEK_API_KEY, DEEPSEEK_API_URL);
    $resort = new CatalogResort($deepseek, IBLOCK_ID, SOURCE_SECTION_ID);

    // Запускаем процесс
    $result = $resort->process($totalProcessed);

    $newTotal = $result['total'];

    echo "<div class='stats-box'>
            <h3>:f09f938a: Статистика обработки</h3>
            <div class='stats-item'>Всего обработано: <strong>{$newTotal}</strong> товаров</div>
            <div class='stats-item'>В текущем батче: <strong>{$result['processed']}</strong> товаров</div>
          </div>";

    echo "<pre>";
    output($result['log']);
    echo "</pre>";

    // Если нужно продолжить обработку
    if ($result['continue']) {
        $nextUrl = $_SERVER['PHP_SELF'] . '?processed=' . $newTotal;
        $delayMs = AUTO_RELOAD_DELAY * 1000;
        echo "<div id='auto-reload' class='info'>
                <div class='spinner'></div>
                <h3>⏳ Автоматическое продолжение обработки...</h3>
                <p>Обработано: <strong>$newTotal</strong> товаров</p>
                <p>Перенаправление через " . AUTO_RELOAD_DELAY . " сек...</p>
                <p><a href='$nextUrl'>Нажмите здесь, если перенаправление не произошло</a></p>
              </div>
              <script>
                setTimeout(function() {
                    window.location.href = '$nextUrl';
                }, $delayMs);
              </script>";
    } else {
        echo "<div class='success' style='padding: 20px; background: #d4edda; border: 2px solid #28a745; border-radius: 5px; margin-top: 20px;'>
                <h3>✅ Обработка завершена!</h3>
                <p><strong>Всего обработано:</strong> $newTotal товаров</p>
                <p>Все товары из раздела \"Товары\" распределены по разделам!</p>
                <p><a href='{$_SERVER['PHP_SELF']}' style='color: #007bff; font-weight: bold;'>Запустить снова (если появятся новые товары)</a></p>
              </div>";
    }

    echo "</div>
</body>
</html>";

} catch (Exception $e) {
    echo "<div class='error'>";
    echo "<h2>Ошибка выполнения скрипта</h2>";
    echo "<p>" . htmlspecialchars($e->getMessage()) . "</p>";
    echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
    echo "</div>";
}


Форма ответов
 
Текст сообщения*
Перетащите файлы
Ничего не найдено
Файл
 

Стоимость разработки на 1С-Битрикс:

Индивидуальная разработка магазина

от 350 000 руб. от 5-ти недель

Разработка магазина на 1С-Битрикс с нуля. Дизайн, сборка и оптимизация производительности под конкретный проект и требования. Реализация любого функционала без ограничений готовых решений.

Запуск сайта на готовом решении

от 150 000 руб. от 7-ми дней

Вариант для тех, кто не хочет тратить много средств на индивидуальный проект, и не имеет серьезных требований к сайту. Магазин, быстро запускается на базе одного из 200-та готовых решений.

Мобильное приложение

от 400 000 руб. от 5-ти недель

Разработка кроссплатформенного мобильного приложения, которое не уступает нативным решениям как в производительности, так и пользовательском опыте. Публикуется в AppStore, GooglePlay и RuStore

Сайт компании

от 300 000 руб. от 2-х недель

Корпоративный сайт с информационными разделами, каталогом товаров или услуг. Включает формы обратной связи карточек каталога, любое количество статичных и динамичных разделов.

Инфоресурс

от 300 000 руб. от 4-х недель

Информационный ресурс любой сложности. Сайт для СМИ, городской портал или многопользовательская доска объявлений. Внутренние форумы, блоги- по необходимости.

3D‑моделирование, визуализация

от 25 000 руб. от 3-х дней

По вашим фото, чертежам или описанию создадим 3D‑модели и отрендерим набор изображений для каталога товаров: общий вид, крупные планы и технические ракурсы или 360°‑обзор товара.