Compare commits

..

3 commits

Author SHA1 Message Date
26db8c41a4 chore(deps): add renovate.json 2026-02-20 00:17:26 +00:00
LinuxJoy
558d029bc5 feat(i18n): add .pot translation template and auto-generate in deploy
Generate wpmind.pot via wp-cli during deploy, enabling translation
pipeline and i18n coverage testing by translate and elementary VMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 23:15:25 +08:00
LinuxJoy
7b1dad3287 feat(updater): integrate WenPai update checker with popup fix
Add Update URI header and WenPai_Updater v1.1.0 for self-hosted update
support via updates.wenpai.net. The updater includes three key fixes over
v1.0.0: external flag to prevent broken wp.org links in popup, API error
response validation, and local plugin header fallback when API unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 22:02:28 +08:00
4 changed files with 3375 additions and 0 deletions

View file

@ -19,6 +19,11 @@ if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi

# 生成 .pot 翻译模板
echo "🌐 生成 .pot 翻译模板..."
mkdir -p "$SOURCE_DIR/languages"
wp i18n make-pot "$SOURCE_DIR" "$SOURCE_DIR/languages/wpmind.pot" --domain=wpmind --skip-audit --quiet

# 同步文件 (排除 .git 目录)
echo "📦 同步文件..."
sudo rsync -av --delete \

View file

@ -0,0 +1,266 @@
<?php
/**
* WenPai 插件更新器
*
* 为文派系插件提供自建更新服务支持。
* 通过文派云桥 (WenPai Bridge) 检查插件更新,
* 利用 WordPress 5.8+ 的 Update URI 机制。
*
* 当 wp-china-yes 插件激活并启用集中更新时,
* 此更新器会通过 wenpai_updater_override filter 自动让位。
*
* @package WenPai
* @version 1.0.0
* @requires WordPress 5.8+
* @requires PHP 7.4+
* @link https://feicode.com/WenPai-org
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

if ( class_exists( 'WenPai_Updater' ) ) {
return;
}

class WenPai_Updater {

/**
* 更新器版本号。
*
* @var string
*/
const VERSION = '1.1.0';

/**
* 云桥 API 地址。
*
* @var string
*/
const API_URL = 'https://updates.wenpai.net/api/v1';

/**
* 插件主文件 basename如 wpslug/wpslug.php
*
* @var string
*/
private $plugin_file;

/**
* 插件 slug如 wpslug
*
* @var string
*/
private $slug;

/**
* 当前插件版本。
*
* @var string
*/
private $version;

/**
* 初始化更新器。
*
* @param string $plugin_file 插件主文件路径plugin_basename 格式)。
* @param string $version 当前插件版本号。
*/
public function __construct( string $plugin_file, string $version ) {
$this->plugin_file = $plugin_file;
$this->slug = dirname( $plugin_file );
$this->version = $version;

// 检查是否被 wp-china-yes 集中更新接管
$is_overridden = apply_filters(
'wenpai_updater_override',
false,
$this->slug
);

if ( ! $is_overridden ) {
$this->register_hooks();
}
}

/**
* 注册 WordPress hooks。
*/
private function register_hooks(): void {
// Update URI: https://updates.wenpai.net 触发此 filter
add_filter(
'update_plugins_updates.wenpai.net',
[ $this, 'check_update' ],
10,
4
);

// 插件详情弹窗
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 20, 3 );
}

/**
* 检查插件更新。
*
* WordPress 在检查更新时,对声明了 Update URI 的插件
* 触发 update_plugins_{hostname} filter。
*
* @param array|false $update 当前更新数据。
* @param array $plugin_data 插件头信息。
* @param string $plugin_file 插件文件路径。
* @param string[] $locales 语言列表。
* @return object|false 更新数据或 false。
*/
public function check_update( $update, array $plugin_data, string $plugin_file, array $locales ) {
if ( $plugin_file !== $this->plugin_file ) {
return $update;
}

$response = $this->api_request( 'update-check', [
'plugins' => [
$this->plugin_file => [
'Version' => $this->version,
],
],
] );

if ( is_wp_error( $response ) || empty( $response['plugins'][ $this->plugin_file ] ) ) {
return $update;
}

$data = $response['plugins'][ $this->plugin_file ];

return (object) [
'id' => $data['id'] ?? '',
'slug' => $data['slug'] ?? $this->slug,
'plugin' => $this->plugin_file,
'version' => $data['version'] ?? '',
'new_version' => $data['version'] ?? '',
'url' => $data['url'] ?? '',
'package' => $data['package'] ?? '',
'icons' => $data['icons'] ?? [],
'banners' => $data['banners'] ?? [],
'requires' => $data['requires'] ?? '',
'tested' => $data['tested'] ?? '',
'requires_php' => $data['requires_php'] ?? '',
];
}

/**
* 插件详情弹窗数据。
*
* 当用户在 WP 后台点击"查看详情"时触发。
*
* @param false|object|array $result 当前结果。
* @param string $action API 动作。
* @param object $args 请求参数。
* @return false|object 插件信息或 false。
*/
public function plugin_info( $result, string $action, object $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}

if ( ! isset( $args->slug ) || $args->slug !== $this->slug ) {
return $result;
}

$response = $this->api_request( "plugins/{$this->slug}/info" );

if ( ! is_wp_error( $response ) && ! isset( $response['error'] ) && ! empty( $response['name'] ) ) {
$info = new stdClass();
$info->name = $response['name'];
$info->slug = $response['slug'] ?? $this->slug;
$info->version = $response['version'] ?? '';
$info->author = $response['author'] ?? '';
$info->homepage = $response['homepage'] ?? '';
$info->download_link = $response['download_link'] ?? '';
$info->requires = $response['requires'] ?? '';
$info->tested = $response['tested'] ?? '';
$info->requires_php = $response['requires_php'] ?? '';
$info->last_updated = $response['last_updated'] ?? '';
$info->icons = $response['icons'] ?? [];
$info->banners = $response['banners'] ?? [];
$info->sections = $response['sections'] ?? [];
$info->external = true;

return $info;
}

// API 不可用或插件未注册时,用本地插件头信息兜底
$plugin_path = WP_PLUGIN_DIR . '/' . $this->plugin_file;
if ( ! file_exists( $plugin_path ) ) {
return $result;
}

if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_data = get_plugin_data( $plugin_path );

$info = new stdClass();
$info->name = $plugin_data['Name'] ?? $this->slug;
$info->slug = $this->slug;
$info->version = $this->version;
$info->author = $plugin_data['AuthorName'] ?? '';
$info->homepage = $plugin_data['PluginURI'] ?? '';
$info->requires = $plugin_data['RequiresWP'] ?? '';
$info->requires_php = $plugin_data['RequiresPHP'] ?? '';
$info->sections = [
'description' => $plugin_data['Description'] ?? '',
];
$info->external = true;

return $info;
}

/**
* 向云桥 API 发送请求。
*
* @param string $endpoint API 端点(不含 /api/v1/ 前缀)。
* @param array|null $body POST 请求体null 则用 GET
* @return array|WP_Error 解码后的响应或错误。
*/
private function api_request( string $endpoint, ?array $body = null ) {
$url = self::API_URL . '/' . ltrim( $endpoint, '/' );

$args = [
'timeout' => 10,
'headers' => [
'Accept' => 'application/json',
],
];

if ( null !== $body ) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode( $body );
$response = wp_remote_post( $url, $args );
} else {
$response = wp_remote_get( $url, $args );
}

if ( is_wp_error( $response ) ) {
return $response;
}

$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
return new WP_Error(
'wenpai_bridge_error',
sprintf( 'WenPai Bridge API returned %d', $code )
);
}

$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $data ) ) {
return new WP_Error(
'wenpai_bridge_parse_error',
'Invalid JSON response from WenPai Bridge'
);
}

return $data;
}
}

3096
languages/wpmind.pot Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@
* Domain Path: /languages
* Requires at least: 6.4
* Requires PHP: 8.1
* Update URI: https://updates.wenpai.net
*
* @package WPMind
*/
@ -38,6 +39,9 @@ if ( ! defined( 'WPMIND_PLUGIN_DIR' ) ) {
if ( ! defined( 'WPMIND_PLUGIN_URL' ) ) {
define( 'WPMIND_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}
if ( ! defined( 'WPMIND_PLUGIN_BASENAME' ) ) {
define( 'WPMIND_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
}
// Alias for module compatibility.
if ( ! defined( 'WPMIND_PATH' ) ) {
define( 'WPMIND_PATH', WPMIND_PLUGIN_DIR );
@ -736,5 +740,9 @@ spl_autoload_register( function ( string $class ): void {
}
} );

// 加载 WenPai 更新器(跨插件共享组件,不走 PSR-4
require_once WPMIND_PLUGIN_DIR . 'includes/class-wenpai-updater.php';
new \WenPai_Updater( WPMIND_PLUGIN_BASENAME, WPMIND_VERSION );

// 加载 Provider 注册模块
require_once WPMIND_PLUGIN_DIR . 'includes/Providers/register.php';