From 72d655a109d5c4060ffa5e76a84bdb932a9098ee Mon Sep 17 00:00:00 2001 From: tianma-if Date: Tue, 8 Sep 2026 19:29:50 +0800 Subject: [PATCH] feat(plugins): notify extensions of settings changes --- .../plugins/plugin-host-events.fixture.mjs | 5 ++++ apps/web/src/lib/plugins/plugin-host.test.mjs | 23 +++++++++++++++++++ apps/web/src/lib/plugins/plugin-host.ts | 18 ++++++++++----- docs/plugin-development.md | 10 ++++++++ docs/plugin-development.zh-CN.md | 10 ++++++++ packages/plugin-api/dist/index.d.ts | 3 +++ packages/plugin-api/src/index.ts | 1 + 7 files changed, 64 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/plugins/plugin-host-events.fixture.mjs b/apps/web/src/lib/plugins/plugin-host-events.fixture.mjs index 045f49c3..c31104c5 100644 --- a/apps/web/src/lib/plugins/plugin-host-events.fixture.mjs +++ b/apps/web/src/lib/plugins/plugin-host-events.fixture.mjs @@ -1,5 +1,9 @@ export default { activate(context) { + const disposeSettings = context.events.on("settings.changed", ({ key }) => { + globalThis.edgeeverPluginObservedSettings ??= []; + globalThis.edgeeverPluginObservedSettings.push({ pluginId: context.pluginId, key }); + }); const disposeNote = context.events.on("note.updated", ({ note }) => { globalThis.edgeeverPluginObservedNote = note; }); @@ -10,6 +14,7 @@ export default { globalThis.edgeeverPluginObservedResource = resource; }); return () => { + disposeSettings(); disposeNote(); disposeTemplate(); disposeResource(); diff --git a/apps/web/src/lib/plugins/plugin-host.test.mjs b/apps/web/src/lib/plugins/plugin-host.test.mjs index 47983e95..5dc50efa 100644 --- a/apps/web/src/lib/plugins/plugin-host.test.mjs +++ b/apps/web/src/lib/plugins/plugin-host.test.mjs @@ -632,10 +632,28 @@ describe("EdgeEverPluginHost", () => { apiVersion: "1", entry: new URL("./plugin-host-events.fixture.mjs", import.meta.url).href, permissions: ["notes:read", "templates:read", "resources:read"], + settings: { fields: [{ key: "mode", type: "select", label: "Mode", default: "daily", options: [ + { value: "daily", label: "Daily" }, + { value: "weekly", label: "Weekly" }, + ] }] }, }, "https://plugins.example/events/manifest.json"); await host.setEnabled("org.edgeever.events", true); await host.activateEnabled(); + host.installManifest({ + type: "plugin", + id: "org.edgeever.events-other", + name: "Other Events", + version: "1.0.0", + apiVersion: "1", + entry: new URL("./plugin-host-events.fixture.mjs", import.meta.url).href, + permissions: [], + settings: { fields: [{ key: "mode", type: "text", label: "Mode" }] }, + }, "https://plugins.example/events-other/manifest.json"); + await host.setEnabled("org.edgeever.events-other", true); + await host.setSettingValue("org.edgeever.events", "mode", "weekly"); + await host.removeSettingValue("org.edgeever.events", "mode"); + await repositoryWithEvents.updateMemo(updatedMemo, {}); await repositoryWithEvents.createTemplate({ name: "Event template" }); await repositoryWithEvents.updateResource("resource-events", new File(["scene"], "drawing.excalidraw"), "old-hash"); @@ -652,9 +670,14 @@ describe("EdgeEverPluginHost", () => { id: "resource-events", contentHash: "resource-event-hash", }); + expect(globalThis.edgeeverPluginObservedSettings).toEqual([ + { pluginId: "org.edgeever.events", key: "mode" }, + { pluginId: "org.edgeever.events", key: "mode" }, + ]); delete globalThis.edgeeverPluginObservedNote; delete globalThis.edgeeverPluginObservedTemplate; delete globalThis.edgeeverPluginObservedResource; + delete globalThis.edgeeverPluginObservedSettings; await host.dispose(); }); }); diff --git a/apps/web/src/lib/plugins/plugin-host.ts b/apps/web/src/lib/plugins/plugin-host.ts index 4a470c00..8536734d 100644 --- a/apps/web/src/lib/plugins/plugin-host.ts +++ b/apps/web/src/lib/plugins/plugin-host.ts @@ -421,7 +421,7 @@ export class EdgeEverPluginHost { private readonly mountedPanels = new Map void>>(); private readonly embeds = new Map(); private readonly mountedEmbeds = new Map void>>(); - private readonly eventListeners = new Map void>>(); + private readonly eventListeners = new Map void }>>(); private extensions = readInstalledExtensions(); private activeThemeId = readStorageItem(ACTIVE_THEME_STORAGE_KEY); private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], recentActions: [], activeThemeId: null }; @@ -690,9 +690,11 @@ export class EdgeEverPluginHost { const normalized = validateSettingValue(field, value); if (field.type === "secret") { await this.secretStorage.set(`${this.scope}:${extensionId}`, `setting:${key}`, String(normalized)); + this.emit("settings.changed", { key }, extensionId); return; } writeStorageItem(`${SETTINGS_STORAGE_PREFIX}:${this.scope}:${extensionId}:${key}`, JSON.stringify(normalized)); + this.emit("settings.changed", { key }, extensionId); } async removeSettingValue(extensionId: string, key: string) { @@ -701,9 +703,11 @@ export class EdgeEverPluginHost { const field = requireSettingField(extension.manifest, key); if (field.type === "secret") { await this.secretStorage.remove(`${this.scope}:${extensionId}`, `setting:${key}`); + this.emit("settings.changed", { key }, extensionId); return; } removeStorageItem(`${SETTINGS_STORAGE_PREFIX}:${this.scope}:${extensionId}:${key}`); + this.emit("settings.changed", { key }, extensionId); } async runCommand(pluginId: string, commandId: string) { @@ -1205,9 +1209,10 @@ export class EdgeEverPluginHost { const permission = EVENT_PERMISSIONS[event]; if (permission) assertPermission(manifest, permission); const listeners = this.eventListeners.get(event) ?? new Set(); - listeners.add(listener as (payload: never) => void); + const entry = { pluginId: manifest.id, listener: listener as (payload: never) => void }; + listeners.add(entry); this.eventListeners.set(event, listeners); - const dispose = () => listeners.delete(listener as (payload: never) => void); + const dispose = () => listeners.delete(entry); disposers.push(dispose); return dispose; }, @@ -1456,10 +1461,11 @@ export class EdgeEverPluginHost { } } - private emit(event: K, payload: PluginEventMap[K]) { - for (const listener of this.eventListeners.get(event) ?? []) { + private emit(event: K, payload: PluginEventMap[K], targetPluginId?: string) { + for (const entry of this.eventListeners.get(event) ?? []) { + if (targetPluginId && entry.pluginId !== targetPluginId) continue; try { - const result = (listener as (value: never) => unknown)(payload as never); + const result = (entry.listener as (value: never) => unknown)(payload as never); if (result && typeof (result as PromiseLike).then === "function") { void Promise.resolve(result).catch((error) => console.error(`Plugin event listener failed for ${event}`, error)); } diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 8b33ad55..ec5108f3 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -307,6 +307,16 @@ await context.settings.set("format", "html"); await context.settings.remove("token"); ``` +Plugins can listen for changes to their own settings and then read the host-validated value again. The event is delivered only to the plugin that owns the setting and does not include the value, keeping secrets and other configuration out of event payloads: + +```ts +context.events.on("settings.changed", async ({ key }) => { + if (key !== "format") return; + const format = await context.settings.get("format"); + // Apply the updated format. +}); +``` + ## Storage and network Plugin storage is namespaced by EdgeEver workspace and plugin ID: diff --git a/docs/plugin-development.zh-CN.md b/docs/plugin-development.zh-CN.md index 5b252e45..d96d3453 100644 --- a/docs/plugin-development.zh-CN.md +++ b/docs/plugin-development.zh-CN.md @@ -307,6 +307,16 @@ await context.settings.set("format", "html"); await context.settings.remove("token"); ``` +插件可以监听自己设置的变化,并重新读取宿主已经校验过的值。事件只会发送给拥有该设置的插件,且不会携带设置值,避免 secret 或其他配置进入事件载荷: + +```ts +context.events.on("settings.changed", async ({ key }) => { + if (key !== "format") return; + const format = await context.settings.get("format"); + // 应用更新后的格式。 +}); +``` + ## 插件存储与网络 插件存储按照 EdgeEver 工作区和插件 ID 隔离: diff --git a/packages/plugin-api/dist/index.d.ts b/packages/plugin-api/dist/index.d.ts index 8983885e..ebd0f739 100644 --- a/packages/plugin-api/dist/index.d.ts +++ b/packages/plugin-api/dist/index.d.ts @@ -210,6 +210,9 @@ export interface PluginTemplate { updatedAt: string; } export type PluginEventMap = { + "settings.changed": { + key: string; + }; "note.created": { note: PluginNote; }; diff --git a/packages/plugin-api/src/index.ts b/packages/plugin-api/src/index.ts index 606c232e..37386eee 100644 --- a/packages/plugin-api/src/index.ts +++ b/packages/plugin-api/src/index.ts @@ -260,6 +260,7 @@ export interface PluginTemplate { } export type PluginEventMap = { + "settings.changed": { key: string }; "note.created": { note: PluginNote }; "note.updated": { note: PluginNote }; "note.deleted": { noteId: string };