Merge pull request #1290 from RockChinQ/feat/plugin-manifest

feat: discovering plugins by manifests
This commit is contained in:
Junyan Qin (Chin)
2025-04-12 21:29:10 +08:00
committed by GitHub
17 changed files with 371 additions and 192 deletions

View File

@@ -17,3 +17,7 @@ spec:
LLMAPIRequester:
fromDirs:
- path: pkg/provider/modelmgr/requesters/
Plugin:
fromDirs:
- path: plugins/
maxDepth: 2

View File

@@ -44,8 +44,16 @@ class PluginsRouterGroup(group.RouterGroup):
'task_id': wrapper.id
})
@self.route('/<author>/<plugin_name>', methods=['DELETE'], auth_type=group.AuthType.USER_TOKEN)
@self.route('/<author>/<plugin_name>', methods=['GET', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
async def _(author: str, plugin_name: str) -> str:
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),
@@ -59,6 +67,22 @@ class PluginsRouterGroup(group.RouterGroup):
'task_id': wrapper.id
})
@self.route('/<author>/<plugin_name>/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:
data = await quart.request.json

View File

@@ -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

View File

@@ -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",

View File

@@ -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()

View File

@@ -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] = []
def recursive_load_component_manifests_in_dir(path: str, depth: int = 1):
if depth > max_depth:
return
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))
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

View File

@@ -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())

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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:

View File

@@ -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
@@ -140,6 +134,9 @@ 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=[""]))

View File

@@ -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()

View File

@@ -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]
@@ -39,34 +42,103 @@ class PluginManager:
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)

View File

@@ -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()

View File

@@ -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,
},

View File

@@ -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

View File

@@ -1,3 +0,0 @@
{
"plugins": []
}