feat: 插件页展示功能

This commit is contained in:
Junyan Qin
2024-10-19 18:38:01 +08:00
parent c330aab48b
commit 16b386eaf7
8 changed files with 435 additions and 22 deletions
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
import traceback
import quart
from .....core import app
from .. import group
@group.group_class('plugins', '/api/v1/plugins')
class PluginsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET'])
async def _() -> str:
plugins = self.ap.plugin_mgr.plugins
plugins_data = [plugin.model_dump() for plugin in plugins]
return self.success(data={
'plugins': plugins_data
})
@self.route('/toggle/<author>/<plugin_name>', methods=['PUT'])
async def _(author: str, plugin_name: str) -> str:
data = await quart.request.json
target_enabled = data.get('target_enabled')
await self.ap.plugin_mgr.update_plugin_status(plugin_name, target_enabled)
return self.success()
+1 -1
View File
@@ -6,7 +6,7 @@ import quart
import quart_cors import quart_cors
from ....core import app from ....core import app
from .groups import logs, system, settings from .groups import logs, system, settings, plugins
from . import group from . import group
+2 -20
View File
@@ -163,24 +163,6 @@ class PluginDelOperator(operator.CommandOperator):
yield entities.CommandReturn(error=errors.CommandError("插件删除失败: "+str(e))) yield entities.CommandReturn(error=errors.CommandError("插件删除失败: "+str(e)))
async def update_plugin_status(plugin_name: str, new_status: bool, ap: app.Application):
if ap.plugin_mgr.get_plugin_by_name(plugin_name) is not None:
for plugin in ap.plugin_mgr.plugins:
if plugin.plugin_name == plugin_name:
plugin.enabled = new_status
for func in plugin.content_functions:
func.enable = new_status
await ap.plugin_mgr.setting.dump_container_setting(ap.plugin_mgr.plugins)
break
return True
else:
return False
@operator.operator_class( @operator.operator_class(
name="on", name="on",
help="启用插件", help="启用插件",
@@ -200,7 +182,7 @@ class PluginEnableOperator(operator.CommandOperator):
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
try: try:
if await update_plugin_status(plugin_name, True, self.ap): if await self.ap.plugin_mgr.update_plugin_status(plugin_name, True):
yield entities.CommandReturn(text="已启用插件: {}".format(plugin_name)) yield entities.CommandReturn(text="已启用插件: {}".format(plugin_name))
else: else:
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name))) yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name)))
@@ -228,7 +210,7 @@ class PluginDisableOperator(operator.CommandOperator):
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
try: try:
if await update_plugin_status(plugin_name, False, self.ap): if await self.ap.plugin_mgr.update_plugin_status(plugin_name, False):
yield entities.CommandReturn(text="已禁用插件: {}".format(plugin_name)) yield entities.CommandReturn(text="已禁用插件: {}".format(plugin_name))
else: else:
yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name))) yield entities.CommandReturn(error=errors.CommandError("插件状态修改失败: 未找到插件 {}".format(plugin_name)))
+28
View File
@@ -320,3 +320,31 @@ class RuntimeContainer(pydantic.BaseModel):
for function in self.content_functions: for function in self.content_functions:
function.enable = self.enabled function.enable = self.enabled
def model_dump(self, *args, **kwargs):
return {
'name': self.plugin_name,
'description': self.plugin_description,
'version': self.plugin_version,
'author': self.plugin_author,
'source': self.plugin_source,
'main_file': self.main_file,
'pkg_path': self.pkg_path,
'enabled': self.enabled,
'priority': self.priority,
'event_handlers': {
event_name.__name__: handler.__name__
for event_name, handler in self.event_handlers.items()
},
'content_functions': [
{
'name': function.name,
'human_desc': function.human_desc,
'description': function.description,
'parameters': function.parameters,
'enable': function.enable,
'func': function.func.__name__,
}
for function in self.content_functions
],
}
+1 -1
View File
@@ -5,7 +5,7 @@ import pkgutil
import importlib import importlib
import traceback import traceback
from .. import loader, events, context, models, host from .. import loader, events, context, models
from ...core import entities as core_entities from ...core import entities as core_entities
from ...provider.tools import entities as tools_entities from ...provider.tools import entities as tools_entities
from ...utils import funcschema from ...utils import funcschema
+17
View File
@@ -186,3 +186,20 @@ class PluginManager:
) )
return ctx return ctx
async def update_plugin_status(self, plugin_name: str, new_status: bool):
if self.get_plugin_by_name(plugin_name) is not None:
for plugin in self.plugins:
if plugin.plugin_name == plugin_name:
plugin.enabled = new_status
for func in plugin.content_functions:
func.enable = new_status
await self.setting.dump_container_setting(self.plugins)
break
return True
else:
return False
+257
View File
@@ -0,0 +1,257 @@
<template>
<div class="plugin-card">
<div class="plugin-card-header">
<div class="plugin-id">
<div class="plugin-card-author">{{ plugin.author }} /</div>
<div class="plugin-card-title">{{ plugin.name }}</div>
</div>
<div class="plugin-card-badges">
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.source != ''"
@click="openGithubSource"></v-icon>
<v-chip class="plugin-disabled" v-if="!plugin.enabled" color="error" variant="outlined"
density="compact">已禁用</v-chip>
<v-chip class="plugin-version" color="primary" density="compact" variant="flat">v{{ plugin.version
}}</v-chip>
</div>
</div>
<div class="plugin-card-description">{{ plugin.description }}</div>
<div class="plugin-card-brief-info">
<div class="plugin-card-brief-info-item">
<v-tooltip text="已注册的事件处理器" location="bottom">
<template v-slot:activator="{ props }">
<div class="plugin-card-events" v-bind="props">
<v-icon class="plugin-card-events-icon" icon="mdi-link-box-variant-outline" />
<div class="plugin-card-events-count">{{ Object.keys(plugin.event_handlers).length }}</div>
</div>
</template>
</v-tooltip>
<v-tooltip text="已注册的内容函数" location="bottom">
<template v-slot:activator="{ props }">
<div class="plugin-card-functions" v-bind="props">
<v-icon class="plugin-card-functions-icon" icon="mdi-tools" />
<div class="plugin-card-functions-count">{{ plugin.content_functions.length }}</div>
</div>
</template>
</v-tooltip>
</div>
<v-menu class="plugin-card-menu">
<template v-slot:activator="{ props }">
<v-icon class="plugin-card-menu-btn" icon="mdi-cog" v-bind="props" variant="text" size="small"/>
</template>
<v-list>
<template v-for="item in menuItems" :key="item.title">
<v-list-item v-if="item.condition(plugin)" @click="item.action">
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-menu>
</div>
</div>
</template>
<script setup>
const props = defineProps({
plugin: {
type: Object,
required: true
},
});
const emit = defineEmits(['toggle', 'update', 'uninstall']);
const openGithubSource = () => {
window.open(props.plugin.source, '_blank');
}
const togglePlugin = () => {
emit('toggle', props.plugin);
}
const updatePlugin = () => {
emit('update', props.plugin);
}
const uninstallPlugin = () => {
emit('uninstall', props.plugin);
}
const menuItems = [
{
title: '禁用',
condition: (plugin) => plugin.enabled,
action: togglePlugin
},
{
title: '启用',
condition: (plugin) => !plugin.enabled,
action: togglePlugin
},
{
title: '更新',
condition: (plugin) => plugin.source != '',
action: updatePlugin
},
{
title: '删除',
condition: (plugin) => plugin.source != '',
action: uninstallPlugin
}
]
</script>
<style scoped>
.plugin-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0.8rem;
padding-left: 1rem;
margin: 1rem 0;
background-color: white;
display: flex;
flex-direction: column;
height: 10rem;
}
.plugin-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.plugin-card-author {
font-size: 0.8rem;
color: #666;
font-weight: 500;
user-select: none;
}
.plugin-card-title {
font-size: 1.1rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.plugin-card-description {
font-size: 0.7rem;
color: #666;
font-weight: 500;
margin-top: 0rem;
height: 2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.plugin-card-badges {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.plugin-github-source {
cursor: pointer;
color: #222;
font-size: 1.3rem;
}
.plugin-disabled {
font-size: 0.7rem;
font-weight: 500;
height: 1.3rem;
padding-inline: 0.4rem;
user-select: none;
}
.plugin-version {
font-size: 0.7rem;
font-weight: 700;
height: 1.3rem;
padding-inline: 0.5rem;
user-select: none;
}
.plugin-card-brief-info {
display: flex;
flex-direction: row;
justify-content: space-between;
/* background-color: #f0f0f0; */
gap: 0.8rem;
margin-left: -0.2rem;
margin-top: 0.5rem;
}
.plugin-card-events {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-events-icon {
font-size: 1.8rem;
color: #666;
}
.plugin-card-events-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-functions {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-functions-icon {
font-size: 1.6rem;
color: #666;
}
.plugin-card-functions-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-brief-info-item {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-brief-info-item:hover {
cursor: pointer;
}
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
color: #333;
}
.plugin-card-menu {
margin-top: 0.5rem;
}
.plugin-card-menu-btn {
font-size: 1.4rem;
margin-top: 0.2rem;
font-weight: 400;
padding: 0rem;
color: #3265ba;
}
.plugin-card-menu-btn:hover {
color: #4271bf;
}
.plugin-card-menu-btn:active {
color: rgb(0, 47, 104);
}
</style>
+98
View File
@@ -1,13 +1,111 @@
<template> <template>
<PageTitle title="插件" @refresh="refresh" /> <PageTitle title="插件" @refresh="refresh" />
<v-card id="plugins-toolbar">
<div id="view-btns">
</div>
<div id="operation-btns">
<v-tooltip text="设置插件优先级" location="top">
<template v-slot:activator="{ props }">
<v-btn prepend-icon="mdi-priority-high" v-bind="props">
编排
</v-btn>
</template>
</v-tooltip>
<v-btn color="primary" prepend-icon="mdi-plus">
安装
</v-btn>
</div>
</v-card>
<div class="plugins-container">
<PluginCard class="plugin-card" v-for="plugin in plugins" :key="plugin.name" :plugin="plugin" @toggle="togglePlugin" />
</div>
</template> </template>
<script setup> <script setup>
import PageTitle from '@/components/PageTitle.vue' import PageTitle from '@/components/PageTitle.vue'
import PluginCard from '@/components/PluginCard.vue'
import { ref, getCurrentInstance, onMounted } from 'vue'
import {inject} from "vue";
const snackbar = inject('snackbar');
const { proxy } = getCurrentInstance()
const plugins = ref([])
const refresh = () => {
proxy.$axios.get('/plugins').then(res => {
if (res.data.code != 0) {
snackbar.error(res.data.msg)
return
}
plugins.value = res.data.data.plugins
}).catch(error => {
snackbar.error(error)
})
}
onMounted(refresh)
const togglePlugin = (plugin) => {
proxy.$axios.put(`/plugins/toggle/${plugin.author}/${plugin.name}`, {
target_enabled: !plugin.enabled
}).then(res => {
if (res.data.code != 0) {
snackbar.error(res.data.msg)
return
}
refresh()
}).catch(error => {
snackbar.error(error)
})
}
</script> </script>
<style scoped> <style scoped>
#plugins-toolbar {
margin-top: 1rem;
margin-inline: 1rem;
height: 3.2rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
#view-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
margin-left: 1rem;
}
#operation-btns {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
margin-right: 1rem;
}
.plugins-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
margin-inline: 1rem;
}
.plugin-card {
width: 18rem;
height: 8rem;
}
</style> </style>