wpmind/modules/geo/includes/EntityLinker.php
LinuxJoy c7108ed376 feat: GEO 模块 v3.10 — 3 个新功能 + 设置页子导航重构
新增组件:
- RobotsTxtManager: robots.txt AI 爬虫管理 (15 个爬虫 Allow/Disallow)
- AiSummaryManager: AI 摘要编辑器字段 + meta 标签 + Schema.org abstract
- EntityLinker: Wikidata 实体关联 + Schema.org about.sameAs

设置页重构:
- 平铺 section 改为 5 个 pill 子导航 (基础/内容/结构化数据/AI索引/爬虫管理)
- 右栏爬虫统计仅在基础设置 tab 显示,其他 tab 全宽布局
- 子 tab 状态通过 sessionStorage 保持

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 15:02:58 +08:00

222 lines
5.9 KiB
PHP
Raw Permalink 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
/**
* Entity Linker
*
* Links posts to Wikidata entities for semantic disambiguation.
*
* @package WPMind\Modules\Geo
* @since 3.10.0
*/
declare(strict_types=1);
namespace WPMind\Modules\Geo;
/**
* Class EntityLinker
*
* Adds Wikidata entity association to posts and enriches
* Schema.org JSON-LD with about/sameAs properties.
*/
class EntityLinker {
/**
* Post meta keys.
*/
private const META_ENTITY_URL = '_wpmind_entity_url';
private const META_ENTITY_NAME = '_wpmind_entity_name';
/**
* Nonce action.
*/
private const NONCE_ACTION = 'wpmind_entity_linker_metabox';
/**
* Constructor.
*/
public function __construct() {
add_action( 'add_meta_boxes', [ $this, 'register_metabox' ] );
add_action( 'save_post', [ $this, 'save_metabox' ] );
add_filter( 'wpmind_article_schema', [ $this, 'enrich_schema' ], 20, 2 );
}
/**
* Enrich Article schema with entity about/sameAs.
*
* @param array $schema Schema data.
* @param \WP_Post $post Post object.
* @return array Modified schema.
*/
public function enrich_schema( array $schema, \WP_Post $post ): array {
$entity_url = get_post_meta( $post->ID, self::META_ENTITY_URL, true );
$entity_name = get_post_meta( $post->ID, self::META_ENTITY_NAME, true );
if ( empty( $entity_url ) || ! is_string( $entity_url ) ) {
return $schema;
}
$about = [
'@type' => 'Thing',
];
if ( ! empty( $entity_name ) && is_string( $entity_name ) ) {
$about['name'] = $entity_name;
}
// Convert Wikipedia URL to Wikidata sameAs if needed.
$about['sameAs'] = $entity_url;
// If it's a Wikipedia URL, also add the Wikidata concept URI.
$wikidata_url = $this->to_wikidata_url( $entity_url );
if ( $wikidata_url && $wikidata_url !== $entity_url ) {
$about['sameAs'] = [ $entity_url, $wikidata_url ];
}
$schema['about'] = $about;
return $schema;
}
/**
* Convert a Wikipedia URL to its Wikidata equivalent.
*
* @param string $url Wikipedia or Wikidata URL.
* @return string|null Wikidata URL or null.
*/
private function to_wikidata_url( string $url ): ?string {
// Already a Wikidata URL.
if ( str_contains( $url, 'wikidata.org' ) ) {
return $url;
}
// Not a Wikipedia URL — return null.
if ( ! str_contains( $url, 'wikipedia.org' ) ) {
return null;
}
// Extract article title from Wikipedia URL.
// e.g. https://en.wikipedia.org/wiki/WordPress → WordPress
if ( preg_match( '#wikipedia\.org/wiki/(.+)$#', $url, $matches ) ) {
$title = urldecode( $matches[1] );
// We can't resolve to Wikidata Q-ID without an API call,
// so just return the Wikipedia URL as the canonical sameAs.
return null;
}
return null;
}
/**
* Get entity data for a post.
*
* @param int $post_id Post ID.
* @return array{url: string, name: string} Entity data.
*/
public function get_entity( int $post_id ): array {
return [
'url' => (string) get_post_meta( $post_id, self::META_ENTITY_URL, true ),
'name' => (string) get_post_meta( $post_id, self::META_ENTITY_NAME, true ),
];
}
/**
* Register metabox on public post types.
*/
public function register_metabox(): void {
$post_types = get_post_types( [ 'public' => true ], 'names' );
foreach ( $post_types as $post_type ) {
if ( 'attachment' === $post_type ) {
continue;
}
add_meta_box(
'wpmind-entity-linker',
__( '实体关联', 'wpmind' ),
[ $this, 'render_metabox' ],
$post_type,
'side',
'default'
);
}
}
/**
* Render metabox UI.
*
* @param \WP_Post $post Current post.
*/
public function render_metabox( \WP_Post $post ): void {
wp_nonce_field( self::NONCE_ACTION, '_wpmind_entity_linker_nonce' );
$entity_url = get_post_meta( $post->ID, self::META_ENTITY_URL, true );
$entity_name = get_post_meta( $post->ID, self::META_ENTITY_NAME, true );
if ( ! is_string( $entity_url ) ) {
$entity_url = '';
}
if ( ! is_string( $entity_name ) ) {
$entity_name = '';
}
?>
<div class="wpmind-entity-linker-metabox">
<p class="description" style="margin-bottom:8px;">
<?php esc_html_e( '关联 Wikidata/Wikipedia 实体,帮助 AI 消除歧义。', 'wpmind' ); ?>
</p>
<p>
<label for="wpmind_entity_name"><?php esc_html_e( '实体名称:', 'wpmind' ); ?></label>
<input type="text" name="wpmind_entity_name" id="wpmind_entity_name"
value="<?php echo esc_attr( $entity_name ); ?>"
placeholder="<?php esc_attr_e( '例如WordPress', 'wpmind' ); ?>"
style="width:100%;margin-top:4px;">
</p>
<p>
<label for="wpmind_entity_url"><?php esc_html_e( '实体 URL', 'wpmind' ); ?></label>
<input type="url" name="wpmind_entity_url" id="wpmind_entity_url"
value="<?php echo esc_attr( $entity_url ); ?>"
placeholder="<?php esc_attr_e( 'https://www.wikidata.org/wiki/Q13166 或 Wikipedia URL', 'wpmind' ); ?>"
style="width:100%;margin-top:4px;">
</p>
<p class="description">
<?php esc_html_e( '输入 Wikidata 或 Wikipedia 链接,将作为 Schema.org about.sameAs 输出。', 'wpmind' ); ?>
</p>
</div>
<?php
}
/**
* Save metabox data.
*
* @param int $post_id Post ID.
*/
public function save_metabox( int $post_id ): void {
if ( ! isset( $_POST['_wpmind_entity_linker_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce(
sanitize_text_field( wp_unslash( $_POST['_wpmind_entity_linker_nonce'] ) ),
self::NONCE_ACTION
) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$entity_url = isset( $_POST['wpmind_entity_url'] )
? esc_url_raw( wp_unslash( $_POST['wpmind_entity_url'] ) )
: '';
$entity_name = isset( $_POST['wpmind_entity_name'] )
? sanitize_text_field( wp_unslash( $_POST['wpmind_entity_name'] ) )
: '';
update_post_meta( $post_id, self::META_ENTITY_URL, $entity_url );
update_post_meta( $post_id, self::META_ENTITY_NAME, $entity_name );
}
}