mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-21 01:40:58 +00:00
Merge pull request #1290 from RockChinQ/feat/plugin-manifest
feat: discovering plugins by manifests
This commit is contained in:
@@ -17,3 +17,7 @@ spec:
|
|||||||
LLMAPIRequester:
|
LLMAPIRequester:
|
||||||
fromDirs:
|
fromDirs:
|
||||||
- path: pkg/provider/modelmgr/requesters/
|
- path: pkg/provider/modelmgr/requesters/
|
||||||
|
Plugin:
|
||||||
|
fromDirs:
|
||||||
|
- path: plugins/
|
||||||
|
maxDepth: 2
|
||||||
|
|||||||
@@ -44,20 +44,44 @@ class PluginsRouterGroup(group.RouterGroup):
|
|||||||
'task_id': wrapper.id
|
'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:
|
async def _(author: str, plugin_name: str) -> str:
|
||||||
ctx = taskmgr.TaskContext.new()
|
if quart.request.method == 'GET':
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name)
|
||||||
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx),
|
if plugin is None:
|
||||||
kind="plugin-operation",
|
return self.http_status(404, -1, 'plugin not found')
|
||||||
name=f'plugin-remove-{plugin_name}',
|
return self.success(data={
|
||||||
label=f'删除插件 {plugin_name}',
|
'plugin': plugin.model_dump()
|
||||||
context=ctx
|
})
|
||||||
)
|
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={
|
return self.success(data={
|
||||||
'task_id': wrapper.id
|
'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)
|
@self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ class Application:
|
|||||||
|
|
||||||
adapter_qq_botpy_meta: config_mgr.ConfigManager = None
|
adapter_qq_botpy_meta: config_mgr.ConfigManager = None
|
||||||
|
|
||||||
plugin_setting_meta: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
llm_models_meta: config_mgr.ConfigManager = None
|
llm_models_meta: config_mgr.ConfigManager = None
|
||||||
|
|
||||||
instance_secret_meta: config_mgr.ConfigManager = None
|
instance_secret_meta: config_mgr.ConfigManager = None
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import sys
|
|||||||
|
|
||||||
required_files = {
|
required_files = {
|
||||||
"plugins/__init__.py": "templates/__init__.py",
|
"plugins/__init__.py": "templates/__init__.py",
|
||||||
"plugins/plugins.json": "templates/plugin-settings.json",
|
|
||||||
"data/config/command.json": "templates/command.json",
|
"data/config/command.json": "templates/command.json",
|
||||||
"data/config/pipeline.json": "templates/pipeline.json",
|
"data/config/pipeline.json": "templates/pipeline.json",
|
||||||
"data/config/platform.json": "templates/platform.json",
|
"data/config/platform.json": "templates/platform.json",
|
||||||
|
|||||||
@@ -66,9 +66,6 @@ class LoadConfigStage(stage.BootingStage):
|
|||||||
doc_link="https://docs.langbot.app/config/function/system.html"
|
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")
|
ap.sensitive_meta = await config.load_json_config("data/metadata/sensitive-words.json", "templates/metadata/sensitive-words.json")
|
||||||
await ap.sensitive_meta.dump_config()
|
await ap.sensitive_meta.dump_config()
|
||||||
|
|
||||||
|
|||||||
+54
-13
@@ -34,6 +34,7 @@ class I18nString(pydantic.BaseModel):
|
|||||||
dic['ja_JP'] = self.ja_JP
|
dic['ja_JP'] = self.ja_JP
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
||||||
class Metadata(pydantic.BaseModel):
|
class Metadata(pydantic.BaseModel):
|
||||||
"""元数据"""
|
"""元数据"""
|
||||||
|
|
||||||
@@ -46,9 +47,18 @@ class Metadata(pydantic.BaseModel):
|
|||||||
description: typing.Optional[I18nString] = None
|
description: typing.Optional[I18nString] = None
|
||||||
"""描述"""
|
"""描述"""
|
||||||
|
|
||||||
|
version: typing.Optional[str] = None
|
||||||
|
"""版本"""
|
||||||
|
|
||||||
icon: typing.Optional[str] = None
|
icon: typing.Optional[str] = None
|
||||||
"""图标"""
|
"""图标"""
|
||||||
|
|
||||||
|
author: typing.Optional[str] = None
|
||||||
|
"""作者"""
|
||||||
|
|
||||||
|
repository: typing.Optional[str] = None
|
||||||
|
"""仓库"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@@ -96,6 +106,9 @@ class Component(pydantic.BaseModel):
|
|||||||
rel_path: str
|
rel_path: str
|
||||||
"""组件清单相对main.py的路径"""
|
"""组件清单相对main.py的路径"""
|
||||||
|
|
||||||
|
rel_dir: str
|
||||||
|
"""组件清单相对main.py的目录"""
|
||||||
|
|
||||||
_metadata: Metadata
|
_metadata: Metadata
|
||||||
"""组件元数据"""
|
"""组件元数据"""
|
||||||
|
|
||||||
@@ -109,12 +122,18 @@ class Component(pydantic.BaseModel):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
rel_path=rel_path
|
rel_path=rel_path,
|
||||||
|
rel_dir=os.path.dirname(rel_path)
|
||||||
)
|
)
|
||||||
self._metadata = Metadata(**manifest['metadata'])
|
self._metadata = Metadata(**manifest['metadata'])
|
||||||
self._spec = manifest['spec']
|
self._spec = manifest['spec']
|
||||||
self._execution = Execution(**manifest['execution']) if 'execution' in manifest else None
|
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
|
@property
|
||||||
def kind(self) -> str:
|
def kind(self) -> str:
|
||||||
"""组件类型"""
|
"""组件类型"""
|
||||||
@@ -132,13 +151,12 @@ class Component(pydantic.BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def execution(self) -> Execution:
|
def execution(self) -> Execution:
|
||||||
"""组件执行"""
|
"""组件可执行文件信息"""
|
||||||
return self._execution
|
return self._execution
|
||||||
|
|
||||||
def get_python_component_class(self) -> typing.Type[typing.Any]:
|
def get_python_component_class(self) -> typing.Type[typing.Any]:
|
||||||
"""获取Python组件类"""
|
"""获取Python组件类"""
|
||||||
parent_path = os.path.dirname(self.rel_path)
|
module_path = os.path.join(self.rel_dir, self.execution.python.path)
|
||||||
module_path = os.path.join(parent_path, self.execution.python.path)
|
|
||||||
if module_path.endswith('.py'):
|
if module_path.endswith('.py'):
|
||||||
module_path = module_path[:-3]
|
module_path = module_path[:-3]
|
||||||
module_path = module_path.replace('/', '.').replace('\\', '.')
|
module_path = module_path.replace('/', '.').replace('\\', '.')
|
||||||
@@ -168,10 +186,12 @@ class ComponentDiscoveryEngine:
|
|||||||
def __init__(self, ap: app.Application):
|
def __init__(self, ap: app.Application):
|
||||||
self.ap = ap
|
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:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
manifest = yaml.safe_load(f)
|
manifest = yaml.safe_load(f)
|
||||||
|
if not Component.is_component_manifest(manifest):
|
||||||
|
return None
|
||||||
comp = Component(
|
comp = Component(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
@@ -183,12 +203,22 @@ class ComponentDiscoveryEngine:
|
|||||||
self.components[comp.kind].append(comp)
|
self.components[comp.kind].append(comp)
|
||||||
return 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] = []
|
components: typing.List[Component] = []
|
||||||
for file in os.listdir(path):
|
|
||||||
if file.endswith('.yaml') or file.endswith('.yml'):
|
def recursive_load_component_manifests_in_dir(path: str, depth: int = 1):
|
||||||
components.append(self.load_component_manifest(os.path.join(path, file), owner, no_save))
|
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
|
return components
|
||||||
|
|
||||||
def load_blueprint_comp_group(self, group: dict, owner: str = 'builtin', no_save: bool = False) -> typing.List[Component]:
|
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] = []
|
components: typing.List[Component] = []
|
||||||
if 'fromFiles' in group:
|
if 'fromFiles' in group:
|
||||||
for file in group['fromFiles']:
|
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:
|
if 'fromDirs' in group:
|
||||||
for dir in group['fromDirs']:
|
for dir in group['fromDirs']:
|
||||||
path = dir['path']
|
path = dir['path']
|
||||||
# depth = dir['depth']
|
max_depth = dir['maxDepth'] if 'maxDepth' in dir else 1
|
||||||
components.extend(self.load_component_manifests_in_dir(path, owner, no_save))
|
components.extend(self.load_component_manifests_in_dir(path, owner, no_save, max_depth))
|
||||||
return components
|
return components
|
||||||
|
|
||||||
def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'):
|
def discover_blueprint(self, blueprint_manifest_path: str, owner: str = 'builtin'):
|
||||||
"""发现蓝图"""
|
"""发现蓝图"""
|
||||||
blueprint_manifest = self.load_component_manifest(blueprint_manifest_path, owner, no_save=True)
|
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`'
|
assert blueprint_manifest.kind == 'Blueprint', '`Kind` must be `Blueprint`'
|
||||||
components: typing.Dict[str, typing.List[Component]] = {}
|
components: typing.Dict[str, typing.List[Component]] = {}
|
||||||
|
|
||||||
@@ -223,9 +257,16 @@ class ComponentDiscoveryEngine:
|
|||||||
|
|
||||||
return blueprint_manifest, components
|
return blueprint_manifest, components
|
||||||
|
|
||||||
|
|
||||||
def get_components_by_kind(self, kind: str) -> typing.List[Component]:
|
def get_components_by_kind(self, kind: str) -> typing.List[Component]:
|
||||||
"""获取指定类型的组件"""
|
"""获取指定类型的组件"""
|
||||||
if kind not in self.components:
|
if kind not in self.components:
|
||||||
raise ValueError(f'No components found for kind: {kind}')
|
raise ValueError(f'No components found for kind: {kind}')
|
||||||
return self.components[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
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -8,7 +8,7 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from . import database
|
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 ..core import app
|
||||||
from .databases import sqlite
|
from .databases import sqlite
|
||||||
|
|
||||||
|
|||||||
+26
-10
@@ -8,6 +8,7 @@ import enum
|
|||||||
from . import events
|
from . import events
|
||||||
from ..provider.tools import entities as tools_entities
|
from ..provider.tools import entities as tools_entities
|
||||||
from ..core import app
|
from ..core import app
|
||||||
|
from ..discover import engine as discover_engine
|
||||||
from ..platform.types import message as platform_message
|
from ..platform.types import message as platform_message
|
||||||
from ..platform import adapter as platform_adapter
|
from ..platform import adapter as platform_adapter
|
||||||
|
|
||||||
@@ -86,9 +87,13 @@ class BasePlugin(metaclass=abc.ABCMeta):
|
|||||||
ap: app.Application
|
ap: app.Application
|
||||||
"""应用程序对象"""
|
"""应用程序对象"""
|
||||||
|
|
||||||
|
config: dict
|
||||||
|
"""插件配置"""
|
||||||
|
|
||||||
def __init__(self, host: APIHost):
|
def __init__(self, host: APIHost):
|
||||||
"""初始化阶段被调用"""
|
"""初始化阶段被调用"""
|
||||||
self.host = host
|
self.host = host
|
||||||
|
self.config = {}
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""初始化阶段被调用"""
|
"""初始化阶段被调用"""
|
||||||
@@ -308,7 +313,10 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
plugin_name: str
|
plugin_name: str
|
||||||
"""插件名称"""
|
"""插件名称"""
|
||||||
|
|
||||||
plugin_description: str
|
plugin_label: discover_engine.I18nString
|
||||||
|
"""插件标签"""
|
||||||
|
|
||||||
|
plugin_description: discover_engine.I18nString
|
||||||
"""插件描述"""
|
"""插件描述"""
|
||||||
|
|
||||||
plugin_version: str
|
plugin_version: str
|
||||||
@@ -317,7 +325,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
plugin_author: str
|
plugin_author: str
|
||||||
"""插件作者"""
|
"""插件作者"""
|
||||||
|
|
||||||
plugin_source: str
|
plugin_repository: str
|
||||||
"""插件源码地址"""
|
"""插件源码地址"""
|
||||||
|
|
||||||
main_file: str
|
main_file: str
|
||||||
@@ -335,6 +343,12 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
priority: typing.Optional[int] = 0
|
priority: typing.Optional[int] = 0
|
||||||
"""优先级"""
|
"""优先级"""
|
||||||
|
|
||||||
|
config_schema: typing.Optional[list[dict]] = []
|
||||||
|
"""插件配置模板"""
|
||||||
|
|
||||||
|
plugin_config: typing.Optional[dict] = {}
|
||||||
|
"""插件配置"""
|
||||||
|
|
||||||
plugin_inst: typing.Optional[BasePlugin] = None
|
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
|
status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED
|
||||||
@@ -355,10 +369,10 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
def to_setting_dict(self):
|
def to_setting_dict(self):
|
||||||
return {
|
return {
|
||||||
'name': self.plugin_name,
|
'name': self.plugin_name,
|
||||||
'description': self.plugin_description,
|
'description': self.plugin_description.to_dict(),
|
||||||
'version': self.plugin_version,
|
'version': self.plugin_version,
|
||||||
'author': self.plugin_author,
|
'author': self.plugin_author,
|
||||||
'source': self.plugin_source,
|
'source': self.plugin_repository,
|
||||||
'main_file': self.main_file,
|
'main_file': self.main_file,
|
||||||
'pkg_path': self.pkg_path,
|
'pkg_path': self.pkg_path,
|
||||||
'priority': self.priority,
|
'priority': self.priority,
|
||||||
@@ -369,26 +383,28 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
self,
|
self,
|
||||||
setting: dict
|
setting: dict
|
||||||
):
|
):
|
||||||
self.plugin_source = setting['source']
|
self.plugin_repository = setting['source']
|
||||||
self.priority = setting['priority']
|
self.priority = setting['priority']
|
||||||
self.enabled = setting['enabled']
|
self.enabled = setting['enabled']
|
||||||
|
|
||||||
def model_dump(self, *args, **kwargs):
|
def model_dump(self, *args, **kwargs):
|
||||||
return {
|
return {
|
||||||
'name': self.plugin_name,
|
'name': self.plugin_name,
|
||||||
'description': self.plugin_description,
|
'label': self.plugin_label.to_dict(),
|
||||||
|
'description': self.plugin_description.to_dict(),
|
||||||
'version': self.plugin_version,
|
'version': self.plugin_version,
|
||||||
'author': self.plugin_author,
|
'author': self.plugin_author,
|
||||||
'source': self.plugin_source,
|
'repository': self.plugin_repository,
|
||||||
'main_file': self.main_file,
|
'main_file': self.main_file,
|
||||||
'pkg_path': self.pkg_path,
|
'pkg_path': self.pkg_path,
|
||||||
'enabled': self.enabled,
|
'enabled': self.enabled,
|
||||||
'priority': self.priority,
|
'priority': self.priority,
|
||||||
|
"config_schema": self.config_schema,
|
||||||
'event_handlers': {
|
'event_handlers': {
|
||||||
event_name.__name__: handler.__name__
|
event_name.__name__: handler.__name__
|
||||||
for event_name, handler in self.event_handlers.items()
|
for event_name, handler in self.event_handlers.items()
|
||||||
},
|
},
|
||||||
'content_functions': [
|
'tools': [
|
||||||
{
|
{
|
||||||
'name': function.name,
|
'name': function.name,
|
||||||
'human_desc': function.human_desc,
|
'human_desc': function.human_desc,
|
||||||
@@ -396,7 +412,7 @@ class RuntimeContainer(pydantic.BaseModel):
|
|||||||
'parameters': function.parameters,
|
'parameters': function.parameters,
|
||||||
'func': function.func.__name__,
|
'func': function.func.__name__,
|
||||||
}
|
}
|
||||||
for function in self.content_functions
|
for function in self.tools
|
||||||
],
|
],
|
||||||
'status': self.status.value,
|
'status': self.status.value,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ class GitHubRepoInstaller(installer.PluginInstaller):
|
|||||||
task_context.trace("安装插件依赖...", "install-plugin")
|
task_context.trace("安装插件依赖...", "install-plugin")
|
||||||
await self.install_requirements("plugins/" + repo_label)
|
await self.install_requirements("plugins/" + repo_label)
|
||||||
task_context.trace("完成.", "install-plugin")
|
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(
|
async def uninstall_plugin(
|
||||||
self,
|
self,
|
||||||
@@ -129,8 +131,8 @@ class GitHubRepoInstaller(installer.PluginInstaller):
|
|||||||
if plugin_container is None:
|
if plugin_container is None:
|
||||||
raise errors.PluginInstallerError('插件不存在或未成功加载')
|
raise errors.PluginInstallerError('插件不存在或未成功加载')
|
||||||
else:
|
else:
|
||||||
if plugin_container.plugin_source:
|
if plugin_container.plugin_repository:
|
||||||
plugin_source = plugin_container.plugin_source
|
plugin_source = plugin_container.plugin_repository
|
||||||
task_context.trace("转交安装任务.", "update-plugin")
|
task_context.trace("转交安装任务.", "update-plugin")
|
||||||
await self.install_plugin(plugin_source, task_context)
|
await self.install_plugin(plugin_source, task_context)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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
|
||||||
|
from ...discover import engine as discover_engine
|
||||||
|
|
||||||
class PluginLoader(loader.PluginLoader):
|
class PluginLoader(loader.PluginLoader):
|
||||||
"""加载 plugins/ 目录下的插件"""
|
"""加载 plugins/ 目录下的插件"""
|
||||||
@@ -31,13 +31,6 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
|
|
||||||
async def initialize(self):
|
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(
|
def register(
|
||||||
self,
|
self,
|
||||||
@@ -49,14 +42,15 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
self.ap.logger.debug(f'注册插件 {name} {version} by {author}')
|
self.ap.logger.debug(f'注册插件 {name} {version} by {author}')
|
||||||
container = context.RuntimeContainer(
|
container = context.RuntimeContainer(
|
||||||
plugin_name=name,
|
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_version=version,
|
||||||
plugin_author=author,
|
plugin_author=author,
|
||||||
plugin_source='',
|
plugin_repository='',
|
||||||
pkg_path=self._current_pkg_path,
|
pkg_path=self._current_pkg_path,
|
||||||
main_file=self._current_module_path,
|
main_file=self._current_module_path,
|
||||||
event_handlers={},
|
event_handlers={},
|
||||||
content_functions=[],
|
tools=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
self._current_container = container
|
self._current_container = container
|
||||||
@@ -126,7 +120,7 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
func=handler,
|
func=handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._current_container.content_functions.append(llm_function)
|
self._current_container.tools.append(llm_function)
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -139,7 +133,10 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
"""注册事件处理器"""
|
"""注册事件处理器"""
|
||||||
self.ap.logger.debug(f'注册事件处理器 {event.__name__}')
|
self.ap.logger.debug(f'注册事件处理器 {event.__name__}')
|
||||||
def wrapper(func: typing.Callable) -> typing.Callable:
|
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
|
self._current_container.event_handlers[event] = func
|
||||||
|
|
||||||
return func
|
return func
|
||||||
@@ -154,6 +151,9 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
self.ap.logger.debug(f'注册内容函数 {name}')
|
self.ap.logger.debug(f'注册内容函数 {name}')
|
||||||
def wrapper(func: typing.Callable) -> typing.Callable:
|
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_schema = funcschema.get_func_schema(func)
|
||||||
function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name)
|
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,
|
func=func,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._current_container.content_functions.append(llm_function)
|
self._current_container.tools.append(llm_function)
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -205,4 +205,11 @@ class PluginLoader(loader.PluginLoader):
|
|||||||
async def load_plugins(self):
|
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=[""]))
|
await self._walk_plugin_path(__import__("plugins", fromlist=[""]))
|
||||||
|
|||||||
@@ -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()
|
||||||
+100
-21
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|||||||
import typing
|
import typing
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
from ..core import app, taskmgr
|
from ..core import app, taskmgr
|
||||||
from . import context, loader, events, installer, setting, models
|
from . import context, loader, events, installer, models
|
||||||
from .loaders import classic
|
from .loaders import classic, manifest
|
||||||
from .installers import github
|
from .installers import github
|
||||||
|
from ..entity.persistence import plugin as persistence_plugin
|
||||||
|
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
@@ -14,14 +17,14 @@ class PluginManager:
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
loader: loader.PluginLoader
|
loaders: list[loader.PluginLoader]
|
||||||
|
|
||||||
installer: installer.PluginInstaller
|
installer: installer.PluginInstaller
|
||||||
|
|
||||||
setting: setting.SettingManager
|
|
||||||
|
|
||||||
api_host: context.APIHost
|
api_host: context.APIHost
|
||||||
|
|
||||||
|
plugin_containers: list[context.RuntimeContainer]
|
||||||
|
|
||||||
def plugins(
|
def plugins(
|
||||||
self,
|
self,
|
||||||
enabled: bool=None,
|
enabled: bool=None,
|
||||||
@@ -29,7 +32,7 @@ class PluginManager:
|
|||||||
) -> list[context.RuntimeContainer]:
|
) -> list[context.RuntimeContainer]:
|
||||||
"""获取插件列表
|
"""获取插件列表
|
||||||
"""
|
"""
|
||||||
plugins = self.loader.plugins
|
plugins = self.plugin_containers
|
||||||
|
|
||||||
if enabled is not None:
|
if enabled is not None:
|
||||||
plugins = [plugin for plugin in plugins if plugin.enabled == enabled]
|
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]
|
plugins = [plugin for plugin in plugins if plugin.status == status]
|
||||||
|
|
||||||
return plugins
|
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):
|
def __init__(self, ap: app.Application):
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
self.loader = classic.PluginLoader(ap)
|
self.loaders = [
|
||||||
|
classic.PluginLoader(ap),
|
||||||
|
manifest.PluginManifestLoader(ap),
|
||||||
|
]
|
||||||
self.installer = github.GitHubRepoInstaller(ap)
|
self.installer = github.GitHubRepoInstaller(ap)
|
||||||
self.setting = setting.SettingManager(ap)
|
|
||||||
self.api_host = context.APIHost(ap)
|
self.api_host = context.APIHost(ap)
|
||||||
|
self.plugin_containers = []
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
await self.loader.initialize()
|
for loader in self.loaders:
|
||||||
|
await loader.initialize()
|
||||||
await self.installer.initialize()
|
await self.installer.initialize()
|
||||||
await self.setting.initialize()
|
|
||||||
await self.api_host.initialize()
|
await self.api_host.initialize()
|
||||||
|
|
||||||
setattr(models, 'require_ver', self.api_host.require_ver)
|
setattr(models, 'require_ver', self.api_host.require_ver)
|
||||||
|
|
||||||
async def load_plugins(self):
|
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):
|
async def initialize_plugin(self, plugin: context.RuntimeContainer):
|
||||||
self.ap.logger.debug(f'初始化插件 {plugin.plugin_name}')
|
self.ap.logger.debug(f'初始化插件 {plugin.plugin_name}')
|
||||||
plugin.plugin_inst = plugin.plugin_class(self.api_host)
|
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.ap = self.ap
|
||||||
plugin.plugin_inst.host = self.api_host
|
plugin.plugin_inst.host = self.api_host
|
||||||
await plugin.plugin_inst.initialize()
|
await plugin.plugin_inst.initialize()
|
||||||
@@ -147,7 +219,7 @@ class PluginManager:
|
|||||||
await self.ap.ctr_mgr.plugin.post_remove_record(
|
await self.ap.ctr_mgr.plugin.post_remove_record(
|
||||||
{
|
{
|
||||||
"name": plugin_name,
|
"name": plugin_name,
|
||||||
"remote": plugin_container.plugin_source,
|
"remote": plugin_container.plugin_repository,
|
||||||
"author": plugin_container.plugin_author,
|
"author": plugin_container.plugin_author,
|
||||||
"version": plugin_container.plugin_version
|
"version": plugin_container.plugin_version
|
||||||
}
|
}
|
||||||
@@ -171,7 +243,7 @@ class PluginManager:
|
|||||||
await self.ap.ctr_mgr.plugin.post_update_record(
|
await self.ap.ctr_mgr.plugin.post_update_record(
|
||||||
plugin={
|
plugin={
|
||||||
"name": plugin_name,
|
"name": plugin_name,
|
||||||
"remote": plugin_container.plugin_source,
|
"remote": plugin_container.plugin_repository,
|
||||||
"author": plugin_container.plugin_author,
|
"author": plugin_container.plugin_author,
|
||||||
"version": plugin_container.plugin_version
|
"version": plugin_container.plugin_version
|
||||||
},
|
},
|
||||||
@@ -238,7 +310,7 @@ class PluginManager:
|
|||||||
plugins_info: list[dict] = [
|
plugins_info: list[dict] = [
|
||||||
{
|
{
|
||||||
'name': plugin.plugin_name,
|
'name': plugin.plugin_name,
|
||||||
'remote': plugin.plugin_source,
|
'remote': plugin.plugin_repository,
|
||||||
'version': plugin.plugin_version,
|
'version': plugin.plugin_version,
|
||||||
'author': plugin.plugin_author
|
'author': plugin.plugin_author
|
||||||
} for plugin in emitted_plugins
|
} for plugin in emitted_plugins
|
||||||
@@ -266,7 +338,7 @@ class PluginManager:
|
|||||||
|
|
||||||
plugin.enabled = new_status
|
plugin.enabled = new_status
|
||||||
|
|
||||||
await self.setting.dump_container_setting(self.loader.plugins)
|
await self.dump_plugin_container_setting(self.plugin_containers)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -280,11 +352,18 @@ class PluginManager:
|
|||||||
plugin_name = plugin.get('name')
|
plugin_name = plugin.get('name')
|
||||||
plugin_priority = plugin.get('priority')
|
plugin_priority = plugin.get('priority')
|
||||||
|
|
||||||
for plugin in self.loader.plugins:
|
for plugin in self.plugin_containers:
|
||||||
if plugin.plugin_name == plugin_name:
|
if plugin.plugin_name == plugin_name:
|
||||||
plugin.priority = plugin_priority
|
plugin.priority = plugin_priority
|
||||||
break
|
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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -23,7 +23,7 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
for plugin in self.ap.plugin_mgr.plugins(
|
for plugin in self.ap.plugin_mgr.plugins(
|
||||||
enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
||||||
):
|
):
|
||||||
all_functions.extend(plugin.content_functions)
|
all_functions.extend(plugin.tools)
|
||||||
|
|
||||||
return all_functions
|
return all_functions
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
for plugin in self.ap.plugin_mgr.plugins(
|
for plugin in self.ap.plugin_mgr.plugins(
|
||||||
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
||||||
):
|
):
|
||||||
for function in plugin.content_functions:
|
for function in plugin.tools:
|
||||||
if function.name == name:
|
if function.name == name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -44,7 +44,7 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
for plugin in self.ap.plugin_mgr.plugins(
|
for plugin in self.ap.plugin_mgr.plugins(
|
||||||
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED
|
||||||
):
|
):
|
||||||
for function in plugin.content_functions:
|
for function in plugin.tools:
|
||||||
if function.name == name:
|
if function.name == name:
|
||||||
return function, plugin.plugin_inst
|
return function, plugin.plugin_inst
|
||||||
return None, None
|
return None, None
|
||||||
@@ -70,7 +70,7 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
plugin = None
|
plugin = None
|
||||||
|
|
||||||
for p in self.ap.plugin_mgr.plugins():
|
for p in self.ap.plugin_mgr.plugins():
|
||||||
if function in p.content_functions:
|
if function in p.tools:
|
||||||
plugin = p
|
plugin = p
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
await self.ap.ctr_mgr.usage.post_function_record(
|
await self.ap.ctr_mgr.usage.post_function_record(
|
||||||
plugin={
|
plugin={
|
||||||
"name": plugin.plugin_name,
|
"name": plugin.plugin_name,
|
||||||
"remote": plugin.plugin_source,
|
"remote": plugin.plugin_repository,
|
||||||
"version": plugin.plugin_version,
|
"version": plugin.plugin_version,
|
||||||
"author": plugin.plugin_author,
|
"author": plugin.plugin_author,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class VersionManager:
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
if await self.ap.ver_mgr.is_new_version_available():
|
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:
|
except Exception as e:
|
||||||
return f"检查版本更新时出错: {e}", logging.WARNING
|
return f"检查版本更新时出错: {e}", logging.WARNING
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user