From ebd091a9e0f66d3107f79f18176a45f5c4afaaaf Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 12 Apr 2025 20:21:43 +0800 Subject: [PATCH] refactor: move plugin setting to db --- pkg/api/http/controller/groups/plugins.py | 32 ++++--- pkg/entity/persistence/plugin.py | 16 ++++ pkg/persistence/mgr.py | 2 +- pkg/plugin/context.py | 7 ++ pkg/plugin/installers/github.py | 8 +- pkg/plugin/loaders/classic.py | 8 +- pkg/plugin/loaders/manifest.py | 3 + pkg/plugin/manager.py | 78 +++++++++++++++-- pkg/plugin/setting.py | 101 ---------------------- pkg/utils/version.py | 2 +- 10 files changed, 130 insertions(+), 127 deletions(-) create mode 100644 pkg/entity/persistence/plugin.py delete mode 100644 pkg/plugin/setting.py diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 00951550..3873872a 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -44,20 +44,28 @@ class PluginsRouterGroup(group.RouterGroup): 'task_id': wrapper.id }) - @self.route('//', methods=['DELETE'], auth_type=group.AuthType.USER_TOKEN) + @self.route('//', methods=['GET', 'DELETE'], auth_type=group.AuthType.USER_TOKEN) async def _(author: str, plugin_name: str) -> str: - ctx = taskmgr.TaskContext.new() - wrapper = self.ap.task_mgr.create_user_task( - self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx), - kind="plugin-operation", - name=f'plugin-remove-{plugin_name}', - label=f'删除插件 {plugin_name}', - context=ctx - ) + if quart.request.method == 'GET': + plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) + if plugin is None: + return self.http_status(404, -1, 'plugin not found') + return self.success(data={ + 'plugin': plugin.model_dump() + }) + elif quart.request.method == 'DELETE': + ctx = taskmgr.TaskContext.new() + wrapper = self.ap.task_mgr.create_user_task( + self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx), + kind="plugin-operation", + name=f'plugin-remove-{plugin_name}', + label=f'删除插件 {plugin_name}', + context=ctx + ) - return self.success(data={ - 'task_id': wrapper.id - }) + return self.success(data={ + 'task_id': wrapper.id + }) @self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: diff --git a/pkg/entity/persistence/plugin.py b/pkg/entity/persistence/plugin.py new file mode 100644 index 00000000..b1e2cac4 --- /dev/null +++ b/pkg/entity/persistence/plugin.py @@ -0,0 +1,16 @@ +import sqlalchemy + +from .base import Base + + +class PluginSetting(Base): + """插件配置""" + __tablename__ = 'plugin_settings' + + plugin_author = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + plugin_name = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) + priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) + config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict) + created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) + updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now()) diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index 2892c52b..56809e6b 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -8,7 +8,7 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio import sqlalchemy from . import database -from ..entity.persistence import base, user, model, pipeline, bot +from ..entity.persistence import base, user, model, pipeline, bot, plugin from ..core import app from .databases import sqlite diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index 16fe414d..63d390f8 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -339,6 +339,12 @@ class RuntimeContainer(pydantic.BaseModel): priority: typing.Optional[int] = 0 """优先级""" + config_schema: typing.Optional[list[dict]] = [] + """插件配置模板""" + + plugin_config: typing.Optional[dict] = {} + """插件配置""" + plugin_inst: typing.Optional[BasePlugin] = None """插件实例""" @@ -389,6 +395,7 @@ class RuntimeContainer(pydantic.BaseModel): 'pkg_path': self.pkg_path, 'enabled': self.enabled, 'priority': self.priority, + "config_schema": self.config_schema, 'event_handlers': { event_name.__name__: handler.__name__ for event_name, handler in self.event_handlers.items() diff --git a/pkg/plugin/installers/github.py b/pkg/plugin/installers/github.py index 77ee7490..ff36cb5b 100644 --- a/pkg/plugin/installers/github.py +++ b/pkg/plugin/installers/github.py @@ -99,9 +99,11 @@ class GitHubRepoInstaller(installer.PluginInstaller): task_context.trace("安装插件依赖...", "install-plugin") await self.install_requirements("plugins/" + repo_label) task_context.trace("完成.", "install-plugin") - await self.ap.plugin_mgr.setting.record_installed_plugin_source( - "plugins/" + repo_label + '/', plugin_source - ) + + # Caution: in the v4.0, plugin without manifest will not be able to be updated + # await self.ap.plugin_mgr.setting.record_installed_plugin_source( + # "plugins/" + repo_label + '/', plugin_source + # ) async def uninstall_plugin( self, diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index 23a107d4..3cbcdbf2 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -133,7 +133,10 @@ class PluginLoader(loader.PluginLoader): """注册事件处理器""" self.ap.logger.debug(f'注册事件处理器 {event.__name__}') def wrapper(func: typing.Callable) -> typing.Callable: - + + if self._current_container is None: # None indicates this plugin is registered through manifest, so ignore it here + return func + self._current_container.event_handlers[event] = func return func @@ -148,6 +151,9 @@ class PluginLoader(loader.PluginLoader): self.ap.logger.debug(f'注册内容函数 {name}') def wrapper(func: typing.Callable) -> typing.Callable: + if self._current_container is None: # None indicates this plugin is registered through manifest, so ignore it here + return func + function_schema = funcschema.get_func_schema(func) function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py index 2f7b83f2..5fd4eea8 100644 --- a/pkg/plugin/loaders/manifest.py +++ b/pkg/plugin/loaders/manifest.py @@ -68,6 +68,8 @@ class PluginManifestLoader(loader.PluginLoader): for plugin_manifest in plugin_manifests: try: + config_schema = plugin_manifest.spec['config'] if 'config' in plugin_manifest.spec else [] + current_plugin_container = context.RuntimeContainer( plugin_name=plugin_manifest.metadata.name, plugin_label=plugin_manifest.metadata.label, @@ -77,6 +79,7 @@ class PluginManifestLoader(loader.PluginLoader): plugin_repository=plugin_manifest.metadata.repository, main_file=os.path.join(plugin_manifest.rel_dir, plugin_manifest.execution.python.path), pkg_path=plugin_manifest.rel_dir, + config_schema=config_schema, event_handlers={}, tools=[], ) diff --git a/pkg/plugin/manager.py b/pkg/plugin/manager.py index cdae7b19..8258e40c 100644 --- a/pkg/plugin/manager.py +++ b/pkg/plugin/manager.py @@ -3,10 +3,13 @@ from __future__ import annotations import typing import traceback +import sqlalchemy + from ..core import app, taskmgr -from . import context, loader, events, installer, setting, models +from . import context, loader, events, installer, models from .loaders import classic, manifest from .installers import github +from ..entity.persistence import plugin as persistence_plugin class PluginManager: @@ -18,8 +21,6 @@ class PluginManager: installer: installer.PluginInstaller - setting: setting.SettingManager - api_host: context.APIHost plugin_containers: list[context.RuntimeContainer] @@ -40,6 +41,18 @@ class PluginManager: plugins = [plugin for plugin in plugins if plugin.status == status] return plugins + + def get_plugin( + self, + author: str, + plugin_name: str, + ) -> context.RuntimeContainer: + """通过作者和插件名获取插件 + """ + for plugin in self.plugins(): + if plugin.plugin_author == author and plugin.plugin_name == plugin_name: + return plugin + return None def __init__(self, ap: app.Application): self.ap = ap @@ -48,7 +61,6 @@ class PluginManager: manifest.PluginManifestLoader(ap), ] self.installer = github.GitHubRepoInstaller(ap) - self.setting = setting.SettingManager(ap) self.api_host = context.APIHost(ap) self.plugin_containers = [] @@ -56,23 +68,73 @@ class PluginManager: for loader in self.loaders: await loader.initialize() await self.installer.initialize() - await self.setting.initialize() await self.api_host.initialize() setattr(models, 'require_ver', self.api_host.require_ver) async def load_plugins(self): + self.ap.logger.info('Loading all plugins...') + for loader in self.loaders: await loader.load_plugins() self.plugin_containers.extend(loader.plugins) - await self.setting.sync_setting(self.plugin_containers) + await self.load_plugin_settings(self.plugin_containers) # 按优先级倒序 self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}') + async def load_plugin_settings( + self, + plugin_containers: list[context.RuntimeContainer] + ): + for plugin_container in plugin_containers: + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_plugin.PluginSetting) \ + .where(persistence_plugin.PluginSetting.plugin_author == plugin_container.plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_container.plugin_name) + ) + + setting = result.first() + + if setting is None: + + new_setting_data = { + 'plugin_author': plugin_container.plugin_author, + 'plugin_name': plugin_container.plugin_name, + 'enabled': plugin_container.enabled, + 'priority': plugin_container.priority, + 'config': plugin_container.plugin_config, + } + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_plugin.PluginSetting).values(**new_setting_data) + ) + continue + else: + plugin_container.enabled = setting.enabled + plugin_container.priority = setting.priority + plugin_container.plugin_config = setting.config + + async def dump_plugin_container_setting( + self, + plugin_container: context.RuntimeContainer + ): + """保存单个插件容器的设置到数据库 + """ + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_container.plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_container.plugin_name) + .values( + enabled=plugin_container.enabled, + priority=plugin_container.priority, + config=plugin_container.plugin_config + ) + ) + async def initialize_plugin(self, plugin: context.RuntimeContainer): self.ap.logger.debug(f'初始化插件 {plugin.plugin_name}') plugin.plugin_inst = plugin.plugin_class(self.api_host) @@ -275,7 +337,7 @@ class PluginManager: plugin.enabled = new_status - await self.setting.dump_container_setting(self.plugin_containers) + await self.dump_plugin_container_setting(self.plugin_containers) break @@ -296,4 +358,4 @@ class PluginManager: self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - await self.setting.dump_container_setting(self.plugin_containers) + await self.dump_plugin_container_setting(self.plugin_containers) diff --git a/pkg/plugin/setting.py b/pkg/plugin/setting.py deleted file mode 100644 index b5c5c06f..00000000 --- a/pkg/plugin/setting.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -from ..core import app -from ..config import manager as cfg_mgr -from . import context - - -class SettingManager: - """插件设置管理器""" - - ap: app.Application - - settings: cfg_mgr.ConfigManager - - def __init__(self, ap: app.Application): - self.ap = ap - - async def initialize(self): - self.settings = self.ap.plugin_setting_meta - - async def sync_setting( - self, - plugin_containers: list[context.RuntimeContainer], - ): - """同步设置 - """ - - not_matched_source_record = [] - - for value in self.settings.data['plugins']: - - if 'name' not in value: # 只有远程地址的,应用到pkg_path相同的插件容器上 - matched = False - - for plugin_container in plugin_containers: - if plugin_container.pkg_path == value['pkg_path']: - matched = True - - plugin_container.plugin_repository = value['source'] - break - - if not matched: - not_matched_source_record.append(value) - else: # 正常的插件设置 - for plugin_container in plugin_containers: - if plugin_container.plugin_name == value['name']: - plugin_container.set_from_setting_dict(value) - break - - self.settings.data = { - 'plugins': [ - p.to_setting_dict() - for p in plugin_containers - ] - } - - self.settings.data['plugins'].extend(not_matched_source_record) - - await self.settings.dump_config() - - async def dump_container_setting( - self, - plugin_containers: list[context.RuntimeContainer] - ): - """保存插件容器设置 - """ - - for plugin in plugin_containers: - for ps in self.settings.data['plugins']: - if ps['name'] == plugin.plugin_name: - plugin_dict = plugin.to_setting_dict() - - for key in plugin_dict: - ps[key] = plugin_dict[key] - - break - - await self.settings.dump_config() - - async def record_installed_plugin_source( - self, - pkg_path: str, - source: str - ): - found = False - - for value in self.settings.data['plugins']: - if value['pkg_path'] == pkg_path: - value['source'] = source - found = True - break - - if not found: - - self.settings.data['plugins'].append( - { - 'pkg_path': pkg_path, - 'source': source - } - ) - await self.settings.dump_config() \ No newline at end of file diff --git a/pkg/utils/version.py b/pkg/utils/version.py index 5e5741c6..9a206171 100644 --- a/pkg/utils/version.py +++ b/pkg/utils/version.py @@ -219,7 +219,7 @@ class VersionManager: try: if await self.ap.ver_mgr.is_new_version_available(): - return "有新版本可用,请使用管理员账号发送 !update 命令更新", logging.INFO + return "有新版本可用,根据文档更新:https://docs.langbot.app/deploy/update.html", logging.INFO except Exception as e: return f"检查版本更新时出错: {e}", logging.WARNING