From 83336a850de330e38443dc3a0f2314b2d57c9405 Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Wed, 2 Sep 2026 13:56:21 +0800 Subject: [PATCH 1/2] fix: heal stale plugin sidebar entries when plugin page errors out The plugin sidebar state is a module-level singleton refreshed only on first mount or extension page operations. If a plugin is uninstalled or disabled outside the current session (another tab, CLI, or API), the sidebar keeps rendering its WebUI entry until the extension page is visited. When PluginPagePage discovers the target plugin is gone or disabled, drop the stale entry from the shared sidebar state (or mark it inactive), so the sidebar item disappears immediately via its existing watch. --- dashboard/src/views/PluginPagePage.vue | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dashboard/src/views/PluginPagePage.vue b/dashboard/src/views/PluginPagePage.vue index 2621d7acf2..a67fd87981 100644 --- a/dashboard/src/views/PluginPagePage.vue +++ b/dashboard/src/views/PluginPagePage.vue @@ -7,6 +7,7 @@ import { fetchWithAuth } from "@/api/http"; import { useModuleI18n } from "@/i18n/composables"; import { usePluginI18n } from "@/utils/pluginI18n"; import { useCustomizerStore } from "@/stores/customizer"; +import { pluginSidebarState } from "@/composables/usePluginSidebarItems"; const BRIDGE_CHANNEL = "astrbot-plugin-page"; @@ -571,11 +572,19 @@ const loadPluginPage = async () => { const pluginData = detailResponse.data?.data || null; if (!pluginData) { errorMessage.value = tm("messages.pluginNotFound"); + // 修正侧边栏共享状态中已卸载插件的陈旧入口 + pluginSidebarState.plugins = pluginSidebarState.plugins.filter( + (p) => p.name !== pluginName.value, + ); return; } if (!pluginData.activated) { errorMessage.value = tm("messages.pluginDisabled"); + // 修正侧边栏共享状态中已禁用插件的陈旧入口 + pluginSidebarState.plugins = pluginSidebarState.plugins.map((p) => + p.name === pluginName.value ? { ...p, activated: false } : p, + ); return; } From bc2e5567a287d64b04147e82909d7176be4f838e Mon Sep 17 00:00:00 2001 From: lxfight <1686540385@qq.com> Date: Wed, 2 Sep 2026 14:01:25 +0800 Subject: [PATCH 2/2] fix: serialize plugin page loads with a sequence guard loadPluginPage read pluginName.value after awaits, so a stale response from a previous route could heal the wrong sidebar entry (navigating from plugin A to B while A's request was pending removed or deactivated B's sidebar item). Capture the requested plugin name up front and bail out when the route changed before each state mutation. This also stops stale responses from overwriting view state and from clearing the loading indicator of the newer request. --- dashboard/src/views/PluginPagePage.vue | 28 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dashboard/src/views/PluginPagePage.vue b/dashboard/src/views/PluginPagePage.vue index a67fd87981..c9b275403b 100644 --- a/dashboard/src/views/PluginPagePage.vue +++ b/dashboard/src/views/PluginPagePage.vue @@ -552,7 +552,13 @@ const handleIframeLoad = () => { sendIframeContext(); }; +let loadSequence = 0; + const loadPluginPage = async () => { + // Sequence guard: a stale response from a previous route must never + // mutate view state or the shared plugin sidebar state. + const requestSequence = ++loadSequence; + const requestedPluginName = pluginName.value; loading.value = true; errorMessage.value = ""; plugin.value = null; @@ -562,7 +568,8 @@ const loadPluginPage = async () => { cleanupSSEConnections(); try { - const detailResponse = await pluginApi.get(pluginName.value); + const detailResponse = await pluginApi.get(requestedPluginName); + if (requestSequence !== loadSequence) return; if (detailResponse.data?.status === "error") { throw new Error( detailResponse.data.message || tm("messages.pluginPageLoadFailed"), @@ -572,23 +579,27 @@ const loadPluginPage = async () => { const pluginData = detailResponse.data?.data || null; if (!pluginData) { errorMessage.value = tm("messages.pluginNotFound"); - // 修正侧边栏共享状态中已卸载插件的陈旧入口 + // Heal the stale plugin entry in the shared sidebar state. pluginSidebarState.plugins = pluginSidebarState.plugins.filter( - (p) => p.name !== pluginName.value, + (p) => p.name !== requestedPluginName, ); return; } if (!pluginData.activated) { errorMessage.value = tm("messages.pluginDisabled"); - // 修正侧边栏共享状态中已禁用插件的陈旧入口 + // Heal the stale plugin entry in the shared sidebar state. pluginSidebarState.plugins = pluginSidebarState.plugins.map((p) => - p.name === pluginName.value ? { ...p, activated: false } : p, + p.name === requestedPluginName ? { ...p, activated: false } : p, ); return; } - const entryResponse = await pluginApi.page(pluginName.value, pageName.value); + const entryResponse = await pluginApi.page( + requestedPluginName, + pageName.value, + ); + if (requestSequence !== loadSequence) return; if (entryResponse.data?.status === "error") { throw new Error( entryResponse.data.message || tm("messages.pluginPageLoadFailed"), @@ -611,12 +622,15 @@ const loadPluginPage = async () => { contentUrl.searchParams.set('theme', themeParam.value); iframeSrc.value = contentUrl.pathname + contentUrl.search + contentUrl.hash; } catch (error) { + if (requestSequence !== loadSequence) return; errorMessage.value = error?.response?.data?.message || error?.message || tm("messages.pluginPageLoadFailed"); } finally { - loading.value = false; + if (requestSequence === loadSequence) { + loading.value = false; + } } };