Files
wordpress-theme-aksrt/inc/classes/ai/class-aksrt-ai-seo.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +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));
}
}