标签: PHP

  • WordPress AI Provider for DeepSeek 插件实现分析

    WordPress AI Provider for DeepSeek 插件实现分析

    前言

    WordPress 6.9 引入了 PHP AI Client SDK,为 WordPress 生态提供了统一的 AI 服务接入标准。本文分析我最近实现的 AI Provider for DeepSeek 插件,它是该 SDK 的 DeepSeek 适配实现,同时支持作为 Composer 包和 WordPress 插件两种使用方式。

    插件架构概览

    插件的目录结构遵循 PSR-4 规范:

    wp-ai-provider-for-deepseek/
    ├── plugin.php                          # WordPress 插件入口
    ├── readme.txt                        # WordPress 插件描述
    ├── assets/images/deepseek.svg        # 供应商 Logo
    └── src/
        ├── autoload.php                 # PSR-4 自动加载器
        ├── Provider/
        │   ├── DeepSeekProvider.php            # 主 Provider 类
        │   └── DeepSeekProviderAvailability.php # API Key 可用性检查器
        ├── Models/
        │   └── DeepSeekTextGenerationModel.php # 文本生成模型实现
        └── Metadata/
            └── DeepSeekModelMetadataDirectory.php # 模型元数据目录
    

    核心实现分析

    1. 插件入口与 Hook 注册(plugin.php)

    插件通过三个关键 Hook 完成初始化:

    // 注册 AI Provider(优先级 5,确保在 AI Client 之后加载)
    add_action('init', __NAMESPACE__ . '\\register_provider', 5);
    
    // 输出 Provider 数据到前端(确保 admin 页面可用)
    add_action('admin_enqueue_scripts', __NAMESPACE__ . '\\ensure_provider_data_output', 5);
    
    // 注册 Connector(WordPress 连接器系统)
    add_action('wp_connectors_init', __NAMESPACE__ . '\\register_connector', 10);
    
    // 自动审批 Connector 使用权限(优先级 20,确保 Approvals_Store 可用)
    add_action('init', __NAMESPACE__ . '\\auto_approve_connector', 20);
    

    设计亮点:

    • register_provider() 先检查 AiClient 类是否存在,避免依赖未安装时 fatal error
    • 使用 $registry->hasProvider() 防止重复注册
    • ensure_provider_data_output() 处理边缘情况:当 AI Client 的脚本未加载时,手动输出 window.aiProviderData

    2. Provider 主类(DeepSeekProvider.php)

    核心类继承自 AbstractApiProvider,实现四个工厂方法:

    class DeepSeekProvider extends AbstractApiProvider
    {
        // API 基础 URL
        protected static function baseUrl(): string
        {
            return 'https://api.deepseek.com';
        }
    
        // 创建模型实例(根据能力分发到具体模型类)
        protected static function createModel(...): ModelInterface
        {
            foreach ($capabilities as $capability) {
                if ($capability->isTextGeneration()) {
                    return new DeepSeekTextGenerationModel(...);
                }
            }
            throw new \RuntimeException('Unsupported model capabilities');
        }
    
        // 创建 Provider 元数据(支持版本化特性)
        protected static function createProviderMetadata(): ProviderMetadata
        {
            $args = [
                'deepseek',
                'DeepSeek',
                ProviderTypeEnum::cloud(),
                'https://platform.deepseek.com/api_keys',
                RequestAuthenticationMethod::apiKey()
            ];
    
            // 1.2.0+ 支持描述
            if (version_compare(AiClient::VERSION, '1.2.0', '>=')) {
                $args[] = __('Text generation with DeepSeek models.', 'ai-provider-for-deepseek');
            }
    
            // 1.3.0+ 支持 Logo
            if (version_compare(AiClient::VERSION, '1.3.0', '>=')) {
                $args[] = dirname(__DIR__, 2) . '/assets/images/deepseek.svg';
            }
    
            return new ProviderMetadata(...$args);
        }
    
        // 自定义可用性检查器(用真实 API 请求验证 Key)
        protected static function createProviderAvailability(): ProviderAvailabilityInterface
        {
            return new DeepSeekProviderAvailability(static::class);
        }
    
        // 模型元数据目录(动态从 API 发现可用模型)
        protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface
        {
            return new DeepSeekModelMetadataDirectory();
        }
    }
    

    版本兼容处理是这段代码的精华:通过 version_compare 逐步添加新特性支持,确保插件在旧版 PHP AI Client 上也能正常运行。

    3. API Key 验证与缓存(DeepSeekProviderAvailability.php)

    DeepSeek 不支持标准的”列出模型”端点,因此插件采用真实请求验证法:发送一个 max_tokens=1 的最小化请求,根据 HTTP 状态码判断 Key 是否有效。

    public function isConfigured(): bool
    {
        // 1. 检查缓存(5 分钟过期)
        $cached = get_transient(self::CACHE_KEY);
        if ($cached !== false) {
            return (bool) $cached;
        }
    
        // 2. 获取 API Key(优先 WordPress 选项,fallback 到环境变量)
        $apiKey = $this->getApiKey();
        if (empty($apiKey)) {
            $this->cacheResult(false);
            return false;
        }
    
        // 3. 发送最小化请求
        $response = wp_remote_post($url, [
            'headers' => [
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $apiKey,
            ],
            'body' => json_encode([
                'model'    => 'deepseek-v4-flash',
                'messages' => [['role' => 'user', 'content' => 'test']],
                'max_tokens' => 1,
            ]),
            'timeout' => 30,
        ]);
    
        // 4. 根据状态码判断(200/400 = Key 有效,401/403 = Key 无效)
        $responseCode = wp_remote_retrieve_response_code($response);
        $isConfigured = !in_array($responseCode, [401, 403], true);
    
        // 5. 缓存结果
        $this->cacheResult($isConfigured);
        return $isConfigured;
    }
    

    缓存策略有效减少了重复验证请求:使用 WordPress Transient API,5 分钟过期。注意错误时不缓存,允许下次重试。

    4. 模型元数据动态发现(DeepSeekModelMetadataDirectory.php)

    插件通过调用 DeepSeek 的 /models 端点动态获取可用模型列表,而非硬编码:

    protected function parseResponseToModelMetadataList(Response $response): array
    {
        $responseData = $response->getData();
        
        // 定义 DeepSeek 模型支持的能力和选项
        $textGenerationCapabilities = [
            CapabilityEnum::textGeneration(),
            CapabilityEnum::chatHistory(),
        ];
        
        $textGenerationOptions = [
            new SupportedOption(OptionEnum::systemInstruction()),
            new SupportedOption(OptionEnum::candidateCount()),
            new SupportedOption(OptionEnum::maxTokens()),
            new SupportedOption(OptionEnum::temperature()),
            new SupportedOption(OptionEnum::topP()),
            new SupportedOption(OptionEnum::customOptions()),
            new SupportedOption(OptionEnum::inputModalities()),  // 修复:添加必需的能力声明
            new SupportedOption(OptionEnum::outputModalities()),
        ];
    
        // 将 API 响应映射为 ModelMetadata 对象
        $models = array_map(function ($modelData) use ($textGenerationCapabilities, $textGenerationOptions) {
            return new ModelMetadata(
                $modelData['id'],    // model ID(如 deepseek-v4-flash)
                $modelData['id'],    // model name
                $textGenerationCapabilities,
                $textGenerationOptions
            );
        }, $responseData['data']);
    
        // 排序:deepseek-v4-flash 优先,其次是 deepseek-v4-pro,然后字母序
        usort($models, [$this, 'modelSortCallback']);
        
        return $models;
    }
    

    注意inputModalitiesoutputModalities 的添加——这是修复 ModelRequirements::fromPromptData()generate_text() 调用的关键,缺少这两个选项会导致运行时错误。

    5. 文本生成模型实现(DeepSeekTextGenerationModel.php)

    DeepSeek API 兼容 OpenAI 的 Chat Completions 格式,因此直接继承 AbstractOpenAiCompatibleTextGenerationModel

    class DeepSeekTextGenerationModel extends AbstractOpenAiCompatibleTextGenerationModel
    {
        protected function createRequest(
            HttpMethodEnum $method,
            string $path,
            array $headers = [],
            $data = null
        ): Request {
            // 覆盖父类:使用 DeepSeek Provider 的 URL 而非 OpenAI 的
            return new Request(
                $method,
                DeepSeekProvider::url($path),  // => https://api.deepseek.com/chat/completions
                $headers,
                $data,
                $this->getRequestOptions()
            );
        }
    }
    

    这个实现非常简洁,因为大部分工作(请求格式化、响应解析、流式输出等)都由父类 AbstractOpenAiCompatibleTextGenerationModel 完成。只需要覆盖 createRequest() 将请求路由到 DeepSeek 的 API 地址即可。

    Connector 审批系统

    WordPress 的 AI Client 引入了 Connector 审批机制:每个插件使用 Connector(如 DeepSeek)前必须获得审批。本插件在 init hook(优先级 20)自动审批自己:

    function auto_approve_connector(): void
    {
        if (!class_exists('\WordPress\AI\Connector_Approval\Approvals_Store')) {
            return;  // 旧版 AI Client 不支持审批系统
        }
    
        $store = new \WordPress\AI\Connector_Approval\Approvals_Store();
        $plugin_basename = plugin_basename(__FILE__);
        $connector_id = 'deepseek';
    
        if ($store->is_approved($plugin_basename, $connector_id)) {
            return;  // 已审批,跳过
        }
    
        $store->set_approval($plugin_basename, $connector_id, true);
    }
    

    这避免了用户手动在设置页面审批插件,提升了用户体验。

    使用方式

    作为 WordPress 插件

    // 1. 安装并激活插件(依赖 PHP AI Client 插件)
    // 2. 配置 API Key
    putenv('DEEPSEEK_API_KEY=your-api-key');
    
    // 3. 使用
    $result = AiClient::prompt('解释量子计算')
        ->usingProvider('deepseek')
        ->generateTextResult();
    
    echo $result->toText();
    

    作为 Composer 包

    composer require wordpress/ai-provider-for-deepseek
    
    <?php
    use WordPress\AiClient\AiClient;
    use WordPress\DeepSeekAiProvider\Provider\DeepSeekProvider;
    
    // 注册 Provider
    $registry = AiClient::defaultRegistry();
    $registry->registerProvider(DeepSeekProvider::class);
    
    // 设置 API Key
    putenv('DEEPSEEK_API_KEY=your-api-key');
    
    // 生成文本
    $result = AiClient::prompt('Explain quantum computing')
        ->usingProvider('deepseek')
        ->generateTextResult();
    
    echo $result->toText();
    

    技术亮点总结

    特性 实现方式 价值
    版本兼容 version_compare 渐进式添加特性 支持旧版 AI Client,无 breaking change
    API Key 验证 最小化真实请求 + Transient 缓存 准确验证 + 减少重复请求
    动态模型发现 调用 /models API + 元数据映射 自动支持新模型,无需更新代码
    Connector 审批 init hook 自动审批 零配置用户体验
    OpenAI 兼容 继承 AbstractOpenAiCompatible* 复用大量现有逻辑,代码量极小
    双模式运行 Composer 包 + WordPress 插件 最大灵活性,可在任何 PHP 项目中使用

    结论

    这个插件的实现展示了如何为一个新的 AI 服务商编写符合 WordPress PHP AI Client 标准的 Provider。核心要点:

    1. 继承正确的抽象类:文本生成继承 AbstractOpenAiCompatibleTextGenerationModel
    2. 实现四个工厂方法baseUrl()createModel()createProviderMetadata()createModelMetadataDirectory()
    3. 自定义可用性检查:当标准方法(列出模型)不可用时,用真实请求验证
    4. 版本兼容处理:使用 version_compare 渐进式添加新特性支持
    5. WordPress 集成:正确注册 Hook、Connector、审批

    完整代码已开源在 GitHub,欢迎试用和贡献。

  • 优化WordPress后台加载速度

    优化WordPress后台加载速度

    1. PHP预加载脚本

    <?php
    
    /**
    *使用OPcache优化WordPress的预加载脚本。
    *将此文件放在WordPress安装的根目录中。
     */
    
    // Define the base path for WordPress
    define('WP_ROOT_DIR', __DIR__);
    
    // Define ABSPATH (required by WordPress core files)
    if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', WP_ROOT_DIR . '/' );
    }
    
    // Define WPINC (required by wp-includes/functions.php)
    if ( ! defined( 'WPINC' ) ) {
        define( 'WPINC', 'wp-includes' );
    }
    
    // Define WP_DEBUG (required by wp-includes/functions.php)
    if ( ! defined( 'WP_DEBUG' ) ) {
        define( 'WP_DEBUG', false );
    }
    
    // 仅预加载最基本的核心文件
    require WP_ROOT_DIR . '/wp-includes/default-constants.php';
    require WP_ROOT_DIR . '/wp-includes/rewrite.php';
    require WP_ROOT_DIR . '/wp-includes/theme.php';
    require WP_ROOT_DIR . '/wp-includes/post.php';
    require WP_ROOT_DIR . '/wp-includes/meta.php';
    require WP_ROOT_DIR . '/wp-includes/user.php';
    require WP_ROOT_DIR . '/wp-includes/cache.php';
    require WP_ROOT_DIR . '/wp-includes/capabilities.php';
    require WP_ROOT_DIR . '/wp-includes/shortcodes.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-query.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-widget.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-roles.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-user.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-post.php';
    
    // 后台管理模块的类
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-media-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-users-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-themes-list-table.php';
    
    // Preload database-related files
    require WP_ROOT_DIR . '/wp-includes/wp-db.php';
    // require WP_ROOT_DIR . '/wp-includes/class-wpdb.php'; // 如果不需要,可以注释
    
  • WordPress添加关键词和描述标签

    WordPress添加关键词和描述标签

    关键词和描述标签作为SEO的基础配置,在wordpress中没有默认添加,这里记录一下在Wordpress中自动添加关键词和描述标签的方法。

    一、实现代码

    实现思路是使用标签来作为关键词,使用文章摘要作为页面描述,找到使用主题的functions.php文件,添加以下代码即可实现。

    /**
     * 添加SEO相关的Keywords和Description标签
     */
    function add_seo_meta_tags() {
        if (is_home()) {
            $tags = get_tags([
                'number' => 15,
                'orderby' => 'count',
                'order' => 'DESC'
            ]);
            $tags = array_map(function ($item) {
                return $item->name;
            }, $tags);
            if (count($tags) > 0) {
                ?>
                <meta name="keywords" content="<?php echo get_bloginfo('name'); ?>,<?php echo implode(',', $tags); ?>">
                <?php
            }
            ?>
                <meta name="description" content="<?php echo get_bloginfo('description'); ?>">
            <?php
        } else if (is_category() || is_tag()) {
            ?>
                <meta name="keywords" content="<?php echo single_cat_title(); ?>">
                <meta name="description" content="<?php echo strip_tags(category_description()); ?>">
            <?php
        } else if (is_singular()) {
            ?>
                <meta name="description" content="<?php echo strip_tags(get_the_excerpt()); ?>">
            <?php
    
            $tags = array_map(function ($item) {
                return $item->name;
            }, get_the_tags() ?: []);
            if (count($tags) > 0) {
                ?>
                    <meta name="keywords" content="<?php echo implode(',', $tags); ?>">
                <?php
            }
        }
    }
    add_action( 'wp_head', 'add_seo_meta_tags' );
    

    二、使用的前置要求

    请注意,需要保证主题的header.php调用过wp_head()函数,类似下面:

    <!DOCTYPE html>
    <html <?php language_attributes(); ?> class="no-js">
    <head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <?php wp_head(); ?>
    </head>
    

    OK,这样我们就给所有页面都添加上关键词和描述标签了~

    相关链接:

    1. wordpress关于meta标签的说明
  • PHP内置服务器与Serverless

    PHP内置服务器与Serverless

    PHP从5.4版本开始就提供了一个内置的WEB服务器,可以通过一个简单的命令php -S启动一个WEB服务器,极大简化了开发环境的搭建。

    到目前为止,官网文档对于内置服务器的使用依然建议用于开发环境,不建议用于生产环境,原因倒是很容易理解,主要有两个方面:

    1. 支持的MIME类型很少,5.4版本放出时只支持.htm和.svg(从5.5版本完善了大部分常见的MIME类型支持)

    2. 仅实现了基本功能,基本没有任何优化,是一个单线程进程(不过从7.4版本开始,内置服务器支持多进程的运行方式)

    从传统开发角度看,这样性能和功能的服务器确实很难应用于生产环境,但是伴随着Serverless的发展,感觉内置服务器的限制突然不是那么重要了。

    使用Serverless服务,不管是AWS Lambda,Google Function,还是国内阿里云的函数计算、腾讯云的云函数,我们关注的点不再聚焦于单机性能释放,而是变成了以下四个方面,我们要做的本质上变成了降低单请求的资源占用和执行时间

    1. 调用次数

    2. CPU时间

    3. 内存占用

    4. 执行时间

    5. 带宽

    我们可以逐个对比以下,

    1. 调用次数显然很难因为WEB服务器的变化有什么变化

    2. CPU时间上,内置服务器作为一个单进程应用,同样的逻辑在函数计算这样的环境下,较少了Nginx与FPM交互的网络开销、Nginx的运行开销,理论上内置服务器应该表现更好

    3. 内存占用方面,内置服务器不再需要运行Nginx,同样逻辑,应该也比传统部署方式占用更少一些

    4. 执行时间,Nginx+FPM需要启动两个进程,需要两个进程间的通信,很难与直接启动PHP进程更快

    5. 带宽基本不用对比,应该不会有什么变化,gzip完全可以在CDN层来实现

    从Serverless的角度看,内置服务器并不算是一个很差的选择,对比传统的运行方式可能更加合适一些,就是不太清楚不建议生产环境使用是否有除性能外的其他原因,回头去翻一翻PHP的issue。

    参考文章

    1. PHP-Built-in web server(https://www.php.net/manual/en/features.commandline.webserver.php

  • Nextcloud安全最佳实践

    Nextcloud安全最佳实践

    1. 复杂密码
    2. Suspicious Login
    3. 两步认证,启用至少一项
      1. *Two-Factor TOTP Provider
      2. *Two-Factor Authentication via Nextcloud Notification
      3. *Two-Factor WebAuthn
      4. Two-Factor Email
  • PHP中对象缓存方式的选择

    PHP中对象缓存方式的选择

    类似于Map的键值类型对象缓存对于提高应用的性能有很大的作用,实现此类缓存的方式也比较多,那么该如何选择对象缓存的方式呢?由于PHP常用的运行方式主要是基于FPM的形式,这篇文章暂不考虑常驻内存形式的缓存。

    一、基于文件系统实现缓存

    这应该是比较常见的一种形式,基于文件系统的缓存优点:

    • 不需要安装额外的扩展、中间件
    • 支持几乎所有运行环境
    • 支持文件锁

    缺点:

    • 相对内存形式的缓存方式,性能一般
    • 存在并发读写时,性能极差(并发写,使用文件锁的情况)
    • 占用磁盘容量
    • 不好统计键调用次数等

    适合的场景:单机运行,单键极少写请求,需要持久化的情况,比如动态页面的静态化。

    二、基于数据库实现缓存

    优点:

    • 支持几乎所有运行环境,仅需要安装对应数据库的驱动程序,大部分环境默认提供至少一种数据库驱动程序
    • 支持锁
    • 方便进行复杂的查询统计

    缺点:

    • 作为最常遇到的性能问题点,不太适合用于缓存场景
    • 读写性能一般

    适合的场景:无法控制宿主安装程序或者扩展。

    三、基于Redis/Memcached等中间件实现缓存

    优点:

    • 读写性能好
    • 支持集群运行
    • 支持多数据结构(Redis)
    • 本身支持缓存淘汰策略

    缺点:

    • 需要额外的中间件
    • 需要额外的扩展、包支持
    • 大多数主机环境不支持(可喜的是随着公有云的发展,主机环境正在被新的虚拟化方式替代)

    适合的场景:只要支持安装,适合绝大多数场景。

  • How to calculate the similarity of two articles using PHP

    How to calculate the similarity of two articles using PHP

    How to calculate the similarity of two articles using PHP?

    • Using the similar_text() function: This is a built-in function in PHP that calculates the similarity of two strings. It returns the number of matching characters in the two strings, or the similarity in percent if the third parameter is passed.

    PHP

    $similarity = similar_text($article1, $article2);
    
    if ($similarity > 0.75) {
      echo "The two articles are very similar.";
    } else {
      echo "The two articles are not very similar.";
    }
    

    Use code with caution. Learn morecontent_copy

    • Using the levenshtein() function: This is another built-in function in PHP that calculates the Levenshtein distance between two strings. The Levenshtein distance is a measure of how similar two strings are, and it is calculated by counting the number of edits (insertions, deletions, or substitutions) required to transform one string into the other.

    PHP

    $levenshtein_distance = levenshtein($article1, $article2);
    
    if ($levenshtein_distance < 10) {
      echo "The two articles are very similar.";
    } else {
      echo "The two articles are not very similar.";
    }
    

    Use code with caution. Learn morecontent_copy

    • Using a custom algorithm: You can also write your own custom algorithm to calculate the similarity of two articles. This could involve using a variety of factors, such as the number of common words, the number of common phrases, the order of the words, and the grammatical structure of the articles.

    Once you have calculated the similarity of two articles, you can use it to do a variety of things, such as:

    • Detecting plagiarism: You can use the similarity of two articles to detect plagiarism. If the similarity is high, then it is likely that one of the articles is a plagiarized version of the other.
    • Recommending similar articles: You can use the similarity of two articles to recommend similar articles to users. For example, if a user reads an article about “how to cook a steak,” you could recommend other articles about “how to cook a chicken” or “how to cook a fish.”
    • Clustering articles: You can use the similarity of two articles to cluster articles together. This could be useful for organizing articles in a database or for creating a search engine.

    I hope this helps!

  • php: /usr/local/lib/libcurl.so.4: no version information available (required by php)

    php: /usr/local/lib/libcurl.so.4: no version information available (required by php)

    出现原因是安装了多个版本的库,ubuntu的一些库似乎增加了一些附加补丁,导出的符号与ubuntu有关联的版本信息,但在标准的库中是没有的。

    可以通过:find / -name libcurl.so*查找系统中安装的所有的对应的库版本及位置。

    解决方案:

    1. 删除出现问题的/usr/local/lib/libcurl.so.4
    2. 寻找系统库中对应库的版本,建立软链ln -s /usr/lib/x86_64-linux-gnu/libcurl.so.4.5.0 /usr/local/lib/libcurl.so.4
  • 构建WordPress插件、主题开发镜像

    构建PHP镜像

    PHP内置服务器的路由脚本可以参考这个链接:使用PHP内置服务器运行WORDPRESS

    FROM php:8.1.27-alpine
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 
    && mkdir /code 
    && docker-php-ext-install mysqli
    CMD [ "php", "-S", "0.0.0.0:8080", "-t", "/code", "/code/router.php" ]

    配置compose

    version: "3"
    services:
      wordpress:
        build: .
        volumes:
          - /Users/ianzhi/Code/php/wordpress:/code
        restart: always
        ports:
          - 8080:8080
      mysql:
        image: mysql:8.0.36
        environment:
          MYSQL_ROOT_PASSWORD: 'root'
        expose:
          - 3306
        ports:
          - 3306:3306