Merge pull request #3850 from zouyonghe/feature/plugin-upgrade-all
增加升级所有插件按钮
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
@@ -19,6 +20,10 @@ 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__(
|
||||
@@ -33,6 +38,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),
|
||||
@@ -63,7 +69,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)
|
||||
@@ -346,7 +352,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)
|
||||
@@ -393,7 +399,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)
|
||||
@@ -418,7 +424,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:
|
||||
@@ -432,6 +438,59 @@ 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.get_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(PLUGIN_UPDATE_CONCURRENCY)
|
||||
|
||||
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)}
|
||||
|
||||
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 = (
|
||||
"批量更新完成,全部成功。"
|
||||
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 (
|
||||
@@ -440,7 +499,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)
|
||||
@@ -458,7 +517,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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "从文件安装",
|
||||
|
||||
@@ -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,56 @@ 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 targets = updatableExtensions.value.map(ext => ext.name);
|
||||
try {
|
||||
const res = await axios.post('/api/plugin/update-all', {
|
||||
names: targets,
|
||||
proxy: localStorage.getItem('selectedGitHubProxy') || ""
|
||||
});
|
||||
|
||||
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 {
|
||||
await getExtensions();
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.message || err.message || String(err);
|
||||
failures.push({ name: 'refresh', status: 'error', message: errorMsg });
|
||||
}
|
||||
|
||||
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);
|
||||
onLoadingDialogResult(2, errorMsg, -1);
|
||||
} finally {
|
||||
updatingAll.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const pluginOn = async (extension) => {
|
||||
try {
|
||||
const res = await axios.post('/api/plugin/on', { name: extension.name });
|
||||
@@ -720,6 +775,12 @@ watch(marketSearch, (newVal) => {
|
||||
{{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="ml-2" color="warning" variant="tonal" :disabled="updatableExtensions.length === 0"
|
||||
:loading="updatingAll" @click="updateAllExtensions">
|
||||
<v-icon>mdi-update</v-icon>
|
||||
{{ tm('buttons.updateAll') }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="ml-2" color="primary" variant="tonal" @click="dialog = true">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
{{ tm('buttons.install') }}
|
||||
|
||||
Reference in New Issue
Block a user