設定

本頁內容

沙箱外掛程式將其設定儲存在外掛程式專屬的 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 自動產生表單。有關該路徑,請參閱原生外掛程式