feat: 完成异步任务跟踪架构基础

This commit is contained in:
Junyan Qin
2024-11-01 22:41:26 +08:00
parent 2f05f5b456
commit 6d2a4c038d
16 changed files with 395 additions and 101 deletions

View File

@@ -5,7 +5,7 @@ import traceback
import quart import quart
from .....core import app from .....core import app, taskmgr
from .. import group from .. import group
@@ -23,12 +23,26 @@ class PluginsRouterGroup(group.RouterGroup):
'plugins': plugins_data 'plugins': plugins_data
}) })
@self.route('/toggle/<author>/<plugin_name>', methods=['PUT']) @self.route('/<author>/<plugin_name>/toggle', methods=['PUT'])
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
data = await quart.request.json data = await quart.request.json
target_enabled = data.get('target_enabled') target_enabled = data.get('target_enabled')
await self.ap.plugin_mgr.update_plugin_status(plugin_name, target_enabled) await self.ap.plugin_mgr.update_plugin_status(plugin_name, target_enabled)
return self.success() return self.success()
@self.route('/<author>/<plugin_name>/update', methods=['POST'])
async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx),
kind="plugin-operation",
name=f"plugin-update-{plugin_name}",
label=f"更新插件 {plugin_name}",
context=ctx
)
return self.success(data={
'task_id': wrapper.id
})
@self.route('/reorder', methods=['PUT']) @self.route('/reorder', methods=['PUT'])
async def _() -> str: async def _() -> str:

View File

@@ -1,6 +1,7 @@
import quart import quart
import asyncio
from .....core import app from .....core import app, taskmgr
from .. import group from .. import group
from .....utils import constants from .....utils import constants
@@ -17,3 +18,23 @@ class SystemRouterGroup(group.RouterGroup):
"debug": constants.debug_mode "debug": constants.debug_mode
} }
) )
@self.route('/tasks', methods=['GET'])
async def _() -> str:
task_type = quart.request.args.get("type")
if task_type == '':
task_type = None
return self.success(
data=self.ap.task_mgr.get_tasks_dict(task_type)
)
@self.route('/tasks/<task_id>', methods=['GET'])
async def _(task_id: str) -> str:
task = self.ap.task_mgr.get_task_by_id(int(task_id))
if task is None:
return self.http_status(404, 404, "Task not found")
return self.success(data=task.to_dict())

View File

@@ -19,39 +19,33 @@ class HTTPController:
def __init__(self, ap: app.Application) -> None: def __init__(self, ap: app.Application) -> None:
self.ap = ap self.ap = ap
self.quart_app = quart.Quart(__name__) self.quart_app = quart.Quart(__name__)
quart_cors.cors(self.quart_app, allow_origin='*') quart_cors.cors(self.quart_app, allow_origin="*")
async def initialize(self) -> None: async def initialize(self) -> None:
await self.register_routes() await self.register_routes()
async def run(self) -> None: async def run(self) -> None:
if self.ap.system_cfg.data['http-api']['enable']: if self.ap.system_cfg.data["http-api"]["enable"]:
async def shutdown_trigger_placeholder(): async def shutdown_trigger_placeholder():
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
# task = asyncio.create_task(self.quart_app.run_task( self.ap.task_mgr.create_task(
# host=self.ap.system_cfg.data['http-api']['host'], self.quart_app.run_task(
# port=self.ap.system_cfg.data['http-api']['port'], host=self.ap.system_cfg.data["http-api"]["host"],
# shutdown_trigger=shutdown_trigger_placeholder port=self.ap.system_cfg.data["http-api"]["port"],
# )) shutdown_trigger=shutdown_trigger_placeholder,
# self.ap.asyncio_tasks.append(task) ),
self.ap.task_mgr.create_task(self.quart_app.run_task( name="http-api-quart",
host=self.ap.system_cfg.data['http-api']['host'], )
port=self.ap.system_cfg.data['http-api']['port'],
shutdown_trigger=shutdown_trigger_placeholder
))
async def register_routes(self) -> None: async def register_routes(self) -> None:
@self.quart_app.route('/healthz') @self.quart_app.route("/healthz")
async def healthz(): async def healthz():
return { return {"code": 0, "msg": "ok"}
"code": 0,
"msg": "ok"
}
for g in group.preregistered_groups: for g in group.preregistered_groups:
ginst = g(self.ap, self.quart_app) ginst = g(self.ap, self.quart_app)
await ginst.initialize() await ginst.initialize()

View File

@@ -14,6 +14,7 @@ from ...core import app
class APIGroup(metaclass=abc.ABCMeta): class APIGroup(metaclass=abc.ABCMeta):
"""API 组抽象类""" """API 组抽象类"""
_basic_info: dict = None _basic_info: dict = None
_runtime_info: dict = None _runtime_info: dict = None
@@ -32,33 +33,28 @@ class APIGroup(metaclass=abc.ABCMeta):
data: dict = None, data: dict = None,
params: dict = None, params: dict = None,
headers: dict = {}, headers: dict = {},
**kwargs **kwargs,
): ):
""" """
执行请求 执行请求
""" """
self._runtime_info['account_id'] = "-1" self._runtime_info["account_id"] = "-1"
url = self.prefix + path url = self.prefix + path
data = json.dumps(data) data = json.dumps(data)
headers['Content-Type'] = 'application/json' headers["Content-Type"] = "application/json"
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.request( async with session.request(
method, method, url, data=data, params=params, headers=headers, **kwargs
url,
data=data,
params=params,
headers=headers,
**kwargs
) as resp: ) as resp:
self.ap.logger.debug("data: %s", data) self.ap.logger.debug("data: %s", data)
self.ap.logger.debug("ret: %s", await resp.text()) self.ap.logger.debug("ret: %s", await resp.text())
except Exception as e: except Exception as e:
self.ap.logger.debug(f'上报失败: {e}') self.ap.logger.debug(f"上报失败: {e}")
async def do( async def do(
self, self,
method: str, method: str,
@@ -66,32 +62,29 @@ class APIGroup(metaclass=abc.ABCMeta):
data: dict = None, data: dict = None,
params: dict = None, params: dict = None,
headers: dict = {}, headers: dict = {},
**kwargs **kwargs,
) -> asyncio.Task: ) -> asyncio.Task:
"""执行请求""" """执行请求"""
# task = asyncio.create_task(self._do(method, path, data, params, headers, **kwargs)) # task = asyncio.create_task(self._do(method, path, data, params, headers, **kwargs))
# self.ap.asyncio_tasks.append(task) # self.ap.asyncio_tasks.append(task)
return self.ap.task_mgr.create_task(
self._do(method, path, data, params, headers, **kwargs),
kind="telemetry-operation",
name=f"{method} {path}",
).task
return self.ap.task_mgr.create_task(self._do(method, path, data, params, headers, **kwargs)).task def gen_rid(self):
def gen_rid(
self
):
"""生成一个请求 ID""" """生成一个请求 ID"""
return str(uuid.uuid4()) return str(uuid.uuid4())
def basic_info( def basic_info(self):
self
):
"""获取基本信息""" """获取基本信息"""
basic_info = APIGroup._basic_info.copy() basic_info = APIGroup._basic_info.copy()
basic_info['rid'] = self.gen_rid() basic_info["rid"] = self.gen_rid()
return basic_info return basic_info
def runtime_info( def runtime_info(self):
self
):
"""获取运行时信息""" """获取运行时信息"""
return APIGroup._runtime_info return APIGroup._runtime_info

View File

@@ -114,17 +114,10 @@ class Application:
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
# tasks = [ self.task_mgr.create_task(self.platform_mgr.run(), name="platform-manager")
# asyncio.create_task(self.platform_mgr.run()), # 消息平台 self.task_mgr.create_task(self.ctrl.run(), name="query-controller")
# asyncio.create_task(self.ctrl.run()), # 消息处理循环 self.task_mgr.create_task(self.http_ctrl.run(), name="http-api-controller")
# asyncio.create_task(self.http_ctrl.run()), # http 接口服务 self.task_mgr.create_task(never_ending(), name="never-ending-task")
# asyncio.create_task(never_ending())
# ]
# self.asyncio_tasks.extend(tasks)
self.task_mgr.create_task(self.platform_mgr.run())
self.task_mgr.create_task(self.ctrl.run())
self.task_mgr.create_task(self.http_ctrl.run())
self.task_mgr.create_task(never_ending())
await self.task_mgr.wait_all() await self.task_mgr.wait_all()
except asyncio.CancelledError: except asyncio.CancelledError:

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import typing import typing
import datetime
from . import app from . import app
@@ -16,22 +17,68 @@ class TaskContext:
"""记录日志""" """记录日志"""
def __init__(self): def __init__(self):
self.current_action = "" self.current_action = "default"
self.log = "" self.log = ""
def log(self, msg: str): def _log(self, msg: str):
self.log += msg + "\n" self.log += msg + "\n"
def set_current_action(self, action: str): def set_current_action(self, action: str):
self.current_action = action self.current_action = action
def trace(
self,
msg: str,
action: str = None,
):
if action is not None:
self.set_current_action(action)
self._log(
f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | {self.current_action} | {msg}"
)
def to_dict(self) -> dict:
return {"current_action": self.current_action, "log": self.log}
@staticmethod
def new() -> TaskContext:
return TaskContext()
@staticmethod
def placeholder() -> TaskContext:
global placeholder_context
if placeholder_context is None:
placeholder_context = TaskContext()
return placeholder_context
placeholder_context: TaskContext | None = None
class TaskWrapper: class TaskWrapper:
"""任务包装器""" """任务包装器"""
_id_index: int = 0
"""任务ID索引"""
id: int
"""任务ID"""
task_type: str = "system" # 任务类型: system 或 user task_type: str = "system" # 任务类型: system 或 user
"""任务类型""" """任务类型"""
kind: str = "system_task"
"""任务种类"""
name: str = ""
"""任务唯一名称"""
label: str = ""
"""任务显示名称"""
task_context: TaskContext task_context: TaskContext
"""任务上下文""" """任务上下文"""
@@ -41,17 +88,61 @@ class TaskWrapper:
ap: app.Application ap: app.Application
"""应用实例""" """应用实例"""
def __init__(self, ap: app.Application, coro: typing.Coroutine, task_type: str = "system", context: TaskContext = None): def __init__(
self,
ap: app.Application,
coro: typing.Coroutine,
task_type: str = "system",
kind: str = "system_task",
name: str = "",
label: str = "",
context: TaskContext = None,
):
self.id = TaskWrapper._id_index
TaskWrapper._id_index += 1
self.ap = ap self.ap = ap
self.task_context = context or TaskContext() self.task_context = context or TaskContext()
self.task = self.ap.event_loop.create_task(coro) self.task = self.ap.event_loop.create_task(coro)
self.task_type = task_type self.task_type = task_type
self.kind = kind
self.name = name
self.label = label if label != "" else name
self.task.set_name(name)
def assume_exception(self):
try:
return self.task.exception()
except:
return None
def assume_result(self):
try:
return self.task.result()
except:
return None
def to_dict(self) -> dict:
return {
"id": self.id,
"task_type": self.task_type,
"kind": self.kind,
"name": self.name,
"label": self.label,
"task_context": self.task_context.to_dict(),
"runtime": {
"done": self.task.done(),
"state": self.task._state,
"exception": self.assume_exception(),
"result": self.assume_result(),
},
}
class AsyncTaskManager: class AsyncTaskManager:
"""保存app中的所有异步任务 """保存app中的所有异步任务
包含系统级的和用户级(插件安装、更新等由用户直接发起的)的""" 包含系统级的和用户级(插件安装、更新等由用户直接发起的)的"""
ap: app.Application ap: app.Application
tasks: list[TaskWrapper] tasks: list[TaskWrapper]
@@ -61,13 +152,48 @@ class AsyncTaskManager:
self.ap = ap self.ap = ap
self.tasks = [] self.tasks = []
def create_task(self, coro: typing.Coroutine, task_type: str = "system", context: TaskContext = None) -> TaskWrapper: def create_task(
wrapper = TaskWrapper(self.ap, coro, task_type, context) self,
coro: typing.Coroutine,
task_type: str = "system",
kind: str = "system-task",
name: str = "",
label: str = "",
context: TaskContext = None,
) -> TaskWrapper:
wrapper = TaskWrapper(self.ap, coro, task_type, kind, name, label, context)
self.tasks.append(wrapper) self.tasks.append(wrapper)
return wrapper return wrapper
def create_user_task(
self,
coro: typing.Coroutine,
kind: str = "user-task",
name: str = "",
label: str = "",
context: TaskContext = None,
) -> TaskWrapper:
return self.create_task(coro, "user", kind, name, label, context)
async def wait_all(self): async def wait_all(self):
await asyncio.gather(*[t.task for t in self.tasks], return_exceptions=True) await asyncio.gather(*[t.task for t in self.tasks], return_exceptions=True)
def get_all_tasks(self) -> list[TaskWrapper]: def get_all_tasks(self) -> list[TaskWrapper]:
return self.tasks return self.tasks
def get_tasks_dict(
self,
type: str = None,
) -> dict:
return {
"tasks": [
t.to_dict() for t in self.tasks if type is None or t.task_type == type
],
"id_index": TaskWrapper._id_index,
}
def get_task_by_id(self, id: int) -> TaskWrapper | None:
for t in self.tasks:
if t.id == id:
return t
return None

View File

@@ -62,7 +62,11 @@ class Controller:
# task = asyncio.create_task(_process_query(selected_query)) # task = asyncio.create_task(_process_query(selected_query))
# self.ap.asyncio_tasks.append(task) # self.ap.asyncio_tasks.append(task)
self.ap.task_mgr.create_task(_process_query(selected_query)) self.ap.task_mgr.create_task(
_process_query(selected_query),
kind="query",
name=f"query-{selected_query.query_id}",
)
except Exception as e: except Exception as e:
# traceback.print_exc() # traceback.print_exc()

View File

@@ -186,7 +186,11 @@ class PlatformManager:
for task in tasks: for task in tasks:
# async_task = asyncio.create_task(task) # async_task = asyncio.create_task(task)
# self.ap.asyncio_tasks.append(async_task) # self.ap.asyncio_tasks.append(async_task)
self.ap.task_mgr.create_task(task) self.ap.task_mgr.create_task(
task,
kind="platform-adapter",
name=f"platform-adapter-{adapter.name}",
)
except Exception as e: except Exception as e:
self.ap.logger.error('平台适配器运行出错: ' + str(e)) self.ap.logger.error('平台适配器运行出错: ' + str(e))

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import typing import typing
import abc import abc
from ..core import app from ..core import app, taskmgr
class PluginInstaller(metaclass=abc.ABCMeta): class PluginInstaller(metaclass=abc.ABCMeta):
@@ -40,6 +40,7 @@ class PluginInstaller(metaclass=abc.ABCMeta):
self, self,
plugin_name: str, plugin_name: str,
plugin_source: str=None, plugin_source: str=None,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""更新插件 """更新插件
""" """

View File

@@ -9,6 +9,7 @@ import requests
from .. import installer, errors from .. import installer, errors
from ...utils import pkgmgr from ...utils import pkgmgr
from ...core import taskmgr
class GitHubRepoInstaller(installer.PluginInstaller): class GitHubRepoInstaller(installer.PluginInstaller):
@@ -94,13 +95,20 @@ class GitHubRepoInstaller(installer.PluginInstaller):
async def install_plugin( async def install_plugin(
self, self,
plugin_source: str, plugin_source: str,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""安装插件 """安装插件
""" """
task_context.trace("下载插件源码...", "install-plugin")
repo_label = await self.download_plugin_source_code(plugin_source, "plugins/") repo_label = await self.download_plugin_source_code(plugin_source, "plugins/")
task_context.trace("安装插件依赖...", "install-plugin")
await self.install_requirements("plugins/" + repo_label) await self.install_requirements("plugins/" + repo_label)
task_context.trace("完成.", "install-plugin")
await self.ap.plugin_mgr.setting.record_installed_plugin_source( await self.ap.plugin_mgr.setting.record_installed_plugin_source(
"plugins/"+repo_label+'/', plugin_source "plugins/"+repo_label+'/', plugin_source
) )
@@ -122,9 +130,12 @@ class GitHubRepoInstaller(installer.PluginInstaller):
self, self,
plugin_name: str, plugin_name: str,
plugin_source: str=None, plugin_source: str=None,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""更新插件 """更新插件
""" """
task_context.trace("更新插件...", "update-plugin")
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is None: if plugin_container is None:
@@ -133,7 +144,9 @@ class GitHubRepoInstaller(installer.PluginInstaller):
if plugin_container.plugin_source: if plugin_container.plugin_source:
plugin_source = plugin_container.plugin_source plugin_source = plugin_container.plugin_source
await self.install_plugin(plugin_source) task_context.trace("转交安装任务.", "update-plugin")
await self.install_plugin(plugin_source, task_context)
else: else:
raise errors.PluginInstallerError('插件无源码信息,无法更新') raise errors.PluginInstallerError('插件无源码信息,无法更新')

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import typing import typing
import traceback import traceback
from ..core import app from ..core import app, taskmgr
from . import context, loader, events, installer, setting, models from . import context, loader, events, installer, setting, models
from .loaders import classic from .loaders import classic
from .installers import github from .installers import github
@@ -102,10 +102,11 @@ class PluginManager:
self, self,
plugin_name: str, plugin_name: str,
plugin_source: str=None, plugin_source: str=None,
task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(),
): ):
"""更新插件 """更新插件
""" """
await self.installer.update_plugin(plugin_name, plugin_source) await self.installer.update_plugin(plugin_name, plugin_source, task_context)
plugin_container = self.get_plugin_by_name(plugin_name) plugin_container = self.get_plugin_by_name(plugin_name)
@@ -120,6 +121,7 @@ class PluginManager:
new_version="HEAD" new_version="HEAD"
) )
def get_plugin_by_name(self, plugin_name: str) -> context.RuntimeContainer: def get_plugin_by_name(self, plugin_name: str) -> context.RuntimeContainer:
"""通过插件名获取插件 """通过插件名获取插件
""" """

View File

@@ -42,7 +42,7 @@
</v-list-item> --> </v-list-item> -->
<v-dialog max-width="500" persistent v-model="taskDialogShow"> <v-dialog max-width="500" persistent v-model="taskDialogShow">
<template v-slot:activator="{ props: activatorProps }"> <template v-slot:activator="{ props: activatorProps }">
<v-list-item id="system-tasks-list-item" title="系统任务" prepend-icon="mdi-align-horizontal-left" v-tooltip="任务列表" v-bind="activatorProps"> <v-list-item id="system-tasks-list-item" title="任务列表" prepend-icon="mdi-align-horizontal-left" v-tooltip="任务列表" v-bind="activatorProps">
</v-list-item> </v-list-item>
</template> </template>

View File

@@ -95,7 +95,7 @@ const menuItems = [
}, },
{ {
title: '删除', title: '删除',
condition: (plugin) => plugin.source != '', condition: (plugin) => true,
action: uninstallPlugin action: uninstallPlugin
} }
] ]

View File

@@ -0,0 +1,78 @@
<template>
<div class="task-card">
<div class="task-card-icon">
<v-progress-circular :size="25" :width="2" indeterminate v-if="task.runtime.state == 'PENDING'" />
<v-icon v-else-if="task.runtime.state == 'FINISHED'" style="color: #4caf50;" :size="25" icon="mdi-check" />
<v-icon v-else-if="task.runtime.state == 'CANCELLED'" style="color: #f44336;" :size="25" icon="mdi-close" />
</div>
<div class="task-card-content">
<div class="task-card-kind">{{ task.kind }}</div>
<div class="task-card-label">{{ task.label }}</div>
<v-chip class="task-card-action" color="primary" variant="outlined" size="small" density="compact">正在执行: {{ task.task_context.current_action }}</v-chip>
</div>
<div class="task-card-actions">
<!-- <v-icon icon="mdi-details" /> -->
<v-btn icon="mdi-information-outline" variant="text" />
</div>
</div>
</template>
<script setup>
defineProps({
task: {
type: Object,
required: true
}
})
</script>
<style scoped>
.task-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100%;
padding-inline: 0.5rem;
}
.task-card-icon {
margin-right: 1rem;
margin-left: 0.8rem;
}
.task-card-content {
flex: 1;
margin-left: 0.4rem;
display: flex;
flex-direction: column;
gap: 0.02rem;
user-select: none;
}
.task-card-kind {
font-size: 0.6rem;
color: #666;
}
.task-card-label {
font-size: 0.8rem;
color: #333;
}
.task-card-action {
font-size: 0.6rem;
width: fit-content;
padding-inline: 0.3rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-actions {
justify-self: flex-end;
}
</style>

View File

@@ -1,27 +1,9 @@
<template> <template>
<v-card prepend-icon="mdi-align-horizontal-left" text="用户发起的任务列表" title="任务列表"> <v-card class="task-dialog" prepend-icon="mdi-align-horizontal-left" text="用户发起的任务列表" title="任务列表">
<v-list id="plugin-orchestration-list"> <v-list id="task-list" v-if="taskList.length > 0">
<draggable v-model="plugins" item-key="name" group="plugins" @start="drag = true" <TaskCard class="task-card" v-for="task in taskList" :key="task.id" :task="task" />
id="plugin-orchestration-draggable"
@end="drag = false">
<template #item="{ element }">
<div class="plugin-orchestration-item">
<div class="plugin-orchestration-item-title">
<div class="plugin-orchestration-item-author">
{{ element.author }} /
</div>
<div class="plugin-orchestration-item-name">
{{ element.name }}
</div>
</div>
<div class="plugin-orchestration-item-action">
<v-icon>mdi-drag</v-icon>
</div>
</div>
</template>
</draggable>
</v-list> </v-list>
<div v-else><v-alert color="warning" icon="$warning" title="暂无任务" text="暂无已添加的用户任务项" density="compact" style="margin-inline: 1rem;"></v-alert></div>
<template v-slot:actions> <template v-slot:actions>
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn> <v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
@@ -35,7 +17,11 @@ defineProps({
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
import { ref } from 'vue' import TaskCard from '@/components/TaskCard.vue'
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
import { inject } from 'vue' import { inject } from 'vue'
@@ -44,8 +30,61 @@ const snackbar = inject('snackbar')
const close = () => { const close = () => {
emit('close') emit('close')
} }
const taskList = ref([])
const refresh = () => {
proxy.$axios.get('/system/tasks', {
params: {
type: 'user'
}
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.message)
return
}
taskList.value = response.data.data.tasks
// 倒序
taskList.value.reverse()
}).catch(error => {
snackbar.error(error.message)
})
}
let refreshTask = null
onMounted(() => {
refresh()
refreshTask = setInterval(refresh, 500)
})
onUnmounted(() => {
clearInterval(refreshTask)
})
</script> </script>
<style scoped> <style scoped>
.task-dialog {
width: 100%;
}
</style> #task-list {
max-height: 20rem;
overflow-y: auto;
margin-inline: 1rem;
width: calc(100% - 2.2rem);
padding-inline: 0.6rem;
}
.task-card {
/* margin-bottom: 0.1rem; */
display: flex;
flex-direction: row;
/* box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1); */
height: 4rem;
/* border: 0.08rem solid #ccc; */
box-shadow: 0.1rem 0.1rem 0.2rem 0.05rem #ccc;
background-color: #ffffff;
}
</style>

View File

@@ -52,7 +52,7 @@
</v-card> </v-card>
<div class="plugins-container"> <div class="plugins-container">
<PluginCard class="plugin-card" v-for="plugin in plugins" :key="plugin.name" :plugin="plugin" <PluginCard class="plugin-card" v-for="plugin in plugins" :key="plugin.name" :plugin="plugin"
@toggle="togglePlugin" /> @toggle="togglePlugin" @update="updatePlugin" />
</div> </div>
</template> </template>
@@ -88,7 +88,7 @@ const refresh = () => {
onMounted(refresh) onMounted(refresh)
const togglePlugin = (plugin) => { const togglePlugin = (plugin) => {
proxy.$axios.put(`/plugins/toggle/${plugin.author}/${plugin.name}`, { proxy.$axios.put(`/plugins/${plugin.author}/${plugin.name}/toggle`, {
target_enabled: !plugin.enabled target_enabled: !plugin.enabled
}).then(res => { }).then(res => {
if (res.data.code != 0) { if (res.data.code != 0) {
@@ -101,6 +101,18 @@ const togglePlugin = (plugin) => {
}) })
} }
const updatePlugin = (plugin) => {
proxy.$axios.post(`/plugins/${plugin.author}/${plugin.name}/update`).then(res => {
if (res.data.code != 0) {
snackbar.error(res.data.msg)
return
}
snackbar.success(`已添加更新任务 请到任务列表查看进度`)
}).catch(error => {
snackbar.error(error)
})
}
const isOrchestrationDialogActive = ref(false) const isOrchestrationDialogActive = ref(false)
const cancelOrderChanges = () => { const cancelOrderChanges = () => {