Просто оставлю это тут - проверено на нескольких каталогах.
Ц меня 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>";
}
|