Merge pull request #911 from RockChinQ/feat/trackable-async-tasks

Feat: 用户级任务系统
This commit is contained in:
Junyan Qin
2024-11-01 22:42:11 +08:00
committed by GitHub
22 changed files with 560 additions and 86 deletions
+7
View File
@@ -54,6 +54,13 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
if __name__ == '__main__': if __name__ == '__main__':
import os import os
import sys
# 必须大于 3.10.1
if sys.version_info < (3, 10, 1):
print("需要 Python 3.10.1 及以上版本,当前 Python 版本为:", sys.version)
input("按任意键退出...")
exit(1)
# 检查本目录是否有main.py,且包含QChatGPT字符串 # 检查本目录是否有main.py,且包含QChatGPT字符串
invalid_pwd = False invalid_pwd = False
+16 -2
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:
+22 -1
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())
+15 -15
View File
@@ -19,33 +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) ),
name="http-api-quart",
)
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()
+21 -27
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,31 +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 task return self.ap.task_mgr.create_task(
self._do(method, path, data, params, headers, **kwargs),
kind="telemetry-operation",
name=f"{method} {path}",
).task
def gen_rid( def gen_rid(self):
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
+9 -24
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import logging import logging
import asyncio import asyncio
import threading
import traceback import traceback
from ..platform import manager as im_mgr from ..platform import manager as im_mgr
@@ -21,6 +22,7 @@ from ..utils import version as version_mgr, proxy as proxy_mgr, announce as anno
from ..persistence import mgr as persistencemgr from ..persistence import mgr as persistencemgr
from ..api.http.controller import main as http_controller from ..api.http.controller import main as http_controller
from ..utils import logcache from ..utils import logcache
from . import taskmgr
class Application: class Application:
@@ -28,7 +30,8 @@ class Application:
event_loop: asyncio.AbstractEventLoop = None event_loop: asyncio.AbstractEventLoop = None
asyncio_tasks: list[asyncio.Task] = [] # asyncio_tasks: list[asyncio.Task] = []
task_mgr: taskmgr.AsyncTaskManager = None
platform_mgr: im_mgr.PlatformManager = None platform_mgr: im_mgr.PlatformManager = None
@@ -103,8 +106,6 @@ class Application:
async def run(self): async def run(self):
await self.plugin_mgr.initialize_plugins() await self.plugin_mgr.initialize_plugins()
tasks = []
try: try:
# 后续可能会允许动态重启其他任务 # 后续可能会允许动态重启其他任务
@@ -113,28 +114,12 @@ 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)
# 挂系统信号处理 await self.task_mgr.wait_all()
import signal
def signal_handler(sig, frame):
for task in self.asyncio_tasks:
task.cancel()
self.logger.info("程序退出.")
# 结束当前事件循环
self.event_loop.stop()
exit(0)
signal.signal(signal.SIGINT, signal_handler)
await asyncio.gather(*tasks, return_exceptions=True)
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: except Exception as e:
+10
View File
@@ -49,6 +49,16 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
async def main(loop: asyncio.AbstractEventLoop): async def main(loop: asyncio.AbstractEventLoop):
try: try:
# 挂系统信号处理
import signal
def signal_handler(sig, frame):
print("[Signal] 程序退出.")
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
app_inst = await make_app(loop) app_inst = await make_app(loop)
await app_inst.run() await app_inst.run()
except Exception as e: except Exception as e:
+2 -2
View File
@@ -27,8 +27,8 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.
if constants.debug_mode: if constants.debug_mode:
level = logging.DEBUG level = logging.DEBUG
log_file_name = "data/logs/qcg-%s.log" % time.strftime( log_file_name = "data/logs/langbot-%s.log" % time.strftime(
"%Y-%m-%d-%H-%M-%S", time.localtime() "%Y-%m-%d", time.localtime()
) )
qcg_logger = logging.getLogger("qcg") qcg_logger = logging.getLogger("qcg")
View File
+2
View File
@@ -18,6 +18,7 @@ from ...platform import manager as im_mgr
from ...persistence import mgr as persistencemgr from ...persistence import mgr as persistencemgr
from ...api.http.controller import main as http_controller from ...api.http.controller import main as http_controller
from ...utils import logcache from ...utils import logcache
from .. import taskmgr
@stage.stage_class("BuildAppStage") @stage.stage_class("BuildAppStage")
@@ -28,6 +29,7 @@ class BuildAppStage(stage.BootingStage):
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""构建app对象的各个组件对象并初始化 """构建app对象的各个组件对象并初始化
""" """
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
proxy_mgr = proxy.ProxyManager(ap) proxy_mgr = proxy.ProxyManager(ap)
await proxy_mgr.initialize() await proxy_mgr.initialize()
+199
View File
@@ -0,0 +1,199 @@
from __future__ import annotations
import asyncio
import typing
import datetime
from . import app
class TaskContext:
"""任务跟踪上下文"""
current_action: str
"""当前正在执行的动作"""
log: str
"""记录日志"""
def __init__(self):
self.current_action = "default"
self.log = ""
def _log(self, msg: str):
self.log += msg + "\n"
def set_current_action(self, action: str):
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:
"""任务包装器"""
_id_index: int = 0
"""任务ID索引"""
id: int
"""任务ID"""
task_type: str = "system" # 任务类型: system 或 user
"""任务类型"""
kind: str = "system_task"
"""任务种类"""
name: str = ""
"""任务唯一名称"""
label: str = ""
"""任务显示名称"""
task_context: TaskContext
"""任务上下文"""
task: asyncio.Task
"""任务"""
ap: app.Application
"""应用实例"""
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.task_context = context or TaskContext()
self.task = self.ap.event_loop.create_task(coro)
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:
"""保存app中的所有异步任务
包含系统级的和用户级(插件安装、更新等由用户直接发起的)的"""
ap: app.Application
tasks: list[TaskWrapper]
"""所有任务"""
def __init__(self, ap: app.Application):
self.ap = ap
self.tasks = []
def create_task(
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)
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):
await asyncio.gather(*[t.task for t in self.tasks], return_exceptions=True)
def get_all_tasks(self) -> list[TaskWrapper]:
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
+7 -2
View File
@@ -60,8 +60,13 @@ class Controller:
# 通知其他协程,有新的请求可以处理了 # 通知其他协程,有新的请求可以处理了
self.ap.query_pool.condition.notify_all() self.ap.query_pool.condition.notify_all()
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),
kind="query",
name=f"query-{selected_query.query_id}",
)
except Exception as e: except Exception as e:
# traceback.print_exc() # traceback.print_exc()
+7 -3
View File
@@ -184,10 +184,14 @@ class PlatformManager:
tasks.append(exception_wrapper(adapter)) tasks.append(exception_wrapper(adapter))
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,
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))
self.ap.logger.debug(f"Traceback: {traceback.format_exc()}") self.ap.logger.debug(f"Traceback: {traceback.format_exc()}")
+2 -1
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(),
): ):
"""更新插件 """更新插件
""" """
+14 -1
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('插件无源码信息,无法更新')
+4 -2
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:
"""通过插件名获取插件 """通过插件名获取插件
""" """
+2 -2
View File
@@ -9,7 +9,7 @@ Pillow
tiktoken tiktoken
PyYaml PyYaml
aiohttp aiohttp
pydantic pydantic<2.0
websockets websockets
urllib3 urllib3
psutil psutil
@@ -18,4 +18,4 @@ ollama
quart quart
sqlalchemy[asyncio] sqlalchemy[asyncio]
aiosqlite aiosqlite
quart-cors quart-cors
+38 -1
View File
@@ -34,6 +34,23 @@
<template v-slot:append> <template v-slot:append>
<div> <div>
<v-list density="compact" nav> <v-list density="compact" nav>
<!-- <v-list-item id="system-tasks-list-item" title="系统任务" prepend-icon="mdi-align-horizontal-left" v-tooltip="系统任务" @click="showTaskDialog" v-bind="props">
<v-dialog activator="parent" max-width="500" persistent v-model="taskDialogShow">
<template v-slot:default="{ isActive }">
</template>
</v-dialog>
</v-list-item> -->
<v-dialog max-width="500" persistent v-model="taskDialogShow">
<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>
</template>
<template v-slot:default="{ isActive }">
<TaskDialog :dialog="{ show: isActive }" @close="closeTaskDialog" />
</template>
</v-dialog>
<v-list-item id="about-list-item" title="系统信息" prepend-icon="mdi-cog-outline" v-tooltip="系统信息"> <v-list-item id="about-list-item" title="系统信息" prepend-icon="mdi-cog-outline" v-tooltip="系统信息">
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -52,6 +69,8 @@
import { getCurrentInstance } from 'vue' import { getCurrentInstance } from 'vue'
import {provide, ref, watch} from 'vue'; import {provide, ref, watch} from 'vue';
import TaskDialog from '@/components/TaskDialog.vue'
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const snackbar = ref(false); const snackbar = ref(false);
@@ -84,6 +103,15 @@ function info(content) {
text.value = content; text.value = content;
} }
const taskDialogShow = ref(false)
function showTaskDialog() {
taskDialogShow.value = true
}
function closeTaskDialog() {
taskDialogShow.value = false
}
provide('snackbar', {success, error, warning, info, location, timeout}); provide('snackbar', {success, error, warning, info, location, timeout});
</script> </script>
@@ -141,4 +169,13 @@ provide('snackbar', {success, error, warning, info, location, timeout});
#about-list-item:active { #about-list-item:active {
background-color: #ddd; background-color: #ddd;
} }
</style>
#system-tasks-list-item:hover {
cursor: pointer;
background-color: #eee;
}
#system-tasks-list-item:active {
background-color: #ddd;
}
</style>
+1 -1
View File
@@ -95,7 +95,7 @@ const menuItems = [
}, },
{ {
title: '删除', title: '删除',
condition: (plugin) => plugin.source != '', condition: (plugin) => true,
action: uninstallPlugin action: uninstallPlugin
} }
] ]
+78
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>
+90
View File
@@ -0,0 +1,90 @@
<template>
<v-card class="task-dialog" prepend-icon="mdi-align-horizontal-left" text="用户发起的任务列表" title="任务列表">
<v-list id="task-list" v-if="taskList.length > 0">
<TaskCard class="task-card" v-for="task in taskList" :key="task.id" :task="task" />
</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>
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
</template>
</v-card>
</template>
<script setup>
defineProps({
})
const emit = defineEmits(['close'])
import TaskCard from '@/components/TaskCard.vue'
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
import { inject } from 'vue'
const snackbar = inject('snackbar')
const 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>
<style scoped>
.task-dialog {
width: 100%;
}
#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>
+14 -2
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 = () => {