设置

本页内容

沙箱插件将其设置存储在插件专属的 KV 存储中,并将编辑界面渲染为 Block Kit 页面。原生插件可以使用的自动生成的 admin.settingsSchema 表单在沙箱中不可用——相反,你需要用 JSON 描述表单并从路由中提供它。

这比 settingsSchema 需要多做一些工作,但所有操作都通过插件已经用于钩子和路由的相同机制进行——没有需要额外学习的内容。

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 }>>;
}

KV 是插件独立的——你写入的键会存储在插件 ID 下,其他插件无法看到。

读取和写入

// 读取
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 deleted = await ctx.kv.delete("state:tempData");

// 按前缀列出
const allSettings = await ctx.kv.list("settings:");
// → [{ key: "settings:enabled", value: true }, ...]

键命名约定

使用前缀来区分不同类型的值。EmDash 插件的约定:

前缀用途示例
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);

Block Kit 设置界面

沙箱插件将其设置页面描述为 Block Kit。管理界面向插件的路由发送 page_load 交互(通常是 routes.admin),插件返回表单的 JSON 描述。当用户点击保存时,管理界面发送 block_actionform_submit 交互;插件写入 KV 并返回更新的块。

import { definePlugin } from "emdash";
import type { PluginContext } from "emdash";

interface BlockInteraction {
	type: "page_load" | "block_action" | "form_submit";
	page?: string;
	action_id?: string;
	values?: Record<string, unknown>;
}

export default definePlugin({
	routes: {
		admin: {
			handler: async (routeCtx, ctx: PluginContext) => {
				const interaction = routeCtx.input as BlockInteraction;

				if (interaction.type === "page_load" && interaction.page === "/settings") {
					return renderSettings(ctx);
				}

				if (interaction.type === "form_submit" && interaction.action_id === "save") {
					await saveSettings(ctx, interaction.values ?? {});
					return {
						...(await renderSettings(ctx)),
						toast: { message: "Settings saved", type: "success" },
					};
				}

				return { blocks: [] };
			},
		},
	},
});

async function renderSettings(ctx: PluginContext) {
	const apiKey = (await ctx.kv.get<string>("settings:apiKey")) ?? "";
	const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
	const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

	return {
		blocks: [
			{ type: "header", text: "Plugin settings" },
			{
				type: "form",
				block_id: "settings",
				fields: [
					{
						type: "secret_input",
						action_id: "apiKey",
						label: "API key",
						initial_value: apiKey,
					},
					{
						type: "toggle",
						action_id: "enabled",
						label: "Enabled",
						initial_value: enabled,
					},
					{
						type: "number_input",
						action_id: "maxItems",
						label: "Max items",
						min: 1,
						max: 1000,
						initial_value: maxItems,
					},
				],
				submit: { label: "Save", action_id: "save" },
			},
		],
	};
}

async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
	for (const [key, value] of Object.entries(values)) {
		if (value !== undefined) {
			await ctx.kv.set(`settings:${key}`, value);
		}
	}
}

要将设置页面接入管理侧边栏,在描述符中声明它:

adminPages: [{ path: "/settings", label: "Settings", icon: "settings" }],

EmDash 会自动将该路径的 page_load 交互路由到你的 admin 路由。

有关完整的块类型、表单字段、条件字段和 @emdash-cms/blocks 构建器帮助程序集,请参阅 Block Kit

密钥值

Block Kit 的 secret_input 字段渲染为掩码输入。谨慎处理用户在那里输入的任何值:

{
	type: "secret_input",
	action_id: "apiKey",
	label: "API key",
	// 不要用真实密钥为 initial_value 赋值——传递空字符串或哨兵值,
	// 仅在用户输入非空值时覆写。
	initial_value: "",
}

保存时,跳过空字符串以避免每次保存时清除现有密钥:

async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
	if (typeof values.apiKey === "string" && values.apiKey.length > 0) {
		await ctx.kv.set("settings:apiKey", values.apiKey);
	}
	// ... 其他字段
}

默认值

KV 读取对于尚未写入的键返回 null。在读取位置传递默认值:

const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;

或在安装期间持久化默认值:

hooks: {
	"plugin:install": async (_event, ctx) => {
		await ctx.kv.set("settings:enabled", true);
		await ctx.kv.set("settings:maxItems", 100);
	},
},

权衡是 plugin:install 每次安装只运行一次。如果你在后续版本中发布新设置,只有新安装才能看到默认值——现有安装需要在 plugin:activate 中进行迁移(幂等:仅在缺失时写入)或继续使用读取时回退。

设置 vs 存储 vs KV

使用场景机制
管理员可编辑偏好ctx.kv 配合 settings: 前缀 + Block Kit 页面
内部插件状态ctx.kv 配合 state: 前缀
文档集合(查询)ctx.storage

KV 用于按字符串键存储的小值——设置、同步游标、缓存计算。无查询,无索引。

Storage 用于带索引查询的文档集合——表单提交、审计日志,任何需要过滤、分页或计数的内容。

存储布局

KV 值存储在 _options 表中,使用插件命名空间键。你的代码使用 settings:apiKey;EmDash 将其存储为 plugin:<your-plugin-id>:settings:apiKey。前缀会自动添加,防止一个插件读取或覆写另一个插件的 KV 数据。

原生插件:settingsSchema

如果你正在编写原生插件(因为需要 React 管理页面或 PT 组件),可以直接在 definePlugin() 中声明设置模式,让 EmDash 自动生成表单。有关该路径,请参阅原生插件