Интеграция Больших Языковых Моделей (LLM) в административную панель WordPress — фундаментальный шаг к созданию интеллектуальных редакционных систем. Однако в enterprise-окружении прямое подключение клиентских скриптов к облачным API является критической уязвимостью: оно ведет к утечке секретных API-ключей, нарушению требований GDPR/HIPAA при отправке персональных данных авторов и риску исчерпания корпоративного бюджета из-за отсутствия лимитов.
Архитектура надежной интеграции строится на гибридной модели LLM Gateway (Шлюз искусственного интеллекта):
apiFetch.current_user_can('edit_posts')), управляет лимитами запросов (Rate Limiting), подмешивает системные промпты и скрывает приватные ключи.При этом для предотвращения зависания PHP-воркеров при генерации длинных ответов шлюз реализует потоковую передачу токенов (Server-Sent Events, SSE) либо асинхронную обработку через очереди Action Scheduler.
Архитектурный поток разделяет приватный контур сервера и клиентский интерфейс редактора:
[ Gutenberg Editor Sidebar (React 19) ]
|
v (HTTPS POST + X-WP-Nonce)
[ WordPress REST API Gateway: /agentic/v1/chat ]
|
+---> Проверка: current_user_can('edit_posts')
+---> Rate Limiter (Transients / Redis)
|
v (Маршрутизация по типу задачи)
+---------------+-------------------------------------+
| |
v (Конфиденциально / Офлайн) v (Высокая сложность)
[ Local Ollama Gateway ] [ Cloud LLM Provider ]
(http://127.0.0.1:11434/api/generate) (OpenAI / Anthropic API)
(Llama 3.3 / DeepSeek R1) (GPT-4o / Claude 3.7)
| |
+---------------+-------------------------------------+
|
v (Стриминг токенов SSE / JSON)
[ Редактор Gutenberg: dispatch('core/editor') обновляет блоки ]Монтирование React-компонента ассистента в редактор постов через @wordpress/plugins.
Отправка текста статьи на эндпоинт /agentic/v1/ai-assist с верификацией сессии через apiFetch.
Проверка пользовательских квот, аутентификации и очистка контекста от персональных данных.
Маршрутизация на локальный Ollama инстанс с fallback на OpenAI при превышении таймаута.
Парсинг JSON-ответа и автоматическая вставка блоков через @wordpress/data dispatch.
post_content и автоматическое назначение категорий и тегов перед отправкой статьи на модерацию.alt-тегов для незрячих пользователей (Accessibility / WCAG).Ниже представлен законченный производственный артефакт: серверный контроллер шлюза на PHP 8.4 с поддержкой Ollama и OpenAI, а также React-компонент боковой панели для Gutenberg.
<?php
declare(strict_types=1);
/**
* Author: Artur Yusupov (Fullstack Agentic Lab)
* Package: @agentic-lab/ai-gateway
* Module: LlmGatewayController
* Standard: WordPress 6.8+ / PHP 8.4 Strict / REST API v2
*/
namespace AgenticLab\Ai\Gateway;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
final class LlmGatewayController extends WP_REST_Controller
{
protected $namespace = 'agentic-lab/v1';
protected $rest_base = 'ai';
private string $ollamaEndpoint;
private string $cloudApiKey;
public function __construct()
{
$this->ollamaEndpoint = (string) (getenv('OLLAMA_HOST') ?: 'http://127.0.0.1:11434');
$this->cloudApiKey = (string) (getenv('OPENAI_API_KEY') ?: '');
}
public function register_routes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base . '/generate', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'handleGenerateRequest'],
'permission_callback' => [$this, 'checkPermissions'],
'args' => [
'prompt' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
],
'mode' => [
'required' => false,
'type' => 'string',
'enum' => ['local', 'cloud', 'auto'],
'default' => 'auto',
],
],
],
]);
}
public function checkPermissions(WP_REST_Request $request): bool|WP_Error
{
if (!current_user_can('edit_posts')) {
return new WP_Error('rest_forbidden', __('Недостаточно прав для генерации контента.', 'agentic-lab'), ['status' => 403]);
}
return true;
}
public function handleGenerateRequest(WP_REST_Request $request): WP_REST_Response|WP_Error
{
$prompt = (string) $request->get_param('prompt');
$mode = (string) $request->get_param('mode');
$userId = get_current_user_id();
// 1. Проверка Rate Limit: не более 10 запросов в минуту на пользователя
$rateLimitKey = "agentic_ai_ratelimit_{$userId}";
$currentHits = (int) get_transient($rateLimitKey);
if ($currentHits >= 10) {
return new WP_Error('rate_limit_exceeded', __('Превышен лимит запросов к ИИ. Попробуйте через минуту.', 'agentic-lab'), ['status' => 429]);
}
set_transient($rateLimitKey, $currentHits + 1, 60);
// 2. Выбор провайдера инференса
$result = null;
if ($mode === 'local' || $mode === 'auto') {
$result = $this->callOllamaLocal($prompt);
}
// 3. Fallback на Cloud, если локальный сервер недоступен
if ($result === null && ($mode === 'cloud' || $mode === 'auto')) {
$result = $this->callCloudProvider($prompt);
}
if ($result === null) {
return new WP_Error('ai_service_unavailable', __('Сервисы генерации временно недоступны.', 'agentic-lab'), ['status' => 503]);
}
return new WP_REST_Response([
'success' => true,
'content' => $result['text'],
'provider' => $result['provider'],
'tokens' => $result['tokens'] ?? 0,
], 200);
}
/**
* Вызов локального инференса через Ollama API.
*
* @return array{text: string, provider: string}|null
*/
private function callOllamaLocal(string $prompt): ?array
{
$response = wp_remote_post("{$this->ollamaEndpoint}/api/generate", [
'timeout' => 15,
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'model' => 'llama3.3:latest',
'prompt' => $prompt,
'stream' => false,
]),
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return null; // Fallback триггер
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!isset($data['response'])) {
return null;
}
return [
'text' => trim((string) $data['response']),
'provider' => 'local_ollama',
];
}
/**
* Вызов облачного шлюза OpenAI в качестве резервного провайдера.
*
* @return array{text: string, provider: string}|null
*/
private function callCloudProvider(string $prompt): ?array
{
if ($this->cloudApiKey === '') {
return null;
}
$response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $this->cloudApiKey,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'system', 'content' => 'You are an enterprise WordPress editorial assistant. Output clear text.'],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.3,
]),
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
$text = $data['choices'][0]['message']['content'] ?? null;
if ($text === null) {
return null;
}
return [
'text' => trim((string) $text),
'provider' => 'cloud_openai',
];
}
}Симптом: Ошибка HTTP 504 Gateway Timeout или Fatal Error: Maximum execution time of 30 seconds exceeded при генерации длинных статей. Причина: Локальный LLM инференс на медленном CPU занимает 45–60 секунд, превышая стандартный лимит времени выполнения PHP-скрипта. Решение: Для тяжелых задач использовать асинхронную обработку через Action Scheduler или фоновый Worker. Для интерактивных запросов настраивать потоковую передачу данных (Streaming / SSE) с отключением буферизации PHP.
Симптом: Нарушение требований конфиденциальности (GDPR), когда конфиденциальные черновики или персональные данные авторов отправляются в публичный API без согласия. Причина: Прямая передача содержимого базы данных во внешний облачный сервис без маскирования и фильтрации. Решение: Применение локального сервера Ollama для всех черновиков и предварительная санитизация контекста перед отправкой в облако.
Симптом: Разрушение верстки страниц, внедрение закрывающих тегов и ошибки валидации блоков в Gutenberg.
Причина: Слепое доверие тексту, возвращенному LLM, который может содержать незакрытые <div> теги или неподдерживаемые атрибуты.
Решение: Очищать сгенерированный контент через wp_kses_post() на сервере и использовать createBlock() API на клиенте для строгого формирования структуры блоков.
Симптом: Огромные счета за API-провайдеров в конце месяца или полный отказ сервера из-за перегрузки CPU/GPU. Причина: Отсутствие контроля частоты обращений к эндпоинту генерации со стороны редакторов. Решение: Внедрение сессионных и пользовательских лимитов через WordPress Transients или Redis.
Автор нажимает кнопку 'Сгенерировать резюме' в боковой панели Gutenberg.
apiFetch отправляет запрос на сервер с nonce-токеном и текстом статьи.
PHP проверяет права edit_posts и лимит запросов в транзиентах.
Шлюз опрашивает Ollama; при сбое автоматически переключается на OpenAI.
Результат записывается в Excerpt через dispatch('core/editor').editPost().