topicBalancer = new TopicBalancer(); $this->openAI = new OpenAIService(); $this->internalLinkService = new InternalLinkService(); $this->imageService = new ImageService(); $this->wordpress = new WordPressService(); } public function publishNext(): array { Logger::info('Rozpoczynam automatyczna publikacje', 'publish'); $sites = Site::findDueForPublishing(); if (empty($sites)) { Logger::info('Brak stron do publikacji', 'publish'); return ['success' => false, 'message' => 'Brak stron wymagajacych publikacji.']; } $site = $sites[0]; return $this->publishForSite($site); } public function publishForSite(array $site): array { Logger::info("Publikacja dla strony: {$site['name']} (ID: {$site['id']})", 'publish'); // 0. Najpierw uzupelnij obrazki w juz opublikowanych artykulach bez miniatury. $publishedWithoutImage = Article::findNextPublishedWithoutImageBySite((int) $site['id']); if ($publishedWithoutImage) { return $this->attachMissingImageToPublishedArticle($site, $publishedWithoutImage); } // 1. Najpierw publikuj gotowe, nieopublikowane artykuly. $retryArticle = Article::findNextRetryableBySite((int) $site['id']); if ($retryArticle) { $topic = Topic::find((int) $retryArticle['topic_id']); if (!$topic) { Logger::error("Nie znaleziono tematu dla artykulu ID {$retryArticle['id']}", 'publish'); return ['success' => false, 'message' => 'Nie znaleziono tematu dla oczekujacego artykulu.']; } Logger::info("Ponowna proba publikacji artykulu ID {$retryArticle['id']}: {$retryArticle['title']}", 'publish'); Article::markRetryAttempt((int) $retryArticle['id']); return $this->publishPreparedArticle( $site, $topic, [ 'title' => (string) $retryArticle['title'], 'content' => (string) $retryArticle['content'], 'model' => $retryArticle['ai_model'] ?? null, 'prompt' => $retryArticle['prompt_used'] ?? null, ], (int) $retryArticle['id'] ); } // 2. Gdy brak zaleglych, generuj nowy artykul. $topic = $this->topicBalancer->getNextTopic($site['id']); if (!$topic) { Logger::error("Brak aktywnych tematow dla strony {$site['name']}", 'publish'); return ['success' => false, 'message' => "Brak aktywnych tematow dla strony {$site['name']}."]; } Logger::info("Wybrany temat: {$topic['name']} (ID: {$topic['id']})", 'publish'); $existingTitles = Article::getRecentTitlesByTopic((int) $topic['id'], 20); $article = $this->openAI->generateArticle( $topic['name'], $topic['description'] ?? '', $existingTitles ); if (!$article) { $this->saveFailedArticle($site, $topic, 'Nie udalo sie wygenerowac artykulu przez OpenAI.'); return ['success' => false, 'message' => 'Blad generowania artykulu przez AI.']; } Logger::info("Wygenerowano artykul: {$article['title']}", 'publish'); $article['title'] = $this->normalizeArticleTitle((string) ($article['title'] ?? ''), (string) $topic['name']); return $this->publishPreparedArticle($site, $topic, $article); } private function publishPreparedArticle(array $site, array $topic, array $article, ?int $existingArticleId = null): array { $linkingResult = $this->internalLinkService->enrichContentWithInternalLinks( $site, (string) ($article['title'] ?? ''), (string) ($article['content'] ?? '') ); $article['content'] = (string) ($linkingResult['content'] ?? (string) ($article['content'] ?? '')); Logger::info( 'Internal linking mode=' . (string) ($linkingResult['mode'] ?? 'unknown') . ', links_added=' . (int) ($linkingResult['links_added'] ?? 0), 'publish' ); $imageUrl = null; $mediaId = null; $image = $this->imageService->generate((string) $article['title'], (string) $topic['name']); if ($image) { $mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']); if ($mediaId) { $imageUrl = 'wp_media:' . $mediaId; Logger::info("Upload obrazka: media_id={$mediaId}", 'publish'); } } else { Logger::warning('Nie udalo sie wygenerowac obrazka, publikacja bez obrazka', 'publish'); } $wpPostId = $this->wordpress->createPost( $site, (string) $article['title'], (string) $article['content'], $topic['wp_category_id'], $mediaId, $this->buildExcerpt((string) $article['content']) ); if (!$wpPostId) { $this->saveFailedArticle( $site, $topic, 'Nie udalo sie opublikowac posta na WordPress.', $article, $existingArticleId ); return ['success' => false, 'message' => 'Blad publikacji na WordPress.']; } Logger::info("Opublikowano post: wp_post_id={$wpPostId}", 'publish'); $wpPostUrl = $this->wordpress->getPostLink($site, (int) $wpPostId); if ($existingArticleId !== null) { Article::update($existingArticleId, [ 'title' => (string) $article['title'], 'content' => (string) $article['content'], 'wp_post_id' => $wpPostId, 'wp_post_url' => $wpPostUrl, 'image_url' => $imageUrl, 'status' => 'published', 'ai_model' => $article['model'] ?? null, 'prompt_used' => $article['prompt'] ?? null, 'error_message' => null, 'published_at' => date('Y-m-d H:i:s'), ]); } else { Article::create([ 'site_id' => $site['id'], 'topic_id' => $topic['id'], 'title' => (string) $article['title'], 'content' => (string) $article['content'], 'wp_post_id' => $wpPostId, 'wp_post_url' => $wpPostUrl, 'image_url' => $imageUrl, 'status' => 'published', 'ai_model' => $article['model'] ?? null, 'prompt_used' => $article['prompt'] ?? null, 'published_at' => date('Y-m-d H:i:s'), ]); } Topic::incrementArticleCount((int) $topic['id']); Site::updateLastPublished((int) $site['id']); $message = "Opublikowano: \"{$article['title']}\" na {$site['name']}"; Logger::info($message, 'publish'); return ['success' => true, 'message' => $message]; } private function normalizeArticleTitle(string $title, string $topicName): string { $title = trim($title); $topicName = trim($topicName); if ($title === '' || $topicName === '') { return $title; } // Remove ": " / " - " prefixes from generated titles. $topicPattern = preg_quote($topicName, '/'); $normalized = preg_replace('/^' . $topicPattern . '\s*[:\-\x{2013}\x{2014}|]\s*/iu', '', $title); $normalized = is_string($normalized) ? trim($normalized) : $title; return $normalized !== '' ? $normalized : $title; } private function saveFailedArticle( array $site, array $topic, string $error, ?array $article = null, ?int $existingArticleId = null ): void { if ($existingArticleId !== null) { Article::update($existingArticleId, [ 'title' => $article['title'] ?? 'FAILED - nie wygenerowano', 'content' => $article['content'] ?? '', 'status' => 'failed', 'ai_model' => $article['model'] ?? null, 'prompt_used' => $article['prompt'] ?? null, 'error_message' => $error, ]); } else { Article::create([ 'site_id' => $site['id'], 'topic_id' => $topic['id'], 'title' => $article['title'] ?? 'FAILED - nie wygenerowano', 'content' => $article['content'] ?? '', 'status' => 'failed', 'ai_model' => $article['model'] ?? null, 'prompt_used' => $article['prompt'] ?? null, 'error_message' => $error, ]); } Logger::error("Publikacja nieudana: {$error}", 'publish'); } private function attachMissingImageToPublishedArticle(array $site, array $article): array { $articleId = (int) $article['id']; $wpPostId = (int) ($article['wp_post_id'] ?? 0); if ($wpPostId <= 0) { Logger::warning("Artykul ID {$articleId} oznaczony jako published bez wp_post_id - pomijam", 'publish'); return ['success' => false, 'message' => 'Brak wp_post_id dla opublikowanego artykulu.']; } $existingFeaturedMediaId = $this->wordpress->getPostFeaturedMedia($site, $wpPostId); if ($existingFeaturedMediaId !== null) { Article::update($articleId, ['image_url' => 'wp_media:' . $existingFeaturedMediaId]); $message = "Artykul ID {$articleId} juz mial miniaturke (media_id={$existingFeaturedMediaId}) - zaktualizowano marker lokalny."; Logger::info($message, 'publish'); return ['success' => true, 'message' => $message]; } $topic = Topic::find((int) ($article['topic_id'] ?? 0)); $topicName = $topic['name'] ?? (string) $article['title']; $title = (string) ($article['title'] ?? 'Artykul bez tytulu'); Article::markRetryAttempt($articleId); Logger::info("Proba uzupelnienia obrazka dla artykulu ID {$articleId}: {$title}", 'publish'); $image = $this->imageService->generate($title, (string) $topicName); if (!$image) { $message = "Nie udalo sie wygenerowac obrazka dla opublikowanego artykulu ID {$articleId}."; Logger::warning($message, 'publish'); return ['success' => false, 'message' => $message]; } $mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']); if (!$mediaId) { $message = "Nie udalo sie wyslac obrazka do WordPress dla artykulu ID {$articleId}."; Logger::warning($message, 'publish'); return ['success' => false, 'message' => $message]; } $updated = $this->wordpress->updatePostFeaturedMedia($site, $wpPostId, $mediaId); if (!$updated) { $this->wordpress->deleteMedia($site, $mediaId); $message = "Nie udalo sie podpiac miniaturki (media_id={$mediaId}) do wpisu wp_post_id={$wpPostId}."; Logger::warning($message, 'publish'); return ['success' => false, 'message' => $message]; } Article::update($articleId, [ 'image_url' => 'wp_media:' . $mediaId, 'error_message' => null, ]); $message = "Uzupelniono miniaturke dla artykulu ID {$articleId} (wp_post_id={$wpPostId}, media_id={$mediaId})."; Logger::info($message, 'publish'); return ['success' => true, 'message' => $message]; } private function buildExcerpt(string $htmlContent): string { $plain = trim((string) preg_replace('/\s+/u', ' ', strip_tags($htmlContent))); if ($plain === '') { return ''; } $maxLength = 155; if (mb_strlen($plain) <= $maxLength) { return $plain; } $cut = mb_substr($plain, 0, $maxLength + 1); $lastSpace = mb_strrpos($cut, ' '); if ($lastSpace !== false && $lastSpace > 80) { $cut = mb_substr($cut, 0, $lastSpace); } else { $cut = mb_substr($cut, 0, $maxLength); } return rtrim($cut, " \t\n\r\0\x0B.,;:!?") . '.'; } }