mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 11:29:39 +08:00
feat: 完成异步任务跟踪架构基础
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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(),
|
||||||
):
|
):
|
||||||
"""更新插件
|
"""更新插件
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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('插件无源码信息,无法更新')
|
||||||
|
|||||||
@@ -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:
|
||||||
"""通过插件名获取插件
|
"""通过插件名获取插件
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const menuItems = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '删除',
|
title: '删除',
|
||||||
condition: (plugin) => plugin.source != '',
|
condition: (plugin) => true,
|
||||||
action: uninstallPlugin
|
action: uninstallPlugin
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
78
web/src/components/TaskCard.vue
Normal file
78
web/src/components/TaskCard.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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 = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user