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
301 lines
14 KiB
PHP
301 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Core;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class SEOMetaBox {
|
|
|
|
private static $instance = null;
|
|
|
|
public const META_TITLE = '_aksrt_seo_title';
|
|
public const META_KEYWORDS = '_aksrt_seo_keywords';
|
|
public const META_DESCRIPTION = '_aksrt_seo_description';
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
public function init(): void {
|
|
add_action('add_meta_boxes', [$this, 'register_meta_box']);
|
|
add_action('save_post', [$this, 'save_meta_box'], 10, 2);
|
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
|
|
|
// Term SEO (category / post_tag)
|
|
add_action('category_edit_form_fields', [$this, 'render_term_fields'], 20);
|
|
add_action('post_tag_edit_form_fields', [$this, 'render_term_fields'], 20);
|
|
add_action('edited_category', [$this, 'save_term_fields']);
|
|
add_action('edited_post_tag', [$this, 'save_term_fields']);
|
|
}
|
|
|
|
public function register_meta_box(): void {
|
|
foreach (['post', 'page'] as $screen) {
|
|
add_meta_box(
|
|
'aksrt_seo_meta',
|
|
__('独立SEO', 'aksrt'),
|
|
[$this, 'render_meta_box'],
|
|
$screen,
|
|
'normal',
|
|
'high'
|
|
);
|
|
}
|
|
}
|
|
|
|
public function render_meta_box(\WP_Post $post): void {
|
|
wp_nonce_field('aksrt_seo_meta_save', 'aksrt_seo_meta_nonce');
|
|
|
|
$title = (string) get_post_meta($post->ID, self::META_TITLE, true);
|
|
$keywords = (string) get_post_meta($post->ID, self::META_KEYWORDS, true);
|
|
$description = (string) get_post_meta($post->ID, self::META_DESCRIPTION, true);
|
|
|
|
$ai_btn = $this->render_ai_button('post', (int) $post->ID);
|
|
$summary_btn = $this->render_summary_button((int) $post->ID);
|
|
?>
|
|
<div class="aksrt-seo-meta-box">
|
|
<div class="notice notice-warning inline"><p><?php esc_html_e('系统默认调用标题、分类标签、描述等自动生成SEO内容,一般无需单独设置SEO内容;如需覆盖,请在下方自定义。', 'aksrt'); ?></p></div>
|
|
<div class="aksrt-seo-preview">
|
|
<strong><?php esc_html_e('SEO预览', 'aksrt'); ?></strong>
|
|
<div class="aksrt-seo-preview__title"><?php echo esc_html($title !== '' ? $title : get_the_title($post)); ?></div>
|
|
<div class="aksrt-seo-preview__url"><?php echo esc_html(get_permalink($post)); ?></div>
|
|
<div class="aksrt-seo-preview__description"><?php echo esc_html($description !== '' ? $description : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 38, '…')); ?></div>
|
|
</div>
|
|
<p>
|
|
<label for="aksrt_seo_title"><strong><?php esc_html_e('自定义SEO标题', 'aksrt'); ?></strong></label>
|
|
<input type="text" class="widefat" id="aksrt_seo_title" name="aksrt_seo_title" value="<?php echo esc_attr($title); ?>" />
|
|
<span class="description"><?php esc_html_e('Title 一般建议15到30个字符', 'aksrt'); ?></span>
|
|
</p>
|
|
<p>
|
|
<label for="aksrt_seo_keywords"><strong><?php esc_html_e('自定义SEO关键词', 'aksrt'); ?></strong></label>
|
|
<input type="text" class="widefat" id="aksrt_seo_keywords" name="aksrt_seo_keywords" value="<?php echo esc_attr($keywords); ?>" placeholder="<?php esc_attr_e('多个关键词用英文逗号分隔', 'aksrt'); ?>" />
|
|
<span class="description"><?php esc_html_e('Keywords 每个关键词用逗号隔开', 'aksrt'); ?></span>
|
|
</p>
|
|
<p>
|
|
<label for="aksrt_seo_description"><strong><?php esc_html_e('自定义SEO描述', 'aksrt'); ?></strong></label>
|
|
<textarea class="widefat" rows="3" id="aksrt_seo_description" name="aksrt_seo_description"><?php echo esc_textarea($description); ?></textarea>
|
|
<span class="description"><?php esc_html_e('Description 一般建议50到150个字符', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
|
|
</p>
|
|
<details class="aksrt-seo-advice"><summary><span class="dashicons dashicons-star-filled"></span> <?php esc_html_e('SEO优化建议', 'aksrt'); ?></summary><div>
|
|
<p><strong><?php esc_html_e('SEO标题:', 'aksrt'); ?></strong><?php esc_html_e('标题应紧扣主要内容,重要关键词靠前,避免关键词堆砌。', 'aksrt'); ?></p>
|
|
<p><strong><?php esc_html_e('SEO关键词:', 'aksrt'); ?></strong><?php esc_html_e('一般建议4到8个,并与网站定位及正文保持一致。', 'aksrt'); ?></p>
|
|
<p><strong><?php esc_html_e('SEO描述:', 'aksrt'); ?></strong><?php esc_html_e('使用一段通顺、有吸引力的文字精炼概括页面内容。', 'aksrt'); ?></p>
|
|
<p class="description"><?php esc_html_e('优化建议来自互联网,仅供参考。', 'aksrt'); ?></p>
|
|
</div></details>
|
|
<?php if ($summary_btn !== '') : ?>
|
|
<p class="aksrt-ai-summary-row">
|
|
<span class="description"><?php esc_html_e('写作辅助:根据正文生成摘要并填入「摘要」字段(不自动保存)。', 'aksrt'); ?></span>
|
|
<?php echo $summary_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
public function save_meta_box(int $post_id, $post = null): void {
|
|
if (!isset($_POST['aksrt_seo_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['aksrt_seo_meta_nonce'])), 'aksrt_seo_meta_save')) {
|
|
return;
|
|
}
|
|
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
|
return;
|
|
}
|
|
if (!current_user_can('edit_post', $post_id)) {
|
|
return;
|
|
}
|
|
if (wp_is_post_revision($post_id)) {
|
|
return;
|
|
}
|
|
|
|
$map = [
|
|
self::META_TITLE => 'aksrt_seo_title',
|
|
self::META_KEYWORDS => 'aksrt_seo_keywords',
|
|
self::META_DESCRIPTION => 'aksrt_seo_description',
|
|
];
|
|
|
|
foreach ($map as $meta_key => $field) {
|
|
if (!isset($_POST[$field])) {
|
|
continue;
|
|
}
|
|
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
|
|
if ($value === '') {
|
|
delete_post_meta($post_id, $meta_key);
|
|
} else {
|
|
update_post_meta($post_id, $meta_key, $value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param \WP_Term $term
|
|
*/
|
|
public function render_term_fields($term): void {
|
|
if (!$term instanceof \WP_Term) {
|
|
return;
|
|
}
|
|
$title = (string) get_term_meta($term->term_id, self::META_TITLE, true);
|
|
$keywords = (string) get_term_meta($term->term_id, self::META_KEYWORDS, true);
|
|
$description = (string) get_term_meta($term->term_id, self::META_DESCRIPTION, true);
|
|
$ai_btn = $this->render_ai_button('term', (int) $term->term_id);
|
|
?>
|
|
<tr class="form-field">
|
|
<th scope="row"><label for="aksrt_seo_title"><?php esc_html_e('SEO 标题', 'aksrt'); ?></label></th>
|
|
<td>
|
|
<input type="text" name="aksrt_seo_title" id="aksrt_seo_title" class="aksrt-term-seo-title" value="<?php echo esc_attr($title); ?>" />
|
|
</td>
|
|
</tr>
|
|
<tr class="form-field">
|
|
<th scope="row"><label for="aksrt_seo_keywords"><?php esc_html_e('SEO 关键词', 'aksrt'); ?></label></th>
|
|
<td>
|
|
<input type="text" name="aksrt_seo_keywords" id="aksrt_seo_keywords" class="aksrt-term-seo-keywords" value="<?php echo esc_attr($keywords); ?>" />
|
|
</td>
|
|
</tr>
|
|
<tr class="form-field">
|
|
<th scope="row"><label for="aksrt_seo_description"><?php esc_html_e('SEO 描述', 'aksrt'); ?></label></th>
|
|
<td>
|
|
<textarea name="aksrt_seo_description" id="aksrt_seo_description" class="aksrt-term-seo-description" rows="4"><?php echo esc_textarea($description); ?></textarea>
|
|
<p class="description"><?php esc_html_e('主题默认会自动设置 SEO 内容,如需单独设置请在此输入。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
}
|
|
|
|
public function save_term_fields(int $term_id): void {
|
|
if (!current_user_can('edit_term', $term_id)) {
|
|
return;
|
|
}
|
|
|
|
$map = [
|
|
self::META_TITLE => 'aksrt_seo_title',
|
|
self::META_KEYWORDS => 'aksrt_seo_keywords',
|
|
self::META_DESCRIPTION => 'aksrt_seo_description',
|
|
];
|
|
|
|
foreach ($map as $meta_key => $field) {
|
|
if (!isset($_POST[$field])) {
|
|
continue;
|
|
}
|
|
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
|
|
if ($value === '') {
|
|
delete_term_meta($term_id, $meta_key);
|
|
} else {
|
|
update_term_meta($term_id, $meta_key, $value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function enqueue_assets(string $hook): void {
|
|
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
|
|
if (!$screen) {
|
|
return;
|
|
}
|
|
$ok = in_array($screen->base, ['post', 'term', 'edit-tags'], true);
|
|
if (!$ok) {
|
|
return;
|
|
}
|
|
|
|
$path = AKSRT_DIR . '/assets/js/admin-ai-seo.js';
|
|
if (!is_file($path)) {
|
|
return;
|
|
}
|
|
|
|
wp_enqueue_script(
|
|
'aksrt-admin-ai-seo',
|
|
AKSRT_URI . '/assets/js/admin-ai-seo.js',
|
|
[],
|
|
(string) filemtime($path),
|
|
true
|
|
);
|
|
|
|
$ai_available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
|
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
|
|
|
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
|
|
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
|
'nonce' => wp_create_nonce('wp_rest'),
|
|
'ai' => [
|
|
'available' => $ai_available,
|
|
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
|
|
'write_assist' => $ai_available,
|
|
],
|
|
'i18n' => [
|
|
'generating' => __('生成中…', 'aksrt'),
|
|
'generate' => __('AI 生成 SEO', 'aksrt'),
|
|
'generateSummary' => __('AI 生成摘要', 'aksrt'),
|
|
'success' => __('已填入 SEO 字段,请检查后更新保存', 'aksrt'),
|
|
'summarySuccess' => __('已填入摘要字段,请检查后更新保存', 'aksrt'),
|
|
'failed' => __('生成失败', 'aksrt'),
|
|
'needSave' => __('请先保存后再生成 SEO', 'aksrt'),
|
|
'needContent' => __('请先填写正文内容', 'aksrt'),
|
|
'notAvailable' => __('AI 未配置或未启用', 'aksrt'),
|
|
'noExcerptField' => __('未找到摘要字段,请展开「摘要」面板', 'aksrt'),
|
|
],
|
|
]);
|
|
|
|
wp_add_inline_style('wp-admin', '
|
|
.aksrt-ai-seo-button{display:inline-block;margin-left:8px;vertical-align:middle}
|
|
.aksrt-ai-seo-generate,.aksrt-ai-summary-generate{cursor:pointer}
|
|
.aksrt-ai-seo-generate.is-loading,.aksrt-ai-summary-generate.is-loading{opacity:.6;pointer-events:none}
|
|
.aksrt-ai-seo-msg{display:block;margin-top:6px;font-size:12px}
|
|
.aksrt-ai-seo-msg.is-error{color:#b32d2e}
|
|
.aksrt-ai-seo-msg.is-ok{color:#007017}
|
|
.aksrt-ai-summary-row{margin-top:8px;padding-top:8px;border-top:1px solid #eee}
|
|
.aksrt-seo-preview{margin:12px 0;padding:14px;border:1px solid #e5e5e5;border-radius:4px;background:#fff}
|
|
.aksrt-seo-preview__title{margin-top:8px;color:#1a0dab;font-size:18px;line-height:1.3}
|
|
.aksrt-seo-preview__url{margin:4px 0;color:#188038;font-size:13px;word-break:break-all}
|
|
.aksrt-seo-preview__description{color:#4d5156;line-height:1.5}
|
|
.aksrt-seo-advice{margin-top:12px;border:1px solid #e5e5e5;border-radius:4px}
|
|
.aksrt-seo-advice summary{padding:10px 12px;cursor:pointer;font-weight:600}
|
|
.aksrt-seo-advice>div{padding:0 12px 10px}
|
|
');
|
|
}
|
|
|
|
private function render_ai_button(string $type, int $id): string {
|
|
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
|
if (empty($seo['seo_enabled']) || empty($seo['seo_ai_enabled'])) {
|
|
return '';
|
|
}
|
|
|
|
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
|
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
|
|
$title = !$available
|
|
? esc_attr__('请先配置 WordPress AI Connectors', 'aksrt')
|
|
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
|
|
|
|
return sprintf(
|
|
'<span class="aksrt-ai-seo-button"><button type="button" class="button aksrt-ai-seo-generate"%s data-type="%s" data-id="%d" title="%s">%s</button></span>',
|
|
$disabled,
|
|
esc_attr($type),
|
|
$id,
|
|
$title,
|
|
esc_html__('AI 生成 SEO', 'aksrt')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 后台「AI 生成摘要」→ 写入 WP 摘要字段(#excerpt / 块编辑器)。
|
|
*/
|
|
private function render_summary_button(int $post_id): string {
|
|
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
|
|
if (!$available) {
|
|
return '';
|
|
}
|
|
|
|
$disabled = $post_id <= 0 ? ' disabled' : '';
|
|
$title = $post_id <= 0
|
|
? esc_attr__('请先保存后再生成', 'aksrt')
|
|
: esc_attr__('根据正文生成摘要,填入摘要框', 'aksrt');
|
|
|
|
return sprintf(
|
|
'<span class="aksrt-ai-seo-button"><button type="button" class="button aksrt-ai-summary-generate"%s data-post-id="%d" title="%s">%s</button></span>',
|
|
$disabled,
|
|
$post_id,
|
|
$title,
|
|
esc_html__('AI 生成摘要', 'aksrt')
|
|
);
|
|
}
|
|
}
|