Remove the "AI 连接配置" page under 扩展&增强 together with the theme's own OpenAI-compatible provider path. AI generation (SEO, summary, tags) now runs solely on the WordPress AI Client (wp_ai_client_prompt), with connections configured through WordPress AI Connectors. - AI.php keeps only the WP AI Client path: no provider config storage, no theme fallback, no rate limiting - Drop /ai/status, /ai/settings and legacy /ai/seo-suggest routes plus their handlers - Remove dead compat code: AISEO::suggest, AIPrompt::build_seo_prompt and the front-submit AI assist flow (its buttons were never rendered and its endpoints were never registered) - Update SEO schema/meta-box guidance that pointed at the deleted page - Add ai-config-removal contract test and refresh the toast and user-features contracts affected by the cleanup
221 lines
6.6 KiB
PHP
221 lines
6.6 KiB
PHP
<?php
|
||
/**
|
||
* @package AKSRT
|
||
*/
|
||
|
||
namespace AKSRT\Ai;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class AISEO {
|
||
|
||
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\Settings')) {
|
||
return false;
|
||
}
|
||
$seo = \AKSRT\Settings\Settings::get_seo_settings();
|
||
return !empty($seo['seo_enabled']) && !empty($seo['seo_ai_enabled']);
|
||
}
|
||
|
||
/**
|
||
* 从已保存实体生成 SEO 三字段(legacy 形状)。
|
||
*
|
||
* @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\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
||
return [
|
||
'success' => false,
|
||
'message' => __('AI 模块未加载', 'aksrt'),
|
||
];
|
||
}
|
||
|
||
$ai = 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 = AIPrompt::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,
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @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(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\Settings') ? \AKSRT\Settings\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));
|
||
}
|
||
}
|