Files
wordpress-theme-aksrt/src/Ai/AISEO.php
T
kate522 cdab7a5e5f refactor(admin): drop AI connection config page for WP AI Connectors
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
2026-10-13 19:32:34 +08:00

221 lines
6.6 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
/**
* @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));
}
}