diff --git a/assets/admin/src/constants/sections/extensions.js b/assets/admin/src/constants/sections/extensions.js index 2cc99dd7..51701bf1 100644 --- a/assets/admin/src/constants/sections/extensions.js +++ b/assets/admin/src/constants/sections/extensions.js @@ -42,7 +42,7 @@ export const extensionsSection = { label: '百度推送', route: '/extensions/push', title: '百度推送', - desc: '配置搜索引擎主动推送、批量提交和状态追踪。', + desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。', canonicalDomain: 'extensions', section: 'push', }), diff --git a/assets/admin/src/schema/sections/extensions/push.js b/assets/admin/src/schema/sections/extensions/push.js index 4e49358b..a16858f9 100644 --- a/assets/admin/src/schema/sections/extensions/push.js +++ b/assets/admin/src/schema/sections/extensions/push.js @@ -2,21 +2,16 @@ export const pushSchema = { key: 'extensions.push', title: '百度推送', icon: 'fa fas fa-paper-plane', - description: '配置搜索引擎主动推送、批量提交和状态追踪。', + description: '配置百度主动推送、收录通道和推送状态。', defaults: { baidu_push_enabled: false, baidu_push_token: '', baidu_push_auto: true, baidu_push_on_publish: true, baidu_push_on_update: false, - baidu_push_batch_size: 50, + baidu_push_normal: true, + baidu_push_fast: false, baidu_push_log_enabled: true, - xzh_on: false, - xzh_appid: '', - xzh_render_tail: true, - xzh_jsonld_single: true, - xzh_jsonld_page: false, - xzh_jsonld_img: false, }, groups: [ { @@ -25,7 +20,7 @@ export const pushSchema = { icon: 'fa fas fa-rocket', fields: [ { key: 'baidu_push_enabled', label: '启用百度推送', type: 'switch', default: false, help: '开启后将自动向百度搜索推送页面链接' }, - { key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token' }, + { key: 'baidu_push_token', label: '推送 Token', type: 'input', default: '', dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在百度搜索资源平台获取的推送准入 Token(仅填 Token,勿填完整链接),并确保平台中的站点为本站' }, ], }, { @@ -34,21 +29,17 @@ export const pushSchema = { icon: 'fa fas fa-bolt', fields: [ { key: 'baidu_push_auto', label: '自动推送 (页面加载时)', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '在前台页面中嵌入百度自动推送 JS 代码' }, - { key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } }, - { key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true } }, + { key: 'baidu_push_on_publish', label: '发布时主动推送', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '文章 / 分类首次发布时推送链接;推送成功后会记录,不再重复提交' }, + { key: 'baidu_push_on_update', label: '更新时推送', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '已发布内容再次保存时,若上次推送未成功则重试;需主动再次提交请在编辑页勾选「重新提交」' }, ], }, { - key: 'xzh', - title: '熊掌号 / JSON-LD', - icon: 'fa fas fa-paw', + key: 'channel', + title: '收录通道', + icon: 'fa fas fa-inbox', fields: [ - { key: 'xzh_on', label: '启用熊掌号', type: 'switch', default: false }, - { key: 'xzh_appid', label: '百度 AppID', type: 'input', default: '', dependsOn: { key: 'xzh_on', value: true } }, - { key: 'xzh_render_tail', label: '文章底部显示熊掌号尾部', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } }, - { key: 'xzh_jsonld_single', label: '文章页 JSON-LD', type: 'switch', default: true, dependsOn: { key: 'xzh_on', value: true } }, - { key: 'xzh_jsonld_page', label: '页面 JSON-LD', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } }, - { key: 'xzh_jsonld_img', label: '不添加图片', type: 'switch', default: false, dependsOn: { key: 'xzh_on', value: true } }, + { key: 'baidu_push_normal', label: '普通收录', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '每天可提交最多10万条有价值内容,收录速度较慢' }, + { key: 'baidu_push_fast', label: '快速收录', type: 'switch', default: false, dependsOn: { key: 'baidu_push_enabled', value: true }, help: '百度新推出的高效收录接口,目前仅对部分优质站点开放,请确保您的站点已开放快速收录功能' }, ], }, { @@ -56,8 +47,7 @@ export const pushSchema = { title: '高级设置', icon: 'fa fas fa-cog', fields: [ - { key: 'baidu_push_batch_size', label: '批量推送条数', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'baidu_push_enabled', value: true } }, - { key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true } }, + { key: 'baidu_push_log_enabled', label: '记录推送日志', type: 'switch', default: true, dependsOn: { key: 'baidu_push_enabled', value: true }, help: 'WP_DEBUG 开启时,将推送结果写入错误日志' }, ], }, ], diff --git a/assets/admin/src/schema/sections/global/seo.js b/assets/admin/src/schema/sections/global/seo.js index bc4db31a..c55f07bc 100644 --- a/assets/admin/src/schema/sections/global/seo.js +++ b/assets/admin/src/schema/sections/global/seo.js @@ -13,10 +13,6 @@ export const seoSchema = { ai_seo_title_prompt: '', ai_seo_keywords_prompt: '', ai_seo_description_prompt: '', - seo_baidu_push: false, - seo_baidu_normal: false, - seo_baidu_fast: false, - seo_baidu_token: '', seo_image_optimize: true, seo_image_default: '', seo_image_always_default: false, @@ -95,17 +91,6 @@ export const seoSchema = { }, ], }, - { - key: 'baidu', - title: '百度推送', - icon: 'fa fas fa-paper-plane', - fields: [ - { key: 'seo_baidu_push', label: '启用百度自动推送', type: 'switch', default: false }, - { key: 'seo_baidu_normal', label: '普通收录推送', type: 'switch', default: false }, - { key: 'seo_baidu_fast', label: '快速收录推送', type: 'switch', default: false }, - { key: 'seo_baidu_token', label: '百度推送 Token', type: 'input', default: '', placeholder: '百度资源平台 Token' }, - ], - }, { key: 'image', title: '图片 SEO', diff --git a/functions.php b/functions.php index adc33850..a141b09f 100644 --- a/functions.php +++ b/functions.php @@ -295,6 +295,9 @@ if (class_exists('\AKSRT\Extensions\MeiliSearch')) { if (class_exists('\AKSRT\Extensions\SearchPush')) { \AKSRT\Extensions\SearchPush::get_instance()->init(); } +if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) { + \AKSRT\Extensions\SearchPushMetaBox::get_instance()->init(); +} if (class_exists('\AKSRT\Core\SEOHead')) { \AKSRT\Core\SEOHead::get_instance()->init(); } diff --git a/src/Admin/Schema/GlobalSchema.php b/src/Admin/Schema/GlobalSchema.php index c17e2005..6073c830 100644 --- a/src/Admin/Schema/GlobalSchema.php +++ b/src/Admin/Schema/GlobalSchema.php @@ -79,12 +79,6 @@ class GlobalSchema { ['key' => 'ai_seo_keywords_prompt', 'label' => '关键词提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'], ['key' => 'ai_seo_description_prompt', 'label' => '描述提示词', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'seo_ai_enabled', 'value' => true], 'help' => '留空使用内置默认。'], ]], - ['key' => 'baidu', 'title' => '百度SEO', 'fields' => [ - ['key' => 'seo_baidu_push', 'label' => '百度链接自动提交', 'type' => 'switch', 'default' => false], - ['key' => 'seo_baidu_normal', 'label' => '百度普通收录', 'type' => 'switch', 'default' => false], - ['key' => 'seo_baidu_fast', 'label' => '百度快速收录', 'type' => 'switch', 'default' => false], - ['key' => 'seo_baidu_token', 'label' => '百度准入密钥Token', 'type' => 'input', 'default' => ''], - ]], ['key' => 'image', 'title' => '图片SEO', 'fields' => [ ['key' => 'seo_image_optimize', 'label' => '搜索引擎图片展示优化', 'type' => 'switch', 'default' => true], ['key' => 'seo_image_default', 'label' => '默认图像', 'type' => 'media', 'default' => ''], diff --git a/src/Admin/Schema/SchemaExtensionsPush.php b/src/Admin/Schema/SchemaExtensionsPush.php index 6174dafb..98cda411 100644 --- a/src/Admin/Schema/SchemaExtensionsPush.php +++ b/src/Admin/Schema/SchemaExtensionsPush.php @@ -25,16 +25,11 @@ class SchemaExtensionsPush { ['key' => 'baidu_push_on_publish', 'label' => '发布时主动推送', 'type' => 'switch', 'default' => true], ['key' => 'baidu_push_on_update', 'label' => '更新时推送', 'type' => 'switch', 'default' => false], ]], - ['key' => 'xzh', 'title' => '熊掌号 / JSON-LD', 'fields' => [ - ['key' => 'xzh_on', 'label' => '启用熊掌号', 'type' => 'switch', 'default' => false], - ['key' => 'xzh_appid', 'label' => '百度 AppID', 'type' => 'input', 'default' => ''], - ['key' => 'xzh_render_tail', 'label' => '文章底部显示熊掌号尾部', 'type' => 'switch', 'default' => true], - ['key' => 'xzh_jsonld_single', 'label' => '文章页 JSON-LD', 'type' => 'switch', 'default' => true], - ['key' => 'xzh_jsonld_page', 'label' => '页面 JSON-LD', 'type' => 'switch', 'default' => false], - ['key' => 'xzh_jsonld_img', 'label' => '不添加图片', 'type' => 'switch', 'default' => false], + ['key' => 'channel', 'title' => '收录通道', 'fields' => [ + ['key' => 'baidu_push_normal', 'label' => '普通收录', 'type' => 'switch', 'default' => true], + ['key' => 'baidu_push_fast', 'label' => '快速收录', 'type' => 'switch', 'default' => false], ]], ['key' => 'advanced', 'title' => '高级设置', 'fields' => [ - ['key' => 'baidu_push_batch_size', 'label' => '批量推送条数', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500], ['key' => 'baidu_push_log_enabled', 'label' => '记录推送日志', 'type' => 'switch', 'default' => true], ]], ], diff --git a/src/Extensions/SearchPush.php b/src/Extensions/SearchPush.php index 843f40aa..3d4c1c83 100644 --- a/src/Extensions/SearchPush.php +++ b/src/Extensions/SearchPush.php @@ -9,6 +9,14 @@ defined('ABSPATH') || exit; class SearchPush { + /** 推送结果 meta 键(编辑页面板与去重逻辑共用)。 */ + public const META_KEY = '_aksrt_baidu_push_back'; + + /** 「重新提交」nonce 与复选框字段名(与 SearchPushMetaBox 共用)。 */ + public const NONCE_ACTION = 'aksrt_push_meta_save'; + public const NONCE_FIELD = 'aksrt_push_meta_nonce'; + public const RESUBMIT_FIELD = 'aksrt_push_resubmit'; + private static $instance = null; public static function get_instance(): self { @@ -17,16 +25,20 @@ class SearchPush { } /** - * 绑定运行时钩子:发布/更新主动推送 + 前台自动推送 JS。 + * 绑定运行时钩子:发布/更新主动推送 + 分类法保存推送 + 前台自动推送 JS。 */ public function init(): void { add_action('transition_post_status', [$this, 'on_transition_post_status'], 10, 3); + add_action('saved_term', [$this, 'on_saved_term'], 10, 3); add_action('wp_footer', [$this, 'output_auto_push_script'], 99); } /** * 文章发布/更新时主动推送链接到百度。 * + * 与参考主题一致:推送结果写入 meta,成功后不再重复提交; + * 编辑页勾选「重新提交」可清空记录并强制再次推送。 + * * @param string $new_status 新状态。 * @param string $old_status 旧状态。 * @param \WP_Post $post 文章对象。 @@ -42,27 +54,230 @@ class SearchPush { return; } // 仅推送公开可索引的内容类型。 - $allowed = function_exists('aksrt_searchable_post_types') - ? aksrt_searchable_post_types() - : ['post']; - if (!in_array($post->post_type, $allowed, true)) { + if (!in_array($post->post_type, self::pushable_post_types(), true)) { return; } $config = $this->get_config(); + if (!$this->should_push($config)) { + return; + } + + if ($this->resubmit_requested()) { + self::clear_push_record($post->ID); + } else { + $is_new = $old_status !== 'publish'; + if ($is_new && empty($config['baidu_push_on_publish'])) { + return; + } + if (!$is_new && empty($config['baidu_push_on_update'])) { + return; + } + // 已成功提交过的链接不再重复推送。 + if ($this->record_is_complete(self::get_push_record($post->ID), $config)) { + return; + } + } + + $this->push_and_record((string) get_permalink($post), $post->ID, false, $config); + } + + /** + * 分类法术语保存时主动推送链接到百度。 + * + * 术语无发布/更新状态,沿用参考主题的 saved_term 触发, + * 与文章推送共用 meta 记录、去重与「重新提交」。 + * + * @param int $term_id 术语 ID。 + * @param int $tt_id 术语分类法 ID。 + * @param string $taxonomy 分类法名称。 + */ + public function on_saved_term(int $term_id, int $tt_id, string $taxonomy): void { + if (!in_array($taxonomy, self::pushable_taxonomies(), true)) { + return; + } + + $config = $this->get_config(); + if (!$this->should_push($config)) { + return; + } + + if ($this->resubmit_requested()) { + self::clear_push_record($term_id, true); + } elseif ($this->record_is_complete(self::get_push_record($term_id, true), $config)) { + return; + } + + $url = get_term_link($term_id, $taxonomy); + if (is_wp_error($url) || $url === '') { + return; + } + + $this->push_and_record((string) $url, $term_id, true, $config); + } + + /** + * 可推送的文章类型(与站内搜索 / MeiliSearch 索引共用同一策略)。 + * + * @return string[] + */ + public static function pushable_post_types(): array { + $types = function_exists('aksrt_searchable_post_types') ? aksrt_searchable_post_types() : ['post']; + + return array_values(array_unique(array_filter((array) $types))); + } + + /** + * 可推送的分类法:可推送文章类型上的公开分类法(排除文章形式等)。 + * + * @return string[] + */ + public static function pushable_taxonomies(): array { + $taxonomies = get_object_taxonomies(self::pushable_post_types(), 'names'); + $taxonomies = array_diff((array) $taxonomies, ['post_format', 'nav_menu']); + + /** + * 过滤百度推送覆盖的分类法。 + * + * @param string[] $taxonomies 分类法 slug 列表。 + */ + return array_values(array_unique(apply_filters('aksrt_baidu_push_taxonomies', $taxonomies))); + } + + /** + * 推送功能是否已启用且配置完整(决定编辑页面板是否展示)。 + */ + public function is_ready(): bool { + return $this->should_push($this->get_config()); + } + + /** + * 读取文章或术语的推送记录。 + * + * @param int $object_id 文章 / 术语 ID。 + * @param bool $is_term 是否为术语。 + * @return array 记录为空时返回 []。 + */ + public static function get_push_record(int $object_id, bool $is_term = false): array { + $record = $is_term ? get_term_meta($object_id, self::META_KEY, true) : get_post_meta($object_id, self::META_KEY, true); + + return is_array($record) ? $record : []; + } + + /** + * 写入推送记录。 + * + * @param int $object_id 文章 / 术语 ID。 + * @param array $record 推送记录。 + * @param bool $is_term 是否为术语。 + */ + public static function save_push_record(int $object_id, array $record, bool $is_term = false): void { + if ($is_term) { + update_term_meta($object_id, self::META_KEY, $record); + } else { + update_post_meta($object_id, self::META_KEY, $record); + } + } + + /** + * 清除推送记录(「重新提交」时调用)。 + * + * @param int $object_id 文章 / 术语 ID。 + * @param bool $is_term 是否为术语。 + */ + public static function clear_push_record(int $object_id, bool $is_term = false): void { + if ($is_term) { + delete_term_meta($object_id, self::META_KEY); + } else { + delete_post_meta($object_id, self::META_KEY); + } + } + + /** + * 推送是否可用:总开关开启、Token 已配置且至少启用一个收录通道。 + * + * @param array $config 推送配置。 + */ + private function should_push(array $config): bool { if (empty($config['baidu_push_enabled'])) { + return false; + } + if ((string) ($config['baidu_push_token'] ?? '') === '') { + return false; + } + + return !empty($config['baidu_push_normal']) || !empty($config['baidu_push_fast']); + } + + /** + * 推送 URL 并把结果写入 meta(成功记录用于去重)。 + * + * @param string $url 待推送链接。 + * @param int $object_id 文章 / 术语 ID。 + * @param bool $is_term 是否为术语。 + * @param array $config 推送配置。 + */ + private function push_and_record(string $url, int $object_id, bool $is_term, array $config): void { + if ($url === '') { return; } - $is_new = $old_status !== 'publish'; - if ($is_new && empty($config['baidu_push_on_publish'])) { - return; + $outcome = $this->push_url($url); + self::save_push_record($object_id, $this->build_record($outcome, $config), $is_term); + } + + /** + * 组装推送记录:通道结果 + 更新时间(结构与参考主题 meta 对齐)。 + * + * @param array $outcome push_url() 返回值。 + * @param array $config 推送配置。 + */ + private function build_record(array $outcome, array $config): array { + $record = ['update_time' => current_time('Y-m-d H:i:s')]; + $result = isset($outcome['result']) && is_array($outcome['result']) ? $outcome['result'] : []; + + if (!empty($config['baidu_push_normal'])) { + $channel = $result['normal'] ?? null; + $record['normal_push'] = is_array($channel) && !empty($channel['success']); + $record['normal_result'] = $channel; } - if (!$is_new && empty($config['baidu_push_on_update'])) { - return; + if (!empty($config['baidu_push_fast'])) { + $channel = $result['daily'] ?? null; + $record['daily_push'] = is_array($channel) && !empty($channel['success']); + $record['daily_result'] = $channel; } - $this->push_url((string) get_permalink($post)); + return $record; + } + + /** + * 推送记录是否已覆盖全部启用通道(去重判断)。 + * + * @param array $record 推送记录。 + * @param array $config 推送配置。 + */ + private function record_is_complete(array $record, array $config): bool { + if (!empty($config['baidu_push_normal']) && empty($record['normal_push'])) { + return false; + } + if (!empty($config['baidu_push_fast']) && empty($record['daily_push'])) { + return false; + } + + return true; + } + + /** + * 编辑页是否勾选了「重新提交」(需通过 metabox nonce 校验)。 + */ + private function resubmit_requested(): bool { + if (empty($_POST[self::RESUBMIT_FIELD])) { + return false; + } + + $nonce = isset($_POST[self::NONCE_FIELD]) ? sanitize_text_field(wp_unslash($_POST[self::NONCE_FIELD])) : ''; + + return (bool) wp_verify_nonce($nonce, self::NONCE_ACTION); } /** @@ -96,7 +311,50 @@ class SearchPush { return ['success' => false, 'message' => __('推送 Token 未配置', 'aksrt'), 'result' => []]; } - $api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}"; + // 收录通道与百度接口 type 参数对应:普通收录不带参数,快速收录为 daily。 + $channels = []; + if (!empty($config['baidu_push_normal'])) { + $channels['normal'] = ''; + } + if (!empty($config['baidu_push_fast'])) { + $channels['daily'] = '&type=daily'; + } + if (!$channels) { + return ['success' => false, 'message' => __('未启用普通收录或快速收录通道', 'aksrt'), 'result' => []]; + } + + $success = false; + $results = []; + $messages = []; + + foreach ($channels as $channel => $suffix) { + $outcome = $this->submit($url, $token, $suffix, $config); + $results[$channel] = $outcome['result']; + if ($outcome['success']) { + $success = true; + } else { + $messages[] = $outcome['message']; + } + } + + if ($success) { + return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $results]; + } + + return ['success' => false, 'message' => implode('; ', $messages), 'result' => $results]; + } + + /** + * 向百度链接提交接口发送单条 URL。 + * + * @param string $url 待推送的完整 URL。 + * @param string $token 百度准入密钥。 + * @param string $suffix 接口附加参数(快速收录为 &type=daily)。 + * @param array $config 推送配置。 + * @return array{success: bool, message: string, result: array} + */ + private function submit(string $url, string $token, string $suffix, array $config): array { + $api_url = 'http://data.zz.baidu.com/urls?site=' . rawurlencode(home_url('/')) . '&token=' . $token . $suffix; $response = wp_remote_post($api_url, [ 'timeout' => 10, @@ -105,7 +363,7 @@ class SearchPush { ]); if (is_wp_error($response)) { - $this->log('error', $url, $response->get_error_message()); + $this->log($config, 'error', $url, $response->get_error_message()); return ['success' => false, 'message' => $response->get_error_message(), 'result' => []]; } @@ -113,59 +371,15 @@ class SearchPush { $status = wp_remote_retrieve_response_code($response); if ($status === 200 && isset($body['success'])) { - $this->log('success', $url, wp_json_encode($body)); + $this->log($config, 'success', $url, wp_json_encode($body)); return ['success' => true, 'message' => __('推送成功', 'aksrt'), 'result' => $body]; } $err_msg = $body['message'] ?? ($body['error'] ?? sprintf('HTTP %d', $status)); - $this->log('error', $url, $err_msg); + $this->log($config, 'error', $url, $err_msg); return ['success' => false, 'message' => $err_msg, 'result' => $body ?? []]; } - /** - * 批量推送 URL - * - * @param string[] $urls - * @return array{success: int, failed: int, results: array} - */ - public function push_batch(array $urls): array { - $config = $this->get_config(); - $batch_size = max(1, min(500, (int) ($config['baidu_push_batch_size'] ?? 50))); - $success = 0; - $failed = 0; - $results = []; - - $chunks = array_chunk($urls, $batch_size); - foreach ($chunks as $chunk) { - $token = (string) ($config['baidu_push_token'] ?? ''); - $api_url = "http://data.zz.baidu.com/urls?site=" . rawurlencode(home_url('/')) . "&token={$token}"; - $body_str = implode("\n", $chunk); - - $response = wp_remote_post($api_url, [ - 'timeout' => 15, - 'headers' => ['Content-Type' => 'text/plain'], - 'body' => $body_str, - ]); - - if (is_wp_error($response)) { - $failed += count($chunk); - $results[] = ['error' => $response->get_error_message()]; - continue; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - if (isset($body['success'])) { - $success += (int) ($body['success'] ?? 0); - $failed += (int) ($body['not_same_site'] ?? 0) + (int) ($body['not_valid'] ?? 0); - } else { - $failed += count($chunk); - } - $results[] = $body ?? []; - } - - return ['success' => $success, 'failed' => $failed, 'results' => $results]; - } - /** * 获取自动推送 JS 代码 */ @@ -175,7 +389,6 @@ class SearchPush { return ''; } - $site_url = home_url('/'); return <<