Files
wordpress-theme-aksrt/inc/classes/class-aksrt-ai-seo.php
T
kate522 b36186da65 feat(ai): 打通 SEO 闭环与投稿写作辅助
对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
2026-07-12 15:36:04 +08:00

282 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT AI SEO — SEO 字段生成(对齐 Zibll 闭环)
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_AI_SEO {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* SEO AI 功能是否启用。
*/
public function is_enabled(): bool {
if (!class_exists('AKSRT_Settings')) {
return false;
}
$seo = AKSRT_Settings::get_seo_settings();
return !empty($seo['seo_enabled']) && !empty($seo['seo_ai_enabled']);
}
/**
* 从已保存实体生成 SEO 三字段(Zibll 形状)。
*
* @return array{success:bool,data?:array,message?:string}
*/
public function generate_fields(string $type, int $id): array {
if (!$this->is_enabled()) {
return [
'success' => false,
'message' => __('AI SEO 功能未启用', 'aksrt'),
];
}
if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
return [
'success' => false,
'message' => __('AI 模块未加载', 'aksrt'),
];
}
$ai = AKSRT_AI::get_instance();
if (!$ai->is_available()) {
return [
'success' => false,
'message' => __('AI 服务未启用或未配置', 'aksrt'),
];
}
$type = $type === 'term' ? 'term' : 'post';
if ($id <= 0) {
return [
'success' => false,
'message' => __('请先保存后再生成 SEO', 'aksrt'),
];
}
$context = $type === 'term'
? $this->build_term_context($id)
: $this->build_post_context($id);
if (is_wp_error($context)) {
return [
'success' => false,
'message' => $context->get_error_message(),
];
}
$prompt = AKSRT_AI_Prompt::get_instance();
$title = $this->run_field(
$ai,
$context,
$prompt->get_seo_title_system_instruction(),
'title'
);
$keywords = $this->run_field(
$ai,
$context,
$prompt->get_seo_keywords_system_instruction(),
'keywords'
);
$description = $this->run_field(
$ai,
$context,
$prompt->get_seo_description_system_instruction(),
'description'
);
return [
'success' => true,
'data' => [
'title' => $title,
'keywords' => $keywords,
'description' => $description,
],
];
}
/**
* 草稿场景:用传入 title/content 生成(兼容旧 /ai/seo-suggest)。
*/
public function suggest(string $title, string $content = ''): array {
if (!$this->is_enabled()) {
return ['success' => false, 'message' => __('AI SEO 功能未启用', 'aksrt')];
}
if (!class_exists('AKSRT_AI') || !class_exists('AKSRT_AI_Prompt')) {
return ['success' => false, 'message' => __('AI 模块未加载', 'aksrt')];
}
$ai = AKSRT_AI::get_instance();
if (!$ai->is_available()) {
return ['success' => false, 'message' => __('AI 服务未启用', 'aksrt')];
}
$title = trim($title);
$content = trim(wp_strip_all_tags($content));
if ($title === '' && $content === '') {
return [
'success' => false,
'message' => __('无可用内容,请先填写标题或正文', 'aksrt'),
];
}
$parts = [];
if ($title !== '') {
$parts[] = '标题: ' . $title;
}
if ($content !== '') {
$content = wp_trim_words($content, 1500, '');
$parts[] = "正文:\n" . $content;
}
$context = implode("\n\n", $parts);
$prompt = AKSRT_AI_Prompt::get_instance();
$title_out = $this->run_field($ai, $context, $prompt->get_seo_title_system_instruction(), 'title');
$kw_out = $this->run_field($ai, $context, $prompt->get_seo_keywords_system_instruction(), 'keywords');
$desc_out = $this->run_field($ai, $context, $prompt->get_seo_description_system_instruction(), 'description');
$keywords = [];
if (!empty($kw_out['content'])) {
$keywords = array_values(array_filter(array_map('trim', explode(',', $kw_out['content']))));
}
return [
'success' => true,
'data' => [
'suggested_title' => (string) ($title_out['content'] ?? ''),
'suggested_description' => (string) ($desc_out['content'] ?? ''),
'suggested_keywords' => $keywords,
'fields' => [
'title' => $title_out,
'keywords' => $kw_out,
'description' => $desc_out,
],
],
];
}
/**
* @return string|WP_Error
*/
public function build_post_context(int $post_id, int $word_limit = 1500) {
$post = get_post($post_id);
if (!$post) {
return new WP_Error('no_post', __('内容不存在', 'aksrt'));
}
$title = trim((string) $post->post_title);
$excerpt = trim((string) $post->post_excerpt);
$content = wp_strip_all_tags((string) $post->post_content);
$content = trim(preg_replace('/\s+/u', ' ', $content) ?? $content);
$content = wp_trim_words($content, max(50, $word_limit), '');
if ($title === '' && $content === '' && $excerpt === '') {
return new WP_Error('empty_content', __('无可用内容,请先填写标题、摘要或正文并保存', 'aksrt'));
}
$parts = [];
if ($title !== '') {
$parts[] = '标题: ' . $title;
}
if ($excerpt !== '') {
$parts[] = '摘要: ' . $excerpt;
}
if ($content !== '') {
$parts[] = "正文:\n" . $content;
}
return implode("\n\n", $parts);
}
/**
* @return string|WP_Error
*/
public function build_term_context(int $term_id) {
$term = get_term($term_id);
if (!$term || is_wp_error($term)) {
return new WP_Error('no_term', __('分类/标签不存在', 'aksrt'));
}
$parts = [];
if ($term->name !== '') {
$parts[] = '标题: ' . $term->name;
}
if ($term->description !== '') {
$parts[] = '描述: ' . wp_strip_all_tags((string) $term->description);
}
if (empty($parts)) {
return new WP_Error('empty_term', __('没有可用的标题或描述', 'aksrt'));
}
return implode("\n\n", $parts);
}
/**
* @return array{content?:string,error?:string}
*/
private function run_field(AKSRT_AI $ai, string $context, string $system, string $field): array {
$result = $ai->generate_text($context, [
'system_instruction' => $system,
'max_tokens' => $field === 'description' ? 400 : 300,
'temperature' => 0.5,
]);
if (empty($result['success'])) {
return [
'error' => (string) ($result['message'] ?? __('生成失败', 'aksrt')),
];
}
$raw = (string) ($result['content'] ?? '');
return [
'content' => $this->post_process_field($field, $raw),
];
}
private function post_process_field(string $field, string $raw): string {
$raw = trim($raw);
// Strip common wrappers
$raw = preg_replace('/^["\'“”]+|["\'“”]+$/u', '', $raw) ?? $raw;
$raw = trim($raw);
if ($field === 'title') {
$title = sanitize_text_field($raw);
$seo = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_seo_settings() : [];
$conn = trim((string) ($seo['seo_connector'] ?? '-')) ?: '-';
$site = trim((string) get_bloginfo('name'));
if ($site !== '' && $title !== '' && mb_stripos($title, $site) === false) {
$title = $title . ' ' . $conn . ' ' . $site;
}
return $title;
}
if ($field === 'keywords') {
$raw = str_replace(['、', ',', ';', ';', "\n", "\r"], ',', $raw);
$parts = array_map('trim', explode(',', $raw));
$parts = array_values(array_unique(array_filter($parts, static function ($p) {
return $p !== '';
})));
$parts = array_map('sanitize_text_field', $parts);
return implode(',', array_slice($parts, 0, 12));
}
// description
$raw = preg_replace('/\s+/u', ' ', $raw) ?? $raw;
return sanitize_text_field(trim($raw));
}
}