外掛設定

本頁內容

外掛需要設定——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"
}

存取設定

透過 ctx.kv 在 hooks 和路由中讀取設定:

"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 儲存 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

這確保外掛不會意外覆寫彼此的資料。