插件需要配置——API 密钥、功能开关、显示偏好。EmDash 提供两种机制:用于管理后台可配置选项的设置 schema,以及用于编程访问的 KV 存储。
设置 Schema
在 admin.settingsSchema 中声明设置 schema,即可自动生成管理 UI:
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 在插件管理区域中生成设置表单。用户无需接触代码即可编辑设置。
字段类型
String
单行或多行字符串的文本输入。
siteTitle: {
type: "string",
label: "Site Title",
description: "Optional help text",
default: "My Site",
multiline: false // 设为 true 使用文本域
}
Number
带可选最小/最大约束的数值输入。
maxItems: {
type: "number",
label: "Maximum Items",
default: 100,
min: 1,
max: 1000
}
Boolean
布尔值的开关切换。
enabled: {
type: "boolean",
label: "Enabled",
description: "Turn this feature on or off",
default: true
}
Select
预定义选项的下拉框。
theme: {
type: "select",
label: "Theme",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "auto", label: "System" }
],
default: "auto"
}
Secret
加密字段,用于 API 密钥等敏感值。保存后不会发送到客户端。
apiKey: {
type: "secret",
label: "API Key",
description: "Stored encrypted"
}
访问设置
在 hooks 和路由中通过 ctx.kv 读取设置:
"content:beforeSave": async (event, ctx) => {
// 读取设置
const maxLength = await ctx.kv.get<number>("settings:maxTitleLength");
const apiKey = await ctx.kv.get<string>("settings:apiKey");
// 未设置时使用默认值
const limit = maxLength ?? 60;
ctx.log.info("Using max length", { limit });
return event.content;
}
设置按约定使用 settings: 前缀存储,以区分用户可配置的值与内部插件状态。
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 }>>;
}
读取值
// 获取单个值
const enabled = await ctx.kv.get<boolean>("settings:enabled");
// 带类型获取
const config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");
写入值
// 设置值
await ctx.kv.set("settings:lastSync", new Date().toISOString());
// 设置复杂值
await ctx.kv.set("state:cache", {
data: items,
expiry: Date.now() + 3600000,
});
列出值
// 列出所有设置
const settings = await ctx.kv.list("settings:");
// 返回:[{ key: "settings:enabled", value: true }, ...]
// 列出所有插件键
const all = await ctx.kv.list();
删除值
const deleted = await ctx.kv.delete("state:tempData");
// 键存在时返回 true
键命名约定
使用前缀组织 KV 数据:
| 前缀 | 用途 | 示例 |
|---|---|---|
settings: | 用户可配置的偏好 | settings:apiKey |
state: | 插件内部状态 | state:lastSync |
cache: | 缓存数据 | cache:results |
// 推荐:清晰的前缀
await ctx.kv.set("settings:webhookUrl", url);
await ctx.kv.set("state:lastRun", timestamp);
await ctx.kv.set("cache:feed", feedData);
// 避免:无前缀,用途不明
await ctx.kv.set("url", url);
设置 vs Storage vs KV
选择合适的存储机制:
| 用例 | 机制 |
|---|---|
| 管理后台可编辑的偏好 | admin.settingsSchema + ctx.kv 配合 settings: |
| 插件内部状态 | ctx.kv 配合 state: |
| 文档集合 | ctx.storage |
设置用于用户可配置的值——管理员可能会更改的内容。自动生成 UI。
KV 用于内部状态,如时间戳、同步游标或缓存计算。无 UI,仅代码。
Storage 用于带索引查询的文档集合——表单提交、审计日志等。
在路由中加载设置
API 路由可将设置暴露给管理 UI 组件:
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 中的设置不会自动持久化。它们是管理 UI 中的默认值。代码应处理缺失值:
"content:afterSave": async (event, ctx) => {
// 始终提供回退值
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) => {
// 持久化 schema 默认值
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。
这确保了插件不会意外覆盖彼此的数据。