diff --git a/components.yaml b/components.yaml index f4e2b7bc..fc2084c6 100644 --- a/components.yaml +++ b/components.yaml @@ -17,3 +17,7 @@ spec: LLMAPIRequester: fromDirs: - path: pkg/provider/modelmgr/requesters/ + Plugin: + fromDirs: + - path: plugins/ + maxDepth: 2 diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 00951550..330231c2 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -44,20 +44,44 @@ 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('///config', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN) + async def _(author: str, plugin_name: str) -> quart.Response: + plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) + if plugin is None: + return self.http_status(404, -1, 'plugin not found') + if quart.request.method == 'GET': + return self.success(data={ + 'config': plugin.plugin_config + }) + elif quart.request.method == 'PUT': + data = await quart.request.json + + await self.ap.plugin_mgr.set_plugin_config(plugin, data) + + return self.success(data={}) @self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: diff --git a/pkg/core/app.py b/pkg/core/app.py index e543cd27..fc01d9f3 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -74,8 +74,6 @@ class Application: adapter_qq_botpy_meta: config_mgr.ConfigManager = None - plugin_setting_meta: config_mgr.ConfigManager = None - llm_models_meta: config_mgr.ConfigManager = None instance_secret_meta: config_mgr.ConfigManager = None diff --git a/pkg/core/bootutils/files.py b/pkg/core/bootutils/files.py index c1d077d7..dbaad8ea 100644 --- a/pkg/core/bootutils/files.py +++ b/pkg/core/bootutils/files.py @@ -7,7 +7,6 @@ import sys required_files = { "plugins/__init__.py": "templates/__init__.py", - "plugins/plugins.json": "templates/plugin-settings.json", "data/config/command.json": "templates/command.json", "data/config/pipeline.json": "templates/pipeline.json", "data/config/platform.json": "templates/platform.json", diff --git a/pkg/core/stages/load_config.py b/pkg/core/stages/load_config.py index e7a7cb05..a1767654 100644 --- a/pkg/core/stages/load_config.py +++ b/pkg/core/stages/load_config.py @@ -66,9 +66,6 @@ class LoadConfigStage(stage.BootingStage): doc_link="https://docs.langbot.app/config/function/system.html" ) - ap.plugin_setting_meta = await config.load_json_config("plugins/plugins.json", "templates/plugin-settings.json") - await ap.plugin_setting_meta.dump_config() - ap.sensitive_meta = await config.load_json_config("data/metadata/sensitive-words.json", "templates/metadata/sensitive-words.json") await ap.sensitive_meta.dump_config() diff --git a/pkg/discover/engine.py b/pkg/discover/engine.py index 3960661c..51990a3c 100644 --- a/pkg/discover/engine.py +++ b/pkg/discover/engine.py @@ -34,6 +34,7 @@ class I18nString(pydantic.BaseModel): dic['ja_JP'] = self.ja_JP return dic + class Metadata(pydantic.BaseModel): """元数据""" @@ -46,9 +47,18 @@ class Metadata(pydantic.BaseModel): description: typing.Optional[I18nString] = None """描述""" + version: typing.Optional[str] = None + """版本""" + icon: typing.Optional[str] = None """图标""" + author: typing.Optional[str] = None + """作者""" + + repository: typing.Optional[str] = None + """仓库""" + def __init__(self, **kwargs): super().__init__(**kwargs) @@ -96,6 +106,9 @@ class Component(pydantic.BaseModel): rel_path: str """组件清单相对main.py的路径""" + rel_dir: str + """组件清单相对main.py的目录""" + _metadata: Metadata """组件元数据""" @@ -109,12 +122,18 @@ class Component(pydantic.BaseModel): super().__init__( owner=owner, manifest=manifest, - rel_path=rel_path + rel_path=rel_path, + rel_dir=os.path.dirname(rel_path) ) self._metadata = Metadata(**manifest['metadata']) self._spec = manifest['spec'] self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None + @classmethod + def is_component_manifest(cls, manifest: typing.Dict[str, typing.Any]) -> bool: + """判断是否为组件清单""" + return 'apiVersion' in manifest and 'kind' in manifest and 'metadata' in manifest and 'spec' in manifest + @property def kind(self) -> str: """组件类型""" @@ -132,13 +151,12 @@ class Component(pydantic.BaseModel): @property def execution(self) -> Execution: - """组件执行""" + """组件可执行文件信息""" return self._execution def get_python_component_class(self) -> typing.Type[typing.Any]: """获取Python组件类""" - parent_path = os.path.dirname(self.rel_path) - module_path = os.path.join(parent_path, self.execution.python.path) + module_path = os.path.join(self.rel_dir, self.execution.python.path) if module_path.endswith('.py'): module_path = module_path[:-3] module_path = module_path.replace('/', '.').replace('\\', '.') @@ -168,10 +186,12 @@ class ComponentDiscoveryEngine: def __init__(self, ap: app.Application): self.ap = ap - def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component: + def load_component_manifest(self, path: str, owner: str = 'builtin', no_save: bool = False) -> Component | None: """加载组件清单""" with open(path, 'r', encoding='utf-8') as f: manifest = yaml.safe_load(f) + if not Component.is_component_manifest(manifest): + return None comp = Component( owner=owner, manifest=manifest, @@ -183,12 +203,22 @@ class ComponentDiscoveryEngine: self.components[comp.kind].append(comp) return comp - def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]: + def load_component_manifests_in_dir(self, path: str, owner: str = 'builtin', no_save: bool = False, max_depth: int = 1) -> typing.List[Component]: """加载目录中的组件清单""" components: typing.List[Component] = [] - for file in os.listdir(path): - if file.endswith('.yaml') or file.endswith('.yml'): - components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save)) + + def recursive_load_component_manifests_in_dir(path: str, depth: int = 1): + if depth > max_depth: + return + for file in os.listdir(path): + if (not os.path.isdir(os.path.join(path, file))) and (file.endswith('.yaml') or file.endswith('.yml')): + comp = self.load_component_manifest(os.path.join(path, file), owner, no_save) + if comp is not None: + components.append(comp) + elif os.path.isdir(os.path.join(path, file)): + recursive_load_component_manifests_in_dir(os.path.join(path, file), depth + 1) + + recursive_load_component_manifests_in_dir(path) return components def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]: @@ -196,17 +226,21 @@ class ComponentDiscoveryEngine: components: typing.List[Component] = [] if 'fromFiles' in group: for file in group['fromFiles']: - components.append(self.load_component_manifest(file, owner, no_save)) + comp = self.load_component_manifest(file, owner, no_save) + if comp is not None: + components.append(comp) if 'fromDirs' in group: for dir in group['fromDirs']: path = dir['path'] - # depth = dir['depth'] - components.extend(self.load_component_manifests_in_dir(path, owner, no_save)) + max_depth = dir['maxDepth'] if 'maxDepth' in dir else 1 + components.extend(self.load_component_manifests_in_dir(path, owner, no_save, max_depth)) return components def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'): """发现蓝图""" blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True) + if blueprint_manifest is None: + raise ValueError(f'Invalid blueprint manifest: {blueprint_manifest_path}') assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`' components: typing.Dict[str, typing.List[Component]] = {} @@ -223,9 +257,16 @@ class ComponentDiscoveryEngine: return blueprint_manifest, components - def get_components_by_kind(self, kind: str) -> typing.List[Component]: """获取指定类型的组件""" if kind not in self.components: raise ValueError(f'No components found for kind: {kind}') return self.components[kind] + + def find_components(self, kind: str, component_list: typing.List[Component]) -> typing.List[Component]: + """查找组件""" + result: typing.List[Component] = [] + for component in component_list: + if component.kind == kind: + result.append(component) + return result 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 7a9be2a1..3ff1ffc7 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -8,6 +8,7 @@ import enum from . import events from ..provider.tools import entities as tools_entities from ..core import app +from ..discover import engine as discover_engine from ..platform.types import message as platform_message from ..platform import adapter as platform_adapter @@ -86,9 +87,13 @@ class BasePlugin(metaclass=abc.ABCMeta): ap: app.Application """应用程序对象""" + config: dict + """插件配置""" + def __init__(self, host: APIHost): """初始化阶段被调用""" self.host = host + self.config = {} async def initialize(self): """初始化阶段被调用""" @@ -308,7 +313,10 @@ class RuntimeContainer(pydantic.BaseModel): plugin_name: str """插件名称""" - plugin_description: str + plugin_label: discover_engine.I18nString + """插件标签""" + + plugin_description: discover_engine.I18nString """插件描述""" plugin_version: str @@ -317,7 +325,7 @@ class RuntimeContainer(pydantic.BaseModel): plugin_author: str """插件作者""" - plugin_source: str + plugin_repository: str """插件源码地址""" main_file: str @@ -335,6 +343,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 """插件实例""" @@ -343,7 +357,7 @@ class RuntimeContainer(pydantic.BaseModel): ]] = {} """事件处理器""" - content_functions: list[tools_entities.LLMFunction] = [] + tools: list[tools_entities.LLMFunction] = [] """内容函数""" status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED @@ -355,10 +369,10 @@ class RuntimeContainer(pydantic.BaseModel): def to_setting_dict(self): return { 'name': self.plugin_name, - 'description': self.plugin_description, + 'description': self.plugin_description.to_dict(), 'version': self.plugin_version, 'author': self.plugin_author, - 'source': self.plugin_source, + 'source': self.plugin_repository, 'main_file': self.main_file, 'pkg_path': self.pkg_path, 'priority': self.priority, @@ -369,26 +383,28 @@ class RuntimeContainer(pydantic.BaseModel): self, setting: dict ): - self.plugin_source = setting['source'] + self.plugin_repository = setting['source'] self.priority = setting['priority'] self.enabled = setting['enabled'] def model_dump(self, *args, **kwargs): return { 'name': self.plugin_name, - 'description': self.plugin_description, + 'label': self.plugin_label.to_dict(), + 'description': self.plugin_description.to_dict(), 'version': self.plugin_version, 'author': self.plugin_author, - 'source': self.plugin_source, + 'repository': self.plugin_repository, 'main_file': self.main_file, '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() }, - 'content_functions': [ + 'tools': [ { 'name': function.name, 'human_desc': function.human_desc, @@ -396,7 +412,7 @@ class RuntimeContainer(pydantic.BaseModel): 'parameters': function.parameters, 'func': function.func.__name__, } - for function in self.content_functions + for function in self.tools ], 'status': self.status.value, } diff --git a/pkg/plugin/installers/github.py b/pkg/plugin/installers/github.py index 039ff196..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, @@ -129,8 +131,8 @@ class GitHubRepoInstaller(installer.PluginInstaller): if plugin_container is None: raise errors.PluginInstallerError('插件不存在或未成功加载') else: - if plugin_container.plugin_source: - plugin_source = plugin_container.plugin_source + if plugin_container.plugin_repository: + plugin_source = plugin_container.plugin_repository task_context.trace("转交安装任务.", "update-plugin") await self.install_plugin(plugin_source, task_context) else: diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index b3710c9e..3cbcdbf2 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -9,7 +9,7 @@ from .. import loader, events, context, models from ...core import entities as core_entities from ...provider.tools import entities as tools_entities from ...utils import funcschema - +from ...discover import engine as discover_engine class PluginLoader(loader.PluginLoader): """加载 plugins/ 目录下的插件""" @@ -31,13 +31,6 @@ class PluginLoader(loader.PluginLoader): async def initialize(self): """初始化""" - setattr(models, 'register', self.register) - setattr(models, 'on', self.on) - setattr(models, 'func', self.func) - - setattr(context, 'register', self.register) - setattr(context, 'handler', self.handler) - setattr(context, 'llm_func', self.llm_func) def register( self, @@ -49,14 +42,15 @@ class PluginLoader(loader.PluginLoader): self.ap.logger.debug(f'注册插件 {name} {version} by {author}') container = context.RuntimeContainer( plugin_name=name, - plugin_description=description, + plugin_label=discover_engine.I18nString(en_US=name, zh_CN=name), + plugin_description=discover_engine.I18nString(en_US=description, zh_CN=description), plugin_version=version, plugin_author=author, - plugin_source='', + plugin_repository='', pkg_path=self._current_pkg_path, main_file=self._current_module_path, event_handlers={}, - content_functions=[], + tools=[], ) self._current_container = container @@ -126,7 +120,7 @@ class PluginLoader(loader.PluginLoader): func=handler, ) - self._current_container.content_functions.append(llm_function) + self._current_container.tools.append(llm_function) return func @@ -139,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 @@ -154,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) @@ -165,7 +165,7 @@ class PluginLoader(loader.PluginLoader): func=func, ) - self._current_container.content_functions.append(llm_function) + self._current_container.tools.append(llm_function) return func @@ -205,4 +205,11 @@ class PluginLoader(loader.PluginLoader): async def load_plugins(self): """加载插件 """ + setattr(models, 'register', self.register) + setattr(models, 'on', self.on) + setattr(models, 'func', self.func) + + setattr(context, 'register', self.register) + setattr(context, 'handler', self.handler) + setattr(context, 'llm_func', self.llm_func) await self._walk_plugin_path(__import__("plugins", fromlist=[""])) diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py new file mode 100644 index 00000000..101fdb3a --- /dev/null +++ b/pkg/plugin/loaders/manifest.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import typing +import abc +import os +import traceback + +from ...core import app +from .. import context, events, models +from .. import loader +from ...utils import funcschema +from ...provider.tools import entities as tools_entities + + +class PluginManifestLoader(loader.PluginLoader): + """通过插件清单发现插件""" + + _current_container: context.RuntimeContainer = None + + def __init__(self, ap: app.Application): + super().__init__(ap) + + def handler( + self, + event: typing.Type[events.BaseEventModel] + ) -> typing.Callable[[typing.Callable], typing.Callable]: + """注册事件处理器""" + self.ap.logger.debug(f'注册事件处理器 {event.__name__}') + def wrapper(func: typing.Callable) -> typing.Callable: + + self._current_container.event_handlers[event] = func + + return func + + return wrapper + + def llm_func( + self, + name: str=None, + ) -> typing.Callable: + """注册内容函数""" + self.ap.logger.debug(f'注册内容函数 {name}') + def wrapper(func: typing.Callable) -> typing.Callable: + + function_schema = funcschema.get_func_schema(func) + function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) + + llm_function = tools_entities.LLMFunction( + name=function_name, + human_desc='', + description=function_schema['description'], + parameters=function_schema['parameters'], + func=func, + ) + + self._current_container.tools.append(llm_function) + + return func + + return wrapper + + async def load_plugins(self): + """加载插件""" + setattr(context, 'handler', self.handler) + setattr(context, 'llm_func', self.llm_func) + + plugin_manifests = self.ap.discover.get_components_by_kind('Plugin') + + 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, + plugin_description=plugin_manifest.metadata.description, + plugin_version=plugin_manifest.metadata.version, + plugin_author=plugin_manifest.metadata.author, + 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=[], + ) + + self._current_container = current_plugin_container + + # extract the plugin class + # this step will load the plugin module, + # so the event handlers and tools will be registered + plugin_class = plugin_manifest.get_python_component_class() + current_plugin_container.plugin_class = plugin_class + + # TODO load component extensions + + self.plugins.append(current_plugin_container) + except Exception as e: + self.ap.logger.error(f'加载插件 {plugin_manifest.metadata.name} 时发生错误') + traceback.print_exc() diff --git a/pkg/plugin/manager.py b/pkg/plugin/manager.py index 2b8e887d..042fb909 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 .loaders import classic +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: @@ -14,14 +17,14 @@ class PluginManager: ap: app.Application - loader: loader.PluginLoader + loaders: list[loader.PluginLoader] installer: installer.PluginInstaller - setting: setting.SettingManager - api_host: context.APIHost + plugin_containers: list[context.RuntimeContainer] + def plugins( self, enabled: bool=None, @@ -29,7 +32,7 @@ class PluginManager: ) -> list[context.RuntimeContainer]: """获取插件列表 """ - plugins = self.loader.plugins + plugins = self.plugin_containers if enabled is not None: plugins = [plugin for plugin in plugins if plugin.enabled == enabled] @@ -38,35 +41,104 @@ 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 - self.loader = classic.PluginLoader(ap) + self.loaders = [ + classic.PluginLoader(ap), + manifest.PluginManifestLoader(ap), + ] self.installer = github.GitHubRepoInstaller(ap) - self.setting = setting.SettingManager(ap) self.api_host = context.APIHost(ap) + self.plugin_containers = [] async def initialize(self): - await self.loader.initialize() + 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): - await self.loader.load_plugins() + self.ap.logger.info('Loading all plugins...') - await self.setting.sync_setting(self.loader.plugins) + for loader in self.loaders: + await loader.load_plugins() + self.plugin_containers.extend(loader.plugins) + + await self.load_plugin_settings(self.plugin_containers) # 按优先级倒序 - self.loader.plugins.sort(key=lambda x: x.priority, reverse=True) + self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - self.ap.logger.debug(f'优先级排序后的插件列表 {self.loader.plugins}') + 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) + plugin.plugin_inst.config = plugin.plugin_config plugin.plugin_inst.ap = self.ap plugin.plugin_inst.host = self.api_host await plugin.plugin_inst.initialize() @@ -147,7 +219,7 @@ class PluginManager: await self.ap.ctr_mgr.plugin.post_remove_record( { "name": plugin_name, - "remote": plugin_container.plugin_source, + "remote": plugin_container.plugin_repository, "author": plugin_container.plugin_author, "version": plugin_container.plugin_version } @@ -171,7 +243,7 @@ class PluginManager: await self.ap.ctr_mgr.plugin.post_update_record( plugin={ "name": plugin_name, - "remote": plugin_container.plugin_source, + "remote": plugin_container.plugin_repository, "author": plugin_container.plugin_author, "version": plugin_container.plugin_version }, @@ -238,7 +310,7 @@ class PluginManager: plugins_info: list[dict] = [ { 'name': plugin.plugin_name, - 'remote': plugin.plugin_source, + 'remote': plugin.plugin_repository, 'version': plugin.plugin_version, 'author': plugin.plugin_author } for plugin in emitted_plugins @@ -266,7 +338,7 @@ class PluginManager: plugin.enabled = new_status - await self.setting.dump_container_setting(self.loader.plugins) + await self.dump_plugin_container_setting(self.plugin_containers) break @@ -280,11 +352,18 @@ class PluginManager: plugin_name = plugin.get('name') plugin_priority = plugin.get('priority') - for plugin in self.loader.plugins: + for plugin in self.plugin_containers: if plugin.plugin_name == plugin_name: plugin.priority = plugin_priority break - self.loader.plugins.sort(key=lambda x: x.priority, reverse=True) + self.plugin_containers.sort(key=lambda x: x.priority, reverse=True) - await self.setting.dump_container_setting(self.loader.plugins) + await self.dump_plugin_container_setting(self.plugin_containers) + + async def set_plugin_config(self, plugin_container: context.RuntimeContainer, new_config: dict): + plugin_container.plugin_config = new_config + + plugin_container.plugin_inst.config = new_config + + await self.dump_plugin_container_setting(plugin_container) diff --git a/pkg/plugin/setting.py b/pkg/plugin/setting.py deleted file mode 100644 index bd50603f..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_source = 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/provider/tools/loaders/plugin.py b/pkg/provider/tools/loaders/plugin.py index 08211334..5b964556 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -23,7 +23,7 @@ class PluginToolLoader(loader.ToolLoader): for plugin in self.ap.plugin_mgr.plugins( enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - all_functions.extend(plugin.content_functions) + all_functions.extend(plugin.tools) return all_functions @@ -32,7 +32,7 @@ class PluginToolLoader(loader.ToolLoader): for plugin in self.ap.plugin_mgr.plugins( enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - for function in plugin.content_functions: + for function in plugin.tools: if function.name == name: return True return False @@ -44,7 +44,7 @@ class PluginToolLoader(loader.ToolLoader): for plugin in self.ap.plugin_mgr.plugins( enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED ): - for function in plugin.content_functions: + for function in plugin.tools: if function.name == name: return function, plugin.plugin_inst return None, None @@ -70,7 +70,7 @@ class PluginToolLoader(loader.ToolLoader): plugin = None for p in self.ap.plugin_mgr.plugins(): - if function in p.content_functions: + if function in p.tools: plugin = p break @@ -79,7 +79,7 @@ class PluginToolLoader(loader.ToolLoader): await self.ap.ctr_mgr.usage.post_function_record( plugin={ "name": plugin.plugin_name, - "remote": plugin.plugin_source, + "remote": plugin.plugin_repository, "version": plugin.plugin_version, "author": plugin.plugin_author, }, 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 diff --git a/templates/plugin-settings.json b/templates/plugin-settings.json deleted file mode 100644 index 1d807ed1..00000000 --- a/templates/plugin-settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": [] -} \ No newline at end of file