- Create migration for global settings table and add google_ads_customer_id and google_ads_start_date columns to clients table. - Add migration to include product_url column in products_data table. - Insert demo data for campaigns, products, and their history for client 'pomysloweprezenty.pl'. - Implement client management interface with modals for adding and editing clients, including Google Ads Customer ID and data retrieval start date.
283 lines
12 KiB
PHP
283 lines
12 KiB
PHP
<?php
|
|
namespace services;
|
|
class OpenAiApi
|
|
{
|
|
static private $api_url = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
static private $system_prompt = 'Jesteś ekspertem od optymalizacji feedów produktowych Google Merchant Center i reklam Google Ads (Performance Max, Shopping).
|
|
|
|
OFICJALNE ZASADY GOOGLE MERCHANT CENTER — BEZWZGLĘDNIE PRZESTRZEGAJ:
|
|
|
|
TYTUŁY (title) — max 150 znaków:
|
|
Zalecana struktura: [Marka] [Typ produktu] [Kluczowe atrybuty: kolor, rozmiar, materiał — TYLKO jeśli wynikają z nazwy]
|
|
- Najważniejsze informacje umieść w pierwszych 70 znakach (reszta może być obcięta na mobile)
|
|
- Każdy wariant produktu musi mieć unikalny tytuł (różne kolory/rozmiary = różne tytuły)
|
|
- Tytuł MUSI być spójny z nazwą produktu na stronie docelowej
|
|
- Używaj naturalnego języka — pisz dla ludzi, nie dla algorytmów
|
|
ZAKAZANE w tytułach:
|
|
- Tekst promocyjny: bestseller, hit, okazja, wyprzedaż, rabat, najlepszy, TOP, #1, idealny, polecamy, nowość
|
|
- Wykrzykniki (!) i nadmierna interpunkcja
|
|
- WIELKIE LITERY (wyjątek: ustalone skróty jak LED, USB, TV)
|
|
- Emotikony, emoji, symbole dekoracyjne (★, ♥, ✓)
|
|
- Informacje o cenie, dostawie, promocji
|
|
- Wezwania do działania (kup teraz, sprawdź, zamów)
|
|
- Upychanie słów kluczowych (keyword stuffing) — np. powtarzanie tych samych fraz
|
|
- Kody wewnętrzne, SKU, numery katalogowe
|
|
- Cechy produktu których NIE MA w oryginalnej nazwie (nie wymyślaj koloru, materiału, rozmiaru)
|
|
|
|
OPISY (description) — max 5000 znaków, ale najważniejsze info w pierwszych 160-500 znakach:
|
|
- Opisz cechy, specyfikacje techniczne, zastosowanie, grupę docelową
|
|
- Wymień atrybuty niewidoczne na zdjęciu (materiał, wzór, przeznaczenie wiekowe)
|
|
- Ton neutralny, informacyjny — jak w katalogu produktowym
|
|
ZAKAZANE w opisach:
|
|
- Tekst promocyjny i reklamowy (te same słowa co w tytułach)
|
|
- Nazwa firmy/sklepu (chyba że to marka produktu)
|
|
- Informacje o wysyłce, cenach, promocjach
|
|
- Wezwania do działania
|
|
- Opisy innych produktów, akcesoriów nie wchodzących w skład oferty
|
|
- Tekst zastępczy (lorem ipsum, placeholder)
|
|
- Powielanie tego samego opisu dla wielu produktów
|
|
- HTML, tagi formatowania
|
|
|
|
OGÓLNE:
|
|
- Pisz poprawną polszczyzną (ortografia, gramatyka, interpunkcja)
|
|
- Używaj pisowni zdaniowej lub tytułowej — NIE CAPS
|
|
- Dane w feedzie MUSZĄ zgadzać się z danymi na stronie produktowej
|
|
- Opisuj TYLKO cechy wynikające z nazwy produktu — nie wymyślaj
|
|
|
|
Twoje odpowiedzi muszą być:
|
|
- Ściśle zgodne z polityką Google Merchant Center
|
|
- Zgodne z prawdą (opisuj tylko cechy widoczne w nazwie produktu)
|
|
- Zoptymalizowane pod kątem trafności wyszukiwań w Google Ads
|
|
- W języku polskim';
|
|
|
|
static public function is_configured()
|
|
{
|
|
return (bool) GoogleAdsApi::get_setting( 'openai_api_key' );
|
|
}
|
|
|
|
static public function fetch_page_content( $url )
|
|
{
|
|
$ch = curl_init( $url );
|
|
curl_setopt_array( $ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
CURLOPT_HTTPHEADER => [
|
|
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language: pl-PL,pl;q=0.9,en;q=0.8',
|
|
],
|
|
CURLOPT_SSL_VERIFYPEER => false
|
|
] );
|
|
|
|
$html = curl_exec( $ch );
|
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
curl_close( $ch );
|
|
|
|
if ( $http_code !== 200 || !$html ) return '';
|
|
|
|
// Usuń skrypty, style, komentarze
|
|
$html = preg_replace( '/<script[^>]*>.*?<\/script>/si', '', $html );
|
|
$html = preg_replace( '/<style[^>]*>.*?<\/style>/si', '', $html );
|
|
$html = preg_replace( '/<!--.*?-->/s', '', $html );
|
|
$html = preg_replace( '/<nav[^>]*>.*?<\/nav>/si', '', $html );
|
|
$html = preg_replace( '/<footer[^>]*>.*?<\/footer>/si', '', $html );
|
|
$html = preg_replace( '/<header[^>]*>.*?<\/header>/si', '', $html );
|
|
|
|
// Zamień tagi na spacje i wyciągnij tekst
|
|
$text = strip_tags( $html );
|
|
$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );
|
|
$text = preg_replace( '/\s+/', ' ', $text );
|
|
$text = trim( $text );
|
|
|
|
// Ogranicz do ~3000 znaków
|
|
if ( mb_strlen( $text ) > 3000 )
|
|
$text = mb_substr( $text, 0, 3000 ) . '...';
|
|
|
|
return $text;
|
|
}
|
|
|
|
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500 )
|
|
{
|
|
$api_key = GoogleAdsApi::get_setting( 'openai_api_key' );
|
|
$model = GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini';
|
|
|
|
// GPT-5.x wymaga max_completion_tokens, starsze modele używają max_tokens
|
|
$tokens_key = ( strpos( $model, 'gpt-5' ) === 0 ) ? 'max_completion_tokens' : 'max_tokens';
|
|
|
|
$payload = [
|
|
'model' => $model,
|
|
'messages' => [
|
|
[ 'role' => 'system', 'content' => $system_prompt ],
|
|
[ 'role' => 'user', 'content' => $user_prompt ]
|
|
],
|
|
'temperature' => 0.7,
|
|
$tokens_key => $max_tokens
|
|
];
|
|
|
|
$ch = curl_init( self::$api_url );
|
|
curl_setopt_array( $ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $api_key
|
|
],
|
|
CURLOPT_POSTFIELDS => json_encode( $payload ),
|
|
CURLOPT_TIMEOUT => 30
|
|
] );
|
|
|
|
$response = curl_exec( $ch );
|
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
curl_close( $ch );
|
|
|
|
if ( $http_code !== 200 )
|
|
{
|
|
$error = json_decode( $response, true );
|
|
return [ 'status' => 'error', 'message' => $error['error']['message'] ?? 'Błąd API OpenAI (HTTP ' . $http_code . ')' ];
|
|
}
|
|
|
|
$data = json_decode( $response, true );
|
|
$content = trim( $data['choices'][0]['message']['content'] ?? '' );
|
|
|
|
return [ 'status' => 'ok', 'suggestion' => $content ];
|
|
}
|
|
|
|
static private function build_context_text( $context )
|
|
{
|
|
$lines = [];
|
|
$lines[] = 'Nazwa produktu: ' . ( $context['original_name'] ?? '—' );
|
|
|
|
if ( !empty( $context['current_title'] ) )
|
|
$lines[] = 'Obecny tytuł (custom): ' . $context['current_title'];
|
|
|
|
if ( !empty( $context['current_description'] ) )
|
|
$lines[] = 'Obecny opis: ' . $context['current_description'];
|
|
|
|
if ( !empty( $context['current_category'] ) )
|
|
$lines[] = 'Obecna kategoria Google: ' . $context['current_category'];
|
|
|
|
if ( !empty( $context['offer_id'] ) )
|
|
$lines[] = 'ID oferty: ' . $context['offer_id'];
|
|
|
|
if ( !empty( $context['custom_label_4'] ) )
|
|
$lines[] = 'Status produktu: ' . $context['custom_label_4'];
|
|
|
|
$lines[] = '';
|
|
$lines[] = 'Metryki reklamowe (ostatnie 30 dni):';
|
|
$lines[] = '- Wyświetlenia: ' . ( $context['impressions_30'] ?? 0 );
|
|
$lines[] = '- Kliknięcia: ' . ( $context['clicks_30'] ?? 0 );
|
|
$lines[] = '- CTR: ' . ( $context['ctr'] ?? 0 ) . '%';
|
|
$lines[] = '- Koszt: ' . ( $context['cost'] ?? 0 ) . ' PLN';
|
|
$lines[] = '- Konwersje: ' . ( $context['conversions'] ?? 0 );
|
|
$lines[] = '- Wartość konwersji: ' . ( $context['conversions_value'] ?? 0 ) . ' PLN';
|
|
$lines[] = '- ROAS: ' . ( $context['roas'] ?? 0 ) . '%';
|
|
|
|
if ( !empty( $context['page_content'] ) )
|
|
{
|
|
$lines[] = '';
|
|
$lines[] = 'Treść ze strony produktu (użyj tych informacji do stworzenia dokładniejszego opisu):';
|
|
$lines[] = $context['page_content'];
|
|
}
|
|
|
|
return implode( "\n", $lines );
|
|
}
|
|
|
|
static public function suggest_title( $context )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
|
|
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
|
|
|
|
WYMAGANIA:
|
|
- Max 150 znaków, najważniejsze info w pierwszych 70 znakach
|
|
- Struktura: [Marka jeśli jest w nazwie] [Typ produktu] [Kluczowe cechy z nazwy] [Dla kogo/okazja jeśli wynika z nazwy]
|
|
- Pisownia zdaniowa lub tytułowa, naturalny język
|
|
- Tytuł musi brzmieć jak wpis w katalogu produktowym
|
|
|
|
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
|
- Słowa promocyjne: bestseller, hit, idealny, najlepszy, polecamy, okazja, nowość, TOP
|
|
- Wykrzykniki (!), emotikony, symbole dekoracyjne
|
|
- WIELKIE LITERY (wyjątek: LED, USB, TV)
|
|
- Wezwania do działania, informacje o cenie/dostawie
|
|
- Cechy wymyślone — opisuj TYLKO to co wynika z oryginalnej nazwy lub treści strony produktu
|
|
- Jeśli podano treść ze strony produktu, wykorzystaj ją do wzbogacenia tytułu o rzeczywiste cechy (marka, materiał, kolor, rozmiar itp.)
|
|
|
|
' . $context_text . '
|
|
|
|
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
|
|
|
return self::call_api( self::$system_prompt, $prompt );
|
|
}
|
|
|
|
static public function suggest_description( $context )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
$has_page = !empty( $context['page_content'] );
|
|
|
|
$length_guide = $has_page
|
|
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
|
|
- Wykorzystaj szczegóły ze strony produktu: skład zestawu, materiały, wymiary, kolory, przeznaczenie
|
|
- Każdy akapit/punkt powinien wnosić NOWĄ informację — NIE powtarzaj tych samych elementów'
|
|
: '- Napisz opis o długości ok. 1000 znaków (800-1200) — jeśli brak szczegółów, opisz ogólnie zastosowanie i grupę docelową
|
|
- Opisuj TYLKO to co wynika z oryginalnej nazwy — nie wymyślaj konkretnych parametrów';
|
|
|
|
$prompt = 'Napisz zoptymalizowany opis produktu dla Google Merchant Center.
|
|
|
|
WYMAGANIA:
|
|
' . $length_guide . '
|
|
- Najważniejsze info na początku (pierwsze 160 znaków widoczne w wynikach)
|
|
- Rzeczowo opisz: co to jest, z czego się składa, do czego służy, dla kogo
|
|
- Ton neutralny, informacyjny — jak w specyfikacji produktowej
|
|
- Każdy akapit musi zawierać INNĄ informację niż poprzedni — NIGDY nie powtarzaj tych samych elementów
|
|
|
|
FORMATOWANIE — Google Merchant Center obsługuje podstawowe HTML:
|
|
- Używaj <br> do oddzielania akapitów/sekcji
|
|
- Używaj <b>pogrubienia</b> dla kluczowych cech (np. nazwy elementów zestawu, materiał)
|
|
- Używaj <ul><li>...</li></ul> gdy wymieniasz elementy zestawu lub listę cech
|
|
- NIE używaj innych tagów HTML (h1, p, div, span, img, a, table itp.)
|
|
- NIE używaj Markdown — tylko dozwolone tagi HTML
|
|
|
|
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
|
- Słowa promocyjne: bestseller, hit, okazja, najlepszy, polecamy, nowość, idealny
|
|
- Wykrzykniki (!), emotikony, WIELKIE LITERY
|
|
- Wezwania do działania: kup teraz, zamów, sprawdź, dodaj do koszyka
|
|
- Informacje o cenie, dostawie, promocjach, nazwie sklepu
|
|
- Opisy akcesoriów/produktów nie wchodzących w skład oferty
|
|
- Cechy wymyślone — opisuj TYLKO to co wynika z nazwy lub treści strony produktu
|
|
|
|
' . $context_text . '
|
|
|
|
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
|
|
|
|
$tokens = $has_page ? 1500 : 1000;
|
|
return self::call_api( self::$system_prompt, $prompt, $tokens );
|
|
}
|
|
|
|
static public function suggest_category( $context, $categories = [] )
|
|
{
|
|
$context_text = self::build_context_text( $context );
|
|
|
|
$cats_text = '';
|
|
if ( !empty( $categories ) )
|
|
{
|
|
$cats_list = array_slice( $categories, 0, 50 );
|
|
$cats_text = "\n\nDostępne kategorie Google Product Taxonomy (format: id - tekst):\n";
|
|
foreach ( $cats_list as $cat )
|
|
{
|
|
$cats_text .= $cat['id'] . ' - ' . $cat['text'] . "\n";
|
|
}
|
|
}
|
|
|
|
$prompt = 'Dopasuj produkt do najlepszej kategorii Google Product Taxonomy.
|
|
Wybierz najbardziej szczegółową (najgłębszą) kategorię pasującą do produktu.
|
|
|
|
' . $context_text . $cats_text . '
|
|
|
|
Zwróć TYLKO ID kategorii (liczbę), bez wyjaśnień.';
|
|
|
|
return self::call_api( self::$system_prompt, $prompt );
|
|
}
|
|
}
|