Topics

整理 EmDash 1.0 的插件规范 —— 沙盒、能力、分布式注册表

  • column

2026 年 09 月 28 日,由 Cloudflare 打造的原生 Astro CMS「EmDash」1.0 版正式发布。随后在 10 月 2 日推出 1.1 版,10 月 7 日推出 1.2 版,在不到两周的时间内连续更新。

关于将内部博客「ゆるろぐ」从 0.22 升级到 1.2 的故事,我在这篇文章中详细记载了。这次,我想整理一下在 1.0 中变化最大的「插件机制」。

关于 EmDash 是什么样的 CMS,可以查看最初的文章。
简单来说就是「用 Astro + Cloudflare 重新构建的 WordPress」。

1.0 中插件有哪些变化

0.x 时代的插件简单来说就是「写在 Astro 配置文件中的 npm 包」,由于它们在与网站相同的进程中运行,插件拥有与网站代码相同的权限,就像 WordPress 一样可以访问数据库和环境变量。

1.0 中引入了沙箱和分布式注册表。
这是一项非常重大的改进。

  • 插件默认在隔离环境中运行,无法访问网站的内容、媒体、用户、机密或网络。
  • 必要的权限(Capability)由插件端声明,由网站管理员在安装时批准。
  • 批准的权限不仅仅是「写在纸上」,而是由运行时实际强制执行的。
  • 插件可以从管理面板一键安装。分发源是 AT Protocol 上的分布式注册表

Cloudflare 的博客中,用与 WordPress「所有插件共享同一进程」的模式进行了对比说明。
了解过 WordPress 插件相关事件的人会特别有共鸣。

插件有两种「运行方式」

首先要了解的是,同一个插件代码有Trusted(信任)和 Sandboxed(沙箱)两种执行模式。

Trusted

Sandboxed

注册位置

astro.config.mjs 中的plugins: []

sandboxed: []或从管理面板安装

执行环境

与网站相同的进程

隔离的 V8 isolate

Capability

仅声明(不强制)

运行时强制

网络

无限制

已阻止。仅 allowedHosts 中写入的主机可通过 ctx.http 访问

数据访问

数据库完全访问

仅自有存储/KV 和授权范围内

Node.js API

可使用

不可使用(仅 Web API)

资源限制

无

CPU 50ms、子请求 10、30 秒、约 128MB

适用场景

自有代码、已审查的 npm 包

第三方、注册表分发

「隔离」的含义是为每个插件准备一个独立的小型执行环境,单独运行而不与网站主体相同。
在 Cloudflare Workers 上,通过 Dynamic Workers 机制在请求时启动用于插件的 Worker。
从 1.0 版本开始,即使在 Node.js 上运行,也会在单独的进程中启动与 Workers 相同的运行时(workerd),用于承载插件,因此「沙箱不再仅限于 Cloudflare」。

关键是 插件代码在两种模式下相同,这意味着 在本地使用 Trusted 开发,在生产环境中放入沙箱 的运维方式能够正常进行。

插件结构:在构建时和运行时区分文件

EmDash 的插件分为两个部分,执行时机不同。

  1. descriptor(描述符):id、version、所需的 Capability、storage 定义等元数据,从 astro.config.mjs 导入,因此 在构建时在 Vite 中 运行。不应该有副作用。
  2. 定义(definition):hooks 或 routes 内部的代码。在请求时在服务器上执行
my-plugin/
├── src/
│   ├── index.ts          # descriptor。astro.config.mjs から import される
│   └── sandbox-entry.ts  # definePlugin({ hooks, routes })。実行時にロードされる
└── package.json          # "." と "./sandbox" を exports で分ける
// src/index.ts
import type { PluginDescriptor } from "emdash";

export function myPlugin(): PluginDescriptor {
	return {
		id: "my-plugin",
		version: "1.0.0",
		format: "standard",
		entrypoint: "@my-org/my-plugin/sandbox",
		capabilities: ["content:read", "network:request"],
		allowedHosts: ["api.example.com"],
		options: {},
	};
}
// src/sandbox-entry.ts
import { definePlugin, type PluginContext } from "emdash";

export default definePlugin({
	hooks: {
		"content:afterSave": async (event, ctx: PluginContext) => {
			ctx.log.info(`saved ${event.collection}/${event.content.id}`);
		},
	},
});

在 descriptor 中用 entrypoint 书写 definition 的模块路径,EmDash 在运行时加载它。虽然想把代码都写在同一个文件里,但因为 Vite 的构建会把运行时代码打包进去,所以分开才是正确的做法。

Standard 和 Native

作为另一个维度,插件有 Standard 和 Native 两种格式。

  • Standard:采用上述结构。管理界面 UI 用后续提及的 Block Kit(JSON)编写。可在 Trusted 和 Sandboxed 中运行,可公开到注册表
  • Native:可以打包 React 制作的管理界面、Portable Text(正文编辑器)的自定义块和网站端渲染用的 Astro 组件。代价是 仅能在 Trusted 中运行,无法发布到注册表

基本用 Standard 编写,只有在「想在编辑器中添加自定义块」或「想在网站 HTML 中插入组件」时才用 Native,这样就能合理取舍。

在 ゆるろぐ 中,我们用 Native 插件自制了 Amazon 商品卡和视频嵌入。两者都是在编辑器添加专用块后用 Astro 组件渲染,这在沙箱中原理上是做不到的(运行时安装的插件无法携带构建时所需的 Astro 组件)。

Capability:权限的词汇体系

Capability 是沙箱的核心。插件用字符串声明「想访问什么」,管理员在安装时的对话框中确认并批准。

在 1.x 中,词汇被以<对象>:<操作>的形式整理。以下是常用的几个例子。

Capability

功能介绍

content:read / content:write

读取文章 / 创建、更新、删除

content:publish

发布、取消发布、定时发布

media:read / media:write

读取媒体 / 上传和删除

network:request

向外部发送 HTTP 请求。仅限allowedHosts中列出的主机

admin.editor-draft:read / patch

读取编辑中的未保存内容 / 建议修改

hooks.content-policy:register

审查内容并阻止发布

此外还有分类法、评论、重定向、用户、邮件发送等,总共 27 种权限。

storage 和 KV 无需声明,随时可用。这些功能按插件进行作用域限制,其他插件的数据不可见。

0.x 时的名称(read:content、network:fetch等)已弃用,但仍作为别名继续支持。运行时会将其规范化为现行名称,因此现有插件可直接使用,但在打包时会生成警告。

Hooks:可以响应什么

1.2.0 版运行时中定义了 30 种 Hook。包括内容的保存、发布、预约、删除,媒体上传,邮件发送,评论投稿和批准,插件自身的安装和激活,以及通过 cron 的定期执行。相比 0.x 版本增加了不少,特别是发布前后(content:beforePublish/afterPublish)成为了不同的 Hook,这是重大变化。

在 content:beforeSave 中 throw 异常可以取消保存,所以验证相关的插件都写在这里。如果想要停止发布本身,需要声明 hooks.content-policy:register,然后使用发布前的 Hook。

1.2 版本的一个容易被忽视但很重要的变更是,升级后首次启动时,现有插件的 plugin:install 和 plugin:activate 将重新执行。对于在 install Hook 中注入初始数据的插件,需要确保即使重复注入也不会损坏。

Block Kit:不运行 JavaScript 的管理界面

在沙箱中,无法在浏览器中运行插件的 JavaScript。因此管理界面用 Block Kit(JSON 格式)来描述。插件返回「页头、表格、表单、开关」等区块的数组,由 EmDash 负责渲染。

流程是这样的。用户打开插件页面时,EmDash 向插件发送「页面已打开」的请求,插件返回区块数组。用户提交表单时,则发送带有输入值的请求,插件保存数据后返回新的区块数组。仅通过请求和响应就能实现 UI,这种设计与沙箱的限制非常兼容。

除了管理界面页面和仪表板小部件外,1.x 版本还可以用 Block Kit 创建编辑器旁的面板(editorPanels)。用于显示每篇文章的状态和操作。例如翻译状态显示、SEO 和文本检查、链接检测、正文摘要生成等,针对「当前正在编写的文章」的功能都可以放在这里。

分布式注册表:没有中央市场的设计

插件的分发机制也在 1.0 版本中发生了变化。从使用者的角度看,只能「从管理界面搜索并一键安装」,但后台是基于 Bluesky 基础设施 AT Protocol 构建的,结构略有不同。

  • 插件的信息存储在作者的账户中。作者通过 Bluesky 系账户登录,对包和发布信息进行签名,并将其放在自己账户的数据区域中。而不是存放在注册表运营者的服务器上
  • Cloudflare 的注册表只是「目录之一」。plugins.emdashcms.com只是汇集各位作者发布的信息,使其可搜索,任何人都可以收集相同的信息并创建另一个目录
  • 安装时验证内容。EmDash 会检查下载的插件是否「与作者签名和发布的内容完全一致」。文件哈希、包名和版本、请求的 Capability 是否与签名信息匹配;如果是通过 GitHub Actions 构建的,还会验证该构建记录。由目录替换或被篡改的内容将在这个阶段被拒绝
  • 审核仅「隐藏」内容。Cloudflare 的目录使用 AI 模型(Clef)审查描述、链接和图像,如有问题则在目录中隐藏。但不能修改插件内容,也不能剥夺作者的所有权

也就是说,这个模型的特点是「即使注册表运营者消失,只要作者账户中保留信息,发布就会继续」「运营者可以从目录中隐藏内容,但无法篡改」。
其设计前提是不拥有像 npm 或 WordPress.org 这样的中央市场。

目前仅支持免费插件,有偿插件将根据 AT Protocol 的进展,按照相同的分布式模型进行处理

翻译机制可以作为插件实现

ゆるろぐ 中,我们运行了一套从日语文章生成 LLM 英语版草稿的机制(用 EmDash 创建多语言博客的故事)。目前是通过 Node 脚本,从.env读取 EmDash 的 API 令牌和翻译 API 密钥,获取日语文章进行翻译,然后创建带有translationOf标签的英语草稿

将其应用到 1.x 的插件规范中,几乎可以直接成为 Standard 插件

  • 触发器:content:afterPublish钩子。日语文章发布时触发
  • 文章的读写 :content:read 和 content:write。翻译结果作为草稿生成
  • 调用翻译 API:network:request 和 allowedHosts 中添加 1 个 API 主机
  • API 密钥:settingsSchema 中的 secret 字段。从管理面板输入并加密保存。不再需要 .env 也不需要发行 API 令牌
  • 「翻译状态」:在存储中记录「翻译了哪个版本的原文章」,然后在 editorPanels 中的文章编辑屏幕上显示「有英语版本 / 原文章已更新,需要重新翻译」

原本需要手动执行的脚本,现在管理员只需要批准 Capability 就能运行。而且运行时会保证只有翻译 API 的主机可以访问,其他主机则无法连接。"API 密钥处理代码应该放在哪里"这样的困扰在结构上消失了,我认为这就是这个机制的妙处。

设计中需要注意一点。沙盒插件每次向宿主请求内容(读取设置、获取文章、写入 storage)时,都会被计为一个子请求,默认上限为 10 个。如果只翻译一篇文章,应该不会超出限制,但是对于"批量翻译未翻译的文章"这样的处理,在循环中调用 ctx.content.get() 很快就会触碰上限。看起来需要采用通过 cron 钩子逐步处理的设计方案。

另外,像语言切换这样的网站端 UI 无法从沙箱中输出,所以需要在主题端进行编写。

总结

  • EmDash 1.0 的插件采用沙箱设计。插件需声明 Capability,由管理员批准,运行时强制执行。
  • 同一份代码可以在 Trusted 和 Sandboxed 两种环境中运行。开发环境使用 Trusted,生产环境使用沙箱,这是基本原则。
  • 如果需要编辑器的自定义块或 Astro 组件,可使用 Native(仅限 Trusted)
  • descriptor(构建时)和 definition(运行时)必须在不同的文件中
  • Capability 的名称在 1.x 中改为 content:read 的形式。旧名称可以作为别名使用,但已被弃用
  • 注册表是基于 AT Protocol 的分散型。目录可以隐藏,但无法篡改
  • 在沙箱中,「一次主机调用 = 一次子请求」,上限为 10 次。需要在设计时考虑调用次数

在 WordPress 中,每安装一个插件,就意味着接受「该插件拥有网站的全部权限」。
即使只有一个存在漏洞的插件,也会使整个网站面临风险。

EmDash 1.0 对于沙箱型插件采取了新的做法——在安装时显示插件可使用的权限,运行时则只允许使用已批准的范围。
对于寻找 WordPress 替代方案的用户来说,插件周围的安全性可能是选择 EmDash 的原因之一。

目前插件数量仍然不多,还远未达到「只要去找就能找到」的 WordPress 那样的丰富程度。不过我们期待插件会继续增加。
由于我们已经在自己的网站上积累了运用实绩,所以决定将其加入我们的服务中。

本文作者

从DTP跨越到Web世界,不知不觉中已掌握标签、前端开发、创意指导、无障碍设计等各项技能的"技术高手"。从Liberogic创业初期就活跃至今,如今是公司内部的"活百科"。最近沉迷于"能否在无障碍适配上更多依赖AI?"这样的思考,正在探索借助AI提示词提高效率的方法。技术实力和思维方式都还在不断进化中

二俣 歩

IAAP 认证网络无障碍专家 (WAS) / 标记语言工程师 / 前端工程师 / 网络总监

查看本员工的文章

安心的团队体制和迅速的反应能力是我们的优势

Liberogic 拥有经验丰富的员工团队,积极推进项目,因此获得了客户的高度评价。
我们会妥善分配项目经理和总监,确保整个项目顺利进行。 通过避免不必要的全职投入导致的成本增加,并采用适当配置人力资源的方式,从把握业务内容到估价的制作和提交速度都赢得了良好的口碑。

* 本公司不积极开展SES驻场工作等业务,敬请谅解。

Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webex 等,您可以使用几乎所有主要的项目管理工具和沟通协作工具。

请咨询我们的网站相关问题。

案例分析