插件设置
插件通常都需要配置,例如 API 密钥、功能开关或显示偏好。EmDash 提供了两种机制:用于管理员可配置选项的 设置 Schema,以及供代码读写的 KV 存储。
在 admin.settingsSchema 中声明设置 Schema,即可自动生成管理界面:
import { definePlugin } from "emdash";
export default definePlugin({ id: "seo", version: "1.0.0",
admin: { settingsSchema: { siteTitle: { type: "string", label: "Site Title", description: "Used in title tags and meta", default: "", }, maxTitleLength: { type: "number", label: "Max Title Length", description: "Characters before truncation", default: 60, min: 30, max: 100, }, generateSitemap: { type: "boolean", label: "Generate Sitemap", description: "Automatically generate sitemap.xml", default: true, }, defaultRobots: { type: "select", label: "Default Robots", options: [ { value: "index,follow", label: "Index & Follow" }, { value: "noindex,follow", label: "No Index, Follow" }, { value: "noindex,nofollow", label: "No Index, No Follow" }, ], default: "index,follow", }, apiKey: { type: "secret", label: "API Key", description: "Encrypted at rest", }, }, },});EmDash 会在插件的管理界面中生成设置表单,用户无需接触代码就能修改配置。
用于单行或多行字符串的文本输入。
siteTitle: { type: "string", label: "Site Title", description: "Optional help text", default: "My Site", multiline: false // Set true for textarea}带有可选最小/最大值约束的数字输入。
maxItems: { type: "number", label: "Maximum Items", default: 100, min: 1, max: 1000}用于真/假值的切换开关。
enabled: { type: "boolean", label: "Enabled", description: "Turn this feature on or off", default: true}用于预定义选项的下拉菜单。
theme: { type: "select", label: "Theme", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, { value: "auto", label: "System" } ], default: "auto"}用于敏感值(如 API 密钥)的加密字段。保存后绝不会发送到客户端。
apiKey: { type: "secret", label: "API Key", description: "Stored encrypted"}通过 ctx.kv 在钩子和路由中读取设置:
"content:beforeSave": async (event, ctx) => { // Read a setting const maxLength = await ctx.kv.get<number>("settings:maxTitleLength"); const apiKey = await ctx.kv.get<string>("settings:apiKey");
// Use defaults if not set const limit = maxLength ?? 60;
ctx.log.info("Using max length", { limit }); return event.content;}按照惯例,设置以 settings: 前缀存储。这可以将用户可配置的值与插件内部状态区分开来。
KV 存储 API
Section titled “KV 存储 API”KV 存储 (ctx.kv) 是一个用于插件数据的通用键值存储:
interface KVAccess { get<T>(key: string): Promise<T | null>; set(key: string, value: unknown): Promise<void>; delete(key: string): Promise<boolean>; list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;}// Get a single valueconst enabled = await ctx.kv.get<boolean>("settings:enabled");
// Get with typeconst config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");// Set a valueawait ctx.kv.set("settings:lastSync", new Date().toISOString());
// Set complex valuesawait ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000,});// List all settingsconst settings = await ctx.kv.list("settings:");// Returns: [{ key: "settings:enabled", value: true }, ...]
// List all plugin keysconst all = await ctx.kv.list();const deleted = await ctx.kv.delete("state:tempData");// Returns true if key existed使用前缀来组织 KV 数据:
| 前缀 | 用途 | 示例 |
|---|---|---|
settings: | 用户可配置的偏好设置 | settings:apiKey |
state: | 插件内部状态 | state:lastSync |
cache: | 缓存数据 | cache:results |
// Good: clear prefixesawait ctx.kv.set("settings:webhookUrl", url);await ctx.kv.set("state:lastRun", timestamp);await ctx.kv.set("cache:feed", feedData);
// Avoid: no prefix, unclear purposeawait ctx.kv.set("url", url);设置 vs 存储 vs KV
Section titled “设置 vs 存储 vs KV”选择合适的存储机制:
| 使用场景 | 机制 |
|---|---|
| 管理员可编辑的偏好设置 | admin.settingsSchema + 带 settings: 的 ctx.kv |
| 插件内部状态 | 带 state: 的 ctx.kv |
| 文档集合 | ctx.storage |
设置用于用户可配置的值——管理员可能会更改的内容。它们会获得一个自动生成的界面。
KV用于内部状态,如时间戳、同步游标或缓存的计算结果。没有界面,只有代码。
存储用于带有索引查询的文档集合——表单提交、审计日志等。
在路由中加载设置
Section titled “在路由中加载设置”API 路由可以向管理界面组件暴露设置:
routes: { settings: { handler: async (ctx) => { const settings = await ctx.kv.list("settings:"); const result: Record<string, unknown> = {};
for (const entry of settings) { const key = entry.key.replace("settings:", ""); result[key] = entry.value; }
return result; } },
"settings/save": { handler: async (ctx) => { const input = ctx.input as Record<string, unknown>;
for (const [key, value] of Object.entries(input)) { if (value !== undefined) { await ctx.kv.set(`settings:${key}`, value); } }
return { success: true }; } }}来自 settingsSchema 的设置不会自动持久化。它们是管理界面中的默认值。你的代码应该处理缺失的值:
"content:afterSave": async (event, ctx) => { // Always provide a fallback const enabled = await ctx.kv.get<boolean>("settings:enabled") ?? true; const maxItems = await ctx.kv.get<number>("settings:maxItems") ?? 100;
if (!enabled) return; // ...}或者,在 plugin:install 中持久化默认值:
hooks: { "plugin:install": async (_event, ctx) => { // Persist schema defaults await ctx.kv.set("settings:enabled", true); await ctx.kv.set("settings:maxItems", 100); }}KV 值存储在 _options 表中,并带有插件命名空间的键:
INSERT INTO _options (name, value) VALUES ('plugin:seo:settings:siteTitle', '"My Site"'), ('plugin:seo:settings:maxTitleLength', '60');plugin:seo: 前缀会自动添加。你的代码使用 settings:siteTitle,而 EmDash 将其存储为 plugin:seo:settings:siteTitle。
这确保了插件不会意外覆盖彼此的数据。