本教程创建一个记录内容保存事件并暴露小型健康检查路由的沙箱插件。你将用插件 CLI 搭建包、添加一个钩子与一条路由、向 EmDash 站点注册插件,并确认两个处理程序都会运行。
若尚未选择插件格式,请先阅读 选择插件格式。
前提条件
你需要:
- Node.js 与 pnpm;
- 已配置 沙箱运行器 的 EmDash 站点;以及
- 用于清单 publisher 字段的 Atmosphere 账户 handle 或 DID。
搭建插件
-
从将包含新项目的目录运行插件脚手架。
pnpm dlx @emdash-cms/plugin-cli init save-log命令会询问 publisher、作者、安全联系人与源仓库,然后在创建以下结构前显示项目摘要:
save-log/ ├── .agents/ │ └── skills -> ../skills ├── .claude/ │ ├── CLAUDE.md -> ../AGENTS.md │ └── skills -> ../skills ├── AGENTS.md ├── emdash-plugin.jsonc ├── package.json ├── pnpm-workspace.yaml ├── README.md ├── skills/ │ └── creating-plugins/SKILL.md ├── src/ │ └── plugin.ts ├── tests/ │ └── plugin.test.ts ├── tsconfig.json ├── vitest.config.ts └── .gitignore -
安装生成包的依赖。
cd save-log pnpm install
定义插件的访问与存储
emdash-plugin.jsonc 包含插件身份、注册表信息与信任契约。添加 content:read 能力,因为 content:afterSave 会将已保存内容暴露给插件。声明 events 存储集合,以便钩子可为每次保存保留可查询记录。
以下清单包含本教程使用的字段。保留脚手架生成的 publisher、author 与 security 值。
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "save-log",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"description": "Records content-save events.",
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"events": { "indexes": ["savedAt"] },
},
}
content:read 声明告诉站点运营者钩子会接收已保存内容,且当此插件格式在进程内运行时是必需的。若集合缺失,访问 ctx.storage.events 会抛错。清单参考 说明其余字段与验证规则。
添加钩子与路由
将生成的 src/plugin.ts 替换为以下运行时定义:
import type { SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
const savedAt = new Date().toISOString();
const contentId = String(event.content.id);
await ctx.storage.events.put(`${savedAt}:${contentId}`, {
savedAt,
collection: event.collection,
contentId,
});
ctx.log.info("Content save recorded", {
collection: event.collection,
contentId,
});
},
},
},
routes: {
health: {
public: true,
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
},
};
export default plugin;
src/plugin.ts 将定义赋给 SandboxedPlugin 类型的常量并默认导出。注解在不为包添加 EmDash 运行时、也不产生包管理器特定声明路径的情况下,为钩子与路由提供参数类型。
钩子处理程序接收 (event, ctx)。路由处理程序接收 (routeCtx, ctx)。health 路由是公开且只读的,因此无需管理会话即可检查。公开路由面向互联网;在暴露真实数据或变更之前,API 路由 说明身份验证与浏览器 origin 规则。
更新生成的测试
脚手架测试通过 Worker Loader 传输主机调用原始 hello 路由。将其替换为 health 路由的测试:
import { afterEach, describe, expect, it } from "vitest";
import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";
let host: PluginTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("health route", () => {
it("identifies the running plugin", async () => {
host = await createPluginTestHost();
const result = await host.invokeRoute("health");
expect(result).toEqual({ ok: true, plugin: "save-log" });
});
});
测试构建插件并通过 Worker Loader 与 PluginBridge 调用路由。沙箱插件测试指南 涵盖钩子、内容夹具、存储断言以及本地 workerd 测试的限制。
验证与构建
运行生成的测试、验证清单并构建 npm 产物。
pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build
构建会创建:
- 包含钩子与路由代码的
dist/plugin.mjs; - 包含运行时清单与发现的钩子与路由名称的
dist/manifest.json;以及 - 站点导入的默认导出描述符
dist/index.mjs。
dist/ 是生成输出。脚手架将其排除在 Git 之外,因为插件构建会重新创建它。
注册插件
将本地包安装到 EmDash 站点。从站点目录运行此命令;若项目不是兄弟目录,请调整相对路径。
pnpm add file:../save-log
在 astro.config.mjs 中导入生成的默认导出,并将其加入 sandboxed:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";
export default defineConfig({
integrations: [
emdash({
sandboxed: [saveLog],
sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
}),
],
});
此示例使用 Node.js workerd 运行器。若站点使用 Cloudflare Workers 或其他受支持的设置,请保留站点已配置的运行器。
运行插件
启动两个开发进程:
- 在插件目录运行
pnpm dev。源或清单更改时,CLI 会重建插件。 - 在站点目录运行站点的开发命令。
在站点上打开以下路由:
http://localhost:4321/_emdash/api/plugins/save-log/health
响应包含标准 API 信封与插件返回的值:
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
在 EmDash 管理中保存一条条目。站点日志包含 Content save recorded,钩子向插件的 events 集合写入一项。