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):