mcpField
C
canai-mcp

canai-mcp

github.com/usewp开发069 次安装1 周前更新
代码仓库

关于 canai-mcp

canai-mcp 是一项通过 CanAI MCP 服务管理 WordPress 站点 CanAI 内容的技能,适用于编辑模板、页面、设置和媒体的工作流。它要求站点内容读写通过 MCP 工具完成,并将 FluentSnippets 排除在外。

主要功能

  1. 01通过 MCP 工具读写 CanAI 内容
  2. 02管理模板、页面与设置
  3. 03处理国际化和媒体
  4. 04提供站点初始化流程
  5. 05限制内容工作范围

适用场景

  1. 01读取或更新 CanAI 模板
  2. 02创建页面并配置站点设置
  3. 03为站点上传媒体
  4. 04初始化 CanAI 站点结构

MCP 相关

该技能只通过 CanAI MCP 服务与 WordPress 站点交互,使用配置中的完整 MCP 端点。它要求内容操作使用 MCP 工具,不以命令行或直接 REST 请求代替。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/usewp/canai --skill canai-mcp

安装后在 Claude Code 中输入/canai-mcp确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
FLUENTCART.md
MARKDOWN-FOR-AGENTS.md
READING-MODE.md
REFERENCE.md
STRUCTURE-NAVIGATION.md
WOOCOMMERCE.md

详细介绍

原文在翻译后有更新,译文可能不是最新。

CanAI MCP 技能

你是仅通过 CanAI MCP 服务(WordPress Abilities API + mcp-adapter)使用 CanAI 的专家。WordPress 站点可以是远程站点,也可以是本地站点;本技能支持读取或更改该站点 CanAI 相关数据的唯一方式,是在用户的 MCP 配置中将 **WP_API_URL 指向该站点的完整 MCP 端点({site}/wp-json/mcp/wpcanai)。

有关 CanAI 注册的 Twig 函数(以及内置函数所用的随附 Twig 版本)和 WooCommerce 上下文,请参阅 references/REFERENCE.md。有关共享的 Atomic Design 导航约定,请参阅 references/STRUCTURE-NAVIGATION.md。如果用户希望在浏览器的阅读模式中打开内容,请参阅 references/READING-MODE.md。

MCP 是传输方式——不使用 shell,不编辑文件

本技能仅包含 CanAI MCP 服务及其工具。与用户的 WordPress 交互时,绝不可用终端 wp、curl、原始 REST 或仓库编辑来替代。站点可以是本地或远程;无论哪种情况,都通过 MCP 访问。

FluentSnippets 属于单独的选择加入技能 canai-yolo。本技能专注于内容(模板、页面、博客文章、设置、i18n、媒体、Tailwind)。如果用户需要编写代码片段,请安装/使用 canai-yolo——不要在本技能中自行设计这些工作流。这也包括代码片段部署循环(wpcanai-replace-in-snippet → sha256 检查 → purged,插件 v1.80.0)。没有 eval 逃逸通道:wpcanai/eval 能力已在插件 v1.59.0 中移除,因此每项功能都必须是实际的 wpcanai/* 能力。


严重警告——仅限 CanAI MCP 服务(严格规定)

对于 CanAI 内容工作,与用户 WordPress 的每一次交互(列出/读取/写入模板和页面、设置、引导设置、扫描、WooCommerce 页面 ID、i18n、媒体等)都必须通过 CanAI MCP 服务,使用宿主提供的工具(名称中带连字符,例如 wpcanai-read-meta、wpcanai-write-meta、wpcanai-setup 等——请查看已配置服务的工具列表,通常为 canai-mcp 或 user-canai-mcp)。不得使用 WP-CLI、wp eval、直接 SQL、针对 wp-admin 的浏览器自动化,或编辑工作区中 wp-content/ 下的文件来更改线上站点数据。

唯一记录在案的例外——二进制文件上传。 MCP 不适合传输文件(文件大小和 base64 开销),因此媒体文件通过 CanAI sideload REST 路由 POST {site}/wp-json/wpcanai/v1/sideload 上传,使用与 MCP 传输相同的 API 密钥。确切步骤见下文 “上传媒体(二进制文件)”。这是唯一获准对站点使用 curl 的情况;其他所有操作仍必须通过 MCP。

  1. 不得使用 Edit、Write 或应用补丁来更改 MCP 提供的站点中的 CanAI 模板 HTML/CSS/JS/上下文(无论路径位于 wp-content/ 下还是仓库中的其他位置)。这些文件并非 MCP 驱动工作所使用的线上 CanAI 存储模型。
  2. 所有 CanAI 内容(_canai_html、_canai_css、_canai_js、_canai_context、_canai_layout)的读取和写入都必须仅通过 CanAI MCP 工具进行。
  3. 使用宿主的 MCP 集成来调用这些工具。如果用户调用了此技能或 /canai-mcp,不得用 Shell wp、Python、原始 HTTP 或本地文件编辑来替代 MCP 操作。(唯一的 curl 例外是记录在案的 sideload 路由——见下文“上传媒体”。)
  4. 工作区中的 **Read、Grep、Glob** 仅用于读取此技能、插件源代码或文档,不得用于“修复”线上我的账户/隐私/模板输出。对于线上内容,请使用 **wpcanai-resolve-content-id** 或 **wpcanai-list-pages** 确认正确的 post_id,然后使用 **wpcanai-read-meta** 读取,再使用 **wpcanai-write-meta** 写入。

如果 MCP 工具不可用或调用失败,请告知用户检查 MCP 客户端设置(canai-mcp 或已配置的服务名称、JWT_TOKEN、WP_API_URL)——不得悄悄改用 WP-CLI、REST 或编辑本地文件来处理站点内容。


如何调用 MCP 工具(任意客户端)

适用于任何支持 MCP 的宿主(IDE、CLI、Agent):Cursor、Claude Desktop、Windsurf、Cline、VS Code 扩展等。

  • 通过宿主的 MCP 工具调用机制调用 CanAI 工具(确切的 API 名称因宿主而异,例如 CallMcpTool、use_mcp_tool、客户端内置的 MCP 工具列表)。用户配置的 MCP 服务名称通常为 **canai-mcp;某些宿主会给服务名称添加前缀**(例如 user-canai-mcp)。请使用宿主为 CanAI MCP 连接列出的服务标识符。
  • MCP 客户端负责处理会话、传输和身份验证——切勿通过 Shell、Python、curl 或原始 HTTP,针对 {site}/wp-json/mcp/wpcanai 重新实现 MCP。大负载不是例外:通过 wpcanai-write-meta 传入完整的 _canai_html 可处理任意大小的内容;手动构造 HTTP 请求访问 MCP 端点只会得到 403(没有会话/身份验证)。如果写入内容大到不值得整体重新发送,请改用 wpcanai-replace-in-meta 进行针对性修改——绝不可改用原始 HTTP。
  • 不得打开工具描述符 JSON 文件或探测 REST 路由来查找参数——请使用下方的内联工具参考。
  • 如果工具调用失败,请用户核实 WordPress 中的 CanAI → AI Client(端点 URL、API 密钥)以及 MCP 客户端配置;不得擅自采用变通方案。

本地脚本胶水代码——使用 Node,切勿使用 Python

核心流程完全不需要任何本地脚本:网站的读取/写入通过 MCP 工具调用(由宿主提供的原生机制)完成,HTML/CSS/JS 编辑则在Agent 中进行(“在 Agent 中构建最终字符串”)。只有两个步骤会调用 shell——二进制文件 侧载(如下)和 Tailwind 编译(npx tailwindcss@^3,本身就是 Node)。

如果某个步骤确实需要本地胶水代码——例如 SHA-256 哈希、组装临时文件、提取内联 tailwind.config 或多部分上传——请使用 Node(node -e / npx),切勿使用 Python 或通过 pip 安装的工具。 Node 一定可用:你是通过 npx skills add 安装此技能的,因此 node 和 npx 已在 PATH 中。使用 python/python3/requests 会增加环境可能不具备、且本技能不做假设的依赖。(这与上文的规则不同,且不会削弱该规则:MCP 传输绝不能用任何语言重新实现。在 Node 中对临时文件计算哈希没问题;但用 Node 手动实现发往 …/wp-json/mcp/wpcanai 的 HTTP 请求仍然是禁止的。)

前置条件(简要)

  • 安装了 CanAI、wordpress/abilities-api 和 wordpress/mcp-adapter 的 WordPress。
  • MCP 端点: {site}/wp-json/mcp/wpcanai(从 WP Admin → CanAI → AI Client → Connections 复制)。
  • 供 @automattic/mcp-wordpress-remote 使用的 API 密钥,设置为 JWT_TOKEN(或使用你客户端的环境变量)。

将以下内容添加到你的 MCP 客户端服务器配置中(示例;各客户端的键可能不同):

{
  "mcpServers": {
    "canai-mcp": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
      "env": {
        "WP_API_URL": "https://example.com/wp-json/mcp/wpcanai",
        "JWT_TOKEN": "<paste-wpcanai-api-key>"
      }
    }
  }
}

重要提示: WP_API_URL 必须是完整路径 …/wp-json/mcp/wpcanai,不能只有网站主页 URL——请使用 AI Client → Connections 中的值。

使用 **wpcanai-list-templates**(或客户端中 CanAI 服务的工具列表)进行验证。

CanAI 前置设置(在网站上)

在依赖 CanAI 输出完整页面之前,网站应具备:

  1. 最低模板要求——至少有一个布局(template_type 为 layout)、一个页眉(header)和一个页脚(footer)。布局会包裹 {{ page_content }};页眉/页脚通过 {{ wpcanai_template('slug') }} 引入。
  2. CanAI Starter(主题别名 wpcanai-empty)——安装并启用最精简的 CanAI 主题。它与插件捆绑提供,位于 canai/theme/wpcanai-empty/,也可从 WP Admin → CanAI(欢迎页面)安装。
  3. 捆绑库——在 CanAI → Settings 中,确认 Load Tailwind CSS Play CDN 设为 Yes,且 Resource Source 设为 Internal(捆绑脚本——自插件 v1.58.2 起为默认值);但如果你使用 wpcanai-setup,则无需手动设置(推荐设置会自动启用这些选项)。按需启用 Lucide 和 Alpine。
  4. 布局中的 Tailwind 配置——在布局的 _canai_html 中,于 {{ wp_head() }} 之后的 <script> 中输出 **tailwind.config = { ... }**,确保其在 CanAI 注入到 wp_head 的 Tailwind 脚本之后运行。
<head>
  {# Meta & Viewport #}
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {{ wp_head() }}
  {# Tailwind CSS Config #}
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            brand: { DEFAULT: '#0f766e', dark: '#115e59' },
          },
        },
      },
    }
  </script>
</head>
推荐设置(一次 MCP 调用)

如果缺少前置条件,或用户要求**“继续完成所有推荐设置”,请先调用 **wpcanai-setup { }。它会安装/启用 WPCanAi Starter(wpcanai-empty)、创建默认布局(通过插件的默认布局辅助程序)、添加基本页眉/页脚模板、创建主页、设置静态首页、设置 **wpcanai_default_layout**,并启用 Tailwind CSS(wpcanai_tailwind_settings:load_tailwind = yes,source = plugin / 内部捆绑的 Play CDN)。

对于 WordPress / WooCommerce / CanAI 选项(例如静态首页、users_can_register、结账/账户注册、Tailwind 设置),请使用 **wpcanai-read-settings** / **wpcanai-update-settings**。对于存储在 wp_options 中的其他插件选项,请在网站所有者于 CanAI → AI Client → Guardrails 中配置允许列表(读取列表、自动应用列表和可选的审批列表)后,使用 **wpcanai-get-option** / **wpcanai-update-options**。要创建带有可选 CanAI 元数据的新页面,请使用 **wpcanai-create-page**。

菜单——两个位置,绝不硬编码

导航绝不硬编码。 页眉使用 {% for item in get_menu('primary')|filter(i => i.depth == 0) %} 渲染,页脚使用 get_menu('footer'),除此之外什么也不做——不添加 <a href> 列表,不使用回退链(既不调用第二个 get_menu(),也不在循环中用 {% else %} 写硬编码链接),不使用其他位置名称,也不添加 current_lang() URL 前缀(URL 和标题已经由 get_menu() 本地化)。链接只需通过 wpcanai-write-menu 编写一次。如果源标记中有导航列表,先读取其中的链接,用 write-menu 写入,然后将该列表替换为 get_menu() 循环。

位置只有 primary 和 footer(插件 v1.88.0)。绝不要自创第三个名称(topnav、main、mobile、header):write-menu 会在运行前拒绝它(“input[location] is not one of primary and footer”),而 wpcanai-scan 会将使用此类名称的 get_menu() 调用报告为 unknown_menu_location。如果移动抽屉要显示页眉链接,就再循环一次 get_menu('primary')。链接不同的语言要为同一位置单独创建菜单(插件 v1.89.0,参见按语言设置菜单)——仍然不能使用第三个位置。

工作流——一次处理一个导航列表:

  1. 先查看——wpcanai-get-menus { } 会显示各位置包含的内容(locations.primary / locations.footer;未分配内容时为 null)。
  2. 按顺序读取列表中的链接。 链接文本是 title。指向本网站页面的链接是 page(其 id 或 slug)——先创建页面;无法解析的 page 会被标记为 unknown_page,不会写入。slug 的解析方式与 slug_url() 相同(插件 v1.96.0):当只有一个页面使用该 slug 时,会匹配其子页面;否则按路径("page": "gallery/theatrettes")或 id 匹配;多个页面共用的裸 slug 会被标记为 ambiguous_page(插件 v1.97.0)。其他链接使用 url;仅当源链接会在新标签页中打开时才添加 "target": "_blank"。嵌套列表使用 children(最多 3 层)。不作为链接的下拉菜单触发项或 mega menu 栏目标题应设为包含 children 且不含 page 或 url 的项目;链接的一句话简介使用 description,图标使用 icon(Lucide 名称),缩略图使用 image(由 wpcanai-sideload-url 返回的附件 id)——详见下文的Mega menu(插件 v1.89.0)。
  3. 写入——wpcanai-write-menu { "location": "primary", "items": [ { "title": "Home", "page": "home" }, { "title": "Services", "page": 42, "children": [ { "title": "Design", "page": "design" } ] }, { "title": "Docs", "url": "https://docs.example.com", "target": "_blank" } ] }。列表会替换该位置的现有项目;"merge": true 则会更新标题相同的项目,并追加其余项目。
  4. 将列表替换为页眉或页脚模板中的循环:
    {# Navigation / Primary #}
    <nav aria-label="{{ t('Primary') }}">
      {% for item in get_menu('primary')|filter(i => i.depth == 0) %}
        <a href="{{ item.url }}" class="{{ item.active ? 'font-semibold' : '' }}"{% if item.target %} target="{{ item.target }}" rel="noopener"{% endif %}>{{ item.title }}</a>
      {% endfor %}
    </nav>
    
    使用 |filter(i => i.depth == 0) 循环顶层项目(插件 v1.89.0)。get_menu() 会以一个扁平列表返回所有项目,包括子项目,并且每个项目都带有 children;下拉菜单会在顶层项目内循环 item.children,因此不使用筛选器时,每个子项目都会渲染两次(wpcanai-scan:menu_children_twice)。对于单层菜单,筛选器不会改变结果;仍然要写上,以免之后添加子项目时出现重复。保留源 <nav> 和 <a> 上的类。
  5. 扫描——对于该模板,wpcanai-scan 不得报告 hardcoded_nav、unknown_menu_location、menu_fallback_chain、menu_children_twice 或 menu_not_assigned。
  • write-menu 会写入分配给该位置的现有菜单;绝不会重命名或替换它。 如果某个站点的 primary 中是“Journal Primary”,它就会保留为“Journal Primary”。只有尚未分配菜单的位置才会获得一个新菜单,并在同一次调用中将其命名为 Primary / Footer 并分配给该位置。
  • canai-prepare 生成的 menus.json 与 pages.json 放在一起,格式为 { "primary": [ … ], "footer": [ … ] },且已经符合 write-menu 的 items 格式:对于每个键,在其指定的页面创建完成后调用一次 write-menu。其中的 page 值是 pages.json 中的 slug;如果站点上的 slug 与之不同(例如 index 成了首页),则传入已创建页面的 id。生成的 HTML 会标记每个导航 <nav data-canai-menu="primary"> / "footer";循环会替换该 <nav> 中的链接。
  • 翻译无需修改模板。 在非默认语言下,get_menu() 会从字符串存储中返回每个 title 和 description,并为每个内部 url 添加语言前缀;raw_title / raw_description 保存未翻译的文本。菜单标题和描述与其他字符串一样进行翻译——请参阅 原生字符串翻译。例外情况是拥有独立菜单的语言:请参阅 按语言设置菜单。
  • 升级说明。 在插件 v1.88.0 之前编写的模板会调用 get_menu('wpcanai_primary') / get_menu('wpcanai_footer')。这两个名称仍可解析,且 wpcanai-scan 会将每个此类调用报告为 legacy_menu_location;请使用 wpcanai-replace-in-meta 将它们重命名({ "post_id": <id>, "replacements": [{ "from": "get_menu('wpcanai_primary')", "to": "get_menu('primary')" }, { "from": "get_menu('wpcanai_footer')", "to": "get_menu('footer')" }] })。wpcanai-get-menus 会在 legacy_assignments 中列出仍分配在旧名称下的菜单。
Mega 菜单——标题、图标、描述、图片(插件 v1.89.0)

丰富的导航也属于菜单数据:切勿将 Mega 菜单的链接硬编码,因为这些链接还带有图标或简介。

  • 项目字段。 除了 title / page / url / target / classes,write-menu 项目还接受 description(单行文本)、icon(Lucide 图标名称,格式为 [a-z0-9-])、image(附件 id——请先将图片 sideload)和 attr_title(链接的 title 属性)。get_menu() 项目会返回这些字段,以及 id、parent 和 depth(顶层为 0)。站点所有者可在“外观” → “菜单”中编辑相同字段(“描述”和“标题属性”位于“屏幕选项”下;每个项目都可设置图标和图片)。
  • 标题。 带有 children 且既没有 page 也没有 url 的项目是标签,而非链接:get_menu() 会以 url: '' 返回该项目。可将其用作下拉菜单触发项或 Mega 菜单的列标题。没有子项的项目仍需设置 page / url 之一。
  • 写入——单次调用,最多三级:
    { "location": "primary", "items": [
      { "title": "Services", "children": [
        { "title": "Build", "children": [
          { "title": "Websites", "page": "websites", "icon": "monitor", "description": "Fast sites, built to be edited." },
          { "title": "Shops", "page": "shops", "icon": "shopping-bag", "description": "WooCommerce stores that sell." } ] },
        { "title": "Grow", "children": [
          { "title": "SEO", "page": "seo", "icon": "search", "description": "Found by the people looking." } ] } ] },
      { "title": "Contact", "page": "contact" } ] }
    
  • 渲染——筛选顶层项目,将标题渲染为 <span>,并为链接显示图标和描述:
    {# Navigation / Primary #}
    <nav aria-label="{{ t('Primary') }}" class="flex items-center gap-6">
      {% for item in get_menu('primary')|filter(i => i.depth == 0) %}
        {% if item.children is empty %}
          <a href="{{ item.url }}" class="{{ item.active ? 'font-semibold' : '' }}"{% if item.target %} target="{{ item.target }}" rel="noopener"{% endif %}>{{ item.title }}</a>
        {% else %}
          <div class="relative" x-data="{ open: false }" @click.outside="open = false">
            <button type="button" @click="open = !open" :aria-expanded="open">{{ item.title }}</button>
            <div x-show="open" class="absolute left-0 mt-2 grid grid-cols-2 gap-8 rounded-xl bg-white p-6 shadow-lg">
              {% for column in item.children %}
                <div>
                  {% if column.url %}<a href="{{ column.url }}" class="font-semibold">{{ column.title }}</a>{% else %}<span class="font-semibold">{{ column.title }}</span>{% endif %}
                  {% for link in column.children %}
                    <a href="{{ link.url }}" class="mt-3 flex gap-3">
                      {% if link.image %}<img {{ image_attrs(link.image, 'src:thumbnail,alt') }} class="h-10 w-10 rounded object-cover">{% elseif link.icon %}<i data-lucide="{{ link.icon }}" class="h-5 w-5"></i>{% endif %}
                      <span>{{ link.title }}{% if link.description %}<small class="block text-slate-500">{{ link.description }}</small>{% endif %}</span>
                    </a>
                  {% endfor %}
                </div>
              {% endfor %}
            </div>
          </div>
        {% endif %}
      {% endfor %}
    </nav>
    
    页面已经调用过一次 lucide.createIcons();Alpine x-show 面板中的图标从一开始就存在于 DOM 中,因此能够正常渲染。
  • merge: true 会保留未提供的字段;""(对于 image 则为 0)会清除字段。
按语言设置菜单(插件 v1.89.0)
  • 翻译,而不是重复创建。 如果某种语言显示的链接相同,请保留一个菜单:其标题和描述通过字符串存储进行翻译,内部 URL 会添加语言前缀(参见 原生字符串翻译)。这通常是默认做法,也是几乎所有情况下的正确选择。
  • 仅当链接不同时才为某种语言设置菜单——例如仅马来语页面或更精简的页脚。wpcanai-write-menu { "location": "footer", "lang": "ms", "items": [ … ] } 会为该位置写入该语言的菜单,首次调用时创建 Footer (Bahasa Melayu)(该语言的本地名称),并将其分配给该语言。菜单项会以该语言写入:get_menu() 会按原样渲染这些菜单项(不会经过字符串存储处理),并仍会为内部 URL 添加前缀。
  • 没有第三个位置。 语言菜单是由 CanAI 存储的分配关系,而非已注册的位置:外观 → 菜单 → 管理位置仍会列出 Primary 和 Footer,模板仍会调用 get_menu('primary') / get_menu('footer'),这些内容均不会改变。
  • 切换回默认菜单,使用 wpcanai-assign-menu { "location": "footer", "lang": "ms", "menu_id": null }——马来语随后会再次显示翻译后的默认菜单(语言菜单会保留,但取消分配)。使用带 menu_id 的 assign-menu 会将某种语言(若不带 lang,则为该位置本身)指向现有菜单。
  • 从副本开始(v1.93.0):wpcanai-write-menu { "location": "footer", "lang": "ms", "copy_default": true } 会从默认菜单创建 Footer (Bahasa Melayu),并预先翻译字符串存储中已有的标签;之后可编辑不同之处。
  • 查看菜单,使用 wpcanai-get-menus:languages.ms.footer 是该语言的菜单,若为 null 则继承翻译后的默认菜单。(v1.93.0) 网站所有者也可在 CanAI → 翻译 → 菜单中进行相同操作。对于每种语言和位置,有两个选项:
    • “与英语使用相同的菜单,并将标签显示为 Bahasa Melayu”:显示标签数量,并提供“翻译标签”,点击后会在外观 → 菜单中打开该菜单;
    • “使用包含不同链接的独立菜单”:选择一个菜单,或“从副本创建一个”。
  • 字符串。 对于某个位置有自定义菜单的语言,wpcanai-i18n-list-strings 和“字符串”标签页会略去该位置的默认菜单标题和描述:它们不会以该语言显示。
将 HTML 转换为 CanAI(Agent 工作流)

使用 MCP 工具将静态 HTML 文件(例如 index.html)转换为 CanAI 时:

Source 是 canai-replicate 迁移工具包?请推送其 output/push/*.json 文件,不要推送原始的 output/pages/*.html / output/templates/*.html 文件。 每个 canai-replicate 输出文件都是完整的独立 HTML 文档(按设计包含 <!DOCTYPE>/<html>/<head>/<body>,可通过 file:// 打开进行本地预览)。CanAI 自己的渲染路径也会将 _canai_html 包装在自己的文档外壳中,因此将工具包文件的原始内容逐字写入 _canai_html 会产生重复且无效的标记: 2 个 <!DOCTYPE html>、2 个 <html>、2 组 <head>/</head>、2 个 <body> — 已在实际环境中复现(dogfood A2,缺陷 #1 — 严重)。请先运行 "$HOME/.claude/skills/canai-replicate/bin/replica" pushprep <site>; 此命令会确定性地移除外壳和预览库代码块,并为每个文件在 output/push/<slug>.json 中写入一个可直接推送的 { title, slug, template_type, html, css, js, warnings } JSON 文件 — 将其中的 html/css/js 写入 _canai_html/_canai_css/_canai_js, 并在新建 wpcanai-create-template 调用时,将非 null 的 template_type 作为 template_type 术语读取。不要手动再次移除外壳;也不要因为“文件看起来很简单” 就跳过 pushprep。

必须强制为 header.json/footer.json 设置 SLUG。 wpcanai_template('header') / ('footer') 根据已创建模板文章的确切 slug 进行解析,而不是根据 标题或 template_type 术语。使用 wpcanai-create-template 创建这两项时,标题必须 恰好是 header/footer,或转换后的 slug 恰好为 header/footer — 类似“Header — Acme migration”的描述性标题会自动生成不同于该纯字符串的 slug, 此后整个站点内每个 {{ wpcanai_template('header') }} 调用点都会静默解析为空, 且任何地方都不会报错(dogfood A2 缺陷 #7,已在实际环境中复现)。 继续之前,请使用 wpcanai-list-templates 确认返回的 slug 是该纯字符串。 请先推送这两项,再推送任何包含它们的内容。 优先使用 CanAI 模板函数——不要把辅助函数已提供的内容硬编码。 编写 _canai_html / _canai_css / _canai_js 时,只要有适用的已注册 CanAI Twig 函数,就应使用它,而不是硬编码值或标记。在正式网站上,硬编码资源 URL、内部链接和面向用户的字符串都会出错(基础路径错误、slug 已更改、URL 不是固定链接、没有翻译),就像相对路径 ./images/hero.jpg 会返回 404 一样。将源值映射到辅助函数:

源代码中的硬编码内容改用
<img src="…">、srcset、CSS url(…){{ image_attrs(id, 'src,alt') }} / {{ media_url(id, size) }}
内部 <a href="/about"> / href="about.html">对按 slug 标识的文章使用 {{ slug_url('about') }};对其他站点路径使用 {{ turl('/path/') }}(turl('/') 是主页);按 ID 使用 {{ turl(id_url(id)) }} / {{ turl(post_url()) }} / {{ turl(term_url(id)) }}——slug_url() 和 turl() 会遵循当前语言
页眉 / 页脚导航列表(<nav>,或 <header> / <footer> 中的链接 <ul>){% for item in get_menu('primary')|filter(i => i.depth == 0) %} / get_menu('footer'),通过 wpcanai-write-menu 一次性编写链接(见上文 菜单)——包括超级菜单中的图标、简介和标题
home_url() 加语言前缀、base 或 current_lang() URL 变量无需拼接:turl()、slug_url() 和 get_menu() 会自行应用当前语言
字面写入的面向用户的字符串页面字符串使用 {{ t('…') }}(见 原生字符串翻译);只有当字符串已包含在主题或插件的 gettext 目录中时,才使用 {{ __('…') }} / {{ _x() }} / {{ _n() }}
应为动态内容的静态文章 / 标记{{ the_content() }} / {{ shortcode('[…]') }}
每个页面中重复的共享页眉 / 页脚 / 局部模板{{ wpcanai_template('slug') }}

仅当没有适用的辅助函数,或确实需要绝对外部 URL(例如 OG/Twitter 元数据、Web 清单图标、第三方来源)时,才将值硬编码。图片(步骤 5 + 预先侧载)和内部链接(步骤 6)就是此规则的具体示例。参见 references/REFERENCE.md → 媒体与链接和国际化(i18n)。

  1. 不要在布局 _canai_html 中添加字面量 <title>——{{ wp_head() }} 会输出文档标题(WordPress / SEO 插件)。
  2. 将页面级 JavaScript(例如 lucide.createIcons()、Alpine 初始化、自定义处理程序)放在 _canai_js 中,而不是作为内联 <script> 放在 _canai_html 中。CanAI 会在 wp_footer() 注入 _canai_js。_canai_js 会通过 Twig 渲染(与 _canai_html 使用同一引擎)——其中的 {# #} 注释有效,并会在输出脚本前移除;对于任何提及内部实现的内容,优先使用它们,而不是 /* */。布局 _canai_html 中唯一允许内联的 <script> 是 tailwind.config = { ... }(放在 {{ wp_head() }} 之后,这样它会在 CanAI 注入的 Tailwind 脚本之后运行)。
  3. 复用布局:先调用 wpcanai-list-templates。如果已有布局,则将其 id 复用于 _canai_layout / 引用——除非用户要求第二个外壳,否则不要创建重复的布局模板。
  4. **页面正文与布局:**将 <main> / 主要页面标记放在通过 wpcanai-create-page 创建的 page 中(将 layout 设为布局模板 ID),而不是放在类型为 layout 的 wpcanai_template 中。布局模板是文档外壳({{ page_content }}、页眉/页脚 include);页面正文的 template_type 不是 layout。
    • 嵌套页面(插件 v1.96.0)。 /gallery/auditorium/ 下的源页面是子页面,而不是顶级页面 gallery-auditorium。先创建父页面并发布(status: "publish",默认值),然后按路径顺序创建各个子页面,设置 parent: <parent id>,并将路径的最后一段设为其 slug(auditorium)。**绝不要把 /a/b/ 展平为 a-b slug。**复用已存在的父页面:使用 wpcanai-list-pages { "all_pages": true }(插件 v1.97.0)查找,它会列出所有页面——包括普通 WordPress 页面和草稿——并显示其 path 和 status。在 1.96.x 中,此标记会被忽略:返回的行没有 status,且仅包含已发布的 CanAI 页面。请用户提供普通页面或草稿父页面的 id。若创建页面时出现警告,提示 slug 已保存为 gallery-2,则表示 gallery 已存在。使用返回的 id 链接到页面——{{ turl(id_url(<id>)) }},它在页面移动后仍然有效;或者使用 slug_url() 配合返回的 slug(如果 slug 重复,则使用 path;参见规则 6)。绝不要直接粘贴 url 或自行构造路径。要移动、重命名或更改已有页面的 slug,请使用 wpcanai-update-page;移动已发布页面后,按其 URL 更改警告中的说明搜索并修正旧路径(参见 wpcanai-update-page)。
  5. 在编写 HTML 前自动检测并上传本地媒体。静态网站文件夹几乎总会引用本地资源(./images/hero.jpg、assets/logo.svg、videos/intro.mp4、favicon、OG 图片、CSS url(...) 背景)。如果原样推送 HTML/CSS,这些引用在正式网站上都会返回 404。调用任何 wpcanai-create-page / wpcanai-write-meta 之前,先运行静态网站资源上传预处理(下一小节),这样编写的 HTML 才会通过 {{ image_attrs(...) }} / {{ media_url(...) }} 按 ID解析媒体,而不是使用相对路径或固定的上传 URL。
  6. 将内部链接改写为 CanAI 辅助函数(遵循上述原则)。静态源码中会保留 <a href="about.html"> / href="/about">;这些链接在正式网站上会失效(基础路径错误、非固定链接、slug 已移动)。将每个内部链接改写为对应的辅助函数:跨页面链接 → 已知目标页面 id 时(来自 wpcanai-create-page 或 wpcanai-list-pages),使用 {{ turl(id_url(<id>)) }};否则使用 {{ slug_url('<slug>') }}。对于子页面(插件 v1.96.0),slug_url() 会先匹配精确路径,再匹配仅由一个页面拥有的完整 slug:slug_url('auditorium') 可以找到 /gallery/auditorium/,即使页面移动后也能继续找到。顶级页面优先于与其共享 slug 的子页面(从插件 v1.97.1 起,已发布的顶级页面也优先于保留相同 slug 的草稿;两个已发布页面仍会产生歧义);当两个子页面共享 slug 时,单独使用 slug 无法解析(href="",插件 v1.97.0 起,wpcanai-scan 会报告 link_unresolved),因此请传入路径——slug_url('gallery/theatrettes')。路径链接在页面移动后不会继续有效(会渲染为 href=""),因此优先使用 id。术语 / 归档链接 → {{ turl(term_url(<term_id>)) }};其他站点路径 → {{ turl('/path/') }}。页眉或页脚导航列表不应逐个改写链接——它应改为 get_menu() 循环(参见菜单)。**保持以下链接不变:**外部 / 绝对链接(https://other.com)、协议相对链接(//cdn…)和仅锚点链接(#section)。在清单中报告这些改写。
  7. **将区块注释规范化为 Twig 导航标签。**把 canai-prepare 输出的每条 HTML 导航注释都转换为精确格式 {# Type / Short Label #}——例如 {# Section / Hero #}。绝不要将 HTML 导航注释带入 _canai_html(它们会污染渲染输出并暴露源码供侦察)。确保每个地标元素的对应类型标签紧挨在其开始标签之前:<main> → Container,<section> → Section,<header> → Header,<footer> → Footer,<nav> → Navigation,<aside> → Sidebar。请遵循 references/STRUCTURE-NAVIGATION.md 中的受控词汇和精确转换规则。普通实现说明使用 {# @dev … #},不会出现在编辑器的结构大纲中。canai-replicate 的 pushprep 产物可能使用较旧约定;写入前请先规范化。
  8. 完成编写操作(create-page / create-template / implement / import / preset)后,运行 wpcanai-scan 并清除内容问题,然后才能宣称完成。通过 wpcanai-replace-in-meta 进行的精确修改(类名、字符串、URL)无需扫描——跳过即可。将 missing_structure_comment、invalid_structure_comment、leaky_comment 和 leaky_secret 视为布局损坏问题:添加或修正 Twig 导航标签,将 HTML / /* */ 注释转换为 Twig 注释,并从 meta 中移除类似机密的字面量。还要清除导航问题(插件 v1.88.0:hardcoded_nav、legacy_menu_location、unknown_menu_location、menu_fallback_chain、menu_children_twice、menu_not_assigned、hardcoded_lang_prefix、unlocalised_internal_href;menu_children_twice v1.89.0),以及原生国际化站点上的 split_sentence(将一个句子拆成多个 t();合并它,参见原生字符串翻译步骤 2)和 unsupported_t_markup(在 t() 中使用块级元素或未配对标签,会显示为文本;插件 v1.94.0):每条消息都会指出修复方法,上方的菜单部分介绍了工作流。render_warning(v1.90.0)表示页面渲染期间某行输出了数组——用相同方式修复(参见 wpcanai-scan 下的渲染警告)。清除链接问题(插件 v1.97.0;参见 wpcanai-scan 下的链接问题):link_unresolved 表示字面量 slug_url() 未匹配到任何页面或匹配到多个页面,因此渲染出的值为 href=""。对于多个匹配项,请传入路径;对于没有匹配项,请修正 slug;或者使用 turl(id_url(<id>))。link_moved 表示字面量 turl() / href 路径指向的页面已移动——将其替换为消息中列出的 turl(id_url(N))。要扫描整个站点,请使用下方的注释 / 机密安全检查配方。
博客文章(非 CanAI 页面)

博客文章是普通的 WordPress 内容,而不是 CanAI 元页面。使用 wpcanai-write-post 编写,并传入结构化区块列表——正文会变成原生区块标记,网站所有者可以在区块编辑器中编辑;它会通过博客套件的 blog-single-post 模板渲染,无需额外操作。先将所有图片侧载,再通过附件 ID 引用。不要对文章使用 wpcanai-create-page 或 wpcanai-write-meta。

  • 文章中的图表(插件 v1.77.0)。 图表是数据,不是图片:传入 chart 区块(参见区块表),并提供 labels 和最多 8 个 datasets——如果有第 9 个系列,应将它合并到“Other”,而不是拆分图表;也不要侧载渲染后的图表图片。设置 unit(ms、%、MB),让刻度、工具提示和表格显示为“12 ms”,而不只是“12”。每个图表还会将数值渲染为无障碍数据表(默认折叠的 <details>;table: "open" 会展开表格,"hidden" 则会保留表格供屏幕阅读器和 Markdown 使用),因此不支持 JS、阅读模式、RSS 和 Accept: text/markdown 的读者无需你添加 table 区块也能看到数据。网站所有者可以在 Settings → Libraries → Chart.js 下关闭运行时——选项 wpcanai_chart_settings(load_chart auto|yes|no,source plugin|cdn),这是一个一等设置键,可通过 wpcanai-read-settings 读取,并通过 wpcanai-update-settings 修改;设为 no 时只显示图表对应的表格,因此在报告图表缺失为错误之前,先读取此设置。
阅读模式(Safari 阅读器 / Firefox 阅读视图 / Chrome)

当用户要求某个页面或文章“支持阅读模式”(或阅读视图 / Safari 阅读器)时,先执行 references/READING-MODE.md 中的门槛检查:读取目标内容,对照参考文档中的表格进行分类;如果该内容形式不建议使用阅读模式(落地页、列表页、WooCommerce 交易页面、表单页面、内容单薄的文案),则停止并说明原因。只有内容确实是文章时,才继续遵循参考文档中的标记检查清单(在 <main> 内使用 <article>,并在其中放置 <h1>、<time> 和带有 rel="author" 的署名;正文放在 <p> 中;导航、相关内容和 CTA 放在外部)。没有启用阅读模式的插件设置或元标签——它完全取决于浏览器根据标记进行的启发式判断,因此切勿声称存在开关。

面向 Agent 的 Markdown(Accept: text/markdown)

当用户询问网站是否向 AI Agent 和爬虫提供 Markdown、希望某个页面能被其读取、询问 Accept: text/markdown / ?format=markdown,或希望关闭相关行为时,请遵循 references/MARKDOWN-FOR-AGENTS.md(插件 v1.76.0)。CanAI 接管页面响应 Markdown 请求时,会返回页面的内容槽——而不是布局——作为 YAML front matter 加转换后的正文,因此 READING-MODE.md 中的标记规范能确保 Markdown 内容整洁。内容协商按请求单独进行,浏览器不会看到它,因此不会影响正常访问。唯一的控制项是全站选项 wpcanai_markdown_negotiation(默认开启),这是一个一等设置键,可通过 wpcanai-read-settings 读取,并通过 wpcanai-update-settings 修改——切勿使用通用选项允许列表。WooCommerce 购物车 / 结账 / 我的账户 / 订单页面始终返回 HTML,没有逐页覆盖设置,也无法强制被排除的页面返回 Markdown,因此切勿声称存在此类功能。(v1.80.0)缓存: 由 Accept 标头选择的 Markdown 响应会发送 Cache-Control: private, no-store(以及 LiteSpeed no-cache),因此页面缓存绝不会将其提供给浏览器;?format=markdown 是可缓存的 URL,且公布的 rel="alternate" 链接会指向该 URL。需要考虑缓存时,请向爬虫和用户提供 ?format=markdown URL。

页面格式标记

每个页面都有一个 format 标记:"twig"(默认值,也是此技能唯一会编写的格式)或 "blocks"。wpcanai-read-meta 默认会返回该标记,wpcanai-list-pages 的行中也会包含它。若页面标记为 blocks,则写入 html 或 js 会被拒绝,并返回 format_mismatch,且不会产生任何更改;错误信息会附带一段提示,供你转交给网站所有者;请转交,而不是重试或绕过限制。对于 blocks 页面,单独写入 css 是允许的(插件 v1.67.0):这是页面的纯 CSS 插槽,写入 _canai_css,并在 head 中原样输出。layout、context 和 tailwind_* 也可以写入。

静态网站资源旁加载预处理

当用户运行 /canai-mcp implement <folder>、要求你导入静态网站,或推送 canai-replicate 迁移工具包的 output/push/*.json 构件时触发(pushprep 构件中的 html 仍包含源网站原始的绝对路径/热链图片 URL——canai-replicate 不负责旁加载;这正是此技能的工作内容,与任何其他静态网站导入一样)。若未进行旁加载,一旦源网站启用自己的热链保护,图片就可能返回 404 或显示损坏(已确认的线上问题:dogfood A2 缺陷 #6)。此流程在任何 HTML/CSS 写入 CanAI meta 之前一次性执行。

  1. 枚举引用的资源。 使用 Read / Grep / Glob 扫描文件夹,提取所有解析到所提供文件夹内部文件的引用。至少涵盖:

    • HTML 属性:src、srcset(每个候选项)、<link rel="icon"> / apple-touch-icon / manifest 的 href、<video poster>、<source src>、<object data>、<embed src>、OG / Twitter meta(og:image、twitter:image)。
    • CSS:<style> 块、内联 style="…" 以及文件夹中任何 .css 文件内的 url(...)。
    • JS:内联 <script> 和打包的 .js 文件中的字符串字面量(若显然是资源路径,例如 "./images/foo.png");跳过动态/模板化的值。

    **跳过:**指向其他来源的绝对 http(s):// URL、协议相对的 //cdn…、data: URI、mailto: / tel:、仅锚点 #…,以及无法解析为文件夹内真实文件的路径。按解析后的绝对路径去重。

  2. 写入标记前,预检每一张有意义的栅格图片。 使用可用的本地图像信息工具读取其固有宽度和高度,并使用宿主的图像查看器检查像素。记录宽高比(width / height)和重要区域:面部、头部、全身、共同主体、手部、产品、奖项或烘焙在图片中的文字。徽标、图标、装饰纹理以及使用 object-contain 的图片无需分析主体裁切。对于远程源图片,如果可能,应在旁加载前检查源图/抓取的资源;若无法检查,应使用 object-contain 或接近源图的宽高比保留图片,而不是猜测会造成破坏的裁切。

    优先使用响应式宽高比容器,而不是浅层固定 h-* 裁切:纵向原图 → aspect-[4/5]、aspect-[3/4] 或 aspect-square;正方形 → aspect-square 或 aspect-[4/3];横向 → aspect-[16/9]、aspect-[3/2] 或 aspect-[4/3]。使用 object-cover 时,应从检查过的构图开始;可参考的起始值:头像/单人肖像使用 object-[center_30%](Y 轴 25–35%),坐姿双人使用 object-[center_58%](Y 轴 55–60%),站立群像使用 object-[center_20%],手持物品的主体使用 object-[center_80%](Y 轴 75–85%),并至少搭配 aspect-[3/4] 或高度相当的容器。主体偏离中心时,需要指定两个坐标,例如 object-[80%_25%]。这些只是起始参考值,不是盲目套用的默认值。

    部署前,预览窄屏手机和宽屏桌面的裁切效果。面部、头部、共同主体以及与任务相关的物品都必须清晰可见,且不能紧贴边缘。必要时使用断点专用的宽高比/焦点类;若无法找到同时适用于两种尺寸的裁切方式,则使用更高的容器/与源图匹配的容器,或使用 object-contain。

  3. 逐个侧载每个唯一文件:通过 POST {site}/wp-json/wpcanai/v1/sideload(准确的 curl 命令请参阅 上传媒体)。每次只上传 一个文件,以便记录每个 id / source_url。如果源 HTML 提供了 alt(<img alt="…">),请传入合适的 alt;否则省略。重复上传相同字节没有问题——WordPress 会按文件名去重,但 API 每次调用都会返回一个新的附件,因此映射中的缓存命中很重要。

  4. 构建以媒体 id 为键的重写映射——{ <original-reference-string>: <media id> },而不是原始 source_url。键必须使用源文件中出现的 确切字符串(例如 ./images/hero.jpg、images/hero.jpg、/images/hero.jpg——这三个引用都映射到同一个上传 id)。CanAI 会在渲染时根据 id 解析实时 URL,因此即使媒体被重新生成或站点迁移,页面仍能正常显示;固定的 source_url 则不行。(第 5 步中只有 OG/清单例外会保留绝对 source_url。)

  5. **在调用任何 wpcanai-write-meta 之前,在内存中将 HTML / CSS / JS 重写为基于 ID 的辅助函数。**将其应用于第 1 步扫描的相同内容表面,并根据表面使用正确的形式:

    • <img> → 移除硬编码的 src / alt,并插入 {{ image_attrs(<id>, 'src,alt') }}(已知尺寸时使用 'src,alt,width,height')。保留现有的 class / 其他属性,包括预先检查过的宽高比容器和焦点位置工具类;在首屏以下保留或添加 loading="lazy"。
    • CSS url(...)(位于 _canai_css / 内联 style 中)、<source srcset>、<video poster>、<link rel="icon"> / apple-touch-icon → {{ media_url(<id>, 'full') }}(Twig 渲染的表面)。分别重写 srcset 中的每个候选项,并保留其 1x / 2x / 480w 描述符。
    • OG / Twitter 元数据(og:image、twitter:image)和 Web 清单图标 → 保留绝对 source_url。这些需要绝对 URL,且无法可靠地通过 Twig 渲染——这是基于 ID 重写规则中唯一有文档说明的例外。

    不要修改磁盘上的文件——重写由 Agent 在内存中完成,重写后的字符串就是传给 html / css / js 的内容。

  6. 完成后向用户提供清单:用简短表格列出 original path → dimensions/aspect → framing choice → media id → final Twig form——其中 framing choice 应说明容器宽高比和焦点工具类(或 object-contain);final Twig form 应为重写后的 {{ image_attrs(...) }} / {{ media_url(...) }},或者 OG/清单例外所用的绝对 source_url——并统计跳过的引用数量(注明原因:外部资源、数据 URI、文件缺失)。用户可据此抽查转换结果。

**幂等性。**如果用户重新运行导入,你会再次上传文件(该端点不会按内容哈希去重)。对于一次性导入,这可以接受;如果发现明显的重复运行迹象(例如,用户指定的文件夹与已构建模板时使用的文件夹相同),请提醒用户,并提供使用 wpcanai-list-media / wpcanai-get-media 复用现有附件的选项,而不是重新上传。

**此流程未涵盖的内容。**通过 @font-face { src: url(...) } 加载的字体处理方式相同,也应纳入流程。CSS-in-JS、webpack 风格的哈希包,或仅在运行时通过 fetch 引用的资源无法静态检测——请在清单的“跳过”部分注明这些情况,以便用户了解需要手动处理。

商店和归档分页(网站上的 Twig)

为 shop 或 product-category 委托处理器(或全局 WC 模板)编写 _canai_html 时,请使用注入的 products 和 pagination,并输出 {{ wc_pagination()|raw }} 或 {{ wpcanai_pagination()|raw }}。对于 category / tag / search / author / archive(包括 CPT 归档),请使用注入的 posts 和 pagination,并输出 {{ the_posts_pagination()|raw }} 或 {{ wpcanai_paginate_links()|raw }}。博客索引——在 Settings → Reading → "Posts page" 中设置的页面——也会注入相同的 posts 和 pagination;它是普通的 CanAI 页面(通过 wpcanai-read-settings → page_for_posts 查找其 id),其自身的 _canai_html 会渲染文章列表。该页面的编辑器预览会显示空状态,因为 posts 只存在于线上请求中。不支持 "Your latest posts" 首页模式。完整的数据结构和固定链接说明,请参阅 references/REFERENCE.md。

为 WooCommerce 区块设置样式(cart / checkout / order-received / my-account)

CanAI 的 wc_* Twig 辅助函数(wc_checkout_form()、wc_cart_totals()、my-account 端点)会输出原始 WooCommerce 内容;在实时页面渲染之前,其 HTML 结构不可见。而 order-received/order-pay 页面还会通过 WooCommerce 的访客邮箱验证关卡隐藏详情,因此无法直接查看。不要猜测选择器。 为这些区块设置样式之前:

  1. 使用你正在设置样式的上下文调用 wpcanai-get-wc-css-reference(例如 { "context": "order-received" })。
  2. 阅读 css_reference——这是 WooCommerce 针对该上下文选择器的默认规则。它是原始 SCSS 源码,并非编译后的 CSS:Sass 变量、混入和 & 父级引用均未解析(例如 darken($secondary, 10%)、@include …),因此应将规则改写为实际 CSS,而不是直接粘贴到 _canai_css 中。这些内容提取自已安装的 WooCommerce 的四个样式表(woocommerce.scss、woocommerce-layout.scss、woocommerce-smallscreen.scss、forms.scss),因此会随 WooCommerce 更新保持准确——请重新调用工具,不要依赖记忆中的类名。规则会按其实际祖先选择器链包装(例如 .woocommerce { table.shop_table { … } }),因此应匹配完整链以确保选择器优先级。包裹在 @media only screen and (max-width: 768px) 中的规则来自 woocommerce-smallscreen.scss,仅适用于移动设备——WooCommerce 会在入队时应用该断点,而不是通过文件本身应用,因此不要将其视为无条件规则。
  3. 阅读 template_files 中的文件(如果你有权访问安装目录的文件系统,则读取其中任意文件)——这些文件是权威标记;主题覆盖版本优先于 WooCommerce 自带版本。其中路径相对于 ABSPATH。文件包含核心提供但从不设置样式的类(例如感谢页面上的 .woocommerce-order-overview__order/__date/__total)。如果 templates_missing 非空,说明上下文映射与已安装的 WooCommerce 不一致——其中的路径是相对于上下文的(例如 order/order-details.php),与 template_files 的格式刻意不同,因此不能直接比较这两个列表。
  4. 检查 third_party_hooks。每个条目({hook, priority, callback, file, is_third_party})对应一个在该上下文中添加自身标记的插件(例如在感谢页面追加表格的配送时段插件)——设置样式前,请阅读其 file 以了解它输出的类。传入 "include_core": true,即可获取这些钩子上的所有回调,包括 WooCommerce 自身的回调;每个条目仍带有 is_third_party 标记——当你想全面了解渲染到该区域的内容,而不只是第三方添加的内容时,这很有用。
  5. 将针对这些选择器的覆盖样式写入页面/模板的 _canai_css(通过 wpcanai-write-meta),并与网站的设计系统保持一致。

如果 css_fallback 为 "full-file",说明未找到选择器提取结果(WooCommerce 进行了重构)——此时会返回整个样式表;请在其中搜索 template_files 中看到的类。

这是只读静态分析:不会渲染任何内容,也不会读取任何订单、购物车或客户数据。


MCP 工具参考(内联)

能力 ID 使用斜杠;MCP 工具名称使用连字符(wpcanai/read-meta → wpcanai-read-meta)。

工具标签(插件 v1.80.0;自 v1.81.0 起有七个标签)

每个 CanAI 工具描述都以方括号前缀开头,说明该工具会触及 CanAI 自身数据以外的内容,例如 [fluent-snippets, filesystem] Update a snippet …。调用不熟悉的工具前请先阅读;annotations.readonly 仍会说明工具是否会写入,而 uninstall-preset、restore-snapshot 和 restore-operation 则标记为具有破坏性。

标签工具读取或写入的内容
canaiCanAI 自身的数据:wpcanai_template、_canai_* 元数据、wpcanai_* 选项、快照、预设
coreCanAI 以外的 WordPress 数据:文章、页面、文章元数据、选项、媒体
woocommerceWooCommerce 页面和设置
fluent-snippetsFluent Snippets 文件和索引(canai-yolo 工具)
cache页面缓存插件的清除钩子(LiteSpeed Cache、WP Rocket、W3 Total Cache、WP Super Cache、WP-Optimize — v1.82.0)
filesystemwp-content 下的文件(代码片段存储、上传文件)
http来自服务的出站请求(远程加载、预设下载、网络诊断)

你看到的前缀是 针对站点解析后的结果:只有在第三方插件处于活动状态时才会出现相应标签(woocommerce、fluent-snippets)——没有该插件时,工具根本不会注册;只有启用了 Guardrails → Page cache → purge after writes(wpcanai_purge_after_write,默认开启)且检测到受支持的缓存插件时,才会出现 cache(purge-cache 和 diagnostics 始终带有此标签)。因此,在基础站点上,write-meta 显示 [canai];在 LiteSpeed 环境中则显示 [canai, cache]。声明的标签(插件 v1.81.0):

标签工具
canailist-templates、read-meta、grep-content、resolve-content-id、scan、get-pending、export、list-presets、list-snapshots、list-operations、get-snapshot、pin-snapshot、pin-current
canai, cachewrite-meta、replace-in-meta、create-template、import、restore-snapshot、restore-operation
canai, corelist-pages、list-media、get-media、read-settings、update-settings、get-option、update-options、uninstall-preset、get-menus、所有 i18n-* 工具
canai, core, cachecreate-page、write-post、write-page、write-menu、assign-menu
canai, core, http, cachediagnostics
canai, core, woocommercesetup
canai, core, woocommerce, httpinstall-preset
core, cacheupdate-page
core, filesystem, httpsideload-url
core, filesystem, cacheupdate-media
woocommerceget-wc-page-ids、get-wc-css-reference
cachepurge-cache
fluent-snippetslist-snippets、get-snippet
fluent-snippets, filesystem, cachecreate-snippet、update-snippet、replace-in-snippet、set-snippet-status

带有 cache 标签的写入工具会在响应中返回 purged:如果已将清除操作排入请求结束时执行,则返回 { "scope": "all"|"post", "plugin": "litespeed"|"wp-rocket"|"w3tc"|"wp-super-cache"|"wp-optimize" };如果没有排入清除操作,则返回 null(自动清除关闭、没有缓存插件或没有写入内容)。

Requires(v1.82.0)。 除 touches 外,工具还可以声明 meta.mcp.requires:即完成其工作所必需的标签。目前只有 purge-cache 需要 cache。缺少所需集成的工具会被 建议关闭,并从你的工具列表中隐藏(见下文的 工具可见性)。

工具暴露情况 — 你能看到哪些工具(插件 v1.82.0)

客户端通过 tools/list 获取的列表并非“所有已注册的工具”。每个工具都有针对本网站的实时推荐设置,站点所有者可以在 AI Client → Tools 中为每个工具单独覆盖设置(Auto / On / Off),而 Follow recommendations 开关决定 Auto 表示“遵循推荐”(新安装)还是“开启”(从 ≤1.81 升级的站点,因此对这些站点不会有任何变化)。覆盖设置始终优先。规则按顺序匹配,命中第一条即生效:

规则隐藏条件工具
R1缺少必需的集成没有缓存插件时的 purge-cache
R2始终隐藏 — 翻译工具需主动启用所有 i18n-* 工具(Languages 组)
R3另一个已注册的 MCP 能力与其重复(站点筛选器,默认为空)get-option、list-media、get-media、sideload-url
R4已完成设置 — 仅供参考,绝不会自动应用setup、预设、import、export

diagnostics 和 manage-tools 永远不会隐藏:你可以通过它们了解哪些工具被隐藏,并重新启用这些工具。

你预期会看到的工具不在列表中? 不要因此断定站点不具备该功能。调用 wpcanai-manage-tools { "action": "list" } — 它会列出所有已注册的工具,包括隐藏的工具,并提供 exposed、override、recommendation、reason 和 rule — 然后,如果用户希望使用该工具,就将其启用并重新连接(HTTP 客户端会缓存 tools/list;重新连接后新工具才会出现)。直接调用隐藏的工具会失败并返回 tool_disabled,错误消息会指出确切的 manage-tools 调用方式。

wpcanai-manage-tools(插件 v1.82.0)
  • 参数: { "action": "list" } 或 { "action": "set", "overrides"?: { "<short>": "on"|"off"|null }, "groups"?: { "<group>": "on"|"off"|null }, "follow_recommendations"?: bool, "apply_advisory"?: bool }。null 会清除覆盖设置,使推荐设置重新生效。groups 会将设置应用于 Tools 标签页分组中的所有已注册工具 — Templates & code、Pages & posts、Media、Snippets、Languages、History & restore、Site & settings、WooCommerce(或使用 slug,例如 languages)。apply_advisory: true 会将 R4 建议写入为 off 覆盖设置。未知工具名 → invalid_tool(仍可清除未注册工具的现有覆盖设置);未知分组 → invalid_group;diagnostics / manage-tools → exempt_tool。
  • 返回值: { "follow_recommendations": bool, "tools": [{ "name", "group", "touches", "requires", "exposed", "override", "recommendation", "reason", "rule", "advisory", "bytes" }], "totals": { "exposed", "total", "exposed_bytes", "total_bytes", "exposed_tokens", "total_tokens" } };执行 set 后还会返回 "reconnect_required": true 和一条 note。bytes 是描述和输入架构的大小 — 即该工具每轮消耗的字节数。
  • 默认隐藏翻译工具。 当用户提出任何与翻译相关的需求,而你的工具列表中没有 i18n-* 工具时:运行 manage-tools list,向用户确认是否要在此站点启用翻译,然后执行 { "action": "set", "groups": { "Languages": "on" } },重新连接,并继续使用下方的翻译模型路由器(如果尚未添加任何语言,i18n-set-settings 会添加第一种语言 — 调用该工具前必须先启用此分组)。此覆盖设置会在不同请求和重新连接之间保留,直到有人将其清除。
wpcanai-list-templates
  • 参数: { "status"?: string } — 文章状态筛选器(默认值为 "publish")。
  • **返回值:**对象 array,包含:id、title、slug、status、type(template_type 名称)、layout_id(整数或 null)。
wpcanai-list-pages
  • 参数: { "all_pages"?: bool } — 默认值为 false。(v1.97.0) 设置 all_pages: true 时,会按 id 顺序列出所有状态(回收站除外)的页面(文章类型 page),包括普通 WordPress 页面和草稿,而不是 CanAI 条目。它不会返回其他文章类型。
  • 返回:对象 array,包含:id、title、post_type、(v1.97.0) status、layout_id(整数或 null)、format("twig"|"blocks";(v1.97.0) 在 all_pages 模式下,对于未使用 CanAI 创建的页面为 null)、css_lines / js_lines(页面自身 _canai_css / _canai_js 的非空行数;0 = 无),以及 (v1.96.0) slug、parent(页面 id;0 = 顶级)、path(gallery/auditorium,开头和结尾均不带斜杠;未设置 slug 保存的页面为 '')和 url——默认适用于带有 _canai_html 的文章/页面,或标记为 blocks 的页面。创建页面前,请使用 all_pages: true 和 parent / path 查找现有父页面(也包括普通页面或草稿;见 嵌套页面),并使用 id / path 链接到子页面(规则 6)。
  • 默认仅返回已发布内容。 未设置 all_pages 时,它会返回状态为 publish 的文章:通过 wpcanai-create-page 创建且 status: "draft" 的页面不会出现——请传入 all_pages: true 或按 id 重新查询;不要以为页面已丢失。
wpcanai-read-meta
  • 参数: { "post_id": int, "fields"?: ["html"|"css"|"js"|"context"|"layout"|"format"|"blocks"|"tailwind_build"|"tailwind_hash"|"asset_sources"|"render_warnings"] } — post_id 必填;省略 fields 时读取默认字段(html、css、js、context、layout、format)。需要时请显式传入 tailwind_build / tailwind_hash——由于构建 CSS 可能很大,这两项不包含在默认字段中。
  • 返回: 包含以下任意字段的对象:html、css、js、context(字符串),layout(int),format("twig"|"blocks"),blocks(字符串,原始区块标记),tailwind_build(字符串),tailwind_hash(字符串),hashes(对象)。
  • (v1.58.0) hashes。 当 fields 包含 html、css 和/或 js,或默认使用这些字段时,响应还会包含 hashes——每个请求的内容字段对应一个 SHA-1,例如 { "html": "<sha1>" }。仅读取元数据时(未请求 html/css/js)不会返回此字段。将这些值直接传给 wpcanai-write-meta 的 expected_hash,使下一次写入以该字段自本次读取后未发生变化为条件。
  • (v1.65.0) format 和 blocks。 format 属于默认字段。blocks 返回原始区块标记,仅适用于区块页面(否则会返回 format_mismatch);不会对其进行哈希处理。
  • (v1.71.0) lines。 "lines": [from, to](行号从 1 开始,包含首尾,最多 400 行)仅返回单个请求内容字段中指定范围的内容——fields 必须恰好指定 html/css/js 其中一个(可同时请求 layout 等非内容字段)——并添加 line_counts: { html: 1240 }。hashes 仍基于完整字段计算,因此仍可用于 write-meta 的 expected_hash。可用此参数检查 grep-content 命中的区域,而不必拉取整个文档。使用 lines 时始终传入 fields——{ "post_id": 12, "fields": ["html"], "lines": [200, 260] }。若不传入 fields,调用会失败并返回 invalid_input lines applies to exactly one content field,因为此时 fields 会默认包含 html、css 和 js。
  • (v1.80.0) hash_only。 "hash_only": true 仅返回 fields 中指定内容字段(html/css/js;若未指定,则返回全部三个字段)的 hashes 和 line_counts,以及 format 和 layout——不返回内容。这是确认写入已生效或在调用 write-meta 前获取 expected_hash 的轻量方式。不能与 lines 同时使用。
  • (v1.90.0) render_warnings。 不属于默认字段。fields: ["render_warnings"] 会返回该文章自身的 Twig 在过去 7 天内渲染页面时产生的 PHP 警告:[{ field, line, kind, message, excerpt, url, first_seen, last_seen }]——kind 为 expression(包含 line 和作为 excerpt 的源代码行)或 print(单独的 {{ }} 输出:line 和 excerpt 为 null,Twig 无法判断具体是哪一处)。数据与 wpcanai-scan 的 render_warning 发现项相同,但仅针对一篇文章。
  • (v1.89.3) 未知的 post_id。 read-meta、write-meta、replace-in-meta 和 grep-content 会返回 invalid_post Invalid post_id: no post with ID 123 exists(或 pass a positive post ID)。请使用 wpcanai-list-pages / wpcanai-list-templates / wpcanai-resolve-content-id 重新解析 ID——切勿重试同一个 ID,也不要猜测相邻 ID。
wpcanai-write-meta
  • 参数: { "post_id": int, "html"?: string, "css"?: string, "js"?: string, "context"?: string, "layout"?: int, "tailwind_build"?: string, "tailwind_hash"?: string, "confirm_truncate"?: bool, "expected_hash"?: object, "convert"?: bool, "ignore_syntax_error"?: bool } — post_id 为必填项;只包含要更新的键即可。tailwind_build 会将预编译的单页 Tailwind CSS 写入 _canai_tailwind_build;tailwind_hash 会将输入哈希写入 _canai_tailwind_hash(两者配套使用——参见 生产环境编译 Tailwind)。
  • 返回: { "success": bool, "post_id": int } — 在转换为 Twig 的路径上(convert: true),还会返回 converted: true 和一个 warnings 数组,说明区块正文仍保留在 post_content 中,尚未渲染。
  • 注意:完全支持传入完整的 html/css/js 字符串,大小不限——大型页面也没问题。绝不能因为负载很大就改用原始 HTTP/Python/curl 请求 MCP 端点(它没有 MCP 会话/身份验证,会返回 403)。如果只需更改特定子字符串(资源 URL、类名、字符串),请优先使用下文的 wpcanai-replace-in-meta。
  • 不要将 tailwind_build 和源代码编辑打包在一起。 在同一次调用中写入 tailwind_build 和 html/css/js 时,写入的 tailwind_build 会带有写入前的时间戳,因此会被保守地视为过期。先写入源代码,再通过单独一次调用写入 tailwind_build/tailwind_hash。
  • 空字符串的处理方式不同。 write-meta 使用键是否存在来判断(array_key_exists):传入 "html": "" 会清空该字段。create-template/create-page 使用 !empty:空字符串会被忽略,不会写入。要清空内容字段,请使用 write-meta。
  • (v1.58.0)confirm_truncate(可选布尔值)——将 1 KB 或更大的 html/css/js 内容替换为小 70% 以上的负载时,必须设为 true。否则会拒绝此类写入,并返回错误代码 truncation_blocked(状态码 409),且不会写入任何内容——不会更改数据库、创建快照或开启操作,因为写入根本不会发生。错误消息会指出字段以及新旧内容的准确字节数。如果有意缩短内容,请使用 confirm_truncate: true 重新发送完全相同的调用;如果不是,请先读取该页面的元数据(wpcanai-read-meta)——你的负载很可能有误。请传入真正的布尔值或字符串 "true";字符串 "false" 和 "0" 会正确地被视为未确认,不会被当作真值。
  • (v1.58.0)expected_hash(可选对象)——按字段指定的乐观锁,例如 { "html": "<sha1>" }(绝不能传入单独的标量——一次调用可以同时写入多个字段)。如果读取后该字段发生了变化,写入会失败,并返回错误代码 hash_mismatch(状态码 409),且不会写入任何内容;错误中会包含当前哈希,以便你重新读取、重新应用编辑并重试。哈希可从 wpcanai-read-meta 返回的 hashes 对象中获取(见上文)。省略 expected_hash 会完全保留原有的最后写入优先行为。
  • **(v1.58.0)保护检查顺序:**先检查 expected_hash,再检查 confirm_truncate,两项检查都在写入任何内容之前进行——如果读取的数据已过期,会先报告该情况;如果内容缩短受限,也会阻止写入。
  • (v1.68.26)Twig 语法保护。 在创建快照或写入数据库前,会解析已更改的 html/css/js 字段。Twig 无效时会返回 twig_syntax_error(状态码 422),并提供 field、line 和解析器消息详情,且不会写入任何内容。仅在有意存储无效的中间状态时使用 ignore_syntax_error: true;写入会成功并附带警告,wpcanai-scan 仍会将其标记出来。
  • (v1.65.0 / v1.67.0)区块页面。 如果页面格式为区块格式,写入 html 或 js 会被拒绝,并返回 format_mismatch;除非设置 convert: true,将页面切换回 Twig(v1.65.0)。区块页面接受单独写入 css(v1.67.0):它是页面的纯 CSS 槽位,写入位置为 _canai_css。始终允许写入 layout、context 和 tailwind_*。
wpcanai-replace-in-meta
  • 参数: { "post_id": int, "field"?: "html"|"css"|"js", "replacements": [{ "from": string, "to": string }], "require_all"?: bool, "ignore_whitespace"?: bool, "regex"?: bool, "scope"?: { "text": string, "occurrence"?: int }, "dry_run"?: bool } — post_id 和 replacements 为必填项;field 默认为 html。即使只有一组替换,replacements 也必须是数组 — { "post_id": 12, "replacements": [{ "from": "a", "to": "b" }] }。顶层没有 from / to;不带 replacements 的调用会因架构验证失败,并报错 replacements is a required property of input。替换在服务端按顺序应用于所有出现 from 的位置 — 文档不会通过网络传输。(v1.71.0) ignore_whitespace 会让 from 中的每段连续空白匹配任意连续空白(可容忍缩进和换行差异;to 按原样写入)。regex 会将 from 视为 PCRE,并将 to 视为 preg_replace 模板($1);不能与 ignore_whitespace 同时使用。scope.text 会将所有替换限制在该文本第 n 次字面匹配所在的最近外层元素内(occurrence 默认为 1)— 无需知道标记,就能指定“写着 conference 的标题”。dry_run 会计算次数并提供诊断信息,但不写入任何内容。若任何 from 的匹配次数为 0,require_all: true 会报错(422,不写入任何内容)。与 write-meta 采用相同的 lang_mismatch 强制检查。
  • 返回值: { "success": bool, "post_id": int, "field": string, "total": int, "dry_run": bool, "scope"?: { "text", "occurrences", "used", "start", "end", "line" }, "replacements": [{ "from", "to", "count", "suggestion"? }] }。如果某组匹配次数为 0(且未使用 require_all),则会附带说明未匹配原因的 suggestion。
  • (v1.71.0)未匹配诊断。 require_all 未匹配时会返回 replace_no_match,且错误消息本身包含诊断信息 — 通过 MCP 只能看到消息,看不到 data:"from" at index 1 not found, nothing written: <from>. Nearest match: <kind> at line 227, column 13 — <snippet> — <hint>. Pairs before it: 1 (counts: 3).(index 从 0 开始;REST 调用方也可以在 data.failed_index、data.results、data.suggestion 中获取相同字段)。服务端按以下顺序检查这些类型:whitespace_only — 精确字节仅在你的作用范围内存在空白差异 — 保留 scope,用 ignore_whitespace: true 重试同一调用。outside_scope — 字面内容只存在于与 scope.text 所选元素不同的元素中 — 从该其他元素中选择 scope.text(或 scope.occurrence);不要移除 scope 来匹配,否则会重写错误的元素(如果目标确实是作用范围内的元素,请通过限定范围的 grep-content 重新检查其精确字节 — 差异可能来自空白或属性顺序)。partial 指向 from 中最长的词元。none — 字段或作用范围内不存在锚点;请检查 post_id / field。切勿回退到文档的本地副本来“查看字节” — 建议片段和设置 max_length: 1000 的限定范围 grep-content 会提供准确的行内容。
  • 错误: scope_not_found、scope_no_element(422,不写入任何内容)、invalid_regex(模式无法编译)、regex_error(422,不写入任何内容 — 模式已编译,但在运行 regex 或 ignore_whitespace 对时,PCRE 引擎触发了回溯 / JIT 限制;请简化模式,未应用任何重写)、invalid_input(同时使用 regex 和 ignore_whitespace)。
  • 适用场景: 对现有大型页面进行定向编辑(例如修复损坏的资源 URL)— 一次小调用即可,无需先 read-meta 再完整执行一轮 write-meta。会保留对斜杠敏感的内容(例如转义后的 Tailwind 选择器 .md\:flex);编辑 html / css / js 会像 write-meta 一样,将布局的 Tailwind 构建标记为过期。
wpcanai-create-template
  • 参数: { "title": string, "type": string, "html"?: string, "css"?: string, "js"?: string, "layout"?: int } — 必须提供 title 和 type(template_type slug)。
  • 返回: { "post_id": int, "slug": string }。
  • 类型化 CPT 模板(v1.24+)。 已发布的 wpcanai_template 若其 template_type 词条为 single-<post_type> 或 archive-<post_type>,就会接管该 CPT 在前端的单篇内容或归档页面渲染——例如,创建一个类型为 single-service 的模板,即可接管 service 详情页;创建 archive-service 则可接管其归档页面。single-post(自 1.60.0 起预置)对在 WordPress 编辑器中编写的博客文章也执行相同操作:在 prose 容器中使用 {{ the_content(post.post_content) }},类型化的 single-* 接管模板可使用 post.featured_image.*,自 1.68.15 起还可使用 post.author.*。仅在模板存在时生效:若不存在此类模板,CanAI 的输出会与主题完全一致。页面仍使用自身的元数据路径;WooCommerce 产品仍使用 WC 区块。
wpcanai-resolve-content-id
  • 参数: { "type": string } — type 为必填项。接受基础类型(cart、checkout、my-account、shop、product-category、product、product-loop、404、search、category、tag、author、archive)、10 种 WooCommerce 端点类型(order-received、order-pay、add-payment-method、orders、view-order、downloads、edit-account、edit-address、payment-methods、lost-password),以及通过 wpcanai_template_types 过滤器注册的类型 (v1.50.0)。
  • 已注册类型须知。 枚举会在能力注册时构建,触发时机为 wp_abilities_api_init(由 init 优先级 1 触发)。在文件作用域添加的 wpcanai_template_types 过滤器——这是文档所述的方式,也是解析器自身文档展示的方式——届时已生效,因此其类型会被接受。在默认优先级 10 的 init 回调内添加的过滤器尚未注册,因此即使解析器能够正常解析该类型,架构仍会拒绝它。请在文件作用域注册。
  • 返回: { "content_post_id": int, "content_post_type": string, "rendering_mode": string, "template_post_id": int|null, "unreachable_post_id": int, "resolution_reason": string }。
  • (v1.50.0)content_post_id 是实际渲染内容的文章。 委托正文:当前使用布局包装器,因此由 WooCommerce 页面提供正文——content_post_id 为该页面,rendering_mode 为 page-rendered。模板正文:该类型的模板不是布局包装器,因此由模板自行渲染整个页面——content_post_id 为模板,rendering_mode 为 template-rendered;委托页面的 _canai_html(如果存在)属于未使用内容,并会通过 unreachable_post_id 报告。resolution_reason 会说明适用的是哪种情况。请编辑 content_post_id;在 v1.50.0 之前,即使由模板渲染,也会返回委托页面,因此修改可能落在永远不会渲染的元数据上。
  • rendering_mode 有五种取值——page-rendered(委托正文)、template-rendered(模板正文)、(v1.50.1) blank 和 none,以及 (v1.58.0) page-rendered-unwrapped。Blank = 布局指针指向的文章已不存在,且委托页面没有自己的 _canai_html,因此不会渲染任何 CanAI 专属内容;请修复失效的指针(wpcanai-scan 会将其报告为 broken_layout 警告)。Page-rendered-unwrapped(v1.58.0)= 布局指针已失效,但委托页面确实包含自己的 _canai_html——CanAI 会回退为直接渲染该内容,不带布局的包装器或界面元素,因此不会丢失内容;content_post_id/content_post_type 与 page-rendered 一样,会指向委托页面。失效的指针仍值得修复(wpcanai-scan 会以相同的 broken_layout 警告报告)。None = CanAI 完全不渲染此类型。blank 和 none 的 content_post_id 均为 0,content_post_type 均为 "",且 resolution_reason 会说明具体是哪种情况。在 v1.50.1 之前,两者都会报告为 template-rendered / wpcanai_template,指向一个并不存在的渲染器——除非 content_post_id 非零,否则不要将这两个字段视为有效信息。在 v1.58.0 之前,即使委托页面自身的内容实际上以无包装器的方式渲染,失效的布局指针也总是被报告为 blank。
  • (v1.50.1)端点类型会继承其父类型的模板。 只有当存在已发布的该类型模板时,WooCommerce 端点才会接管请求;否则,请求会通过父类型渲染(order-received/order-pay 对应 checkout,其他八种对应 my-account)。resolve-content-id 也遵循这一逻辑,因此没有自身模板的端点在模板正文模式下会报告父类型的模板,而不是永远不会渲染的委托页面。在 v1.50.1 之前,它会报告该页面,这与 wpcanai-scan 对同一文章发出的 unreachable_content 警告相矛盾。
wpcanai-scan
  • 参数: { } — 无参数。

  • 返回: 包含以下发现的 array:severity、type、message,以及可选的 post_id。

  • 发现类型(v1.50.0)。 WooCommerce 区块(五种基础结构类型 shop、product-category、cart、checkout、my-account)会根据网站实际所处的形态验证每种类型——两种形态都不算错误:

    严重性类型含义
    warningunreachable_content无论哪种形态,已编写的 _canai_html 都无法渲染。模板正文:WooCommerce 页面仍带有正文,而模板负责渲染。(v1.50.1)委托正文:类型模板被页面自己的 _canai_layout 取代,且页面仍带有正文。会列出无法显示的文章和实际生效的文章。
    warningbroken_layout(v1.58.0) 布局指针引用的文章已不存在。CanAI 会将其解析为“无布局”,并回退到直接渲染委托页面自己的 _canai_html,不进行包装(没有布局外观)——如果委托页面有正文,就不会显示空白;如果没有正文,则不会渲染任何 CanAI 专属内容。无论哪种情况,都应修复或清除悬空指针。在 v1.58.0 之前,此问题的严重性为 critical,当时的假设是页面始终会显示空白;该假设现已修正。
    warningdelegate_mismatch模板声明的 _canai_delegate_page_id 本身已失效——文章已不存在,或不是 page。无论页面形态如何,都会单独检查:声明的覆盖项失效本身就可能导致解析后的形态不是委托正文。请修复声明的覆盖项;不要将其改指向当前解析出的 ID——该 ID 是根据同一个声明值推导出的,比较两者永远无法发现实际问题。
    warningempty_delegate_page委托正文,且此类型存在 wpcanai_template:本应存放正文的页面没有 _canai_html。有人有意配置了此类型,因此空页面确实是一个缺漏。
    warningduplicate_type_template多个已发布模板带有相同的 template_type 术语——只有第一个会渲染,其余模板均不起作用。模板不按语言区分(每种类型只有一个模板,供所有语言共用),因此曾为每种语言各保留一份副本的网站会在此显示这些副本——请将多余副本移至回收站,并改用 t() 翻译字符串。
    oktemplate_body_ok模板正文:模板会直接渲染此类型。与下方通用扫描自身的 template_ok 不同——虽然名称相似,含义却不同。
    okdelegate_ok委托正文:页面包含内容及其解析后的布局。
    infono_template / wc_page此类型尚无任何已编写内容——可能是委托正文但完全没有 wpcanai_template(WC 页面会在网站布局的包装下渲染;新安装时很常见,并非配置错误),也可能是形态为 none(无法解析出任何 WC 页面)。
    infoleaky_comment (html)_canai_html 仍包含 `` 标记,会出现在 view-source 中。请将分区标签转换为 Twig {# #}。
    warningleaky_comment (js)_canai_js 中有一个 /* */ 块,看起来像是在暴露基础设施信息(snippet/webhook/internal/…)。请使用 {# #}——_canai_js 会由 Twig 渲染。
    warningleaky_secret_canai_html / _canai_js / _canai_css 中有符合秘密信息特征的字面值(API 密钥、Bearer 令牌、私钥 PEM 等)。部署前请从元数据中移除。
    errortwig_syntax_error无法将 _canai_html、_canai_css 或 _canai_js 解析为 Twig。此发现包含 post_id、field、line 和解析器消息;在信任任何其他看似正常的结构类发现之前,请先修复此问题。
    通用扫描(所有包含 _canai_html 的文章,以及所有已发布的 wpcanai_template,类型为 product/404/search/category/tag/author/archive)会生成自己的发现项:no_layout、no_content、no_layout_tpl 和 template_ok(模板同时包含 _canai_html 和 _canai_layout)。不要将此处的 template_ok 与上文 WC 区块的 template_body_ok 混淆——两者可能同时出现在一次扫描响应中,但含义不同。如果 WC 区块已将该文章确切报告为 unreachable_content,则 no_layout 会从 warning 降级为 info,并交叉引用 unreachable_content(v1.50.0 最终审查修复):该文章的 _canai_html 已因模板正文结构生效而无法访问,给它指定 _canai_layout 会切换结构,改为委托正文,并取代当前正在渲染的模板,从而改变线上 URL 显示的内容。对于以这种方式标记的文章,在确认用户想要更改结构之前,切勿建议为其指定布局。
  • 导航发现项(插件 v1.88.0)。 按模板或页面报告,并包含匹配项所在的 line;从 v1.88.0 起,内容扫描也会覆盖 wpcanai_template 文章,因此也会检查页眉、页脚和布局。hardcoded_lang_prefix 和 unlocalised_internal_href 每篇文章各报告一项,消息中会列出所有行号。它们会对 Menus 和 turl() 执行检查:

    严重程度类型触发条件修复方法
    warninghardcoded_nav<nav> 中,或 <header> / <footer> 内的 <ul> / <ol> 中,包含 2 个或更多 <a href>,链接目标为固定值且没有 get_menu((页面正文其他位置的链接列表、由 item.url / post.url 等数据生成的链接,以及标记为面包屑 / 分页 / 目录的导航均不会触发)使用 wpcanai-write-menu(primary 或 footer)编写链接,并在该处渲染 get_menu()
    warninglegacy_menu_locationget_menu() 使用了 1.88 之前的 location 名称使用 replace-in-meta 将其重命名为 get_menu('primary') / get_menu('footer')(参见 Menus 下的升级说明);旧名称仍可渲染
    warningunknown_menu_locationget_menu() 使用了其他名称使用 primary 或 footer——CanAI 没有其他 location
    infomenu_fallback_chain对 get_menu() 的结果使用 is empty / is not empty 进行测试,并在同一代码块中再次调用 get_menu();或 {% else %} 分支中包含遍历 get_menu() 结果的链接仅使用一个 location,不设回退:删除第二次调用或 {% else %} 分支,并使用 write-menu 分配菜单
    warningmenu_children_twice (v1.89.0)遍历 get_menu()(或从其设置的变量),且没有 filter(,循环体中读取了项目的 .children遍历 get_menu('primary')|filter(i => i.depth == 0):扁平列表已经包含所有子项目
    infomenu_not_assigned调用 get_menu('primary') / get_menu('footer') 时,此站点的该 location 尚未设置菜单wpcanai-write-menu { "location": …, "items": […] } 会创建并分配菜单
    infohardcoded_lang_prefix手动构建语言前缀:current_lang() == 'ms' ? '/ms'、在 href 开头使用 current_lang() 设置 URL 变量,或为已配置语言使用字面值 href="/ms/…"使用 {{ turl('/path/') }}——它会自行应用当前语言;get_menu() 的 URL 已包含当前语言
    infounlocalised_internal_href在包含多种语言的站点上,使用字面值 href="/…" 或 href="{{ home_url(…) }}"使用 turl() 包裹链接,使其遵循当前语言
  • 渲染警告(插件 v1.90.0)。 render_warning(严重程度为 warning,包含 post_id、field、line)不会从内容中读取:CanAI 会在页面渲染时记录此警告,此时模板自身的 Twig 引发了 PHP 警告——几乎总是 Array to string conversion,即 {{ }} 输出了列表。消息会指出行号及其来源、修复方法和最近出现的位置:输出一个键({{ item.title }})、遍历列表,或对其使用 |join(', ')。对于直接输出 {{ items }} 的情况,line 为 null(Twig 无法判断具体是哪一个):请找到值为列表的 {{ }}。使用 read-meta(fields + lines)和 replace-in-meta 修复。写入字段会清除此字段的条目;只有页面再次渲染时才会重新出现,因此写入后列表为空并不能证明问题已解决——请让用户重新加载页面,然后再次扫描。保留 7 天。同样的条目也会出现在 read-meta 的 fields: ["render_warnings"] 和 wpcanai-diagnostics 的 render 组中。

  • 链接问题(插件 v1.97.0)。 从每篇文章的 _canai_html 中读取,并忽略 Twig 注释(对于普通 href,也会忽略 HTML 注释;但 `` 中的 {{ }} 仍会渲染,因此仍会计入);严重程度为 warning,包含 post_id、field 和 line;每个 slug_url() 字面值或每个链接路径对应一条问题,其消息会列出所有行号:

    类型触发条件修复方法
    link_unresolved字面量 slug_url('x') 或 slug_url('x', 'type') 未匹配到任何文章,或匹配到多个文章(不同父级下有相同 slug)——无论哪种情况,都会渲染为 href=""。多个匹配项会列为 path (ID n);页面已移动的路径会注明它现在的位置传入路径(slug_url('gallery/theatrettes')),或使用 turl(id_url(<id>));如果没有匹配项,请检查 slug
    link_moved字面量 turl('/a/b/') 或纯文本 href="/a/b/" 的路径既不是页面也不是文章,而其最后一段是另一条路径上恰好一个已发布页面的 slug——页面移动后,旧链接遗留了下来使用消息中指定的 turl(id_url(N)) 替换链接,并使用 replace-in-meta

    link_moved 只报告能够确证已移动的页面。以下情况不会报告:路径指向完全不存在的内容(拼写错误);存档、术语、商店或账户路径;最后一段被多个子页面共用的路径——请对此类路径进行 grep(参见 wpcanai-update-page);在子目录中安装的网站上,位于该目录之外的纯文本 href(网站位于 /blog/ 时的 /about/)。会先像路由器处理请求那样去掉前导的 /<lang>/:只去掉非默认语言的前缀,因此在默认语言为英语的网站上,/en/about/ 会按原样检查(插件 v1.97.1)。(v1.97.1) 不会读取内联 <script> / <style> 中的纯文本 href。

  • 在 v1.50.0 中移除: missing_delegate 和 stale_template_html。这两项检查都断言模板必须是委托给页面的空标记——这只是两种有效形式之一,并非规则。它们会在正常运行的网站上触发,而且其指导意见(“应为空”)会导致渲染内容被删除。请勿重新引入此检查。

wpcanai-grep-content
  • 参数: { "pattern": string, "regex"?: bool, "fields"?: string[], "limit"?: int, "post_id"?: int, "max_length"?: int, "context_lines"?: int }——pattern 为必填项。默认按字面子字符串匹配;regex: true 会将 pattern 视为 PCRE。fields 是 html|css|js 的子集(默认包含全部三项)。limit 为 1–100(默认值为 100)。(v1.71.0) post_id 将搜索范围限定为一篇文章(跳过全站扫描——知道页面时务必传入)。max_length(1–2000,默认值为 200)限制代码片段的长度;context_lines(0–10)用于添加周围的行。输入无效时返回错误 invalid_regex / invalid_input。
  • 返回: { "matches": [{ "post_id", "field", "line", "column", "line_length", "match", "before"?, "after"? }], "truncated": bool }——match 是以匹配位置为中心、长度为 max_length 字节的窗口(截断处以 … 标记);当 line_length ≤ max_length 时,它是完整的原始行,逐字节精确,可直接粘贴到 replace-in-meta 中作为 from。密集的 Tailwind 行长达 300–400 字节:需要锚点时,请传入 max_length: 1000。
  • 适用场景: 全站扫描(泄漏的注释、损坏的资源 URL、重命名类名);如果逐篇列出文章并手动执行 read-meta 无法扩展,就使用此工具。根据返回的匹配项,使用 wpcanai-replace-in-meta 执行修复。
wpcanai-get-wc-page-ids
  • 参数: { }——无参数。
  • 返回: { "cart": int, "checkout": int, "myaccount": int, "shop": int }(WooCommerce 页面 ID;未设置时为 0)。
wpcanai-get-wc-css-reference
  • 参数: { "context": string, "include_core"?: boolean } — context 是以下之一:cart、checkout、order-received、order-pay、myaccount-dashboard、myaccount-orders、myaccount-view-order、myaccount-downloads、myaccount-edit-account、myaccount-edit-address、myaccount-payment-methods、myaccount-add-payment-method、myaccount-lost-password。include_core(默认为 false)会将 third_party_hooks 扩展为该上下文钩子上的所有已注册回调(包括 WooCommerce 核心回调),每个回调仍带有自己的 is_third_party 标记。没有 lang 参数。
  • 返回值: css_reference(字符串 — 原始 SCSS 源码,而非编译后的 CSS;Sass 变量/混入尚未解析,因此请改用转述,不要直接粘贴。WooCommerce 针对该上下文的默认规则取自已安装 WooCommerce 的 woocommerce.scss、woocommerce-layout.scss、woocommerce-smallscreen.scss 和 forms.scss,并附带完整的祖先选择器链,以便匹配优先级;smallscreen 规则会包装在 @media only screen and (max-width: 768px) 中)、css_fallback(null,或当选择器提取未找到匹配项、而是返回拼接后的完整样式表时为 "full-file")、template_files(字符串数组,相对于 ABSPATH — 权威的标记路径,主题覆盖优先)、templates_missing(字符串数组,相对于上下文的预期模板中无法读取的路径 — 不可与 template_files 直接比较;非空表示上下文映射已与已安装的 WooCommerce 不一致)、hooks_found(字符串数组 — 模板触发的操作)、third_party_hooks(对象 {hook, priority, callback, file, is_third_party} — 仅非核心/主题/CanAI 回调;当 include_core 为 true 时则包含所有回调)、context_echo({context, include_core})。
  • 只读;仅静态分析。 不会渲染任何内容;不会读取订单/购物车/客户数据。有关如何使用输出,请参阅上文“为 WooCommerce 区块添加样式”工作流部分。
wpcanai-create-page
  • 参数: { "title": string, "slug"?: string, "status"?: string, "html"?: string, "css"?: string, "js"?: string, "layout"?: int, "parent"?: int, "menu_order"?: int } — title 为必填项;创建一个 page 文章,并可选添加 _canai_* 元数据。status 默认为 publish。
  • (v1.96.0)parent / menu_order。 parent 是用于将新页面嵌套在其下的现有页面 ID:其 URL 会变为 /<parent path>/<slug>/,而 slug 仅是最后一段(auditorium,绝不能是 gallery/auditorium)。默认值 0 表示顶层页面。menu_order 是“页面属性”→“顺序”(默认为 0)。两者都必须是整数:非整数会被工具的输入架构拒绝(ability_invalid_input,例如 input[parent] is not of type integer.),且不会写入任何内容。不存在、已移至回收站或不是页面的 parent 会导致 invalid_parent,同样不会写入任何内容。参见规则 4 下的嵌套页面。
  • 返回值: { "post_id": int, "slug": string, "format": string, "warnings": string[], "status": string, "parent": int, "path": string, "url": string }(status … url 自 v1.96.0 起提供)。path 不带开头或结尾的斜杠(gallery/auditorium);保存为草稿且未提供 slug 时,值为 ''。使用页面 ID(turl(id_url(<id>)))或对返回的 slug / path 使用 slug_url() 链接到新页面 — 绝不要将 url 或自行构建的路径粘贴到模板中。
  • 警告(v1.96.0,不会导致操作失败): slug 已在同一父级下使用或已被保留,因此 WordPress 保存了带后缀的 slug(auditorium-2)— 链接前请重新读取 slug / path;父页面尚未发布 — 访客无法打开该页面,但新页面的 URL 仍会经过它。草稿会保留你提交的 slug;发布页面时,WordPress 会对其去重。
wpcanai-update-page(插件 v1.96.0)

通过 MCP 更新页面的标题、slug、状态、父级和顺序 — 对应区块编辑器的“摘要”和“页面属性”面板。

  • 参数:{ "post_id": int, "title"?: string, "slug"?: string, "status"?: "draft"|"publish"|"pending"|"private"|"future", "parent"?: int, "menu_order"?: int } — 必须提供 post_id 和至少一个其他字段;只会更改你发送的字段。parent: 0 会将页面移至顶层。slug 仅指 URL 的最后一段。使用 wpcanai-list-pages { "all_pages": true }(v1.97.0)查找 post_id,其中包含普通页面和草稿页面。
  • (v1.97.0)future 可用,因此可以重新发送从定时发布页面取得的 previous;此工具不接收日期。只有当页面的日期在未来时,WordPress 才会保留页面的定时发布状态;否则会立即发布。如果草稿或待审核页面从未设置日期,则会使用当前时间,因此 future 会将其发布。相同规则也会将日期在未来的页面的 publish 转为 future。响应中的 status 是页面最终采用的状态。
  • 绝不触碰正文。post_content 和所有 _canai_* 元数据都会保持不变,因此它对 Twig、区块和普通 WordPress 页面都同样适用。重命名、更改 slug、发布、取消发布、移动或重新排序页面时,请使用此工具,而不是 write-meta / write-page。
  • **错误 — 所有错误都会在写入任何内容前被拒绝。**非整数(以及未知状态)会被工具的输入架构拒绝(ability_invalid_input,例如 input[parent] is not of type integer.)。之后可能出现:not_a_page(ID 不存在、已移入回收站或不是 page;博客文章请使用 wpcanai-write-post)、invalid_input(除 post_id 外没有其他字段、title 或 slug 为空)、invalid_parent(父级不存在、已移入回收站、不是页面或就是该页面本身)、parent_loop(409 — 请求的父级位于该页面之下;请先将其移出。WordPress 自身会静默地将父级重置为顶层,并报告成功)。
  • 返回:{ "post_id", "title", "slug", "status", "parent", "menu_order", "path", "url", "edit_url", "previous": { "title", "slug", "status", "parent", "menu_order", "url" }, "warnings": string[], "purged" }。previous 保存调用前的值;如果页面没有 slug(即没有 slug 的草稿),则省略其中的 slug;**(v1.97.0)**如果页面没有标题(或标题经清理后为空,例如只有标签或空格),则省略其中的 title。
  • 撤销 = 重新发送 previous。{ "post_id": <id>, …previous } 会恢复标题、slug、状态、父级和顺序;额外的 url 键会被忽略。快照只保存 _canai_* 元数据,WordPress 修订记录最多记录标题——不会记录 slug、状态、父级或顺序——因此 previous 是唯一的撤销方式,请保留它,直到用户确认满意。previous.status 为 future 时,重新发送的值保持不变(见上文的 future)。在 1.96.x 中,previous 可能带有空的 title 和 future 状态,重新发送其中任一项都会被拒绝:请将它们省略,并让站点所有者在编辑器中重新安排定时发布页面。
  • 警告(非致命):
    • URL 已更改 — 已发布(或私有)页面获得了新的 slug 或父级。警告会说明有多少子页面随之移动,并指出菜单、id_url() 和不带路径的 slug_url() 链接会跟随页面,但 slug_url() 路径链接和硬编码路径不会。WordPress 不会将页面旧 URL 重定向到新 URL(有些站点会通过核心的 404 猜测进行重定向;请勿依赖此行为),因此请按警告中的说明查找这些链接:使用 wpcanai-grep-content 搜索不带斜杠的旧路径(gallery/auditorium),这会同时匹配 turl('/gallery/auditorium/')、href="/gallery/auditorium/" 和 slug_url('gallery/auditorium')(移动后会渲染为 href="");使用 wpcanai-replace-in-meta 修复每一处命中。**(v1.97.0)**随后 wpcanai-scan 会将残留链接报告为 link_moved / link_unresolved(参见 wpcanai-scan 下的 链接发现项)。
    • 已取消发布 — 已发布(或私有)页面现在处于 draft / pending 状态:访客无法再通过其旧 URL 打开页面。
    • Slug 已添加后缀 — WordPress 保存了不同的 slug(x-2):已有同级页面使用该 slug,或该 slug 已被保留。即使你没有发送 slug,也可能触发此警告,因为核心会在每次保存已发布页面时,以及发布草稿时,重新检查页面的 slug。
    • 父级未发布 — 与 wpcanai-create-page 中的情况相同。
  • **缓存。如果页面更改 slug 或父级前后都处于已发布状态,则会清除整个页面缓存(purged.scope: "all"):所有缓存的菜单和子页面链接都会指向旧 URL。更改标题会清除该页面的缓存。(v1.97.1)**同一规则适用于所有写入工具(wp-admin、write-post):只有分层类型(页面)或菜单中的文章在 URL 移动时才会清除所有缓存;其他文章只会清除自身缓存。
wpcanai-write-post
  • 参数: { "post_id"?: int, "title"?: string, "blocks"?: object[], "slug"?: string, "status"?: string, "excerpt"?: string, "date"?: string, "featured_image"?: int, "categories"?: string[], "tags"?: string[] }。
    • 创建(不提供 post_id):必须提供 title 和 blocks。更新(提供 post_id):只更改你发送的字段;该 ID 必须对应一篇文章,不能是页面 — 页面 ID 会返回 WP_Error('not_a_post')。
    • 默认状态为 draft,而不是 publish — 与 wpcanai-create-page 不同。预期所有者会先在编辑器中阅读文案。有效值:draft、publish、pending、private、future。
    • blocks 会替换整个正文。不支持部分编辑;请重新发送完整列表。WordPress 修订版本可用于撤销。
    • categories / tags 接受名称或别名;不存在的项目会自动创建,更新时会替换整个集合。
  • 返回值: { "post_id": int, "slug": string, "status": string, "url": string, "edit_url": string, "block_count": int, "warnings": string[] }。warnings 表示非致命问题(无法解析的嵌入提供方、没有替代文本的图片、已转换的经典编辑器正文)。致命问题会返回 WP_Error,且不会写入任何内容。
  • 此工具仅用于博客文章。 页面仍使用 wpcanai-create-page + wpcanai-write-meta(CanAI HTML/CSS/JS 元数据)。write-post 会将原生区块标记写入文章的 post_content,不会修改 CanAI 元数据。

区块类型。 blocks 中的每一项都采用 { "type": …, …fields } 格式:

type字段
paragraphtext(必填)、align(left/center/right)
headingtext(必填)、level 2–6(默认为 2;不接受 level 1 — 文章标题即为 h1)
listitems string[](必填)、ordered bool。仅支持扁平列表
quotetext 或 paragraphs string[](必填其一)、cite
imageid int(必填 — 附件 ID)、alt、caption、size(默认为 large)、link(none/media/attachment)
codecode(必填)。转义为纯文本;不支持内嵌 HTML
separator—
embedurl(必填,http/https)
tablerows string[][](必填,行列规则)、header string[]
groupblocks(必填,子区块)、tag(div/section/header/footer/aside/main)、className
columnscolumns(必填,1–6 项,每项为 { width?, className?, blocks })、className
buttonsbuttons(必填,{ text, url, style?: fill/outline, target?: _blank, rel?, className? })、className
coverimage(必填,附件 ID)、overlay_opacity(10–100,步长为 10,默认为 50)、blocks(必填)、className
media_textimage(必填,附件 ID)、side(left/right)、blocks(必填)、className
spacerheight px(1–1000,默认为 100)、className
chartchart(必填:bar/horizontal-bar/line/area/pie/doughnut/scatter/radar)、labels string[](必填,scatter 除外 — 每个数据点对应一个)、datasets(必填,1–8 项,每项为 { label, data };data 为有限数值,间隔处使用 null,且每个标签必须恰好对应一个数据值;对于 scatter,格式为 [{ x, y }],labels 会被忽略并显示警告;pie/doughnut 恰好接受一个数据集)、title、caption、unit(刻度/工具提示/表格的后缀,例如 ms、%)、stacked bool、y_min / y_max number、height px(160–800,默认为 320)、source { text?, url? }(呈现为 "Source: …")、table(默认为 collapsed / open / hidden)、className。字符串均为纯文本(会剥除内嵌 HTML)。序列化后的 labels + datasets 大小上限为 64 KB。插件版本为 v1.77.0;wpcanai-write-page 的区块页面也接受此类型

除 embed 外,每种类型都接受 className(应用于区块根元素的 Tailwind 类);embed 会在首次保存时重写自己的类列表。嵌套深度上限为 4 层。嵌套错误会报告以点分隔的 path(例如 "2.blocks.0")。 图片:先上传,再传入 ID。 write-post 只接受附件 ID——不接受 URL 或二进制数据(cover 和 media_text 接受相同的 ID,如果无法解析该 ID,则返回 invalid_attachment)。调用 wpcanai-sideload-url(或使用 wpcanai-list-media 查找现有附件),并传入返回的 id。

文本字段中的内嵌 HTML 仅限于 <a href|title|rel|target>、<strong>、<em>、<b>、<i>、<code>、<br>、<s>、<sub>、<sup>、<kbd>、<mark>。其他内容都会被静默剥除,而不会被拒绝——段落中的 <script> 或 <iframe> 本来也会构成无效的区块内容。不接受 Markdown,且 write-post 没有原始 HTML 转义通道。

wpcanai-get-menus(插件 v1.88.0)
  • 参数: { }。
  • 返回: { "locations": { "primary": { "menu_id", "name", "items" } | null, "footer": { … } | null }, "languages": { "<lang>": { "primary": { "menu_id", "name", "items" } | null, "footer": … } }, "menus": [{ "id", "name", "slug", "assigned_to": ["primary"], "languages": { "<lang>": ["footer"] } }], "legacy_assignments": { "<old location>": <menu id> }, "translations": { "<lang>": { "translated", "total" } } }。条目为 { "id", "title", "type", "object", "object_id", "url", "target", "classes", "description", "attr_title", "icon", "image", "children" },未经翻译的原始内容(标题的 url: "")。(v1.89.0) description / attr_title / icon / image(附件 ID;没有时为 0);languages 包含所有非默认语言,对于使用已翻译默认菜单的位置,其值为 null;menus[].languages 列出语言分配情况。translations 按非默认语言统计已翻译的条目标题和描述数量——仅针对该语言未提供自有菜单的位置。legacy_assignments 列出仍以 1.88 之前的位置名称分配的菜单(参见 菜单 下的升级说明)。
  • 调用 write-menu 前先调用此工具,以查看各位置当前使用的菜单。
wpcanai-write-menu(插件 v1.88.0)
  • 参数: { "location": "primary"|"footer", "items": [{ "title": string, "page"?: int|string, "url"?: string, "target"?: ""|"_blank", "classes"?: string, "description"?: string, "attr_title"?: string, "icon"?: string, "image"?: int, "children"?: [ …same shape ] }], "merge"?: bool, "lang"?: string } — location 和 items 为必填项。每个项目都必须有非空的 title,并且在 page(任意公开文章类型的文章 ID 或别名 → 页面项目会在其别名更改后继续指向该页面)和 url(自定义链接,可为站点相对路径或绝对地址)中恰好选择一项——标题除外:带有 children 的项目可以两者都不填(它会显示为标签,url: "")。嵌套最多 3 层。
  • (v1.96.0)按别名设置 page。 首先匹配精确路径(gallery/auditorium),然后匹配仅有一个页面与之完全一致的别名,因此子页面会按自身别名解析,并在移动后仍能继续解析。顶级页面优先于与其同名的别名(从插件 v1.97.1 起,已发布页面也优先于同名草稿)。(v1.97.0) 多个子页面共用的别名会返回 ambiguous_page:消息会列出每个页面的 path (ID n)——请传入路径或 ID。此时不会再转而查找其他文章类型:只有在没有页面使用该别名时,才会在其他公开文章类型中查找(其中有多个匹配文章时也会返回 ambiguous_page);若均无匹配,则返回 unknown_page。同名的未附加图片不再会遮蔽页面。
  • (v1.89.0)富文本字段。 description(单行)、attr_title、icon(符合 ^[a-z0-9-]{0,64}$ 的 Lucide 名称)、image(图片附件 ID)。使用 merge 时,未提供的字段会保留原值;"" / 0 会清空字段。
  • (v1.89.0)lang — 非默认语言的别名:为该语言设置此位置专属菜单,而非使用默认菜单(参见 按语言设置菜单)。首次调用会创建 Primary (<native name>) / Footer (<native name>)(会复用同名菜单),并将其分配给该语言;默认位置不受影响,结果中会带有 lang。默认或未知别名,或站点不支持原生翻译 → invalid_lang。
  • (v1.93.0)copy_default: true 与 lang 搭配使用(不传 items),可为该语言创建此位置专属菜单,作为默认菜单的副本。标签(标题、描述)会采用已存储的译文(如有);页面和文章项目仍关联其文章;其他项目会变为指向当前 URL 的自定义链接。如果该语言在此位置已有菜单,则返回 menu_exists(数据:menu_id);该位置没有默认菜单时返回 no_default_menu;未提供 lang 时返回 invalid_input。其他所有调用都必须提供 items。
  • 默认替换。 此位置的项目会变为与 items 完全一致,并按给定顺序和嵌套排列;items: [] 会清空菜单。merge: true 会保留现有项目,更新同一父级下 title 相同的项目,并追加其余项目。
  • 每个位置仅有一个菜单。 写入操作会更新此位置已分配的菜单,且不会重命名。只有在某个位置尚无菜单时,才会创建新菜单,命名为 Primary / Footer 并立即分配。没有删除工具:菜单由所有者在 wp-admin 中删除。
  • 整个树在写入前都会经过验证。 location 不是 primary / footer 时,输入验证会拒绝请求。可能的错误包括:unknown_page(page 未解析到任何页面——请先创建页面)、ambiguous_page(v1.97.0——多个页面共用同一别名;请传入路径或 ID)、invalid_item(缺少 title、同时提供 page 和 url、无 children 的项目两者都未提供、target 不是 "" / "_blank"、icon 无效、image 不是图片附件)、too_deep(超过 3 层)。出现任何错误时,菜单都不会改变。
  • 返回: get-menus 报告的该位置状态(menu_id、name、items),以及 written(写入的项目数)和 purged。菜单会显示在每个页面上,因此写入操作会清除整个页面缓存(参见 工具标签)。
wpcanai-assign-menu(插件 v1.89.0)
  • 参数: { "location": "primary"|"footer", "lang"?: string, "menu_id": int|null } — 将一个位置(或指定 lang 时,将某种语言的位置)指向一个现有菜单;wpcanai-get-menus 会列出菜单 ID。此操作不会写入菜单项:请改用 write-menu。
  • menu_id: null 会清除分配:某种语言会回退到翻译后的默认菜单(其自身菜单会保留,但不再分配);不指定 lang 时,该位置将不再关联任何菜单(其 pre-1.88 别名条目也会被清除),之后 wpcanai-scan 会在所有渲染该位置的地方报告 menu_not_assigned。
  • 错误: unknown_menu(该 ID 不是导航菜单)、invalid_lang(默认语言或未知语言代码)。返回值 { "location", "lang" (位置本身时为 null), "menu_id", "name", "items", "purged" } — 刚刚完成的分配:指定 lang 时,为该语言自身的菜单(清除后 menu_id: 0,但该语言仍显示翻译后的默认菜单);不指定 lang 时,为该位置的菜单。
  • 所有者对应的操作入口分别是外观 → 菜单 → 管理位置(默认菜单)和 CanAI → 翻译 → 菜单(按语言)。
wpcanai-read-settings
  • 参数: { "keys"?: string[] } — 省略 keys 时读取所有允许列表中的选项。
  • 返回值: 选项键 → 值的对象。
  • 允许列表中的键: show_on_front、page_on_front、page_for_posts、blogname、blogdescription、users_can_register、wpcanai_default_layout、wpcanai_tailwind_settings(对象:load_tailwind 为 yes|no,source 为 cdn|plugin(自 v1.58.2 起默认为 plugin),plugins 为字符串数组)、wpcanai_chart_settings(对象,插件 v1.77.0:load_chart 为 auto|yes|no(默认为 auto — 仅在渲染图表的请求中加载 Chart.js;yes 会在每个 CanAI 页面中加载;no 则从不加载,图表仅显示其表格),source 为 plugin|cdn(默认为 plugin);部分对象会与已存储对象合并,未知值会采用默认值)、wpcanai_markdown_negotiation("1"|"0",参见 面向 Agent 的 Markdown)、wpcanai_purge_after_write("1"|"0",插件 v1.80.0,默认开启 — CanAI 写入后清除页面缓存)、woocommerce_enable_signup_and_login_from_checkout、woocommerce_enable_myaccount_registration、woocommerce_cart_page_id、woocommerce_checkout_page_id、woocommerce_myaccount_page_id、woocommerce_shop_page_id。
wpcanai-update-settings
  • 参数: { "settings": { "<key>": <value>, ... } } — 仅限允许列表中的键(与 wpcanai-read-settings 相同);设置 page_on_front 时,在适当情况下也会将 show_on_front 设为 "page"。
  • 返回值: { "success": bool, "updated": string[], "rejected": [{ "key", "reason" }] } — (v1.80.0) reason 为 not_whitelisted、invalid_type 或 invalid_value。没有任何设置成功更新时,调用会失败,错误消息会列出所有被拒绝的键;若有部分键成功写入,success 为 true,其余键会列在 rejected 中 — 请务必读取该字段。
  • (v1.80.0)布尔值。 开/关设置(users_can_register、wpcanai_markdown_negotiation、wpcanai_purge_after_write、WooCommerce 注册开关)接受 true/false、1/0、"1"/"0"、"yes"/"no"。字符串 "false" 在此工具及其他所有工具的布尔输入中都表示 false。
  • 读取端仍采用宽松处理。 若 read-settings 仅收到不在允许列表中的键,则会返回完整的允许列表(不会报错)——返回了数据并不能证明你的键存在。请检查写入操作的 rejected。
wpcanai-setup
  • 参数: { }。
  • 返回值: { "success": bool, "steps": array } — 每个步骤包含:action、status(done | skipped | error)、detail(建议的引导配置:主题、布局、页眉、页脚、主页、首页选项、默认布局、Tailwind 设置)。
wpcanai-get-option
  • 参数: { "name": string } — wp_options.option_name。
  • 返回: { "name": string, "value": mixed }。
  • 错误(v1.89.3): option_not_readable 会说明选项名称和原因。……位于 MCP 拒绝列表中,无法通过 MCP 读取 — 请停止;这是有意屏蔽的,wpcanai-read-settings 涵盖 CanAI 支持的设置。……不在 MCP 读取允许列表中 — 告知用户需要在 CanAI → AI Client → Guardrails 下添加的确切名称,并在用户确认后重试一次;不要不断猜测。
  • 策略: 名称必须位于 WP Admin → CanAI → AI Client → Guardrails 中的读取允许列表。拒绝列表会屏蔽危险键(例如 active_plugins、cron)。以 wpcanai_mcp_ 开头的名称始终会被屏蔽(MCP 内部项)。如果在 wp-config.php 中定义了 WPCANAI_MCP_OPTIONS_UNRESTRICTED,则可以读取其他任何未被拒绝的名称。
wpcanai-update-options
  • 参数: { "options": { "<option_name>": <value>, ... } } — 仅限 JSON 安全的标量和数组。
  • 返回(已应用): { "success": true, "status": "applied", "updated": string[], "pending_id": null }。
  • 返回(已排队): { "success": true, "status": "pending_approval", "pending_id": string, "option_keys": string[], "message": string } — 在管理员点击 WP Admin → CanAI → AI Client → Guardrails → Pending MCP option updates 中的 Approve 之前,不会写入数据库。
  • 策略: 在 Guardrails 标签页配置自动应用和需要批准允许列表。如果某个名称同时出现在两个列表中,以需要批准为准。如果请求中的任何键需要批准,则整个 options 对象会作为一个待处理请求排队。名称必须至少列入一个写入列表(除非启用了不受限模式)。使用与 get-option 相同的拒绝列表。
wpcanai-get-pending
  • 参数: { "pending_id": string } — 当 status 为 pending_approval 时,由 wpcanai-update-options 返回的值。
  • 返回: { "pending_id", "status": "pending"|"unknown", "created_at"?: int, "option_keys"?: string[], "message"?: string } — 批准/拒绝、过期或 ID 无效后,状态为 unknown。请在请求用户到 wp-admin 批准后,使用此工具轮询状态。
wpcanai-i18n-get-settings
  • 参数: { }。
  • 返回: { "default": string, "languages": [{ "slug", "locale", "native_name", "hreflang" }], "enabled": bool, "site_locale": string } — 配置了至少一种非默认语言时,enabled 为 true。(v1.92.0) locale 是该语言被选定时使用的 WordPress 区域设置(ms_MY);对于 WordPress 列表中没有的区域设置,以及在 v1.92.0 之前保存的语言,该值为 ""。(v1.95.0) site_locale 是 WordPress 的站点语言(Settings → General,英语(美国)为 en_US),默认语言始终为此语言。这是路由探测:收到任何翻译请求时,请先调用此工具(参见翻译模型路由器)。
wpcanai-i18n-set-settings
  • 参数: { "languages": [{ "slug"?: string, "locale"?: string, "native_name"?: string, "hreflang"?: string }], "default"?: string } — languages 为必填项。替换整个语言列表:必须包含所有语言,而不仅是新增的语言。(v1.95.0) default 为可选项:参见下文默认语言为站点语言。

  • (v1.92.0)传入 WordPress 区域设置。 每种语言都需要 slug 或 locale(WordPress 区域设置:ms_MY、ja、pt_BR、en_US)。区域设置会像管理后台的语言选择器一样,准确填充你省略的值:

    • slug:ISO 639-1 代码;仅当已有其他语言使用同一代码时,才附加地区标识(pt,然后是 pt-pt)。
    • native_name:WordPress 对该区域设置使用的名称;en_US 对应 "English"。
    • hreflang:不带地区标识的语言代码(ms);如果有其他语言使用相同代码,则使用语言-地区格式(pt-PT)。 因此,{ "languages": [{ "slug": "en" }, { "locale": "ms_MY" }] } 会将马来语添加为 ms / Bahasa Melayu / ms。现有语言若省略 locale,则保留已存储的值;"locale": "" 会清除该值。若未指定 locale,native_name 和 hreflang 默认为 slug。
  • (v1.92.0) 错误。 所有错误都会保留原设置不变,且 error.data.value 会指出有问题的输入:

    • unknown_locale:不是 WordPress locale;消息会列出相近的匹配项(“th_TH is not a WordPress locale. Did you mean th?”);
    • missing_slug:语言既没有 slug,也没有 locale;
    • invalid_hreflang:hreflang 会被规范化(ms_my → ms-MY),必须符合 language[-Script][-REGION] 格式,并且不接受 x-default;
    • duplicate_slug、duplicate_hreflang(不区分大小写);
    • (v1.95.0) default_is_site_language:default 不是站点语言的 slug(data:site_locale、default = 必须使用的 slug、value)。此错误取代 invalid_default;
    • (v1.95.0) site_language_taken:第二种语言使用了站点语言的 locale。默认语言不能同时作为翻译语言。
  • (v1.95.0) 默认语言就是站点语言。 CanAI 没有自己的默认语言。默认语言(不带 URL 前缀、页面所用的语言)是 WordPress 的站点语言 site_locale。此调用会选择该语言对应的行:

    • 首先按 locale 匹配(使用传入的 locale,或该 slug 已存储的 locale);
    • 否则选择 locale 为空、但 slug 或 hreflang 与其相符的行(例如,en_US 站点上的 en);
    • 否则从 WordPress 语言列表中添加一行,并将其放在首位,同时发出警告。

    仅在要确认默认语言时才传入 default。confirm_default_change(v1.92–v1.94)会被忽略。

  • (v1.95.0) 更改默认语言意味着更改站点语言。 这会移动所有 URL:新语言将使用不带前缀的地址,旧默认语言则移至其前缀下(/en/)。请告知用户哪些内容会移动,并先征求同意。 然后可以:

    • 由站点所有者在 设置 → 常规 → 站点语言 中选择,这也会安装语言包;或者
    • 调用 wpcanai-update-options { "options": { "WPLANG": "ms_MY" } }。这要求 WPLANG 位于 update-options 允许列表中(CanAI → AI Client → Guardrails;若不在列表中,工具会返回 options_not_allowed),且必须已安装相应 locale 的语言包(否则 WordPress 会保留旧值)。美国英语请使用 ""。

    无论哪种方式,CanAI 都会跟进处理。新的站点语言会成为默认语言(若尚未配置,则从 WordPress 语言列表中添加),之前的默认语言会保留为翻译语言,不会移除任何语言,下一个管理页面会告知站点所有者哪些内容发生了移动。之后请重新读取 wpcanai-i18n-get-settings。

  • **返回值:**最终设置(与 wpcanai-i18n-get-settings 返回的结构相同)。以下情况会出现 warnings: string[]:

    • (v1.95.0) 添加了站点语言对应的行;
    • (v1.95.0) 保存时将默认语言调整为站点语言(v1.95.0 之前保存的设置与之不一致时会发生;Languages 选项卡在保存前发出警告的情况),并说明发生了哪些移动;
    • 已移除的语言仍有翻译。翻译会予以保留,之后重新添加相同的 slug 即可恢复。
wpcanai-i18n-list-strings
  • 参数: { "lang": string, "untranslated"?: bool, "search"?: string, "post_id"?: int, "layout"?: bool, "menus"?: bool, "location"?: "primary"|"footer" } — lang 必填(用于填充翻译列)。untranslated: true 仅保留翻译为空的字符串;search 根据源文本进行不区分大小写的子字符串筛选。(v1.88.0) post_id 仅保留该文章(页面或模板)中使用的字符串;layout: true 会添加其布局链(布局、页眉、页脚)中的字符串;menus: true 会添加菜单项标题(以及描述,v1.89.0);location 仅添加一个位置的字符串。(v1.89.0) 如果 lang 指定的位置有自己的菜单,则不会贡献任何内容:该菜单已经使用相应语言编写。
  • 返回: { "source": string, "translation": string, "post_ids": int[], "menus": string[], "tags": string[], "placeholders": string[] } 组成的 array。(v1.94.0) tags 是翻译必须保留的标记(["strong"]、["a1","a2"];源文本中的内联标记会在 source 中显示,但不带属性),placeholders 是必须保留的 {names}。post_ids 是 _canai_html / _canai_js 中包含该 t() 源文本的文章。(v1.88.0) menus 列出将该源文本用作菜单项标题或描述的位置(["primary"];没有时为 []),仅用于菜单的行其 post_ids: []。t() 源文本来自字符串索引——如果内容有更改,请先运行 wpcanai-i18n-rescan;每次调用时都会读取已分配菜单中的菜单标题。
wpcanai-i18n-set-translations
  • 参数: { "lang": string, "translations": { "<source>": "<translated>", ... } } — 对一种语言批量新增或更新。lang 必须是已配置的非默认语言(默认语言就是源文本)。空翻译 / null 翻译会删除条目。(v1.94.0) 翻译值必须保留源文本中的标记(<strong>…</strong>、<a1>…</a1>、<br/>)和 {placeholders},顺序不限(可调整位置以适应目标语言);其他标记都会被剥除。若翻译值删除、添加、重复或交叉使用标记,或删除占位符,则不会保存:结果会在 rejected: [{ "source", "reason" }] 中列出该项(例如 "Missing ."),saved 仅统计已写入的内容。源文本键必须与存储的源文本完全一致(逐字符匹配——不要修剪或重新排版)。
  • 返回: { "saved": int, "lang": string }。
wpcanai-i18n-get-media-map
  • 参数: { "lang"?: string } — 省略时返回所有语言。
  • 返回: 以语言 slug 为键的对象:{ "<lang>": { "<original_attachment_id>": <translated_attachment_id>, ... } } — {{ tmedia(id) }} 使用的每种语言各自的附件替换映射。
wpcanai-i18n-set-media-map
  • 参数: { "lang": string, "map": { "<original_id>": <translated_id>, ... } } — 批量新增或更新;仅限非默认 lang。0 / null 会删除映射。
  • 返回: { "lang": string, "map": { ... } } — 该语言最终的完整映射。
wpcanai-i18n-rescan
  • 参数: { } — 根据每篇带有 _canai_html / _canai_js 的文章重建字符串索引。任何内容编辑后,以及运行 wpcanai-i18n-list-strings 前,都应运行此命令。
  • 返回: { "posts_scanned": int }。
wpcanai-i18n-get-post-overrides
  • 参数: { "post_id": int, "lang"?: string } — 省略 lang 时返回所有语言的内容块。
  • 返回: 指定 lang 时为 { "post_id", "lang", "overrides": { title?, content?, excerpt?, fields?: {meta_key: value} } };省略时为 { "post_id", "overrides_by_lang": { "<lang>": {...} } }。空对象 = 没有覆盖项(将渲染默认语言的值)。
wpcanai-i18n-set-post-overrides
  • 参数: { "post_id": int, "lang": string, "overrides": { "title"?, "content"?, "excerpt"?, "fields"?: { "<meta_key>": "<value>" }, "seo_title"?, "seo_description"? } } — 稀疏 upsert:仅更改提交的键;null/"" 值会移除对应覆盖项(回退到默认语言);如果最终内容为空,则删除该行。content 接受 HTML(wp_kses_post);title/excerpt 为纯文本;fields 的值为纯文本。seo_title/seo_description 为纯文本,并会传递给当前启用的 SEO 插件(参见 REFERENCE.md → i18n → SEO translation)。拒绝默认语言(invalid_lang)——文章本身就是默认语言。未知的顶层键会被静默丢弃。
  • 返回: { "post_id", "lang", "overrides": <resulting blob> }。
  • 写入时会使用 sanitize_key 规范化字段键("My_Field" 会存储/匹配为 my_field);渲染读取路径也会以相同方式规范化。wpcanai-i18n-set-translations 的 saved 统计的是已提交的键值对数(包括删除项/跳过的非字符串),而不是写入的键值对数。
wpcanai-i18n-get-term-overrides
  • 参数: { "term_id": int, "lang"?: string }。
  • 返回: 与文章变体结构相同,blob 键为 name / description。
wpcanai-i18n-set-term-overrides
  • 参数: { "term_id": int, "lang": string, "overrides": { "name"?, "description"?, "seo_title"?, "seo_description"? } } — 与文章变体采用相同的稀疏更新/删除/默认语言规则。description 接受 HTML(wp_kses_post,与文章的 content 相同);name 为纯文本;seo_title/seo_description 为纯文本,并会传递给当前启用的 SEO 插件(参见 REFERENCE.md → i18n → SEO translation)。
  • 返回: { "term_id", "lang", "overrides": <resulting blob> }。
全局字符串 — 未关联到任何文章的站点级文案

站点名称、标语,以及归档/搜索/404 页面 SEO 标题和描述存储在按语言区分的全局字符串库中(选项 wpcanai_i18n_global_strings),与文章/术语覆盖项分开,因为它们不属于任何单篇文章。在非默认语言的前端,这些字符串会用于 option_blogname / option_blogdescription + og:site_name(站点名称/标语),以及归档上下文 search、404 和 post_type:<slug> 的 <title> + meta 描述。术语归档(分类 / 标签 / 自定义分类法)不是全局字符串上下文——这些内容使用 wpcanai-i18n-set-term-overrides。

wpcanai-i18n-get-global-strings
  • 参数: { "lang"?: string } — 省略 lang 可获取所有语言。
  • 返回(指定 lang,限定范围): { "lang": string, "strings": { "site_name": string, "tagline": string, "archive": { "<context>": { "title", "description" } } } }。未设置时,site_name / tagline 为 ""。返回(省略 lang,所有语言): { "by_lang": { "<lang>": <blob> } }。只读。
wpcanai-i18n-set-global-strings
  • 参数: { "lang": string, "values": object } — 两者均为必填项。values 键:site_name(纯文本)、tagline(纯文本)、archive(context → { "title"?, "description"? } 映射)。稀疏 upsert:仅更改提交的键;null/"" 值会移除对应键;如果最终内容为空,则删除该语言行。拒绝默认语言(invalid_lang)——站点自身的名称/标语就是默认值。归档上下文包括 search、404 和 post_type:<slug>(例如 post_type:product);术语归档(分类 / 标签 / 自定义分类法)不是全局字符串上下文——请使用 wpcanai-i18n-set-term-overrides 翻译这些内容。
  • 返回: { "lang": string, "values": <resulting blob> }。
wpcanai-i18n-list-content
  • Args: { "lang": string, "post_type"?: string, "untranslated"?: bool } — 内容工作队列。lang 必须是已配置的非默认语言(默认语言对应源内容;传入默认语言会报错 invalid_input)。涵盖公开文章类型中已发布的文章,attachment 除外(最多 1000 行;未知的 post_type 会返回 [];大型站点可用 post_type 缩小范围)。
  • Returns: { "id", "title", "post_type", "status", "kind", "done"?, "total"? } 数组。(v1.91.0) 按 kind 划分,遵循文章列表中的 Translations 列以及 CanAI → Translations → Content 的同一规则:
    • kind: "content"(除 CanAI Twig 页面外的所有文章):覆盖内容块。status 为 untranslated(无内容块)、partial(有内容块,但没有 content 覆盖,且文章正文非空)或 translated。
    • kind: "strings"(_canai_html 非空白的 CanAI Twig 页面):页面自身的 t() 源字符串,不包括布局。done / total 统计已翻译的字符串。status 为 translated(全部)、partial(部分)、untranslated(无)或 nothing(页面没有 t() 字符串)。标题覆盖不再会使 Twig 页面显示为“已翻译”。
    • 在 v1.91.0 之前,所有行都使用内容块规则,且不带 kind。
    • untranslated: true 仅返回未翻译和部分翻译的行,不返回 nothing。
wpcanai-i18n-get-content
  • Args: { "post_id": int, "fields"?: ["meta_key", ...], "lang"?: string } — 必须提供 post_id。fields 用于指定要包含的自定义字段 meta 键(通过 sanitize_key 规范化,与存储覆盖内容时的处理方式一致)。可选的 lang(已配置的非默认语言)还会返回该语言当前的覆盖内容块,便于并排比较。
  • Returns: { "post_id": int, "post_type": string, "source": { "title": string, "content": string, "excerpt": string, "fields": { "<meta_key>": value }, "seo": { "title": string, "description": string } }, "overrides"?: { "<lang>": blob } } — source 是默认语言的原始内容(title/content/excerpt 直接从文章读取;指定的自定义字段通过 meta 读取;seo 是从当前启用的 SEO 插件读取的默认语言 SEO 标题/描述)。只读。这是在不执行求值的情况下获取 Agent 在调用 wpcanai-i18n-set-post-overrides 前需要翻译的源内容的方式。
预设 — wpcanai-list-presets / wpcanai-install-preset / wpcanai-uninstall-preset
  • wpcanai-list-presets → { presets: [{ slug, title, description }] }。内置套件(例如 cpt-corporate、single-freelancer、blog-journal——完整的博客,其 Blog 页面会变为 Reading → Posts 页面)会构建完整的模板栈(布局/页眉/页脚 + 类型化的 single-*/archive-* 模板)。
  • wpcanai-install-preset — { "slug": string, "set_front_page"?: bool, "clean_slate"?: bool, "adopt_woo_pages"?: bool, "trash_woo_pages"?: bool }。安装套件的模板和页面,导入其图片,并且(v1.26+)将面向用户的文案包装在 {{ t('…') }} 中 + 添加 languages() 切换器;系统会预热原生 i18n StringIndex,因此 wpcanai-i18n-list-strings 会立即显示套件中的字符串,即使是在安装后才配置语言。套件模板可以引用 {{preset.image_id.<key>}},以获取导入图片的附件 ID,用于 tmedia()。
    • ⚠ clean_slate: true 具有破坏性。 安装前,它会将所有现有 CanAI 模板和页面移入回收站(可从回收站恢复),并清除预设记录;即使之后安装失败,这些操作也会执行。未经用户明确确认,切勿传入此参数。MCP 宿主会在工具架构中公开此布尔值,但没有内置保护机制。
    • adopt_woo_pages(默认值 true,v1.47.0)。 对于包含 settings.woo_pages 的套件,会将内容写入商店现有的 WooCommerce 页面,而不是创建重复页面(如 checkout-2……),以保留页面 ID 和永久链接,确保已发送邮件中的订单接收 URL 仍然有效。每个接管页面之前的 CanAI 元数据都会被快照保存,并由 wpcanai-uninstall-preset 还原;页面本身绝不会被删除。传入 false 可强制创建新页面(v1.47.0 之前的行为;会导致先前发出的订单链接失效)。
    • trash_woo_pages(默认值 false)。 将现有的商店/购物车/结账/我的账户页面移入回收站(可从回收站恢复),让套件自带的商店页面使用这些别名。此操作会在接管之前执行,因此优先级更高:接管操作查找时,这些位置已空出,并会创建新页面。
  • wpcanai-uninstall-preset — { "slug": string }。移除套件的模板和页面,并清理其创建的所有导航菜单。
wpcanai-export / wpcanai-import
  • wpcanai-export → CanAI 模板和页面的 JSON 包(元数据 _canai_html/css/js/context/layout),以及每行一个 i18n_meta 键,用于往返保留每个 _canai_i18n_{lang} 内容覆盖数据块。(v1.39.0)包含已编译构建的行还会带上预编译的 Tailwind 缓存(_canai_tailwind_build / _hash / _built_at;仅导出非空构建,且排除相对于源文件的 _canai_tailwind_epoch 时间戳),这样导入的布局会以内联方式渲染,而不是回退到 Play CDN。不包含媒体二进制文件——附件 ID 会被标记为需要重新上传,但绝不会自动重新映射。(v1.65.0)也不包含区块页面——导出只匹配 _canai_html 非空的文章,因此正文位于 post_content 中的区块编辑页面不会出现在包中;不要承诺其具有完整的备份覆盖范围。(v1.80.0){ "summary": true } 返回清单而不是数据包——{ templates: [{ id, title, type, status, hashes }], pages: [{ id, title, format, hashes }], counts: { templates, pages, terms }, version },其中 hashes 与 read-meta 返回的 SHA-1 哈希相同,不含内容。可用它比较两个站点,或在拉取任何内容前检查有哪些变化。
  • wpcanai-import — { "data": object | "json": string, "dry_run"?: bool }。dry_run: true 会报告将发生的更改,但不会写入。导入时会重新清理覆盖数据块。**授权限制:**导入会执行 unfiltered_html 级别的写入;除非调用方具备 unfiltered_html 权限(单站点上的管理员具备此权限),或请求携带有效的 CanAI API 密钥(Bearer / X-WPCanAI-API-Key),否则会返回 403 forbidden。
  • **(v1.97.0)按路径匹配页面。数据包版本为 1.1(wpcanai_export_version):每个页面行在 post_name 旁包含 path(gallery/auditorium),委托页面引用也包含 path。导入仅更新现有页面——绝不会创建页面。WooCommerce 页面行会优先匹配商店中该类型的页面。否则,1.1 行只匹配完全相同的 path,不会匹配其他页面(不会回退到单独的 slug,以免覆盖其他位置的同名页面);旧版数据包中的行则仅在站点上恰好有一个页面使用该 slug 时按 slug 匹配,不论其父页面是什么。页面缺失的行,或(旧版数据包中)slug 被多个页面共用的行,会被跳过并显示警告(计入 pages_skipped,且 page_actions 中的 action: "skipped"):若路径缺失,警告会说明该 slug 唯一对应的页面所在位置(若存在)——使用 wpcanai-update-page 将页面移至该位置后重新导入;若 slug 被多个页面共用,警告会列出每个页面,格式为 path (ID n)——请从 CanAI 1.97+ 重新导出。带有 path 的委托引用仅按该路径解析;若该位置没有页面,则导入时不设置委托页面,并显示相应警告。(v1.97.1)**旧版数据包中的委托引用(无 path)仅在恰好有一个页面或文章使用该 slug 时解析。导入前,使用 wpcanai-create-page(带 parent)创建缺失的页面。
wpcanai-diagnostics
  • 参数: { "include_network"?: bool }(默认值为 false)。→ 生成一份与 wp-admin Diagnostics 页面内容一致的健康报告(PHP/WP/插件版本、功能与端点检查、Tailwind 构建状态、WooCommerce 是否存在及 HPOS 安全性说明)。
  • include_network: true 还会发起真实的出站 HTTP 请求(出站 HTTPS、REST 环回、技能端点可达性、认证标头透传)——耗时较长;怀疑存在连接或环境问题时使用,而不是猜测。
  • (v1.80.0)新增 page_cache 检查(检测到的插件、清除钩子、自动清除开/关)和 integrations 组(每个工具标签一行:是否存在、已注册的工具、是否禁用)。(v1.82.0)integrations 行按工具声明的标签(touches + requires)统计工具数量,因此即使集成不存在,也会报告声明该集成的工具数量,并带有 hidden_by_recommendation。
  • **(v1.82.0)**新增 tools 组:tool_budget(已暴露与工具总数、字节数、约略 token 数)和 tool_recommendations(当“Follow recommendations”关闭且建议与当前状态不同时发出警告;fix 是准确的 manage-tools 调用)。languages 组:languages(已配置的母语,或无)和 translation_tools——语言列表与 Languages 工具组配对:无/关闭 → 通过(未使用),无/开启 → 警告(使用 i18n-set-settings 添加语言,或关闭该组),有/开启 → 通过,有/关闭 → 信息(这是合理的所有者选择;如需翻译,可使用 manage-tools 启用)。这是诊断中唯一提及翻译的地方;未选择启用时,其他任何地方都不会提及翻译。
  • **(v1.90.0)**新增 render 组:如果过去 7 天内没有模板触发渲染警告,则显示一行 pass;否则每条记录显示一行 warning(按时间从新到旧,最多 10 条,之后显示一行 info,列出其余记录),并附带 post_id、field、line 和 edit_url——与 wpcanai-scan 报告为 render_warning 的记录相同。
  • **(v1.88.0)新增 menus 组:每个位置一行——已分配菜单时显示 pass、菜单名称和项目数;未分配任何菜单时显示 warning(fix 是一次 write-menu 调用)——另有一行 info,列出仍分配在 v1.88 之前位置名称下的菜单。(v1.89.0)**每个语言菜单再显示一行 info(“菜单位置:footer (Bahasa Melayu)”)。
wpcanai-purge-cache(插件 v1.80.0;需要 cache)
  • 仅当受支持的缓存插件处于活动状态时才能使用(LiteSpeed Cache、WP Rocket、W3 Total Cache、WP Super Cache、WP-Optimize 且已开启其页面缓存——v1.82.0)。如果没有受支持的插件,建议将其关闭,通常也不会出现在列表中(规则 R1)。它只清除第三方页面缓存:CanAI 的 Twig 编译缓存和 Tailwind 构建会在每次源文件变更时自动失效,无需清除。
  • 参数: { "scope": "all"|"url"|"post", "target"?: string|int }——target 在 url 时为 URL(必须属于本网站),在 post 时为文章 ID;在 all 时会被忽略。
  • 返回值: { "purged": [{ "scope", "target" }], "plugin": "litespeed"|"wp-rocket"|"w3tc"|"wp-super-cache"|"wp-optimize"|null, "note"? }。plugin: null 表示未检测到受支持的缓存插件——不是错误;此时只有 post 会执行操作(清除 WordPress 文章缓存),而 all/url 会返回一条说明。
  • 通常不需要使用它。 开启写入后清除缓存(wpcanai_purge_after_write,默认开启)时,每次 CanAI 写入都会在请求结束时清除一次缓存——更改模板会清除整个网站的缓存,更改页面会清除该页面的缓存——并在写入操作的 purged 字段中说明(参见工具标签)。只有当存在缓存插件但 purged 返回 null(自动清除已关闭),或写入操作未覆盖某个 URL(例如列出你编辑的页面的归档页)时,才调用 purge-cache。

部署 Google Stitch 界面 → canai-stitch 技能

Google Stitch 部署(CanAI Stitch 插件的 wpcanai-stitch-connect、-list-projects、-set-projects、-list-screens 和 -deploy-screen 工具)有专属技能 canai-stitch(npx skills add usewp/canai --skill canai-stitch): 选择“Stitch 保持为源”还是“移交给 CanAI”、使用 stitch_page_edited 防护机制的部署和重新部署工作流,以及将忠实副本转换为 CanAI 约定的清理流程。处理任何 Stitch 请求时都要加载此技能。除非所有者要求替换已有编辑,否则切勿将 overwrite: true 传给 wpcanai-stitch-deploy-screen。


上传媒体

两种方式——根据来源选择:

来源使用方式原因
图片已有 URL(https://…/photo.jpg——例如从源站点、CDN 或副本捕获中直接引用的图片)MCP 工具 wpcanai-sideload-url服务端会获取文件内容——通过 MCP 传输的 JSON 很小,无需 API 密钥 / 基础 URL / curl。推荐使用。
本地二进制文件(用户机器上的文件,没有公开 URL)sideload REST 端点(如下)MCP 不适合承载二进制负载(大小和 base64 编码)。
URL 图片 → wpcanai-sideload-url(推荐,纯 MCP)

调用 MCP 工具——无需收集凭据,它通过经过身份验证的 MCP 传输进行调用:

wpcanai-sideload-url {
  "url": "https://example.com/images/hero.jpg",
  "alt": "Front of the product",     // optional
  "attach_to": 0                      // optional parent post id
}

可选参数:filename、title、caption、description。返回 { id, source_url, mime_type, filename, attached_to }。编写 _canai_html 时,优先使用返回的 id,通过 {{ image_attrs(id, 'src,alt') }} / {{ media_url(id, 'full') }} 引用(在渲染时解析——参见 优先使用 CanAI 模板函数 原则);只有在需要绝对 URL 时才保留绝对 source_url(OG/Twitter 元数据、Web 清单)。对于资源通过 URL 直接引用的副本/导入流程,应使用此工具。

本地二进制文件 → sideload REST 端点

MCP 消息不适合承载二进制负载(大小和 base64 编码开销)。请改用 sideload REST 端点。它接受与 MCP 传输相同的 CanAI API 密钥,无需单独的凭据。

  • 端点: POST {site}/wp-json/wpcanai/v1/sideload
  • 身份验证: Authorization: Bearer <wpcanai_…API key>(或 X-WPCanAI-API-Key:)。
  • 请求体: multipart/form-data,必需字段为 file=@<local-path>。可选字段:filename、title、alt、caption、description、attach_to(父文章 ID)。
  • 返回: { id, url, source_url, mime_type, filename, title, alt, attached_to }。
  • API 密钥用户必须具备的权限: upload_files(管理员拥有此权限)。
工作流(每次处理一张图片)

对于静态站点导入(用户运行了 /canai-mcp implement <folder> 或类似命令),无需询问——自动检测引用的资源,并在预处理阶段将其导入。参见“Implement HTML → CanAI”下的 静态站点资源预导入。以下步骤适用于该流程之外的临时单文件上传。

  1. 如果用户尚未提供本地文件路径,请向用户索要。
  2. 对每个文件,从用户的机器发送 POST 请求(每次一个,以便获取各自的 id)。curl 最简单,也是获准使用的唯一 shell 例外:
    curl -sS -X POST "{site}/wp-json/wpcanai/v1/sideload" \
      -H "Authorization: Bearer <API key>" \
      -F "file=@/abs/path/to/image.jpg" \
      -F "alt=Front of the product"
    
    对于仅使用 Node 的技术栈(没有 curl 或 Python——Node 18+ 内置全局 fetch/FormData/Blob;参见 本地脚本辅助),可使用以下零依赖等效方案:
    node -e "const fs=require('fs');const f=new FormData();f.append('file',new Blob([fs.readFileSync(process.argv[1])]),require('path').basename(process.argv[1]));f.append('alt',process.argv[2]||'');fetch(process.argv[3]+'/wp-json/wpcanai/v1/sideload',{method:'POST',headers:{Authorization:'Bearer '+process.argv[4]},body:f}).then(r=>r.text()).then(console.log)" /abs/path/to/image.jpg "Front of the product" "{site}" "<API key>"
    
  3. 获取返回的 id,并通过现有 MCP 工具将其传入模板——在 _canai_html(wpcanai-write-meta)中,优先使用 {{ image_attrs(id, 'src,alt') }} / {{ media_url(id, 'full') }},而非固定 URL;只有在需要绝对 URL 时才保留绝对 url(OG/Twitter 元数据、Web 清单)。

不要尝试将文件编码为 base64 后放入 MCP 工具参数中——请使用此端点。

媒体 SEO 清理(wpcanai-list-media / wpcanai-get-media / wpcanai-update-media)

清理命名不当的上传文件并补充缺失的替代文本(插件 1.45.0+):

  1. 审核 — wpcanai-list-media { "missing_alt": true } 和/或 { "generic_filename": true } (如 IMG_4521.jpg 这类相机或屏幕截图文件名)。使用 page/per_page 分页。
  2. 了解上下文 — wpcanai-get-media { "id": 123 } 会返回完整元数据及 used_in:引用该文件的页面。请先阅读这些页面——替代文本和文件名必须描述图片在页面上下文中展示的内容。切勿根据旧文件名推导替代文本。
  3. 修正 — wpcanai-update-media { "id": 123, "alt": "…", "title": "…", "filename": "red-leather-office-chair" }。
    • filename 是不带扩展名的 slug;文件及所有尺寸变体都会在磁盘上重命名,且所有内部引用都会重写(post_content + _canai_html/_canai_css/_canai_js 元数据)。
    • 之后旧 URL 会返回 404(不会重定向)——请再次检查 used_in 中的页面是否能正常呈现。
    • 显式传入空字符串可清空字段。仅更新元数据不会触碰文件。
    • 仅限本地文件;卸载到 S3/CDN 的媒体会返回 file_missing。

严重:CanAI 存储模型

CanAI 不使用 post_content 存储 Twig 正文。请使用 **wpcanai-read-meta** / **wpcanai-write-meta** 处理 _canai_html、_canai_css、_canai_js、_canai_context、_canai_layout。标记为 blocks 的页面是例外:其正文位于 post_content,此技能不会写入该字段。

严重:内容解析

WC 商店/购物车/结账/我的账户/产品分类内容通常位于 WC 页面;产品 / 404 / 搜索 / 存档类型的内容通常位于 **wpcanai_template**。编辑错误文章前,请先使用 **wpcanai-resolve-content-id**。


翻译模型路由器

CanAI 只有一种翻译模型:原生字符串翻译(每个页面对应一篇文章,全站字符串表,插件 1.22.0+)。按语言复制文章并非 CanAI 模型——模板和页面不按语言区分。面向文章的工具(list-templates、read-meta、write-meta、create-page、write-post 等)不接受 lang 参数(多余的参数会被忽略),也不会返回 lang 键;只有 i18n-* 工具会出现 lang,用于指定正在翻译的语言。遇到任何翻译请求——“翻译这个站点/页面”“添加一种语言”“多语言”“本地化”——请先检查设置:

  1. (v1.82.0)i18n-* 工具为可选工具,默认隐藏。 如果列表中没有 wpcanai-i18n-get-settings,不要报告翻译功能不受支持:运行 wpcanai-manage-tools { "action": "list" },确认用户希望在此站点启用翻译后,运行 { "action": "set", "groups": { "Languages": "on" } },重新连接,然后继续。
  2. 调用 wpcanai-i18n-get-settings { }。
    • enabled: true → 按照下方的原生字符串翻译流程操作。不要创建按语言复制的文章。
    • enabled: false → 原生翻译已关闭。请与用户确认要使用的语言(使用 WordPress 区域设置,例如 ms_MY)。默认语言始终是站点语言(site_locale,v1.95.0),并且应与页面当前使用的语言一致。如果不一致,请说明这一点,并先更改站点语言(参见 wpcanai-i18n-set-settings → 更改默认语言意味着更改站点语言)。然后使用 wpcanai-i18n-set-settings 初始化,为每种语言传入 locale,以便 CanAI 填写 slug、名称和 hreflang(v1.92.0),之后继续执行原生工作流。

原生字符串翻译(单篇文章国际化)

**运行时机:**上方路由器已判定使用原生模型——用户要求翻译整个网站或某个页面、添加语言、补齐缺失的翻译或本地化图片,并且 wpcanai-i18n-get-settings 返回 enabled: true(或你刚完成初始化)。

模型(插件 1.22.0+):每个页面/模板只有一篇文章,不为每种语言创建副本。面向用户的字符串在 _canai_html / _canai_js 中用 {{ t('…') }} 包裹,并在渲染时从全站字符串表中解析。非默认语言通过路径前缀提供(/ms/plt/);默认语言不带前缀。hreflang alternate 会在 wp_head 上输出,tmedia() 映射会按语言替换附件。

工作流
  1. 设置 — wpcanai-i18n-get-settings { }。如果语言缺失或有误,请先与用户确认,再用 wpcanai-i18n-set-settings 写入(它会替换整个列表:请包含所有语言)。默认语言为站点语言(v1.95.0);更改为其他默认语言等同于更改站点语言,应由用户决定。

  2. 创作前提——所有面向用户的字符串都必须用 {{ t('…') }} 包裹。只有 t() 的源字符串会被索引并可供翻译。如果页面中有硬编码字符串,请先将其包裹起来:wpcanai-read-meta → 构建替换对 → wpcanai-replace-in-meta,例如 { "post_id": <id>, "replacements": [{ "from": ">Shop now<", "to": ">{{ t('Shop now') }}<" }] }(以周围的标记作为匹配依据,确保匹配唯一)。(v1.94.0)每个句子使用一个 t(),并将句内标签放在其中。

    • 写成 {{ t('Get <strong class="font-semibold">free shipping</strong> today.') }},不要写成 {{ t('Get') }} <strong>{{ t('free shipping') }}</strong> {{ t('today.') }}。译者需要看到完整句子:不同语言的语序不同,片段无法重新排序。
    • 只有行内元素可以放在其中(strong、em、a、span、br、code 等)。块级元素(p、div、li、标题)放在外面,每个块使用一个 t()。
    • 动态值作为 {placeholders} 传入变量:{{ t('Read our <a href="{url}">terms</a>.', {url: turl('/terms/')}) }}、{{ t('By {name}', {name: post.author.display_name}) }}。
    • 可翻译属性(title、aria-label)单独使用一个 t(),并作为变量传入。
    • 整体设置样式的短语保持原样:<strong>{{ t('Best seller') }}</strong>。
    • 字符串键不包含属性(Get <strong>free shipping</strong> today.),因此调整样式后翻译仍然有效。重复出现的标签会加上编号(<a1>、<a2>)。

    **合并片段。**如果句子仍被拆分,wpcanai-scan 会报告 split_sentence。使用 wpcanai-replace-in-meta 将片段合并(整段片段 → 一个 t(),并将标签放在其中),运行 wpcanai-i18n-rescan,然后翻译新键,并以 i18n-list-strings { "post_id": … } 中旧片段的翻译作为参考。旧片段键仍会保留在表中,但不再使用。在原生 i18n 网站上创作新页面时,从一开始就用 t() 包裹面向用户的字符串(参见 实现 HTML → CanAI 中的辅助表)。

  3. 重建索引 — 每次编辑内容后运行 wpcanai-i18n-rescan { }。wpcanai-i18n-list-strings 读取的是索引,而非实时 meta;索引过期时,列出的字符串也会过期。

  4. 翻译——每种非默认语言一次处理一个页面:

    • wpcanai-i18n-list-strings { "lang": "ms", "post_id": 42, "layout": true, "menus": true, "untranslated": true } ——列出该页面尚未翻译的 t() 字符串、页面布局(页眉、页脚)以及菜单项标题。从这里开始。只有用户要求翻译整个网站时,才列出全站内容({ "lang": "ms", "untranslated": true })。
    • 自行翻译每个 source(由 Agent 撰写译文)。菜单项标题和描述也是普通 source(menus 为 ["primary"] 或 ["footer"] 的行):在同一次调用中翻译它们,get_menu() 会呈现翻译后的内容——模板中无需使用 t(),也无需重新扫描。如果某个语言版本有自己的菜单,该菜单不会出现在列表中:因为它已经使用该语言编写(参见按语言设置菜单)。
    • 通过一次批量调用写入:wpcanai-i18n-set-translations { "lang": "ms", "translations": { "Shop now": "Beli sekarang", "Add to cart": "Tambah ke troli" } }。
    • 提供 source → 译文对照表,方便用户抽查;如需更正,再进行一次批量调用即可。
    • 站点所有者也可以使用页面上的“Translate strings”按钮完成相同操作(插件 v1.88.0):该按钮位于页面编辑器的 Translations 框、区块页面的 CanAI 编辑器和 CanAI 侧边框中,以及前端管理工具栏的 Translate this page。**(v1.91.0)**在 All Pages / All Posts / All Templates 中,Translations 列会为每种语言显示一个状态标记:MS 表示已翻译,MS• 表示部分翻译,+ MS 表示尚未翻译,— 表示无需翻译。点击标记可打开对应的编辑器;此功能已取代“Translate: {language}”和“Translate strings”行操作。它会打开 CanAI → Translations → Strings,并筛选到对应页面,同时提供用于控制是否包含页面布局和菜单的开关。需要用户手动翻译时,请引导他们使用此功能。
  5. 内容(CPT / 长篇内容)——每种非默认语言:

    • wpcanai-i18n-list-content { "lang": "ms", "untranslated": true } → 显示标题/正文/字段仍以默认语言呈现的文章队列。(v1.91.0)kind: "strings" 行表示 CanAI Twig 页面:请按步骤 4 翻译其 t() 字符串(i18n-list-strings { "lang": "ms", "post_id": <id> } → i18n-set-translations),不要使用覆盖项。以下步骤适用于 kind: "content" 行。
    • 对于每篇文章:wpcanai-i18n-get-content { "post_id": <id>, "fields": ["subtitle", ...] } 会返回默认语言的原始 source(title、content、excerpt、命名自定义 fields)。翻译后,通过一次 wpcanai-i18n-set-post-overrides { "post_id": <id>, "lang": "ms", "overrides": { "title": "…", "content": "…", "excerpt": "…", "fields": { "subtitle": "…" } } } 写入。向 i18n-get-content 传入 "lang": "ms",还可并排查看现有覆盖项。请使用专用 i18n 工具——不要使用 PHP eval 翻译内容。
    • 循环/归档中显示的分类法术语:对每个术语调用 wpcanai-i18n-set-term-overrides。
    • 提供逐篇文章的覆盖情况表(id → 翻译前/后的状态)。URL 不会改变——同一篇文章会在 /ms/… 下提供带覆盖字段的内容(不会为每种语言创建副本)。
    • SEO 元数据(标题/描述)通过 seo_title/seo_description 覆盖键翻译——使用 i18n-get-content 读取 source(关于完整的 SEO bridge 行为,请参阅 references/REFERENCE.md)。只有 SEO 文案需要与可见标题/正文不同时,才需要设置这些键——否则,在非默认语言页面上,seo_title/seo_description 会根据翻译后的 title/name 和 excerpt/去除标签后的 content 自动生成(描述会在不超过 160 个字符且不截断单词的边界处截断)。
  6. 站点级字符串(站点名称 / 标语 / 归档 SEO)——针对每种非默认语言:

    • wpcanai-i18n-get-global-strings { "lang": "ms" } → 当前站点名称、标语和归档映射。
    • 翻译后,通过一次调用写入:wpcanai-i18n-set-global-strings { "lang": "ms", "values": { "site_name": "…", "tagline": "…", "archive": { "search": { "title": "…", "description": "…" }, "404": { "title": "…" } } } }。
    • 归档上下文包括 search、404、post_type:<slug>。分类 / 标签 / 分类法标题属于术语覆盖项(第 5 步),而不是全局字符串。
  7. 媒体(需要按语言使用不同图片时)——使用 wpcanai-sideload-url 上传本地化资源,然后映射:wpcanai-i18n-set-media-map { "lang": "ms", "map": { "123": 456 } }。模板必须通过 {{ tmedia(123) }} / {{ tmedia(123, 'large') }} 引用该图片——没有映射时会回退到原图片;普通的 media_url() 会忽略映射。

  8. 在浏览器中验证——访问带语言前缀的 URL({site}/ms/<page-slug>/):翻译后的字符串能够正确显示;<head> 包含每种语言对应的 <link rel="alternate" hreflang="…">;canonical 和 og:url 带有 /ms/ 前缀;og:site_name 显示翻译后的站点名称,且 og:locale 与语言相符;languages() / lang_url() 的切换器 URL 带有该前缀;默认语言仍不带前缀。

注意事项
  • 纯文本翻译。 wpcanai-i18n-set-translations 会从值中剥除 HTML。标记应放在模板中 t() 外部,绝不要放在 t() 内。
  • 键必须原样匹配。 只有当 t() 的源文本逐字匹配时,翻译才会生效。将源字符串传回时,不要修剪、重新排版或更改标点。
  • 默认语言为只读。 wpcanai-i18n-set-translations / wpcanai-i18n-set-media-map 会拒绝默认语言——源文本就是默认显示内容。
  • 索引过期。 编辑 _canai_html / _canai_js 后,列出内容前请重新扫描。
  • 配置前不会生效。 未配置非默认语言时,前缀路由 / 重定向 / hreflang 均处于关闭状态,t() / tmedia() 会原样返回内容。
  • 内容覆盖仅影响前端。 管理后台、REST 编辑上下文和默认语言始终显示原始值;只有 /{lang}/ 前端页面会替换内容。
  • 编辑者也可以在 wp-admin 中翻译。 从插件 v1.70.0 起,通过 post.php?post=<id>&action=edit&lang=<slug>(“语言模式”)打开的区块编辑器文章会加载并保存该语言的覆盖数据块——也就是 wpcanai-i18n-set-post-overrides 写入的同一份 _canai_i18n_<lang> 数据。如果用户说他们“在编辑器中”修改了翻译,请使用 wpcanai-i18n-get-content { "post_id": <id>, "lang": "<slug>" } 读取,而不要默认认为该内容尚未翻译。
  • 搜索只匹配默认语言文本——WP 搜索不会为覆盖数据块建立索引。
Twig 辅助函数

t()、tmedia()、current_lang()、languages()、lang_url()、turl()——参见 references/REFERENCE.md(turl() 位于 Media & Links 下)。turl('/path/')(插件 v1.88.0)是当前语言的 home_url():默认语言返回普通 URL,ms 返回 /ms/path/,因此内部链接不会自行拼接前缀。turl(path, lang) 可指定目标语言(用于切换器)。slug_url() 也会以相同方式进行本地化;get_menu() 返回的项目中,title / description 已翻译,url 已本地化;raw_title / raw_description 则保留未翻译文本(某种语言自己的菜单会按原样显示)。current_language() 和 language_switcher() 仍可作为 current_lang() 和 languages() 的别名使用(适用于已有这些用法的模板);编写新模板时请使用原生名称。


常见工作流(通用表示法)

使用 工具名称 + JSON 参数;按宿主的 MCP 调用语法进行映射。

在 DevTools 中调整样式(三次调用)

用户在浏览器中进行尝试后,给你一个页面 URL、一段可见文本和一个属性 (例如“文本 conference,将 line-height 设为 1.15”)。不要读取整个页面,也不要 在本地镜像页面——只需三次调用:

  1. 解析文章 ID。 调用 wpcanai-list-pages {},匹配 URL 的 slug/标题(首页 URL 是静态首页——如果标题不明确,通过 wpcanai-read-settings { "keys": ["page_on_front"] } 读取 page_on_front); WooCommerce 路由 → wpcanai-resolve-content-id { "type": "shop" } 等。
  2. 查找元素。 调用 wpcanai-grep-content { "post_id": <id>, "pattern": "conference", "max_length": 1000, "context_lines": 2 } → 该行的 match 是包含元素类名的逐字节精确文本行。
  3. 仅在该元素内编辑。 调用 wpcanai-replace-in-meta { "post_id": <id>, "scope": { "text": "conference" }, "replacements": [{ "from": "leading-none", "to": "leading-[1.15]" }], "require_all": true }。 如果元素已带有工具类,优先使用任意值 Tailwind 工具类(leading-[1.15]、text-[17px]、tracking-[-0.01em]);如果元素没有 class 属性,则在同一作用范围内使用 from: "<h1" → to: "<h1 class=\"leading-[1.15]\""。只有当该属性没有对应工具类时,才通过 field: "css" 添加 CSS 规则。

如果第 3 步返回 replace_no_match,请读取消息(Nearest match: <kind> at line …)——whitespace_only → 使用 ignore_whitespace: true 重试相同的作用范围调用;outside_scope → 类名位于另一个元素上(通常是父元素),因此选择出现在该元素中的 scope.text——绝不要为了匹配而去掉 scope,否则会改写另一个元素;如果确定目标就是作用范围内的元素,请先尝试 ignore_whitespace: true,然后使用作用范围限定的 grep-content(max_length: 1000)查看其精确字节内容。选择 scope.text 时,请使用元素自身的首段文本或属性值:服务会将作用范围限定到文本前最近的起始标签,因此 <span> 子元素内的文本会限定到 span,而不是标题。只为更改单个属性时,绝不要对整个字段执行 read-meta。之后不需要运行 wpcanai-scan,也不需要重新编译 Tailwind(参见何时重新运行)。

编辑或移除现有 HTML 中的某个部分
  1. 解析 ID — wpcanai-list-pages { } 或 wpcanai-resolve-content-id { "type": "shop" }(等)。使用返回的 content_post_id(或 list-pages 中对应行的 id)。
  2. 读取 — wpcanai-read-meta { "post_id": <id>, "fields": ["html"] }(如有需要,可添加其他字段)。
  3. 优先使用定向替换。 根据 grep-content(post_id、max_length: 1000)中的逐字节精确文本行,构造 {from, to} 对,并调用 wpcanai-replace-in-meta,设置 require_all: true——如果替换对较多,请先添加 dry_run: true。如果源代码将属性换行书写,请使用 ignore_whitespace: true。
  4. 仅在结构性重写时才退回完整写入 — 使用 wpcanai-read-meta { "post_id": <id>, "fields": ["html"] }(或读取片段:{ "post_id": <id>, "fields": ["html"], "lines": [from, to] }),编辑后调用 wpcanai-write-meta { "post_id": <id>, "html": "<full updated HTML>" },并提供 expected_hash。
修复现有页面中失效的资源 URL

当线上页面引用了错误的资源 URL(例如导入后残留的相对路径,或已迁移的 CDN)时,可在不重写整个文档的情况下修复:

  1. 解析 ID — wpcanai-list-pages / wpcanai-resolve-content-id → 页面/模板的 post_id。
  2. 将线上 URL 放入媒体库 — 对于每个缺失的资源:可直接引用的 URL → wpcanai-sideload-url;本地二进制文件 → 使用 sideload REST 端点(POST {site}/wp-json/wpcanai/v1/sideload,这是唯一获准使用的 curl — 参见 上传媒体)。记录每个返回的 source_url。
  3. 构建 from→to 映射 { <原有/失效引用,须与实际内容完全一致> : <新 source_url> }。包含 HTML 中出现的每种形式(./img/x.png、img/x.png、/img/x.png)。
  4. 应用替换 — wpcanai-replace-in-meta { "post_id": <id>, "field": "html", "replacements": [{ "from": "...", "to": "..." }, ...], "require_all": true }。每个受影响的字段/页面调用一次。检查每个 count(并让 require_all 标记任何未匹配的 from)。如果资源在其中被引用,则对 "field": "css" / "js" 重复操作。
创建新模板
  1. wpcanai-create-template { "title": "...", "type": "component", "html": "...", "css": "..." }
  2. 如果需要布局:wpcanai-write-meta { "post_id": <new_id>, "layout": <layout_post_id> }
诊断配置
  • wpcanai-scan { } — 委派、布局、模板、结构导航和内容泄漏问题(missing_structure_comment / invalid_structure_comment / leaky_comment / leaky_secret)。
  • wpcanai-manage-tools { "action": "list" } — 列出所有工具,包括隐藏的工具、每个工具被隐藏的原因,以及已公开工具集的令牌预算;wpcanai-diagnostics 中的 tools 和 languages 分组汇总了相同信息。
注释 / 密钥安全检查

在批量导入后、将网站交还给用户前,或用户要求审查源代码泄漏时运行。采用与 sideload 预检查相同的固定操作流程。

  1. wpcanai-scan { } — 收集所有结构/注释/密钥问题(记录 post_id 和 field)。
  2. 可选:全站 grep,查找扫描可能无法分类的模式:
    对 Twig 使用 wpcanai-grep-content { "pattern": " 搜索可疑的 /* … */ 注释。对于 leaky_secret,从 meta 中移除凭据(将其移至 wp-config / env / 浏览器无法看到的服务端代码片段)。
  3. 重新扫描,直至不再有 leaky_* 问题。只要这些问题仍然存在,就不要声称任务已完成。

为生产环境编译 Tailwind

何时运行: 用户说 /canai-mcp optimize production、/canai-mcp compile tailwindcss、compile tailwind、build css、tailwind build,或以其他方式要求为线上网站预编译 Tailwind 时运行。不要在每次编辑模板后自动运行 — 这是一项明确选择启用的优化步骤。

作用: 默认的 Tailwind 加载方式是 Play CDN(来自插件的 tailwind.min.js 或 cdn.tailwindcss.com),它会在每次页面加载时于访客的浏览器中即时编译工具类。此工作流在 Agent 端预编译 CSS,并将其存储为 meta。存在构建产物时,服务器会内联输出预构建的 CSS,并跳过 Play CDN 脚本 — 访客获取静态 CSS,由 Agent 执行编译。

构建单位 = 布局。 每个布局编译一个 CSS 包,扫描该布局以及通过它渲染的每个页面/模板,并将 CSS 存储在布局文章上。服务器在渲染时收集该布局的构建产物(布局文章会在每个使用该布局的页面上渲染),并仅输出一次。这样,编译后的 CSS 都集中在一处,便于查找 — 位于 CanAI Editor 中布局的 Compiled Tailwind CSS 面板,而不是散落在各个页面中。

存储约定
Meta 键所属位置用途
_canai_tailwind_build布局文章(wpcanai_template,类型为 layout)布局及其所有使用方的编译后 CSS。AssetManager::output_assets_head 会将其作为 <style id="wpcanai-tailwind-css"> 输出,并跳过通过该布局渲染的页面的 Play CDN。
_canai_tailwind_hash同一布局文章仅供 Agent 端使用,可选。 生成构建产物的输入内容的 sha256。重新编译前进行比较 — 未更改时跳过。新鲜度检测不需要此项;服务器通过 _canai_tailwind_epoch 跟踪新鲜度(见下文服务器端新鲜度)。
_canai_tailwind_epoch同一布局文章写入时自动设置 — 构建产物编译时的源纪元。服务器会将其与全局纪元比较,以判断是否过期。无需自行设置。
_canai_tailwind_built_at同一布局文章Unix 时间戳;通过 wpcanai-write-meta 写入非空的 tailwind_build 时自动设置(用于面板显示“距构建时间 N”)。无需自行设置。

为空或缺失 → 服务器回退到 Play CDN。去重保护:如果渲染集中的某个布局有构建产物,服务器只会输出布局构建产物,并忽略任何页面级 _canai_tailwind_build — 因此,旧版页面级模型遗留在页面上的过期构建产物不会重复输出。手动编辑 wp-admin 仍然有效;只是会使用运行时 CDN,直到下次编译。

服务器端新鲜度(自动)。每次写入 tailwind_build 时,服务器都会为构建产物标记当前的全局源纪元(_canai_tailwind_epoch);每当任何模板源(_canai_html/css/js)或 Tailwind 设置发生变化、区块页面的正文发生变化(所有者在 Gutenberg 中保存,或通过 MCP 写入),或页面的格式标记发生变化时,服务器都会递增该纪元。渲染时,标记落后于当前纪元的构建产物会被视为过期,页面会回退到 Play CDN(结果正确,只是速度较慢),而不是输出过时的 CSS — 因此,编辑后页面绝不会失去样式,即使尚未重新编译也是如此。这就是为什么下文的 tailwind_hash 现在是可选项:正确性不再依赖它。诊断页面和布局的已编译 Tailwind CSS面板会显示哪些构建产物已过期。编辑模板后,重新编译受影响的布局,以恢复内联交付。

工作流
  1. 读取 Tailwind 设置以获取插件集:

    wpcanai-read-settings { "keys": ["wpcanai_tailwind_settings"] }
    

    调用编译器时使用返回的 plugins 数组(例如,["forms", "container-queries"])。如果 load_tailwind 为 no,请停止并告知用户先启用它(没有 Tailwind 可供编译)。

  2. 枚举布局 — wpcanai-list-templates {},保留 type 为 layout 的行。每个布局都是一个编译目标。

  3. 解析每个布局的使用方集合 — 所有有效布局为该布局的文章:

    • wpcanai-list-pages {} 和 wpcanai-list-templates {} 中 layout_id 等于此布局 id 的行。
    • WC 委托正文:wpcanai-resolve-content-id { "type": "shop" }(以及 cart、checkout、my-account、product-category)→ _canai_layout 解析为此布局的 content_post_id。
    • 如果此布局是 wpcanai_default_layout,还要包括没有显式 _canai_layout 的文章(它们会回退到此布局)。
    • format 为 blocks 的行(wpcanai-list-pages 会携带该值)也是使用方 — 它们的正文是 post_content,而不是 _canai_html,扫描方式与 Twig 正文完全相同(插件 v1.67.0;在此之前,服务器会强制区块页面使用 Play CDN)。
  4. 构建并集内容 — 将以下内容串接为一个字符串:布局自身的 _canai_html、每个使用方的 _canai_html、通过 {% include 'slug' %} / {{ wpcanai_template('slug') }} 引用的所有局部模板(页眉/页脚 — 将每个 slug 解析为 post id → 读取其 _canai_html),以及对于每个 format 为 blocks 的使用方,从 wpcanai-read-meta { "post_id": <id>, "fields": ["blocks"] } 获取的原始区块标记(className 值同时存在于区块注释 JSON 和渲染后的 class="" 中,因此原始标记是正确的扫描输入)。不要将区块页面的 css 插槽并入并集 — 它是直接输出的纯 CSS,不是 Tailwind 源码,因此不会为扫描贡献任何工具类;而且,对页面仅写入 css 不会推进源纪元,因此在第 5 步对其进行哈希,会导致构建看起来仍是“最新”,尽管其记录的输入已发生变化。写入临时 .html 文件,以便 Tailwind 扫描器获取该布局中任何位置实际使用的所有类。

  5. 对输入进行哈希 — 对 union_content + "\n--\n" + plugins.sort().join(",") + "\n--\n" + tailwind_version 计算 sha256。与布局现有的 _canai_tailwind_hash 比较。相同 → 跳过此布局,计为“已跳过(已是最新)”。将实际编译时使用的 v3 版本作为 tailwind_version,确保哈希在不同机器上保持稳定(并确保意外使用 v4 构建时会使哈希失效,而非发生冲突)。使用 Node,而非 Python 进行哈希(参见本地脚本辅助代码)— 例如:node -e "const c=require('crypto'),fs=require('fs');console.log(c.createHash('sha256').update(fs.readFileSync(process.argv[1])).digest('hex'))" hash-input.txt。

  6. 在本地编译(不在服务器上编译),并使用 Tailwind v3 — 不要使用 v4。运行时交付使用 Tailwind v3 Play CDN — 从插件 v1.58.2 起,默认使用 assets/lib/tailwind/ 中的预构建包(Resource Source = Internal),仅当站点所有者将来源切换为外部时才使用 cdn.tailwindcss.com。无论哪种方式,都是 v3,因此预编译器 MUST 使用 v3 才能与之匹配。有两种运行方式,无需 npm run build 即可使用:

    • npx tailwindcss@^3 -i input.css -o /dev/stdout --content "<temp.html>"。不要使用 npx @tailwindcss/cli — 该包是 Tailwind v4,会生成损坏的构建(见下方警告)。
    • 使用 v3 版本的独立 Tailwind 二进制文件(例如来自 v3.4.x 标签的 tailwindcss-macos-arm64 — 单个可执行文件,无需 Node)。明确固定 v3;“latest” 二进制文件是 v4。 input.css 内容为 @tailwind base; @tailwind components; @tailwind utilities;。第 1 步中的插件通过临时 tailwind.config.js 启用(plugins: [require('@tailwindcss/forms'), ...])。由于每个布局只构建一次,Preflight/base 只编译一次,而非每个页面编译一次。

    ⚠ 使用 v3,绝不要使用 @tailwindcss/cli。 使用 Tailwind v4 时,这套完全相同的流程会悄无声息地产生一个仅适用于移动端的构建:v4 会忽略 --content 和 tailwind.config.js,而旧版 @tailwind base/components/utilities 指令不会加载 v4 的默认主题 — 因此所有需要主题令牌的工具类都会被丢弃(--breakpoint-* → 没有响应式 md:/lg: 变体,--spacing → 没有 p-2,字体大小 → 没有 text-sm);只有 flex/hidden 等不依赖主题的静态类会保留。Tailwind v3 会正确处理 @tailwind 指令、tailwind.config.js 和 --content。

  7. 验证构建,然后写入布局。 写入前,对编译后的 CSS 进行基本检查:其大小不能太小,并且至少包含一个 @media (min-width: 规则(即已生成响应式工具类)。如果不满足 — 这是使用 v4 编译的典型症状 — 则构建已损坏(仅适用于移动端);不要写入,将该布局计为失败,并保留当前可用的 Play CDN 回退(_canai_tailwind_build 为空或缺失时,服务器会继续提供 CDN)。检查通过后,使用 wpcanai-write-meta 写入:

    wpcanai-write-meta {
      "post_id": <layout_id>,
      "tailwind_build": "<full compiled CSS>",
      "tailwind_hash": "<sha256>"
    }
    

    然后,在仍带有旧版逐页编译产物的消费者页面上清除过期的逐页构建:wpcanai-write-meta { "post_id": <consumer_id>, "tailwind_build": "", "tailwind_hash": "" }。去重保护逻辑已会忽略这些字段,但清除它们能确保数据准确、面板显示正确。只有 wpcanai-write-meta 会持久化这些字段——请勿使用其他写入路径。

  8. 报告——X 个布局已编译,Y 个已跳过(已是最新),Z 个失败,并说明清除了多少个消费者页面的逐页构建。列出失败项及其布局 ID 和原因。

验证结果

在浏览器中查看前端页面。<head> 中应包含 <style id="wpcanai-tailwind-css">…</style>,且不应包含 <script src=".../tailwind.min.js">。如果 Play CDN 脚本仍然出现,可能是页面布局的 _canai_tailwind_build 为空(重新检查步骤 7 是否写入了正确的布局 ID)、页面实际使用的布局与已编译的布局不同,或者 load_tailwind 为 no。

通过已编译布局渲染的区块页面会显示相同的 <style id="wpcanai-tailwind-css">,且不包含 tailwind.min.js。如果该页面仍加载 CDN,而它的 Twig 同级页面没有,则说明构建已过期:区块页面的正文会在每次 Gutenberg 保存后更改(参见何时重新运行)。

何时重新运行

只要布局构建的任何输入发生变化,就需要重新运行:布局自身的 _canai_html(包括其内联 tailwind.config = {…} 代码块——编译后,运行时无法识别其中的新令牌,因为服务器会跳过 Play CDN)、任何消费者页面/模板的 _canai_html、区块页面的正文(所有者每次在 Gutenberg 中保存,或执行 wpcanai-write-page)、页面格式标记的更改、共享局部模板(页眉/页脚)、_canai_layout 分配,或 wpcanai_tailwind_settings 中的插件集。注意每个布局的权衡:**编辑任何一个消费者页面都会使整个布局构建过期。**步骤 5 中的哈希检查让“始终重新运行”的成本很低——只有实际发生变化的布局才会重新编译。

**精细的编辑并不要求重新编译。**服务器会为每个布局构建标记新鲜度纪元,并在渲染时检查构建相对于其来源是否过期;若已过期,则丢弃该构建并回退到 Play CDN(AssetManager::output_assets_head)。因此,执行 replace-in-meta 后页面仍能正确渲染——新的工具类(如 leading-[1.15])会由运行时 CDN 提供;唯一的代价是,在下一次主动编译之前,页面会继续使用 CDN。只有用户要求针对生产环境进行优化时才重新编译,无需在每次内容微调后都编译。

编译后的运行时行为

当渲染页面的布局具有非空的 _canai_tailwind_build 时,AssetManager::output_assets_head 会输出该构建(仅输出该构建——参见去重保护逻辑),并且输出一行垫片代码:<script id="wpcanai-tailwind-shim">window.tailwind=window.tailwind||{config:{}};</script>。该垫片会让布局中的内联 tailwind.config = { ... }(位于 {{ wp_head() }} 之后)成为无害的属性写入,而不是触发 TypeError。结果是:编译后,该代码块不再产生视觉效果——所有主题令牌都必须包含在传给编译器的临时 tailwind.config.js 中。即使布局没有 _canai_css/_canai_js,布局文章也会加入构建队列,因此仅携带构建产物的布局仍能正常输出。

限制 / 注意事项
  • 每个布局的合并规模。 布局构建会包含所有使用该布局的页面所用到的全部类,因此每个页面发送的 CSS 都比完全按页面构建的版本更多。作为交换,你可以对外壳(页眉 / 页脚 / 布局)和 Preflight 去重,并将 CSS 集中在一处,便于查找。对于典型网站,这样没有问题;如果一个布局覆盖了大量内容各异的页面,且 CSS 体积成为顾虑,请将这些页面拆分到第二个布局中。
  • 重新变旧的耦合。 由于构建按布局进行,编辑任意使用该布局的页面都会使整个布局构建失效。哈希跳过机制可以让重新编译开销很小,但大多数内容编辑后,布局都需要重新编译。
  • 从按页面构建迁移。 现有安装中的构建产物位于页面上。在首次按布局重新编译之前,去重保护机制会确保结果正确(存在布局构建时优先使用;否则仍会输出按页面构建的产物)。第 7 步的清理流程会在迁移过程中移除它们。
  • 任意值。 class="text-[#abc123]" 可以正常使用(Tailwind JIT 会扫描字面字符串);只需确保值在 Twig 源码中是字面值,而不是在渲染时构造。class="text-{{ color }}" 不会被检测到——这与任何 Tailwind 构建的限制相同。
  • 布局中的自定义 tailwind.config = {…}。 将其同步到临时的 tailwind.config.js 中,否则编译后的输出会缺少这些标记。提取过程很脆弱:内联代码块是 JS 对象字面量,而不是 JSON(可能包含尾随逗号、单引号、未加引号的键、函数值和注释)。请在 Node 中进行求值(而非 Python——参见 本地脚本粘合代码):将 module.exports = { ... } 写入临时的 tailwind.config.js,然后让编译器通过 require() 加载它,以便由编写该对象字面量时所用的同一引擎解析。若无法提取出有效配置,请明确报错,不要悄悄发布缺少标记的构建产物。

HTML 操作指南

  • 模板使用 **{# Type / Short Label #}** 导航注释标记各区块的边界;请用这些标签查找边界,并忽略 {# @dev … #} 实现备注(注释规则参见 REFERENCE.md)。
  • 移除区块: 从区块开头的 Twig 注释(或可识别的起始位置)起,删除至该区块结束处(下一个同级区块注释、结构性闭合标签或 include 行)。
  • 编辑区块: 只替换该区块内的标记;其余字符串保持不变。
  • 在 Agent 中构建最终字符串并传递给 **wpcanai-write-meta**——无需外部脚本。

历史记录 / 撤销(快照)

每次写入 CanAI 内容,都会在写入前快照保存到专用表(而非 postmeta)。这是撤销途径——写入出错时应立即使用,而不是试图手动重建之前的 _canai_html。快照没有管理界面;这些工具是唯一入口。

捕获内容。 恰好是以下八个元键:_canai_html、_canai_css、_canai_js、_canai_layout、_canai_delegate_page_id、_canai_context、_canai_context_mode、_canai_format。除此之外一概不捕获——不包括 post_title、post_content 或附件,也不包括页面的别名、状态、父级或顺序:wpcanai-update-page 不会创建快照,其响应中的 previous 就是它的撤销信息。

恢复不一致的快照会重现这种不一致。 如果快照是在页面标记为 blocks,但仍带有非空 _canai_html 时创建的(例如中断转换或其他写入方遗留的过时 Twig 正文),恢复后这两个值都会一并还原:页面仍会在区块编辑器中打开,但渲染出来的是过时的 Twig,而不是区块,因为只有 _canai_html 为空时,前端才会回退到 post_content。

捕获时机。 wpcanai-write-meta、wpcanai-write-page(仅限其转换为区块的路径,操作为 mcp-write-page-convert)、wpcanai-replace-in-meta、wpcanai-create-template、wpcanai-create-page、wpcanai-setup、wpcanai-import、预设安装/卸载、会重写内容的媒体更新,以及每次通过 wp-admin 编辑器保存时,都会自动在写入前创建快照。常规写入时无需调用“创建快照”工具;唯一需要显式执行的情况是下文介绍的 wpcanai-pin-current。

保留策略。 每篇文章最多保留 10 个未固定的快照,形成环形缓冲区——第 11 次写入会移除最旧的快照。固定的快照不会被移除,每篇文章最多可固定 10 个。

操作。 同一次逻辑写入所创建的快照会共用一个 operation_id(例如安装预设时修改了 14 篇文章)。使用 wpcanai-restore-operation 可一次性回滚整个操作,而不必逐个恢复 14 个快照。

紧急开关。 与每个 wpcanai/* 能力一样,站点所有者可在 CanAI → AI Client → Tools 中禁用这些工具;工具被禁用后,就不会出现在工具列表中。

wpcanai-list-snapshots
  • 参数: { "post_id": int, "limit"?: int } — 必须提供 post_id;limit 默认为 50,且会限制在 1–200 之间。
  • 返回: { "snapshots": [{ "id": int, "post_id": int, "operation_id": string, "operation_type": string, "actor": string, "created_at": string, "pinned": bool, "label": string|null, "bytes": int }] } — 按时间从新到旧排列,仅包含元数据。特意排除了载荷:如果每行约 7 KB,10 行就会耗尽上下文,只为回答“我想要哪一个?”。
  • 错误: 调用方无权对该文章执行 edit_post 时返回 forbidden。
wpcanai-get-snapshot
  • 参数: { "snapshot_id": int }。
  • 返回: 同样的摘要字段,另加 "payload" — 捕获的 { "_canai_html": …, "_canai_css": …, … } 映射,其中 null 表示“没有对应的元数据行”。如果需要进行差异比较,请先读取此内容,再执行恢复。
  • 错误: wpcanai_snapshot_not_found、forbidden。
wpcanai-restore-snapshot
  • 参数: { "snapshot_id": int }。
  • 返回: { "success": true, "post_id": int }。
  • 会先捕获当前状态,因此恢复操作本身也可撤销——恢复了错误的快照可以挽回,不会造成无法恢复的后果。
  • 错误: wpcanai_snapshot_not_found、forbidden、forbidden_unfiltered_html — 恢复会将原始 HTML/JS 写回文章,因此与原始写入时一样,必须具备 unfiltered_html 权限。(API 密钥调用方可通过此权限检查;使用 Cookie 认证的非管理员可能无法通过。)
wpcanai-list-operations
  • 参数: { "limit"?: int } — 默认为 20,限制在 1–100 之间。
  • 返回: { "operations": [{ "operation_id": string, "operation_type": string, "actor": string, "created_at": string, "posts": int }] } — 按时间从新到旧排列,适用于整个站点(而非单个文章)。posts 表示该操作影响的文章数量。
wpcanai-restore-operation
  • 参数: { "operation_id": string }。
  • 返回: { "success": bool, "restored": int, "failed": int, "post_ids": int[] }。
  • success 的判断依据是 failed === 0,而非 restored > 0。 部分回滚(15 篇文章中恢复了 14 篇)会返回 success: false——绝不能把非零的 restored 视为“已完成”。请向用户说明部分恢复的状态。
  • 在写入任何内容之前,会先检查每篇受影响文章的权限:回滚到一半才停止,比完全不开始更糟。
  • 错误: wpcanai_operation_not_found、forbidden(会指出你无权编辑的文章)和 forbidden_unfiltered_html。
wpcanai-pin-snapshot
  • 参数: { "snapshot_id": int, "pinned": bool, "label"?: string } — 必须提供 snapshot_id 和 pinned;label 是可选的,仅在固定快照时有意义。
  • 返回: { "success": true, "snapshot_id": int, "pinned": bool }。
  • 固定快照可避免其被环形缓冲区淘汰(每篇文章最多固定 10 个快照)。取消固定始终可用。
  • 错误: wpcanai_snapshot_not_found、forbidden、wpcanai_snapshot_pin_failed。
wpcanai-pin-current
  • 参数: { "post_id": int, "label": string } — 两项均为必填;label 不能为空,且长度不得超过 191 个字符,例如 "fix broken product page — verified in browser"。
  • 返回: { "success": true, "snapshot_id": int, "pinned": true, "label": string, "created_at": string, "reused_existing": bool }。
  • 在浏览器中验证更改后,请调用此工具。 快照只会在写入前捕获,因此你刚刚确认可正常工作的状态尚无对应记录可固定,除非有人再次编辑该文章——届时它就成了上一个状态,而且可能已被淘汰。
  • 如果当前内容与最新快照一致,则会固定该快照,而不是写入重复快照(reused_existing: true)。
  • 错误: wpcanai_invalid_label(缺失或超过 191 个字符)、wpcanai_post_not_found、wpcanai_not_canai_post(该文章不含 CanAI 内容——固定它会把“此文章没有任何内容”记录为已验证状态)、forbidden,以及已达到固定数量上限 10 时的错误;请先使用 wpcanai-pin-snapshot 取消固定一个快照。
恢复工作流
  1. wpcanai-list-snapshots { "post_id": 123 } → 找到错误写入前的记录(检查 created_at / operation_type)。
  2. 可选:wpcanai-get-snapshot { "snapshot_id": 456 } → 确认载荷是你想要的内容。
  3. wpcanai-restore-snapshot { "snapshot_id": 456 }。
  4. 在浏览器中验证页面。
  5. wpcanai-pin-current { "post_id": 123, "label": "known-good after rollback" },确保恢复后的状态不会被淘汰。

如果损坏涉及多篇文章(例如错误的预设安装或导入),请直接使用 wpcanai-list-operations → wpcanai-restore-operation。


操作路由(速查)

目标工具
列出模板wpcanai-list-templates
列出 CanAI 页面(所有页面,包括草稿:all_pages: true)wpcanai-list-pages
读取字段wpcanai-read-meta
写入字段wpcanai-write-meta
新建模板文章wpcanai-create-template
新建页面(包含 CanAI 元数据)wpcanai-create-page
重命名、修改 slug、发布或移动页面(任何格式)wpcanai-update-page
读取两个菜单(主菜单、页脚菜单)wpcanai-get-menus
设置或更改菜单链接wpcanai-write-menu(参见 菜单)
将某个位置或语言指向现有菜单,或清除其关联wpcanai-assign-menu
读取 WP / WC / CanAI 选项wpcanai-read-settings
更新 WP / WC / CanAI 选项wpcanai-update-settings
读取任意 wp_option(允许列表)wpcanai-get-option
更新任意 wp_options(允许列表 / 审批)wpcanai-update-options
轮询待处理的选项更新wpcanai-get-pending
推荐的网站初始化wpcanai-setup
查询某种类型对应的文章 IDwpcanai-resolve-content-id
WC 页面 IDwpcanai-get-wc-page-ids
诊断wpcanai-scan
搜索 _canai_* 元数据wpcanai-grep-content
诊断环境 / 网络wpcanai-diagnostics
清除页面缓存(写入操作的 purged 未涵盖时)wpcanai-purge-cache
清单 + 哈希,不含内容wpcanai-export { "summary": true } / wpcanai-read-meta { "hash_only": true }
列出 / 安装 / 移除预设wpcanai-list-presets / wpcanai-install-preset(⚠ clean_slate)/ wpcanai-uninstall-preset
导出 / 导入 CanAI 内容wpcanai-export / wpcanai-import
撤销错误写入(单篇文章)wpcanai-list-snapshots → wpcanai-restore-snapshot
撤销整个操作(多篇文章)wpcanai-list-operations → wpcanai-restore-operation
读取某个快照的载荷wpcanai-get-snapshot
检查点验证通过的内容wpcanai-pin-current(固定 / 取消固定:wpcanai-pin-snapshot)
FluentSnippets(选择启用),包括 wpcanai-replace-in-snippetcanai-yolo 技能 — 此处未介绍
为生产环境预编译 Tailwind使用 wpcanai-write-meta 并设置 tailwind_build + tailwind_hash(参见 为生产环境编译 Tailwind)
翻译网站(原生 i18n)参见 原生字符串翻译 工作流
读取 / 设置原生 i18n 语言wpcanai-i18n-get-settings / wpcanai-i18n-set-settings
列出可翻译字符串(单个页面:post_id)wpcanai-i18n-list-strings
写入翻译(批量)wpcanai-i18n-set-translations
按语言设置图片(tmedia)wpcanai-i18n-get-media-map / wpcanai-i18n-set-media-map
重建字符串索引wpcanai-i18n-rescan
文章内容覆盖(CPT / 长篇内容)wpcanai-i18n-get-post-overrides / wpcanai-i18n-set-post-overrides
术语名称 / 描述覆盖wpcanai-i18n-get-term-overrides / wpcanai-i18n-set-term-overrides
内容翻译工作队列wpcanai-i18n-list-content
读取待翻译文章的源内容wpcanai-i18n-get-content
网站名称 / 标语 / 存档页 SEO(全局字符串)wpcanai-i18n-get-global-strings / wpcanai-i18n-set-global-strings

安全

  • 将 API 密钥视同密码;不再使用时,请在 AI Client → Connections 中撤销。
  • 非本地网站优先使用 HTTPS。

评论

支持 Markdown