认识 WordPress Abilities API:让插件能力可注册、可发现、可调用

原作者:Jonathan Bossenger · 2025 年 11 月 14 日 · 中文翻译与技术校注:未完纪

原文:Introducing the WordPress Abilities API

自从 WordPress Core AI 团队正式成立以来,这个团队推进的项目中,最让人期待的一个就是全新的 Abilities API。它把功能本身作为接口,能够跨越不同的调用环境,让其他工具和应用程序与 WordPress 交互。

这套 API 希望为 WordPress 核心、插件和主题定义并公开自身能力的方式,提供统一、可发现且有权限约束的机制。原文所处的时间点,第一批实现计划随 WordPress 6.9 进入核心,为更顺畅的开发流程、自动化和 AI 集成打下基础。

Abilities API 为开发者带来什么?

Abilities API 提供了一个集中注册表。WordPress 的各种功能,也就是 ability,可以按机器易于读取、人也容易理解的格式注册到这里。开发者能够发现它们,不同平台上的自动化工具,包括 AI agent,也能通过程序访问它们。

这个项目的主要目标包括:

  • 可发现:通过标准接口列出并检查已有能力。
  • 互操作:统一的 schema 让彼此独立的组件能够组合工作流。
  • 权限明确:清楚控制谁、或什么工具,可以调用能力。
  • 逐步采用:先以插件和 Composer 包提供,再平稳迁入 WordPress 核心。

可以把它想成一个集中目录,说明 WordPress、某个插件或某个主题到底能做什么,而且采用人和工具都能理解的注册方式。

对我来说,看一个东西真正运行起来,更容易理解它。因此,这篇文章会把 Abilities 接入我维护的 List All URLs 插件。完整代码可以在 GitHub 仓库中找到。

从 List All URLs 插件说起

这个插件很精简:它在“工具”菜单下增加一个子菜单,打开后是管理页面,可以选择列出站点中所有文章、页面或自定义文章类型的 URL。

List All URLs 管理页面,可选择全部URL、仅页面或仅文章,并勾选将结果生成为可点击链接。
List All URLs 管理页面。图片来自 Jonathan Bossenger 的 WordPress Developer Blog 原文;这是原作者的开发环境截图。
同一管理页面在提交选择后,显示从测试站点获得的五条URL链接。
管理页面显示 URL 列表的结果。来源同上,保留原图,未重绘为截图。

获取和显示数据主要依靠 list_all_urls_generate_url_list()。它接收两个参数,返回要在页面上显示的 URL 列表:

/**
* Generate a list of URLs based on the provided arguments
* Optionally make them clickable links
*
* @param array $arguments Arguments to customize the URL generation.
* @param bool  $makelinks Whether to return clickable links or plain URLs (escaped).
*
* @return array List of generated URLs.
*/
function list_all_urls_generate_url_list( array $arguments = array(), bool $makelinks = false ): array {
   $default_args   = array(
           'post_type'      => 'post',
           'posts_per_page' => - 1,
           'post_status'    => 'publish',
   );
   $args           = wp_parse_args( $arguments, $default_args );
   $posts          = get_posts( $args );

   $links = array();
   foreach ( $posts as $post ) {
      $permalink = get_permalink( $post );
      if ( $makelinks ) {
         $links[] = '<a href="' . esc_url( $permalink ) . '">' . esc_html( $permalink ) . '</a>';
      } else {
         $links[] = esc_html( $permalink );
      }
   }

   return $links;
}

这个函数内部调用 WordPress 的 get_posts() 取得数据,再根据是否需要可点击链接来格式化结果。第一个参数是查询参数数组,第二个布尔值决定是否生成锚点 HTML;返回的是字符串数组。

我还希望增加一些便利功能,例如限制大站点返回的 URL 数量,按分类或日期范围筛选,以及导出结果。不过,更值得考虑的还有两项:

  1. 让 WordPress 外部也能取得 URL 列表,例如通过 REST API 端点接到外部服务。
  2. 提供一个区块,让用户在文章、页面,甚至模板等区块编辑器场景中插入 URL 列表。

要实现这些功能,首先需要做一些准备。完整的传统实现可参阅仓库的 rest-blocks 分支。

你需要用 register_rest_route() 注册自定义 REST 路由和 GET 端点,再为它编写获取数据的回调。回调可以调用 list_all_urls_generate_url_list,不传第二个 $makelinks 参数,让它返回数据:

add_action( 'rest_api_init', 'list_all_urls_register_rest_route' );
function list_all_urls_register_rest_route (): void {
   register_rest_route(
           'list-all-urls/v1',
           '/urls',
           array(
               'methods' => 'GET',
               'callback' => 'list_all_urls_rest_fetch_all_urls',
               'args' => array(
                       'type' => array(
                               'validate_callback' => function( $param ) {
                                   return is_string( $param );
                               }
                       ),
               ),
           )
   );
}

function list_all_urls_rest_fetch_all_urls( $arguments ){
   if ( isset($arguments['type'] ) ) {
       $post_type = sanitize_text_field( wp_unslash( $arguments['type'] ) );
   } else {
       $post_type = 'any';
   }
   $args = array(
       'post_type'      => $post_type,
   );
   return list_all_urls_generate_url_list( $args );
}

区块部分可以用 create-block 搭出基础结构,再在 Edit 组件中通过 api-fetch 调用自定义端点,把结果渲染到编辑器里:

export default function Edit() {
    const [urls, setUrls] = useState([]);

    useEffect(() => {
        apiFetch( { path: '/list-all-urls/v1/urls' } ).then( ( urls ) => {
            setUrls( urls );
        } );
    }, []);

    if ( ! urls ) {
        return (
            <div { ...useBlockProps() }>
                <p>{ __(
                    'Loading...',
                    'list-all-urls'
                ) }</p>
            </div>
        );
    }

    let urlsList = urls.map( ( url ) => {
        return <li><a href={ url }>{ url }</a></li>;
    });

    return (
        <div { ...useBlockProps() }>
            <ul>{ urlsList }</ul>
        </div>
    );
}

前台则适合使用动态区块:在 render.php 中调用 list_all_urls_generate_url_list,由服务器渲染:

<?php
/**
* Render file for the List All URLs block.
*/
$block_attributes = get_block_wrapper_attributes();
$urls = list_all_urls_generate_url_list( array( 'post_type' => 'any' ), true );
$urlList = '';
foreach ( $urls as $url ) {
   $urlList .= '<li>' .  wp_kses_post( $url ) . '</li>';
}
?>
<div <?php echo $block_attributes; ?>>
   <ul>
       <?php echo $urlList; ?>
   </ul>
</div>

如果还想让用户在区块里选择文章类型、决定是否生成可点击链接,就需要继续增加区块属性。

可见,仅仅为了在保留已有管理页面的同时,让“列出 URL”这项功能能从 REST API 和区块编辑器中调用,就已经需要写不少代码。这正是适合自定义 ability 的场景:把功能集中注册,再从需要它的地方获取和执行。

安装 Abilities API

原文建议先确保使用适合测试的最新 API。写作当时有三种安装方式:

  • 把 仓库克隆到 wp-content/plugins,安装依赖、构建并启用插件。
  • 从仓库 releases 页面下载版本,再上传、安装插件 ZIP。
  • 把 Composer 包作为插件或主题依赖。

克隆仓库可用于测试 trunk 分支最新代码,原文命令如下:

$ git clone git@github.com:WordPress/abilities-api.git
$ cd abilities-api
$ composer install
$ npm install
$ npm run build 

也可以通过 Composer 安装,让插件使用稳定版本中的功能,并保持与核心已有 API 的兼容性:

$ cd /wp-content/plugins/list-all-urls
$ composer require wordpress/abilities-api

用 ability 集中承载功能

在这个案例中,一个自定义 ability 就能承载大部分核心需求。PHP 文档说明了如何注册和使用,下面看看 List All URLs 所需要的内容。完整实现可查阅仓库的 abilities 分支。

在 PHP 中使用 wp_register_ability() 注册。为了在正确阶段完成注册,应把调用放在 wp_abilities_api_init action 对应的回调里:

add_action( 'wp_abilities_api_init', 'list_all_urls_register_abilities' );
/**
 * Register the ability to list all URLs
 *
 * @return void
 */
function list_all_urls_register_abilities() {
    wp_register_ability(
        'list-all-urls/urls',
        array(
            'label' => __( 'Get All URLs', 'list-all-urls' ),
            'description' => __( 'Retrieves a list of URLs from the WordPress site, optionally as clickable anchor links.', 'list-all-urls' ),
            'category' => 'site',
            'input_schema' => array(
                'type' => 'object',
                'properties' => array(
                        'post_type' => array(
                                'type' => 'string',
                                'description' => 'The post type to retrieve URLs from (e.g., post, page, custom post type).',
                            ),
                        'posts_per_page' => array(
                                'type' => 'integer',
                                'description' => 'Number of posts to retrieve. Use -1 to retrieve all posts.',
                        ),
                        'post_status' => array(
                                'type' => 'string',
                                'description' => 'The status of the posts to retrieve (e.g., publish, draft).',
                        ),
                        'makelinks' => array(
                                'type' => 'boolean',
                                'description' => 'Whether to return URLs as clickable anchor links.',
                        ),
                ),
            ),
            'output_schema' => array(
                    'type' => 'object',
                    'properties' => array(
                            'url' => array(
                                    'type' => 'string',
                                    'description' => 'URL or clickable link to the URL'
                            )
                    )
            ),
            'execute_callback' => 'list_all_urls_generate_url_list',
            'permission_callback' => '__return_true',
        )
    );
}

逐项理解注册参数

注册一个 ability,需要唯一标识符(本例为 list-all-urls/urls)以及参数数组。原文重点介绍以下几项:

  • label:供人阅读的名称。
  • description:说明它做什么。
  • category:所属分类。可以注册自己的分类,原文此处使用已有的 site 分类。
  • output_schema:描述返回数据的结构。
  • execute_callback:执行能力时调用的函数。
  • permission_callback:判断当前用户有没有执行权限的函数。

input_schema 是可选参数,但如果能力需要输入,就很有必要定义它。本例希望传入与 list_all_urls_generate_url_list() 相关的参数。

schema 不仅告诉调用方输入、输出应该长什么样,也支持自动验证。例如,posts_per_page 如果不满足所声明的整数类型要求,就会触发输入验证错误,阻止执行。

原文直接把已有的 list_all_urls_generate_url_list() 指定为执行回调,并认为不必修改它。这里需要对照前面的函数签名细看。

// 编辑补充:替换原注册数组中的对应配置项。
'output_schema' => array(
    'type'  => 'array',
    'items' => array( 'type' => 'string' ),
),
'execute_callback' => function ( array $input ): array {
    $make_links = ! empty( $input['makelinks'] );
    unset( $input['makelinks'] );
    // 本修订将范围收窄到已发布内容,且每次最多 100 条。
    $input['post_status'] = 'publish';
    $requested_limit = (int) ( $input['posts_per_page'] ?? 100 );
    $input['posts_per_page'] = max( 1, min( 100, $requested_limit ) );
    return list_all_urls_generate_url_list( $input, $make_links );
},
'permission_callback' => function (): bool {
    return current_user_can( 'manage_options' );
},

这段编辑补充改变了原例的行为:不再支持查询任意文章状态,不再允许用 -1 无限取回记录,并只允许具有 manage_options 的用户执行。实际需求如果是面向其他角色或前台公开展示,应分别设计权限与数据范围,不要机械套用这个管理员限制;还应调整输入 schema、加入文章类型允许列表和分页规则。它保留原函数生成链接的方式,因此 URL 数据与 HTML 表示的分离仍需在完整工程中处理。

在 PHP 中获取和调用能力

接下来,管理页面也可以改成通过 ability 获取并执行功能。主要变化是准备 $input,找到能力,再调用它:

$input = array(
    'post_type'      => $post_type,
    'posts_per_page' => - 1,
    'post_status'    => 'publish',
    'makelinks'      => $makelinks,
);

$urlsAbility = wp_get_ability( 'list-all-urls/urls' );
$urls = $urlsAbility->execute( $input );

这个方式的扩展性很吸引我。如果其他插件或主题也想使用这项功能,我只需公开能力标识、输入 schema 和输出 schema。

API 还提供 wp_get_abilities() 来列出能力,以及 wp_has_ability() 来判断特定能力是否存在。在开发期间,可以通过 WP-CLI 的 shell 命令检查它们。

例如,查看当前已注册能力:

$ wp shell
wp> $abilities = wp_get_abilities();

返回的信息比较长。下面完整保留原文输出,用来观察对象、schema、回调和元数据;其中对象编号、站点能力数量均属于原作者环境,不是你应当得到的固定结果。

展开:原文 wp_get_abilities() 的完整输出
=> array(4) {
  ["core/get-site-info"]=>
  object(WP_Ability)#2691 (9) {
    ["name":protected]=>
    string(18) "core/get-site-info"
    ["label":protected]=>
    string(20) "Get Site Information"
    ["description":protected]=>
    string(113) "Returns site information configured in WordPress. By default returns all fields, or optionally a filtered subset."
    ["category":protected]=>
    string(4) "site"
    ["input_schema":protected]=>
    array(4) {
      ["type"]=>
      string(6) "object"
      ["properties"]=>
      array(1) {
        ["fields"]=>
        array(3) {
          ["type"]=>
          string(5) "array"
          ["items"]=>
          array(2) {
            ["type"]=>
            string(6) "string"
            ["enum"]=>
            array(8) {
              [0]=>
              string(4) "name"
              [1]=>
              string(11) "description"
              [2]=>
              string(3) "url"
              [3]=>
              string(5) "wpurl"
              [4]=>
              string(11) "admin_email"
              [5]=>
              string(7) "charset"
              [6]=>
              string(8) "language"
              [7]=>
              string(7) "version"
            }
          }
          ["description"]=>
          string(81) "Optional: Limit response to specific fields. If omitted, all fields are returned."
        }
      }
      ["additionalProperties"]=>
      bool(false)
      ["default"]=>
      array(0) {
      }
    }
    ["output_schema":protected]=>
    array(3) {
      ["type"]=>
      string(6) "object"
      ["properties"]=>
      array(8) {
        ["name"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(15) "The site title."
        }
        ["description"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(17) "The site tagline."
        }
        ["url"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(18) "The site home URL."
        }
        ["wpurl"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(31) "The WordPress installation URL."
        }
        ["admin_email"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(37) "The site administrator email address."
        }
        ["charset"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(28) "The site character encoding."
        }
        ["language"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(30) "The site language locale code."
        }
        ["version"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(22) "The WordPress version."
        }
      }
      ["additionalProperties"]=>
      bool(false)
    }
    ["execute_callback":protected]=>
    object(Closure)#2689 (2) {
      ["static"]=>
      array(1) {
        ["site_info_fields"]=>
        array(8) {
          [0]=>
          string(4) "name"
          [1]=>
          string(11) "description"
          [2]=>
          string(3) "url"
          [3]=>
          string(5) "wpurl"
          [4]=>
          string(11) "admin_email"
          [5]=>
          string(7) "charset"
          [6]=>
          string(8) "language"
          [7]=>
          string(7) "version"
        }
      }
      ["parameter"]=>
      array(1) {
        ["$input"]=>
        string(10) "<optional>"
      }
    }
    ["permission_callback":protected]=>
    object(Closure)#2690 (0) {
    }
    ["meta":protected]=>
    array(2) {
      ["annotations"]=>
      array(3) {
        ["readonly"]=>
        bool(true)
        ["destructive"]=>
        bool(false)
        ["idempotent"]=>
        bool(true)
      }
      ["show_in_rest"]=>
      bool(true)
    }
  }
  ["core/get-user-info"]=>
  object(WP_Ability)#2694 (9) {
    ["name":protected]=>
    string(18) "core/get-user-info"
    ["label":protected]=>
    string(20) "Get User Information"
    ["description":protected]=>
    string(129) "Returns basic profile details for the current authenticated user to support personalization, auditing, and access-aware behavior."
    ["category":protected]=>
    string(4) "user"
    ["input_schema":protected]=>
    array(0) {
    }
    ["output_schema":protected]=>
    array(4) {
      ["type"]=>
      string(6) "object"
      ["required"]=>
      array(6) {
        [0]=>
        string(2) "id"
        [1]=>
        string(12) "display_name"
        [2]=>
        string(13) "user_nicename"
        [3]=>
        string(10) "user_login"
        [4]=>
        string(5) "roles"
        [5]=>
        string(6) "locale"
      }
      ["properties"]=>
      array(6) {
        ["id"]=>
        array(2) {
          ["type"]=>
          string(7) "integer"
          ["description"]=>
          string(12) "The user ID."
        }
        ["display_name"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(29) "The display name of the user."
        }
        ["user_nicename"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(35) "The URL-friendly name for the user."
        }
        ["user_login"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(32) "The login username for the user."
        }
        ["roles"]=>
        array(3) {
          ["type"]=>
          string(5) "array"
          ["description"]=>
          string(31) "The roles assigned to the user."
          ["items"]=>
          array(1) {
            ["type"]=>
            string(6) "string"
          }
        }
        ["locale"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(46) "The locale string for the user, such as en_US."
        }
      }
      ["additionalProperties"]=>
      bool(false)
    }
    ["execute_callback":protected]=>
    object(Closure)#2692 (0) {
    }
    ["permission_callback":protected]=>
    object(Closure)#2693 (0) {
    }
    ["meta":protected]=>
    array(2) {
      ["annotations"]=>
      array(3) {
        ["readonly"]=>
        bool(true)
        ["destructive"]=>
        bool(false)
        ["idempotent"]=>
        bool(true)
      }
      ["show_in_rest"]=>
      bool(false)
    }
  }
  ["core/get-environment-info"]=>
  object(WP_Ability)#2697 (9) {
    ["name":protected]=>
    string(25) "core/get-environment-info"
    ["label":protected]=>
    string(20) "Get Environment Info"
    ["description":protected]=>
    string(156) "Returns core details about the site's runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version)."
    ["category":protected]=>
    string(4) "site"
    ["input_schema":protected]=>
    array(0) {
    }
    ["output_schema":protected]=>
    array(4) {
      ["type"]=>
      string(6) "object"
      ["required"]=>
      array(4) {
        [0]=>
        string(11) "environment"
        [1]=>
        string(11) "php_version"
        [2]=>
        string(14) "db_server_info"
        [3]=>
        string(10) "wp_version"
      }
      ["properties"]=>
      array(4) {
        ["environment"]=>
        array(3) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(109) "The site's runtime environment classification (can be one of these: production, staging, development, local)."
          ["enum"]=>
          array(4) {
            [0]=>
            string(10) "production"
            [1]=>
            string(7) "staging"
            [2]=>
            string(11) "development"
            [3]=>
            string(5) "local"
          }
        }
        ["php_version"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(44) "The PHP runtime version executing WordPress."
        }
        ["db_server_info"]=>
        array(3) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(69) "The database server vendor and version string reported by the driver."
          ["examples"]=>
          array(2) {
            [0]=>
            string(6) "8.0.34"
            [1]=>
            string(15) "10.11.6-MariaDB"
          }
        }
        ["wp_version"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(48) "The WordPress core version running on this site."
        }
      }
      ["additionalProperties"]=>
      bool(false)
    }
    ["execute_callback":protected]=>
    object(Closure)#2695 (0) {
    }
    ["permission_callback":protected]=>
    object(Closure)#2696 (0) {
    }
    ["meta":protected]=>
    array(2) {
      ["annotations"]=>
      array(3) {
        ["readonly"]=>
        bool(true)
        ["destructive"]=>
        bool(false)
        ["idempotent"]=>
        bool(true)
      }
      ["show_in_rest"]=>
      bool(true)
    }
  }
  ["list-all-urls/urls"]=>
  object(WP_Ability)#2698 (9) {
    ["name":protected]=>
    string(18) "list-all-urls/urls"
    ["label":protected]=>
    string(12) "Get All URLs"
    ["description":protected]=>
    string(87) "Retrieves a list of URLs from the WordPress site, optionally as clickable anchor links."
    ["category":protected]=>
    string(13) "list-all-urls"
    ["input_schema":protected]=>
    array(2) {
      ["type"]=>
      string(6) "object"
      ["properties"]=>
      array(4) {
        ["post_type"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(73) "The post type to retrieve URLs from (e.g., post, page, custom post type)."
        }
        ["posts_per_page"]=>
        array(2) {
          ["type"]=>
          string(7) "integer"
          ["description"]=>
          string(58) "Number of posts to retrieve. Use -1 to retrieve all posts."
        }
        ["post_status"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(59) "The status of the posts to retrieve (e.g., publish, draft)."
        }
        ["makelinks"]=>
        array(2) {
          ["type"]=>
          string(7) "boolean"
          ["description"]=>
          string(49) "Whether to return URLs as clickable anchor links."
        }
      }
    }
    ["output_schema":protected]=>
    array(2) {
      ["type"]=>
      string(6) "object"
      ["properties"]=>
      array(1) {
        ["url"]=>
        array(2) {
          ["type"]=>
          string(6) "string"
          ["description"]=>
          string(32) "URL or clickable link to the URL"
        }
      }
    }
    ["execute_callback":protected]=>
    string(31) "list_all_urls_generate_url_list"
    ["permission_callback":protected]=>
    string(13) "__return_true"
    ["meta":protected]=>
    array(2) {
      ["annotations"]=>
      array(3) {
        ["readonly"]=>
        NULL
        ["destructive"]=>
        NULL
        ["idempotent"]=>
        NULL
      }
      ["show_in_rest"]=>
      bool(true)
    }
  }
}

判断一个能力是否存在:

$ wp shell
wp> $found = wp_has_ability('list-all-urls/urls');
=> bool(true)

只获取某个能力时,同样返回完整的能力对象,因此可以检查它做什么、期望哪些输入、返回什么结果:

展开:原文单个能力对象及其属性
$ wp shell
wp> $ability = wp_get_ability('list-all-urls/urls');
=> object(WP_Ability)#2698 (9) {
  ["name":protected]=>
  string(18) "list-all-urls/urls"
  ["label":protected]=>
  string(12) "Get All URLs"
  ["description":protected]=>
  string(87) "Retrieves a list of URLs from the WordPress site, optionally as clickable anchor links."
  ["category":protected]=>
  string(13) "list-all-urls"
  ["input_schema":protected]=>
  array(2) {
    ["type"]=>
    string(6) "object"
    ["properties"]=>
    array(4) {
      ["post_type"]=>
      array(2) {
        ["type"]=>
        string(6) "string"
        ["description"]=>
        string(73) "The post type to retrieve URLs from (e.g., post, page, custom post type)."
      }
      ["posts_per_page"]=>
      array(2) {
        ["type"]=>
        string(7) "integer"
        ["description"]=>
        string(58) "Number of posts to retrieve. Use -1 to retrieve all posts."
      }
      ["post_status"]=>
      array(2) {
        ["type"]=>
        string(6) "string"
        ["description"]=>
        string(59) "The status of the posts to retrieve (e.g., publish, draft)."
      }
      ["makelinks"]=>
      array(2) {
        ["type"]=>
        string(7) "boolean"
        ["description"]=>
        string(49) "Whether to return URLs as clickable anchor links."
      }
    }
  }
  ["output_schema":protected]=>
  array(2) {
    ["type"]=>
    string(6) "object"
    ["properties"]=>
    array(1) {
      ["url"]=>
      array(2) {
        ["type"]=>
        string(6) "string"
        ["description"]=>
        string(32) "URL or clickable link to the URL"
      }
    }
  }
  ["execute_callback":protected]=>
  string(31) "list_all_urls_generate_url_list"
  ["permission_callback":protected]=>
  string(13) "__return_true"
  ["meta":protected]=>
  array(2) {
    ["annotations"]=>
    array(3) {
      ["readonly"]=>
      NULL
      ["destructive"]=>
      NULL
      ["idempotent"]=>
      NULL
    }
    ["show_in_rest"]=>
    bool(true)
  }
}

直接提供 REST API 支持

还有一点很方便:能力可以像自定义文章类型那样,通过注册配置开启 REST 支持。把 meta.show_in_rest 设置为 true 即可:

'meta' => array(
       'show_in_rest' => true,
),

Abilities REST API 位于 wp-json/wp-abilities/v1 命名空间。访问需要已认证用户,沿用 WordPress REST API 的认证方法。

与 PHP 函数对应,可以通过这些端点执行常见操作:

  • 列出能力:GET /wp-json/wp-abilities/v1/abilities。
  • 获取单个能力:GET /wp-json/wp-abilities/v1/{namespace/ability},把占位符替换成注册标识,例如 list-all-urls/urls。
  • 执行能力:调用 /wp-json/wp-abilities/v1/{namespace/ability}/run,原文说明根据只读设置选择 GET 或 POST。

通过 REST 执行时,注册时设置的权限回调也会被检查。因此,正确认证和正确的权限回调需要同时满足;开启一个注册参数,就能复用这些标准入口。

Abilities 进入 WordPress 核心

原文写作时,服务端 PHP 的注册、获取、执行及 REST 支持,刚刚获准合入 WordPress 6.9。作者当时预计在 12 月初即可直接使用,并邀请读者通过 Help Test WordPress 6.9参与 6.9 测试。

这一节记录的是 2025 年 11 月的进度;今天查阅时应以目标版本的正式文档为准,而不是重新把“即将合入”当作当前状态。

在 JavaScript 中使用能力

你可能会想:“那我还是得用 api-fetch 之类的工具去访问 REST 端点吧?”有趣的地方来了:Abilities API 还提供 JavaScript 客户端,内置了从浏览器获取和执行能力的支持。

在原文写作时,客户端只在 GitHub 仓库中提供,目标是最终作为 Gutenberg 包发布。作者因此推测它可能随 WordPress 7.0 进入核心,或者更早随 Gutenberg 插件提供。这个判断是当时的计划和猜测,不是本译稿对某个未来版本的保证。

原文说明,以 Composer 包作为依赖,可以在插件或主题中使用 API,同时避免与核心 API 冲突。JavaScript 客户端提供列出、获取、执行能力,以及在 JavaScript 中创建能力的函数。List All URLs 这里只需要执行能力的函数:

import { executeAbility } from '@wordpress/abilities';

然后传入所需 JSON 对象,并在区块中使用返回的数据:

useEffect(() => {
   executeAbility( 'list-all-urls/urls', { 'makelinks': attributes.makeLinks } ).then( ( urls ) => {
       setUrls( urls );
   } );
}, []);

继续学习与参与开发

这只是开始

想想 WordPress 核心、插件和主题向开发者开放功能的各种方式:action 和 filter hook、全局函数、REST 端点、供扩展的对象,或者它们的组合。过去并没有一种强制统一的公开 API 表达方式。

Abilities 提供了标准化、可发现的途径,来描述并开放这些常见功能。于是,WordPress 的 PHP 应用层、JavaScript 展示层,以及外部 REST 调用方,就能通过同一种能力契约获取和执行它们。

如果再接入 WordPress MCP adapter,AI agent 也能与这些能力交互,带来更多可能性。不过,那是另一个教程的话题了。

Abilities API 为 WordPress 开发提供了一种令人期待的新方式。我很想看看它会为 WordPress 的未来带来什么。

原文致谢:感谢 @juanmaguitar、@bph 和 @areziaal 对本文的反馈与审阅。

版权与授权:原文和两张原配图署名 Jonathan Bossenger / WordPress Developer Blog;页面未提供足以把整篇文章重新标为开放许可的明确声明,本稿不作此推断。List All URLs 插件文件声明 GPL v2 or higher;Abilities API 项目采用 GPL v2 或后续版本,软件许可不自动等同于文章与图片许可。技术校注和局部修订为未完纪编辑新增,未执行源命令或验证运行结果。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

WP-Abilities-LICENSE.md

                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    {description}
    Copyright (C) {year}  {fullname}

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  {signature of Ty Coon}, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容