From 80d8161d58b18fcac2c7f3a863c906541d4826eb Mon Sep 17 00:00:00 2001 From: ZouYonghe <1259085392z@gmail.com> Date: Sun, 30 Nov 2025 10:40:46 +0800 Subject: [PATCH 1/5] feat: add update all plugins action --- .../locales/en-US/features/extension.json | 7 ++- .../locales/zh-CN/features/extension.json | 7 ++- dashboard/src/views/ExtensionPage.vue | 57 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json index 911523e22..b1ec35191 100644 --- a/dashboard/src/i18n/locales/en-US/features/extension.json +++ b/dashboard/src/i18n/locales/en-US/features/extension.json @@ -32,7 +32,8 @@ "actions": "Actions", "back": "Back", "selectFile": "Select File", - "refresh": "Refresh" + "refresh": "Refresh", + "updateAll": "Update All" }, "status": { "enabled": "Enabled", @@ -141,7 +142,9 @@ "confirmDelete": "Are you sure you want to delete this extension?", "fillUrlOrFile": "Please fill in extension URL or upload extension file", "dontFillBoth": "Please don't fill in both extension URL and upload file", - "supportedFormats": "Supports .zip extension files" + "supportedFormats": "Supports .zip extension files", + "updateAllSuccess": "All upgradable extensions have been updated!", + "updateAllFailed": "{failed} of {total} extensions failed to update:" }, "upload": { "fromFile": "Install from File", diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json index e52494d7c..cf7273042 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/extension.json +++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json @@ -32,7 +32,8 @@ "actions": "操作", "back": "返回", "selectFile": "选择文件", - "refresh": "刷新" + "refresh": "刷新", + "updateAll": "更新全部" }, "status": { "enabled": "启用", @@ -141,7 +142,9 @@ "confirmDelete": "确定要删除插件吗?", "fillUrlOrFile": "请填写插件链接或上传插件文件", "dontFillBoth": "请不要同时填写插件链接和上传文件", - "supportedFormats": "支持 .zip 格式的插件文件" + "supportedFormats": "支持 .zip 格式的插件文件", + "updateAllSuccess": "所有可更新的插件都已更新!", + "updateAllFailed": "有 {failed}/{total} 个插件更新失败:" }, "upload": { "fromFile": "从文件安装", diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index 42f981a44..a51200e60 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -42,6 +42,7 @@ const loadingDialog = reactive({ const showPluginInfoDialog = ref(false); const selectedPlugin = ref({}); const curr_namespace = ref(""); +const updatingAll = ref(false); const readmeDialog = reactive({ show: false, @@ -226,6 +227,10 @@ const paginatedPlugins = computed(() => { return sortedPlugins.value.slice(start, end); }); +const updatableExtensions = computed(() => { + return extension_data?.data?.filter(ext => ext.has_update) || []; +}); + // 方法 const toggleShowReserved = () => { showReserved.value = !showReserved.value; @@ -372,6 +377,52 @@ const updateExtension = async (extension_name) => { } }; +const updateAllExtensions = async () => { + if (updatingAll.value || updatableExtensions.value.length === 0) return; + updatingAll.value = true; + loadingDialog.title = tm('status.loading'); + loadingDialog.statusCode = 0; + loadingDialog.result = ""; + loadingDialog.show = true; + + const failures = []; + const targets = [...updatableExtensions.value]; + + for (const ext of targets) { + try { + const res = await axios.post('/api/plugin/update', { + name: ext.name, + proxy: localStorage.getItem('selectedGitHubProxy') || "" + }); + if (res.data.status === "error") { + failures.push(`${ext.name}: ${res.data.message}`); + } + } catch (err) { + const errorMsg = err.response?.data?.message || err.message || String(err); + failures.push(`${ext.name}: ${errorMsg}`); + } + } + + try { + await getExtensions(); + } catch (err) { + const errorMsg = err.response?.data?.message || err.message || String(err); + failures.push(tm('messages.refreshFailed') + " " + errorMsg); + } + + if (failures.length === 0) { + onLoadingDialogResult(1, tm('messages.updateAllSuccess')); + } else { + const failureText = tm('messages.updateAllFailed', { + failed: failures.length, + total: targets.length + }); + onLoadingDialogResult(2, `${failureText}\n${failures.join('\n')}`, -1); + } + + updatingAll.value = false; +}; + const pluginOn = async (extension) => { try { const res = await axios.post('/api/plugin/on', { name: extension.name }); @@ -720,6 +771,12 @@ watch(marketSearch, (newVal) => { {{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }} + + mdi-update + {{ tm('buttons.updateAll') }} + + mdi-plus {{ tm('buttons.install') }} From 1dd1623e7d2fe0aa544851998246854f7898ab2b Mon Sep 17 00:00:00 2001 From: ZouYonghe <1259085392z@gmail.com> Date: Sun, 30 Nov 2025 11:11:36 +0800 Subject: [PATCH 2/5] feat: batch update plugins via new api --- astrbot/dashboard/routes/plugin.py | 43 ++++++++++++++++++++ dashboard/src/views/ExtensionPage.vue | 58 ++++++++++++++------------- 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 597a245d4..e479b8b4f 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -1,3 +1,4 @@ +import asyncio import json import os import ssl @@ -33,6 +34,7 @@ class PluginRoute(Route): "/plugin/install": ("POST", self.install_plugin), "/plugin/install-upload": ("POST", self.install_plugin_upload), "/plugin/update": ("POST", self.update_plugin), + "/plugin/update-all": ("POST", self.update_all_plugins), "/plugin/uninstall": ("POST", self.uninstall_plugin), "/plugin/market_list": ("GET", self.get_online_plugins), "/plugin/off": ("POST", self.off_plugin), @@ -432,6 +434,47 @@ class PluginRoute(Route): logger.error(f"/api/plugin/update: {traceback.format_exc()}") return Response().error(str(e)).__dict__ + async def update_all_plugins(self): + if DEMO_MODE: + return ( + Response() + .error("You are not permitted to do this operation in demo mode") + .__dict__ + ) + + post_data = await request.json + plugin_names: list[str] = post_data.get("names") or [] + proxy: str = post_data.get("proxy", "") + + if not isinstance(plugin_names, list) or not plugin_names: + return Response().error("插件列表不能为空").__dict__ + + results = [] + sem = asyncio.Semaphore(3) # 控制并发数量 + + async def _update_one(name: str): + async with sem: + try: + logger.info(f"批量更新插件 {name}") + await self.plugin_manager.update_plugin(name, proxy) + return {"name": name, "status": "ok", "message": "更新成功"} + except Exception as e: + logger.error( + f"/api/plugin/update-all: 更新插件 {name} 失败: {traceback.format_exc()}", + ) + return {"name": name, "status": "error", "message": str(e)} + + results = await asyncio.gather(*[_update_one(name) for name in plugin_names]) + + failed = [r for r in results if r["status"] == "error"] + message = ( + "批量更新完成,全部成功。" + if not failed + else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。" + ) + + return Response().ok({"results": results}, message).__dict__ + async def off_plugin(self): if DEMO_MODE: return ( diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index a51200e60..a9dec0679 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -385,42 +385,46 @@ const updateAllExtensions = async () => { loadingDialog.result = ""; loadingDialog.show = true; - const failures = []; - const targets = [...updatableExtensions.value]; + const targets = updatableExtensions.value.map(ext => ext.name); + try { + const res = await axios.post('/api/plugin/update-all', { + names: targets, + proxy: localStorage.getItem('selectedGitHubProxy') || "" + }); - for (const ext of targets) { + if (res.data.status === "error") { + onLoadingDialogResult(2, res.data.message || tm('messages.updateAllFailed', { + failed: targets.length, + total: targets.length + }), -1); + return; + } + + const results = res.data.data?.results || []; + const failures = results.filter(r => r.status !== 'ok'); try { - const res = await axios.post('/api/plugin/update', { - name: ext.name, - proxy: localStorage.getItem('selectedGitHubProxy') || "" - }); - if (res.data.status === "error") { - failures.push(`${ext.name}: ${res.data.message}`); - } + await getExtensions(); } catch (err) { const errorMsg = err.response?.data?.message || err.message || String(err); - failures.push(`${ext.name}: ${errorMsg}`); + failures.push({ name: 'refresh', status: 'error', message: errorMsg }); } - } - try { - await getExtensions(); + if (failures.length === 0) { + onLoadingDialogResult(1, tm('messages.updateAllSuccess')); + } else { + const failureText = tm('messages.updateAllFailed', { + failed: failures.length, + total: targets.length + }); + const detail = failures.map(f => `${f.name}: ${f.message}`).join('\n'); + onLoadingDialogResult(2, `${failureText}\n${detail}`, -1); + } } catch (err) { const errorMsg = err.response?.data?.message || err.message || String(err); - failures.push(tm('messages.refreshFailed') + " " + errorMsg); + onLoadingDialogResult(2, errorMsg, -1); + } finally { + updatingAll.value = false; } - - if (failures.length === 0) { - onLoadingDialogResult(1, tm('messages.updateAllSuccess')); - } else { - const failureText = tm('messages.updateAllFailed', { - failed: failures.length, - total: targets.length - }); - onLoadingDialogResult(2, `${failureText}\n${failures.join('\n')}`, -1); - } - - updatingAll.value = false; }; const pluginOn = async (extension) => { From e7e8664ab4ce97263977c6360449b66f112f3e6e Mon Sep 17 00:00:00 2001 From: ZouYonghe <1259085392z@gmail.com> Date: Sun, 30 Nov 2025 11:18:30 +0800 Subject: [PATCH 3/5] chore: tweak update all label --- dashboard/src/i18n/locales/zh-CN/features/extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json index cf7273042..4b7a8058f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/extension.json +++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json @@ -33,7 +33,7 @@ "back": "返回", "selectFile": "选择文件", "refresh": "刷新", - "updateAll": "更新全部" + "updateAll": "更新全部插件" }, "status": { "enabled": "启用", From 4c03e82570d1951b38760c455ab23283804094ae Mon Sep 17 00:00:00 2001 From: ZouYonghe <1259085392z@gmail.com> Date: Sun, 30 Nov 2025 11:50:46 +0800 Subject: [PATCH 4/5] Fix plugin update JSON parsing and concurrency handling --- astrbot/dashboard/routes/plugin.py | 32 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index e479b8b4f..94197399e 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -20,6 +20,8 @@ from astrbot.core.star.star_manager import PluginManager from .route import Response, Route, RouteContext +PLUGIN_UPDATE_CONCURRENCY = 3 # limit concurrent updates to avoid overwhelming plugin sources + class PluginRoute(Route): def __init__( @@ -65,7 +67,7 @@ class PluginRoute(Route): .__dict__ ) - data = await request.json + data = await request.get_json() plugin_name = data.get("name", None) try: success, message = await self.plugin_manager.reload(plugin_name) @@ -348,7 +350,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() repo_url = post_data["url"] proxy: str = post_data.get("proxy", None) @@ -395,7 +397,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() plugin_name = post_data["name"] delete_config = post_data.get("delete_config", False) delete_data = post_data.get("delete_data", False) @@ -420,7 +422,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() plugin_name = post_data["name"] proxy: str = post_data.get("proxy", None) try: @@ -442,7 +444,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() plugin_names: list[str] = post_data.get("names") or [] proxy: str = post_data.get("proxy", "") @@ -450,7 +452,7 @@ class PluginRoute(Route): return Response().error("插件列表不能为空").__dict__ results = [] - sem = asyncio.Semaphore(3) # 控制并发数量 + sem = asyncio.Semaphore(PLUGIN_UPDATE_CONCURRENCY) async def _update_one(name: str): async with sem: @@ -464,7 +466,19 @@ class PluginRoute(Route): ) return {"name": name, "status": "error", "message": str(e)} - results = await asyncio.gather(*[_update_one(name) for name in plugin_names]) + raw_results = await asyncio.gather( + *(_update_one(name) for name in plugin_names), + return_exceptions=True, + ) + for name, result in zip(plugin_names, raw_results): + if isinstance(result, asyncio.CancelledError): + raise result + if isinstance(result, BaseException): + results.append( + {"name": name, "status": "error", "message": str(result)} + ) + else: + results.append(result) failed = [r for r in results if r["status"] == "error"] message = ( @@ -483,7 +497,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() plugin_name = post_data["name"] try: await self.plugin_manager.turn_off_plugin(plugin_name) @@ -501,7 +515,7 @@ class PluginRoute(Route): .__dict__ ) - post_data = await request.json + post_data = await request.get_json() plugin_name = post_data["name"] try: await self.plugin_manager.turn_on_plugin(plugin_name) From 8d3ff61e0d5fd69fd5f19e8c99ca7701e4bd0c36 Mon Sep 17 00:00:00 2001 From: ZouYonghe <1259085392z@gmail.com> Date: Sun, 30 Nov 2025 11:56:24 +0800 Subject: [PATCH 5/5] Format plugin route with ruff --- astrbot/dashboard/routes/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 94197399e..f2a35dfe1 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -20,7 +20,9 @@ from astrbot.core.star.star_manager import PluginManager from .route import Response, Route, RouteContext -PLUGIN_UPDATE_CONCURRENCY = 3 # limit concurrent updates to avoid overwhelming plugin sources +PLUGIN_UPDATE_CONCURRENCY = ( + 3 # limit concurrent updates to avoid overwhelming plugin sources +) class PluginRoute(Route):