From af493c117c10a641493b748f2bac4839b5d6d714 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 2 Jun 2025 11:48:22 +0800 Subject: [PATCH 01/78] deps: add `langbot-plugin` --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5e85bfb0..e33bf4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,8 @@ dependencies = [ "ruff>=0.11.9", "pre-commit>=4.2.0", "uv>=0.7.11", + "mypy>=1.16.0", + "langbot-plugin>=0.1.0a4", ] keywords = [ "bot", From 2d06f1cadb30b82808bd12a3ce0f7012c7fe5649 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 9 Jun 2025 12:10:48 +0800 Subject: [PATCH 02/78] feat: connector for plugin runtime --- pkg/core/app.py | 5 ++ pkg/core/stages/build_app.py | 5 ++ pkg/persistence/mgr.py | 64 +++++++++---------- .../migrations/dbm004_plugin_config.py | 20 ++++++ pkg/plugin/connector.py | 57 +++++++++++++++++ pkg/plugin/handler.py | 7 ++ pkg/utils/constants.py | 2 +- pyproject.toml | 2 +- templates/config.yaml | 2 + 9 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 pkg/persistence/migrations/dbm004_plugin_config.py create mode 100644 pkg/plugin/connector.py create mode 100644 pkg/plugin/handler.py diff --git a/pkg/core/app.py b/pkg/core/app.py index 911acd3d..b16bab95 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -13,6 +13,7 @@ from ..provider.tools import toolmgr as llm_tool_mgr from ..config import manager as config_mgr from ..command import cmdmgr from ..plugin import manager as plugin_mgr +from ..plugin import connector as plugin_connector from ..pipeline import pool from ..pipeline import controller, pipelinemgr from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr @@ -77,6 +78,8 @@ class Application: plugin_mgr: plugin_mgr.PluginManager = None + plugin_connector: plugin_connector.PluginRuntimeConnector = None + query_pool: pool.QueryPool = None ctrl: controller.Controller = None @@ -117,6 +120,8 @@ class Application: async def run(self): try: + await self.plugin_connector.initialize_plugins() + await self.plugin_mgr.initialize_plugins() # 后续可能会允许动态重启其他任务 diff --git a/pkg/core/stages/build_app.py b/pkg/core/stages/build_app.py index 6ee35610..d8689789 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -5,6 +5,7 @@ from .. import stage, app from ...utils import version, proxy, announce from ...pipeline import pool, controller, pipelinemgr from ...plugin import manager as plugin_mgr +from ...plugin import connector as plugin_connector from ...command import cmdmgr from ...provider.session import sessionmgr as llm_session_mgr from ...provider.modelmgr import modelmgr as llm_model_mgr @@ -64,6 +65,10 @@ class BuildAppStage(stage.BootingStage): ap.plugin_mgr = plugin_mgr_inst await plugin_mgr_inst.load_plugins() + plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap) + await plugin_connector_inst.initialize() + ap.plugin_connector = plugin_connector_inst + cmd_mgr_inst = cmdmgr.CommandManager(ap) await cmd_mgr_inst.initialize() ap.cmd_mgr = cmd_mgr_inst diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index 606aa9fd..23f08aa7 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -44,6 +44,38 @@ class PersistenceManager: await self.create_tables() + # run migrations + database_version = await self.execute_async( + sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version') + ) + + database_version = int(database_version.fetchone()[1]) + required_database_version = constants.required_database_version + + if database_version < required_database_version: + migrations = migration.preregistered_db_migrations + migrations.sort(key=lambda x: x.number) + + last_migration_number = database_version + + for migration_cls in migrations: + migration_instance = migration_cls(self.ap) + + if ( + migration_instance.number > database_version + and migration_instance.number <= required_database_version + ): + await migration_instance.upgrade() + await self.execute_async( + sqlalchemy.update(metadata.Metadata) + .where(metadata.Metadata.key == 'database_version') + .values({'value': str(migration_instance.number)}) + ) + last_migration_number = migration_instance.number + self.ap.logger.info(f'Migration {migration_instance.number} completed.') + + self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.') + async def create_tables(self): # create tables async with self.get_db_engine().connect() as conn: @@ -87,38 +119,6 @@ class PersistenceManager: # ================================= - # run migrations - database_version = await self.execute_async( - sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version') - ) - - database_version = int(database_version.fetchone()[1]) - required_database_version = constants.required_database_version - - if database_version < required_database_version: - migrations = migration.preregistered_db_migrations - migrations.sort(key=lambda x: x.number) - - last_migration_number = database_version - - for migration_cls in migrations: - migration_instance = migration_cls(self.ap) - - if ( - migration_instance.number > database_version - and migration_instance.number <= required_database_version - ): - await migration_instance.upgrade() - await self.execute_async( - sqlalchemy.update(metadata.Metadata) - .where(metadata.Metadata.key == 'database_version') - .values({'value': str(migration_instance.number)}) - ) - last_migration_number = migration_instance.number - self.ap.logger.info(f'Migration {migration_instance.number} completed.') - - self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.') - async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult: async with self.get_db_engine().connect() as conn: result = await conn.execute(*args, **kwargs) diff --git a/pkg/persistence/migrations/dbm004_plugin_config.py b/pkg/persistence/migrations/dbm004_plugin_config.py new file mode 100644 index 00000000..fc7a175a --- /dev/null +++ b/pkg/persistence/migrations/dbm004_plugin_config.py @@ -0,0 +1,20 @@ +from .. import migration + + +@migration.migration_class(4) +class DBMigratePluginConfig(migration.DBMigration): + """插件配置""" + + async def upgrade(self): + """升级""" + + if 'plugin' not in self.ap.instance_config.data: + self.ap.instance_config.data['plugin'] = { + 'runtime_ws_url': 'ws://localhost:5400/control/ws', + } + + await self.ap.instance_config.dump_config() + + async def downgrade(self): + """降级""" + pass diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py new file mode 100644 index 00000000..5eadc900 --- /dev/null +++ b/pkg/plugin/connector.py @@ -0,0 +1,57 @@ +# For connect to plugin runtime. +from __future__ import annotations + +import asyncio +import os +import sys + +from ..core import app +from . import handler +from ..utils import platform +from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller +from langbot_plugin.runtime.io.connections import stdio as stdio_connection +from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller + + +class PluginRuntimeConnector: + """Plugin runtime connector""" + + ap: app.Application + + handler: handler.RuntimeConnectionHandler + + handler_task: asyncio.Task + + stdio_client_controller: stdio_client_controller.StdioClientController + + def __init__(self, ap: app.Application): + self.ap = ap + + async def initialize(self): + async def new_connection_callback(connection: stdio_connection.StdioConnection): + self.ap.logger.info('Connected to plugin runtime.') + self.handler = handler.RuntimeConnectionHandler(connection) + self.handler_task = asyncio.create_task(self.handler.run()) + + if platform.get_platform() == 'docker': # use websocket + ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url'] + ctrl = ws_client_controller.WebSocketClientController( + ws_url=ws_url, + ) + await ctrl.run(new_connection_callback) + else: # stdio + # cmd: lbp rt -s + python_path = sys.executable + env = os.environ.copy() + ctrl = stdio_client_controller.StdioClientController( + command=python_path, + args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'], + env=env, + ) + await ctrl.run(new_connection_callback) + + async def run(self): + pass + + async def initialize_plugins(self): + pass diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py new file mode 100644 index 00000000..077c1649 --- /dev/null +++ b/pkg/plugin/handler.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from langbot_plugin.runtime.io import handler + + +class RuntimeConnectionHandler(handler.Handler): + """Runtime connection handler""" diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 450a2e16..f822b477 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.0.7' -required_database_version = 3 +required_database_version = 4 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" debug_mode = False diff --git a/pyproject.toml b/pyproject.toml index e33bf4af..40f172c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin>=0.1.0a4", + "langbot-plugin==0.1.0a6", ] keywords = [ "bot", diff --git a/templates/config.yaml b/templates/config.yaml index 109cd8d7..c896417c 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -18,3 +18,5 @@ system: jwt: expire: 604800 secret: '' +plugin: + runtime_ws_url: 'ws://plugin-runtime:5400/control/ws' \ No newline at end of file From 6f2fd72af6639ab663af66b4a3a79cc0db51f776 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 14 Jun 2025 17:27:21 +0800 Subject: [PATCH 03/78] feat(plugin): basic communication --- pkg/plugin/connector.py | 16 +++- pkg/plugin/context.py | 158 ++++++++++++++++++++-------------------- pkg/plugin/handler.py | 57 +++++++++++++++ pkg/utils/platform.py | 5 +- 4 files changed, 150 insertions(+), 86 deletions(-) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 5eadc900..f85d4be3 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -29,17 +29,23 @@ class PluginRuntimeConnector: async def initialize(self): async def new_connection_callback(connection: stdio_connection.StdioConnection): - self.ap.logger.info('Connected to plugin runtime.') - self.handler = handler.RuntimeConnectionHandler(connection) + self.handler = handler.RuntimeConnectionHandler(connection, self.ap) self.handler_task = asyncio.create_task(self.handler.run()) + _ = await self.handler.ping() + self.ap.logger.info('Connected to plugin runtime.') + await self.handler_task + + task: asyncio.Task | None = None if platform.get_platform() == 'docker': # use websocket + self.ap.logger.info('use websocket to connect to plugin runtime') ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url'] ctrl = ws_client_controller.WebSocketClientController( ws_url=ws_url, ) - await ctrl.run(new_connection_callback) + task = ctrl.run(new_connection_callback) else: # stdio + self.ap.logger.info('use stdio to connect to plugin runtime') # cmd: lbp rt -s python_path = sys.executable env = os.environ.copy() @@ -48,7 +54,9 @@ class PluginRuntimeConnector: args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'], env=env, ) - await ctrl.run(new_connection_callback) + task = ctrl.run(new_connection_callback) + + asyncio.create_task(task) async def run(self): pass diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index dfd691f3..86d940c4 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -2,13 +2,9 @@ from __future__ import annotations import typing import abc -import pydantic.v1 as pydantic -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 @@ -285,104 +281,104 @@ class EventContext: EventContext.eid += 1 -class RuntimeContainerStatus(enum.Enum): - """插件容器状态""" +# class RuntimeContainerStatus(enum.Enum): +# """插件容器状态""" - MOUNTED = 'mounted' - """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" +# MOUNTED = 'mounted' +# """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" - INITIALIZED = 'initialized' - """已初始化""" +# INITIALIZED = 'initialized' +# """已初始化""" -class RuntimeContainer(pydantic.BaseModel): - """运行时的插件容器 +# class RuntimeContainer(pydantic.BaseModel): +# """运行时的插件容器 - 运行期间存储单个插件的信息 - """ +# 运行期间存储单个插件的信息 +# """ - plugin_name: str - """插件名称""" +# plugin_name: str +# """插件名称""" - plugin_label: discover_engine.I18nString - """插件标签""" +# plugin_label: discover_engine.I18nString +# """插件标签""" - plugin_description: discover_engine.I18nString - """插件描述""" +# plugin_description: discover_engine.I18nString +# """插件描述""" - plugin_version: str - """插件版本""" +# plugin_version: str +# """插件版本""" - plugin_author: str - """插件作者""" +# plugin_author: str +# """插件作者""" - plugin_repository: str - """插件源码地址""" +# plugin_repository: str +# """插件源码地址""" - main_file: str - """插件主文件路径""" +# main_file: str +# """插件主文件路径""" - pkg_path: str - """插件包路径""" +# pkg_path: str +# """插件包路径""" - plugin_class: typing.Type[BasePlugin] = None - """插件类""" +# plugin_class: typing.Type[BasePlugin] = None +# """插件类""" - enabled: typing.Optional[bool] = True - """是否启用""" +# enabled: typing.Optional[bool] = True +# """是否启用""" - priority: typing.Optional[int] = 0 - """优先级""" +# priority: typing.Optional[int] = 0 +# """优先级""" - config_schema: typing.Optional[list[dict]] = [] - """插件配置模板""" +# config_schema: typing.Optional[list[dict]] = [] +# """插件配置模板""" - plugin_config: typing.Optional[dict] = {} - """插件配置""" +# plugin_config: typing.Optional[dict] = {} +# """插件配置""" - plugin_inst: typing.Optional[BasePlugin] = None - """插件实例""" +# plugin_inst: typing.Optional[BasePlugin] = None +# """插件实例""" - event_handlers: dict[ - typing.Type[events.BaseEventModel], - typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]], - ] = {} - """事件处理器""" +# event_handlers: dict[ +# typing.Type[events.BaseEventModel], +# typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]], +# ] = {} +# """事件处理器""" - tools: list[tools_entities.LLMFunction] = [] - """内容函数""" +# tools: list[tools_entities.LLMFunction] = [] +# """内容函数""" - status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED - """插件状态""" +# status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED +# """插件状态""" - class Config: - arbitrary_types_allowed = True +# class Config: +# arbitrary_types_allowed = True - def model_dump(self, *args, **kwargs): - return { - 'name': self.plugin_name, - 'label': self.plugin_label.to_dict(), - 'description': self.plugin_description.to_dict(), - 'version': self.plugin_version, - 'author': self.plugin_author, - '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() - }, - 'tools': [ - { - 'name': function.name, - 'human_desc': function.human_desc, - 'description': function.description, - 'parameters': function.parameters, - 'func': function.func.__name__, - } - for function in self.tools - ], - 'status': self.status.value, - } +# def model_dump(self, *args, **kwargs): +# return { +# 'name': self.plugin_name, +# 'label': self.plugin_label.to_dict(), +# 'description': self.plugin_description.to_dict(), +# 'version': self.plugin_version, +# 'author': self.plugin_author, +# '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() +# }, +# 'tools': [ +# { +# 'name': function.name, +# 'human_desc': function.human_desc, +# 'description': function.description, +# 'parameters': function.parameters, +# 'func': function.func.__name__, +# } +# for function in self.tools +# ], +# 'status': self.status.value, +# } diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 077c1649..d9360650 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -1,7 +1,64 @@ from __future__ import annotations +from typing import Any + +import sqlalchemy + from langbot_plugin.runtime.io import handler +from langbot_plugin.runtime.io.connection import Connection +from langbot_plugin.entities.io.actions.enums import ( + CommonAction, + RuntimeToLangBotAction, +) + +from ..entity.persistence import plugin as persistence_plugin + +from ..core import app class RuntimeConnectionHandler(handler.Handler): """Runtime connection handler""" + + ap: app.Application + + def __init__(self, connection: Connection, ap: app.Application): + super().__init__(connection) + self.ap = ap + + @self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS) + async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: + """Get plugin settings""" + + plugin_author = data['plugin_author'] + plugin_name = data['plugin_name'] + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) + + data = { + 'enabled': False, + 'priority': 0, + 'plugin_config': {}, + } + + setting = result.first() + + if setting is not None: + data['enabled'] = setting.enabled + data['priority'] = setting.priority + data['plugin_config'] = setting.config + + return handler.ActionResponse.success( + data=data, + ) + + async def ping(self) -> dict[str, Any]: + """Ping the runtime""" + return await self.call_action( + CommonAction.PING, + {}, + timeout=10, + ) diff --git a/pkg/utils/platform.py b/pkg/utils/platform.py index 0d4a1f26..0145081a 100644 --- a/pkg/utils/platform.py +++ b/pkg/utils/platform.py @@ -5,7 +5,10 @@ import sys def get_platform() -> str: """获取当前平台""" # 检查是不是在 docker 里 - if os.path.exists('/.dockerenv'): + + DOCKER_ENV = os.environ.get('DOCKER_ENV', 'false') + + if os.path.exists('/.dockerenv') or DOCKER_ENV == 'true': return 'docker' return sys.platform From c5eeab2fd0ac8c4526cf51c95c43d71aa58b2a67 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 14 Jun 2025 19:23:31 +0800 Subject: [PATCH 04/78] feat: listing plugins --- pkg/api/http/controller/groups/plugins.py | 6 +- pkg/core/app.py | 5 - pkg/core/stages/build_app.py | 6 - pkg/plugin/connector.py | 10 +- pkg/plugin/handler.py | 11 + pkg/plugin/manager.py | 308 ---------------------- 6 files changed, 20 insertions(+), 326 deletions(-) delete mode 100644 pkg/plugin/manager.py diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index daf6ea7d..4551cb07 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -12,11 +12,9 @@ class PluginsRouterGroup(group.RouterGroup): async def initialize(self) -> None: @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - plugins = self.ap.plugin_mgr.plugins() + plugins = await self.ap.plugin_connector.handler.list_plugins() - plugins_data = [plugin.model_dump() for plugin in plugins] - - return self.success(data={'plugins': plugins_data}) + return self.success(data={'plugins': plugins}) @self.route( '///toggle', diff --git a/pkg/core/app.py b/pkg/core/app.py index b16bab95..c795d6c0 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -12,7 +12,6 @@ from ..provider.modelmgr import modelmgr as llm_model_mgr from ..provider.tools import toolmgr as llm_tool_mgr from ..config import manager as config_mgr from ..command import cmdmgr -from ..plugin import manager as plugin_mgr from ..plugin import connector as plugin_connector from ..pipeline import pool from ..pipeline import controller, pipelinemgr @@ -76,8 +75,6 @@ class Application: # ========================= - plugin_mgr: plugin_mgr.PluginManager = None - plugin_connector: plugin_connector.PluginRuntimeConnector = None query_pool: pool.QueryPool = None @@ -122,8 +119,6 @@ class Application: try: await self.plugin_connector.initialize_plugins() - await self.plugin_mgr.initialize_plugins() - # 后续可能会允许动态重启其他任务 # 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程 async def never_ending(): diff --git a/pkg/core/stages/build_app.py b/pkg/core/stages/build_app.py index d8689789..54204c85 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -4,7 +4,6 @@ from __future__ import annotations from .. import stage, app from ...utils import version, proxy, announce from ...pipeline import pool, controller, pipelinemgr -from ...plugin import manager as plugin_mgr from ...plugin import connector as plugin_connector from ...command import cmdmgr from ...provider.session import sessionmgr as llm_session_mgr @@ -60,11 +59,6 @@ class BuildAppStage(stage.BootingStage): ap.persistence_mgr = persistence_mgr_inst await persistence_mgr_inst.initialize() - plugin_mgr_inst = plugin_mgr.PluginManager(ap) - await plugin_mgr_inst.initialize() - ap.plugin_mgr = plugin_mgr_inst - await plugin_mgr_inst.load_plugins() - plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap) await plugin_connector_inst.initialize() ap.plugin_connector = plugin_connector_inst diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index f85d4be3..26cf6fb7 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -11,6 +11,7 @@ from ..utils import platform from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller from langbot_plugin.runtime.io.connections import stdio as stdio_connection from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller +from langbot_plugin.api.entities import events, context class PluginRuntimeConnector: @@ -58,8 +59,11 @@ class PluginRuntimeConnector: asyncio.create_task(task) - async def run(self): - pass - async def initialize_plugins(self): pass + + async def emit_event( + self, + event: events.BaseEventModel, + ) -> context.EventContext: + pass diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index d9360650..056b45d2 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -9,6 +9,7 @@ from langbot_plugin.runtime.io.connection import Connection from langbot_plugin.entities.io.actions.enums import ( CommonAction, RuntimeToLangBotAction, + LangBotToRuntimeAction, ) from ..entity.persistence import plugin as persistence_plugin @@ -62,3 +63,13 @@ class RuntimeConnectionHandler(handler.Handler): {}, timeout=10, ) + + async def list_plugins(self) -> list[dict[str, Any]]: + """List plugins""" + result = await self.call_action( + LangBotToRuntimeAction.LIST_PLUGINS, + {}, + timeout=10, + ) + + return result['plugins'] diff --git a/pkg/plugin/manager.py b/pkg/plugin/manager.py deleted file mode 100644 index bf2027f4..00000000 --- a/pkg/plugin/manager.py +++ /dev/null @@ -1,308 +0,0 @@ -from __future__ import annotations - -import traceback - -import sqlalchemy - -from ..core import app, taskmgr -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: - """插件管理器""" - - ap: app.Application - - loaders: list[loader.PluginLoader] - - installer: installer.PluginInstaller - - api_host: context.APIHost - - plugin_containers: list[context.RuntimeContainer] - - def plugins( - self, - enabled: bool = None, - status: context.RuntimeContainerStatus = None, - ) -> list[context.RuntimeContainer]: - """获取插件列表""" - plugins = self.plugin_containers - - if enabled is not None: - plugins = [plugin for plugin in plugins if plugin.enabled == enabled] - - if status is not None: - 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.loaders = [ - classic.PluginLoader(ap), - manifest.PluginManifestLoader(ap), - ] - self.installer = github.GitHubRepoInstaller(ap) - self.api_host = context.APIHost(ap) - self.plugin_containers = [] - - async def initialize(self): - for loader in self.loaders: - await loader.initialize() - await self.installer.initialize() - await self.api_host.initialize() - - setattr(models, 'require_ver', self.api_host.require_ver) - - async def load_plugins(self): - self.ap.logger.info('Loading all plugins...') - - for loader in self.loaders: - await loader.load_plugins() - self.plugin_containers.extend(loader.plugins) - - await self.load_plugin_settings(self.plugin_containers) - - # 按优先级倒序 - self.plugin_containers.sort(key=lambda x: x.priority, reverse=False) - - 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() - plugin.status = context.RuntimeContainerStatus.INITIALIZED - - async def initialize_plugins(self): - for plugin in self.plugins(): - if not plugin.enabled: - self.ap.logger.debug(f'插件 {plugin.plugin_name} 未启用,跳过初始化') - continue - try: - await self.initialize_plugin(plugin) - except Exception as e: - self.ap.logger.error(f'插件 {plugin.plugin_name} 初始化失败: {e}') - self.ap.logger.exception(e) - continue - - async def destroy_plugin(self, plugin: context.RuntimeContainer): - if plugin.status != context.RuntimeContainerStatus.INITIALIZED: - return - - self.ap.logger.debug(f'释放插件 {plugin.plugin_name}') - plugin.plugin_inst.__del__() - await plugin.plugin_inst.destroy() - plugin.plugin_inst = None - plugin.status = context.RuntimeContainerStatus.MOUNTED - - async def destroy_plugins(self): - for plugin in self.plugins(): - if plugin.status != context.RuntimeContainerStatus.INITIALIZED: - self.ap.logger.debug(f'插件 {plugin.plugin_name} 未初始化,跳过释放') - continue - - try: - await self.destroy_plugin(plugin) - except Exception as e: - self.ap.logger.error(f'插件 {plugin.plugin_name} 释放失败: {e}') - self.ap.logger.exception(e) - continue - - async def install_plugin( - self, - plugin_source: str, - task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), - ): - """安装插件""" - await self.installer.install_plugin(plugin_source, task_context) - - # TODO statistics - - task_context.trace('重载插件..', 'reload-plugin') - await self.ap.reload(scope='plugin') - - async def uninstall_plugin( - self, - plugin_name: str, - task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), - ): - """卸载插件""" - - plugin_container = self.get_plugin_by_name(plugin_name) - - if plugin_container is None: - raise ValueError(f'插件 {plugin_name} 不存在') - - await self.destroy_plugin(plugin_container) - await self.installer.uninstall_plugin(plugin_name, task_context) - - # TODO statistics - - task_context.trace('重载插件..', 'reload-plugin') - await self.ap.reload(scope='plugin') - - async def update_plugin( - self, - plugin_name: str, - plugin_source: str = None, - task_context: taskmgr.TaskContext = taskmgr.TaskContext.placeholder(), - ): - """更新插件""" - await self.installer.update_plugin(plugin_name, plugin_source, task_context) - - # TODO statistics - - task_context.trace('重载插件..', 'reload-plugin') - await self.ap.reload(scope='plugin') - - def get_plugin_by_name(self, plugin_name: str) -> context.RuntimeContainer: - """通过插件名获取插件""" - for plugin in self.plugins(): - if plugin.plugin_name == plugin_name: - return plugin - return None - - async def emit_event(self, event: events.BaseEventModel) -> context.EventContext: - """触发事件""" - - ctx = context.EventContext(host=self.api_host, event=event) - - emitted_plugins: list[context.RuntimeContainer] = [] - - for plugin in self.plugins(enabled=True, status=context.RuntimeContainerStatus.INITIALIZED): - if event.__class__ in plugin.event_handlers: - self.ap.logger.debug(f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__}') - - is_prevented_default_before_call = ctx.is_prevented_default() - - try: - await plugin.event_handlers[event.__class__](plugin.plugin_inst, ctx) - except Exception as e: - self.ap.logger.error( - f'插件 {plugin.plugin_name} 处理事件 {event.__class__.__name__} 时发生错误: {e}' - ) - self.ap.logger.debug(f'Traceback: {traceback.format_exc()}') - - emitted_plugins.append(plugin) - - if not is_prevented_default_before_call and ctx.is_prevented_default(): - self.ap.logger.debug(f'插件 {plugin.plugin_name} 阻止了默认行为执行') - - if ctx.is_prevented_postorder(): - self.ap.logger.debug(f'插件 {plugin.plugin_name} 阻止了后序插件的执行') - break - - for key in ctx.__return_value__.keys(): - if hasattr(ctx.event, key): - setattr(ctx.event, key, ctx.__return_value__[key][0]) - - self.ap.logger.debug(f'事件 {event.__class__.__name__}({ctx.eid}) 处理完成,返回值 {ctx.__return_value__}') - - # TODO statistics - - return ctx - - async def update_plugin_switch(self, plugin_name: str, new_status: bool): - if self.get_plugin_by_name(plugin_name) is not None: - for plugin in self.plugins(): - if plugin.plugin_name == plugin_name: - if plugin.enabled == new_status: - return False - - # 初始化/释放插件 - if new_status: - await self.initialize_plugin(plugin) - else: - await self.destroy_plugin(plugin) - - plugin.enabled = new_status - - await self.dump_plugin_container_setting(plugin) - - break - - return True - else: - return False - - async def reorder_plugins(self, plugins: list[dict]): - for plugin in plugins: - plugin_name = plugin.get('name') - plugin_priority = plugin.get('priority') - - for plugin in self.plugin_containers: - if plugin.plugin_name == plugin_name: - plugin.priority = plugin_priority - break - - self.plugin_containers.sort(key=lambda x: x.priority, reverse=False) - - for plugin in self.plugin_containers: - await self.dump_plugin_container_setting(plugin) - - 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) From 0c2560cafb72898bc5743d44f9aa4fc898726498 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 15 Jun 2025 12:51:51 +0800 Subject: [PATCH 05/78] feat: switch tool entities and format --- libs/qq_official_api/api.py | 8 +- libs/slack_api/api.py | 7 +- libs/wechatpad_api/__init__.py | 5 +- libs/wechatpad_api/api/chatroom.py | 6 +- libs/wechatpad_api/api/downloadpai.py | 25 +- libs/wechatpad_api/api/friend.py | 5 - libs/wechatpad_api/api/login.py | 60 +-- libs/wechatpad_api/api/message.py | 111 ++--- libs/wechatpad_api/api/user.py | 17 +- libs/wechatpad_api/client.py | 48 +-- libs/wechatpad_api/util/http_util.py | 48 +-- libs/wechatpad_api/util/terminal_printer.py | 21 +- libs/wecom_api/api.py | 17 +- libs/wecom_customer_service_api/api.py | 4 +- pkg/api/http/service/model.py | 4 +- pkg/command/cmdmgr.py | 3 +- pkg/command/entities.py | 3 +- pkg/core/entities.py | 68 +-- pkg/pipeline/cntfilter/cntfilter.py | 4 +- pkg/pipeline/pipelinemgr.py | 4 +- pkg/pipeline/preproc/preproc.py | 35 +- pkg/pipeline/process/handlers/command.py | 12 +- pkg/platform/sources/aiocqhttp.py | 56 +-- pkg/platform/sources/lark.py | 8 +- pkg/platform/sources/nakuru.py | 5 +- pkg/platform/sources/officialaccount.py | 4 +- pkg/platform/sources/qqbotpy.py | 2 +- pkg/platform/sources/qqofficial.py | 9 +- pkg/platform/sources/slack.py | 8 +- pkg/platform/sources/telegram.py | 4 +- pkg/platform/sources/wechatpad.py | 406 +++++++----------- pkg/platform/sources/wecom.py | 6 +- pkg/platform/sources/wecomcs.py | 6 +- pkg/plugin/events.py | 3 +- pkg/plugin/handler.py | 15 + pkg/plugin/loaders/classic.py | 6 +- pkg/plugin/loaders/manifest.py | 4 +- pkg/provider/modelmgr/modelmgr.py | 22 +- pkg/provider/modelmgr/requester.py | 4 +- .../modelmgr/requesters/anthropicmsgs.py | 4 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 6 +- .../modelmgr/requesters/deepseekchatcmpl.py | 4 +- .../modelmgr/requesters/giteeaichatcmpl.py | 4 +- .../modelmgr/requesters/modelscopechatcmpl.py | 6 +- .../modelmgr/requesters/moonshotchatcmpl.py | 4 +- .../modelmgr/requesters/ollamachat.py | 6 +- pkg/provider/runners/localagent.py | 14 +- pkg/provider/session/sessionmgr.py | 22 +- pkg/provider/tools/entities.py | 31 -- pkg/provider/tools/loader.py | 4 +- pkg/provider/tools/loaders/mcp.py | 11 +- pkg/provider/tools/loaders/plugin.py | 9 +- pkg/provider/tools/toolmgr.py | 11 +- pkg/utils/image.py | 8 +- pkg/utils/importutil.py | 2 +- 55 files changed, 455 insertions(+), 774 deletions(-) delete mode 100644 pkg/provider/tools/entities.py diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index fa38073d..cb5f658a 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -104,7 +104,7 @@ class QQOfficialClient: return {'code': 0, 'message': 'success'} except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') return {'error': str(e)}, 400 async def run_task(self, host: str, port: int, *args, **kwargs): @@ -168,7 +168,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/v2/users/' + user_openid + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -193,7 +192,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/v2/groups/' + group_openid + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -209,7 +207,7 @@ class QQOfficialClient: if response.status_code == 200: return else: - await self.logger.error(f"发送群聊消息失败:{response.json()}") + await self.logger.error(f'发送群聊消息失败:{response.json()}') raise Exception(response.read().decode()) async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str): @@ -217,7 +215,6 @@ class QQOfficialClient: if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/channels/' + channel_id + '/messages' async with httpx.AsyncClient() as client: headers = { @@ -240,7 +237,6 @@ class QQOfficialClient: """发送频道私聊消息""" if not await self.check_access_token(): await self.get_access_token() - url = self.base_url + '/dms/' + guild_id + '/messages' async with httpx.AsyncClient() as client: diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index c291e92f..746d15da 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -34,7 +34,6 @@ class SlackClient: if self.bot_user_id and bot_user_id == self.bot_user_id: return jsonify({'status': 'ok'}) - # 处理私信 if data and data.get('event', {}).get('channel_type') in ['im']: @@ -52,7 +51,7 @@ class SlackClient: return jsonify({'status': 'ok'}) except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') raise (e) async def _handle_message(self, event: SlackEvent): @@ -82,7 +81,7 @@ class SlackClient: self.bot_user_id = response['message']['bot_id'] return except Exception as e: - await self.logger.error(f"Error in send_message: {e}") + await self.logger.error(f'Error in send_message: {e}') raise e async def send_message_to_one(self, text: str, user_id: str): @@ -93,7 +92,7 @@ class SlackClient: return except Exception as e: - await self.logger.error(f"Error in send_message: {traceback.format_exc()}") + await self.logger.error(f'Error in send_message: {traceback.format_exc()}') raise e async def run_task(self, host: str, port: int, *args, **kwargs): diff --git a/libs/wechatpad_api/__init__.py b/libs/wechatpad_api/__init__.py index 23c23fb2..ff27058b 100644 --- a/libs/wechatpad_api/__init__.py +++ b/libs/wechatpad_api/__init__.py @@ -1 +1,4 @@ -from .client import WeChatPadClient \ No newline at end of file +from .client import WeChatPadClient + + +__all__ = ['WeChatPadClient'] diff --git a/libs/wechatpad_api/api/chatroom.py b/libs/wechatpad_api/api/chatroom.py index a7af207c..2d9281a2 100644 --- a/libs/wechatpad_api/api/chatroom.py +++ b/libs/wechatpad_api/api/chatroom.py @@ -1,4 +1,4 @@ -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json class ChatRoomApi: @@ -7,8 +7,6 @@ class ChatRoomApi: self.token = token def get_chatroom_member_detail(self, chatroom_name): - params = { - "ChatRoomName": chatroom_name - } + params = {'ChatRoomName': chatroom_name} url = self.base_url + '/group/GetChatroomMemberDetail' return post_json(url, token=self.token, data=params) diff --git a/libs/wechatpad_api/api/downloadpai.py b/libs/wechatpad_api/api/downloadpai.py index a82a5674..2d45fac6 100644 --- a/libs/wechatpad_api/api/downloadpai.py +++ b/libs/wechatpad_api/api/downloadpai.py @@ -1,32 +1,23 @@ -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json import httpx import base64 + class DownloadApi: def __init__(self, base_url, token): self.base_url = base_url self.token = token def send_download(self, aeskey, file_type, file_url): - json_data = { - "AesKey": aeskey, - "FileType": file_type, - "FileURL": file_url - } - url = self.base_url + "/message/SendCdnDownload" + json_data = {'AesKey': aeskey, 'FileType': file_type, 'FileURL': file_url} + url = self.base_url + '/message/SendCdnDownload' return post_json(url, token=self.token, data=json_data) - def get_msg_voice(self,buf_id, length, new_msgid): - json_data = { - "Bufid": buf_id, - "Length": length, - "NewMsgId": new_msgid, - "ToUserName": "" - } - url = self.base_url + "/message/GetMsgVoice" + def get_msg_voice(self, buf_id, length, new_msgid): + json_data = {'Bufid': buf_id, 'Length': length, 'NewMsgId': new_msgid, 'ToUserName': ''} + url = self.base_url + '/message/GetMsgVoice' return post_json(url, token=self.token, data=json_data) - async def download_url_to_base64(self, download_url): async with httpx.AsyncClient() as client: response = await client.get(download_url) @@ -36,4 +27,4 @@ class DownloadApi: base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式 return base64_str else: - raise Exception('获取文件失败') \ No newline at end of file + raise Exception('获取文件失败') diff --git a/libs/wechatpad_api/api/friend.py b/libs/wechatpad_api/api/friend.py index 00701a5d..a7a448aa 100644 --- a/libs/wechatpad_api/api/friend.py +++ b/libs/wechatpad_api/api/friend.py @@ -1,11 +1,6 @@ -from libs.wechatpad_api.util.http_util import post_json,async_request -from typing import List, Dict, Any, Optional - - class FriendApi: """联系人API类,处理所有与联系人相关的操作""" def __init__(self, base_url: str, token: str): self.base_url = base_url self.token = token - diff --git a/libs/wechatpad_api/api/login.py b/libs/wechatpad_api/api/login.py index 142a3c85..4aa4ae8d 100644 --- a/libs/wechatpad_api/api/login.py +++ b/libs/wechatpad_api/api/login.py @@ -1,37 +1,34 @@ -from libs.wechatpad_api.util.http_util import async_request,post_json,get_json +from libs.wechatpad_api.util.http_util import post_json, get_json class LoginApi: def __init__(self, base_url: str, token: str = None, admin_key: str = None): - ''' + """ Args: base_url: 原始路径 token: token admin_key: 管理员key - ''' + """ self.base_url = base_url self.token = token # self.admin_key = admin_key - def get_token(self, admin_key, day: int=365): + def get_token(self, admin_key, day: int = 365): # 获取普通token - url = f"{self.base_url}/admin/GenAuthKey1" - json_data = { - "Count": 1, - "Days": day - } + url = f'{self.base_url}/admin/GenAuthKey1' + json_data = {'Count': 1, 'Days': day} return post_json(base_url=url, token=admin_key, data=json_data) - def get_login_qr(self, Proxy: str = ""): - ''' + def get_login_qr(self, Proxy: str = ''): + """ Args: Proxy:异地使用时代理 Returns:json数据 - ''' + """ """ { @@ -49,54 +46,37 @@ class LoginApi: } """ - #获取登录二维码 - url = f"{self.base_url}/login/GetLoginQrCodeNew" + # 获取登录二维码 + url = f'{self.base_url}/login/GetLoginQrCodeNew' check = False - if Proxy != "": + if Proxy != '': check = True - json_data = { - "Check": check, - "Proxy": Proxy - } + json_data = {'Check': check, 'Proxy': Proxy} return post_json(base_url=url, token=self.token, data=json_data) - def get_login_status(self): # 获取登录状态 url = f'{self.base_url}/login/GetLoginStatus' return get_json(base_url=url, token=self.token) - - def logout(self): # 退出登录 url = f'{self.base_url}/login/LogOut' return post_json(base_url=url, token=self.token) - - - - def wake_up_login(self, Proxy: str = ""): + def wake_up_login(self, Proxy: str = ''): # 唤醒登录 url = f'{self.base_url}/login/WakeUpLogin' check = False - if Proxy != "": + if Proxy != '': check = True - json_data = { - "Check": check, - "Proxy": "" - } + json_data = {'Check': check, 'Proxy': ''} return post_json(base_url=url, token=self.token, data=json_data) - - - def login(self,admin_key): + def login(self, admin_key): login_status = self.get_login_status() - if login_status["Code"] == 300 and login_status["Text"] == "你已退出微信": - print("token已经失效,重新获取") + if login_status['Code'] == 300 and login_status['Text'] == '你已退出微信': + print('token已经失效,重新获取') token_data = self.get_token(admin_key) - self.token = token_data["Data"][0] - - - + self.token = token_data['Data'][0] diff --git a/libs/wechatpad_api/api/message.py b/libs/wechatpad_api/api/message.py index 2089ce96..cca76313 100644 --- a/libs/wechatpad_api/api/message.py +++ b/libs/wechatpad_api/api/message.py @@ -1,5 +1,4 @@ - -from libs.wechatpad_api.util.http_util import async_request, post_json +from libs.wechatpad_api.util.http_util import post_json class MessageApi: @@ -7,8 +6,8 @@ class MessageApi: self.base_url = base_url self.token = token - def post_text(self, to_wxid, content, ats: list= []): - ''' + def post_text(self, to_wxid, content, ats: list = []): + """ Args: app_id: 微信id @@ -18,106 +17,64 @@ class MessageApi: Returns: - ''' - url = self.base_url + "/message/SendTextMessage" + """ + url = self.base_url + '/message/SendTextMessage' """发送文字消息""" json_data = { - "MsgItem": [ - { - "AtWxIDList": ats, - "ImageContent": "", - "MsgType": 0, - "TextContent": content, - "ToUserName": to_wxid - } - ] - } - return post_json(base_url=url, token=self.token, data=json_data) + 'MsgItem': [ + {'AtWxIDList': ats, 'ImageContent': '', 'MsgType': 0, 'TextContent': content, 'ToUserName': to_wxid} + ] + } + return post_json(base_url=url, token=self.token, data=json_data) - - - - def post_image(self, to_wxid, img_url, ats: list= []): + def post_image(self, to_wxid, img_url, ats: list = []): """发送图片消息""" # 这里好像可以尝试发送多个暂时未测试 json_data = { - "MsgItem": [ - { - "AtWxIDList": ats, - "ImageContent": img_url, - "MsgType": 0, - "TextContent": '', - "ToUserName": to_wxid - } + 'MsgItem': [ + {'AtWxIDList': ats, 'ImageContent': img_url, 'MsgType': 0, 'TextContent': '', 'ToUserName': to_wxid} ] } - url = self.base_url + "/message/SendImageMessage" + url = self.base_url + '/message/SendImageMessage' return post_json(base_url=url, token=self.token, data=json_data) def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration): """发送语音消息""" json_data = { - "ToUserName": to_wxid, - "VoiceData": voice_data, - "VoiceFormat": voice_forma, - "VoiceSecond": voice_duration + 'ToUserName': to_wxid, + 'VoiceData': voice_data, + 'VoiceFormat': voice_forma, + 'VoiceSecond': voice_duration, } - url = self.base_url + "/message/SendVoice" + url = self.base_url + '/message/SendVoice' return post_json(base_url=url, token=self.token, data=json_data) - - - - def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag): """发送名片消息""" param = { - "CardAlias": alias, - "CardFlag": flag, - "CardNickName": nick_name, - "CardWxId": name_card_wxid, - "ToUserName": to_wxid + 'CardAlias': alias, + 'CardFlag': flag, + 'CardNickName': nick_name, + 'CardWxId': name_card_wxid, + 'ToUserName': to_wxid, } - url = f"{self.base_url}/message/ShareCardMessage" + url = f'{self.base_url}/message/ShareCardMessage' return post_json(base_url=url, token=self.token, data=param) - def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0): + def post_emoji(self, to_wxid, emoji_md5, emoji_size: int = 0): """发送emoji消息""" - json_data = { - "EmojiList": [ - { - "EmojiMd5": emoji_md5, - "EmojiSize": emoji_size, - "ToUserName": to_wxid - } - ] - } - url = f"{self.base_url}/message/SendEmojiMessage" + json_data = {'EmojiList': [{'EmojiMd5': emoji_md5, 'EmojiSize': emoji_size, 'ToUserName': to_wxid}]} + url = f'{self.base_url}/message/SendEmojiMessage' return post_json(base_url=url, token=self.token, data=json_data) - def post_app_msg(self, to_wxid,xml_data, contenttype:int=0): + def post_app_msg(self, to_wxid, xml_data, contenttype: int = 0): """发送appmsg消息""" - json_data = { - "AppList": [ - { - "ContentType": contenttype, - "ContentXML": xml_data, - "ToUserName": to_wxid - } - ] - } - url = f"{self.base_url}/message/SendAppMessage" + json_data = {'AppList': [{'ContentType': contenttype, 'ContentXML': xml_data, 'ToUserName': to_wxid}]} + url = f'{self.base_url}/message/SendAppMessage' return post_json(base_url=url, token=self.token, data=json_data) - - def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time): """撤回消息""" - param = { - "ClientMsgId": msg_id, - "CreateTime": create_time, - "NewMsgId": new_msg_id, - "ToUserName": to_wxid - } - url = f"{self.base_url}/message/RevokeMsg" - return post_json(base_url=url, token=self.token, data=param) \ No newline at end of file + param = {'ClientMsgId': msg_id, 'CreateTime': create_time, 'NewMsgId': new_msg_id, 'ToUserName': to_wxid} + url = f'{self.base_url}/message/RevokeMsg' + return post_json(base_url=url, token=self.token, data=param) diff --git a/libs/wechatpad_api/api/user.py b/libs/wechatpad_api/api/user.py index 2dc73bd2..d2187c7c 100644 --- a/libs/wechatpad_api/api/user.py +++ b/libs/wechatpad_api/api/user.py @@ -12,12 +12,9 @@ class UserApi: return get_json(base_url=url, token=self.token) - def get_qr_code(self, recover:bool=True, style:int=8): + def get_qr_code(self, recover: bool = True, style: int = 8): """获取自己的二维码""" - param = { - "Recover": recover, - "Style": style - } + param = {'Recover': recover, 'Style': style} url = f'{self.base_url}/user/GetMyQRCode' return post_json(base_url=url, token=self.token, data=param) @@ -26,12 +23,8 @@ class UserApi: url = f'{self.base_url}/equipment/GetSafetyInfo' return post_json(base_url=url, token=self.token) - - - async def update_head_img(self, head_img_base64): + async def update_head_img(self, head_img_base64): """修改头像""" - param = { - "Base64": head_img_base64 - } + param = {'Base64': head_img_base64} url = f'{self.base_url}/user/UploadHeadImage' - return await async_request(base_url=url, token_key=self.token, json=param) \ No newline at end of file + return await async_request(base_url=url, token_key=self.token, json=param) diff --git a/libs/wechatpad_api/client.py b/libs/wechatpad_api/client.py index f5ded1cb..5e699d03 100644 --- a/libs/wechatpad_api/client.py +++ b/libs/wechatpad_api/client.py @@ -1,4 +1,3 @@ - from libs.wechatpad_api.api.login import LoginApi from libs.wechatpad_api.api.friend import FriendApi from libs.wechatpad_api.api.message import MessageApi @@ -7,9 +6,6 @@ from libs.wechatpad_api.api.downloadpai import DownloadApi from libs.wechatpad_api.api.chatroom import ChatRoomApi - - - class WeChatPadClient: def __init__(self, base_url, token, logger=None): self._login_api = LoginApi(base_url, token) @@ -20,16 +16,16 @@ class WeChatPadClient: self._chatroom_api = ChatRoomApi(base_url, token) self.logger = logger - def get_token(self,admin_key, day: int): - '''获取token''' + def get_token(self, admin_key, day: int): + """获取token""" return self._login_api.get_token(admin_key, day) - def get_login_qr(self, Proxy:str=""): + def get_login_qr(self, Proxy: str = ''): """登录二维码""" return self._login_api.get_login_qr(Proxy=Proxy) - def awaken_login(self, Proxy:str=""): - '''唤醒登录''' + def awaken_login(self, Proxy: str = ''): + """唤醒登录""" return self._login_api.wake_up_login(Proxy=Proxy) def log_out(self): @@ -40,59 +36,57 @@ class WeChatPadClient: """获取登录状态""" return self._login_api.get_login_status() - def send_text_message(self, to_wxid, message, ats: list=[]): + def send_text_message(self, to_wxid, message, ats: list = []): """发送文本消息""" - return self._message_api.post_text(to_wxid, message, ats) + return self._message_api.post_text(to_wxid, message, ats) - def send_image_message(self, to_wxid, img_url, ats: list=[]): + def send_image_message(self, to_wxid, img_url, ats: list = []): """发送图片消息""" - return self._message_api.post_image(to_wxid, img_url, ats) + return self._message_api.post_image(to_wxid, img_url, ats) def send_voice_message(self, to_wxid, voice_data, voice_forma, voice_duration): """发送音频消息""" - return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration) + return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration) def send_app_message(self, to_wxid, app_message, type): """发送app消息""" - return self._message_api.post_app_msg(to_wxid, app_message, type) + return self._message_api.post_app_msg(to_wxid, app_message, type) def send_emoji_message(self, to_wxid, emoji_md5, emoji_size): """发送emoji消息""" - return self._message_api.post_emoji(to_wxid,emoji_md5,emoji_size) + return self._message_api.post_emoji(to_wxid, emoji_md5, emoji_size) def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time): """撤回消息""" - return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time) + return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time) def get_profile(self): """获取用户信息""" return self._user_api.get_profile() - def get_qr_code(self, recover:bool=True, style:int=8): + def get_qr_code(self, recover: bool = True, style: int = 8): """获取用户二维码""" - return self._user_api.get_qr_code(recover=recover, style=style) + return self._user_api.get_qr_code(recover=recover, style=style) def get_safety_info(self): """获取设备信息""" - return self._user_api.get_safety_info() + return self._user_api.get_safety_info() - def update_head_img(self, head_img_base64): + def update_head_img(self, head_img_base64): """上传用户头像""" - return self._user_api.update_head_img(head_img_base64) + return self._user_api.update_head_img(head_img_base64) def cdn_download(self, aeskey, file_type, file_url): """cdn下载""" - return self._download_api.send_download( aeskey, file_type, file_url) + return self._download_api.send_download(aeskey, file_type, file_url) - def get_msg_voice(self,buf_id, length, msgid): + def get_msg_voice(self, buf_id, length, msgid): """下载语音""" return self._download_api.get_msg_voice(buf_id, length, msgid) - async def download_base64(self,url): + async def download_base64(self, url): return await self._download_api.download_url_to_base64(download_url=url) def get_chatroom_member_detail(self, chatroom_name): """查看群成员详情""" return self._chatroom_api.get_chatroom_member_detail(chatroom_name) - - diff --git a/libs/wechatpad_api/util/http_util.py b/libs/wechatpad_api/util/http_util.py index 754003e9..447c29df 100644 --- a/libs/wechatpad_api/util/http_util.py +++ b/libs/wechatpad_api/util/http_util.py @@ -1,10 +1,9 @@ import requests +import aiohttp + def post_json(base_url, token, data=None): - headers = { - 'Content-Type': 'application/json' - } - + headers = {'Content-Type': 'application/json'} url = base_url + f'?key={token}' @@ -18,14 +17,12 @@ def post_json(base_url, token, data=None): else: raise RuntimeError(response.text) except Exception as e: - print(f"http请求失败, url={url}, exception={e}") + print(f'http请求失败, url={url}, exception={e}') raise RuntimeError(str(e)) -def get_json(base_url, token): - headers = { - 'Content-Type': 'application/json' - } +def get_json(base_url, token): + headers = {'Content-Type': 'application/json'} url = base_url + f'?key={token}' @@ -39,21 +36,18 @@ def get_json(base_url, token): else: raise RuntimeError(response.text) except Exception as e: - print(f"http请求失败, url={url}, exception={e}") + print(f'http请求失败, url={url}, exception={e}') raise RuntimeError(str(e)) -import aiohttp -import asyncio - async def async_request( - base_url: str, - token_key: str, - method: str = 'POST', - params: dict = None, - # headers: dict = None, - data: dict = None, - json: dict = None + base_url: str, + token_key: str, + method: str = 'POST', + params: dict = None, + # headers: dict = None, + data: dict = None, + json: dict = None, ): """ 通用异步请求函数 @@ -67,18 +61,11 @@ async def async_request( :param json: JSON数据 :return: 响应文本 """ - headers = { - 'Content-Type': 'application/json' - } - url = f"{base_url}?key={token_key}" + headers = {'Content-Type': 'application/json'} + url = f'{base_url}?key={token_key}' async with aiohttp.ClientSession() as session: async with session.request( - method=method, - url=url, - params=params, - headers=headers, - data=data, - json=json + method=method, url=url, params=params, headers=headers, data=data, json=json ) as response: response.raise_for_status() # 如果状态码不是200,抛出异常 result = await response.json() @@ -89,4 +76,3 @@ async def async_request( # return await result # else: # raise RuntimeError("请求失败",response.text) - diff --git a/libs/wechatpad_api/util/terminal_printer.py b/libs/wechatpad_api/util/terminal_printer.py index 48af021e..19a35ffa 100644 --- a/libs/wechatpad_api/util/terminal_printer.py +++ b/libs/wechatpad_api/util/terminal_printer.py @@ -1,31 +1,34 @@ import qrcode + def print_green(text): - print(f"\033[32m{text}\033[0m") + print(f'\033[32m{text}\033[0m') + def print_yellow(text): - print(f"\033[33m{text}\033[0m") + print(f'\033[33m{text}\033[0m') + def print_red(text): - print(f"\033[31m{text}\033[0m") + print(f'\033[31m{text}\033[0m') + def make_and_print_qr(url): """生成并打印二维码 - + Args: url: 需要生成二维码的URL字符串 - + Returns: None - + 功能: 1. 在终端打印二维码的ASCII图形 2. 同时提供在线二维码生成链接作为备选 """ - print_green("请扫描下方二维码登录") + print_green('请扫描下方二维码登录') qr = qrcode.QRCode() qr.add_data(url) qr.make() qr.print_ascii(invert=True) - print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}") - + print_green(f'也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}') diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index cbd1b73f..c1328b0d 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -57,7 +57,7 @@ class WecomClient: if 'access_token' in data: return data['access_token'] else: - await self.logger.error(f"获取accesstoken失败:{response.json()}") + await self.logger.error(f'获取accesstoken失败:{response.json()}') raise Exception(f'未获取access token: {data}') async def get_users(self): @@ -129,7 +129,7 @@ class WecomClient: response = await client.post(url, json=params) data = response.json() except Exception as e: - await self.logger.error(f"发送图片失败:{data}") + await self.logger.error(f'发送图片失败:{data}') raise Exception('Failed to send image: ' + str(e)) # 企业微信错误码40014和42001,代表accesstoken问题 @@ -164,7 +164,7 @@ class WecomClient: self.access_token = await self.get_access_token(self.secret) return await self.send_private_msg(user_id, agent_id, content) if data['errcode'] != 0: - await self.logger.error(f"发送消息失败:{data}") + await self.logger.error(f'发送消息失败:{data}') raise Exception('Failed to send message: ' + str(data)) async def handle_callback_request(self): @@ -181,7 +181,7 @@ class WecomClient: echostr = request.args.get('echostr') ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) if ret != 0: - await self.logger.error("验证失败") + await self.logger.error('验证失败') raise Exception(f'验证失败,错误码: {ret}') return reply_echo_str @@ -189,9 +189,8 @@ class WecomClient: encrypt_msg = await request.data ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) if ret != 0: - await self.logger.error("消息解密失败") + await self.logger.error('消息解密失败') raise Exception(f'消息解密失败,错误码: {ret}') - # 解析消息并处理 message_data = await self.get_message(xml_msg) @@ -202,7 +201,7 @@ class WecomClient: return 'success' except Exception as e: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') return f'Error processing request: {str(e)}', 400 async def run_task(self, host: str, port: int, *args, **kwargs): @@ -301,7 +300,7 @@ class WecomClient: except binascii.Error as e: raise ValueError(f'Invalid base64 string: {str(e)}') else: - await self.logger.error("Image对象出错") + await self.logger.error('Image对象出错') raise ValueError('image对象出错') # 设置 multipart/form-data 格式的文件 @@ -325,7 +324,7 @@ class WecomClient: self.access_token = await self.get_access_token(self.secret) media_id = await self.upload_to_work(image) if data.get('errcode', 0) != 0: - await self.logger.error(f"上传图片失败:{data}") + await self.logger.error(f'上传图片失败:{data}') raise Exception('failed to upload file') media_id = data.get('media_id') diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 09805aa9..32fab7f7 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -187,7 +187,7 @@ class WecomCSClient: self.access_token = await self.get_access_token(self.secret) return await self.send_text_msg(open_kfid, external_userid, msgid, content) if data['errcode'] != 0: - await self.logger.error(f"发送消息失败:{data}") + await self.logger.error(f'发送消息失败:{data}') raise Exception('Failed to send message') return data @@ -227,7 +227,7 @@ class WecomCSClient: return 'success' except Exception as e: if self.logger: - await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}") + await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}') else: traceback.print_exc() return f'Error processing request: {str(e)}', 400 diff --git a/pkg/api/http/service/model.py b/pkg/api/http/service/model.py index 74fb4e02..3dab181e 100644 --- a/pkg/api/http/service/model.py +++ b/pkg/api/http/service/model.py @@ -7,7 +7,7 @@ from ....core import app from ....entity.persistence import model as persistence_model from ....entity.persistence import pipeline as persistence_pipeline from ....provider.modelmgr import requester as model_requester -from ....provider import entities as llm_entities +from langbot_plugin.api.entities.builtin.provider import message as provider_message class ModelsService: @@ -99,7 +99,7 @@ class ModelsService: await runtime_llm_model.requester.invoke_llm( query=None, model=runtime_llm_model, - messages=[llm_entities.Message(role='user', content='Hello, world!')], + messages=[provider_message.Message(role='user', content='Hello, world!')], funcs=[], extra_args={}, ) diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index 1bd03fcf..a8cf5eae 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -5,6 +5,7 @@ import typing from ..core import app, entities as core_entities from . import entities, operator, errors from ..utils import importutil +import langbot_plugin.api.entities.builtin.provider.session as provider_session # 引入所有算子以便注册 from . import operators @@ -90,7 +91,7 @@ class CommandManager: self, command_text: str, query: core_entities.Query, - session: core_entities.Session, + session: provider_session.Session, ) -> typing.AsyncGenerator[entities.CommandReturn, None]: """执行命令""" diff --git a/pkg/command/entities.py b/pkg/command/entities.py index cccd588e..e80d203f 100644 --- a/pkg/command/entities.py +++ b/pkg/command/entities.py @@ -4,6 +4,7 @@ import typing import pydantic.v1 as pydantic +import langbot_plugin.api.entities.builtin.provider.session as provider_session from ..core import entities as core_entities from . import errors from ..platform.types import message as platform_message @@ -37,7 +38,7 @@ class ExecuteContext(pydantic.BaseModel): query: core_entities.Query """本次消息的请求对象""" - session: core_entities.Session + session: provider_session.Session """本次消息所属的会话对象""" command_text: str diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 4caf18ed..3bc0349c 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -2,17 +2,15 @@ from __future__ import annotations import enum import typing -import datetime -import asyncio import pydantic.v1 as pydantic from ..provider import entities as llm_entities -from ..provider.modelmgr import requester -from ..provider.tools import entities as tools_entities from ..platform import adapter as msadapter from ..platform.types import message as platform_message from ..platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class LifecycleControlScope(enum.Enum): @@ -65,7 +63,7 @@ class Query(pydantic.BaseModel): adapter: msadapter.MessagePlatformAdapter """消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器""" - session: typing.Optional[Session] = None + session: typing.Optional[provider_session.Session] = None """会话对象,由前置处理器阶段设置""" messages: typing.Optional[list[llm_entities.Message]] = [] @@ -80,10 +78,10 @@ class Query(pydantic.BaseModel): variables: typing.Optional[dict[str, typing.Any]] = None """变量,由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。""" - use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None + use_llm_model_uuid: typing.Optional[str] = None """使用的对话模型,由前置处理器阶段设置""" - use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None + use_funcs: typing.Optional[list[resource_tool.LLMTool]] = None """使用的函数,由前置处理器阶段设置""" resp_messages: ( @@ -95,7 +93,7 @@ class Query(pydantic.BaseModel): """回复消息链,从resp_messages包装而得""" # ======= 内部保留 ======= - current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None + current_stage_name: typing.Optional[str] = None """当前所处阶段""" class Config: @@ -120,57 +118,3 @@ class Query(pydantic.BaseModel): if self.variables is None: return {} return self.variables - - -class Conversation(pydantic.BaseModel): - """对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation""" - - prompt: llm_entities.Prompt - - messages: list[llm_entities.Message] - - create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) - - update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) - - use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None - - use_funcs: typing.Optional[list[tools_entities.LLMFunction]] - - pipeline_uuid: str - """流水线UUID。""" - - bot_uuid: str - """机器人UUID。""" - - uuid: typing.Optional[str] = None - """该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。""" - - class Config: - arbitrary_types_allowed = True - - -class Session(pydantic.BaseModel): - """会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}""" - - launcher_type: LauncherTypes - - launcher_id: typing.Union[int, str] - - sender_id: typing.Optional[typing.Union[int, str]] = 0 - - use_prompt_name: typing.Optional[str] = 'default' - - using_conversation: typing.Optional[Conversation] = None - - conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list) - - create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) - - update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now) - - semaphore: typing.Optional[asyncio.Semaphore] = None - """当前会话的信号量,用于限制并发""" - - class Config: - arbitrary_types_allowed = True diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 0bbc5103..fb562a42 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -5,7 +5,7 @@ from ...core import app from .. import stage, entities from ...core import entities as core_entities from . import filter as filter_model, entities as filter_entities -from ...provider import entities as llm_entities +from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...platform.types import message as platform_message from ...utils import importutil @@ -142,7 +142,7 @@ class ContentFilterStage(stage.PipelineStage): return await self._pre_process(str(query.message_chain).strip(), query) elif stage_inst_name == 'PostContentFilterStage': # 仅处理 query.resp_messages[-1].content 是 str 的情况 - if isinstance(query.resp_messages[-1], llm_entities.Message) and isinstance( + if isinstance(query.resp_messages[-1], provider_message.Message) and isinstance( query.resp_messages[-1].content, str ): return await self._post_process(query.resp_messages[-1].content, query) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index b61e34ad..78cffa73 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -136,7 +136,7 @@ class RuntimePipeline: while i < len(self.stage_containers): stage_container = self.stage_containers[i] - query.current_stage = stage_container # 标记到 Query 对象里 + query.current_stage_name = stage_container.inst_name # 标记到 Query 对象里 result = stage_container.inst.process(query, stage_container.inst_name) @@ -196,7 +196,7 @@ class RuntimePipeline: await self._execute_from_stage(0, query) except Exception as e: - inst_name = query.current_stage.inst_name if query.current_stage else 'unknown' + inst_name = query.current_stage_name if query.current_stage_name else 'unknown' self.ap.logger.error(f'处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}') self.ap.logger.error(f'Traceback: {traceback.format_exc()}') finally: diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 19478200..da56ca6e 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -4,7 +4,7 @@ import datetime from .. import stage, entities from ...core import entities as core_entities -from ...provider import entities as llm_entities +from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...plugin import events from ...platform.types import message as platform_message @@ -49,19 +49,20 @@ class PreProcessor(stage.PipelineStage): query.bot_uuid, ) - conversation.use_llm_model = llm_model - # 设置query query.session = session query.prompt = conversation.prompt.copy() query.messages = conversation.messages.copy() - query.use_llm_model = llm_model + query.use_llm_model_uuid = llm_model.model_entity.uuid if selected_runner == 'local-agent': - query.use_funcs = ( - conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('func_call') else None - ) + query.use_funcs = [] + + if llm_model.model_entity.abilities.__contains__('func_call'): + query.use_funcs = await self.ap.tool_mgr.get_all_functions( + plugin_enabled=True, + ) query.variables = { 'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}', @@ -73,7 +74,7 @@ class PreProcessor(stage.PipelineStage): # Check if this model supports vision, if not, remove all images # TODO this checking should be performed in runner, and in this stage, the image should be reserved - if selected_runner == 'local-agent' and not query.use_llm_model.model_entity.abilities.__contains__('vision'): + if selected_runner == 'local-agent' and not llm_model.model_entity.abilities.__contains__('vision'): for msg in query.messages: if isinstance(msg.content, list): for me in msg.content: @@ -87,28 +88,24 @@ class PreProcessor(stage.PipelineStage): for me in query.message_chain: if isinstance(me, platform_message.Plain): - content_list.append(llm_entities.ContentElement.from_text(me.text)) + content_list.append(provider_message.ContentElement.from_text(me.text)) plain_text += me.text elif isinstance(me, platform_message.Image): - if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__( - 'vision' - ): + if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'): if me.base64 is not None: - content_list.append(llm_entities.ContentElement.from_image_base64(me.base64)) + content_list.append(provider_message.ContentElement.from_image_base64(me.base64)) elif isinstance(me, platform_message.Quote) and qoute_msg: for msg in me.origin: if isinstance(msg, platform_message.Plain): - content_list.append(llm_entities.ContentElement.from_text(msg.text)) + content_list.append(provider_message.ContentElement.from_text(msg.text)) elif isinstance(msg, platform_message.Image): - if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__( - 'vision' - ): + if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'): if msg.base64 is not None: - content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64)) + content_list.append(provider_message.ContentElement.from_image_base64(msg.base64)) query.variables['user_message_text'] = plain_text - query.user_message = llm_entities.Message(role='user', content=content_list) + query.user_message = provider_message.Message(role='user', content=content_list) # =========== 触发事件 PromptPreProcessing event_ctx = await self.ap.plugin_mgr.emit_event( diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index cc0e9314..efce5615 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -5,7 +5,7 @@ import typing from .. import handler from ... import entities from ....core import entities as core_entities -from ....provider import entities as llm_entities +from langbot_plugin.api.entities.builtin.provider import message as provider_message from ....plugin import events from ....platform.types import message as platform_message @@ -64,7 +64,7 @@ class CommandHandler(handler.MessageHandler): async for ret in self.ap.cmd_mgr.execute(command_text=command_text, query=query, session=session): if ret.error is not None: query.resp_messages.append( - llm_entities.Message( + provider_message.Message( role='command', content=str(ret.error), ) @@ -74,16 +74,16 @@ class CommandHandler(handler.MessageHandler): yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) elif ret.text is not None or ret.image_url is not None: - content: list[llm_entities.ContentElement] = [] + content: list[provider_message.ContentElement] = [] if ret.text is not None: - content.append(llm_entities.ContentElement.from_text(ret.text)) + content.append(provider_message.ContentElement.from_text(ret.text)) if ret.image_url is not None: - content.append(llm_entities.ContentElement.from_image_url(ret.image_url)) + content.append(provider_message.ContentElement.from_image_url(ret.image_url)) query.resp_messages.append( - llm_entities.Message( + provider_message.Message( role='command', content=content, ) diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index 3f3ef512..8cdfd204 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -16,7 +16,6 @@ from ..logger import EventLogger class AiocqhttpMessageConverter(adapter.MessageConverter): - @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -78,8 +77,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): return msg_list, msg_id, msg_time @staticmethod - async def target2yiri(message: str, message_id: int = -1,bot=None): - print(message) + async def target2yiri(message: str, message_id: int = -1, bot=None): message = aiocqhttp.Message(message) def get_face_name(face_id): @@ -119,30 +117,28 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): return face_code_dict.get(face_id,'') async def process_message_data(msg_data, reply_list): - if msg_data["type"] == "image": - image_base64, image_format = await image.qq_image_url_to_base64(msg_data["data"]['url']) - reply_list.append( - platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}')) + if msg_data['type'] == 'image': + image_base64, image_format = await image.qq_image_url_to_base64(msg_data['data']['url']) + reply_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}')) - elif msg_data["type"] == "text": - reply_list.append(platform_message.Plain(text=msg_data["data"]["text"])) + elif msg_data['type'] == 'text': + reply_list.append(platform_message.Plain(text=msg_data['data']['text'])) - elif msg_data["type"] == "forward": # 这里来应该传入转发消息组,暂时传入qoute - for forward_msg_datas in msg_data["data"]["content"]: - for forward_msg_data in forward_msg_datas["message"]: + elif msg_data['type'] == 'forward': # 这里来应该传入转发消息组,暂时传入qoute + for forward_msg_datas in msg_data['data']['content']: + for forward_msg_data in forward_msg_datas['message']: await process_message_data(forward_msg_data, reply_list) - elif msg_data["type"] == "at": - if msg_data["data"]['qq'] == 'all': + elif msg_data['type'] == 'at': + if msg_data['data']['qq'] == 'all': reply_list.append(platform_message.AtAll()) else: reply_list.append( platform_message.At( - target=msg_data["data"]['qq'], + target=msg_data['data']['qq'], ) ) - yiri_msg_list = [] yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) @@ -178,14 +174,15 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): # await process_message_data(msg_data, yiri_msg_list) pass - elif msg.type == 'reply': # 此处处理引用消息传入Qoute - msg_datas = await bot.get_msg(message_id=msg.data["id"]) + msg_datas = await bot.get_msg(message_id=msg.data['id']) - for msg_data in msg_datas["message"]: + for msg_data in msg_datas['message']: await process_message_data(msg_data, reply_list) - reply_msg = platform_message.Quote(message_id=msg.data["id"],sender_id=msg_datas["user_id"],origin=reply_list) + reply_msg = platform_message.Quote( + message_id=msg.data['id'], sender_id=msg_datas['user_id'], origin=reply_list + ) yiri_msg_list.append(reply_msg) elif msg.type == 'file': @@ -210,32 +207,19 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): face_id = msg.data['result'] yiri_msg_list.append(platform_message.Face(face_type='dice',face_id=int(face_id),face_name='骰子')) - - - - - - - - chain = platform_message.MessageChain(yiri_msg_list) return chain - - - - class AiocqhttpEventConverter(adapter.EventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): return event.source_platform_object @staticmethod - async def target2yiri(event: aiocqhttp.Event,bot=None): - yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id,bot) - + async def target2yiri(event: aiocqhttp.Event, bot=None): + yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot) if event.message_type == 'group': @@ -345,7 +329,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id try: - return await callback(await self.event_converter.target2yiri(event,self.bot), self) + return await callback(await self.event_converter.target2yiri(event, self.bot), self) except Exception: await self.logger.error(f'Error in on_message: {traceback.format_exc()}') traceback.print_exc() diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index d1116362..f8faf522 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -378,15 +378,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter): if 'im.message.receive_v1' == type: try: event = await self.event_converter.target2yiri(p2v1, self.api_client) - except Exception as e: - await self.logger.error(f"Error in lark callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return {'code': 200, 'message': 'ok'} - except Exception as e: - await self.logger.error(f"Error in lark callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in lark callback: {traceback.format_exc()}') return {'code': 500, 'message': 'error'} async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1): diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/nakuru.py index 389a2db1..16ad54db 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/nakuru.py @@ -72,8 +72,9 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): content=content_list, ) nakuru_forward_node_list.append(nakuru_forward_node) - except Exception as e: + except Exception: import traceback + traceback.print_exc() nakuru_msg_list.append(nakuru_forward_node_list) @@ -276,7 +277,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): # 注册监听器 self.bot.receiver(source_cls.__name__)(listener_wrapper) except Exception as e: - self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}") + self.logger.error(f'Error in nakuru register_listener: {traceback.format_exc()}') raise e def unregister_listener( diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 030db56d..3fc1e393 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -125,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in officialaccount callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/qqbotpy.py index 39c8dc8a..d4a4d526 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/qqbotpy.py @@ -501,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): for event_handler in event_handler_mapping[event_type]: setattr(self.bot, event_handler, wrapper) except Exception as e: - self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}") + self.logger.error(f'Error in qqbotpy callback: {traceback.format_exc()}') raise e def unregister_listener( diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index c61afea4..63ab531f 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -154,10 +154,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): raise ParamNotEnoughError('QQ官方机器人缺少相关配置项,请查看文档或联系管理员') self.bot = QQOfficialClient( - app_id=config['appid'], - secret=config['secret'], - token=config['token'], - logger=self.logger + app_id=config['appid'], secret=config['secret'], token=config['token'], logger=self.logger ) async def reply_message( @@ -224,8 +221,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = 'justbot' try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in qqofficial callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 6dfcff59..1bd5aa2d 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -104,7 +104,9 @@ class SlackAdapter(adapter.MessagePlatformAdapter): if missing_keys: raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') - self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger) + self.bot = SlackClient( + bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger + ) async def reply_message( self, @@ -139,8 +141,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = 'SlackBot' try: return await callback(await self.event_converter.target2yiri(event, self.bot), self) - except Exception as e: - await self.logger.error(f"Error in slack callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in slack callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('im')(on_message) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 266d994e..c2fcc22e 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -160,8 +160,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): try: lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id) await self.listeners[type(lb_event)](lb_event, self) - except Exception as e: - await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') self.application = ApplicationBuilder().token(self.config['token']).build() self.bot = self.application.bot diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index fdd4a69b..88ec9bd9 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -1,5 +1,4 @@ import requests -import websockets import websocket import json import time @@ -10,53 +9,42 @@ from libs.wechatpad_api.client import WeChatPadClient import typing import asyncio import traceback -import time import re import base64 -import uuid -import json import os import copy -import datetime import threading import quart -import aiohttp from .. import adapter -from ...pipeline.longtext.strategies import forward from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities -from ...utils import image from ..logger import EventLogger import xml.etree.ElementTree as ET -from typing import Optional, List, Tuple +from typing import Optional, Tuple from functools import partial import logging -class WeChatPadMessageConverter(adapter.MessageConverter): +class WeChatPadMessageConverter(adapter.MessageConverter): def __init__(self, config: dict): self.config = config - self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"]) - self.logger = logging.getLogger("WeChatPadMessageConverter") + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) + self.logger = logging.getLogger('WeChatPadMessageConverter') @staticmethod - async def yiri2target( - message_chain: platform_message.MessageChain - ) -> list[dict]: + async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: content_list = [] - current_file_path = os.path.abspath(__file__) - - + _ = os.path.abspath(__file__) for component in message_chain: if isinstance(component, platform_message.At): - content_list.append({"type": "at", "target": component.target}) + content_list.append({'type': 'at', 'target': component.target}) elif isinstance(component, platform_message.Plain): - content_list.append({"type": "text", "content": component.text}) + content_list.append({'type': 'text', 'content': component.text}) elif isinstance(component, platform_message.Image): if component.url: async with httpx.AsyncClient() as client: @@ -68,15 +56,16 @@ class WeChatPadMessageConverter(adapter.MessageConverter): else: raise Exception('获取文件失败') # pass - content_list.append({"type": "image", "image": base64_str}) + content_list.append({'type': 'image', 'image': base64_str}) elif component.base64: - content_list.append({"type": "image", "image": component.base64}) + content_list.append({'type': 'image', 'image': component.base64}) elif isinstance(component, platform_message.WeChatEmoji): content_list.append( - {'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size}) + {'type': 'WeChatEmoji', 'emoji_md5': component.emoji_md5, 'emoji_size': component.emoji_size} + ) elif isinstance(component, platform_message.Voice): - content_list.append({"type": "voice", "data": component.url, "duration": component.length, "forma": 0}) + content_list.append({'type': 'voice', 'data': component.url, 'duration': component.length, 'forma': 0}) elif isinstance(component, platform_message.WeChatAppMsg): content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg}) elif isinstance(component, platform_message.Forward): @@ -86,28 +75,23 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return content_list - - async def target2yiri( - self, - message: dict, - bot_account_id: str - ) -> platform_message.MessageChain: + async def target2yiri(self, message: dict, bot_account_id: str) -> platform_message.MessageChain: """外部消息转平台消息""" # 数据预处理 message_list = [] ats_bot = False # 是否被@ - content = message["content"]["str"] + content = message['content']['str'] content_no_preifx = content # 群消息则去掉前缀 is_group_message = self._is_group_message(message) if is_group_message: ats_bot = self._ats_bot(message, bot_account_id) - if "@所有人" in content: + if '@所有人' in content: message_list.append(platform_message.AtAll()) elif ats_bot: message_list.append(platform_message.At(target=bot_account_id)) content_no_preifx, _ = self._extract_content_and_sender(content) - msg_type = message["msg_type"] + msg_type = message['msg_type'] # 映射消息类型到处理器方法 handler_map = { @@ -129,11 +113,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_list) - async def _handler_text( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_text(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理文本消息 (msg_type=1)""" if message and self._is_group_message(message): pattern = r'@\S{1,20}' @@ -141,16 +121,12 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain([platform_message.Plain(content_no_preifx)]) - async def _handler_image( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_image(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理图像消息 (msg_type=3)""" try: image_xml = content_no_preifx if not image_xml: - return platform_message.MessageChain([platform_message.Unknown("[图片内容为空]")]) + return platform_message.MessageChain([platform_message.Unknown('[图片内容为空]')]) root = ET.fromstring(image_xml) # 提取img标签的属性 @@ -160,28 +136,22 @@ class WeChatPadMessageConverter(adapter.MessageConverter): cdnthumburl = img_tag.get('cdnthumburl') # cdnmidimgurl = img_tag.get('cdnmidimgurl') - image_data = self.bot.cdn_download(aeskey=aeskey, file_type=1, file_url=cdnthumburl) - if image_data["Data"]['FileData'] == '': + if image_data['Data']['FileData'] == '': image_data = self.bot.cdn_download(aeskey=aeskey, file_type=2, file_url=cdnthumburl) - base64_str = image_data["Data"]['FileData'] + base64_str = image_data['Data']['FileData'] # self.logger.info(f"data:image/png;base64,{base64_str}") - elements = [ - platform_message.Image(base64=f"data:image/png;base64,{base64_str}"), + platform_message.Image(base64=f'data:image/png;base64,{base64_str}'), # platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发 ] return platform_message.MessageChain(elements) except Exception as e: - self.logger.error(f"处理图片失败: {str(e)}") - return platform_message.MessageChain([platform_message.Unknown("[图片处理失败]")]) + self.logger.error(f'处理图片失败: {str(e)}') + return platform_message.MessageChain([platform_message.Unknown('[图片处理失败]')]) - async def _handler_voice( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_voice(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理语音消息 (msg_type=34)""" message_List = [] try: @@ -197,39 +167,33 @@ class WeChatPadMessageConverter(adapter.MessageConverter): bufid = voicemsg.get('bufid') length = voicemsg.get('voicelength') voice_data = self.bot.get_msg_voice(buf_id=str(bufid), length=int(length), msgid=str(new_msg_id)) - audio_base64 = voice_data["Data"]['Base64'] + audio_base64 = voice_data['Data']['Base64'] # 验证语音数据有效性 if not audio_base64: - message_List.append(platform_message.Unknown(text="[语音内容为空]")) + message_List.append(platform_message.Unknown(text='[语音内容为空]')) return platform_message.MessageChain(message_List) # 转换为平台支持的语音格式(如 Silk 格式) - voice_element = platform_message.Voice( - base64=f"data:audio/silk;base64,{audio_base64}" - ) + voice_element = platform_message.Voice(base64=f'data:audio/silk;base64,{audio_base64}') message_List.append(voice_element) except KeyError as e: - self.logger.error(f"语音数据字段缺失: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音数据解析失败]")) + self.logger.error(f'语音数据字段缺失: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音数据解析失败]')) except Exception as e: - self.logger.error(f"处理语音消息异常: {str(e)}") - message_List.append(platform_message.Unknown(text="[语音处理失败]")) + self.logger.error(f'处理语音消息异常: {str(e)}') + message_List.append(platform_message.Unknown(text='[语音处理失败]')) return platform_message.MessageChain(message_List) - async def _handler_compound( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_compound(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理复合消息 (msg_type=49),根据子类型分派""" try: xml_data = ET.fromstring(content_no_preifx) appmsg_data = xml_data.find('.//appmsg') if appmsg_data: - data_type = appmsg_data.findtext('.//type', "") + data_type = appmsg_data.findtext('.//type', '') # 二次分派处理器 sub_handler_map = { '57': self._handler_compound_quote, @@ -238,9 +202,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter): '74': self._handler_compound_file, '33': self._handler_compound_mini_program, '36': self._handler_compound_mini_program, - '2000': partial(self._handler_compound_unsupported, text="[转账消息]"), - '2001': partial(self._handler_compound_unsupported, text="[红包消息]"), - '51': partial(self._handler_compound_unsupported, text="[视频号消息]"), + '2000': partial(self._handler_compound_unsupported, text='[转账消息]'), + '2001': partial(self._handler_compound_unsupported, text='[红包消息]'), + '51': partial(self._handler_compound_unsupported, text='[视频号消息]'), } handler = sub_handler_map.get(data_type, self._handler_compound_unsupported) @@ -251,56 +215,54 @@ class WeChatPadMessageConverter(adapter.MessageConverter): else: return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) except Exception as e: - self.logger.error(f"解析复合消息失败: {str(e)}") + self.logger.error(f'解析复合消息失败: {str(e)}') return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)]) async def _handler_compound_quote( - self, - message: Optional[dict], - xml_data: ET.Element + self, message: Optional[dict], xml_data: ET.Element ) -> platform_message.MessageChain: """处理引用消息 (data_type=57)""" message_list = [] -# self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) + # self.logger.info("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode')) appmsg_data = xml_data.find('.//appmsg') - quote_data = "" # 引用原文 + quote_data = '' # 引用原文 quote_id = None # 引用消息的原发送者 tousername = None # 接收方: 所属微信的wxid - user_data = "" # 用户消息 + user_data = '' # 用户消息 sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member # 引用消息转发 if appmsg_data: - user_data = appmsg_data.findtext('.//title') or "" + user_data = appmsg_data.findtext('.//title') or '' quote_data = appmsg_data.find('.//refermsg').findtext('.//content') quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') - message_list.append( - platform_message.WeChatAppMsg( - app_msg=ET.tostring(appmsg_data, encoding='unicode')) - ) + message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg_data, encoding='unicode'))) if message: - tousername = message['to_user_name']["str"] - + tousername = message['to_user_name']['str'] + + _ = tousername + _ = quote_id + if quote_data: quote_data_message_list = platform_message.MessageChain() # 文本消息 try: - if "" not in quote_data: + if '' not in quote_data: quote_data_message_list.append(platform_message.Plain(quote_data)) else: # 引用消息展开 quote_data_xml = ET.fromstring(quote_data) - if quote_data_xml.find("img"): + if quote_data_xml.find('img'): quote_data_message_list.extend(await self._handler_image(None, quote_data)) - elif quote_data_xml.find("voicemsg"): + elif quote_data_xml.find('voicemsg'): quote_data_message_list.extend(await self._handler_voice(None, quote_data)) - elif quote_data_xml.find("videomsg"): + elif quote_data_xml.find('videomsg'): quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理 else: # appmsg quote_data_message_list.extend(await self._handler_compound(None, quote_data)) except Exception as e: - self.logger.error(f"处理引用消息异常 expcetion:{e}") + self.logger.error(f'处理引用消息异常 expcetion:{e}') quote_data_message_list.append(platform_message.Plain(quote_data)) message_list.append( platform_message.Quote( @@ -315,11 +277,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_list) - async def _handler_compound_file( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_file(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理文件消息 (data_type=6)""" file_data = xml_data.find('.//appmsg') @@ -357,11 +315,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): platform_message.WeChatForwardFile(xml_data=xml_data_str) ]) - async def _handler_compound_link( - self, - message: dict, - xml_data: ET.Element - ) -> platform_message.MessageChain: + async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" message_list = [] try: @@ -374,56 +328,38 @@ class WeChatPadMessageConverter(adapter.MessageConverter): link_title=appmsg.findtext('title', ''), link_desc=appmsg.findtext('des', ''), link_url=appmsg.findtext('url', ''), - link_thumb_url=appmsg.findtext("thumburl", '') # 这个字段拿不到 + link_thumb_url=appmsg.findtext('thumburl', ''), # 这个字段拿不到 ) ) # 还没有发链接的接口, 暂时还需要自己构造appmsg, 先用WeChatAppMsg。 - message_list.append( - platform_message.WeChatAppMsg( - app_msg=ET.tostring(appmsg, encoding='unicode') - ) - ) + message_list.append(platform_message.WeChatAppMsg(app_msg=ET.tostring(appmsg, encoding='unicode'))) except Exception as e: - self.logger.error(f"解析链接消息失败: {str(e)}") + self.logger.error(f'解析链接消息失败: {str(e)}') return platform_message.MessageChain(message_list) async def _handler_compound_mini_program( - self, - message: dict, - xml_data: ET.Element + self, message: dict, xml_data: ET.Element ) -> platform_message.MessageChain: """处理小程序消息(如小程序卡片、服务通知)""" xml_data_str = ET.tostring(xml_data, encoding='unicode') - return platform_message.MessageChain([ - platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str) - ]) + return platform_message.MessageChain([platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)]) - async def _handler_default( - self, - message: Optional[dict], - content_no_preifx: str - ) -> platform_message.MessageChain: + async def _handler_default(self, message: Optional[dict], content_no_preifx: str) -> platform_message.MessageChain: """处理未知消息类型""" if message: - msg_type = message["msg_type"] + msg_type = message['msg_type'] else: - msg_type = "" - return platform_message.MessageChain([ - platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]") - ]) + msg_type = '' + return platform_message.MessageChain([platform_message.Unknown(text=f'[未知消息类型 msg_type:{msg_type}]')]) def _handler_compound_unsupported( - self, - message: dict, - xml_data: str, - text: Optional[str] = None + self, message: dict, xml_data: str, text: Optional[str] = None ) -> platform_message.MessageChain: """处理未支持复合消息类型(msg_type=49)子类型""" if not text: - text = f"[xml_data={xml_data}]" + text = f'[xml_data={xml_data}]' content_list = [] - content_list.append( - platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}")) + content_list.append(platform_message.Unknown(text=f'[处理未支持复合消息类型[msg_type=49]|{text}')) return platform_message.MessageChain(content_list) @@ -432,7 +368,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): ats_bot = False try: to_user_name = message['to_user_name']['str'] # 接收方: 所属微信的wxid - raw_content = message["content"]["str"] # 原始消息内容 + raw_content = message['content']['str'] # 原始消息内容 content_no_prefix, _ = self._extract_content_and_sender(raw_content) # 直接艾特机器人(这个有bug,当被引用的消息里面有@bot,会套娃 # ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix) @@ -443,7 +379,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): msg_source = message.get('msg_source', '') or '' if len(msg_source) > 0: msg_source_data = ET.fromstring(msg_source) - at_user_list = msg_source_data.findtext("atuserlist") or "" + at_user_list = msg_source_data.findtext('atuserlist') or '' ats_bot = ats_bot or (to_user_name in at_user_list) # 引用bot if message.get('msg_type', 0) == 49: @@ -454,7 +390,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者 ats_bot = ats_bot or (quote_id == tousername) except Exception as e: - self.logger.error(f"_ats_bot got except: {e}") + self.logger.error(f'_ats_bot got except: {e}') finally: return ats_bot @@ -463,47 +399,41 @@ class WeChatPadMessageConverter(adapter.MessageConverter): try: # 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 # add: 有些用户的wxid不是上述格式。换成user_name: - regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:") - line_split = raw_content.split("\n") + regex = re.compile(r'^[a-zA-Z0-9_\-]{5,20}:') + line_split = raw_content.split('\n') if len(line_split) > 0 and regex.match(line_split[0]): - raw_content = "\n".join(line_split[1:]) - sender_id = line_split[0].strip(":") + raw_content = '\n'.join(line_split[1:]) + sender_id = line_split[0].strip(':') return raw_content, sender_id except Exception as e: - self.logger.error(f"_extract_content_and_sender got except: {e}") + self.logger.error(f'_extract_content_and_sender got except: {e}') finally: return raw_content, None # 是否是群消息 def _is_group_message(self, message: dict) -> bool: from_user_name = message['from_user_name']['str'] - return from_user_name.endswith("@chatroom") + return from_user_name.endswith('@chatroom') class WeChatPadEventConverter(adapter.EventConverter): - def __init__(self, config: dict): self.config = config self.message_converter = WeChatPadMessageConverter(config) - self.logger = logging.getLogger("WeChatPadEventConverter") - + self.logger = logging.getLogger('WeChatPadEventConverter') + @staticmethod - async def yiri2target( - event: platform_events.MessageEvent - ) -> dict: + async def yiri2target(event: platform_events.MessageEvent) -> dict: pass - async def target2yiri( - self, - event: dict, - bot_account_id: str - ) -> platform_events.MessageEvent: - + async def target2yiri(self, event: dict, bot_account_id: str) -> platform_events.MessageEvent: # 排除公众号以及微信团队消息 - if event['from_user_name']['str'].startswith('gh_') \ - or event['from_user_name']['str']=='weixin'\ - or event['from_user_name']['str'] == "newsapp"\ - or event['from_user_name']['str'] == self.config["wxid"]: + if ( + event['from_user_name']['str'].startswith('gh_') + or event['from_user_name']['str'] == 'weixin' + or event['from_user_name']['str'] == 'newsapp' + or event['from_user_name']['str'] == self.config['wxid'] + ): return None message_chain = await self.message_converter.target2yiri(copy.deepcopy(event), bot_account_id) @@ -512,7 +442,7 @@ class WeChatPadEventConverter(adapter.EventConverter): if '@chatroom' in event['from_user_name']['str']: # 找出开头的 wxid_ 字符串,以:结尾 - sender_wxid = event['content']['str'].split(":")[0] + sender_wxid = event['content']['str'].split(':')[0] return platform_events.GroupMessage( sender=platform_entities.GroupMember( @@ -524,13 +454,13 @@ class WeChatPadEventConverter(adapter.EventConverter): name=event['from_user_name']['str'], permission=platform_entities.Permission.Member, ), - special_title="", + special_title='', join_timestamp=0, last_speak_timestamp=0, mute_time_remaining=0, ), message_chain=message_chain, - time=event["create_time"], + time=event['create_time'], source_platform_object=event, ) else: @@ -541,13 +471,13 @@ class WeChatPadEventConverter(adapter.EventConverter): remark='', ), message_chain=message_chain, - time=event["create_time"], + time=event['create_time'], source_platform_object=event, ) class WeChatPadAdapter(adapter.MessagePlatformAdapter): - name: str = "WeChatPad" # 定义适配器名称 + name: str = 'WeChatPad' # 定义适配器名称 bot: WeChatPadClient quart_app: quart.Quart @@ -580,27 +510,21 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # self.ap.logger.debug(f"Gewechat callback event: {data}") # print(data) - try: event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) - except Exception as e: - await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wechatpad callback: {traceback.format_exc()}') if event.__class__ in self.listeners: await self.listeners[event.__class__](event, self) return 'ok' - - async def _handle_message( - self, - message: platform_message.MessageChain, - target_id: str - ): + async def _handle_message(self, message: platform_message.MessageChain, target_id: str): """统一消息处理核心逻辑""" content_list = await self.message_converter.yiri2target(message) # print(content_list) - at_targets = [item["target"] for item in content_list if item["type"] == "at"] + at_targets = [item['target'] for item in content_list if item['type'] == 'at'] # print(at_targets) # 处理@逻辑 at_targets = at_targets or [] @@ -608,7 +532,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if at_targets: member_info = self.bot.get_chatroom_member_detail( target_id, - )["Data"]["member_data"]["chatroom_member_list"] + )['Data']['member_data']['chatroom_member_list'] # 处理消息组件 for msg in content_list: @@ -616,63 +540,51 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if msg['type'] == 'text' and at_targets: at_nick_name_list = [] for member in member_info: - if member["user_name"] in at_targets: + if member['user_name'] in at_targets: at_nick_name_list.append(f'@{member["nick_name"]}') msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}' # 统一消息派发 handler_map = { 'text': lambda msg: self.bot.send_text_message( - to_wxid=target_id, - message=msg['content'], - ats=at_targets + to_wxid=target_id, message=msg['content'], ats=at_targets ), 'image': lambda msg: self.bot.send_image_message( - to_wxid=target_id, - img_url=msg["image"], - ats = at_targets + to_wxid=target_id, img_url=msg['image'], ats=at_targets ), 'WeChatEmoji': lambda msg: self.bot.send_emoji_message( - to_wxid=target_id, - emoji_md5=msg['emoji_md5'], - emoji_size=msg['emoji_size'] + to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'] ), - 'voice': lambda msg: self.bot.send_voice_message( to_wxid=target_id, voice_data=msg['data'], - voice_duration=msg["duration"], - voice_forma=msg["forma"], + voice_duration=msg['duration'], + voice_forma=msg['forma'], ), 'WeChatAppMsg': lambda msg: self.bot.send_app_message( to_wxid=target_id, app_message=msg['app_msg'], type=0, ), - 'at': lambda msg: None + 'at': lambda msg: None, } if handler := handler_map.get(msg['type']): handler(msg) # self.ap.logger.warning(f"未处理的消息类型: {ret}") else: - self.ap.logger.warning(f"未处理的消息类型: {msg['type']}") + self.ap.logger.warning(f'未处理的消息类型: {msg["type"]}') continue - async def send_message( - self, - target_type: str, - target_id: str, - message: platform_message.MessageChain - ): + async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): """主动发送消息""" return await self._handle_message(message, target_id) async def reply_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False + self, + message_source: platform_events.MessageEvent, + message: platform_message.MessageChain, + quote_origin: bool = False, ): """回复消息""" if message_source.source_platform_object: @@ -683,58 +595,49 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): pass def register_listener( - self, - event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): self.listeners[event_type] = callback def unregister_listener( - self, - event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None] + self, + event_type: typing.Type[platform_events.Event], + callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ): pass async def run_async(self): - - if not self.config["admin_key"] and not self.config["token"]: - raise RuntimeError("无wechatpad管理密匙,请填入配置文件后重启") + if not self.config['admin_key'] and not self.config['token']: + raise RuntimeError('无wechatpad管理密匙,请填入配置文件后重启') else: - if self.config["token"]: - self.bot = WeChatPadClient( - self.config['wechatpad_url'], - self.config["token"] - ) + if self.config['token']: + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) data = self.bot.get_login_status() self.ap.logger.info(data) - if data["Code"] == 300 and data["Text"] == "你已退出微信": + if data['Code'] == 300 and data['Text'] == '你已退出微信': response = requests.post( - f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}", - json={"Count": 1, "Days": 365} + f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', + json={'Count': 1, 'Days': 365}, ) if response.status_code != 200: - raise Exception(f"获取token失败: {response.text}") - self.config["token"] = response.json()["Data"][0] + raise Exception(f'获取token失败: {response.text}') + self.config['token'] = response.json()['Data'][0] - elif not self.config["token"]: + elif not self.config['token']: response = requests.post( - f"{self.config['wechatpad_url']}/admin/GenAuthKey1?key={self.config['admin_key']}", - json={"Count": 1, "Days": 365} + f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', + json={'Count': 1, 'Days': 365}, ) if response.status_code != 200: - raise Exception(f"获取token失败: {response.text}") - self.config["token"] = response.json()["Data"][0] + raise Exception(f'获取token失败: {response.text}') + self.config['token'] = response.json()['Data'][0] - self.bot = WeChatPadClient( - self.config['wechatpad_url'], - self.config["token"], - logger=self.logger - ) - self.ap.logger.info(self.config["token"]) + self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token'], logger=self.logger) + self.ap.logger.info(self.config['token']) thread_1 = threading.Event() - def wechat_login_process(): # 不登录,这些先注释掉,避免登陆态尝试拉qrcode。 # login_data =self.bot.get_login_qr() @@ -742,67 +645,54 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # url = login_data['Data']["QrCodeUrl"] # self.ap.logger.info(login_data) - - profile =self.bot.get_profile() + profile = self.bot.get_profile() self.ap.logger.info(profile) - self.bot_account_id = profile["Data"]["userInfo"]["nickName"]["str"] - self.config["wxid"] = profile["Data"]["userInfo"]["userName"]["str"] + self.bot_account_id = profile['Data']['userInfo']['nickName']['str'] + self.config['wxid'] = profile['Data']['userInfo']['userName']['str'] thread_1.set() - # asyncio.create_task(wechat_login_process) threading.Thread(target=wechat_login_process).start() def connect_websocket_sync() -> None: - thread_1.wait() - uri = f"{self.config['wechatpad_ws']}/GetSyncMsg?key={self.config['token']}" - self.ap.logger.info(f"Connecting to WebSocket: {uri}") + uri = f'{self.config["wechatpad_ws"]}/GetSyncMsg?key={self.config["token"]}' + self.ap.logger.info(f'Connecting to WebSocket: {uri}') + def on_message(ws, message): try: data = json.loads(message) - self.ap.logger.debug(f"Received message: {data}") + self.ap.logger.debug(f'Received message: {data}') # 这里需要确保ws_message是同步的,或者使用asyncio.run调用异步方法 asyncio.run(self.ws_message(data)) except json.JSONDecodeError: - self.ap.logger.error(f"Non-JSON message: {message[:100]}...") + self.ap.logger.error(f'Non-JSON message: {message[:100]}...') def on_error(ws, error): - self.ap.logger.error(f"WebSocket error: {str(error)[:200]}") + self.ap.logger.error(f'WebSocket error: {str(error)[:200]}') def on_close(ws, close_status_code, close_msg): - self.ap.logger.info("WebSocket closed, reconnecting...") + self.ap.logger.info('WebSocket closed, reconnecting...') time.sleep(5) connect_websocket_sync() # 自动重连 def on_open(ws): - self.ap.logger.info("WebSocket connected successfully!") + self.ap.logger.info('WebSocket connected successfully!') ws = websocket.WebSocketApp( - uri, - on_message=on_message, - on_error=on_error, - on_close=on_close, - on_open=on_open - ) - ws.run_forever( - ping_interval=60, - ping_timeout=20 + uri, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open ) + ws.run_forever(ping_interval=60, ping_timeout=20) # 直接调用同步版本(会阻塞) # connect_websocket_sync() # 这行代码会在WebSocket连接断开后才会执行 # self.ap.logger.info("WebSocket client thread started") - thread = threading.Thread( - target=connect_websocket_sync, - name="WebSocketClientThread", - daemon=True - ) + thread = threading.Thread(target=connect_websocket_sync, name='WebSocketClientThread', daemon=True) thread.start() - self.ap.logger.info("WebSocket client thread started") + self.ap.logger.info('WebSocket client thread started') async def kill(self) -> bool: pass diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index f1cc677e..7be05a85 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -157,7 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter): token=config['token'], EncodingAESKey=config['EncodingAESKey'], contacts_secret=config['contacts_secret'], - logger=self.logger + logger=self.logger, ) async def reply_message( @@ -201,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wecom callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index aab8d394..da84ac6d 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -145,7 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): secret=config['secret'], token=config['token'], EncodingAESKey=config['EncodingAESKey'], - logger=self.logger + logger=self.logger, ) async def reply_message( @@ -178,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): self.bot_account_id = event.receiver_id try: return await callback(await self.event_converter.target2yiri(event), self) - except Exception as e: - await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}") + except Exception: + await self.logger.error(f'Error in wecomcs callback: {traceback.format_exc()}') if event_type == platform_events.FriendMessage: self.bot.on_message('text')(on_message) diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py index 61e84714..777b61d6 100644 --- a/pkg/plugin/events.py +++ b/pkg/plugin/events.py @@ -7,6 +7,7 @@ import pydantic.v1 as pydantic from ..core import entities as core_entities from ..provider import entities as llm_entities from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.provider.session as provider_session class BaseEventModel(pydantic.BaseModel): @@ -139,7 +140,7 @@ class NormalMessageResponded(BaseEventModel): sender_id: typing.Union[int, str] - session: core_entities.Session + session: provider_session.Session """会话对象""" prefix: str diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 056b45d2..7a6a299f 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -73,3 +73,18 @@ class RuntimeConnectionHandler(handler.Handler): ) return result['plugins'] + + async def emit_event( + self, + event_context: dict[str, Any], + ) -> dict[str, Any]: + """Emit event""" + result = await self.call_action( + LangBotToRuntimeAction.EMIT_EVENT, + { + 'event_context': event_context, + }, + timeout=10, + ) + + return result['event_context'] diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index 7bc5631b..c94b0d7d 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -7,7 +7,7 @@ import traceback from .. import loader, events, context, models from ...core import entities as core_entities -from ...provider.tools import entities as tools_entities +from langbot_plugin.api.entities.builtin.resource import tool as resource_tool from ...utils import funcschema from ...discover import engine as discover_engine @@ -101,7 +101,7 @@ class PluginLoader(loader.PluginLoader): async def handler(plugin: context.BasePlugin, query: core_entities.Query, *args, **kwargs): return func(*args, **kwargs) - llm_function = tools_entities.LLMFunction( + llm_function = resource_tool.LLMTool( name=function_name, human_desc='', description=function_schema['description'], @@ -147,7 +147,7 @@ class PluginLoader(loader.PluginLoader): 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( + llm_function = resource_tool.LLMTool( name=function_name, human_desc='', description=function_schema['description'], diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py index cce6c9e3..c5a78078 100644 --- a/pkg/plugin/loaders/manifest.py +++ b/pkg/plugin/loaders/manifest.py @@ -8,7 +8,7 @@ from ...core import app from .. import context, events from .. import loader from ...utils import funcschema -from ...provider.tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class PluginManifestLoader(loader.PluginLoader): @@ -41,7 +41,7 @@ class PluginManifestLoader(loader.PluginLoader): 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( + llm_function = resource_tool.LLMTool( name=function_name, human_desc='', description=function_schema['description'], diff --git a/pkg/provider/modelmgr/modelmgr.py b/pkg/provider/modelmgr/modelmgr.py index b15e53a9..27621e6d 100644 --- a/pkg/provider/modelmgr/modelmgr.py +++ b/pkg/provider/modelmgr/modelmgr.py @@ -3,7 +3,7 @@ from __future__ import annotations import sqlalchemy import traceback -from . import entities, requester +from . import requester from ...core import app from ...discover import engine from . import token @@ -16,14 +16,6 @@ FETCH_MODEL_LIST_URL = 'https://api.qchatgpt.rockchin.top/api/v2/fetch/model_lis class ModelManager: """模型管理器""" - model_list: list[entities.LLMModelInfo] # deprecated - - requesters: dict[str, requester.LLMAPIRequester] # deprecated - - token_mgrs: dict[str, token.TokenManager] # deprecated - - # ====== 4.0 ====== - ap: app.Application llm_models: list[requester.RuntimeLLMModel] @@ -34,9 +26,6 @@ class ModelManager: def __init__(self, ap: app.Application): self.ap = ap - self.model_list = [] - self.requesters = {} - self.token_mgrs = {} self.llm_models = [] self.requester_components = [] self.requester_dict = {} @@ -109,14 +98,7 @@ class ModelManager: runtime_llm_model = await self.init_runtime_llm_model(model_info) self.llm_models.append(runtime_llm_model) - async def get_model_by_name(self, name: str) -> entities.LLMModelInfo: # deprecated - """通过名称获取模型""" - for model in self.model_list: - if model.name == name: - return model - raise ValueError(f'无法确定模型 {name} 的信息') - - async def get_model_by_uuid(self, uuid: str) -> entities.LLMModelInfo: + async def get_model_by_uuid(self, uuid: str) -> requester.RuntimeLLMModel: """通过uuid获取模型""" for model in self.llm_models: if model.model_entity.uuid == uuid: diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 244f4c82..4008ca16 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -6,8 +6,8 @@ import typing from ...core import app from ...core import entities as core_entities from .. import entities as llm_entities -from ..tools import entities as tools_entities from ...entity.persistence import model as persistence_model +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from . import token @@ -59,7 +59,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): query: core_entities.Query, model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: """调用API diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 38573854..4655b3e0 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -11,8 +11,8 @@ from .. import errors, requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities from ....utils import image +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class AnthropicMessages(requester.LLMAPIRequester): @@ -51,7 +51,7 @@ class AnthropicMessages(requester.LLMAPIRequester): query: core_entities.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = model.token_mgr.get_token() diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 513086e5..00ff0a41 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -10,7 +10,7 @@ import httpx from .. import errors, requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class OpenAIChatCompletions(requester.LLMAPIRequester): @@ -63,7 +63,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -104,7 +104,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index 6d664b01..6dced3c9 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -6,7 +6,7 @@ from . import chatcmpl from .. import errors, requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -22,7 +22,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 3795ef99..26da7d6d 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -7,7 +7,7 @@ from . import chatcmpl from .. import requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -23,7 +23,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index b8868f4d..e46d102e 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -11,7 +11,7 @@ import httpx from .. import entities, errors, requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class ModelScopeChatCompletions(requester.LLMAPIRequester): @@ -128,7 +128,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() @@ -169,7 +169,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, model: entities.LLMModelInfo, messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index f3621a09..e5019426 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -7,7 +7,7 @@ from . import chatcmpl from .. import requester from ....core import entities as core_entities from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -23,7 +23,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: self.client.api_key = use_model.token_mgr.get_token() diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index 2ea4bb7d..2afe34b3 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -11,7 +11,7 @@ import ollama from .. import errors, requester from ... import entities as llm_entities -from ...tools import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from ....core import entities as core_entities REQUESTER_NAME: str = 'ollama-chat' @@ -42,7 +42,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, - use_funcs: list[tools_entities.LLMFunction] = None, + use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: args = extra_args.copy() @@ -108,7 +108,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): query: core_entities.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], - funcs: typing.List[tools_entities.LLMFunction] = None, + funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, ) -> llm_entities.Message: req_messages: list = [] diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 7d5e04c5..e87ee81d 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -18,13 +18,15 @@ class LocalAgentRunner(runner.RequestRunner): req_messages = query.prompt.messages.copy() + query.messages.copy() + [query.user_message] + use_llm_model = await self.ap.model_mgr.get_model_by_uuid(query.use_llm_model_uuid) + # 首次请求 - msg = await query.use_llm_model.requester.invoke_llm( + msg = await use_llm_model.requester.invoke_llm( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, ) yield msg @@ -61,12 +63,12 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(err_msg) # 处理完所有调用,再次请求 - msg = await query.use_llm_model.requester.invoke_llm( + msg = await use_llm_model.requester.invoke_llm( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, ) yield msg diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index f54b50e7..500ab49c 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -3,7 +3,8 @@ from __future__ import annotations import asyncio from ...core import app, entities as core_entities -from ...provider import entities as provider_entities +from langbot_plugin.api.entities.builtin.provider import message as provider_message, prompt as provider_prompt +import langbot_plugin.api.entities.builtin.provider.session as provider_session class SessionManager: @@ -11,7 +12,7 @@ class SessionManager: ap: app.Application - session_list: list[core_entities.Session] + session_list: list[provider_session.Session] def __init__(self, ap: app.Application): self.ap = ap @@ -20,7 +21,7 @@ class SessionManager: async def initialize(self): pass - async def get_session(self, query: core_entities.Query) -> core_entities.Session: + async def get_session(self, query: core_entities.Query) -> provider_session.Session: """获取会话""" for session in self.session_list: if query.launcher_type == session.launcher_type and query.launcher_id == session.launcher_id: @@ -28,7 +29,7 @@ class SessionManager: session_concurrency = self.ap.instance_config.data['concurrency']['session'] - session = core_entities.Session( + session = provider_session.Session( launcher_type=query.launcher_type, launcher_id=query.launcher_id, semaphore=asyncio.Semaphore(session_concurrency), @@ -39,11 +40,11 @@ class SessionManager: async def get_conversation( self, query: core_entities.Query, - session: core_entities.Session, + session: provider_session.Session, prompt_config: list[dict], pipeline_uuid: str, bot_uuid: str, - ) -> core_entities.Conversation: + ) -> provider_session.Conversation: """获取对话或创建对话""" if not session.conversations: @@ -53,20 +54,17 @@ class SessionManager: prompt_messages = [] for prompt_message in prompt_config: - prompt_messages.append(provider_entities.Message(**prompt_message)) + prompt_messages.append(provider_message.Message(**prompt_message)) - prompt = provider_entities.Prompt( + prompt = provider_prompt.Prompt( name='default', messages=prompt_messages, ) if session.using_conversation is None or session.using_conversation.pipeline_uuid != pipeline_uuid: - conversation = core_entities.Conversation( + conversation = provider_session.Conversation( prompt=prompt, messages=[], - use_funcs=await self.ap.tool_mgr.get_all_functions( - plugin_enabled=True, - ), pipeline_uuid=pipeline_uuid, bot_uuid=bot_uuid, ) diff --git a/pkg/provider/tools/entities.py b/pkg/provider/tools/entities.py deleted file mode 100644 index 102e03d3..00000000 --- a/pkg/provider/tools/entities.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import typing - -import pydantic.v1 as pydantic - - -class LLMFunction(pydantic.BaseModel): - """函数""" - - name: str - """函数名""" - - human_desc: str - - description: str - """给LLM识别的函数描述""" - - parameters: dict - - func: typing.Callable - """供调用的python异步方法 - - 此异步方法第一个参数接收当前请求的query对象,可以从其中取出session等信息。 - query参数不在parameters中,但在调用时会自动传入。 - 但在当前版本中,插件提供的内容函数都是同步的,且均为请求无关的,故在此版本的实现(以及考虑了向后兼容性的版本)中, - 对插件的内容函数进行封装并存到这里来。 - """ - - class Config: - arbitrary_types_allowed = True diff --git a/pkg/provider/tools/loader.py b/pkg/provider/tools/loader.py index 76b7d248..fca9aa93 100644 --- a/pkg/provider/tools/loader.py +++ b/pkg/provider/tools/loader.py @@ -4,7 +4,7 @@ import abc import typing from ...core import app, entities as core_entities -from . import entities as tools_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool preregistered_loaders: list[typing.Type[ToolLoader]] = [] @@ -35,7 +35,7 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: + async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: """获取所有工具""" pass diff --git a/pkg/provider/tools/loaders/mcp.py b/pkg/provider/tools/loaders/mcp.py index f3223f42..bf35990e 100644 --- a/pkg/provider/tools/loaders/mcp.py +++ b/pkg/provider/tools/loaders/mcp.py @@ -7,8 +7,9 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from mcp.client.sse import sse_client -from .. import loader, entities as tools_entities +from .. import loader from ....core import app, entities as core_entities +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class RuntimeMCPSession: @@ -24,7 +25,7 @@ class RuntimeMCPSession: exit_stack: AsyncExitStack - functions: list[tools_entities.LLMFunction] = [] + functions: list[resource_tool.LLMTool] = [] def __init__(self, server_name: str, server_config: dict, ap: app.Application): self.server_name = server_name @@ -91,7 +92,7 @@ class RuntimeMCPSession: func.__name__ = tool.name self.functions.append( - tools_entities.LLMFunction( + resource_tool.LLMTool( name=tool.name, human_desc=tool.description, description=tool.description, @@ -114,7 +115,7 @@ class MCPLoader(loader.ToolLoader): sessions: dict[str, RuntimeMCPSession] = {} - _last_listed_functions: list[tools_entities.LLMFunction] = [] + _last_listed_functions: list[resource_tool.LLMTool] = [] def __init__(self, ap: app.Application): super().__init__(ap) @@ -130,7 +131,7 @@ class MCPLoader(loader.ToolLoader): # self.ap.event_loop.create_task(session.initialize()) self.sessions[server_config['name']] = session - async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: + async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: all_functions = [] for session in self.sessions.values(): diff --git a/pkg/provider/tools/loaders/plugin.py b/pkg/provider/tools/loaders/plugin.py index b7df2d67..c6ecda7d 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -3,9 +3,10 @@ from __future__ import annotations import typing import traceback -from .. import loader, entities as tools_entities +from .. import loader from ....core import entities as core_entities from ....plugin import context as plugin_context +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool @loader.loader_class('plugin-tool-loader') @@ -15,9 +16,9 @@ class PluginToolLoader(loader.ToolLoader): 本加载器中不存储工具信息,仅负责从插件系统中获取工具信息。 """ - async def get_tools(self, enabled: bool = True) -> list[tools_entities.LLMFunction]: + async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: # 从插件系统获取工具(内容函数) - all_functions: list[tools_entities.LLMFunction] = [] + all_functions: list[resource_tool.LLMTool] = [] for plugin in self.ap.plugin_mgr.plugins( enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED @@ -38,7 +39,7 @@ class PluginToolLoader(loader.ToolLoader): async def _get_function_and_plugin( self, name: str - ) -> typing.Tuple[tools_entities.LLMFunction, plugin_context.BasePlugin]: + ) -> typing.Tuple[resource_tool.LLMTool, plugin_context.BasePlugin]: """获取函数和插件实例""" for plugin in self.ap.plugin_mgr.plugins( enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED diff --git a/pkg/provider/tools/toolmgr.py b/pkg/provider/tools/toolmgr.py index b1d43d08..5f0cbdbf 100644 --- a/pkg/provider/tools/toolmgr.py +++ b/pkg/provider/tools/toolmgr.py @@ -3,9 +3,10 @@ from __future__ import annotations import typing from ...core import app, entities as core_entities -from . import entities, loader as tools_loader +from . import loader as tools_loader from ...utils import importutil from . import loaders +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool importutil.import_modules_in_pkg(loaders) @@ -28,16 +29,16 @@ class ToolManager: await loader_inst.initialize() self.loaders.append(loader_inst) - async def get_all_functions(self, plugin_enabled: bool = None) -> list[entities.LLMFunction]: + async def get_all_functions(self, plugin_enabled: bool = None) -> list[resource_tool.LLMTool]: """获取所有函数""" - all_functions: list[entities.LLMFunction] = [] + all_functions: list[resource_tool.LLMTool] = [] for loader in self.loaders: all_functions.extend(await loader.get_tools(plugin_enabled)) return all_functions - async def generate_tools_for_openai(self, use_funcs: list[entities.LLMFunction]) -> list: + async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list: """生成函数列表""" tools = [] @@ -54,7 +55,7 @@ class ToolManager: return tools - async def generate_tools_for_anthropic(self, use_funcs: list[entities.LLMFunction]) -> list: + async def generate_tools_for_anthropic(self, use_funcs: list[resource_tool.LLMTool]) -> list: """为anthropic生成函数列表 e.g. diff --git a/pkg/utils/image.py b/pkg/utils/image.py index f69d29d2..d9518e12 100644 --- a/pkg/utils/image.py +++ b/pkg/utils/image.py @@ -204,9 +204,9 @@ async def get_slack_image_to_base64(pic_url: str, bot_token: str): try: async with aiohttp.ClientSession() as session: async with session.get(pic_url, headers=headers) as resp: - mime_type = resp.headers.get("Content-Type", "application/octet-stream") + mime_type = resp.headers.get('Content-Type', 'application/octet-stream') file_bytes = await resp.read() - base64_str = base64.b64encode(file_bytes).decode("utf-8") - return f"data:{mime_type};base64,{base64_str}" + base64_str = base64.b64encode(file_bytes).decode('utf-8') + return f'data:{mime_type};base64,{base64_str}' except Exception as e: - raise (e) \ No newline at end of file + raise (e) diff --git a/pkg/utils/importutil.py b/pkg/utils/importutil.py index 8acc5c45..1933d611 100644 --- a/pkg/utils/importutil.py +++ b/pkg/utils/importutil.py @@ -32,7 +32,7 @@ def import_dir(path: str): rel_path = full_path.replace(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '') rel_path = rel_path[1:] rel_path = rel_path.replace('/', '.')[:-3] - rel_path = rel_path.replace("\\",".") + rel_path = rel_path.replace('\\', '.') importlib.import_module(rel_path) From 6b782f876168315f873d8c133670db9d8632f3d9 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 15 Jun 2025 22:04:31 +0800 Subject: [PATCH 06/78] feat: switch Query to langbot-plugin definition --- main.py | 10 +-- pkg/api/http/controller/groups/system.py | 9 --- pkg/command/cmdmgr.py | 5 +- pkg/command/entities.py | 6 +- pkg/core/app.py | 57 ------------------ pkg/core/entities.py | 14 +---- pkg/pipeline/bansess/bansess.py | 4 +- pkg/pipeline/cntfilter/cntfilter.py | 9 ++- pkg/pipeline/cntfilter/entities.py | 2 +- pkg/pipeline/cntfilter/filter.py | 6 +- .../cntfilter/filters/baiduexamine.py | 5 +- pkg/pipeline/cntfilter/filters/banwords.py | 4 +- pkg/pipeline/cntfilter/filters/cntignore.py | 4 +- pkg/pipeline/controller.py | 15 +++-- pkg/pipeline/entities.py | 6 +- pkg/pipeline/longtext/longtext.py | 5 +- pkg/pipeline/longtext/strategies/forward.py | 6 +- pkg/pipeline/longtext/strategies/image.py | 6 +- pkg/pipeline/longtext/strategy.py | 4 +- pkg/pipeline/msgtrun/msgtrun.py | 5 +- pkg/pipeline/msgtrun/truncator.py | 6 +- pkg/pipeline/msgtrun/truncators/round.py | 4 +- pkg/pipeline/pipelinemgr.py | 15 +++-- pkg/pipeline/pool.py | 11 ++-- pkg/pipeline/preproc/preproc.py | 4 +- pkg/pipeline/process/handler.py | 4 +- pkg/pipeline/process/handlers/chat.py | 8 ++- pkg/pipeline/process/handlers/command.py | 7 ++- pkg/pipeline/process/process.py | 4 +- pkg/pipeline/ratelimit/algo.py | 7 ++- pkg/pipeline/ratelimit/algos/fixedwin.py | 6 +- pkg/pipeline/ratelimit/ratelimit.py | 5 +- pkg/pipeline/respback/respback.py | 4 +- pkg/pipeline/resprule/entities.py | 2 +- pkg/pipeline/resprule/resprule.py | 5 +- pkg/pipeline/resprule/rule.py | 5 +- pkg/pipeline/resprule/rules/atbot.py | 4 +- pkg/pipeline/resprule/rules/prefix.py | 4 +- pkg/pipeline/resprule/rules/random.py | 4 +- pkg/pipeline/resprule/rules/regexp.py | 4 +- pkg/pipeline/stage.py | 5 +- pkg/pipeline/wrapper/wrapper.py | 5 +- pkg/platform/adapter.py | 12 ++-- pkg/platform/botmgr.py | 9 +-- pkg/platform/sources/aiocqhttp.py | 6 +- pkg/platform/sources/dingtalk.py | 5 +- pkg/platform/sources/discord.py | 6 +- pkg/platform/sources/lark.py | 7 +-- .../sources/{ => legacy}/gewechat.png | Bin pkg/platform/sources/{ => legacy}/gewechat.py | 22 +++---- .../sources/{ => legacy}/gewechat.yaml | 0 pkg/platform/sources/{ => legacy}/nakuru.png | Bin pkg/platform/sources/{ => legacy}/nakuru.py | 15 +++-- pkg/platform/sources/{ => legacy}/nakuru.yaml | 0 pkg/platform/sources/{ => legacy}/qqbotpy.py | 18 +++--- pkg/platform/sources/{ => legacy}/qqbotpy.svg | 0 .../sources/{ => legacy}/qqbotpy.yaml | 0 pkg/platform/sources/officialaccount.py | 5 +- pkg/platform/sources/qqofficial.py | 5 +- pkg/platform/sources/slack.py | 5 +- pkg/platform/sources/telegram.py | 7 +-- pkg/platform/sources/webchat.py | 5 +- pkg/platform/sources/wechatpad.py | 27 ++++----- pkg/platform/sources/wecom.py | 5 +- pkg/platform/sources/wecomcs.py | 5 +- pkg/plugin/events.py | 4 +- pkg/plugin/loaders/classic.py | 4 +- pkg/provider/entities.py | 2 +- pkg/provider/modelmgr/entities.py | 2 +- pkg/provider/modelmgr/requester.py | 4 +- .../modelmgr/requesters/anthropicmsgs.py | 4 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 6 +- .../modelmgr/requesters/deepseekchatcmpl.py | 4 +- .../modelmgr/requesters/giteeaichatcmpl.py | 4 +- .../modelmgr/requesters/modelscopechatcmpl.py | 6 +- .../modelmgr/requesters/moonshotchatcmpl.py | 4 +- .../modelmgr/requesters/ollamachat.py | 6 +- pkg/provider/runner.py | 5 +- pkg/provider/runners/dashscopeapi.py | 13 ++-- pkg/provider/runners/difysvapi.py | 16 ++--- pkg/provider/runners/localagent.py | 4 +- pkg/provider/runners/n8nsvapi.py | 9 +-- pkg/provider/session/sessionmgr.py | 7 ++- pkg/provider/tools/loader.py | 5 +- pkg/provider/tools/loaders/mcp.py | 7 ++- pkg/provider/tools/loaders/plugin.py | 4 +- pkg/provider/tools/toolmgr.py | 5 +- pkg/utils/announce.py | 2 +- 88 files changed, 248 insertions(+), 348 deletions(-) rename pkg/platform/sources/{ => legacy}/gewechat.png (100%) rename pkg/platform/sources/{ => legacy}/gewechat.py (98%) rename pkg/platform/sources/{ => legacy}/gewechat.yaml (100%) rename pkg/platform/sources/{ => legacy}/nakuru.png (100%) rename pkg/platform/sources/{ => legacy}/nakuru.py (97%) rename pkg/platform/sources/{ => legacy}/nakuru.yaml (100%) rename pkg/platform/sources/{ => legacy}/qqbotpy.py (97%) rename pkg/platform/sources/{ => legacy}/qqbotpy.svg (100%) rename pkg/platform/sources/{ => legacy}/qqbotpy.yaml (100%) diff --git a/main.py b/main.py index 19cb32d6..b7d62e07 100644 --- a/main.py +++ b/main.py @@ -47,13 +47,13 @@ async def main_entry(loop: asyncio.AbstractEventLoop): if not args.skip_plugin_deps_check: await deps.precheck_plugin_deps() - # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1 - import pydantic.version + # # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1 + # import pydantic.version - if pydantic.version.VERSION < '2.0': - import pydantic + # if pydantic.version.VERSION < '2.0': + # import pydantic - sys.modules['pydantic.v1'] = pydantic + # sys.modules['pydantic.v1'] = pydantic # 检查配置文件 diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index c4cab602..1089626d 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -35,15 +35,6 @@ class SystemRouterGroup(group.RouterGroup): return self.success(data=task.to_dict()) - @self.route('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) - async def _() -> str: - json_data = await quart.request.json - - scope = json_data.get('scope') - - await self.ap.reload(scope=scope) - return self.success() - @self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: if not constants.debug_mode: diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index a8cf5eae..14c3d9e4 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -2,10 +2,11 @@ from __future__ import annotations import typing -from ..core import app, entities as core_entities +from ..core import app from . import entities, operator, errors from ..utils import importutil import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query # 引入所有算子以便注册 from . import operators @@ -90,7 +91,7 @@ class CommandManager: async def execute( self, command_text: str, - query: core_entities.Query, + query: pipeline_query.Query, session: provider_session.Session, ) -> typing.AsyncGenerator[entities.CommandReturn, None]: """执行命令""" diff --git a/pkg/command/entities.py b/pkg/command/entities.py index e80d203f..7d6eecdc 100644 --- a/pkg/command/entities.py +++ b/pkg/command/entities.py @@ -2,12 +2,12 @@ from __future__ import annotations import typing -import pydantic.v1 as pydantic +import pydantic import langbot_plugin.api.entities.builtin.provider.session as provider_session -from ..core import entities as core_entities from . import errors from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class CommandReturn(pydantic.BaseModel): @@ -35,7 +35,7 @@ class CommandReturn(pydantic.BaseModel): class ExecuteContext(pydantic.BaseModel): """单次命令执行上下文""" - query: core_entities.Query + query: pipeline_query.Query """本次消息的请求对象""" session: provider_session.Session diff --git a/pkg/core/app.py b/pkg/core/app.py index c795d6c0..4b3e3b82 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging import asyncio import traceback -import sys import os from ..platform import botmgr as im_mgr @@ -183,59 +182,3 @@ class Application: """.strip() for line in tips.split('\n'): self.logger.info(line) - - async def reload( - self, - scope: core_entities.LifecycleControlScope, - ): - match scope: - case core_entities.LifecycleControlScope.PLATFORM.value: - self.logger.info('执行热重载 scope=' + scope) - await self.platform_mgr.shutdown() - - self.platform_mgr = im_mgr.PlatformManager(self) - - await self.platform_mgr.initialize() - - self.task_mgr.create_task( - self.platform_mgr.run(), - name='platform-manager', - scopes=[ - core_entities.LifecycleControlScope.APPLICATION, - core_entities.LifecycleControlScope.PLATFORM, - ], - ) - case core_entities.LifecycleControlScope.PLUGIN.value: - self.logger.info('执行热重载 scope=' + scope) - await self.plugin_mgr.destroy_plugins() - - # 删除 sys.module 中所有的 plugins/* 下的模块 - for mod in list(sys.modules.keys()): - if mod.startswith('plugins.'): - del sys.modules[mod] - - self.plugin_mgr = plugin_mgr.PluginManager(self) - await self.plugin_mgr.initialize() - - await self.plugin_mgr.initialize_plugins() - - await self.plugin_mgr.load_plugins() - await self.plugin_mgr.initialize_plugins() - case core_entities.LifecycleControlScope.PROVIDER.value: - self.logger.info('执行热重载 scope=' + scope) - - await self.tool_mgr.shutdown() - - llm_model_mgr_inst = llm_model_mgr.ModelManager(self) - await llm_model_mgr_inst.initialize() - self.model_mgr = llm_model_mgr_inst - - llm_session_mgr_inst = llm_session_mgr.SessionManager(self) - await llm_session_mgr_inst.initialize() - self.sess_mgr = llm_session_mgr_inst - - llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self) - await llm_tool_mgr_inst.initialize() - self.tool_mgr = llm_tool_mgr_inst - case _: - pass diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 3bc0349c..5abb7c74 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -3,7 +3,7 @@ from __future__ import annotations import enum import typing -import pydantic.v1 as pydantic +import pydantic from ..provider import entities as llm_entities from ..platform import adapter as msadapter @@ -20,23 +20,13 @@ class LifecycleControlScope(enum.Enum): PROVIDER = 'provider' -class LauncherTypes(enum.Enum): - """一个请求的发起者类型""" - - PERSON = 'person' - """私聊""" - - GROUP = 'group' - """群聊""" - - class Query(pydantic.BaseModel): """一次请求的信息封装""" query_id: int """请求ID,添加进请求池时生成""" - launcher_type: LauncherTypes + launcher_type: provider_session.LauncherTypes """会话类型,platform处理阶段设置""" launcher_id: typing.Union[int, str] diff --git a/pkg/pipeline/bansess/bansess.py b/pkg/pipeline/bansess/bansess.py index 3b927a55..0cd498f6 100644 --- a/pkg/pipeline/bansess/bansess.py +++ b/pkg/pipeline/bansess/bansess.py @@ -1,7 +1,7 @@ from __future__ import annotations from .. import stage, entities -from ...core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @stage.stage_class('BanSessionCheckStage') @@ -14,7 +14,7 @@ class BanSessionCheckStage(stage.PipelineStage): async def initialize(self, pipeline_config: dict): pass - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: found = False mode = query.pipeline_config['trigger']['access-control']['mode'] diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index fb562a42..1708363a 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -3,12 +3,11 @@ from __future__ import annotations from ...core import app from .. import stage, entities -from ...core import entities as core_entities from . import filter as filter_model, entities as filter_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...platform.types import message as platform_message from ...utils import importutil - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import filters importutil.import_modules_in_pkg(filters) @@ -58,7 +57,7 @@ class ContentFilterStage(stage.PipelineStage): async def _pre_process( self, message: str, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.StageProcessResult: """请求llm前处理消息 只要有一个不通过就不放行,只放行 PASS 的消息 @@ -93,7 +92,7 @@ class ContentFilterStage(stage.PipelineStage): async def _post_process( self, message: str, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.StageProcessResult: """请求llm后处理响应 只要是 PASS 或者 MASKED 的就通过此 filter,将其 replacement 设置为message,进入下一个 filter @@ -123,7 +122,7 @@ class ContentFilterStage(stage.PipelineStage): return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: """处理""" if stage_inst_name == 'PreContentFilterStage': contain_non_text = False diff --git a/pkg/pipeline/cntfilter/entities.py b/pkg/pipeline/cntfilter/entities.py index 5e804c0d..607eba9a 100644 --- a/pkg/pipeline/cntfilter/entities.py +++ b/pkg/pipeline/cntfilter/entities.py @@ -1,6 +1,6 @@ import enum -import pydantic.v1 as pydantic +import pydantic class ResultLevel(enum.Enum): diff --git a/pkg/pipeline/cntfilter/filter.py b/pkg/pipeline/cntfilter/filter.py index 0a3ceaae..dafc539a 100644 --- a/pkg/pipeline/cntfilter/filter.py +++ b/pkg/pipeline/cntfilter/filter.py @@ -3,9 +3,9 @@ from __future__ import annotations import abc import typing -from ...core import app, entities as core_entities +from ...core import app from . import entities - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_filters: list[typing.Type[ContentFilter]] = [] @@ -60,7 +60,7 @@ class ContentFilter(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def process(self, query: core_entities.Query, message: str = None, image_url=None) -> entities.FilterResult: + async def process(self, query: pipeline_query.Query, message: str = None, image_url=None) -> entities.FilterResult: """处理消息 分为前后阶段,具体取决于 enable_stages 的值。 diff --git a/pkg/pipeline/cntfilter/filters/baiduexamine.py b/pkg/pipeline/cntfilter/filters/baiduexamine.py index 9637aec2..4213e662 100644 --- a/pkg/pipeline/cntfilter/filters/baiduexamine.py +++ b/pkg/pipeline/cntfilter/filters/baiduexamine.py @@ -4,8 +4,7 @@ import aiohttp from .. import entities from .. import filter as filter_model -from ....core import entities as core_entities - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}' BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token' @@ -27,7 +26,7 @@ class BaiduCloudExamine(filter_model.ContentFilter): ) as resp: return (await resp.json())['access_token'] - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: + async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult: async with aiohttp.ClientSession() as session: async with session.post( BAIDU_EXAMINE_URL.format(await self._get_token()), diff --git a/pkg/pipeline/cntfilter/filters/banwords.py b/pkg/pipeline/cntfilter/filters/banwords.py index 916a1bc1..e04de8c4 100644 --- a/pkg/pipeline/cntfilter/filters/banwords.py +++ b/pkg/pipeline/cntfilter/filters/banwords.py @@ -3,7 +3,7 @@ import re from .. import filter as filter_model from .. import entities -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @filter_model.filter_class('ban-word-filter') @@ -13,7 +13,7 @@ class BanWordFilter(filter_model.ContentFilter): async def initialize(self): pass - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: + async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult: found = False for word in self.ap.sensitive_meta.data['words']: diff --git a/pkg/pipeline/cntfilter/filters/cntignore.py b/pkg/pipeline/cntfilter/filters/cntignore.py index 5e410e31..0a3ef709 100644 --- a/pkg/pipeline/cntfilter/filters/cntignore.py +++ b/pkg/pipeline/cntfilter/filters/cntignore.py @@ -3,7 +3,7 @@ import re from .. import entities from .. import filter as filter_model -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @filter_model.filter_class('content-ignore') @@ -16,7 +16,7 @@ class ContentIgnore(filter_model.ContentFilter): entities.EnableStage.PRE, ] - async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: + async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult: if 'prefix' in query.pipeline_config['trigger']['ignore-rules']: for rule in query.pipeline_config['trigger']['ignore-rules']['prefix']: if message.startswith(rule): diff --git a/pkg/pipeline/controller.py b/pkg/pipeline/controller.py index 6679bd88..11bd8d46 100644 --- a/pkg/pipeline/controller.py +++ b/pkg/pipeline/controller.py @@ -3,7 +3,10 @@ from __future__ import annotations import asyncio import traceback -from ..core import app, entities +from ..core import app +from ..core import entities as core_entities + +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class Controller: @@ -22,11 +25,11 @@ class Controller: """事件处理循环""" try: while True: - selected_query: entities.Query = None + selected_query: pipeline_query.Query = None # 取请求 async with self.ap.query_pool: - queries: list[entities.Query] = self.ap.query_pool.queries + queries: list[pipeline_query.Query] = self.ap.query_pool.queries for query in queries: session = await self.ap.sess_mgr.get_session(query) @@ -46,7 +49,7 @@ class Controller: if selected_query: - async def _process_query(selected_query: entities.Query): + async def _process_query(selected_query: pipeline_query.Query): async with self.semaphore: # 总并发上限 # find pipeline # Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one. @@ -68,8 +71,8 @@ class Controller: kind='query', name=f'query-{selected_query.query_id}', scopes=[ - entities.LifecycleControlScope.APPLICATION, - entities.LifecycleControlScope.PLATFORM, + core_entities.LifecycleControlScope.APPLICATION, + core_entities.LifecycleControlScope.PLATFORM, ], ) diff --git a/pkg/pipeline/entities.py b/pkg/pipeline/entities.py index dd6434c0..7e7f23ce 100644 --- a/pkg/pipeline/entities.py +++ b/pkg/pipeline/entities.py @@ -3,10 +3,10 @@ from __future__ import annotations import enum import typing -import pydantic.v1 as pydantic +import pydantic from ..platform.types import message as platform_message -from ..core import entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class ResultType(enum.Enum): @@ -20,7 +20,7 @@ class ResultType(enum.Enum): class StageProcessResult(pydantic.BaseModel): result_type: ResultType - new_query: entities.Query + new_query: pipeline_query.Query user_notice: typing.Optional[ typing.Union[ diff --git a/pkg/pipeline/longtext/longtext.py b/pkg/pipeline/longtext/longtext.py index 5be20650..6356a16f 100644 --- a/pkg/pipeline/longtext/longtext.py +++ b/pkg/pipeline/longtext/longtext.py @@ -5,10 +5,9 @@ import traceback from . import strategy from .. import stage, entities -from ...core import entities as core_entities from ...platform.types import message as platform_message from ...utils import importutil - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import strategies importutil.import_modules_in_pkg(strategies) @@ -67,7 +66,7 @@ class LongTextProcessStage(stage.PipelineStage): await self.strategy_impl.initialize() - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: # 检查是否包含非 Plain 组件 contains_non_plain = False diff --git a/pkg/pipeline/longtext/strategies/forward.py b/pkg/pipeline/longtext/strategies/forward.py index 6228d580..574239b8 100644 --- a/pkg/pipeline/longtext/strategies/forward.py +++ b/pkg/pipeline/longtext/strategies/forward.py @@ -3,9 +3,9 @@ from __future__ import annotations from .. import strategy as strategy_model -from ....core import entities as core_entities -from ....platform.types import message as platform_message +from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay Forward = platform_message.Forward @@ -13,7 +13,7 @@ Forward = platform_message.Forward @strategy_model.strategy_class('forward') class ForwardComponentStrategy(strategy_model.LongTextStrategy): - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]: display = ForwardMessageDiaplay( title='群聊的聊天记录', brief='[聊天记录]', diff --git a/pkg/pipeline/longtext/strategies/image.py b/pkg/pipeline/longtext/strategies/image.py index f96f7265..ba6ddc1b 100644 --- a/pkg/pipeline/longtext/strategies/image.py +++ b/pkg/pipeline/longtext/strategies/image.py @@ -11,7 +11,7 @@ import functools from ....platform.types import message as platform_message from .. import strategy as strategy_model -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @strategy_model.strategy_class('image') @@ -27,7 +27,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): encoding='utf-8', ) - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]: img_path = self.text_to_image( text_str=message, save_as='temp/{}.png'.format(int(time.time())), @@ -131,7 +131,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy): text_str: str, save_as='temp.png', width=800, - query: core_entities.Query = None, + query: pipeline_query.Query = None, ): text_str = text_str.replace('\t', ' ') diff --git a/pkg/pipeline/longtext/strategy.py b/pkg/pipeline/longtext/strategy.py index 0ddec0c6..dd69b2bb 100644 --- a/pkg/pipeline/longtext/strategy.py +++ b/pkg/pipeline/longtext/strategy.py @@ -4,8 +4,8 @@ import typing from ...core import app -from ...core import entities as core_entities from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_strategies: list[typing.Type[LongTextStrategy]] = [] @@ -49,7 +49,7 @@ class LongTextStrategy(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: + async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]: """处理长文本 在 platform.json 中配置 long-text-process 字段,只要 文本长度超过了 threshold 就会调用此方法 diff --git a/pkg/pipeline/msgtrun/msgtrun.py b/pkg/pipeline/msgtrun/msgtrun.py index c64f67fc..3acd7e5c 100644 --- a/pkg/pipeline/msgtrun/msgtrun.py +++ b/pkg/pipeline/msgtrun/msgtrun.py @@ -1,10 +1,9 @@ from __future__ import annotations from .. import stage, entities -from ...core import entities as core_entities from . import truncator from ...utils import importutil - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import truncators importutil.import_modules_in_pkg(truncators) @@ -29,7 +28,7 @@ class ConversationMessageTruncator(stage.PipelineStage): else: raise ValueError(f'未知的截断器: {use_method}') - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: """处理""" query = await self.trun.truncate(query) diff --git a/pkg/pipeline/msgtrun/truncator.py b/pkg/pipeline/msgtrun/truncator.py index 9e8b8a6c..180982d3 100644 --- a/pkg/pipeline/msgtrun/truncator.py +++ b/pkg/pipeline/msgtrun/truncator.py @@ -3,8 +3,8 @@ from __future__ import annotations import typing import abc -from ...core import entities as core_entities, app - +from ...core import app +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_truncators: list[typing.Type[Truncator]] = [] @@ -47,7 +47,7 @@ class Truncator(abc.ABC): pass @abc.abstractmethod - async def truncate(self, query: core_entities.Query) -> core_entities.Query: + async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query: """截断 一般只需要操作query.messages,也可以扩展操作query.prompt, query.user_message。 diff --git a/pkg/pipeline/msgtrun/truncators/round.py b/pkg/pipeline/msgtrun/truncators/round.py index fa72a0e1..c6b1fba4 100644 --- a/pkg/pipeline/msgtrun/truncators/round.py +++ b/pkg/pipeline/msgtrun/truncators/round.py @@ -1,14 +1,14 @@ from __future__ import annotations from .. import truncator -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @truncator.truncator_class('round') class RoundTruncator(truncator.Truncator): """前文回合数阶段器""" - async def truncate(self, query: core_entities.Query) -> core_entities.Query: + async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query: """截断""" max_round = query.pipeline_config['ai']['local-agent']['max-round'] diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 78cffa73..debdbb93 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -5,7 +5,7 @@ import traceback import sqlalchemy -from ..core import app, entities +from ..core import app from . import entities as pipeline_entities from ..entity.persistence import pipeline as persistence_pipeline from . import stage @@ -13,6 +13,9 @@ from ..platform.types import message as platform_message, events as platform_eve from ..plugin import events from ..utils import importutil +import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query + from . import ( resprule, bansess, @@ -75,11 +78,11 @@ class RuntimePipeline: self.pipeline_entity = pipeline_entity self.stage_containers = stage_containers - async def run(self, query: entities.Query): + async def run(self, query: pipeline_query.Query): query.pipeline_config = self.pipeline_entity.config await self.process_query(query) - async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult): + async def _check_output(self, query: pipeline_query.Query, result: pipeline_entities.StageProcessResult): """检查输出""" if result.user_notice: # 处理str类型 @@ -109,7 +112,7 @@ class RuntimePipeline: async def _execute_from_stage( self, stage_index: int, - query: entities.Query, + query: pipeline_query.Query, ): """从指定阶段开始执行,实现了责任链模式和基于生成器的阶段分叉功能。 @@ -169,13 +172,13 @@ class RuntimePipeline: i += 1 - async def process_query(self, query: entities.Query): + async def process_query(self, query: pipeline_query.Query): """处理请求""" try: # ======== 触发 MessageReceived 事件 ======== event_type = ( events.PersonMessageReceived - if query.launcher_type == entities.LauncherTypes.PERSON + if query.launcher_type == provider_session.LauncherTypes.PERSON else events.GroupMessageReceived ) diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index 6975e53c..a4313cdd 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -3,10 +3,11 @@ from __future__ import annotations import asyncio import typing -from ..core import entities from ..platform import adapter as msadapter from ..platform.types import message as platform_message from ..platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class QueryPool: @@ -16,7 +17,7 @@ class QueryPool: pool_lock: asyncio.Lock - queries: list[entities.Query] + queries: list[pipeline_query.Query] condition: asyncio.Condition @@ -29,16 +30,16 @@ class QueryPool: async def add_query( self, bot_uuid: str, - launcher_type: entities.LauncherTypes, + launcher_type: provider_session.LauncherTypes, launcher_id: typing.Union[int, str], sender_id: typing.Union[int, str], message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, adapter: msadapter.MessagePlatformAdapter, pipeline_uuid: typing.Optional[str] = None, - ) -> entities.Query: + ) -> pipeline_query.Query: async with self.condition: - query = entities.Query( + query = pipeline_query.Query( bot_uuid=bot_uuid, query_id=self.query_id_counter, launcher_type=launcher_type, diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index da56ca6e..af851c96 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -3,10 +3,10 @@ from __future__ import annotations import datetime from .. import stage, entities -from ...core import entities as core_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...plugin import events from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @stage.stage_class('PreProcessor') @@ -26,7 +26,7 @@ class PreProcessor(stage.PipelineStage): async def process( self, - query: core_entities.Query, + query: pipeline_query.Query, stage_inst_name: str, ) -> entities.StageProcessResult: """处理""" diff --git a/pkg/pipeline/process/handler.py b/pkg/pipeline/process/handler.py index 8a32bcfb..181d257d 100644 --- a/pkg/pipeline/process/handler.py +++ b/pkg/pipeline/process/handler.py @@ -3,8 +3,8 @@ from __future__ import annotations import abc from ...core import app -from ...core import entities as core_entities from .. import entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class MessageHandler(metaclass=abc.ABCMeta): @@ -19,7 +19,7 @@ class MessageHandler(metaclass=abc.ABCMeta): @abc.abstractmethod async def handle( self, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.StageProcessResult: raise NotImplementedError diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 35fa1611..b871de81 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -6,13 +6,15 @@ import traceback from .. import handler from ... import entities -from ....core import entities as core_entities from ....provider import runner as runner_module from ....plugin import events from ....platform.types import message as platform_message from ....utils import importutil from ....provider import runners +import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query + importutil.import_modules_in_pkg(runners) @@ -20,7 +22,7 @@ importutil.import_modules_in_pkg(runners) class ChatMessageHandler(handler.MessageHandler): async def handle( self, - query: core_entities.Query, + query: pipeline_query.Query, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: """处理""" # 调API @@ -29,7 +31,7 @@ class ChatMessageHandler(handler.MessageHandler): # 触发插件事件 event_class = ( events.PersonNormalMessageReceived - if query.launcher_type == core_entities.LauncherTypes.PERSON + if query.launcher_type == provider_session.LauncherTypes.PERSON else events.GroupNormalMessageReceived ) diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index efce5615..15c33ebd 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -4,16 +4,17 @@ import typing from .. import handler from ... import entities -from ....core import entities as core_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message from ....plugin import events from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class CommandHandler(handler.MessageHandler): async def handle( self, - query: core_entities.Query, + query: pipeline_query.Query, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: """处理""" @@ -28,7 +29,7 @@ class CommandHandler(handler.MessageHandler): event_class = ( events.PersonCommandSent - if query.launcher_type == core_entities.LauncherTypes.PERSON + if query.launcher_type == provider_session.LauncherTypes.PERSON else events.GroupCommandSent ) diff --git a/pkg/pipeline/process/process.py b/pkg/pipeline/process/process.py index 64903552..704af5fd 100644 --- a/pkg/pipeline/process/process.py +++ b/pkg/pipeline/process/process.py @@ -1,10 +1,10 @@ from __future__ import annotations -from ...core import entities as core_entities from . import handler from .handlers import chat, command from .. import entities from .. import stage +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @stage.stage_class('MessageProcessor') @@ -30,7 +30,7 @@ class Processor(stage.PipelineStage): async def process( self, - query: core_entities.Query, + query: pipeline_query.Query, stage_inst_name: str, ) -> entities.StageProcessResult: """处理""" diff --git a/pkg/pipeline/ratelimit/algo.py b/pkg/pipeline/ratelimit/algo.py index 3bcc347a..efbc326b 100644 --- a/pkg/pipeline/ratelimit/algo.py +++ b/pkg/pipeline/ratelimit/algo.py @@ -2,7 +2,8 @@ from __future__ import annotations import abc import typing -from ...core import app, entities as core_entities +from ...core import app +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_algos: list[typing.Type[ReteLimitAlgo]] = [] @@ -33,7 +34,7 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta): @abc.abstractmethod async def require_access( self, - query: core_entities.Query, + query: pipeline_query.Query, launcher_type: str, launcher_id: typing.Union[int, str], ) -> bool: @@ -53,7 +54,7 @@ class ReteLimitAlgo(metaclass=abc.ABCMeta): @abc.abstractmethod async def release_access( self, - query: core_entities.Query, + query: pipeline_query.Query, launcher_type: str, launcher_id: typing.Union[int, str], ): diff --git a/pkg/pipeline/ratelimit/algos/fixedwin.py b/pkg/pipeline/ratelimit/algos/fixedwin.py index cc816f73..6a2a8e97 100644 --- a/pkg/pipeline/ratelimit/algos/fixedwin.py +++ b/pkg/pipeline/ratelimit/algos/fixedwin.py @@ -3,7 +3,7 @@ import asyncio import time import typing from .. import algo -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query # 固定窗口算法 @@ -32,7 +32,7 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): async def require_access( self, - query: core_entities.Query, + query: pipeline_query.Query, launcher_type: str, launcher_id: typing.Union[int, str], ) -> bool: @@ -91,7 +91,7 @@ class FixedWindowAlgo(algo.ReteLimitAlgo): async def release_access( self, - query: core_entities.Query, + query: pipeline_query.Query, launcher_type: str, launcher_id: typing.Union[int, str], ): diff --git a/pkg/pipeline/ratelimit/ratelimit.py b/pkg/pipeline/ratelimit/ratelimit.py index 23de4ec6..cab62b8d 100644 --- a/pkg/pipeline/ratelimit/ratelimit.py +++ b/pkg/pipeline/ratelimit/ratelimit.py @@ -4,9 +4,10 @@ import typing from .. import entities, stage from . import algo -from ...core import entities as core_entities from ...utils import importutil +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query + from . import algos importutil.import_modules_in_pkg(algos) @@ -39,7 +40,7 @@ class RateLimit(stage.PipelineStage): async def process( self, - query: core_entities.Query, + query: pipeline_query.Query, stage_inst_name: str, ) -> typing.Union[ entities.StageProcessResult, diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index 39d3abb1..b5a1ed74 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -8,14 +8,14 @@ from ...platform.types import events as platform_events from ...platform.types import message as platform_message from .. import stage, entities -from ...core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @stage.stage_class('SendResponseBackStage') class SendResponseBackStage(stage.PipelineStage): """发送响应消息""" - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: """处理""" random_range = ( diff --git a/pkg/pipeline/resprule/entities.py b/pkg/pipeline/resprule/entities.py index a0ba7807..c2d964fe 100644 --- a/pkg/pipeline/resprule/entities.py +++ b/pkg/pipeline/resprule/entities.py @@ -1,4 +1,4 @@ -import pydantic.v1 as pydantic +import pydantic from ...platform.types import message as platform_message diff --git a/pkg/pipeline/resprule/resprule.py b/pkg/pipeline/resprule/resprule.py index 0193f2ce..1a3560ff 100644 --- a/pkg/pipeline/resprule/resprule.py +++ b/pkg/pipeline/resprule/resprule.py @@ -4,9 +4,10 @@ from __future__ import annotations from . import rule from .. import stage, entities -from ...core import entities as core_entities from ...utils import importutil +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query + from . import rules importutil.import_modules_in_pkg(rules) @@ -32,7 +33,7 @@ class GroupRespondRuleCheckStage(stage.PipelineStage): await rule_inst.initialize() self.rule_matchers.append(rule_inst) - async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: + async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult: if query.launcher_type.value != 'group': # 只处理群消息 return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/pipeline/resprule/rule.py b/pkg/pipeline/resprule/rule.py index 3fdb0386..7c91373f 100644 --- a/pkg/pipeline/resprule/rule.py +++ b/pkg/pipeline/resprule/rule.py @@ -2,10 +2,11 @@ from __future__ import annotations import abc import typing -from ...core import app, entities as core_entities +from ...core import app from . import entities from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregisetered_rules: list[typing.Type[GroupRespondRule]] = [] @@ -39,7 +40,7 @@ class GroupRespondRule(metaclass=abc.ABCMeta): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.RuleJudgeResult: """判断消息是否匹配规则""" raise NotImplementedError diff --git a/pkg/pipeline/resprule/rules/atbot.py b/pkg/pipeline/resprule/rules/atbot.py index 340b92c7..fc3b5510 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -3,8 +3,8 @@ from __future__ import annotations from .. import rule as rule_model from .. import entities -from ....core import entities as core_entities from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @rule_model.rule_class('at-bot') @@ -14,7 +14,7 @@ class AtBotRule(rule_model.GroupRespondRule): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.RuleJudgeResult: if message_chain.has(platform_message.At(query.adapter.bot_account_id)) and rule_dict['at']: message_chain.remove(platform_message.At(query.adapter.bot_account_id)) diff --git a/pkg/pipeline/resprule/rules/prefix.py b/pkg/pipeline/resprule/rules/prefix.py index c712d3e8..2ae89fe1 100644 --- a/pkg/pipeline/resprule/rules/prefix.py +++ b/pkg/pipeline/resprule/rules/prefix.py @@ -1,7 +1,7 @@ from .. import rule as rule_model from .. import entities -from ....core import entities as core_entities from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @rule_model.rule_class('prefix') @@ -11,7 +11,7 @@ class PrefixRule(rule_model.GroupRespondRule): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.RuleJudgeResult: prefixes = rule_dict['prefix'] diff --git a/pkg/pipeline/resprule/rules/random.py b/pkg/pipeline/resprule/rules/random.py index d2f782ab..04818ef0 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -3,8 +3,8 @@ import random from .. import rule as rule_model from .. import entities -from ....core import entities as core_entities from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @rule_model.rule_class('random') @@ -14,7 +14,7 @@ class RandomRespRule(rule_model.GroupRespondRule): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.RuleJudgeResult: random_rate = rule_dict['random'] diff --git a/pkg/pipeline/resprule/rules/regexp.py b/pkg/pipeline/resprule/rules/regexp.py index daac0869..51589e0c 100644 --- a/pkg/pipeline/resprule/rules/regexp.py +++ b/pkg/pipeline/resprule/rules/regexp.py @@ -3,8 +3,8 @@ import re from .. import rule as rule_model from .. import entities -from ....core import entities as core_entities from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @rule_model.rule_class('regexp') @@ -14,7 +14,7 @@ class RegExpRule(rule_model.GroupRespondRule): message_text: str, message_chain: platform_message.MessageChain, rule_dict: dict, - query: core_entities.Query, + query: pipeline_query.Query, ) -> entities.RuleJudgeResult: regexps = rule_dict['regexp'] diff --git a/pkg/pipeline/stage.py b/pkg/pipeline/stage.py index 18a94b73..0ff1af7e 100644 --- a/pkg/pipeline/stage.py +++ b/pkg/pipeline/stage.py @@ -3,8 +3,9 @@ from __future__ import annotations import abc import typing -from ..core import app, entities as core_entities +from ..core import app from . import entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_stages: dict[str, type[PipelineStage]] = {} @@ -33,7 +34,7 @@ class PipelineStage(metaclass=abc.ABCMeta): @abc.abstractmethod async def process( self, - query: core_entities.Query, + query: pipeline_query.Query, stage_inst_name: str, ) -> typing.Union[ entities.StageProcessResult, diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 3299a226..8063ff36 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -2,12 +2,11 @@ from __future__ import annotations import typing - -from ...core import entities as core_entities from .. import entities from .. import stage from ...plugin import events from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @stage.stage_class('ResponseWrapper') @@ -25,7 +24,7 @@ class ResponseWrapper(stage.PipelineStage): async def process( self, - query: core_entities.Query, + query: pipeline_query.Query, stage_inst_name: str, ) -> typing.AsyncGenerator[entities.StageProcessResult, None]: """处理""" diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py index f28ad3dc..f27efc75 100644 --- a/pkg/platform/adapter.py +++ b/pkg/platform/adapter.py @@ -3,15 +3,14 @@ from __future__ import annotations # MessageSource的适配器 import typing import abc +import pydantic - -from ..core import app from .types import message as platform_message from .types import events as platform_events from .logger import EventLogger -class MessagePlatformAdapter(metaclass=abc.ABCMeta): +class MessagePlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta): """消息平台适配器基类""" name: str @@ -21,11 +20,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): config: dict - ap: app.Application + logger: EventLogger = pydantic.Field(exclude=True) - logger: EventLogger - - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): """初始化适配器 Args: @@ -33,7 +30,6 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta): ap (app.Application): 应用上下文 """ self.config = config - self.ap = ap self.logger = logger async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 5855525f..8f247ca4 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -19,6 +19,8 @@ from ..entity.errors import platform as platform_errors from .logger import EventLogger +import langbot_plugin.api.entities.builtin.provider.session as provider_session + # 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 from . import types as mirai @@ -73,7 +75,7 @@ class RuntimeBot: await self.ap.query_pool.add_query( bot_uuid=self.bot_entity.uuid, - launcher_type=core_entities.LauncherTypes.PERSON, + launcher_type=provider_session.LauncherTypes.PERSON, launcher_id=event.sender.id, sender_id=event.sender.id, message_event=event, @@ -98,7 +100,7 @@ class RuntimeBot: await self.ap.query_pool.add_query( bot_uuid=self.bot_entity.uuid, - launcher_type=core_entities.LauncherTypes.GROUP, + launcher_type=provider_session.LauncherTypes.GROUP, launcher_id=event.group.id, sender_id=event.sender.id, message_event=event, @@ -172,9 +174,9 @@ class PlatformManager: webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap) webchat_adapter_inst = webchat_adapter_class( {}, - self.ap, webchat_logger, ) + webchat_adapter_inst.ap = self.ap self.webchat_proxy_bot = RuntimeBot( ap=self.ap, @@ -231,7 +233,6 @@ class PlatformManager: adapter_inst = self.adapter_dict[bot_entity.adapter]( bot_entity.adapter_config, - self.ap, logger, ) diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index 8cdfd204..b2616bb0 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -7,7 +7,6 @@ import datetime import aiocqhttp from .. import adapter -from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities @@ -273,11 +272,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): config: dict - ap: app.Application - on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = [] - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config self.logger = logger @@ -287,7 +284,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): self.config['shutdown_trigger'] = shutdown_trigger_placeholder - self.ap = ap self.on_websocket_connection_event_cache = [] if 'access-token' in config: diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 3147c984..1727a771 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -4,7 +4,6 @@ from libs.dingtalk_api.dingtalkevent import DingTalkEvent from pkg.platform.types import message as platform_message from pkg.platform.adapter import MessagePlatformAdapter from .. import adapter -from ...core import app from ..types import events as platform_events from ..types import entities as platform_entities from libs.dingtalk_api.api import DingTalkClient @@ -94,15 +93,13 @@ class DingTalkEventConverter(adapter.EventConverter): class DingTalkAdapter(adapter.MessagePlatformAdapter): bot: DingTalkClient - ap: app.Application bot_account_id: str message_converter: DingTalkMessageConverter = DingTalkMessageConverter() event_converter: DingTalkEventConverter = DingTalkEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ 'client_id', diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index f159c628..52bd5e5b 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -12,7 +12,6 @@ import datetime import aiohttp from .. import adapter -from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities @@ -161,8 +160,6 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): config: dict - ap: app.Application - message_converter: DiscordMessageConverter = DiscordMessageConverter() event_converter: DiscordEventConverter = DiscordEventConverter() @@ -171,9 +168,8 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger self.bot_account_id = self.config['client_id'] diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index f8faf522..9e727ad3 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -19,7 +19,6 @@ import quart from lark_oapi.api.im.v1 import * from .. import adapter -from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities @@ -337,11 +336,9 @@ class LarkAdapter(adapter.MessagePlatformAdapter): config: dict quart_app: quart.Quart - ap: app.Application - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger self.quart_app = quart.Quart(__name__) self.listeners = {} @@ -351,8 +348,6 @@ class LarkAdapter(adapter.MessagePlatformAdapter): try: data = await quart.request.json - self.ap.logger.debug(f'Lark callback event: {data}') - if 'encrypt' in data: cipher = AESCipher(self.config['encrypt-key']) data = cipher.decrypt_string(data['encrypt']) diff --git a/pkg/platform/sources/gewechat.png b/pkg/platform/sources/legacy/gewechat.png similarity index 100% rename from pkg/platform/sources/gewechat.png rename to pkg/platform/sources/legacy/gewechat.png diff --git a/pkg/platform/sources/gewechat.py b/pkg/platform/sources/legacy/gewechat.py similarity index 98% rename from pkg/platform/sources/gewechat.py rename to pkg/platform/sources/legacy/gewechat.py index 01d9f946..7e7b7715 100644 --- a/pkg/platform/sources/gewechat.py +++ b/pkg/platform/sources/legacy/gewechat.py @@ -11,16 +11,16 @@ import threading import quart import aiohttp -from .. import adapter -from ...core import app -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ...utils import image +from ... import adapter +from ....core import app +from ...types import message as platform_message +from ...types import events as platform_events +from ...types import entities as platform_entities +from ....utils import image import xml.etree.ElementTree as ET from typing import Optional, Tuple from functools import partial -from ..logger import EventLogger +from ...logger import EventLogger class GewechatMessageConverter(adapter.MessageConverter): @@ -491,7 +491,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): async def gewechat_callback(): data = await quart.request.json # print(json.dumps(data, indent=4, ensure_ascii=False)) - self.ap.logger.debug(f'Gewechat callback event: {data}') + await self.logger.debug(f'Gewechat callback event: {data}') if 'data' in data: data['Data'] = data['data'] @@ -601,7 +601,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): if handler := handler_map.get(msg['type']): handler(msg) else: - self.ap.logger.warning(f'未处理的消息类型: {msg["type"]}') + await self.logger.warning(f'未处理的消息类型: {msg["type"]}') continue async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -656,9 +656,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): self.config['app_id'] = app_id - self.ap.logger.info(f'Gewechat 登录成功,app_id: {app_id}') - - self.ap.platform_mgr.write_back_config('gewechat', self, self.config) + print(f'Gewechat 登录成功,app_id: {app_id}') # 获取 nickname profile = self.bot.get_profile(self.config['app_id']) diff --git a/pkg/platform/sources/gewechat.yaml b/pkg/platform/sources/legacy/gewechat.yaml similarity index 100% rename from pkg/platform/sources/gewechat.yaml rename to pkg/platform/sources/legacy/gewechat.yaml diff --git a/pkg/platform/sources/nakuru.png b/pkg/platform/sources/legacy/nakuru.png similarity index 100% rename from pkg/platform/sources/nakuru.png rename to pkg/platform/sources/legacy/nakuru.png diff --git a/pkg/platform/sources/nakuru.py b/pkg/platform/sources/legacy/nakuru.py similarity index 97% rename from pkg/platform/sources/nakuru.py rename to pkg/platform/sources/legacy/nakuru.py index 16ad54db..5afb6356 100644 --- a/pkg/platform/sources/nakuru.py +++ b/pkg/platform/sources/legacy/nakuru.py @@ -9,12 +9,12 @@ import traceback import nakuru import nakuru.entities.components as nkc -from .. import adapter as adapter_model -from ...pipeline.longtext.strategies import forward -from ...platform.types import message as platform_message -from ...platform.types import entities as platform_entities -from ...platform.types import events as platform_events -from ..logger import EventLogger +from ... import adapter as adapter_model +from ....pipeline.longtext.strategies import forward +from ...types import message as platform_message +from ...types import entities as platform_entities +from ...types import events as platform_events +from ...logger import EventLogger class NakuruProjectMessageConverter(adapter_model.MessageConverter): @@ -262,7 +262,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): source_cls = NakuruProjectEventConverter.yiri2target(event_type) # 包装函数 - async def listener_wrapper(app: nakuru.CQHTTP, source: source_cls): + async def listener_wrapper(app: nakuru.CQHTTP, source: source_cls): # type: ignore await callback(self.event_converter.target2yiri(source), self) # 将包装函数和原函数的对应关系存入列表 @@ -322,7 +322,6 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): except Exception: raise Exception('获取go-cqhttp账号信息失败, 请检查是否已启动go-cqhttp并配置正确') await self.bot._run() - self.ap.logger.info('运行 Nakuru 适配器') while True: await asyncio.sleep(1) diff --git a/pkg/platform/sources/nakuru.yaml b/pkg/platform/sources/legacy/nakuru.yaml similarity index 100% rename from pkg/platform/sources/nakuru.yaml rename to pkg/platform/sources/legacy/nakuru.yaml diff --git a/pkg/platform/sources/qqbotpy.py b/pkg/platform/sources/legacy/qqbotpy.py similarity index 97% rename from pkg/platform/sources/qqbotpy.py rename to pkg/platform/sources/legacy/qqbotpy.py index d4a4d526..7e8fb125 100644 --- a/pkg/platform/sources/qqbotpy.py +++ b/pkg/platform/sources/legacy/qqbotpy.py @@ -10,14 +10,14 @@ import botpy import botpy.message as botpy_message import botpy.types.message as botpy_message_type -from .. import adapter as adapter_model -from ...pipeline.longtext.strategies import forward -from ...core import app -from ...config import manager as cfg_mgr -from ...platform.types import entities as platform_entities -from ...platform.types import events as platform_events -from ...platform.types import message as platform_message -from ..logger import EventLogger +from ... import adapter as adapter_model +from ....pipeline.longtext.strategies import forward +from ....core import app +from ....config import manager as cfg_mgr +from ...types import entities as platform_entities +from ...types import events as platform_events +from ...types import message as platform_message +from ...logger import EventLogger class OfficialGroupMessage(platform_events.GroupMessage): @@ -519,7 +519,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): self.cfg['ret_coro'] = True - self.ap.logger.info('运行 QQ 官方适配器') + await self.logger.info('运行 QQ 官方适配器') await (await self.bot.start(**self.cfg)) async def kill(self) -> bool: diff --git a/pkg/platform/sources/qqbotpy.svg b/pkg/platform/sources/legacy/qqbotpy.svg similarity index 100% rename from pkg/platform/sources/qqbotpy.svg rename to pkg/platform/sources/legacy/qqbotpy.svg diff --git a/pkg/platform/sources/qqbotpy.yaml b/pkg/platform/sources/legacy/qqbotpy.yaml similarity index 100% rename from pkg/platform/sources/qqbotpy.yaml rename to pkg/platform/sources/legacy/qqbotpy.yaml diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 3fc1e393..925b0ee4 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -10,7 +10,6 @@ from libs.official_account_api.oaevent import OAEvent from libs.official_account_api.api import OAClient from libs.official_account_api.api import OAClientForLongerResponse from .. import adapter -from ...core import app from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError from ..logger import EventLogger @@ -58,15 +57,13 @@ class OAEventConverter(adapter.EventConverter): class OfficialAccountAdapter(adapter.MessagePlatformAdapter): bot: OAClient | OAClientForLongerResponse - ap: app.Application bot_account_id: str message_converter: OAMessageConverter = OAMessageConverter() event_converter: OAEventConverter = OAEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index 63ab531f..cd7beb31 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -8,7 +8,6 @@ import datetime from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from .. import adapter -from ...core import app from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError from libs.qq_official_api.api import QQOfficialClient @@ -134,15 +133,13 @@ class QQOfficialEventConverter(adapter.EventConverter): class QQOfficialAdapter(adapter.MessagePlatformAdapter): bot: QQOfficialClient - ap: app.Application config: dict bot_account_id: str message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter() event_converter: QQOfficialEventConverter = QQOfficialEventConverter() - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index 1bd5aa2d..ff14ce1c 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -9,7 +9,6 @@ from libs.slack_api.api import SlackClient from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.slack_api.slackevent import SlackEvent -from pkg.core import app from .. import adapter from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError @@ -86,15 +85,13 @@ class SlackEventConverter(adapter.EventConverter): class SlackAdapter(adapter.MessagePlatformAdapter): bot: SlackClient - ap: app.Application bot_account_id: str message_converter: SlackMessageConverter = SlackMessageConverter() event_converter: SlackEventConverter = SlackEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ 'bot_token', diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index c2fcc22e..52d79853 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -10,10 +10,7 @@ import traceback import base64 import aiohttp -from lark_oapi.api.im.v1 import * - from .. import adapter -from ...core import app from ..types import message as platform_message from ..types import events as platform_events from ..types import entities as platform_entities @@ -141,16 +138,14 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): event_converter: TelegramEventConverter = TelegramEventConverter() config: dict - ap: app.Application listeners: typing.Dict[ typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 51b0479f..0a35c1ac 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -44,13 +44,14 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): webchat_person_session: WebChatSession webchat_group_session: WebChatSession + ap: app.Application # set by bot manager + listeners: typing.Dict[ typing.Type[platform_events.Event], typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): - self.ap = ap + def __init__(self, config: dict, logger: EventLogger): self.logger = logger self.config = config diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index 88ec9bd9..0188d788 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -488,6 +488,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): ap: app.Application + logger: EventLogger + message_converter: WeChatPadMessageConverter event_converter: WeChatPadEventConverter @@ -507,8 +509,6 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): async def ws_message(self, data): """处理接收到的消息""" - # self.ap.logger.debug(f"Gewechat callback event: {data}") - # print(data) try: event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id) @@ -571,9 +571,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if handler := handler_map.get(msg['type']): handler(msg) - # self.ap.logger.warning(f"未处理的消息类型: {ret}") else: - self.ap.logger.warning(f'未处理的消息类型: {msg["type"]}') + print(f'未处理的消息类型: {msg["type"]}') continue async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -615,7 +614,6 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): if self.config['token']: self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) data = self.bot.get_login_status() - self.ap.logger.info(data) if data['Code'] == 300 and data['Text'] == '你已退出微信': response = requests.post( f'{self.config["wechatpad_url"]}/admin/GenAuthKey1?key={self.config["admin_key"]}', @@ -635,7 +633,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): self.config['token'] = response.json()['Data'][0] self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token'], logger=self.logger) - self.ap.logger.info(self.config['token']) + await self.logger.info(self.config['token']) thread_1 = threading.Event() def wechat_login_process(): @@ -643,10 +641,9 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # login_data =self.bot.get_login_qr() # url = login_data['Data']["QrCodeUrl"] - # self.ap.logger.info(login_data) profile = self.bot.get_profile() - self.ap.logger.info(profile) + self.logger.info(profile) self.bot_account_id = profile['Data']['userInfo']['nickName']['str'] self.config['wxid'] = profile['Data']['userInfo']['userName']['str'] @@ -658,27 +655,26 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): def connect_websocket_sync() -> None: thread_1.wait() uri = f'{self.config["wechatpad_ws"]}/GetSyncMsg?key={self.config["token"]}' - self.ap.logger.info(f'Connecting to WebSocket: {uri}') + print(f'Connecting to WebSocket: {uri}') def on_message(ws, message): try: data = json.loads(message) - self.ap.logger.debug(f'Received message: {data}') # 这里需要确保ws_message是同步的,或者使用asyncio.run调用异步方法 asyncio.run(self.ws_message(data)) except json.JSONDecodeError: - self.ap.logger.error(f'Non-JSON message: {message[:100]}...') + print(f'Non-JSON message: {message[:100]}...') def on_error(ws, error): - self.ap.logger.error(f'WebSocket error: {str(error)[:200]}') + print(f'WebSocket error: {str(error)[:200]}') def on_close(ws, close_status_code, close_msg): - self.ap.logger.info('WebSocket closed, reconnecting...') + print('WebSocket closed, reconnecting...') time.sleep(5) connect_websocket_sync() # 自动重连 def on_open(ws): - self.ap.logger.info('WebSocket connected successfully!') + print('WebSocket connected successfully!') ws = websocket.WebSocketApp( uri, on_message=on_message, on_error=on_error, on_close=on_close, on_open=on_open @@ -689,10 +685,9 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): # connect_websocket_sync() # 这行代码会在WebSocket连接断开后才会执行 - # self.ap.logger.info("WebSocket client thread started") thread = threading.Thread(target=connect_websocket_sync, name='WebSocketClientThread', daemon=True) thread.start() - self.ap.logger.info('WebSocket client thread started') + self.logger.info('WebSocket client thread started') async def kill(self) -> bool: pass diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 7be05a85..7bb0a757 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -10,7 +10,6 @@ from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.wecom_api.wecomevent import WecomEvent from .. import adapter -from ...core import app from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError from ...utils import image @@ -129,15 +128,13 @@ class WecomEventConverter: class WecomAdapter(adapter.MessagePlatformAdapter): bot: WecomClient - ap: app.Application bot_account_id: str message_converter: WecomMessageConverter = WecomMessageConverter() event_converter: WecomEventConverter = WecomEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index da84ac6d..fcd5378e 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -9,7 +9,6 @@ from libs.wecom_customer_service_api.api import WecomCSClient from pkg.platform.adapter import MessagePlatformAdapter from pkg.platform.types import events as platform_events, message as platform_message from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent -from pkg.core import app from .. import adapter from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError @@ -119,15 +118,13 @@ class WecomEventConverter: class WecomCSAdapter(adapter.MessagePlatformAdapter): bot: WecomCSClient - ap: app.Application bot_account_id: str message_converter: WecomMessageConverter = WecomMessageConverter() event_converter: WecomEventConverter = WecomEventConverter() config: dict - def __init__(self, config: dict, ap: app.Application, logger: EventLogger): + def __init__(self, config: dict, logger: EventLogger): self.config = config - self.ap = ap self.logger = logger required_keys = [ diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py index 777b61d6..e6e2dccb 100644 --- a/pkg/plugin/events.py +++ b/pkg/plugin/events.py @@ -4,16 +4,16 @@ import typing import pydantic.v1 as pydantic -from ..core import entities as core_entities from ..provider import entities as llm_entities from ..platform.types import message as platform_message import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class BaseEventModel(pydantic.BaseModel): """事件模型基类""" - query: typing.Union[core_entities.Query, None] + query: typing.Union[pipeline_query.Query, None] """此次请求的query对象,非请求过程的事件时为None""" class Config: diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index c94b0d7d..6613bb63 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -6,10 +6,10 @@ import importlib import traceback from .. import loader, events, context, models -from ...core import entities as core_entities from langbot_plugin.api.entities.builtin.resource import tool as resource_tool from ...utils import funcschema from ...discover import engine as discover_engine +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class PluginLoader(loader.PluginLoader): @@ -98,7 +98,7 @@ class PluginLoader(loader.PluginLoader): function_schema = funcschema.get_func_schema(func) function_name = self._current_container.plugin_name + '-' + (func.__name__ if name is None else name) - async def handler(plugin: context.BasePlugin, query: core_entities.Query, *args, **kwargs): + async def handler(plugin: context.BasePlugin, query: pipeline_query.Query, *args, **kwargs): return func(*args, **kwargs) llm_function = resource_tool.LLMTool( diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 94b812d9..1f38ca01 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -import pydantic.v1 as pydantic +import pydantic from pkg.provider import entities diff --git a/pkg/provider/modelmgr/entities.py b/pkg/provider/modelmgr/entities.py index cf856894..91d1d6e9 100644 --- a/pkg/provider/modelmgr/entities.py +++ b/pkg/provider/modelmgr/entities.py @@ -2,7 +2,7 @@ from __future__ import annotations import typing -import pydantic.v1 as pydantic +import pydantic from . import requester from . import token diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index 4008ca16..b8443b2c 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -4,11 +4,11 @@ import abc import typing from ...core import app -from ...core import entities as core_entities from .. import entities as llm_entities from ...entity.persistence import model as persistence_model import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from . import token +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class RuntimeLLMModel: @@ -56,7 +56,7 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): @abc.abstractmethod async def invoke_llm( self, - query: core_entities.Query, + query: pipeline_query.Query, model: RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 4655b3e0..1a100ca3 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -9,10 +9,10 @@ import httpx from .. import errors, requester -from ....core import entities as core_entities from ... import entities as llm_entities from ....utils import image import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class AnthropicMessages(requester.LLMAPIRequester): @@ -48,7 +48,7 @@ class AnthropicMessages(requester.LLMAPIRequester): async def invoke_llm( self, - query: core_entities.Query, + query: pipeline_query.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 00ff0a41..944e0eef 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,9 +8,9 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ....core import entities as core_entities from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class OpenAIChatCompletions(requester.LLMAPIRequester): @@ -60,7 +60,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, @@ -101,7 +101,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def invoke_llm( self, - query: core_entities.Query, + query: pipeline_query.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index 6dced3c9..ecf7a697 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -4,9 +4,9 @@ import typing from . import chatcmpl from .. import errors, requester -from ....core import entities as core_entities from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -19,7 +19,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 26da7d6d..9828e2ca 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -5,9 +5,9 @@ import typing from . import chatcmpl from .. import requester -from ....core import entities as core_entities from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -20,7 +20,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index e46d102e..68eb7399 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -9,9 +9,9 @@ import openai.types.chat.chat_completion_message_tool_call as chat_completion_me import httpx from .. import entities, errors, requester -from ....core import entities as core_entities from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class ModelScopeChatCompletions(requester.LLMAPIRequester): @@ -125,7 +125,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, @@ -166,7 +166,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): async def invoke_llm( self, - query: core_entities.Query, + query: pipeline_query.Query, model: entities.LLMModelInfo, messages: typing.List[llm_entities.Message], funcs: typing.List[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index e5019426..20c3427c 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -5,9 +5,9 @@ import typing from . import chatcmpl from .. import requester -from ....core import entities as core_entities from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -20,7 +20,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index 2afe34b3..b22895a6 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -12,7 +12,7 @@ import ollama from .. import errors, requester from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -from ....core import entities as core_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query REQUESTER_NAME: str = 'ollama-chat' @@ -39,7 +39,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): async def _closure( self, - query: core_entities.Query, + query: pipeline_query.Query, req_messages: list[dict], use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, @@ -105,7 +105,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): async def invoke_llm( self, - query: core_entities.Query, + query: pipeline_query.Query, model: requester.RuntimeLLMModel, messages: typing.List[llm_entities.Message], funcs: typing.List[resource_tool.LLMTool] = None, diff --git a/pkg/provider/runner.py b/pkg/provider/runner.py index a74a2dc5..42f702f8 100644 --- a/pkg/provider/runner.py +++ b/pkg/provider/runner.py @@ -3,8 +3,9 @@ from __future__ import annotations import abc import typing -from ..core import app, entities as core_entities +from ..core import app from . import entities as llm_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_runners: list[typing.Type[RequestRunner]] = [] @@ -35,6 +36,6 @@ class RequestRunner(abc.ABC): self.pipeline_config = pipeline_config @abc.abstractmethod - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" pass diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 02cb0b51..7c71d6b3 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -6,8 +6,9 @@ import re import dashscope from .. import runner -from ...core import app, entities as core_entities +from ...core import app from .. import entities as llm_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class DashscopeAPIError(Exception): @@ -65,7 +66,7 @@ class DashScopeAPIRunner(runner.RequestRunner): # 使用 re.sub() 进行替换 return pattern.sub(replacement, text) - async def _preprocess_user_message(self, query: core_entities.Query) -> tuple[str, list[str]]: + async def _preprocess_user_message(self, query: pipeline_query.Query) -> tuple[str, list[str]]: """预处理用户消息,提取纯文本,阿里云提供的上传文件方法过于复杂,暂不支持上传文件(包括图片)""" plain_text = '' image_ids = [] @@ -89,7 +90,7 @@ class DashScopeAPIRunner(runner.RequestRunner): return plain_text, image_ids - async def _agent_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _agent_messages(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 智能体对话请求""" # 局部变量 @@ -147,7 +148,9 @@ class DashScopeAPIRunner(runner.RequestRunner): content=pending_content, ) - async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _workflow_messages( + self, query: pipeline_query.Query + ) -> typing.AsyncGenerator[llm_entities.Message, None]: """Dashscope 工作流对话请求""" # 局部变量 @@ -210,7 +213,7 @@ class DashScopeAPIRunner(runner.RequestRunner): content=pending_content, ) - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行""" if self.app_type == 'agent': async for msg in self._agent_messages(query): diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index b2542491..c5819de3 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -8,10 +8,10 @@ import base64 from .. import runner -from ...core import app, entities as core_entities +from ...core import app from .. import entities as llm_entities from ...utils import image - +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from libs.dify_service_api.v1 import client, errors @@ -62,7 +62,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): content_text = re.sub(pattern, '', resp_text, flags=re.DOTALL) return f'{thinking_text.group(1)}\n{content_text}' - async def _preprocess_user_message(self, query: core_entities.Query) -> tuple[str, list[str]]: + async def _preprocess_user_message(self, query: pipeline_query.Query) -> tuple[str, list[str]]: """预处理用户消息,提取纯文本,并将图片上传到 Dify 服务 Returns: @@ -90,7 +90,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): return plain_text, image_ids - async def _chat_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _chat_messages(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用聊天助手""" cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id @@ -152,7 +152,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.session.using_conversation.uuid = chunk['conversation_id'] async def _agent_chat_messages( - self, query: core_entities.Query + self, query: pipeline_query.Query ) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用聊天助手""" cov_id = query.session.using_conversation.uuid or '' @@ -244,7 +244,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): query.session.using_conversation.uuid = chunk['conversation_id'] - async def _workflow_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _workflow_messages( + self, query: pipeline_query.Query + ) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用工作流""" if not query.session.using_conversation.uuid: @@ -316,7 +318,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): yield msg - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': async for msg in self._chat_messages(query): diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index e87ee81d..5a879bcb 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -4,15 +4,15 @@ import json import typing from .. import runner -from ...core import entities as core_entities from .. import entities as llm_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @runner.runner_class('local-agent') class LocalAgentRunner(runner.RequestRunner): """本地Agent请求运行器""" - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" pending_tool_calls = [] diff --git a/pkg/provider/runners/n8nsvapi.py b/pkg/provider/runners/n8nsvapi.py index 7044cce1..37567d15 100644 --- a/pkg/provider/runners/n8nsvapi.py +++ b/pkg/provider/runners/n8nsvapi.py @@ -6,8 +6,9 @@ import uuid import aiohttp from .. import runner -from ...core import app, entities as core_entities +from ...core import app from .. import entities as llm_entities +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class N8nAPIError(Exception): @@ -49,7 +50,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): self.header_name = self.pipeline_config['ai']['n8n-service-api'].get('header-name', '') self.header_value = self.pipeline_config['ai']['n8n-service-api'].get('header-value', '') - async def _preprocess_user_message(self, query: core_entities.Query) -> str: + async def _preprocess_user_message(self, query: pipeline_query.Query) -> str: """预处理用户消息,提取纯文本 Returns: @@ -67,7 +68,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): return plain_text - async def _call_webhook(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _call_webhook(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """调用n8n webhook""" # 生成会话ID(如果不存在) if not query.session.using_conversation.uuid: @@ -153,7 +154,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): self.ap.logger.error(f'n8n webhook call exception: {str(e)}') raise N8nAPIError(f'n8n webhook call exception: {str(e)}') - async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: """运行请求""" async for msg in self._call_webhook(query): yield msg diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index 500ab49c..03465e0b 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio -from ...core import app, entities as core_entities +from ...core import app from langbot_plugin.api.entities.builtin.provider import message as provider_message, prompt as provider_prompt import langbot_plugin.api.entities.builtin.provider.session as provider_session +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class SessionManager: @@ -21,7 +22,7 @@ class SessionManager: async def initialize(self): pass - async def get_session(self, query: core_entities.Query) -> provider_session.Session: + async def get_session(self, query: pipeline_query.Query) -> provider_session.Session: """获取会话""" for session in self.session_list: if query.launcher_type == session.launcher_type and query.launcher_id == session.launcher_id: @@ -39,7 +40,7 @@ class SessionManager: async def get_conversation( self, - query: core_entities.Query, + query: pipeline_query.Query, session: provider_session.Session, prompt_config: list[dict], pipeline_uuid: str, diff --git a/pkg/provider/tools/loader.py b/pkg/provider/tools/loader.py index fca9aa93..658fdeb6 100644 --- a/pkg/provider/tools/loader.py +++ b/pkg/provider/tools/loader.py @@ -3,8 +3,9 @@ from __future__ import annotations import abc import typing -from ...core import app, entities as core_entities +from ...core import app import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_loaders: list[typing.Type[ToolLoader]] = [] @@ -45,7 +46,7 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: """执行工具调用""" pass diff --git a/pkg/provider/tools/loaders/mcp.py b/pkg/provider/tools/loaders/mcp.py index bf35990e..577c704e 100644 --- a/pkg/provider/tools/loaders/mcp.py +++ b/pkg/provider/tools/loaders/mcp.py @@ -8,8 +8,9 @@ from mcp.client.stdio import stdio_client from mcp.client.sse import sse_client from .. import loader -from ....core import app, entities as core_entities +from ....core import app import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class RuntimeMCPSession: @@ -83,7 +84,7 @@ class RuntimeMCPSession: for tool in tools.tools: - async def func(query: core_entities.Query, *, _tool=tool, **kwargs): + async def func(query: pipeline_query.Query, *, _tool=tool, **kwargs): result = await self.session.call_tool(_tool.name, kwargs) if result.isError: raise Exception(result.content[0].text) @@ -144,7 +145,7 @@ class MCPLoader(loader.ToolLoader): async def has_tool(self, name: str) -> bool: return name in [f.name for f in self._last_listed_functions] - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: for server_name, session in self.sessions.items(): for function in session.functions: if function.name == name: diff --git a/pkg/provider/tools/loaders/plugin.py b/pkg/provider/tools/loaders/plugin.py index c6ecda7d..7dfaea97 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -4,9 +4,9 @@ import typing import traceback from .. import loader -from ....core import entities as core_entities from ....plugin import context as plugin_context import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @loader.loader_class('plugin-tool-loader') @@ -49,7 +49,7 @@ class PluginToolLoader(loader.ToolLoader): return function, plugin.plugin_inst return None, None - async def invoke_tool(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: try: function, plugin = await self._get_function_and_plugin(name) if function is None: diff --git a/pkg/provider/tools/toolmgr.py b/pkg/provider/tools/toolmgr.py index 5f0cbdbf..e1105750 100644 --- a/pkg/provider/tools/toolmgr.py +++ b/pkg/provider/tools/toolmgr.py @@ -2,11 +2,12 @@ from __future__ import annotations import typing -from ...core import app, entities as core_entities +from ...core import app from . import loader as tools_loader from ...utils import importutil from . import loaders import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query importutil.import_modules_in_pkg(loaders) @@ -90,7 +91,7 @@ class ToolManager: return tools - async def execute_func_call(self, query: core_entities.Query, name: str, parameters: dict) -> typing.Any: + async def execute_func_call(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: """执行函数调用""" for loader in self.loaders: diff --git a/pkg/utils/announce.py b/pkg/utils/announce.py index 7108a08c..56de579d 100644 --- a/pkg/utils/announce.py +++ b/pkg/utils/announce.py @@ -6,7 +6,7 @@ import os import base64 import logging -import pydantic.v1 as pydantic +import pydantic import requests from ..core import app From 62b2884011f86044853be70521b4c204ce81d789 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 15 Jun 2025 23:24:58 +0800 Subject: [PATCH 07/78] chore: delete Query class --- pkg/core/entities.py | 100 ------------------------------------------- 1 file changed, 100 deletions(-) diff --git a/pkg/core/entities.py b/pkg/core/entities.py index 5abb7c74..4383f07f 100644 --- a/pkg/core/entities.py +++ b/pkg/core/entities.py @@ -1,16 +1,6 @@ from __future__ import annotations import enum -import typing - -import pydantic - -from ..provider import entities as llm_entities -from ..platform import adapter as msadapter -from ..platform.types import message as platform_message -from ..platform.types import events as platform_events -import langbot_plugin.api.entities.builtin.provider.session as provider_session -import langbot_plugin.api.entities.builtin.resource.tool as resource_tool class LifecycleControlScope(enum.Enum): @@ -18,93 +8,3 @@ class LifecycleControlScope(enum.Enum): PLATFORM = 'platform' PLUGIN = 'plugin' PROVIDER = 'provider' - - -class Query(pydantic.BaseModel): - """一次请求的信息封装""" - - query_id: int - """请求ID,添加进请求池时生成""" - - launcher_type: provider_session.LauncherTypes - """会话类型,platform处理阶段设置""" - - launcher_id: typing.Union[int, str] - """会话ID,platform处理阶段设置""" - - sender_id: typing.Union[int, str] - """发送者ID,platform处理阶段设置""" - - message_event: platform_events.MessageEvent - """事件,platform收到的原始事件""" - - message_chain: platform_message.MessageChain - """消息链,platform收到的原始消息链""" - - bot_uuid: typing.Optional[str] = None - """机器人UUID。""" - - pipeline_uuid: typing.Optional[str] = None - """流水线UUID。""" - - pipeline_config: typing.Optional[dict[str, typing.Any]] = None - """流水线配置,由 Pipeline 在运行开始时设置。""" - - adapter: msadapter.MessagePlatformAdapter - """消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器""" - - session: typing.Optional[provider_session.Session] = None - """会话对象,由前置处理器阶段设置""" - - messages: typing.Optional[list[llm_entities.Message]] = [] - """历史消息列表,由前置处理器阶段设置""" - - prompt: typing.Optional[llm_entities.Prompt] = None - """情景预设内容,由前置处理器阶段设置""" - - user_message: typing.Optional[llm_entities.Message] = None - """此次请求的用户消息对象,由前置处理器阶段设置""" - - variables: typing.Optional[dict[str, typing.Any]] = None - """变量,由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。""" - - use_llm_model_uuid: typing.Optional[str] = None - """使用的对话模型,由前置处理器阶段设置""" - - use_funcs: typing.Optional[list[resource_tool.LLMTool]] = None - """使用的函数,由前置处理器阶段设置""" - - resp_messages: ( - typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]] - ) = [] - """由Process阶段生成的回复消息对象列表""" - - resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None - """回复消息链,从resp_messages包装而得""" - - # ======= 内部保留 ======= - current_stage_name: typing.Optional[str] = None - """当前所处阶段""" - - class Config: - arbitrary_types_allowed = True - - # ========== 插件可调用的 API(请求 API) ========== - - def set_variable(self, key: str, value: typing.Any): - """设置变量""" - if self.variables is None: - self.variables = {} - self.variables[key] = value - - def get_variable(self, key: str) -> typing.Any: - """获取变量""" - if self.variables is None: - return None - return self.variables.get(key) - - def get_variables(self) -> dict[str, typing.Any]: - """获取所有变量""" - if self.variables is None: - return {} - return self.variables From a1777860630810b9fc6e4a64bfc0308dd982be54 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 16 Jun 2025 13:18:59 +0800 Subject: [PATCH 08/78] feat: switch message platform adapters to sdk --- libs/qq_official_api/api.py | 2 +- libs/slack_api/api.py | 2 +- libs/wecom_api/api.py | 2 +- libs/wecom_customer_service_api/api.py | 2 +- pkg/command/entities.py | 2 +- pkg/pipeline/cntfilter/cntfilter.py | 2 +- pkg/pipeline/entities.py | 2 +- pkg/pipeline/longtext/longtext.py | 2 +- pkg/pipeline/longtext/strategies/forward.py | 2 +- pkg/pipeline/longtext/strategies/image.py | 2 +- pkg/pipeline/longtext/strategy.py | 3 +- pkg/pipeline/pipelinemgr.py | 3 +- pkg/pipeline/pool.py | 8 +- pkg/pipeline/preproc/preproc.py | 2 +- pkg/pipeline/process/handlers/chat.py | 2 +- pkg/pipeline/process/handlers/command.py | 4 +- pkg/pipeline/respback/respback.py | 4 +- pkg/pipeline/resprule/entities.py | 2 +- pkg/pipeline/resprule/rule.py | 2 +- pkg/pipeline/resprule/rules/atbot.py | 2 +- pkg/pipeline/resprule/rules/prefix.py | 2 +- pkg/pipeline/resprule/rules/random.py | 2 +- pkg/pipeline/resprule/rules/regexp.py | 2 +- pkg/pipeline/wrapper/wrapper.py | 2 +- pkg/platform/adapter.py | 156 ---- pkg/platform/adapter.yaml | 14 - pkg/platform/botmgr.py | 65 +- pkg/platform/logger.py | 5 +- pkg/platform/sources/aiocqhttp.py | 42 +- pkg/platform/sources/dingtalk.py | 23 +- pkg/platform/sources/discord.py | 50 +- pkg/platform/sources/lark.py | 62 +- pkg/platform/sources/legacy/gewechat.py | 24 +- pkg/platform/sources/legacy/nakuru.py | 22 +- pkg/platform/sources/legacy/qqbotpy.py | 22 +- pkg/platform/sources/officialaccount.py | 22 +- pkg/platform/sources/qqofficial.py | 22 +- pkg/platform/sources/slack.py | 22 +- pkg/platform/sources/telegram.py | 54 +- pkg/platform/sources/webchat.py | 50 +- pkg/platform/sources/wechatpad.py | 43 +- pkg/platform/sources/wecom.py | 22 +- pkg/platform/sources/wecomcs.py | 49 +- pkg/platform/types/__init__.py | 3 - pkg/platform/types/base.py | 107 --- pkg/platform/types/entities.py | 88 -- pkg/platform/types/events.py | 106 --- pkg/platform/types/message.py | 975 -------------------- pkg/plugin/context.py | 8 +- pkg/plugin/events.py | 6 +- pkg/provider/entities.py | 2 +- 51 files changed, 361 insertions(+), 1763 deletions(-) delete mode 100644 pkg/platform/adapter.py delete mode 100644 pkg/platform/adapter.yaml delete mode 100644 pkg/platform/types/__init__.py delete mode 100644 pkg/platform/types/base.py delete mode 100644 pkg/platform/types/entities.py delete mode 100644 pkg/platform/types/events.py delete mode 100644 pkg/platform/types/message.py diff --git a/libs/qq_official_api/api.py b/libs/qq_official_api/api.py index cb5f658a..c5728437 100644 --- a/libs/qq_official_api/api.py +++ b/libs/qq_official_api/api.py @@ -3,7 +3,7 @@ from quart import request import httpx from quart import Quart from typing import Callable, Dict, Any -from pkg.platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.events as platform_events from .qqofficialevent import QQOfficialEvent import json import traceback diff --git a/libs/slack_api/api.py b/libs/slack_api/api.py index 746d15da..241a42cf 100644 --- a/libs/slack_api/api.py +++ b/libs/slack_api/api.py @@ -4,7 +4,7 @@ from quart import Quart, jsonify, request from slack_sdk.web.async_client import AsyncWebClient from .slackevent import SlackEvent from typing import Callable -from pkg.platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.events as platform_events class SlackClient: diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index c1328b0d..352a550c 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -8,7 +8,7 @@ from quart import Quart import xml.etree.ElementTree as ET from typing import Callable, Dict, Any from .wecomevent import WecomEvent -from pkg.platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import aiofiles diff --git a/libs/wecom_customer_service_api/api.py b/libs/wecom_customer_service_api/api.py index 32fab7f7..f912326e 100644 --- a/libs/wecom_customer_service_api/api.py +++ b/libs/wecom_customer_service_api/api.py @@ -8,7 +8,7 @@ from quart import Quart import xml.etree.ElementTree as ET from typing import Callable from .wecomcsevent import WecomCSEvent -from pkg.platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import aiofiles diff --git a/pkg/command/entities.py b/pkg/command/entities.py index 7d6eecdc..2e4f8a96 100644 --- a/pkg/command/entities.py +++ b/pkg/command/entities.py @@ -6,7 +6,7 @@ import pydantic import langbot_plugin.api.entities.builtin.provider.session as provider_session from . import errors -from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index 1708363a..b40ecd3c 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -5,7 +5,7 @@ from ...core import app from .. import stage, entities from . import filter as filter_model, entities as filter_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...utils import importutil import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import filters diff --git a/pkg/pipeline/entities.py b/pkg/pipeline/entities.py index 7e7f23ce..5426685e 100644 --- a/pkg/pipeline/entities.py +++ b/pkg/pipeline/entities.py @@ -4,9 +4,9 @@ import enum import typing import pydantic -from ..platform.types import message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message class ResultType(enum.Enum): diff --git a/pkg/pipeline/longtext/longtext.py b/pkg/pipeline/longtext/longtext.py index 6356a16f..097c166a 100644 --- a/pkg/pipeline/longtext/longtext.py +++ b/pkg/pipeline/longtext/longtext.py @@ -5,7 +5,7 @@ import traceback from . import strategy from .. import stage, entities -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...utils import importutil import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from . import strategies diff --git a/pkg/pipeline/longtext/strategies/forward.py b/pkg/pipeline/longtext/strategies/forward.py index 574239b8..8040efff 100644 --- a/pkg/pipeline/longtext/strategies/forward.py +++ b/pkg/pipeline/longtext/strategies/forward.py @@ -4,8 +4,8 @@ from __future__ import annotations from .. import strategy as strategy_model -from ....platform.types import message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay Forward = platform_message.Forward diff --git a/pkg/pipeline/longtext/strategies/image.py b/pkg/pipeline/longtext/strategies/image.py index ba6ddc1b..110f1f81 100644 --- a/pkg/pipeline/longtext/strategies/image.py +++ b/pkg/pipeline/longtext/strategies/image.py @@ -8,10 +8,10 @@ import re from PIL import Image, ImageDraw, ImageFont import functools -from ....platform.types import message as platform_message from .. import strategy as strategy_model import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message @strategy_model.strategy_class('image') diff --git a/pkg/pipeline/longtext/strategy.py b/pkg/pipeline/longtext/strategy.py index dd69b2bb..018fb991 100644 --- a/pkg/pipeline/longtext/strategy.py +++ b/pkg/pipeline/longtext/strategy.py @@ -4,7 +4,8 @@ import typing from ...core import app -from ...platform.types import message as platform_message + +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index debdbb93..6dbfd1fa 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -9,7 +9,8 @@ from ..core import app from . import entities as pipeline_entities from ..entity.persistence import pipeline as persistence_pipeline from . import stage -from ..platform.types import message as platform_message, events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ..plugin import events from ..utils import importutil diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index a4313cdd..eb32fce6 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -3,11 +3,11 @@ from __future__ import annotations import asyncio import typing -from ..platform import adapter as msadapter -from ..platform.types import message as platform_message -from ..platform.types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter class QueryPool: @@ -35,7 +35,7 @@ class QueryPool: sender_id: typing.Union[int, str], message_event: platform_events.MessageEvent, message_chain: platform_message.MessageChain, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, pipeline_uuid: typing.Optional[str] = None, ) -> pipeline_query.Query: async with self.condition: diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index af851c96..894ceebf 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -5,7 +5,7 @@ import datetime from .. import stage, entities from langbot_plugin.api.entities.builtin.provider import message as provider_message from ...plugin import events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index b871de81..717727d0 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -9,7 +9,7 @@ from ... import entities from ....provider import runner as runner_module from ....plugin import events -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ....utils import importutil from ....provider import runners import langbot_plugin.api.entities.builtin.provider.session as provider_session diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index 15c33ebd..a6156946 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -4,9 +4,9 @@ import typing from .. import handler from ... import entities -from langbot_plugin.api.entities.builtin.provider import message as provider_message from ....plugin import events -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.provider.message as provider_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/respback/respback.py b/pkg/pipeline/respback/respback.py index b5a1ed74..4ffc9ca4 100644 --- a/pkg/pipeline/respback/respback.py +++ b/pkg/pipeline/respback/respback.py @@ -4,8 +4,8 @@ import random import asyncio -from ...platform.types import events as platform_events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message from .. import stage, entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/entities.py b/pkg/pipeline/resprule/entities.py index c2d964fe..71973c8a 100644 --- a/pkg/pipeline/resprule/entities.py +++ b/pkg/pipeline/resprule/entities.py @@ -1,6 +1,6 @@ import pydantic -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message class RuleJudgeResult(pydantic.BaseModel): diff --git a/pkg/pipeline/resprule/rule.py b/pkg/pipeline/resprule/rule.py index 7c91373f..34e89a72 100644 --- a/pkg/pipeline/resprule/rule.py +++ b/pkg/pipeline/resprule/rule.py @@ -5,7 +5,7 @@ import typing from ...core import app from . import entities -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/atbot.py b/pkg/pipeline/resprule/rules/atbot.py index fc3b5510..cf31cc31 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -3,7 +3,7 @@ from __future__ import annotations from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/prefix.py b/pkg/pipeline/resprule/rules/prefix.py index 2ae89fe1..72f0de77 100644 --- a/pkg/pipeline/resprule/rules/prefix.py +++ b/pkg/pipeline/resprule/rules/prefix.py @@ -1,6 +1,6 @@ from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/random.py b/pkg/pipeline/resprule/rules/random.py index 04818ef0..2bfe8b71 100644 --- a/pkg/pipeline/resprule/rules/random.py +++ b/pkg/pipeline/resprule/rules/random.py @@ -3,7 +3,7 @@ import random from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/resprule/rules/regexp.py b/pkg/pipeline/resprule/rules/regexp.py index 51589e0c..41e1df8e 100644 --- a/pkg/pipeline/resprule/rules/regexp.py +++ b/pkg/pipeline/resprule/rules/regexp.py @@ -3,7 +3,7 @@ import re from .. import rule as rule_model from .. import entities -from ....platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 8063ff36..2c6e218e 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -5,7 +5,7 @@ import typing from .. import entities from .. import stage from ...plugin import events -from ...platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query diff --git a/pkg/platform/adapter.py b/pkg/platform/adapter.py deleted file mode 100644 index f27efc75..00000000 --- a/pkg/platform/adapter.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import annotations - -# MessageSource的适配器 -import typing -import abc -import pydantic - -from .types import message as platform_message -from .types import events as platform_events -from .logger import EventLogger - - -class MessagePlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta): - """消息平台适配器基类""" - - name: str - - bot_account_id: int - """机器人账号ID,需要在初始化时设置""" - - config: dict - - logger: EventLogger = pydantic.Field(exclude=True) - - def __init__(self, config: dict, logger: EventLogger): - """初始化适配器 - - Args: - config (dict): 对应的配置 - ap (app.Application): 应用上下文 - """ - self.config = config - self.logger = logger - - async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): - """主动发送消息 - - Args: - target_type (str): 目标类型,`person`或`group` - target_id (str): 目标ID - message (platform.types.MessageChain): 消息链 - """ - raise NotImplementedError - - async def reply_message( - self, - message_source: platform_events.MessageEvent, - message: platform_message.MessageChain, - quote_origin: bool = False, - ): - """回复消息 - - Args: - message_source (platform.types.MessageEvent): 消息源事件 - message (platform.types.MessageChain): 消息链 - quote_origin (bool, optional): 是否引用原消息. Defaults to False. - """ - raise NotImplementedError - - async def is_muted(self, group_id: int) -> bool: - """获取账号是否在指定群被禁言""" - raise NotImplementedError - - def register_listener( - self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], - ): - """注册事件监听器 - - Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 - """ - raise NotImplementedError - - def unregister_listener( - self, - event_type: typing.Type[platform_message.Event], - callback: typing.Callable[[platform_message.Event, MessagePlatformAdapter], None], - ): - """注销事件监听器 - - Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 - """ - raise NotImplementedError - - async def run_async(self): - """异步运行""" - raise NotImplementedError - - async def kill(self) -> bool: - """关闭适配器 - - Returns: - bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层 - """ - raise NotImplementedError - - -class MessageConverter: - """消息链转换器基类""" - - @staticmethod - def yiri2target(message_chain: platform_message.MessageChain): - """将源平台消息链转换为目标平台消息链 - - Args: - message_chain (platform.types.MessageChain): 源平台消息链 - - Returns: - typing.Any: 目标平台消息链 - """ - raise NotImplementedError - - @staticmethod - def target2yiri(message_chain: typing.Any) -> platform_message.MessageChain: - """将目标平台消息链转换为源平台消息链 - - Args: - message_chain (typing.Any): 目标平台消息链 - - Returns: - platform.types.MessageChain: 源平台消息链 - """ - raise NotImplementedError - - -class EventConverter: - """事件转换器基类""" - - @staticmethod - def yiri2target(event: typing.Type[platform_message.Event]): - """将源平台事件转换为目标平台事件 - - Args: - event (typing.Type[platform.types.Event]): 源平台事件 - - Returns: - typing.Any: 目标平台事件 - """ - raise NotImplementedError - - @staticmethod - def target2yiri(event: typing.Any) -> platform_message.Event: - """将目标平台事件的调用参数转换为源平台的事件参数对象 - - Args: - event (typing.Any): 目标平台事件 - - Returns: - typing.Type[platform.types.Event]: 源平台事件 - """ - raise NotImplementedError diff --git a/pkg/platform/adapter.yaml b/pkg/platform/adapter.yaml deleted file mode 100644 index d32b412d..00000000 --- a/pkg/platform/adapter.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: ComponentTemplate -metadata: - name: MessagePlatformAdapter - label: - en_US: Message Platform Adapter - zh_Hans: 消息平台适配器模板类 -spec: - type: - - python -execution: - python: - path: ./adapter.py - attr: MessagePlatformAdapter diff --git a/pkg/platform/botmgr.py b/pkg/platform/botmgr.py index 8f247ca4..59341493 100644 --- a/pkg/platform/botmgr.py +++ b/pkg/platform/botmgr.py @@ -1,15 +1,10 @@ from __future__ import annotations -import sys import asyncio import traceback import sqlalchemy -# FriendMessage, Image, MessageChain, Plain -from . import adapter as msadapter - from ..core import app, entities as core_entities, taskmgr -from .types import events as platform_events, message as platform_message from ..discover import engine @@ -20,11 +15,9 @@ from ..entity.errors import platform as platform_errors from .logger import EventLogger import langbot_plugin.api.entities.builtin.provider.session as provider_session - -# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题 -from . import types as mirai - -sys.modules['mirai'] = mirai +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter class RuntimeBot: @@ -36,7 +29,7 @@ class RuntimeBot: enable: bool - adapter: msadapter.MessagePlatformAdapter + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter task_wrapper: taskmgr.TaskWrapper @@ -48,7 +41,7 @@ class RuntimeBot: self, ap: app.Application, bot_entity: persistence_bot.Bot, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, logger: EventLogger, ): self.ap = ap @@ -61,7 +54,7 @@ class RuntimeBot: async def initialize(self): async def on_friend_message( event: platform_events.FriendMessage, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): image_components = [ component for component in event.message_chain if isinstance(component, platform_message.Image) @@ -86,7 +79,7 @@ class RuntimeBot: async def on_group_message( event: platform_events.GroupMessage, - adapter: msadapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): image_components = [ component for component in event.message_chain if isinstance(component, platform_message.Image) @@ -153,7 +146,7 @@ class PlatformManager: adapter_components: list[engine.Component] - adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] + adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] def __init__(self, ap: app.Application = None): self.ap = ap @@ -163,7 +156,7 @@ class PlatformManager: async def initialize(self): self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter') - adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]] = {} + adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] = {} for component in self.adapter_components: adapter_dict[component.metadata.name] = component.get_python_component_class() self.adapter_dict = adapter_dict @@ -175,6 +168,7 @@ class PlatformManager: webchat_adapter_inst = webchat_adapter_class( {}, webchat_logger, + ap=self.ap, ) webchat_adapter_inst.ap = self.ap @@ -195,7 +189,7 @@ class PlatformManager: await self.load_bots_from_db() - def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]: + def get_running_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]: return [bot.adapter for bot in self.bots if bot.enable] async def load_bots_from_db(self): @@ -275,43 +269,6 @@ class PlatformManager: return component return None - async def write_back_config( - self, - adapter_name: str, - adapter_inst: msadapter.MessagePlatformAdapter, - config: dict, - ): - # index = -2 - - # for i, adapter in enumerate(self.adapters): - # if adapter == adapter_inst: - # index = i - # break - - # if index == -2: - # raise Exception('平台适配器未找到') - - # # 只修改启用的适配器 - # real_index = -1 - - # for i, adapter in enumerate(self.ap.platform_cfg.data['platform-adapters']): - # if adapter['enable']: - # index -= 1 - # if index == -1: - # real_index = i - # break - - # new_cfg = { - # 'adapter': adapter_name, - # 'enable': True, - # **config - # } - # self.ap.platform_cfg.data['platform-adapters'][real_index] = new_cfg - # await self.ap.platform_cfg.dump_config() - - # TODO implement this - pass - async def run(self): # This method will only be called when the application launching await self.webchat_proxy_bot.run() diff --git a/pkg/platform/logger.py b/pkg/platform/logger.py index 340baa07..cedaeb50 100644 --- a/pkg/platform/logger.py +++ b/pkg/platform/logger.py @@ -9,7 +9,8 @@ import traceback import uuid from ..core import app -from .types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_event_logger class EventLogLevel(enum.Enum): @@ -55,7 +56,7 @@ MAX_LOG_COUNT = 200 DELETE_COUNT_PER_TIME = 50 -class EventLogger: +class EventLogger(abstract_platform_event_logger.AbstractEventLogger): """used for logging bot events""" ap: app.Application diff --git a/pkg/platform/sources/aiocqhttp.py b/pkg/platform/sources/aiocqhttp.py index b2616bb0..ccfd3e53 100644 --- a/pkg/platform/sources/aiocqhttp.py +++ b/pkg/platform/sources/aiocqhttp.py @@ -5,16 +5,17 @@ import traceback import datetime import aiocqhttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...utils import image -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class AiocqhttpMessageConverter(adapter.MessageConverter): +class AiocqhttpMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -69,7 +70,6 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): elif msg.face_type=='dice': msg_list.append(aiocqhttp.MessageSegment.dice()) - else: msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) @@ -190,6 +190,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): file_data = await bot.get_file(file_id=file_id) file_name = file_data.get('file_name') file_path = file_data.get('file') + _ = file_path file_url = file_data.get('file_url') file_size = file_data.get('file_size') yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size)) @@ -211,7 +212,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter): return chain -class AiocqhttpEventConverter(adapter.EventConverter): +class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): return event.source_platform_object @@ -262,21 +263,19 @@ class AiocqhttpEventConverter(adapter.EventConverter): ) -class AiocqhttpAdapter(adapter.MessagePlatformAdapter): - bot: aiocqhttp.CQHttp - - bot_account_id: int +class AiocqhttpAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: aiocqhttp.CQHttp = pydantic.Field(exclude=True, default_factory=aiocqhttp.CQHttp) message_converter: AiocqhttpMessageConverter = AiocqhttpMessageConverter() event_converter: AiocqhttpEventConverter = AiocqhttpEventConverter() - config: dict - on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = [] - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + super().__init__( + config=config, + logger=logger, + ) async def shutdown_trigger_placeholder(): while True: @@ -296,7 +295,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] if target_type == 'group': - await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) elif target_type == 'person': await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg) @@ -320,7 +318,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: aiocqhttp.Event): self.bot_account_id = event.self_id @@ -351,7 +351,9 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index 1727a771..2a76c219 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -1,17 +1,16 @@ import traceback import typing from libs.dingtalk_api.dingtalkevent import DingTalkEvent -from pkg.platform.types import message as platform_message -from pkg.platform.adapter import MessagePlatformAdapter -from .. import adapter -from ..types import events as platform_events -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from libs.dingtalk_api.api import DingTalkClient import datetime from ..logger import EventLogger -class DingTalkMessageConverter(adapter.MessageConverter): +class DingTalkMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content = '' @@ -47,7 +46,7 @@ class DingTalkMessageConverter(adapter.MessageConverter): return chain -class DingTalkEventConverter(adapter.EventConverter): +class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent): return event.source_platform_object @@ -91,7 +90,7 @@ class DingTalkEventConverter(adapter.EventConverter): ) -class DingTalkAdapter(adapter.MessagePlatformAdapter): +class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: DingTalkClient bot_account_id: str message_converter: DingTalkMessageConverter = DingTalkMessageConverter() @@ -137,7 +136,9 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: DingTalkEvent): try: @@ -171,6 +172,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 52bd5e5b..5d24c77b 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -10,15 +10,16 @@ import os import datetime import aiohttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class DiscordMessageConverter(adapter.MessageConverter): +class DiscordMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, @@ -111,7 +112,7 @@ class DiscordMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(element_list) -class DiscordEventConverter(adapter.EventConverter): +class DiscordEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event) -> discord.Message: pass @@ -153,26 +154,21 @@ class DiscordEventConverter(adapter.EventConverter): ) -class DiscordAdapter(adapter.MessagePlatformAdapter): - bot: discord.Client - - bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 - - config: dict +class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: discord.Client = pydantic.Field(exclude=True) message_converter: DiscordMessageConverter = DiscordMessageConverter() event_converter: DiscordEventConverter = DiscordEventConverter() listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + bot_account_id = config['client_id'] - self.bot_account_id = self.config['client_id'] + listeners = {} adapter_self = self @@ -192,7 +188,15 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): if os.getenv('http_proxy'): args['proxy'] = os.getenv('http_proxy') - self.bot = MyClient(intents=intents, **args) + bot = MyClient(intents=intents, **args) + + super().__init__( + config=config, + logger=logger, + bot_account_id=bot_account_id, + listeners=listeners, + bot=bot, + ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass @@ -227,14 +231,18 @@ class DiscordAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 9e727ad3..7c8ae2eb 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -17,12 +17,13 @@ import aiohttp import lark_oapi.ws.exception import quart from lark_oapi.api.im.v1 import * +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger class AESCipher(object): @@ -51,7 +52,7 @@ class AESCipher(object): return self.decrypt(enc).decode('utf8') -class LarkMessageConverter(adapter.MessageConverter): +class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target( message_chain: platform_message.MessageChain, api_client: lark_oapi.Client @@ -275,7 +276,7 @@ class LarkMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(lb_msg_list) -class LarkEventConverter(adapter.EventConverter): +class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target( event: platform_events.MessageEvent, @@ -319,31 +320,24 @@ class LarkEventConverter(adapter.EventConverter): ) -class LarkAdapter(adapter.MessagePlatformAdapter): - bot: lark_oapi.ws.Client - api_client: lark_oapi.Client - - bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识 - lark_tenant_key: str # 飞书企业key +class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: lark_oapi.ws.Client = pydantic.Field(exclude=True) + api_client: lark_oapi.Client = pydantic.Field(exclude=True) message_converter: LarkMessageConverter = LarkMessageConverter() event_converter: LarkEventConverter = LarkEventConverter() listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] - config: dict - quart_app: quart.Quart + quart_app: quart.Quart = pydantic.Field(exclude=True) - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - self.quart_app = quart.Quart(__name__) - self.listeners = {} + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + quart_app = quart.Quart(__name__) - @self.quart_app.route('/lark/callback', methods=['POST']) + @quart_app.route('/lark/callback', methods=['POST']) async def lark_callback(): try: data = await quart.request.json @@ -396,10 +390,20 @@ class LarkAdapter(adapter.MessagePlatformAdapter): lark_oapi.EventDispatcherHandler.builder('', '').register_p2_im_message_receive_v1(sync_on_message).build() ) - self.bot_account_id = config['bot_name'] + bot_account_id = config['bot_name'] - self.bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler) - self.api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build() + bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler) + api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build() + + super().__init__( + config=config, + logger=logger, + listeners={}, + quart_app=quart_app, + bot=bot, + api_client=api_client, + bot_account_id=bot_account_id, + ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): pass @@ -448,14 +452,18 @@ class LarkAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/legacy/gewechat.py b/pkg/platform/sources/legacy/gewechat.py index 7e7b7715..cd5dcf22 100644 --- a/pkg/platform/sources/legacy/gewechat.py +++ b/pkg/platform/sources/legacy/gewechat.py @@ -11,11 +11,11 @@ import threading import quart import aiohttp -from ... import adapter +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ....core import app -from ...types import message as platform_message -from ...types import events as platform_events -from ...types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ....utils import image import xml.etree.ElementTree as ET from typing import Optional, Tuple @@ -23,7 +23,7 @@ from functools import partial from ...logger import EventLogger -class GewechatMessageConverter(adapter.MessageConverter): +class GewechatMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict): self.config = config @@ -398,7 +398,7 @@ class GewechatMessageConverter(adapter.MessageConverter): return from_user_name.endswith('@chatroom') -class GewechatEventConverter(adapter.EventConverter): +class GewechatEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict): self.config = config self.message_converter = GewechatMessageConverter(config) @@ -458,7 +458,7 @@ class GewechatEventConverter(adapter.EventConverter): ) -class GeWeChatAdapter(adapter.MessagePlatformAdapter): +class GeWeChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): name: str = 'gewechat' # 定义适配器名称 bot: gewechat_client.GewechatClient @@ -475,7 +475,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application, logger: EventLogger): @@ -625,14 +625,18 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): pass diff --git a/pkg/platform/sources/legacy/nakuru.py b/pkg/platform/sources/legacy/nakuru.py index 5afb6356..52609792 100644 --- a/pkg/platform/sources/legacy/nakuru.py +++ b/pkg/platform/sources/legacy/nakuru.py @@ -9,15 +9,15 @@ import traceback import nakuru import nakuru.entities.components as nkc -from ... import adapter as adapter_model from ....pipeline.longtext.strategies import forward -from ...types import message as platform_message -from ...types import entities as platform_entities -from ...types import events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ...logger import EventLogger -class NakuruProjectMessageConverter(adapter_model.MessageConverter): +class NakuruProjectMessageConverter(abstract_platform_adapter.AbstractMessageConverter): """消息转换器""" @staticmethod @@ -109,7 +109,7 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter): return chain -class NakuruProjectEventConverter(adapter_model.EventConverter): +class NakuruProjectEventConverter(abstract_platform_adapter.AbstractEventConverter): """事件转换器""" @staticmethod @@ -164,7 +164,7 @@ class NakuruProjectEventConverter(adapter_model.EventConverter): raise Exception('未支持转换的事件类型: ' + str(event)) -class NakuruAdapter(adapter_model.MessagePlatformAdapter): +class NakuruAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """nakuru-project适配器""" bot: nakuru.CQHTTP @@ -256,7 +256,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): try: source_cls = NakuruProjectEventConverter.yiri2target(event_type) @@ -283,7 +285,9 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): nakuru_event_name = self.event_converter.yiri2target(event_type).__name__ diff --git a/pkg/platform/sources/legacy/qqbotpy.py b/pkg/platform/sources/legacy/qqbotpy.py index 7e8fb125..90e4c2d7 100644 --- a/pkg/platform/sources/legacy/qqbotpy.py +++ b/pkg/platform/sources/legacy/qqbotpy.py @@ -10,13 +10,13 @@ import botpy import botpy.message as botpy_message import botpy.types.message as botpy_message_type -from ... import adapter as adapter_model +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from ....pipeline.longtext.strategies import forward from ....core import app from ....config import manager as cfg_mgr -from ...types import entities as platform_entities -from ...types import events as platform_events -from ...types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ...logger import EventLogger @@ -133,7 +133,7 @@ class OpenIDMapping(typing.Generic[K, V]): return value -class OfficialMessageConverter(adapter_model.MessageConverter): +class OfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): """QQ 官方消息转换器""" @staticmethod @@ -237,7 +237,7 @@ class OfficialMessageConverter(adapter_model.MessageConverter): return chain -class OfficialEventConverter(adapter_model.EventConverter): +class OfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): """事件转换器""" def __init__(self): @@ -333,7 +333,7 @@ class OfficialEventConverter(adapter_model.EventConverter): ) -class OfficialAdapter(adapter_model.MessagePlatformAdapter): +class OfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """QQ 官方消息适配器""" bot: botpy.Client = None @@ -484,7 +484,9 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): try: @@ -507,7 +509,9 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter_model.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): delattr(self.bot, event_handler_mapping[event_type]) diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 925b0ee4..74321b92 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -4,18 +4,18 @@ import asyncio import traceback import datetime -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.official_account_api.oaevent import OAEvent from libs.official_account_api.api import OAClient from libs.official_account_api.api import OAClientForLongerResponse -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ...command.errors import ParamNotEnoughError from ..logger import EventLogger -class OAMessageConverter(adapter.MessageConverter): +class OAMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): for msg in message_chain: @@ -33,7 +33,7 @@ class OAMessageConverter(adapter.MessageConverter): return chain -class OAEventConverter(adapter.EventConverter): +class OAEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def target2yiri(event: OAEvent): if event.type == 'text': @@ -55,7 +55,7 @@ class OAEventConverter(adapter.EventConverter): return None -class OfficialAccountAdapter(adapter.MessagePlatformAdapter): +class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: OAClient | OAClientForLongerResponse bot_account_id: str message_converter: OAMessageConverter = OAMessageConverter() @@ -116,7 +116,9 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: OAEvent): self.bot_account_id = event.receiver_id @@ -147,6 +149,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index cd7beb31..1160fd0e 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -5,10 +5,10 @@ import traceback import datetime -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...command.errors import ParamNotEnoughError from libs.qq_official_api.api import QQOfficialClient from libs.qq_official_api.qqofficialevent import QQOfficialEvent @@ -16,7 +16,7 @@ from ...utils import image from ..logger import EventLogger -class QQOfficialMessageConverter(adapter.MessageConverter): +class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] @@ -45,7 +45,7 @@ class QQOfficialMessageConverter(adapter.MessageConverter): return chain -class QQOfficialEventConverter(adapter.EventConverter): +class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent: return event.source_platform_object @@ -131,7 +131,7 @@ class QQOfficialEventConverter(adapter.EventConverter): ) -class QQOfficialAdapter(adapter.MessagePlatformAdapter): +class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: QQOfficialClient config: dict bot_account_id: str @@ -212,7 +212,9 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: QQOfficialEvent): self.bot_account_id = 'justbot' @@ -245,6 +247,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter): def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index ff14ce1c..c2997828 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -6,17 +6,17 @@ import traceback import datetime from libs.slack_api.api import SlackClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.slack_api.slackevent import SlackEvent -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...command.errors import ParamNotEnoughError from ...utils import image from ..logger import EventLogger -class SlackMessageConverter(adapter.MessageConverter): +class SlackMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain): content_list = [] @@ -43,7 +43,7 @@ class SlackMessageConverter(adapter.MessageConverter): return chain -class SlackEventConverter(adapter.EventConverter): +class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> SlackEvent: return event.source_platform_object @@ -83,7 +83,7 @@ class SlackEventConverter(adapter.EventConverter): ) -class SlackAdapter(adapter.MessagePlatformAdapter): +class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: SlackClient bot_account_id: str message_converter: SlackMessageConverter = SlackMessageConverter() @@ -132,7 +132,9 @@ class SlackAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: SlackEvent): self.bot_account_id = 'SlackBot' @@ -163,6 +165,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/telegram.py b/pkg/platform/sources/telegram.py index 52d79853..69e781ef 100644 --- a/pkg/platform/sources/telegram.py +++ b/pkg/platform/sources/telegram.py @@ -9,15 +9,16 @@ import typing import traceback import base64 import aiohttp +import pydantic -from .. import adapter -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class TelegramMessageConverter(adapter.MessageConverter): +class TelegramMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: telegram.Bot) -> list[dict]: components = [] @@ -86,7 +87,7 @@ class TelegramMessageConverter(adapter.MessageConverter): return platform_message.MessageChain(message_components) -class TelegramEventConverter(adapter.EventConverter): +class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.MessageEvent, bot: telegram.Bot): return event.source_platform_object @@ -128,26 +129,19 @@ class TelegramEventConverter(adapter.EventConverter): ) -class TelegramAdapter(adapter.MessagePlatformAdapter): - bot: telegram.Bot - application: telegram.ext.Application - - bot_account_id: str +class TelegramAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: telegram.Bot = pydantic.Field(exclude=True) + application: telegram.ext.Application = pydantic.Field(exclude=True) message_converter: TelegramMessageConverter = TelegramMessageConverter() event_converter: TelegramEventConverter = TelegramEventConverter() - config: dict - listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message.from_user.is_bot: return @@ -158,10 +152,16 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): except Exception: await self.logger.error(f'Error in telegram callback: {traceback.format_exc()}') - self.application = ApplicationBuilder().token(self.config['token']).build() - self.bot = self.application.bot - self.application.add_handler( - MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback) + application = ApplicationBuilder().token(config['token']).build() + bot = application.bot + application.add_handler(MessageHandler(filters.TEXT | (filters.COMMAND) | filters.PHOTO, telegram_callback)) + super().__init__( + config=config, + logger=logger, + bot=bot, + application=application, + bot_account_id='', + listeners={}, ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): @@ -201,14 +201,18 @@ class TelegramAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners.pop(event_type) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 0a35c1ac..0b5fc0ff 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -3,17 +3,19 @@ import logging import typing from datetime import datetime -from pydantic import BaseModel +import pydantic -from .. import adapter as msadapter -from ..types import events as platform_events, message as platform_message, entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities from ...core import app -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger logger = logging.getLogger(__name__) -class WebChatMessage(BaseModel): +class WebChatMessage(pydantic.BaseModel): id: int role: str content: str @@ -38,28 +40,35 @@ class WebChatSession: return self.message_lists[pipeline_uuid] -class WebChatAdapter(msadapter.MessagePlatformAdapter): +class WebChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): """WebChat调试适配器,用于流水线调试""" - webchat_person_session: WebChatSession - webchat_group_session: WebChatSession + webchat_person_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession) + webchat_group_session: WebChatSession = pydantic.Field(exclude=True, default_factory=WebChatSession) - ap: app.Application # set by bot manager + ap: app.Application = pydantic.Field(exclude=True) # set by bot manager - listeners: typing.Dict[ + listeners: dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None], - ] = {} + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], + ] = pydantic.Field(default_factory=dict, exclude=True) - def __init__(self, config: dict, logger: EventLogger): - self.logger = logger - self.config = config + debug_messages: dict[str, list[dict]] = pydantic.Field(default_factory=dict, exclude=True) + + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, ap: app.Application): + super().__init__( + config=config, + logger=logger, + ap=ap, + ) self.webchat_person_session = WebChatSession(id='webchatperson') self.webchat_group_session = WebChatSession(id='webchatgroup') self.bot_account_id = 'webchatbot' + self.debug_messages = {} + async def send_message( self, target_type: str, @@ -112,7 +121,9 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + func: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None] + ], ): """注册事件监听器""" self.listeners[event_type] = func @@ -120,11 +131,16 @@ class WebChatAdapter(msadapter.MessagePlatformAdapter): def unregister_listener( self, event_type: typing.Type[platform_events.Event], - func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]], + func: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], typing.Awaitable[None] + ], ): """取消注册事件监听器""" del self.listeners[event_type] + async def is_muted(self, group_id: int) -> bool: + return False + async def run_async(self): """运行适配器""" await self.logger.info('WebChat调试适配器已启动') diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index 0188d788..53d7a952 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -17,19 +17,19 @@ import threading import quart -from .. import adapter from ...core import app -from ..types import message as platform_message -from ..types import events as platform_events -from ..types import entities as platform_entities from ..logger import EventLogger import xml.etree.ElementTree as ET from typing import Optional, Tuple from functools import partial import logging +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter -class WeChatPadMessageConverter(adapter.MessageConverter): +class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict): self.config = config self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) @@ -281,7 +281,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): """处理文件消息 (data_type=6)""" file_data = xml_data.find('.//appmsg') - if file_data.findtext('.//type', "") == "74": + if file_data.findtext('.//type', '') == '74': return None else: @@ -304,16 +304,19 @@ class WeChatPadMessageConverter(adapter.MessageConverter): file_data = self.bot.cdn_download(aeskey=aeskey, file_type=5, file_url=cdnthumburl) - file_base64 = file_data["Data"]['FileData'] + file_base64 = file_data['Data']['FileData'] # print(file_data) - file_size = file_data["Data"]['TotalSize'] + file_size = file_data['Data']['TotalSize'] # print(file_base64) - return platform_message.MessageChain([ - platform_message.WeChatFile(file_id=file_id, file_name=file_name, file_size=file_size, - file_base64=file_base64), - platform_message.WeChatForwardFile(xml_data=xml_data_str) - ]) + return platform_message.MessageChain( + [ + platform_message.WeChatFile( + file_id=file_id, file_name=file_name, file_size=file_size, file_base64=file_base64 + ), + platform_message.WeChatForwardFile(xml_data=xml_data_str), + ] + ) async def _handler_compound_link(self, message: dict, xml_data: ET.Element) -> platform_message.MessageChain: """处理链接消息(如公众号文章、外部网页)""" @@ -416,7 +419,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter): return from_user_name.endswith('@chatroom') -class WeChatPadEventConverter(adapter.EventConverter): +class WeChatPadEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict): self.config = config self.message_converter = WeChatPadMessageConverter(config) @@ -476,7 +479,7 @@ class WeChatPadEventConverter(adapter.EventConverter): ) -class WeChatPadAdapter(adapter.MessagePlatformAdapter): +class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): name: str = 'WeChatPad' # 定义适配器名称 bot: WeChatPadClient @@ -495,7 +498,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): listeners: typing.Dict[ typing.Type[platform_events.Event], - typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None], ] = {} def __init__(self, config: dict, ap: app.Application, logger: EventLogger): @@ -596,14 +599,18 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): self.listeners[event_type] = callback def unregister_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): pass diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 7bb0a757..88b89e03 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -6,17 +6,17 @@ import traceback import datetime from libs.wecom_api.api import WecomClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.wecom_api.wecomevent import WecomEvent -from .. import adapter -from ..types import entities as platform_entities from ...command.errors import ParamNotEnoughError from ...utils import image from ..logger import EventLogger +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities -class WecomMessageConverter(adapter.MessageConverter): +class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomClient): content_list = [] @@ -70,7 +70,7 @@ class WecomMessageConverter(adapter.MessageConverter): return chain -class WecomEventConverter: +class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomClient) -> WecomEvent: # only for extracting user information @@ -126,7 +126,7 @@ class WecomEventConverter: return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, time=event.timestamp) -class WecomAdapter(adapter.MessagePlatformAdapter): +class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): bot: WecomClient bot_account_id: str message_converter: WecomMessageConverter = WecomMessageConverter() @@ -192,7 +192,9 @@ class WecomAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: WecomEvent): self.bot_account_id = event.receiver_id @@ -224,6 +226,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter): async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index fcd5378e..0958db68 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -4,18 +4,19 @@ import asyncio import traceback import datetime +import pydantic from libs.wecom_customer_service_api.api import WecomCSClient -from pkg.platform.adapter import MessagePlatformAdapter -from pkg.platform.types import events as platform_events, message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent -from .. import adapter -from ..types import entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.platform.events as platform_events from ...command.errors import ParamNotEnoughError -from ..logger import EventLogger +import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger -class WecomMessageConverter(adapter.MessageConverter): +class WecomMessageConverter(abstract_platform_adapter.AbstractMessageConverter): @staticmethod async def yiri2target(message_chain: platform_message.MessageChain, bot: WecomCSClient): content_list = [] @@ -68,7 +69,7 @@ class WecomMessageConverter(adapter.MessageConverter): return chain -class WecomEventConverter: +class WecomEventConverter(abstract_platform_adapter.AbstractEventConverter): @staticmethod async def yiri2target(event: platform_events.Event, bot_account_id: int, bot: WecomCSClient) -> WecomCSEvent: # only for extracting user information @@ -116,17 +117,12 @@ class WecomEventConverter: ) -class WecomCSAdapter(adapter.MessagePlatformAdapter): - bot: WecomCSClient - bot_account_id: str +class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): + bot: WecomCSClient = pydantic.Field(exclude=True) message_converter: WecomMessageConverter = WecomMessageConverter() event_converter: WecomEventConverter = WecomEventConverter() - config: dict - - def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): required_keys = [ 'corpid', 'secret', @@ -137,12 +133,20 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): if missing_keys: raise ParamNotEnoughError('企业微信客服缺少相关配置项,请查看文档或联系管理员') - self.bot = WecomCSClient( + bot = WecomCSClient( corpid=config['corpid'], secret=config['secret'], token=config['token'], EncodingAESKey=config['EncodingAESKey'], - logger=self.logger, + logger=logger, + ) + + super().__init__( + config=config, + logger=logger, + bot=bot, + bot_account_id='', + listeners={}, ) async def reply_message( @@ -169,7 +173,9 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): def register_listener( self, event_type: typing.Type[platform_events.Event], - callback: typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): async def on_message(event: WecomCSEvent): self.bot_account_id = event.receiver_id @@ -198,9 +204,14 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter): async def kill(self) -> bool: return False + async def is_muted(self, group_id: int) -> bool: + return False + async def unregister_listener( self, event_type: type, - callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None], + callback: typing.Callable[ + [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None + ], ): return super().unregister_listener(event_type, callback) diff --git a/pkg/platform/types/__init__.py b/pkg/platform/types/__init__.py deleted file mode 100644 index 998b0fb8..00000000 --- a/pkg/platform/types/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .entities import * -from .events import * -from .message import * diff --git a/pkg/platform/types/base.py b/pkg/platform/types/base.py deleted file mode 100644 index da58d4ed..00000000 --- a/pkg/platform/types/base.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Dict, List, Type - -import pydantic.v1.main as pdm -from pydantic.v1 import BaseModel - - -class PlatformMetaclass(pdm.ModelMetaclass): - """此类是平台中使用的 pydantic 模型的元类的基类。""" - - -def to_camel(name: str) -> str: - """将下划线命名风格转换为小驼峰命名。""" - if name[:2] == '__': # 不处理双下划线开头的特殊命名。 - return name - name_parts = name.split('_') - return ''.join(name_parts[:1] + [x.title() for x in name_parts[1:]]) - - -class PlatformBaseModel(BaseModel, metaclass=PlatformMetaclass): - """模型基类。 - - 启用了三项配置: - 1. 允许解析时传入额外的值,并将额外值保存在模型中。 - 2. 允许通过别名访问字段。 - 3. 自动生成小驼峰风格的别名。 - """ - - def __init__(self, *args, **kwargs): - """""" - super().__init__(*args, **kwargs) - - def __repr__(self) -> str: - return ( - self.__class__.__name__ + '(' + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if v)) + ')' - ) - - class Config: - extra = 'allow' - allow_population_by_field_name = True - alias_generator = to_camel - - -class PlatformIndexedMetaclass(PlatformMetaclass): - """可以通过子类名获取子类的类的元类。""" - - __indexedbases__: List[Type['PlatformIndexedModel']] = [] - __indexedmodel__ = None - - def __new__(cls, name, bases, attrs, **kwargs): - new_cls = super().__new__(cls, name, bases, attrs, **kwargs) - # 第一类:PlatformIndexedModel - if name == 'PlatformIndexedModel': - cls.__indexedmodel__ = new_cls - new_cls.__indexes__ = {} - return new_cls - # 第二类:PlatformIndexedModel 的直接子类,这些是可以通过子类名获取子类的类。 - if cls.__indexedmodel__ in bases: - cls.__indexedbases__.append(new_cls) - new_cls.__indexes__ = {} - return new_cls - # 第三类:PlatformIndexedModel 的直接子类的子类,这些添加到直接子类的索引中。 - for base in cls.__indexedbases__: - if issubclass(new_cls, base): - base.__indexes__[name] = new_cls - return new_cls - - def __getitem__(cls, name): - return cls.get_subtype(name) - - -class PlatformIndexedModel(PlatformBaseModel, metaclass=PlatformIndexedMetaclass): - """可以通过子类名获取子类的类。""" - - __indexes__: Dict[str, Type['PlatformIndexedModel']] - - @classmethod - def get_subtype(cls, name: str) -> Type['PlatformIndexedModel']: - """根据类名称,获取相应的子类类型。 - - Args: - name: 类名称。 - - Returns: - Type['PlatformIndexedModel']: 子类类型。 - """ - try: - type_ = cls.__indexes__.get(name) - if not (type_ and issubclass(type_, cls)): - raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') - return type_ - except AttributeError: - raise ValueError(f'`{name}` 不是 `{cls.__name__}` 的子类!') from None - - @classmethod - def parse_subtype(cls, obj: dict) -> 'PlatformIndexedModel': - """通过字典,构造对应的模型对象。 - - Args: - obj: 一个字典,包含了模型对象的属性。 - - Returns: - PlatformIndexedModel: 构造的对象。 - """ - if cls in PlatformIndexedModel.__subclasses__(): - ModelType = cls.get_subtype(obj['type']) - return ModelType.parse_obj(obj) - return super().parse_obj(obj) diff --git a/pkg/platform/types/entities.py b/pkg/platform/types/entities.py deleted file mode 100644 index d989ffce..00000000 --- a/pkg/platform/types/entities.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -""" -此模块提供实体和配置项模型。 -""" - -import abc -from datetime import datetime -from enum import Enum -import typing - -import pydantic.v1 as pydantic - - -class Entity(pydantic.BaseModel): - """实体,表示一个用户或群。""" - - id: int - """ID。""" - - @abc.abstractmethod - def get_name(self) -> str: - """名称。""" - - -class Friend(Entity): - """私聊对象。""" - - id: typing.Union[int, str] - """ID。""" - nickname: typing.Optional[str] - """昵称。""" - remark: typing.Optional[str] - """备注。""" - - def get_name(self) -> str: - return self.nickname or self.remark or '' - - -class Permission(str, Enum): - """群成员身份权限。""" - - Member = 'MEMBER' - """成员。""" - Administrator = 'ADMINISTRATOR' - """管理员。""" - Owner = 'OWNER' - """群主。""" - - def __repr__(self) -> str: - return repr(self.value) - - -class Group(Entity): - """群。""" - - id: typing.Union[int, str] - """群号。""" - name: str - """群名称。""" - permission: Permission - """Bot 在群中的权限。""" - - def get_name(self) -> str: - return self.name - - -class GroupMember(Entity): - """群成员。""" - - id: typing.Union[int, str] - """群员 ID。""" - member_name: str - """群员名称。""" - permission: Permission - """在群中的权限。""" - group: Group - """群。""" - special_title: str = '' - """群头衔。""" - join_timestamp: datetime = datetime.utcfromtimestamp(0) - """加入群的时间。""" - last_speak_timestamp: datetime = datetime.utcfromtimestamp(0) - """最后一次发言的时间。""" - mute_time_remaining: int = 0 - """禁言剩余时间。""" - - def get_name(self) -> str: - return self.member_name diff --git a/pkg/platform/types/events.py b/pkg/platform/types/events.py deleted file mode 100644 index 5ffccb9b..00000000 --- a/pkg/platform/types/events.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -""" -此模块提供事件模型。 -""" - -import typing - -import pydantic.v1 as pydantic - -from . import entities as platform_entities -from . import message as platform_message - - -class Event(pydantic.BaseModel): - """事件基类。 - - Args: - type: 事件名。 - """ - - type: str - """事件名。""" - - def __repr__(self): - return ( - self.__class__.__name__ - + '(' - + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if k != 'type' and v)) - + ')' - ) - - @classmethod - def parse_subtype(cls, obj: dict) -> 'Event': - try: - return typing.cast(Event, super().parse_subtype(obj)) - except ValueError: - return Event(type=obj['type']) - - @classmethod - def get_subtype(cls, name: str) -> typing.Type['Event']: - try: - return typing.cast(typing.Type[Event], super().get_subtype(name)) - except ValueError: - return Event - - -############################### -# Message Event -class MessageEvent(Event): - """消息事件。 - - Args: - type: 事件名。 - message_chain: 消息内容。 - """ - - type: str - """事件名。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - time: float | None = None - """消息发送时间戳。""" - - source_platform_object: typing.Optional[typing.Any] = None - """原消息平台对象。 - 供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息, - 那么可以将其存到这个字段以供之后取出使用。""" - - -class FriendMessage(MessageEvent): - """私聊消息。 - - Args: - type: 事件名。 - sender: 发送消息的好友。 - message_chain: 消息内容。 - """ - - type: str = 'FriendMessage' - """事件名。""" - sender: platform_entities.Friend - """发送消息的好友。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - -class GroupMessage(MessageEvent): - """群消息。 - - Args: - type: 事件名。 - sender: 发送消息的群成员。 - message_chain: 消息内容。 - """ - - type: str = 'GroupMessage' - """事件名。""" - sender: platform_entities.GroupMember - """发送消息的群成员。""" - message_chain: platform_message.MessageChain - """消息内容。""" - - @property - def group(self) -> platform_entities.Group: - return self.sender.group diff --git a/pkg/platform/types/message.py b/pkg/platform/types/message.py deleted file mode 100644 index 7dad4145..00000000 --- a/pkg/platform/types/message.py +++ /dev/null @@ -1,975 +0,0 @@ -import itertools -import logging -import typing -from datetime import datetime -from pathlib import Path -import base64 - -import aiofiles -import httpx -import pydantic.v1 as pydantic - -from . import entities as platform_entities -from .base import PlatformBaseModel, PlatformIndexedMetaclass, PlatformIndexedModel - -logger = logging.getLogger(__name__) - - -class MessageComponentMetaclass(PlatformIndexedMetaclass): - """消息组件元类。""" - - __message_component__ = None - - def __new__(cls, name, bases, attrs, **kwargs): - new_cls = super().__new__(cls, name, bases, attrs, **kwargs) - if name == 'MessageComponent': - cls.__message_component__ = new_cls - - if not cls.__message_component__: - return new_cls - - for base in bases: - if issubclass(base, cls.__message_component__): - # 获取字段名 - if hasattr(new_cls, '__fields__'): - # 忽略 type 字段 - new_cls.__parameter_names__ = list(new_cls.__fields__)[1:] - else: - new_cls.__parameter_names__ = [] - break - - return new_cls - - -class MessageComponent(PlatformIndexedModel, metaclass=MessageComponentMetaclass): - """消息组件。""" - - type: str - """消息组件类型。""" - - def __str__(self): - return '' - - def __repr__(self): - return ( - self.__class__.__name__ - + '(' - + ', '.join((f'{k}={repr(v)}' for k, v in self.__dict__.items() if k != 'type' and v)) - + ')' - ) - - def __init__(self, *args, **kwargs): - # 解析参数列表,将位置参数转化为具名参数 - parameter_names = self.__parameter_names__ - if len(args) > len(parameter_names): - raise TypeError(f'`{self.type}`需要{len(parameter_names)}个参数,但传入了{len(args)}个。') - for name, value in zip(parameter_names, args): - if name in kwargs: - raise TypeError(f'在 `{self.type}` 中,具名参数 `{name}` 与位置参数重复。') - kwargs[name] = value - - super().__init__(**kwargs) - - -TMessageComponent = typing.TypeVar('TMessageComponent', bound=MessageComponent) - - -class MessageChain(PlatformBaseModel): - """消息链。 - - 一个构造消息链的例子: - ```py - message_chain = MessageChain([ - AtAll(), - Plain("Hello World!"), - ]) - ``` - - `Plain` 可以省略。 - ```py - message_chain = MessageChain([ - AtAll(), - "Hello World!", - ]) - ``` - - 在调用 API 时,参数中需要 MessageChain 的,也可以使用 `List[MessageComponent]` 代替。 - 例如,以下两种写法是等价的: - ```py - await bot.send_friend_message(12345678, [ - Plain("Hello World!") - ]) - ``` - ```py - await bot.send_friend_message(12345678, MessageChain([ - Plain("Hello World!") - ])) - ``` - - 可以使用 `in` 运算检查消息链中: - 1. 是否有某个消息组件。 - 2. 是否有某个类型的消息组件。 - - ```py - if AtAll in message_chain: - print('AtAll') - - if At(bot.qq) in message_chain: - print('At Me') - ``` - - """ - - __root__: typing.List[MessageComponent] - - @staticmethod - def _parse_message_chain(msg_chain: typing.Iterable): - result = [] - for msg in msg_chain: - if isinstance(msg, dict): - result.append(MessageComponent.parse_subtype(msg)) - elif isinstance(msg, MessageComponent): - result.append(msg) - elif isinstance(msg, str): - result.append(Plain(msg)) - else: - raise TypeError(f'消息链中元素需为 dict 或 str 或 MessageComponent,当前类型:{type(msg)}') - return result - - @pydantic.validator('__root__', always=True, pre=True) - def _parse_component(cls, msg_chain): - if isinstance(msg_chain, (str, MessageComponent)): - msg_chain = [msg_chain] - if not msg_chain: - msg_chain = [] - return cls._parse_message_chain(msg_chain) - - @classmethod - def parse_obj(cls, msg_chain: typing.Iterable): - """通过列表形式的消息链,构造对应的 `MessageChain` 对象。 - - Args: - msg_chain: 列表形式的消息链。 - """ - result = cls._parse_message_chain(msg_chain) - return cls(__root__=result) - - def __init__(self, __root__: typing.Iterable[MessageComponent] = None): - super().__init__(__root__=__root__) - - def __str__(self): - return ''.join(str(component) for component in self.__root__) - - def __repr__(self): - return f'{self.__class__.__name__}({self.__root__!r})' - - def __iter__(self): - yield from self.__root__ - - def get_first(self, t: typing.Type[TMessageComponent]) -> typing.Optional[TMessageComponent]: - """获取消息链中第一个符合类型的消息组件。""" - for component in self: - if isinstance(component, t): - return component - return None - - @typing.overload - def __getitem__(self, index: int) -> MessageComponent: ... - - @typing.overload - def __getitem__(self, index: slice) -> typing.List[MessageComponent]: ... - - @typing.overload - def __getitem__(self, index: typing.Type[TMessageComponent]) -> typing.List[TMessageComponent]: ... - - @typing.overload - def __getitem__( - self, index: typing.Tuple[typing.Type[TMessageComponent], int] - ) -> typing.List[TMessageComponent]: ... - - def __getitem__( - self, - index: typing.Union[ - int, - slice, - typing.Type[TMessageComponent], - typing.Tuple[typing.Type[TMessageComponent], int], - ], - ) -> typing.Union[MessageComponent, typing.List[MessageComponent], typing.List[TMessageComponent]]: - return self.get(index) - - def __setitem__( - self, - key: typing.Union[int, slice], - value: typing.Union[MessageComponent, str, typing.Iterable[typing.Union[MessageComponent, str]]], - ): - if isinstance(value, str): - value = Plain(value) - if isinstance(value, typing.Iterable): - value = (Plain(c) if isinstance(c, str) else c for c in value) - self.__root__[key] = value # type: ignore - - def __delitem__(self, key: typing.Union[int, slice]): - del self.__root__[key] - - def __reversed__(self) -> typing.Iterable[MessageComponent]: - return reversed(self.__root__) - - def has( - self, - sub: typing.Union[MessageComponent, typing.Type[MessageComponent], 'MessageChain', str], - ) -> bool: - """判断消息链中: - 1. 是否有某个消息组件。 - 2. 是否有某个类型的消息组件。 - - Args: - sub (`Union[MessageComponent, Type[MessageComponent], 'MessageChain', str]`): - 若为 `MessageComponent`,则判断该组件是否在消息链中。 - 若为 `Type[MessageComponent]`,则判断该组件类型是否在消息链中。 - - Returns: - bool: 是否找到。 - """ - if isinstance(sub, type): # 检测消息链中是否有某种类型的对象 - for i in self: - if type(i) is sub: - return True - return False - if isinstance(sub, MessageComponent): # 检查消息链中是否有某个组件 - for i in self: - if i == sub: - return True - return False - raise TypeError(f'类型不匹配,当前类型:{type(sub)}') - - def __contains__(self, sub) -> bool: - return self.has(sub) - - def __ge__(self, other): - return other in self - - def __len__(self) -> int: - return len(self.__root__) - - def __add__(self, other: typing.Union['MessageChain', MessageComponent, str]) -> 'MessageChain': - if isinstance(other, MessageChain): - return self.__class__(self.__root__ + other.__root__) - if isinstance(other, str): - return self.__class__(self.__root__ + [Plain(other)]) - if isinstance(other, MessageComponent): - return self.__class__(self.__root__ + [other]) - return NotImplemented - - def __radd__(self, other: typing.Union[MessageComponent, str]) -> 'MessageChain': - if isinstance(other, MessageComponent): - return self.__class__([other] + self.__root__) - if isinstance(other, str): - return self.__class__([typing.cast(MessageComponent, Plain(other))] + self.__root__) - return NotImplemented - - def __mul__(self, other: int): - if isinstance(other, int): - return self.__class__(self.__root__ * other) - return NotImplemented - - def __rmul__(self, other: int): - return self.__mul__(other) - - def __iadd__(self, other: typing.Iterable[typing.Union[MessageComponent, str]]): - self.extend(other) - - def __imul__(self, other: int): - if isinstance(other, int): - self.__root__ *= other - return NotImplemented - - def index( - self, - x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - i: int = 0, - j: int = -1, - ) -> int: - """返回 x 在消息链中首次出现项的索引号(索引号在 i 或其后且在 j 之前)。 - - Args: - x (`Union[MessageComponent, Type[MessageComponent]]`): - 要查找的消息元素或消息元素类型。 - i: 从哪个位置开始查找。 - j: 查找到哪个位置结束。 - - Returns: - int: 如果找到,则返回索引号。 - - Raises: - ValueError: 没有找到。 - TypeError: 类型不匹配。 - """ - if isinstance(x, type): - l = len(self) - if i < 0: - i += l - if i < 0: - i = 0 - if j < 0: - j += l - if j > l: - j = l - for index in range(i, j): - if type(self[index]) is x: - return index - raise ValueError('消息链中不存在该类型的组件。') - if isinstance(x, MessageComponent): - return self.__root__.index(x, i, j) - raise TypeError(f'类型不匹配,当前类型:{type(x)}') - - def count(self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]]) -> int: - """返回消息链中 x 出现的次数。 - - Args: - x (`Union[MessageComponent, Type[MessageComponent]]`): - 要查找的消息元素或消息元素类型。 - - Returns: - int: 次数。 - """ - if isinstance(x, type): - return sum(1 for i in self if type(i) is x) - if isinstance(x, MessageComponent): - return self.__root__.count(x) - raise TypeError(f'类型不匹配,当前类型:{type(x)}') - - def extend(self, x: typing.Iterable[typing.Union[MessageComponent, str]]): - """将另一个消息链中的元素添加到消息链末尾。 - - Args: - x: 另一个消息链,也可为消息元素或字符串元素的序列。 - """ - self.__root__.extend(Plain(c) if isinstance(c, str) else c for c in x) - - def append(self, x: typing.Union[MessageComponent, str]): - """将一个消息元素或字符串元素添加到消息链末尾。 - - Args: - x: 消息元素或字符串元素。 - """ - self.__root__.append(Plain(x) if isinstance(x, str) else x) - - def insert(self, i: int, x: typing.Union[MessageComponent, str]): - """将一个消息元素或字符串添加到消息链中指定位置。 - - Args: - i: 插入位置。 - x: 消息元素或字符串元素。 - """ - self.__root__.insert(i, Plain(x) if isinstance(x, str) else x) - - def pop(self, i: int = -1) -> MessageComponent: - """从消息链中移除并返回指定位置的元素。 - - Args: - i: 移除位置。默认为末尾。 - - Returns: - MessageComponent: 移除的元素。 - """ - return self.__root__.pop(i) - - def remove(self, x: typing.Union[MessageComponent, typing.Type[MessageComponent]]): - """从消息链中移除指定元素或指定类型的一个元素。 - - Args: - x: 指定的元素或元素类型。 - """ - if isinstance(x, type): - self.pop(self.index(x)) - if isinstance(x, MessageComponent): - self.__root__.remove(x) - - def exclude( - self, - x: typing.Union[MessageComponent, typing.Type[MessageComponent]], - count: int = -1, - ) -> 'MessageChain': - """返回移除指定元素或指定类型的元素后剩余的消息链。 - - Args: - x: 指定的元素或元素类型。 - count: 至多移除的数量。默认为全部移除。 - - Returns: - MessageChain: 剩余的消息链。 - """ - - def _exclude(): - nonlocal count - x_is_type = isinstance(x, type) - for c in self: - if count > 0 and ((x_is_type and type(c) is x) or c == x): - count -= 1 - continue - yield c - - return self.__class__(_exclude()) - - def reverse(self): - """将消息链原地翻转。""" - self.__root__.reverse() - - @classmethod - def join(cls, *args: typing.Iterable[typing.Union[str, MessageComponent]]): - return cls(Plain(c) if isinstance(c, str) else c for c in itertools.chain(*args)) - - @property - def source(self) -> typing.Optional['Source']: - """获取消息链中的 `Source` 对象。""" - return self.get_first(Source) - - @property - def message_id(self) -> int: - """获取消息链的 message_id,若无法获取,返回 -1。""" - source = self.source - return source.id if source else -1 - - -TMessage = typing.Union[ - MessageChain, - typing.Iterable[typing.Union[MessageComponent, str]], - MessageComponent, - str, -] -"""可以转化为 MessageChain 的类型。""" - - -class Source(MessageComponent): - """源。包含消息的基本信息。""" - - type: str = 'Source' - """消息组件类型。""" - id: typing.Union[int, str] - """消息的识别号,用于引用回复(Source 类型永远为 MessageChain 的第一个元素)。""" - time: datetime - """消息时间。""" - - -class Plain(MessageComponent): - """纯文本。""" - - type: str = 'Plain' - """消息组件类型。""" - text: str - """文字消息。""" - - def __str__(self): - return self.text - - def __repr__(self): - return f'Plain({self.text!r})' - - -class Quote(MessageComponent): - """引用。""" - - type: str = 'Quote' - """消息组件类型。""" - id: typing.Optional[int] = None - """被引用回复的原消息的 message_id。""" - group_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息所接收的群号,当为好友消息时为0。""" - sender_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息的发送者的ID。""" - target_id: typing.Optional[typing.Union[int, str]] = None - """被引用回复的原消息的接收者者的ID或群ID。""" - origin: MessageChain - """被引用回复的原消息的消息链对象。""" - - @pydantic.validator('origin', always=True, pre=True) - def origin_formater(cls, v): - return MessageChain.parse_obj(v) - - -class At(MessageComponent): - """At某人。""" - - type: str = 'At' - """消息组件类型。""" - target: typing.Union[int, str] - """群员 ID。""" - display: typing.Optional[str] = None - """At时显示的文字,发送消息时无效,自动使用群名片。""" - - def __eq__(self, other): - return isinstance(other, At) and self.target == other.target - - def __str__(self): - return f'@{self.display or self.target}' - - -class AtAll(MessageComponent): - """At全体。""" - - type: str = 'AtAll' - """消息组件类型。""" - - def __str__(self): - return '@全体成员' - - -class Image(MessageComponent): - """图片。""" - - type: str = 'Image' - """消息组件类型。""" - image_id: typing.Optional[str] = None - """图片的 image_id,不为空时将忽略 url 属性。""" - url: typing.Optional[pydantic.HttpUrl] = None - """图片的 URL,发送时可作网络图片的链接;接收时为图片的链接,可用于图片下载。""" - path: typing.Union[str, Path, None] = None - """图片的路径,发送本地图片。""" - base64: typing.Optional[str] = None - """图片的 Base64 编码。""" - - def __eq__(self, other): - return isinstance(other, Image) and self.type == other.type and self.uuid == other.uuid - - def __str__(self): - return '[图片]' - - @pydantic.validator('path') - def validate_path(cls, path: typing.Union[str, Path, None]): - """修复 path 参数的行为,使之相对于 LangBot 的启动路径。""" - if path: - try: - return str(Path(path).resolve(strict=True)) - except FileNotFoundError: - raise ValueError(f'无效路径:{path}') - else: - return path - - @property - def uuid(self): - image_id = self.image_id - if image_id[0] == '{': # 群图片 - image_id = image_id[1:37] - elif image_id[0] == '/': # 好友图片 - image_id = image_id[1:] - return image_id - - async def get_bytes(self) -> typing.Tuple[bytes, str]: - """获取图片的 bytes 和 mime type""" - if self.url: - async with httpx.AsyncClient() as client: - response = await client.get(self.url) - response.raise_for_status() - return response.content, response.headers.get('Content-Type') - elif self.base64: - mime_type = 'image/jpeg' - - split_index = self.base64.find(';base64,') - if split_index == -1: - raise ValueError('Invalid base64 string') - - mime_type = self.base64[5:split_index] - base64_data = self.base64[split_index + 8 :] - - return base64.b64decode(base64_data), mime_type - elif self.path: - async with aiofiles.open(self.path, 'rb') as f: - return await f.read(), 'image/jpeg' - else: - raise ValueError('Can not get bytes from image') - - @classmethod - async def from_local( - cls, - filename: typing.Union[str, Path, None] = None, - content: typing.Optional[bytes] = None, - ) -> 'Image': - """从本地文件路径加载图片,以 base64 的形式传递。 - - Args: - filename: 从本地文件路径加载图片,与 `content` 二选一。 - content: 从本地文件内容加载图片,与 `filename` 二选一。 - - Returns: - Image: 图片对象。 - """ - if content: - pass - elif filename: - path = Path(filename) - import aiofiles - - async with aiofiles.open(path, 'rb') as f: - content = await f.read() - else: - raise ValueError('请指定图片路径或图片内容!') - import base64 - - img = cls(base64=base64.b64encode(content).decode()) - return img - - @classmethod - def from_unsafe_path(cls, path: typing.Union[str, Path]) -> 'Image': - """从不安全的路径加载图片。 - - Args: - path: 从不安全的路径加载图片。 - - Returns: - Image: 图片对象。 - """ - return cls.construct(path=str(path)) - - -class Unknown(MessageComponent): - """未知。""" - - type: str = 'Unknown' - """消息组件类型。""" - text: str - """文本。""" - - def __str__(self): - return f'Unknown Message: {self.text}' - - -class Voice(MessageComponent): - """语音。""" - - type: str = 'Voice' - """消息组件类型。""" - voice_id: typing.Optional[str] = None - """语音的 voice_id,不为空时将忽略 url 属性。""" - url: typing.Optional[str] = None - """语音的 URL,发送时可作网络语音的链接;接收时为语音文件的链接,可用于语音下载。""" - path: typing.Optional[str] = None - """语音的路径,发送本地语音。""" - base64: typing.Optional[str] = None - """语音的 Base64 编码。""" - length: typing.Optional[int] = None - """语音的长度,单位为秒。""" - - @pydantic.validator('path') - def validate_path(cls, path: typing.Optional[str]): - """修复 path 参数的行为,使之相对于 LangBot 的启动路径。""" - if path: - try: - return str(Path(path).resolve(strict=True)) - except FileNotFoundError: - raise ValueError(f'无效路径:{path}') - else: - return path - - def __str__(self): - return '[语音]' - - async def download( - self, - filename: typing.Union[str, Path, None] = None, - directory: typing.Union[str, Path, None] = None, - ): - """下载语音到本地。 - - Args: - filename: 下载到本地的文件路径。与 `directory` 二选一。 - directory: 下载到本地的文件夹路径。与 `filename` 二选一。 - """ - if not self.url: - logger.warning(f'语音 `{self.voice_id}` 无 url 参数,下载失败。') - return - - import httpx - - async with httpx.AsyncClient() as client: - response = await client.get(self.url) - response.raise_for_status() - content = response.content - - if filename: - path = Path(filename) - path.parent.mkdir(parents=True, exist_ok=True) - elif directory: - path = Path(directory) - path.mkdir(parents=True, exist_ok=True) - path = path / f'{self.voice_id}.silk' - else: - raise ValueError('请指定文件路径或文件夹路径!') - - import aiofiles - - async with aiofiles.open(path, 'wb') as f: - await f.write(content) - - @classmethod - async def from_local( - cls, - filename: typing.Union[str, Path, None] = None, - content: typing.Optional[bytes] = None, - ) -> 'Voice': - """从本地文件路径加载语音,以 base64 的形式传递。 - - Args: - filename: 从本地文件路径加载语音,与 `content` 二选一。 - content: 从本地文件内容加载语音,与 `filename` 二选一。 - """ - if content: - pass - if filename: - path = Path(filename) - import aiofiles - - async with aiofiles.open(path, 'rb') as f: - content = await f.read() - else: - raise ValueError('请指定语音路径或语音内容!') - import base64 - - img = cls(base64=base64.b64encode(content).decode()) - return img - - -class ForwardMessageNode(pydantic.BaseModel): - """合并转发中的一条消息。""" - - sender_id: typing.Optional[typing.Union[int, str]] = None - """发送人ID。""" - sender_name: typing.Optional[str] = None - """显示名称。""" - message_chain: typing.Optional[MessageChain] = None - """消息内容。""" - message_id: typing.Optional[int] = None - """消息的 message_id。""" - time: typing.Optional[datetime] = None - """发送时间。""" - - @pydantic.validator('message_chain', check_fields=False) - def _validate_message_chain(cls, value: typing.Union[MessageChain, list]): - if isinstance(value, list): - return MessageChain.parse_obj(value) - return value - - @classmethod - def create( - cls, - sender: typing.Union[platform_entities.Friend, platform_entities.GroupMember], - message: MessageChain, - ) -> 'ForwardMessageNode': - """从消息链生成转发消息。 - - Args: - sender: 发送人。 - message: 消息内容。 - - Returns: - ForwardMessageNode: 生成的一条消息。 - """ - return ForwardMessageNode(sender_id=sender.id, sender_name=sender.get_name(), message_chain=message) - - -class ForwardMessageDiaplay(pydantic.BaseModel): - title: str = '群聊的聊天记录' - brief: str = '[聊天记录]' - source: str = '聊天记录' - preview: typing.List[str] = [] - summary: str = '查看x条转发消息' - - -class Forward(MessageComponent): - """合并转发。""" - - type: str = 'Forward' - """消息组件类型。""" - display: ForwardMessageDiaplay - """显示信息""" - node_list: typing.List[ForwardMessageNode] - """转发消息节点列表。""" - - def __init__(self, *args, **kwargs): - if len(args) == 1: - self.node_list = args[0] - super().__init__(**kwargs) - super().__init__(*args, **kwargs) - - def __str__(self): - return '[聊天记录]' - - -class File(MessageComponent): - """文件。""" - - type: str = 'File' - """消息组件类型。""" - id: str = '' - """文件识别 ID。""" - name: str - """文件名称。""" - size: int = 0 - """文件大小。""" - url: str - """文件路径""" - - def __str__(self): - return f'[文件]{self.name}' - -class Face(MessageComponent): - """系统表情 - 此处将超级表情骰子/划拳,一同归类于face - 当face_type为rps(划拳)时 face_id 对应的是手势 - 当face_type为dice(骰子)时 face_id 对应的是点数 - """ - type: str = 'Face' - """表情类型""" - face_type: str = 'face' - """表情id""" - face_id: int = 0 - """表情名""" - face_name: str = '' - - def __str__(self): - if self.face_type == 'face': - return f'[表情]{self.face_name}' - elif self.face_type == 'dice': - return f'[表情]{self.face_id}点的{self.face_name}' - elif self.face_type == 'rps': - return f'[表情]{self.face_name}({self.rps_data(self.face_id)})' - - - def rps_data(self,face_id): - rps_dict ={ - 1 : "布", - 2 : "剪刀", - 3 : "石头", - } - return rps_dict[face_id] - -# ================ 个人微信专用组件 ================ - - -class WeChatMiniPrograms(MessageComponent): - """小程序。个人微信专用组件。""" - - type: str = 'WeChatMiniPrograms' - """小程序id""" - mini_app_id: str - """小程序归属用户id""" - user_name: str - """小程序名称""" - display_name: typing.Optional[str] = '' - """打开地址""" - page_path: typing.Optional[str] = '' - """小程序标题""" - title: typing.Optional[str] = '' - """首页图片""" - image_url: typing.Optional[str] = '' - - -class WeChatForwardMiniPrograms(MessageComponent): - """转发小程序。个人微信专用组件。""" - - type: str = 'WeChatForwardMiniPrograms' - """xml数据""" - xml_data: str - """首页图片""" - image_url: typing.Optional[str] = None - - def __str__(self): - return self.xml_data - - -class WeChatEmoji(MessageComponent): - """emoji表情。个人微信专用组件。""" - - type: str = 'WeChatEmoji' - """emojimd5""" - emoji_md5: str - """emoji大小""" - emoji_size: int - - -class WeChatLink(MessageComponent): - """发送链接。个人微信专用组件。""" - - type: str = 'WeChatLink' - """标题""" - link_title: str = '' - """链接描述""" - link_desc: str = '' - """链接地址""" - link_url: str = '' - """链接略缩图""" - link_thumb_url: str = '' - - -class WeChatForwardLink(MessageComponent): - """转发链接。个人微信专用组件。""" - - type: str = 'WeChatForwardLink' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatForwardImage(MessageComponent): - """转发图片。个人微信专用组件。""" - - type: str = 'WeChatForwardImage' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatForwardFile(MessageComponent): - """转发文件。个人微信专用组件。""" - - type: str = 'WeChatForwardFile' - """xml数据""" - xml_data: str - - def __str__(self): - return self.xml_data - - -class WeChatAppMsg(MessageComponent): - """通用appmsg发送。个人微信专用组件。""" - - type: str = 'WeChatAppMsg' - """xml数据""" - app_msg: str - - def __str__(self): - return self.app_msg - - -class WeChatForwardQuote(MessageComponent): - """转发引用消息。个人微信专用组件。""" - - type: str = 'WeChatForwardQuote' - """xml数据""" - app_msg: str - - def __str__(self): - return self.app_msg - - -class WeChatFile(MessageComponent): - """文件。""" - - type: str = 'File' - """消息组件类型。""" - file_id: str = '' - """文件识别 ID。""" - file_name: str = '' - """文件名称。""" - file_size: int = 0 - """文件大小。""" - file_path: str = '' - """文件地址""" - file_base64: str = '' - """base64""" - def __str__(self): - return f'[文件]{self.file_name}' \ No newline at end of file diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index 86d940c4..2a081127 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -5,8 +5,8 @@ import abc from . import events from ..core import app -from ..platform.types import message as platform_message -from ..platform import adapter as platform_adapter +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter def register( @@ -115,7 +115,7 @@ class APIHost: # ========== 插件可调用的 API(主程序API) ========== - def get_platform_adapters(self) -> list[platform_adapter.MessagePlatformAdapter]: + def get_platform_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]: """获取已启用的消息平台适配器列表 Returns: @@ -125,7 +125,7 @@ class APIHost: async def send_active_message( self, - adapter: platform_adapter.MessagePlatformAdapter, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, target_type: str, target_id: str, message: platform_message.MessageChain, diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py index e6e2dccb..f60dddfa 100644 --- a/pkg/plugin/events.py +++ b/pkg/plugin/events.py @@ -4,10 +4,10 @@ import typing import pydantic.v1 as pydantic -from ..provider import entities as llm_entities -from ..platform.types import message as platform_message -import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.provider.session as provider_session +from ..provider import entities as llm_entities class BaseEventModel(pydantic.BaseModel): diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 1f38ca01..6de61e39 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -6,7 +6,7 @@ import pydantic from pkg.provider import entities -from ..platform.types import message as platform_message +import langbot_plugin.api.entities.builtin.platform.message as platform_message class FunctionCall(pydantic.BaseModel): From 01613b2f0dcccfc699d3fa690838e94e0f19eec0 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 16 Jun 2025 21:18:26 +0800 Subject: [PATCH 09/78] chore: remove adapter meta manifest from components.yaml --- components.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/components.yaml b/components.yaml index b91b8813..5d8e75d2 100644 --- a/components.yaml +++ b/components.yaml @@ -9,7 +9,6 @@ spec: components: ComponentTemplate: fromFiles: - - pkg/platform/adapter.yaml - pkg/provider/modelmgr/requester.yaml MessagePlatformAdapter: fromDirs: From 5553a86ac8a2f396069e63debc52842d83dd232b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 17 Jun 2025 15:00:49 +0800 Subject: [PATCH 10/78] feat: preliminary migration of events entities --- pkg/pipeline/pipelinemgr.py | 30 +++++++--- pkg/plugin/context.py | 105 +-------------------------------- pkg/plugin/loaders/classic.py | 3 +- pkg/plugin/loaders/manifest.py | 3 +- pkg/plugin/models.py | 3 +- 5 files changed, 28 insertions(+), 116 deletions(-) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 6dbfd1fa..9407daf8 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -11,11 +11,12 @@ from ..entity.persistence import pipeline as persistence_pipeline from . import stage import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events -from ..plugin import events +import langbot_plugin.api.entities.events as events from ..utils import importutil import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.context as event_context from . import ( resprule, @@ -183,16 +184,27 @@ class RuntimePipeline: else events.GroupMessageReceived ) - event_ctx = await self.ap.plugin_mgr.emit_event( - event=event_type( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - message_chain=query.message_chain, - query=query, - ) + print(query) + print(query.model_dump(exclude_none=True)) + + event_obj = event_type( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + message_chain=query.message_chain, + query=query, ) + event_ctx = event_context.EventContext( + event=event_obj, + ) + + event_ctx_result = await self.ap.plugin_connector.handler.emit_event( + event_ctx.model_dump(exclude_none=True) + ) + + event_ctx.update(**event_ctx_result) + if event_ctx.is_prevented_default(): return diff --git a/pkg/plugin/context.py b/pkg/plugin/context.py index 2a081127..a95660c1 100644 --- a/pkg/plugin/context.py +++ b/pkg/plugin/context.py @@ -3,10 +3,10 @@ from __future__ import annotations import typing import abc -from . import events from ..core import app import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter +import langbot_plugin.api.entities.events as events def register( @@ -279,106 +279,3 @@ class EventContext: self.__prevent_postorder__ = False self.__return_value__ = {} EventContext.eid += 1 - - -# class RuntimeContainerStatus(enum.Enum): -# """插件容器状态""" - -# MOUNTED = 'mounted' -# """已加载进内存,所有位于运行时记录中的 RuntimeContainer 至少是这个状态""" - -# INITIALIZED = 'initialized' -# """已初始化""" - - -# class RuntimeContainer(pydantic.BaseModel): -# """运行时的插件容器 - -# 运行期间存储单个插件的信息 -# """ - -# plugin_name: str -# """插件名称""" - -# plugin_label: discover_engine.I18nString -# """插件标签""" - -# plugin_description: discover_engine.I18nString -# """插件描述""" - -# plugin_version: str -# """插件版本""" - -# plugin_author: str -# """插件作者""" - -# plugin_repository: str -# """插件源码地址""" - -# main_file: str -# """插件主文件路径""" - -# pkg_path: str -# """插件包路径""" - -# plugin_class: typing.Type[BasePlugin] = None -# """插件类""" - -# enabled: typing.Optional[bool] = True -# """是否启用""" - -# priority: typing.Optional[int] = 0 -# """优先级""" - -# config_schema: typing.Optional[list[dict]] = [] -# """插件配置模板""" - -# plugin_config: typing.Optional[dict] = {} -# """插件配置""" - -# plugin_inst: typing.Optional[BasePlugin] = None -# """插件实例""" - -# event_handlers: dict[ -# typing.Type[events.BaseEventModel], -# typing.Callable[[BasePlugin, EventContext], typing.Awaitable[None]], -# ] = {} -# """事件处理器""" - -# tools: list[tools_entities.LLMFunction] = [] -# """内容函数""" - -# status: RuntimeContainerStatus = RuntimeContainerStatus.MOUNTED -# """插件状态""" - -# class Config: -# arbitrary_types_allowed = True - -# def model_dump(self, *args, **kwargs): -# return { -# 'name': self.plugin_name, -# 'label': self.plugin_label.to_dict(), -# 'description': self.plugin_description.to_dict(), -# 'version': self.plugin_version, -# 'author': self.plugin_author, -# '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() -# }, -# 'tools': [ -# { -# 'name': function.name, -# 'human_desc': function.human_desc, -# 'description': function.description, -# 'parameters': function.parameters, -# 'func': function.func.__name__, -# } -# for function in self.tools -# ], -# 'status': self.status.value, -# } diff --git a/pkg/plugin/loaders/classic.py b/pkg/plugin/loaders/classic.py index 6613bb63..98a625d4 100644 --- a/pkg/plugin/loaders/classic.py +++ b/pkg/plugin/loaders/classic.py @@ -5,11 +5,12 @@ import pkgutil import importlib import traceback -from .. import loader, events, context, models +from .. import loader, context, models from langbot_plugin.api.entities.builtin.resource import tool as resource_tool from ...utils import funcschema from ...discover import engine as discover_engine import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.events as events class PluginLoader(loader.PluginLoader): diff --git a/pkg/plugin/loaders/manifest.py b/pkg/plugin/loaders/manifest.py index c5a78078..91b15f02 100644 --- a/pkg/plugin/loaders/manifest.py +++ b/pkg/plugin/loaders/manifest.py @@ -5,10 +5,11 @@ import os import traceback from ...core import app -from .. import context, events +from .. import context from .. import loader from ...utils import funcschema import langbot_plugin.api.entities.builtin.resource.tool as resource_tool +import langbot_plugin.api.entities.events as events class PluginManifestLoader(loader.PluginLoader): diff --git a/pkg/plugin/models.py b/pkg/plugin/models.py index dbde89a9..1b3a9b3f 100644 --- a/pkg/plugin/models.py +++ b/pkg/plugin/models.py @@ -8,6 +8,7 @@ import typing from .context import BasePlugin as Plugin from .events import * +import langbot_plugin.api.entities.events as events def register( @@ -17,7 +18,7 @@ def register( def on( - event: typing.Type[BaseEventModel], + event: typing.Type[events.BaseEventModel], ) -> typing.Callable[[typing.Callable], typing.Callable]: pass From f474e42b79f213b0c1c8fa6d619a2d649ac3b57b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 27 Jun 2025 21:16:52 +0800 Subject: [PATCH 11/78] fix: serialization bug in events emitting --- pkg/pipeline/cntfilter/cntfilter.py | 2 +- pkg/pipeline/pipelinemgr.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index b40ecd3c..c40a2042 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -85,7 +85,7 @@ class ContentFilterStage(stage.PipelineStage): elif result.level == filter_entities.ResultLevel.PASS: # 传到下一个 message = result.replacement - query.message_chain = platform_message.MessageChain(platform_message.Plain(message)) + query.message_chain = platform_message.MessageChain(platform_message.Plain(text=message)) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 9407daf8..83faaee3 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -184,24 +184,20 @@ class RuntimePipeline: else events.GroupMessageReceived ) - print(query) - print(query.model_dump(exclude_none=True)) - event_obj = event_type( launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, sender_id=query.sender_id, message_chain=query.message_chain, - query=query, ) event_ctx = event_context.EventContext( event=event_obj, ) - event_ctx_result = await self.ap.plugin_connector.handler.emit_event( - event_ctx.model_dump(exclude_none=True) - ) + event_ctx_data = event_ctx.model_dump(serialize_as_any=True) + + event_ctx_result = await self.ap.plugin_connector.handler.emit_event(event_ctx_data) event_ctx.update(**event_ctx_result) From c246470b377b4147e2bcde1d3bf4b192af061f40 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 1 Jul 2025 22:44:46 +0800 Subject: [PATCH 12/78] feat: minor changes adapt to event emitting --- pkg/pipeline/pipelinemgr.py | 2 +- pkg/plugin/handler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 83faaee3..b32ad98d 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -199,7 +199,7 @@ class RuntimePipeline: event_ctx_result = await self.ap.plugin_connector.handler.emit_event(event_ctx_data) - event_ctx.update(**event_ctx_result) + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) if event_ctx.is_prevented_default(): return diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 7a6a299f..42734261 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -87,4 +87,4 @@ class RuntimeConnectionHandler(handler.Handler): timeout=10, ) - return result['event_context'] + return result From ee3da8aa17e63448d526a1cb8aeb5f8c587f001b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 2 Jul 2025 11:04:03 +0800 Subject: [PATCH 13/78] feat: adapt more events --- pkg/pipeline/cntfilter/cntfilter.py | 4 ++-- pkg/pipeline/controller.py | 6 +++--- pkg/pipeline/pipelinemgr.py | 2 +- pkg/pipeline/preproc/preproc.py | 11 +++++++++-- pkg/pipeline/process/handlers/chat.py | 11 +++++++++-- pkg/pipeline/wrapper/wrapper.py | 13 +++++++++++-- pkg/provider/entities.py | 2 +- pkg/provider/session/sessionmgr.py | 2 +- 8 files changed, 37 insertions(+), 14 deletions(-) diff --git a/pkg/pipeline/cntfilter/cntfilter.py b/pkg/pipeline/cntfilter/cntfilter.py index c40a2042..26b00411 100644 --- a/pkg/pipeline/cntfilter/cntfilter.py +++ b/pkg/pipeline/cntfilter/cntfilter.py @@ -66,7 +66,7 @@ class ContentFilterStage(stage.PipelineStage): if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg': return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) if not message.strip(): - return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) + return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) else: for filter in self.filter_chain: if filter_entities.EnableStage.PRE in filter.enable_stages: @@ -85,7 +85,7 @@ class ContentFilterStage(stage.PipelineStage): elif result.level == filter_entities.ResultLevel.PASS: # 传到下一个 message = result.replacement - query.message_chain = platform_message.MessageChain(platform_message.Plain(text=message)) + query.message_chain = platform_message.MessageChain([platform_message.Plain(text=message)]) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) diff --git a/pkg/pipeline/controller.py b/pkg/pipeline/controller.py index 11bd8d46..b1dde4a6 100644 --- a/pkg/pipeline/controller.py +++ b/pkg/pipeline/controller.py @@ -35,9 +35,9 @@ class Controller: session = await self.ap.sess_mgr.get_session(query) self.ap.logger.debug(f'Checking query {query} session {session}') - if not session.semaphore.locked(): + if not session._semaphore.locked(): selected_query = query - await session.semaphore.acquire() + await session._semaphore.acquire() break @@ -62,7 +62,7 @@ class Controller: await pipeline.run(selected_query) async with self.ap.query_pool: - (await self.ap.sess_mgr.get_session(selected_query)).semaphore.release() + (await self.ap.sess_mgr.get_session(selected_query))._semaphore.release() # 通知其他协程,有新的请求可以处理了 self.ap.query_pool.condition.notify_all() diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index b32ad98d..6abb3972 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -90,7 +90,7 @@ class RuntimePipeline: # 处理str类型 if isinstance(result.user_notice, str): - result.user_notice = platform_message.MessageChain(platform_message.Plain(result.user_notice)) + result.user_notice = platform_message.MessageChain([platform_message.Plain(text=result.user_notice)]) elif isinstance(result.user_notice, list): result.user_notice = platform_message.MessageChain(*result.user_notice) diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 894ceebf..344a136c 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -4,9 +4,10 @@ import datetime from .. import stage, entities from langbot_plugin.api.entities.builtin.provider import message as provider_message -from ...plugin import events +import langbot_plugin.api.entities.events as events import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.context as event_context @stage.stage_class('PreProcessor') @@ -108,7 +109,7 @@ class PreProcessor(stage.PipelineStage): query.user_message = provider_message.Message(role='user', content=content_list) # =========== 触发事件 PromptPreProcessing - event_ctx = await self.ap.plugin_mgr.emit_event( + event_ctx = event_context.EventContext( event=events.PromptPreProcessing( session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', default_prompt=query.prompt.messages, @@ -117,6 +118,12 @@ class PreProcessor(stage.PipelineStage): ) ) + event_ctx_result = await self.ap.plugin_connector.handler.emit_event( + event_ctx.model_dump(serialize_as_any=True) + ) + + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + query.prompt.messages = event_ctx.event.default_prompt query.messages = event_ctx.event.prompt diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 717727d0..24f4553d 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -7,13 +7,14 @@ import traceback from .. import handler from ... import entities from ....provider import runner as runner_module -from ....plugin import events import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.events as events from ....utils import importutil from ....provider import runners import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.context as event_context importutil.import_modules_in_pkg(runners) @@ -35,7 +36,7 @@ class ChatMessageHandler(handler.MessageHandler): else events.GroupNormalMessageReceived ) - event_ctx = await self.ap.plugin_mgr.emit_event( + event_ctx = event_context.EventContext( event=event_class( launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, @@ -45,6 +46,12 @@ class ChatMessageHandler(handler.MessageHandler): ) ) + event_ctx_result = await self.ap.plugin_connector.handler.emit_event( + event_ctx.model_dump(serialize_as_any=True) + ) + + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: mc = platform_message.MessageChain(event_ctx.event.reply) diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 2c6e218e..3608d616 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -4,9 +4,11 @@ import typing from .. import entities from .. import stage -from ...plugin import events + import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.context as event_context +import langbot_plugin.api.entities.events as events @stage.stage_class('ResponseWrapper') @@ -57,7 +59,7 @@ class ResponseWrapper(stage.PipelineStage): reply_text = str(result.get_content_platform_message_chain()) # ============= 触发插件事件 =============== - event_ctx = await self.ap.plugin_mgr.emit_event( + event_ctx = event_context.EventContext( event=events.NormalMessageResponded( launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, @@ -72,6 +74,13 @@ class ResponseWrapper(stage.PipelineStage): query=query, ) ) + + serialized_event_ctx = event_ctx.model_dump(serialize_as_any=True) + + event_ctx_result = await self.ap.plugin_connector.handler.emit_event(serialized_event_ctx) + + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + if event_ctx.is_prevented_default(): yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py index 6de61e39..b03ece38 100644 --- a/pkg/provider/entities.py +++ b/pkg/provider/entities.py @@ -96,7 +96,7 @@ class Message(pydantic.BaseModel): if self.content is None: return None elif isinstance(self.content, str): - return platform_message.MessageChain([platform_message.Plain(prefix_text + self.content)]) + return platform_message.MessageChain([platform_message.Plain(text=(prefix_text + self.content))]) elif isinstance(self.content, list): mc = [] for ce in self.content: diff --git a/pkg/provider/session/sessionmgr.py b/pkg/provider/session/sessionmgr.py index 03465e0b..11d0254c 100644 --- a/pkg/provider/session/sessionmgr.py +++ b/pkg/provider/session/sessionmgr.py @@ -33,8 +33,8 @@ class SessionManager: session = provider_session.Session( launcher_type=query.launcher_type, launcher_id=query.launcher_id, - semaphore=asyncio.Semaphore(session_concurrency), ) + session._semaphore = asyncio.Semaphore(session_concurrency) self.session_list.append(session) return session From e2124054bfd5f6254eeed2fe3d363e9efaeb4953 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 2 Jul 2025 11:58:10 +0800 Subject: [PATCH 14/78] feat: switch all event emitting logic to new method --- pkg/pipeline/process/handlers/command.py | 11 +++++++++-- pkg/pipeline/wrapper/wrapper.py | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index a6156946..cc659955 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -4,11 +4,12 @@ import typing from .. import handler from ... import entities -from ....plugin import events import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.context as event_context +import langbot_plugin.api.entities.events as events class CommandHandler(handler.MessageHandler): @@ -33,7 +34,7 @@ class CommandHandler(handler.MessageHandler): else events.GroupCommandSent ) - event_ctx = await self.ap.plugin_mgr.emit_event( + event_ctx = event_context.EventContext( event=event_class( launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, @@ -46,6 +47,12 @@ class CommandHandler(handler.MessageHandler): ) ) + event_ctx_result = await self.ap.plugin_connector.handler.emit_event( + event_ctx.model_dump(serialize_as_any=True) + ) + + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: mc = platform_message.MessageChain(event_ctx.event.reply) diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index 3608d616..ba17091c 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -108,7 +108,7 @@ class ResponseWrapper(stage.PipelineStage): ) if query.pipeline_config['output']['misc']['track-function-calls']: - event_ctx = await self.ap.plugin_mgr.emit_event( + event_ctx = event_context.EventContext( event=events.NormalMessageResponded( launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, @@ -124,6 +124,10 @@ class ResponseWrapper(stage.PipelineStage): ) ) + event_ctx_result = await self.ap.plugin_connector.handler.emit_event(serialized_event_ctx) + + event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + if event_ctx.is_prevented_default(): yield entities.StageProcessResult( result_type=entities.ResultType.INTERRUPT, From 1a10b40b179f898d34cb30a6bc363734d9fea50b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 2 Jul 2025 12:46:30 +0800 Subject: [PATCH 15/78] refactor: use `emit_event` from connector --- pkg/pipeline/pipelinemgr.py | 11 +---- pkg/pipeline/preproc/preproc.py | 19 +++---- pkg/pipeline/process/handlers/chat.py | 21 +++----- pkg/pipeline/process/handlers/command.py | 27 ++++------ pkg/pipeline/wrapper/wrapper.py | 63 ++++++++++-------------- pkg/plugin/connector.py | 10 +++- 6 files changed, 59 insertions(+), 92 deletions(-) diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 6abb3972..5719b9e6 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -16,7 +16,6 @@ from ..utils import importutil import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.context as event_context from . import ( resprule, @@ -191,15 +190,7 @@ class RuntimePipeline: message_chain=query.message_chain, ) - event_ctx = event_context.EventContext( - event=event_obj, - ) - - event_ctx_data = event_ctx.model_dump(serialize_as_any=True) - - event_ctx_result = await self.ap.plugin_connector.handler.emit_event(event_ctx_data) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event_obj) if event_ctx.is_prevented_default(): return diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 344a136c..5b82bdf4 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -7,7 +7,6 @@ from langbot_plugin.api.entities.builtin.provider import message as provider_mes import langbot_plugin.api.entities.events as events import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.context as event_context @stage.stage_class('PreProcessor') @@ -109,20 +108,14 @@ class PreProcessor(stage.PipelineStage): query.user_message = provider_message.Message(role='user', content=content_list) # =========== 触发事件 PromptPreProcessing - event_ctx = event_context.EventContext( - event=events.PromptPreProcessing( - session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', - default_prompt=query.prompt.messages, - prompt=query.messages, - query=query, - ) + event = events.PromptPreProcessing( + session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}', + default_prompt=query.prompt.messages, + prompt=query.messages, + query=query, ) - event_ctx_result = await self.ap.plugin_connector.handler.emit_event( - event_ctx.model_dump(serialize_as_any=True) - ) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event) query.prompt.messages = event_ctx.event.default_prompt query.messages = event_ctx.event.prompt diff --git a/pkg/pipeline/process/handlers/chat.py b/pkg/pipeline/process/handlers/chat.py index 24f4553d..5bb5f07b 100644 --- a/pkg/pipeline/process/handlers/chat.py +++ b/pkg/pipeline/process/handlers/chat.py @@ -14,7 +14,6 @@ from ....utils import importutil from ....provider import runners import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.context as event_context importutil.import_modules_in_pkg(runners) @@ -36,21 +35,15 @@ class ChatMessageHandler(handler.MessageHandler): else events.GroupNormalMessageReceived ) - event_ctx = event_context.EventContext( - event=event_class( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - text_message=str(query.message_chain), - query=query, - ) + event = event_class( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + text_message=str(query.message_chain), + query=query, ) - event_ctx_result = await self.ap.plugin_connector.handler.emit_event( - event_ctx.model_dump(serialize_as_any=True) - ) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event) if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index cc659955..7a2ec08f 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -8,7 +8,6 @@ import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.context as event_context import langbot_plugin.api.entities.events as events @@ -34,24 +33,18 @@ class CommandHandler(handler.MessageHandler): else events.GroupCommandSent ) - event_ctx = event_context.EventContext( - event=event_class( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - command=spt[0], - params=spt[1:] if len(spt) > 1 else [], - text_message=str(query.message_chain), - is_admin=(privilege == 2), - query=query, - ) + event = event_class( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + command=spt[0], + params=spt[1:] if len(spt) > 1 else [], + text_message=str(query.message_chain), + is_admin=(privilege == 2), + query=query, ) - event_ctx_result = await self.ap.plugin_connector.handler.emit_event( - event_ctx.model_dump(serialize_as_any=True) - ) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event) if event_ctx.is_prevented_default(): if event_ctx.event.reply is not None: diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index ba17091c..f3323622 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -7,7 +7,6 @@ from .. import stage import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.context as event_context import langbot_plugin.api.entities.events as events @@ -59,27 +58,21 @@ class ResponseWrapper(stage.PipelineStage): reply_text = str(result.get_content_platform_message_chain()) # ============= 触发插件事件 =============== - event_ctx = event_context.EventContext( - event=events.NormalMessageResponded( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - session=session, - prefix='', - response_text=reply_text, - finish_reason='stop', - funcs_called=[fc.function.name for fc in result.tool_calls] - if result.tool_calls is not None - else [], - query=query, - ) + event = events.NormalMessageResponded( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + session=session, + prefix='', + response_text=reply_text, + finish_reason='stop', + funcs_called=[fc.function.name for fc in result.tool_calls] + if result.tool_calls is not None + else [], + query=query, ) - serialized_event_ctx = event_ctx.model_dump(serialize_as_any=True) - - event_ctx_result = await self.ap.plugin_connector.handler.emit_event(serialized_event_ctx) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event) if event_ctx.is_prevented_default(): yield entities.StageProcessResult( @@ -108,25 +101,21 @@ class ResponseWrapper(stage.PipelineStage): ) if query.pipeline_config['output']['misc']['track-function-calls']: - event_ctx = event_context.EventContext( - event=events.NormalMessageResponded( - launcher_type=query.launcher_type.value, - launcher_id=query.launcher_id, - sender_id=query.sender_id, - session=session, - prefix='', - response_text=reply_text, - finish_reason='stop', - funcs_called=[fc.function.name for fc in result.tool_calls] - if result.tool_calls is not None - else [], - query=query, - ) + event = events.NormalMessageResponded( + launcher_type=query.launcher_type.value, + launcher_id=query.launcher_id, + sender_id=query.sender_id, + session=session, + prefix='', + response_text=reply_text, + finish_reason='stop', + funcs_called=[fc.function.name for fc in result.tool_calls] + if result.tool_calls is not None + else [], + query=query, ) - event_ctx_result = await self.ap.plugin_connector.handler.emit_event(serialized_event_ctx) - - event_ctx = event_context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = await self.ap.plugin_connector.emit_event(event) if event_ctx.is_prevented_default(): yield entities.StageProcessResult( diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 26cf6fb7..302ee600 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -66,4 +66,12 @@ class PluginRuntimeConnector: self, event: events.BaseEventModel, ) -> context.EventContext: - pass + event_ctx = context.EventContext( + event=event, + ) + + event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True)) + + event_ctx = context.EventContext.parse_from_dict(event_ctx_result['event_context']) + + return event_ctx From a60aa6f644c981875d41694e9b7584f6251461de Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 2 Jul 2025 22:20:20 +0800 Subject: [PATCH 16/78] feat: runtime reconnecting --- pkg/core/stages/build_app.py | 7 ++++++- pkg/plugin/connector.py | 37 ++++++++++++++++++++++++++++++++---- pkg/plugin/handler.py | 10 ++++++++-- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/pkg/core/stages/build_app.py b/pkg/core/stages/build_app.py index 54204c85..f4361008 100644 --- a/pkg/core/stages/build_app.py +++ b/pkg/core/stages/build_app.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from .. import stage, app from ...utils import version, proxy, announce @@ -59,7 +60,11 @@ class BuildAppStage(stage.BootingStage): ap.persistence_mgr = persistence_mgr_inst await persistence_mgr_inst.initialize() - plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap) + async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None: + await asyncio.sleep(3) + await plugin_connector_inst.initialize() + + plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback) await plugin_connector_inst.initialize() ap.plugin_connector = plugin_connector_inst diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 302ee600..7b1bcdc2 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import typing import os import sys @@ -9,9 +10,9 @@ from ..core import app from . import handler from ..utils import platform from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller -from langbot_plugin.runtime.io.connections import stdio as stdio_connection from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller from langbot_plugin.api.entities import events, context +import langbot_plugin.runtime.io.connection as base_connection class PluginRuntimeConnector: @@ -25,12 +26,34 @@ class PluginRuntimeConnector: stdio_client_controller: stdio_client_controller.StdioClientController - def __init__(self, ap: app.Application): + runtime_disconnect_callback: typing.Callable[ + [PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None] + ] + + def __init__( + self, + ap: app.Application, + runtime_disconnect_callback: typing.Callable[ + [PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None] + ], + ): self.ap = ap + self.runtime_disconnect_callback = runtime_disconnect_callback async def initialize(self): - async def new_connection_callback(connection: stdio_connection.StdioConnection): - self.handler = handler.RuntimeConnectionHandler(connection, self.ap) + async def new_connection_callback(connection: base_connection.Connection): + async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool: + if platform.get_platform() == 'docker': + self.ap.logger.error('Disconnected from plugin runtime, trying to reconnect...') + await self.runtime_disconnect_callback(self) + return False + else: + self.ap.logger.error( + 'Disconnected from plugin runtime, cannot automatically reconnect while LangBot connects to plugin runtime via stdio, please restart LangBot.' + ) + return False + + self.handler = handler.RuntimeConnectionHandler(connection, disconnect_callback, self.ap) self.handler_task = asyncio.create_task(self.handler.run()) _ = await self.handler.ping() self.ap.logger.info('Connected to plugin runtime.') @@ -41,8 +64,14 @@ class PluginRuntimeConnector: if platform.get_platform() == 'docker': # use websocket self.ap.logger.info('use websocket to connect to plugin runtime') ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url'] + + async def make_connection_failed_callback(ctrl: ws_client_controller.WebSocketClientController) -> None: + self.ap.logger.error('Failed to connect to plugin runtime, trying to reconnect...') + await self.runtime_disconnect_callback(self) + ctrl = ws_client_controller.WebSocketClientController( ws_url=ws_url, + make_connection_failed_callback=make_connection_failed_callback, ) task = ctrl.run(new_connection_callback) else: # stdio diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 42734261..afa0bbbc 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing from typing import Any import sqlalchemy @@ -22,8 +23,13 @@ class RuntimeConnectionHandler(handler.Handler): ap: app.Application - def __init__(self, connection: Connection, ap: app.Application): - super().__init__(connection) + def __init__( + self, + connection: Connection, + disconnect_callback: typing.Callable[[], typing.Coroutine[typing.Any, typing.Any, bool]], + ap: app.Application, + ): + super().__init__(connection, disconnect_callback) self.ap = ap @self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS) From 5b044a1917ddc4061325e859bf16478ec9920c70 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 6 Jul 2025 21:03:33 +0800 Subject: [PATCH 17/78] feat: add Tool component --- pkg/api/http/controller/groups/plugins.py | 2 +- pkg/api/http/controller/groups/system.py | 13 ++++- pkg/command/operators/func.py | 4 +- pkg/pipeline/preproc/preproc.py | 4 +- pkg/plugin/connector.py | 16 +++++- pkg/plugin/handler.py | 23 +++++++++ pkg/provider/runners/localagent.py | 2 +- pkg/provider/tools/loader.py | 5 +- pkg/provider/tools/loaders/mcp.py | 9 ++-- pkg/provider/tools/loaders/plugin.py | 63 +++++++---------------- pkg/provider/tools/toolmgr.py | 9 ++-- 11 files changed, 84 insertions(+), 66 deletions(-) diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 4551cb07..86ad25e8 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -12,7 +12,7 @@ class PluginsRouterGroup(group.RouterGroup): async def initialize(self) -> None: @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: - plugins = await self.ap.plugin_connector.handler.list_plugins() + plugins = await self.ap.plugin_connector.list_plugins() return self.success(data={'plugins': plugins}) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 1089626d..979d60b2 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -35,7 +35,7 @@ class SystemRouterGroup(group.RouterGroup): return self.success(data=task.to_dict()) - @self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: if not constants.debug_mode: return self.http_status(403, 403, 'Forbidden') @@ -45,3 +45,14 @@ class SystemRouterGroup(group.RouterGroup): ap = self.ap return self.success(data=exec(py_code, {'ap': ap})) + + @self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + async def _() -> str: + if not constants.debug_mode: + return self.http_status(403, 403, 'Forbidden') + + data = await quart.request.json + + return self.success( + data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters']) + ) diff --git a/pkg/command/operators/func.py b/pkg/command/operators/func.py index 648cc5e2..48dbd316 100644 --- a/pkg/command/operators/func.py +++ b/pkg/command/operators/func.py @@ -11,9 +11,7 @@ class FuncOperator(operator.CommandOperator): index = 1 - all_functions = await self.ap.tool_mgr.get_all_functions( - plugin_enabled=True, - ) + all_functions = await self.ap.tool_mgr.get_all_tools() for func in all_functions: reply_str += '{}. {}:\n{}\n\n'.format( diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index 5b82bdf4..b48ced64 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -60,9 +60,7 @@ class PreProcessor(stage.PipelineStage): query.use_funcs = [] if llm_model.model_entity.abilities.__contains__('func_call'): - query.use_funcs = await self.ap.tool_mgr.get_all_functions( - plugin_enabled=True, - ) + query.use_funcs = await self.ap.tool_mgr.get_all_tools() query.variables = { 'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}', diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 7b1bcdc2..c7496e70 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any import typing import os import sys @@ -11,8 +12,10 @@ from . import handler from ..utils import platform from langbot_plugin.runtime.io.controllers.stdio import client as stdio_client_controller from langbot_plugin.runtime.io.controllers.ws import client as ws_client_controller -from langbot_plugin.api.entities import events, context +from langbot_plugin.api.entities import events +from langbot_plugin.api.entities import context import langbot_plugin.runtime.io.connection as base_connection +from langbot_plugin.api.definition.components.manifest import ComponentManifest class PluginRuntimeConnector: @@ -91,6 +94,9 @@ class PluginRuntimeConnector: async def initialize_plugins(self): pass + async def list_plugins(self) -> list[dict[str, Any]]: + return await self.handler.list_plugins() + async def emit_event( self, event: events.BaseEventModel, @@ -104,3 +110,11 @@ class PluginRuntimeConnector: event_ctx = context.EventContext.parse_from_dict(event_ctx_result['event_context']) return event_ctx + + async def list_tools(self) -> list[ComponentManifest]: + list_tools_data = await self.handler.list_tools() + + return [ComponentManifest.model_validate(tool) for tool in list_tools_data] + + async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: + return await self.handler.call_tool(tool_name, parameters) diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index afa0bbbc..16c95770 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -94,3 +94,26 @@ class RuntimeConnectionHandler(handler.Handler): ) return result + + async def list_tools(self) -> list[dict[str, Any]]: + """List tools""" + result = await self.call_action( + LangBotToRuntimeAction.LIST_TOOLS, + {}, + timeout=10, + ) + + return result['tools'] + + async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: + """Call tool""" + result = await self.call_action( + LangBotToRuntimeAction.CALL_TOOL, + { + 'tool_name': tool_name, + 'tool_parameters': parameters, + }, + timeout=30, + ) + + return result['tool_response'] diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 5a879bcb..950f7756 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -43,7 +43,7 @@ class LocalAgentRunner(runner.RequestRunner): parameters = json.loads(func.arguments) - func_ret = await self.ap.tool_mgr.execute_func_call(query, func.name, parameters) + func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters) msg = llm_entities.Message( role='tool', diff --git a/pkg/provider/tools/loader.py b/pkg/provider/tools/loader.py index 658fdeb6..f3d65fd2 100644 --- a/pkg/provider/tools/loader.py +++ b/pkg/provider/tools/loader.py @@ -5,7 +5,6 @@ import typing from ...core import app import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query preregistered_loaders: list[typing.Type[ToolLoader]] = [] @@ -36,7 +35,7 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: + async def get_tools(self) -> list[resource_tool.LLMTool]: """获取所有工具""" pass @@ -46,7 +45,7 @@ class ToolLoader(abc.ABC): pass @abc.abstractmethod - async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, name: str, parameters: dict) -> typing.Any: """执行工具调用""" pass diff --git a/pkg/provider/tools/loaders/mcp.py b/pkg/provider/tools/loaders/mcp.py index 577c704e..36fa9751 100644 --- a/pkg/provider/tools/loaders/mcp.py +++ b/pkg/provider/tools/loaders/mcp.py @@ -10,7 +10,6 @@ from mcp.client.sse import sse_client from .. import loader from ....core import app import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query class RuntimeMCPSession: @@ -84,7 +83,7 @@ class RuntimeMCPSession: for tool in tools.tools: - async def func(query: pipeline_query.Query, *, _tool=tool, **kwargs): + async def func(*, _tool=tool, **kwargs): result = await self.session.call_tool(_tool.name, kwargs) if result.isError: raise Exception(result.content[0].text) @@ -132,7 +131,7 @@ class MCPLoader(loader.ToolLoader): # self.ap.event_loop.create_task(session.initialize()) self.sessions[server_config['name']] = session - async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: + async def get_tools(self) -> list[resource_tool.LLMTool]: all_functions = [] for session in self.sessions.values(): @@ -145,11 +144,11 @@ class MCPLoader(loader.ToolLoader): async def has_tool(self, name: str) -> bool: return name in [f.name for f in self._last_listed_functions] - async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, name: str, parameters: dict) -> typing.Any: for server_name, session in self.sessions.items(): for function in session.functions: if function.name == name: - return await function.func(query, **parameters) + return await function.func(**parameters) raise ValueError(f'未找到工具: {name}') diff --git a/pkg/provider/tools/loaders/plugin.py b/pkg/provider/tools/loaders/plugin.py index 7dfaea97..94296470 100644 --- a/pkg/provider/tools/loaders/plugin.py +++ b/pkg/provider/tools/loaders/plugin.py @@ -4,9 +4,7 @@ import typing import traceback from .. import loader -from ....plugin import context as plugin_context import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @loader.loader_class('plugin-tool-loader') @@ -16,63 +14,42 @@ class PluginToolLoader(loader.ToolLoader): 本加载器中不存储工具信息,仅负责从插件系统中获取工具信息。 """ - async def get_tools(self, enabled: bool = True) -> list[resource_tool.LLMTool]: + async def get_tools(self) -> list[resource_tool.LLMTool]: # 从插件系统获取工具(内容函数) all_functions: list[resource_tool.LLMTool] = [] - for plugin in self.ap.plugin_mgr.plugins( - enabled=enabled, status=plugin_context.RuntimeContainerStatus.INITIALIZED - ): - all_functions.extend(plugin.tools) + for tool in await self.ap.plugin_connector.list_tools(): + tool_obj = resource_tool.LLMTool( + name=tool.metadata.name, + human_desc=tool.metadata.description.en_US, + description=tool.spec['llm_prompt'], + parameters=tool.spec['parameters'], + func=lambda parameters: {}, + ) + all_functions.append(tool_obj) return all_functions async def has_tool(self, name: str) -> bool: """检查工具是否存在""" - for plugin in self.ap.plugin_mgr.plugins( - enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED - ): - for function in plugin.tools: - if function.name == name: - return True + for tool in await self.ap.plugin_connector.list_tools(): + if tool.metadata.name == name: + return True return False - async def _get_function_and_plugin( - self, name: str - ) -> typing.Tuple[resource_tool.LLMTool, plugin_context.BasePlugin]: - """获取函数和插件实例""" - for plugin in self.ap.plugin_mgr.plugins( - enabled=True, status=plugin_context.RuntimeContainerStatus.INITIALIZED - ): - for function in plugin.tools: - if function.name == name: - return function, plugin.plugin_inst - return None, None + async def _get_tool(self, name: str) -> resource_tool.LLMTool: + for tool in await self.ap.plugin_connector.list_tools(): + if tool.metadata.name == name: + return tool + return None - async def invoke_tool(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: + async def invoke_tool(self, name: str, parameters: dict) -> typing.Any: try: - function, plugin = await self._get_function_and_plugin(name) - if function is None: - return None - - parameters = parameters.copy() - - parameters = {'query': query, **parameters} - - return await function.func(plugin, **parameters) + return await self.ap.plugin_connector.call_tool(name, parameters) except Exception as e: self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}') traceback.print_exc() return f'error occurred when executing function {name}: {e}' - finally: - plugin = None - - for p in self.ap.plugin_mgr.plugins(): - if function in p.tools: - plugin = p - break - - # TODO statistics async def shutdown(self): """关闭工具""" diff --git a/pkg/provider/tools/toolmgr.py b/pkg/provider/tools/toolmgr.py index e1105750..43960aba 100644 --- a/pkg/provider/tools/toolmgr.py +++ b/pkg/provider/tools/toolmgr.py @@ -7,7 +7,6 @@ from . import loader as tools_loader from ...utils import importutil from . import loaders import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query importutil.import_modules_in_pkg(loaders) @@ -30,12 +29,12 @@ class ToolManager: await loader_inst.initialize() self.loaders.append(loader_inst) - async def get_all_functions(self, plugin_enabled: bool = None) -> list[resource_tool.LLMTool]: + async def get_all_tools(self) -> list[resource_tool.LLMTool]: """获取所有函数""" all_functions: list[resource_tool.LLMTool] = [] for loader in self.loaders: - all_functions.extend(await loader.get_tools(plugin_enabled)) + all_functions.extend(await loader.get_tools()) return all_functions @@ -91,12 +90,12 @@ class ToolManager: return tools - async def execute_func_call(self, query: pipeline_query.Query, name: str, parameters: dict) -> typing.Any: + async def execute_func_call(self, name: str, parameters: dict) -> typing.Any: """执行函数调用""" for loader in self.loaders: if await loader.has_tool(name): - return await loader.invoke_tool(query, name, parameters) + return await loader.invoke_tool(name, parameters) else: raise ValueError(f'未找到工具: {name}') From 10a44c70b627e38fab04243a5041c59e35061f17 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 10 Jul 2025 10:51:36 +0800 Subject: [PATCH 18/78] feat: switch command entities to sdk --- pkg/command/cmdmgr.py | 15 ++-- pkg/command/entities.py | 75 -------------------- pkg/command/errors.py | 26 ------- pkg/command/operator.py | 10 +-- pkg/command/operators/cmd.py | 13 ++-- pkg/command/operators/delc.py | 21 +++--- pkg/command/operators/func.py | 9 ++- pkg/command/operators/help.py | 9 ++- pkg/command/operators/last.py | 15 ++-- pkg/command/operators/list.py | 11 +-- pkg/command/operators/next.py | 15 ++-- pkg/command/operators/plugin.py | 91 ++++++++++++++----------- pkg/command/operators/prompt.py | 11 +-- pkg/command/operators/resend.py | 11 +-- pkg/command/operators/reset.py | 9 ++- pkg/command/operators/update.py | 9 ++- pkg/command/operators/version.py | 9 ++- pkg/platform/sources/officialaccount.py | 4 +- pkg/platform/sources/qqofficial.py | 4 +- pkg/platform/sources/slack.py | 4 +- pkg/platform/sources/wecom.py | 4 +- pkg/platform/sources/wecomcs.py | 4 +- 22 files changed, 169 insertions(+), 210 deletions(-) delete mode 100644 pkg/command/entities.py delete mode 100644 pkg/command/errors.py diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index 14c3d9e4..b9dae7c8 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -3,10 +3,11 @@ from __future__ import annotations import typing from ..core import app -from . import entities, operator, errors +from . import operator from ..utils import importutil import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors # 引入所有算子以便注册 from . import operators @@ -57,10 +58,10 @@ class CommandManager: async def _execute( self, - context: entities.ExecuteContext, + context: command_context.ExecuteContext, operator_list: list[operator.CommandOperator], operator: operator.CommandOperator = None, - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行命令""" found = False @@ -80,10 +81,10 @@ class CommandManager: if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错 if operator is None: - yield entities.CommandReturn(error=errors.CommandNotFoundError(context.crt_params[0])) + yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(context.crt_params[0])) else: if operator.lowest_privilege > context.privilege: - yield entities.CommandReturn(error=errors.CommandPrivilegeError(operator.name)) + yield command_context.CommandReturn(error=command_errors.CommandPrivilegeError(operator.name)) else: async for ret in operator.execute(context): yield ret @@ -93,7 +94,7 @@ class CommandManager: command_text: str, query: pipeline_query.Query, session: provider_session.Session, - ) -> typing.AsyncGenerator[entities.CommandReturn, None]: + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行命令""" privilege = 1 @@ -101,7 +102,7 @@ class CommandManager: if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']: privilege = 2 - ctx = entities.ExecuteContext( + ctx = command_context.ExecuteContext( query=query, session=session, command_text=command_text, diff --git a/pkg/command/entities.py b/pkg/command/entities.py deleted file mode 100644 index 2e4f8a96..00000000 --- a/pkg/command/entities.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import typing - -import pydantic - -import langbot_plugin.api.entities.builtin.provider.session as provider_session -from . import errors -import langbot_plugin.api.entities.builtin.platform.message as platform_message -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query - - -class CommandReturn(pydantic.BaseModel): - """命令返回值""" - - text: typing.Optional[str] = None - """文本 - """ - - image: typing.Optional[platform_message.Image] = None - """弃用""" - - image_url: typing.Optional[str] = None - """图片链接 - """ - - error: typing.Optional[errors.CommandError] = None - """错误 - """ - - class Config: - arbitrary_types_allowed = True - - -class ExecuteContext(pydantic.BaseModel): - """单次命令执行上下文""" - - query: pipeline_query.Query - """本次消息的请求对象""" - - session: provider_session.Session - """本次消息所属的会话对象""" - - command_text: str - """命令完整文本""" - - command: str - """命令名称""" - - crt_command: str - """当前命令 - - 多级命令中crt_command为当前命令,command为根命令。 - 例如:!plugin on Webwlkr - 处理到plugin时,command为plugin,crt_command为plugin - 处理到on时,command为plugin,crt_command为on - """ - - params: list[str] - """命令参数 - - 整个命令以空格分割后的参数列表 - """ - - crt_params: list[str] - """当前命令参数 - - 多级命令中crt_params为当前命令参数,params为根命令参数。 - 例如:!plugin on Webwlkr - 处理到plugin时,params为['on', 'Webwlkr'],crt_params为['on', 'Webwlkr'] - 处理到on时,params为['on', 'Webwlkr'],crt_params为['Webwlkr'] - """ - - privilege: int - """发起人权限""" diff --git a/pkg/command/errors.py b/pkg/command/errors.py deleted file mode 100644 index df05b3d1..00000000 --- a/pkg/command/errors.py +++ /dev/null @@ -1,26 +0,0 @@ -class CommandError(Exception): - def __init__(self, message: str = None): - self.message = message - - def __str__(self): - return self.message - - -class CommandNotFoundError(CommandError): - def __init__(self, message: str = None): - super().__init__('未知命令: ' + message) - - -class CommandPrivilegeError(CommandError): - def __init__(self, message: str = None): - super().__init__('权限不足: ' + message) - - -class ParamNotEnoughError(CommandError): - def __init__(self, message: str = None): - super().__init__('参数不足: ' + message) - - -class CommandOperationError(CommandError): - def __init__(self, message: str = None): - super().__init__('操作失败: ' + message) diff --git a/pkg/command/operator.py b/pkg/command/operator.py index 9ee3de37..0157cf28 100644 --- a/pkg/command/operator.py +++ b/pkg/command/operator.py @@ -4,7 +4,7 @@ import typing import abc from ..core import app -from . import entities +from langbot_plugin.api.entities.builtin.command import context as command_context preregistered_operators: list[typing.Type[CommandOperator]] = [] @@ -95,16 +95,18 @@ class CommandOperator(metaclass=abc.ABCMeta): pass @abc.abstractmethod - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """实现此方法以执行命令 支持多次yield以返回多个结果。 例如:一个安装插件的命令,可能会有下载、解压、安装等多个步骤,每个步骤都可以返回一个结果。 Args: - context (entities.ExecuteContext): 命令执行上下文 + context (command_context.ExecuteContext): 命令执行上下文 Yields: - entities.CommandReturn: 命令返回封装 + command_context.CommandReturn: 命令返回封装 """ pass diff --git a/pkg/command/operators/cmd.py b/pkg/command/operators/cmd.py index f5a69a7b..cb0c3554 100644 --- a/pkg/command/operators/cmd.py +++ b/pkg/command/operators/cmd.py @@ -2,14 +2,17 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>') class CmdOperator(operator.CommandOperator): """命令列表""" - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行""" if len(context.crt_params) == 0: reply_str = '当前所有命令: \n\n' @@ -20,7 +23,7 @@ class CmdOperator(operator.CommandOperator): reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助' - yield entities.CommandReturn(text=reply_str.strip()) + yield command_context.CommandReturn(text=reply_str.strip()) else: cmd_name = context.crt_params[0] @@ -33,9 +36,9 @@ class CmdOperator(operator.CommandOperator): break if cmd is None: - yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name)) + yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(cmd_name)) else: reply_str = f'{cmd.name}: {cmd.help}\n\n' reply_str += f'使用方法: \n{cmd.usage}' - yield entities.CommandReturn(text=reply_str.strip()) + yield command_context.CommandReturn(text=reply_str.strip()) diff --git a/pkg/command/operators/delc.py b/pkg/command/operators/delc.py index 7e72ff3c..06db3d1e 100644 --- a/pkg/command/operators/delc.py +++ b/pkg/command/operators/delc.py @@ -2,23 +2,26 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all') class DelOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if context.session.conversations: delete_index = 0 if len(context.crt_params) > 0: try: delete_index = int(context.crt_params[0]) except Exception: - yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('索引必须是整数')) return if delete_index < 0 or delete_index >= len(context.session.conversations): - yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('索引超出范围')) return # 倒序 @@ -29,15 +32,17 @@ class DelOperator(operator.CommandOperator): del context.session.conversations[to_delete_index] - yield entities.CommandReturn(text=f'已删除对话: {delete_index}') + yield command_context.CommandReturn(text=f'已删除对话: {delete_index}') else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话')) @operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator) class DelAllOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: context.session.conversations = [] context.session.using_conversation = None - yield entities.CommandReturn(text='已删除所有对话') + yield command_context.CommandReturn(text='已删除所有对话') diff --git a/pkg/command/operators/func.py b/pkg/command/operators/func.py index 48dbd316..e7828a51 100644 --- a/pkg/command/operators/func.py +++ b/pkg/command/operators/func.py @@ -1,12 +1,15 @@ from __future__ import annotations from typing import AsyncGenerator -from .. import operator, entities +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context @operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func') class FuncOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> AsyncGenerator[command_context.CommandReturn, None]: reply_str = '当前已启用的内容函数: \n\n' index = 1 @@ -21,4 +24,4 @@ class FuncOperator(operator.CommandOperator): ) index += 1 - yield entities.CommandReturn(text=reply_str) + yield command_context.CommandReturn(text=reply_str) diff --git a/pkg/command/operators/help.py b/pkg/command/operators/help.py index 91ad66dc..609f05ad 100644 --- a/pkg/command/operators/help.py +++ b/pkg/command/operators/help.py @@ -2,14 +2,17 @@ from __future__ import annotations import typing -from .. import operator, entities +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context @operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>') class HelpOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接:https://langbot.app' help += '\n发送命令 !cmd 可查看命令列表' - yield entities.CommandReturn(text=help) + yield command_context.CommandReturn(text=help) diff --git a/pkg/command/operators/last.py b/pkg/command/operators/last.py index 25b1fc6a..3f92e2e2 100644 --- a/pkg/command/operators/last.py +++ b/pkg/command/operators/last.py @@ -3,26 +3,31 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='last', help='切换到前一个对话', usage='!last') class LastOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if context.session.conversations: # 找到当前会话的上一个会话 for index in range(len(context.session.conversations) - 1, -1, -1): if context.session.conversations[index] == context.session.using_conversation: if index == 0: - yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了')) + yield command_context.CommandReturn( + error=command_errors.CommandOperationError('已经是第一个对话了') + ) return else: context.session.using_conversation = context.session.conversations[index - 1] time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S') - yield entities.CommandReturn( + yield command_context.CommandReturn( text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}' ) return else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话')) diff --git a/pkg/command/operators/list.py b/pkg/command/operators/list.py index 70ff3945..ca1bf8e9 100644 --- a/pkg/command/operators/list.py +++ b/pkg/command/operators/list.py @@ -2,19 +2,22 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>') class ListOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: page = 0 if len(context.crt_params) > 0: try: page = int(context.crt_params[0] - 1) except Exception: - yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('页码应为整数')) return record_per_page = 10 @@ -45,4 +48,4 @@ class ListOperator(operator.CommandOperator): else: content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}' - yield entities.CommandReturn(text=f'第 {page + 1} 页 (时间倒序):\n{content}') + yield command_context.CommandReturn(text=f'第 {page + 1} 页 (时间倒序):\n{content}') diff --git a/pkg/command/operators/next.py b/pkg/command/operators/next.py index 938c8331..87cc565c 100644 --- a/pkg/command/operators/next.py +++ b/pkg/command/operators/next.py @@ -2,26 +2,31 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='next', help='切换到后一个对话', usage='!next') class NextOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if context.session.conversations: # 找到当前会话的下一个会话 for index in range(len(context.session.conversations)): if context.session.conversations[index] == context.session.using_conversation: if index == len(context.session.conversations) - 1: - yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了')) + yield command_context.CommandReturn( + error=command_errors.CommandOperationError('已经是最后一个对话了') + ) return else: context.session.using_conversation = context.session.conversations[index + 1] time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S') - yield entities.CommandReturn( + yield command_context.CommandReturn( text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}' ) return else: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话')) diff --git a/pkg/command/operators/plugin.py b/pkg/command/operators/plugin.py index 40ec0e3a..1c135bd4 100644 --- a/pkg/command/operators/plugin.py +++ b/pkg/command/operators/plugin.py @@ -2,7 +2,8 @@ from __future__ import annotations import typing import traceback -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class( @@ -11,7 +12,9 @@ from .. import operator, entities, errors usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>', ) class PluginOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: plugin_list = self.ap.plugin_mgr.plugins() reply_str = '所有插件({}):\n'.format(len(plugin_list)) idx = 0 @@ -27,32 +30,36 @@ class PluginOperator(operator.CommandOperator): idx += 1 - yield entities.CommandReturn(text=reply_str) + yield command_context.CommandReturn(text=reply_str) @operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator) class PluginGetOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址')) + yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件仓库地址')) else: repo = context.crt_params[0] - yield entities.CommandReturn(text='正在安装插件...') + yield command_context.CommandReturn(text='正在安装插件...') try: await self.ap.plugin_mgr.install_plugin(repo) - yield entities.CommandReturn(text='插件安装成功,请重启程序以加载插件') + yield command_context.CommandReturn(text='插件安装成功,请重启程序以加载插件') except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件安装失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件安装失败: ' + str(e))) @operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator) class PluginUpdateOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称')) else: plugin_name = context.crt_params[0] @@ -60,24 +67,26 @@ class PluginUpdateOperator(operator.CommandOperator): plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) if plugin_container is not None: - yield entities.CommandReturn(text='正在更新插件...') + yield command_context.CommandReturn(text='正在更新插件...') await self.ap.plugin_mgr.update_plugin(plugin_name) - yield entities.CommandReturn(text='插件更新成功,请重启程序以加载插件') + yield command_context.CommandReturn(text='插件更新成功,请重启程序以加载插件') else: - yield entities.CommandReturn(error=errors.CommandError('插件更新失败: 未找到插件')) + yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: 未找到插件')) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e))) @operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator) class PluginUpdateAllOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: try: plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()] if plugins: - yield entities.CommandReturn(text='正在更新插件...') + yield command_context.CommandReturn(text='正在更新插件...') updated = [] try: for plugin_name in plugins: @@ -85,20 +94,22 @@ class PluginUpdateAllOperator(operator.CommandOperator): updated.append(plugin_name) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) - yield entities.CommandReturn(text='已更新插件: {}'.format(', '.join(updated))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e))) + yield command_context.CommandReturn(text='已更新插件: {}'.format(', '.join(updated))) else: - yield entities.CommandReturn(text='没有可更新的插件') + yield command_context.CommandReturn(text='没有可更新的插件') except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e))) @operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator) class PluginDelOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称')) else: plugin_name = context.crt_params[0] @@ -106,51 +117,55 @@ class PluginDelOperator(operator.CommandOperator): plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) if plugin_container is not None: - yield entities.CommandReturn(text='正在删除插件...') + yield command_context.CommandReturn(text='正在删除插件...') await self.ap.plugin_mgr.uninstall_plugin(plugin_name) - yield entities.CommandReturn(text='插件删除成功,请重启程序以加载插件') + yield command_context.CommandReturn(text='插件删除成功,请重启程序以加载插件') else: - yield entities.CommandReturn(error=errors.CommandError('插件删除失败: 未找到插件')) + yield command_context.CommandReturn(error=command_errors.CommandError('插件删除失败: 未找到插件')) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件删除失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件删除失败: ' + str(e))) @operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator) class PluginEnableOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称')) else: plugin_name = context.crt_params[0] try: if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True): - yield entities.CommandReturn(text='已启用插件: {}'.format(plugin_name)) + yield command_context.CommandReturn(text='已启用插件: {}'.format(plugin_name)) else: - yield entities.CommandReturn( - error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) + yield command_context.CommandReturn( + error=command_errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件状态修改失败: ' + str(e))) @operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator) class PluginDisableOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: if len(context.crt_params) == 0: - yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) + yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称')) else: plugin_name = context.crt_params[0] try: if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False): - yield entities.CommandReturn(text='已禁用插件: {}'.format(plugin_name)) + yield command_context.CommandReturn(text='已禁用插件: {}'.format(plugin_name)) else: - yield entities.CommandReturn( - error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) + yield command_context.CommandReturn( + error=command_errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) ) except Exception as e: traceback.print_exc() - yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e))) + yield command_context.CommandReturn(error=command_errors.CommandError('插件状态修改失败: ' + str(e))) diff --git a/pkg/command/operators/prompt.py b/pkg/command/operators/prompt.py index fdcba2bd..b43be2cf 100644 --- a/pkg/command/operators/prompt.py +++ b/pkg/command/operators/prompt.py @@ -2,19 +2,22 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt') class PromptOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行""" if context.session.using_conversation is None: - yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) + yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话')) else: reply_str = '当前对话所有内容:\n\n' for msg in context.session.using_conversation.messages: reply_str += f'{msg.role}: {msg.content}\n' - yield entities.CommandReturn(text=reply_str) + yield command_context.CommandReturn(text=reply_str) diff --git a/pkg/command/operators/resend.py b/pkg/command/operators/resend.py index 39789fef..14bfee99 100644 --- a/pkg/command/operators/resend.py +++ b/pkg/command/operators/resend.py @@ -2,15 +2,18 @@ from __future__ import annotations import typing -from .. import operator, entities, errors +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors @operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend') class ResendOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: # 回滚到最后一条用户message前 if context.session.using_conversation is None: - yield entities.CommandReturn(error=errors.CommandError('当前没有对话')) + yield command_context.CommandReturn(error=command_errors.CommandError('当前没有对话')) else: conv_msg = context.session.using_conversation.messages @@ -23,4 +26,4 @@ class ResendOperator(operator.CommandOperator): conv_msg.pop() # 不重发了,提示用户已删除就行了 - yield entities.CommandReturn(text='已删除最后一次请求记录') + yield command_context.CommandReturn(text='已删除最后一次请求记录') diff --git a/pkg/command/operators/reset.py b/pkg/command/operators/reset.py index 008143a1..0c85fb32 100644 --- a/pkg/command/operators/reset.py +++ b/pkg/command/operators/reset.py @@ -2,13 +2,16 @@ from __future__ import annotations import typing -from .. import operator, entities +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context @operator.operator_class(name='reset', help='重置当前会话', usage='!reset') class ResetOperator(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行""" context.session.using_conversation = None - yield entities.CommandReturn(text='已重置当前会话') + yield command_context.CommandReturn(text='已重置当前会话') diff --git a/pkg/command/operators/update.py b/pkg/command/operators/update.py index 29b8f560..84e5c9cc 100644 --- a/pkg/command/operators/update.py +++ b/pkg/command/operators/update.py @@ -2,10 +2,13 @@ from __future__ import annotations import typing -from .. import operator, entities +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context @operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2) class UpdateCommand(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: - yield entities.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。') + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: + yield command_context.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。') diff --git a/pkg/command/operators/version.py b/pkg/command/operators/version.py index 200875aa..5b3c3358 100644 --- a/pkg/command/operators/version.py +++ b/pkg/command/operators/version.py @@ -2,12 +2,15 @@ from __future__ import annotations import typing -from .. import operator, entities +from .. import operator +from langbot_plugin.api.entities.builtin.command import context as command_context @operator.operator_class(name='version', help='显示版本信息', usage='!version') class VersionCommand(operator.CommandOperator): - async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: + async def execute( + self, context: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}' try: @@ -16,4 +19,4 @@ class VersionCommand(operator.CommandOperator): except Exception: pass - yield entities.CommandReturn(text=reply_str.strip()) + yield command_context.CommandReturn(text=reply_str.strip()) diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py index 74321b92..01a2c868 100644 --- a/pkg/platform/sources/officialaccount.py +++ b/pkg/platform/sources/officialaccount.py @@ -11,7 +11,7 @@ from libs.official_account_api.api import OAClientForLongerResponse import langbot_plugin.api.entities.builtin.platform.entities as platform_entities import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events -from ...command.errors import ParamNotEnoughError +from langbot_plugin.api.entities.builtin.command import errors as command_errors from ..logger import EventLogger @@ -75,7 +75,7 @@ class OfficialAccountAdapter(abstract_platform_adapter.AbstractMessagePlatformAd ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('微信公众号缺少相关配置项,请查看文档或联系管理员') + raise command_errors.ParamNotEnoughError('微信公众号缺少相关配置项,请查看文档或联系管理员') if self.config['Mode'] == 'drop': self.bot = OAClient( diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index 1160fd0e..28a09d8c 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -9,7 +9,7 @@ import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platf import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.platform.entities as platform_entities -from ...command.errors import ParamNotEnoughError +from langbot_plugin.api.entities.builtin.command import errors as command_errors from libs.qq_official_api.api import QQOfficialClient from libs.qq_official_api.qqofficialevent import QQOfficialEvent from ...utils import image @@ -148,7 +148,7 @@ class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('QQ官方机器人缺少相关配置项,请查看文档或联系管理员') + raise command_errors.ParamNotEnoughError('QQ官方机器人缺少相关配置项,请查看文档或联系管理员') self.bot = QQOfficialClient( app_id=config['appid'], secret=config['secret'], token=config['token'], logger=self.logger diff --git a/pkg/platform/sources/slack.py b/pkg/platform/sources/slack.py index c2997828..e08cc8c0 100644 --- a/pkg/platform/sources/slack.py +++ b/pkg/platform/sources/slack.py @@ -11,7 +11,7 @@ from libs.slack_api.slackevent import SlackEvent import langbot_plugin.api.entities.builtin.platform.events as platform_events import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.entities as platform_entities -from ...command.errors import ParamNotEnoughError +from langbot_plugin.api.entities.builtin.command import errors as command_errors from ...utils import image from ..logger import EventLogger @@ -99,7 +99,7 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') + raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员') self.bot = SlackClient( bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 88b89e03..392db801 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -8,7 +8,7 @@ import datetime from libs.wecom_api.api import WecomClient import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from libs.wecom_api.wecomevent import WecomEvent -from ...command.errors import ParamNotEnoughError +from langbot_plugin.api.entities.builtin.command import errors as command_errors from ...utils import image from ..logger import EventLogger import langbot_plugin.api.entities.builtin.platform.message as platform_message @@ -146,7 +146,7 @@ class WecomAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('企业微信缺少相关配置项,请查看文档或联系管理员') + raise command_errors.ParamNotEnoughError('企业微信缺少相关配置项,请查看文档或联系管理员') self.bot = WecomClient( corpid=config['corpid'], diff --git a/pkg/platform/sources/wecomcs.py b/pkg/platform/sources/wecomcs.py index 0958db68..a0c24c08 100644 --- a/pkg/platform/sources/wecomcs.py +++ b/pkg/platform/sources/wecomcs.py @@ -12,7 +12,7 @@ from libs.wecom_customer_service_api.wecomcsevent import WecomCSEvent import langbot_plugin.api.entities.builtin.platform.entities as platform_entities import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events -from ...command.errors import ParamNotEnoughError +from langbot_plugin.api.entities.builtin.command import errors as command_errors import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger @@ -131,7 +131,7 @@ class WecomCSAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ] missing_keys = [key for key in required_keys if key not in config] if missing_keys: - raise ParamNotEnoughError('企业微信客服缺少相关配置项,请查看文档或联系管理员') + raise command_errors.ParamNotEnoughError('企业微信客服缺少相关配置项,请查看文档或联系管理员') bot = WecomCSClient( corpid=config['corpid'], From 5922be7e15377784ba4583150793798d5839cf79 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 10:26:48 +0800 Subject: [PATCH 19/78] feat: command execution via plugin --- pkg/api/http/controller/groups/system.py | 21 +++++++++++++ pkg/command/cmdmgr.py | 40 ++++++++---------------- pkg/plugin/connector.py | 16 ++++++++++ pkg/plugin/handler.py | 22 +++++++++++++ 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 979d60b2..2db910b9 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -56,3 +56,24 @@ class SystemRouterGroup(group.RouterGroup): return self.success( data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters']) ) + + @self.route('/debug/plugin/action', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + async def _() -> str: + if not constants.debug_mode: + return self.http_status(403, 403, 'Forbidden') + + data = await quart.request.json + + class AnoymousAction: + value = 'anonymous_action' + + def __init__(self, value: str): + self.value = value + + resp = await self.ap.plugin_connector.handler.call_action( + AnoymousAction(data['action']), + data['data'], + timeout=data.get('timeout', 10), + ) + + return self.success(data=resp) diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index b9dae7c8..133e6a80 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -16,13 +16,11 @@ importutil.import_modules_in_pkg(operators) class CommandManager: - """命令管理器""" - ap: app.Application cmd_list: list[operator.CommandOperator] """ - 运行时命令列表,扁平存储,各个对象包含对应的子节点引用 + Runtime command list, flat storage, each object contains a reference to the corresponding child node """ def __init__(self, ap: app.Application): @@ -64,30 +62,15 @@ class CommandManager: ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: """执行命令""" - found = False - if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名 - for oper in operator_list: - if (context.crt_params[0] == oper.name or context.crt_params[0] in oper.alias) and ( - oper.parent_class is None or oper.parent_class == operator.__class__ - ): - found = True + command_list = await self.ap.plugin_connector.list_commands() - context.crt_command = context.crt_params[0] - context.crt_params = context.crt_params[1:] - - async for ret in self._execute(context, oper.children, oper): - yield ret - break - - if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错 - if operator is None: - yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(context.crt_params[0])) - else: - if operator.lowest_privilege > context.privilege: - yield command_context.CommandReturn(error=command_errors.CommandPrivilegeError(operator.name)) - else: - async for ret in operator.execute(context): - yield ret + for command in command_list: + if command.metadata.name == context.command: + async for ret in self.ap.plugin_connector.execute_command(context): + yield ret + break + else: + yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(context.command)) async def execute( self, @@ -103,7 +86,6 @@ class CommandManager: privilege = 2 ctx = command_context.ExecuteContext( - query=query, session=session, command_text=command_text, command='', @@ -113,5 +95,9 @@ class CommandManager: privilege=privilege, ) + ctx.command = ctx.params[0] + + ctx.shift() + async for ret in self._execute(ctx, self.cmd_list): yield ret diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index c7496e70..8969d35d 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -16,6 +16,7 @@ from langbot_plugin.api.entities import events from langbot_plugin.api.entities import context import langbot_plugin.runtime.io.connection as base_connection from langbot_plugin.api.definition.components.manifest import ComponentManifest +from langbot_plugin.api.entities.builtin.command import context as command_context class PluginRuntimeConnector: @@ -118,3 +119,18 @@ class PluginRuntimeConnector: async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: return await self.handler.call_tool(tool_name, parameters) + + async def list_commands(self) -> list[ComponentManifest]: + list_commands_data = await self.handler.list_commands() + + return [ComponentManifest.model_validate(command) for command in list_commands_data] + + async def execute_command( + self, command_ctx: command_context.ExecuteContext + ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: + gen = self.handler.execute_command(command_ctx.model_dump(serialize_as_any=True)) + + async for ret in gen: + cmd_ret = command_context.CommandReturn.model_validate(ret) + + yield cmd_ret diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 16c95770..9e9449cc 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -117,3 +117,25 @@ class RuntimeConnectionHandler(handler.Handler): ) return result['tool_response'] + + async def list_commands(self) -> list[dict[str, Any]]: + """List commands""" + result = await self.call_action( + LangBotToRuntimeAction.LIST_COMMANDS, + {}, + timeout=10, + ) + return result['commands'] + + async def execute_command(self, command_context: dict[str, Any]) -> typing.AsyncGenerator[dict[str, Any], None]: + """Execute command""" + gen = self.call_action_generator( + LangBotToRuntimeAction.EXECUTE_COMMAND, + { + 'command_context': command_context, + }, + timeout=10, + ) + + async for ret in gen: + yield ret From 4b57771eb1f8a3f659ee5f5d595761c850f71628 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 16:31:25 +0800 Subject: [PATCH 20/78] feat: `reply_message` api --- pkg/command/operators/update.py | 14 -------------- pkg/pipeline/pipelinemgr.py | 1 + pkg/plugin/connector.py | 6 ++---- pkg/plugin/handler.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 18 deletions(-) delete mode 100644 pkg/command/operators/update.py diff --git a/pkg/command/operators/update.py b/pkg/command/operators/update.py deleted file mode 100644 index 84e5c9cc..00000000 --- a/pkg/command/operators/update.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import typing - -from .. import operator -from langbot_plugin.api.entities.builtin.command import context as command_context - - -@operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2) -class UpdateCommand(operator.CommandOperator): - async def execute( - self, context: command_context.ExecuteContext - ) -> typing.AsyncGenerator[command_context.CommandReturn, None]: - yield command_context.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。') diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index 5719b9e6..b5cf664a 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -184,6 +184,7 @@ class RuntimePipeline: ) event_obj = event_type( + query=query, launcher_type=query.launcher_type.value, launcher_id=query.launcher_id, sender_id=query.sender_id, diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 8969d35d..b46302d5 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -102,13 +102,11 @@ class PluginRuntimeConnector: self, event: events.BaseEventModel, ) -> context.EventContext: - event_ctx = context.EventContext( - event=event, - ) + event_ctx = context.EventContext.from_event(event) event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True)) - event_ctx = context.EventContext.parse_from_dict(event_ctx_result['event_context']) + event_ctx = context.EventContext.model_validate(event_ctx_result['event_context']) return event_ctx diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 9e9449cc..a4a77ea6 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -11,7 +11,10 @@ from langbot_plugin.entities.io.actions.enums import ( CommonAction, RuntimeToLangBotAction, LangBotToRuntimeAction, + PluginToRuntimeAction, ) +import langbot_plugin.api.entities.context as event_context_module +import langbot_plugin.api.entities.builtin.platform.message as platform_message from ..entity.persistence import plugin as persistence_plugin @@ -62,6 +65,32 @@ class RuntimeConnectionHandler(handler.Handler): data=data, ) + @self.action(PluginToRuntimeAction.REPLY_MESSAGE) + async def reply_message(data: dict[str, Any]) -> handler.ActionResponse: + """Reply message""" + eid = data['eid'] + message_chain = data['message_chain'] + quote_origin = data['quote_origin'] + + if eid not in event_context_module.cached_event_contexts: + return handler.ActionResponse.error( + message=f'Event context with eid {eid} not found', + ) + + event_context = event_context_module.cached_event_contexts[eid] + + message_chain_obj = platform_message.MessageChain.model_validate(message_chain) + + await event_context.event.query.adapter.reply_message( + event_context.event.query.message_event, + message_chain_obj, + quote_origin, + ) + + return handler.ActionResponse.success( + data={}, + ) + async def ping(self) -> dict[str, Any]: """Ping the runtime""" return await self.call_action( From 9f269d161433146e8515a5d19c18a93371eef81c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 17:44:20 +0800 Subject: [PATCH 21/78] feat: get bot uuid api --- pkg/command/cmdmgr.py | 1 + pkg/pipeline/pipelinemgr.py | 1 + pkg/pipeline/pool.py | 8 +++++++- pkg/plugin/handler.py | 30 +++++++++++++++++++++++------- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/pkg/command/cmdmgr.py b/pkg/command/cmdmgr.py index 133e6a80..17da8161 100644 --- a/pkg/command/cmdmgr.py +++ b/pkg/command/cmdmgr.py @@ -86,6 +86,7 @@ class CommandManager: privilege = 2 ctx = command_context.ExecuteContext( + query_id=query.query_id, session=session, command_text=command_text, command='', diff --git a/pkg/pipeline/pipelinemgr.py b/pkg/pipeline/pipelinemgr.py index b5cf664a..e71ff0dc 100644 --- a/pkg/pipeline/pipelinemgr.py +++ b/pkg/pipeline/pipelinemgr.py @@ -205,6 +205,7 @@ class RuntimePipeline: self.ap.logger.error(f'Traceback: {traceback.format_exc()}') finally: self.ap.logger.debug(f'Query {query} processed') + del self.ap.query_pool.cached_queries[query.query_id] class PipelineManager: diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index eb32fce6..898cfad6 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -19,12 +19,16 @@ class QueryPool: queries: list[pipeline_query.Query] + cached_queries: dict[int, pipeline_query.Query] + """Cached queries, used for plugin backward api call, will be removed after the query completely processed""" + condition: asyncio.Condition def __init__(self): self.query_id_counter = 0 self.pool_lock = asyncio.Lock() self.queries = [] + self.cached_queries = {} self.condition = asyncio.Condition(self.pool_lock) async def add_query( @@ -39,9 +43,10 @@ class QueryPool: pipeline_uuid: typing.Optional[str] = None, ) -> pipeline_query.Query: async with self.condition: + query_id = self.query_id_counter query = pipeline_query.Query( bot_uuid=bot_uuid, - query_id=self.query_id_counter, + query_id=query_id, launcher_type=launcher_type, launcher_id=launcher_id, sender_id=sender_id, @@ -53,6 +58,7 @@ class QueryPool: pipeline_uuid=pipeline_uuid, ) self.queries.append(query) + self.cached_queries[query_id] = query self.query_id_counter += 1 self.condition.notify_all() diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index a4a77ea6..8cd0f357 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -13,7 +13,6 @@ from langbot_plugin.entities.io.actions.enums import ( LangBotToRuntimeAction, PluginToRuntimeAction, ) -import langbot_plugin.api.entities.context as event_context_module import langbot_plugin.api.entities.builtin.platform.message as platform_message from ..entity.persistence import plugin as persistence_plugin @@ -68,21 +67,21 @@ class RuntimeConnectionHandler(handler.Handler): @self.action(PluginToRuntimeAction.REPLY_MESSAGE) async def reply_message(data: dict[str, Any]) -> handler.ActionResponse: """Reply message""" - eid = data['eid'] + query_id = data['query_id'] message_chain = data['message_chain'] quote_origin = data['quote_origin'] - if eid not in event_context_module.cached_event_contexts: + if query_id not in self.ap.query_pool.cached_queries: return handler.ActionResponse.error( - message=f'Event context with eid {eid} not found', + message=f'Query with query_id {query_id} not found', ) - event_context = event_context_module.cached_event_contexts[eid] + query = self.ap.query_pool.cached_queries[query_id] message_chain_obj = platform_message.MessageChain.model_validate(message_chain) - await event_context.event.query.adapter.reply_message( - event_context.event.query.message_event, + await query.adapter.reply_message( + query.message_event, message_chain_obj, quote_origin, ) @@ -91,6 +90,23 @@ class RuntimeConnectionHandler(handler.Handler): data={}, ) + @self.action(PluginToRuntimeAction.GET_BOT_UUID) + async def get_bot_uuid(data: dict[str, Any]) -> handler.ActionResponse: + """Get bot uuid""" + query_id = data['query_id'] + if query_id not in self.ap.query_pool.cached_queries: + return handler.ActionResponse.error( + message=f'Query with query_id {query_id} not found', + ) + + query = self.ap.query_pool.cached_queries[query_id] + + return handler.ActionResponse.success( + data={ + 'bot_uuid': query.bot_uuid, + }, + ) + async def ping(self) -> dict[str, Any]: """Ping the runtime""" return await self.call_action( From 4a319b2b20ca1f8cfc4b3cde5ffe3441326206be Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 18:41:04 +0800 Subject: [PATCH 22/78] feat: query-based apis --- pkg/pipeline/pool.py | 1 + pkg/pipeline/preproc/preproc.py | 3 +- pkg/plugin/handler.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/pkg/pipeline/pool.py b/pkg/pipeline/pool.py index 898cfad6..eb7df66b 100644 --- a/pkg/pipeline/pool.py +++ b/pkg/pipeline/pool.py @@ -52,6 +52,7 @@ class QueryPool: sender_id=sender_id, message_event=message_event, message_chain=message_chain, + variables={}, resp_messages=[], resp_message_chain=[], adapter=adapter, diff --git a/pkg/pipeline/preproc/preproc.py b/pkg/pipeline/preproc/preproc.py index b48ced64..8cbcf8c7 100644 --- a/pkg/pipeline/preproc/preproc.py +++ b/pkg/pipeline/preproc/preproc.py @@ -62,13 +62,14 @@ class PreProcessor(stage.PipelineStage): if llm_model.model_entity.abilities.__contains__('func_call'): query.use_funcs = await self.ap.tool_mgr.get_all_tools() - query.variables = { + variables = { 'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}', 'conversation_id': conversation.uuid, 'msg_create_time': ( int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp()) ), } + query.variables.update(variables) # Check if this model supports vision, if not, remove all images # TODO this checking should be performed in runner, and in this stage, the image should be reserved diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 8cd0f357..77fc46bd 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -107,6 +107,62 @@ class RuntimeConnectionHandler(handler.Handler): }, ) + @self.action(PluginToRuntimeAction.SET_QUERY_VAR) + async def set_query_var(data: dict[str, Any]) -> handler.ActionResponse: + """Set query var""" + query_id = data['query_id'] + key = data['key'] + value = data['value'] + + if query_id not in self.ap.query_pool.cached_queries: + return handler.ActionResponse.error( + message=f'Query with query_id {query_id} not found', + ) + + query = self.ap.query_pool.cached_queries[query_id] + + query.variables[key] = value + + return handler.ActionResponse.success( + data={}, + ) + + @self.action(PluginToRuntimeAction.GET_QUERY_VAR) + async def get_query_var(data: dict[str, Any]) -> handler.ActionResponse: + """Get query var""" + query_id = data['query_id'] + key = data['key'] + + if query_id not in self.ap.query_pool.cached_queries: + return handler.ActionResponse.error( + message=f'Query with query_id {query_id} not found', + ) + + query = self.ap.query_pool.cached_queries[query_id] + + return handler.ActionResponse.success( + data={ + 'value': query.variables[key], + }, + ) + + @self.action(PluginToRuntimeAction.GET_QUERY_VARS) + async def get_query_vars(data: dict[str, Any]) -> handler.ActionResponse: + """Get query vars""" + query_id = data['query_id'] + if query_id not in self.ap.query_pool.cached_queries: + return handler.ActionResponse.error( + message=f'Query with query_id {query_id} not found', + ) + + query = self.ap.query_pool.cached_queries[query_id] + + return handler.ActionResponse.success( + data={ + 'vars': query.variables, + }, + ) + async def ping(self) -> dict[str, Any]: """Ping the runtime""" return await self.call_action( From 6a1de889b4ab87b11782f250a76711807d41385c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 20:30:17 +0800 Subject: [PATCH 23/78] refactor: switch llm_entities to plugin sdk --- pkg/plugin/events.py | 171 ------------------ pkg/provider/entities.py | 135 -------------- pkg/provider/modelmgr/requester.py | 6 +- .../modelmgr/requesters/anthropicmsgs.py | 14 +- pkg/provider/modelmgr/requesters/chatcmpl.py | 12 +- .../modelmgr/requesters/deepseekchatcmpl.py | 4 +- .../modelmgr/requesters/giteeaichatcmpl.py | 4 +- .../modelmgr/requesters/modelscopechatcmpl.py | 12 +- .../modelmgr/requesters/moonshotchatcmpl.py | 4 +- .../modelmgr/requesters/ollamachat.py | 22 +-- pkg/provider/runner.py | 4 +- pkg/provider/runners/dashscopeapi.py | 14 +- pkg/provider/runners/difysvapi.py | 36 ++-- pkg/provider/runners/localagent.py | 8 +- pkg/provider/runners/n8nsvapi.py | 8 +- 15 files changed, 76 insertions(+), 378 deletions(-) delete mode 100644 pkg/plugin/events.py delete mode 100644 pkg/provider/entities.py diff --git a/pkg/plugin/events.py b/pkg/plugin/events.py deleted file mode 100644 index f60dddfa..00000000 --- a/pkg/plugin/events.py +++ /dev/null @@ -1,171 +0,0 @@ -from __future__ import annotations - -import typing - -import pydantic.v1 as pydantic - -import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query -import langbot_plugin.api.entities.builtin.platform.message as platform_message -import langbot_plugin.api.entities.builtin.provider.session as provider_session -from ..provider import entities as llm_entities - - -class BaseEventModel(pydantic.BaseModel): - """事件模型基类""" - - query: typing.Union[pipeline_query.Query, None] - """此次请求的query对象,非请求过程的事件时为None""" - - class Config: - arbitrary_types_allowed = True - - -class PersonMessageReceived(BaseEventModel): - """收到任何私聊消息时""" - - launcher_type: str - """发起对象类型(group/person)""" - - launcher_id: typing.Union[int, str] - """发起对象ID(群号/QQ号)""" - - sender_id: typing.Union[int, str] - """发送者ID(QQ号)""" - - message_chain: platform_message.MessageChain - - -class GroupMessageReceived(BaseEventModel): - """收到任何群聊消息时""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - message_chain: platform_message.MessageChain - - -class PersonNormalMessageReceived(BaseEventModel): - """判断为应该处理的私聊普通消息时触发""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - text_message: str - - alter: typing.Optional[str] = None - """修改后的消息文本""" - - reply: typing.Optional[list] = None - """回复消息组件列表""" - - -class PersonCommandSent(BaseEventModel): - """判断为应该处理的私聊命令时触发""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - command: str - - params: list[str] - - text_message: str - - is_admin: bool - - alter: typing.Optional[str] = None - """修改后的完整命令文本""" - - reply: typing.Optional[list] = None - """回复消息组件列表""" - - -class GroupNormalMessageReceived(BaseEventModel): - """判断为应该处理的群聊普通消息时触发""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - text_message: str - - alter: typing.Optional[str] = None - """修改后的消息文本""" - - reply: typing.Optional[list] = None - """回复消息组件列表""" - - -class GroupCommandSent(BaseEventModel): - """判断为应该处理的群聊命令时触发""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - command: str - - params: list[str] - - text_message: str - - is_admin: bool - - alter: typing.Optional[str] = None - """修改后的完整命令文本""" - - reply: typing.Optional[list] = None - """回复消息组件列表""" - - -class NormalMessageResponded(BaseEventModel): - """回复普通消息时触发""" - - launcher_type: str - - launcher_id: typing.Union[int, str] - - sender_id: typing.Union[int, str] - - session: provider_session.Session - """会话对象""" - - prefix: str - """回复消息的前缀""" - - response_text: str - """回复消息的文本""" - - finish_reason: str - """响应结束原因""" - - funcs_called: list[str] - """调用的函数列表""" - - reply: typing.Optional[list] = None - """回复消息组件列表""" - - -class PromptPreProcessing(BaseEventModel): - """会话中的Prompt预处理时触发""" - - session_name: str - - default_prompt: list[llm_entities.Message] - """此对话的情景预设,可修改""" - - prompt: list[llm_entities.Message] - """此对话现有消息记录,可修改""" diff --git a/pkg/provider/entities.py b/pkg/provider/entities.py deleted file mode 100644 index b03ece38..00000000 --- a/pkg/provider/entities.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations - -import typing -import pydantic - -from pkg.provider import entities - - -import langbot_plugin.api.entities.builtin.platform.message as platform_message - - -class FunctionCall(pydantic.BaseModel): - name: str - - arguments: str - - -class ToolCall(pydantic.BaseModel): - id: str - - type: str - - function: FunctionCall - - -class ImageURLContentObject(pydantic.BaseModel): - url: str - - def __str__(self): - return self.url[:128] + ('...' if len(self.url) > 128 else '') - - -class ContentElement(pydantic.BaseModel): - type: str - """内容类型""" - - text: typing.Optional[str] = None - - image_url: typing.Optional[ImageURLContentObject] = None - - image_base64: typing.Optional[str] = None - - def __str__(self): - if self.type == 'text': - return self.text - elif self.type == 'image_url': - return f'[图片]({self.image_url})' - else: - return '未知内容' - - @classmethod - def from_text(cls, text: str): - return cls(type='text', text=text) - - @classmethod - def from_image_url(cls, image_url: str): - return cls(type='image_url', image_url=ImageURLContentObject(url=image_url)) - - @classmethod - def from_image_base64(cls, image_base64: str): - return cls(type='image_base64', image_base64=image_base64) - - -class Message(pydantic.BaseModel): - """消息""" - - role: str # user, system, assistant, tool, command, plugin - """消息的角色""" - - name: typing.Optional[str] = None - """名称,仅函数调用返回时设置""" - - content: typing.Optional[list[ContentElement]] | typing.Optional[str] = None - """内容""" - - tool_calls: typing.Optional[list[ToolCall]] = None - """工具调用""" - - tool_call_id: typing.Optional[str] = None - - def readable_str(self) -> str: - if self.content is not None: - return str(self.role) + ': ' + str(self.get_content_platform_message_chain()) - elif self.tool_calls is not None: - return f'调用工具: {self.tool_calls[0].id}' - else: - return '未知消息' - - def get_content_platform_message_chain(self, prefix_text: str = '') -> platform_message.MessageChain | None: - """将内容转换为平台消息 MessageChain 对象 - - Args: - prefix_text (str): 首个文字组件的前缀文本 - """ - - if self.content is None: - return None - elif isinstance(self.content, str): - return platform_message.MessageChain([platform_message.Plain(text=(prefix_text + self.content))]) - elif isinstance(self.content, list): - mc = [] - for ce in self.content: - if ce.type == 'text': - mc.append(platform_message.Plain(ce.text)) - elif ce.type == 'image_url': - if ce.image_url.url.startswith('http'): - mc.append(platform_message.Image(url=ce.image_url.url)) - else: # base64 - b64_str = ce.image_url.url - - if b64_str.startswith('data:'): - b64_str = b64_str.split(',')[1] - - mc.append(platform_message.Image(base64=b64_str)) - - # 找第一个文字组件 - if prefix_text: - for i, c in enumerate(mc): - if isinstance(c, platform_message.Plain): - mc[i] = platform_message.Plain(prefix_text + c.text) - break - else: - mc.insert(0, platform_message.Plain(prefix_text)) - - return platform_message.MessageChain(mc) - - -class Prompt(pydantic.BaseModel): - """供AI使用的Prompt""" - - name: str - """名称""" - - messages: list[entities.Message] - """消息列表""" diff --git a/pkg/provider/modelmgr/requester.py b/pkg/provider/modelmgr/requester.py index b8443b2c..1620dac7 100644 --- a/pkg/provider/modelmgr/requester.py +++ b/pkg/provider/modelmgr/requester.py @@ -4,11 +4,11 @@ import abc import typing from ...core import app -from .. import entities as llm_entities from ...entity.persistence import model as persistence_model import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from . import token import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class RuntimeLLMModel: @@ -58,10 +58,10 @@ class LLMAPIRequester(metaclass=abc.ABCMeta): self, query: pipeline_query.Query, model: RuntimeLLMModel, - messages: typing.List[llm_entities.Message], + messages: typing.List[provider_message.Message], funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: """调用API Args: diff --git a/pkg/provider/modelmgr/requesters/anthropicmsgs.py b/pkg/provider/modelmgr/requesters/anthropicmsgs.py index 1a100ca3..880c61bd 100644 --- a/pkg/provider/modelmgr/requesters/anthropicmsgs.py +++ b/pkg/provider/modelmgr/requesters/anthropicmsgs.py @@ -9,10 +9,10 @@ import httpx from .. import errors, requester -from ... import entities as llm_entities from ....utils import image import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class AnthropicMessages(requester.LLMAPIRequester): @@ -50,10 +50,10 @@ class AnthropicMessages(requester.LLMAPIRequester): self, query: pipeline_query.Query, model: requester.RuntimeLLMModel, - messages: typing.List[llm_entities.Message], + messages: typing.List[provider_message.Message], funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = model.token_mgr.get_token() args = extra_args.copy() @@ -73,7 +73,7 @@ class AnthropicMessages(requester.LLMAPIRequester): if system_role_message: messages.pop(i) - if isinstance(system_role_message, llm_entities.Message) and isinstance(system_role_message.content, str): + if isinstance(system_role_message, provider_message.Message) and isinstance(system_role_message.content, str): args['system'] = system_role_message.content req_messages = [] @@ -157,16 +157,16 @@ class AnthropicMessages(requester.LLMAPIRequester): args['content'] += block.text elif block.type == 'tool_use': assert type(block) is anthropic.types.tool_use_block.ToolUseBlock - tool_call = llm_entities.ToolCall( + tool_call = provider_message.ToolCall( id=block.id, type='function', - function=llm_entities.FunctionCall(name=block.name, arguments=json.dumps(block.input)), + function=provider_message.FunctionCall(name=block.name, arguments=json.dumps(block.input)), ) if 'tool_calls' not in args: args['tool_calls'] = [] args['tool_calls'].append(tool_call) - return llm_entities.Message(**args) + return provider_message.Message(**args) except anthropic.AuthenticationError as e: raise errors.RequesterError(f'api-key 无效: {e.message}') except anthropic.BadRequestError as e: diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 944e0eef..78f427b8 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -8,9 +8,9 @@ import openai.types.chat.chat_completion as chat_completion import httpx from .. import errors, requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class OpenAIChatCompletions(requester.LLMAPIRequester): @@ -41,7 +41,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, - ) -> llm_entities.Message: + ) -> provider_message.Message: chatcmpl_message = chat_completion.choices[0].message.model_dump() # 确保 role 字段存在且不为 None @@ -54,7 +54,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): if reasoning_content is not None: chatcmpl_message['content'] = '\n' + reasoning_content + '\n\n' + chatcmpl_message['content'] - message = llm_entities.Message(**chatcmpl_message) + message = provider_message.Message(**chatcmpl_message) return message @@ -65,7 +65,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -103,10 +103,10 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): self, query: pipeline_query.Query, model: requester.RuntimeLLMModel, - messages: typing.List[llm_entities.Message], + messages: typing.List[provider_message.Message], funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) diff --git a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py index ecf7a697..95c9fbe2 100644 --- a/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/deepseekchatcmpl.py @@ -4,9 +4,9 @@ import typing from . import chatcmpl from .. import errors, requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -24,7 +24,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} diff --git a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py index 9828e2ca..38d8931e 100644 --- a/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py +++ b/pkg/provider/modelmgr/requesters/giteeaichatcmpl.py @@ -5,9 +5,9 @@ import typing from . import chatcmpl from .. import requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -25,7 +25,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} diff --git a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py index 68eb7399..657d2ab8 100644 --- a/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py +++ b/pkg/provider/modelmgr/requesters/modelscopechatcmpl.py @@ -9,9 +9,9 @@ import openai.types.chat.chat_completion_message_tool_call as chat_completion_me import httpx from .. import entities, errors, requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class ModelScopeChatCompletions(requester.LLMAPIRequester): @@ -112,14 +112,14 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): async def _make_msg( self, chat_completion: chat_completion.ChatCompletion, - ) -> llm_entities.Message: + ) -> provider_message.Message: chatcmpl_message = chat_completion.choices[0].message.dict() # 确保 role 字段存在且不为 None if 'role' not in chatcmpl_message or chatcmpl_message['role'] is None: chatcmpl_message['role'] = 'assistant' - message = llm_entities.Message(**chatcmpl_message) + message = provider_message.Message(**chatcmpl_message) return message @@ -130,7 +130,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} @@ -168,10 +168,10 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester): self, query: pipeline_query.Query, model: entities.LLMModelInfo, - messages: typing.List[llm_entities.Message], + messages: typing.List[provider_message.Message], funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行 for m in messages: msg_dict = m.dict(exclude_none=True) diff --git a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py index 20c3427c..1f67fb1e 100644 --- a/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py +++ b/pkg/provider/modelmgr/requesters/moonshotchatcmpl.py @@ -5,9 +5,9 @@ import typing from . import chatcmpl from .. import requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): @@ -25,7 +25,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: self.client.api_key = use_model.token_mgr.get_token() args = {} diff --git a/pkg/provider/modelmgr/requesters/ollamachat.py b/pkg/provider/modelmgr/requesters/ollamachat.py index b22895a6..20ba3ebf 100644 --- a/pkg/provider/modelmgr/requesters/ollamachat.py +++ b/pkg/provider/modelmgr/requesters/ollamachat.py @@ -10,9 +10,9 @@ import json import ollama from .. import errors, requester -from ... import entities as llm_entities import langbot_plugin.api.entities.builtin.resource.tool as resource_tool import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message REQUESTER_NAME: str = 'ollama-chat' @@ -44,7 +44,7 @@ class OllamaChatCompletions(requester.LLMAPIRequester): use_model: requester.RuntimeLLMModel, use_funcs: list[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: args = extra_args.copy() args['model'] = use_model.model_entity.name @@ -73,27 +73,27 @@ class OllamaChatCompletions(requester.LLMAPIRequester): args['tools'] = tools resp = await self._req(args) - message: llm_entities.Message = await self._make_msg(resp) + message: provider_message.Message = await self._make_msg(resp) return message - async def _make_msg(self, chat_completions: ollama.ChatResponse) -> llm_entities.Message: + async def _make_msg(self, chat_completions: ollama.ChatResponse) -> provider_message.Message: message: ollama.Message = chat_completions.message if message is None: raise ValueError("chat_completions must contain a 'message' field") - ret_msg: llm_entities.Message = None + ret_msg: provider_message.Message = None if message.content is not None: - ret_msg = llm_entities.Message(role='assistant', content=message.content) + ret_msg = provider_message.Message(role='assistant', content=message.content) if message.tool_calls is not None and len(message.tool_calls) > 0: - tool_calls: list[llm_entities.ToolCall] = [] + tool_calls: list[provider_message.ToolCall] = [] for tool_call in message.tool_calls: tool_calls.append( - llm_entities.ToolCall( + provider_message.ToolCall( id=uuid.uuid4().hex, type='function', - function=llm_entities.FunctionCall( + function=provider_message.FunctionCall( name=tool_call.function.name, arguments=json.dumps(tool_call.function.arguments), ), @@ -107,10 +107,10 @@ class OllamaChatCompletions(requester.LLMAPIRequester): self, query: pipeline_query.Query, model: requester.RuntimeLLMModel, - messages: typing.List[llm_entities.Message], + messages: typing.List[provider_message.Message], funcs: typing.List[resource_tool.LLMTool] = None, extra_args: dict[str, typing.Any] = {}, - ) -> llm_entities.Message: + ) -> provider_message.Message: req_messages: list = [] for m in messages: msg_dict: dict = m.dict(exclude_none=True) diff --git a/pkg/provider/runner.py b/pkg/provider/runner.py index 42f702f8..1fff4a76 100644 --- a/pkg/provider/runner.py +++ b/pkg/provider/runner.py @@ -4,7 +4,7 @@ import abc import typing from ..core import app -from . import entities as llm_entities +import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query @@ -36,6 +36,6 @@ class RequestRunner(abc.ABC): self.pipeline_config = pipeline_config @abc.abstractmethod - async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """运行请求""" pass diff --git a/pkg/provider/runners/dashscopeapi.py b/pkg/provider/runners/dashscopeapi.py index 7c71d6b3..98d504e2 100644 --- a/pkg/provider/runners/dashscopeapi.py +++ b/pkg/provider/runners/dashscopeapi.py @@ -7,8 +7,8 @@ import dashscope from .. import runner from ...core import app -from .. import entities as llm_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class DashscopeAPIError(Exception): @@ -90,7 +90,9 @@ class DashScopeAPIRunner(runner.RequestRunner): return plain_text, image_ids - async def _agent_messages(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _agent_messages( + self, query: pipeline_query.Query + ) -> typing.AsyncGenerator[provider_message.Message, None]: """Dashscope 智能体对话请求""" # 局部变量 @@ -143,14 +145,14 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=pending_content, ) async def _workflow_messages( self, query: pipeline_query.Query - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + ) -> typing.AsyncGenerator[provider_message.Message, None]: """Dashscope 工作流对话请求""" # 局部变量 @@ -208,12 +210,12 @@ class DashScopeAPIRunner(runner.RequestRunner): # 将参考资料替换到文本中 pending_content = self._replace_references(pending_content, references_dict) - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=pending_content, ) - async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """运行""" if self.app_type == 'agent': async for msg in self._agent_messages(query): diff --git a/pkg/provider/runners/difysvapi.py b/pkg/provider/runners/difysvapi.py index c5819de3..779f2ef6 100644 --- a/pkg/provider/runners/difysvapi.py +++ b/pkg/provider/runners/difysvapi.py @@ -9,7 +9,7 @@ import base64 from .. import runner from ...core import app -from .. import entities as llm_entities +import langbot_plugin.api.entities.builtin.provider.message as provider_message from ...utils import image import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query from libs.dify_service_api.v1 import client, errors @@ -90,7 +90,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): return plain_text, image_ids - async def _chat_messages(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _chat_messages( + self, query: pipeline_query.Query + ) -> typing.AsyncGenerator[provider_message.Message, None]: """调用聊天助手""" cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id @@ -132,7 +134,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): if mode == 'workflow': if chunk['event'] == 'node_finished': if chunk['data']['node_type'] == 'answer': - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=self._try_convert_thinking(chunk['data']['outputs']['answer']), ) @@ -140,7 +142,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['event'] == 'message': basic_mode_pending_chunk += chunk['answer'] elif chunk['event'] == 'message_end': - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=self._try_convert_thinking(basic_mode_pending_chunk), ) @@ -153,7 +155,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): async def _agent_chat_messages( self, query: pipeline_query.Query - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + ) -> typing.AsyncGenerator[provider_message.Message, None]: """调用聊天助手""" cov_id = query.session.using_conversation.uuid or '' query.variables['conversation_id'] = cov_id @@ -198,7 +200,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): else: if pending_agent_message.strip() != '': pending_agent_message = pending_agent_message.replace('Action:', '') - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=self._try_convert_thinking(pending_agent_message), ) @@ -209,13 +211,13 @@ class DifyServiceAPIRunner(runner.RequestRunner): continue if chunk['tool']: - msg = llm_entities.Message( + msg = provider_message.Message( role='assistant', tool_calls=[ - llm_entities.ToolCall( + provider_message.ToolCall( id=chunk['id'], type='function', - function=llm_entities.FunctionCall( + function=provider_message.FunctionCall( name=chunk['tool'], arguments=json.dumps({}), ), @@ -232,9 +234,9 @@ class DifyServiceAPIRunner(runner.RequestRunner): image_url = base_url + chunk['url'] - yield llm_entities.Message( + yield provider_message.Message( role='assistant', - content=[llm_entities.ContentElement.from_image_url(image_url)], + content=[provider_message.ContentElement.from_image_url(image_url)], ) if chunk['event'] == 'error': raise errors.DifyAPIError('dify 服务错误: ' + chunk['message']) @@ -246,7 +248,7 @@ class DifyServiceAPIRunner(runner.RequestRunner): async def _workflow_messages( self, query: pipeline_query.Query - ) -> typing.AsyncGenerator[llm_entities.Message, None]: + ) -> typing.AsyncGenerator[provider_message.Message, None]: """调用工作流""" if not query.session.using_conversation.uuid: @@ -290,14 +292,14 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['data']['node_type'] == 'start' or chunk['data']['node_type'] == 'end': continue - msg = llm_entities.Message( + msg = provider_message.Message( role='assistant', content=None, tool_calls=[ - llm_entities.ToolCall( + provider_message.ToolCall( id=chunk['data']['node_id'], type='function', - function=llm_entities.FunctionCall( + function=provider_message.FunctionCall( name=chunk['data']['title'], arguments=json.dumps({}), ), @@ -311,14 +313,14 @@ class DifyServiceAPIRunner(runner.RequestRunner): if chunk['data']['error']: raise errors.DifyAPIError(chunk['data']['error']) - msg = llm_entities.Message( + msg = provider_message.Message( role='assistant', content=chunk['data']['outputs']['summary'], ) yield msg - async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """运行请求""" if self.pipeline_config['ai']['dify-service-api']['app-type'] == 'chat': async for msg in self._chat_messages(query): diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 950f7756..da88d490 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -4,15 +4,15 @@ import json import typing from .. import runner -from .. import entities as llm_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message @runner.runner_class('local-agent') class LocalAgentRunner(runner.RequestRunner): """本地Agent请求运行器""" - async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """运行请求""" pending_tool_calls = [] @@ -45,7 +45,7 @@ class LocalAgentRunner(runner.RequestRunner): func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters) - msg = llm_entities.Message( + msg = provider_message.Message( role='tool', content=json.dumps(func_ret, ensure_ascii=False), tool_call_id=tool_call.id, @@ -56,7 +56,7 @@ class LocalAgentRunner(runner.RequestRunner): req_messages.append(msg) except Exception as e: # 工具调用出错,添加一个报错信息到 req_messages - err_msg = llm_entities.Message(role='tool', content=f'err: {e}', tool_call_id=tool_call.id) + err_msg = provider_message.Message(role='tool', content=f'err: {e}', tool_call_id=tool_call.id) yield err_msg diff --git a/pkg/provider/runners/n8nsvapi.py b/pkg/provider/runners/n8nsvapi.py index 37567d15..d2b5aa78 100644 --- a/pkg/provider/runners/n8nsvapi.py +++ b/pkg/provider/runners/n8nsvapi.py @@ -7,8 +7,8 @@ import aiohttp from .. import runner from ...core import app -from .. import entities as llm_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query +import langbot_plugin.api.entities.builtin.provider.message as provider_message class N8nAPIError(Exception): @@ -68,7 +68,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): return plain_text - async def _call_webhook(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def _call_webhook(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """调用n8n webhook""" # 生成会话ID(如果不存在) if not query.session.using_conversation.uuid: @@ -146,7 +146,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): output_content = json.dumps(response_data, ensure_ascii=False) # 返回消息 - yield llm_entities.Message( + yield provider_message.Message( role='assistant', content=output_content, ) @@ -154,7 +154,7 @@ class N8nServiceAPIRunner(runner.RequestRunner): self.ap.logger.error(f'n8n webhook call exception: {str(e)}') raise N8nAPIError(f'n8n webhook call exception: {str(e)}') - async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[llm_entities.Message, None]: + async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]: """运行请求""" async for msg in self._call_webhook(query): yield msg From 214bc8ada9ff9802462bcd716fc8c4f19a509d98 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 20:45:45 +0800 Subject: [PATCH 24/78] feat: backward call apis --- pkg/api/http/service/bot.py | 16 +++++-- pkg/api/http/service/model.py | 12 ++++- pkg/persistence/mgr.py | 5 +- pkg/plugin/handler.py | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/pkg/api/http/service/bot.py b/pkg/api/http/service/bot.py index e5010007..a3987ba5 100644 --- a/pkg/api/http/service/bot.py +++ b/pkg/api/http/service/bot.py @@ -17,15 +17,19 @@ class BotService: def __init__(self, ap: app.Application) -> None: self.ap = ap - async def get_bots(self) -> list[dict]: + async def get_bots(self, include_secret: bool = True) -> list[dict]: """获取所有机器人""" result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot)) bots = result.all() - return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) for bot in bots] + masked_columns = [] + if not include_secret: + masked_columns = ['adapter_config'] - async def get_bot(self, bot_uuid: str) -> dict | None: + return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) for bot in bots] + + async def get_bot(self, bot_uuid: str, include_secret: bool = True) -> dict | None: """获取机器人""" result = await self.ap.persistence_mgr.execute_async( sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid) @@ -36,7 +40,11 @@ class BotService: if bot is None: return None - return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) + masked_columns = [] + if not include_secret: + masked_columns = ['adapter_config'] + + return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) async def create_bot(self, bot_data: dict) -> str: """创建机器人""" diff --git a/pkg/api/http/service/model.py b/pkg/api/http/service/model.py index 3dab181e..e04a50b2 100644 --- a/pkg/api/http/service/model.py +++ b/pkg/api/http/service/model.py @@ -16,11 +16,19 @@ class ModelsService: def __init__(self, ap: app.Application) -> None: self.ap = ap - async def get_llm_models(self) -> list[dict]: + async def get_llm_models(self, include_secret: bool = True) -> list[dict]: result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel)) models = result.all() - return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models] + + masked_columns = [] + if not include_secret: + masked_columns = ['api_keys'] + + return [ + self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns) + for model in models + ] async def create_llm_model(self, model_data: dict) -> str: model_data['uuid'] = str(uuid.uuid4()) diff --git a/pkg/persistence/mgr.py b/pkg/persistence/mgr.py index 23f08aa7..33f71769 100644 --- a/pkg/persistence/mgr.py +++ b/pkg/persistence/mgr.py @@ -128,10 +128,13 @@ class PersistenceManager: def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine: return self.db.get_engine() - def serialize_model(self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base) -> dict: + def serialize_model( + self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base, masked_columns: list[str] = [] + ) -> dict: return { column.name: getattr(data, column.name) if not isinstance(getattr(data, column.name), (datetime.datetime)) else getattr(data, column.name).isoformat() for column in model.__table__.columns + if column.name not in masked_columns } diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 77fc46bd..c3fd9fe1 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -14,10 +14,13 @@ from langbot_plugin.entities.io.actions.enums import ( PluginToRuntimeAction, ) import langbot_plugin.api.entities.builtin.platform.message as platform_message +import langbot_plugin.api.entities.builtin.provider.message as provider_message +import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from ..entity.persistence import plugin as persistence_plugin from ..core import app +from ..utils import constants class RuntimeConnectionHandler(handler.Handler): @@ -163,6 +166,92 @@ class RuntimeConnectionHandler(handler.Handler): }, ) + @self.action(PluginToRuntimeAction.GET_LANGBOT_VERSION) + async def get_langbot_version(data: dict[str, Any]) -> handler.ActionResponse: + """Get langbot version""" + return handler.ActionResponse.success( + data={ + 'version': constants.semantic_version, + }, + ) + + @self.action(PluginToRuntimeAction.GET_BOTS) + async def get_bots(data: dict[str, Any]) -> handler.ActionResponse: + """Get bots""" + bots = await self.ap.bot_service.get_bots(include_secret=False) + return handler.ActionResponse.success( + data={ + 'bots': bots, + }, + ) + + @self.action(PluginToRuntimeAction.SEND_MESSAGE) + async def send_message(data: dict[str, Any]) -> handler.ActionResponse: + """Send message""" + bot_uuid = data['bot_uuid'] + target_type = data['target_type'] + target_id = data['target_id'] + message_chain = data['message_chain'] + + message_chain_obj = platform_message.MessageChain.model_validate(message_chain) + + bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid) + if bot is None: + return handler.ActionResponse.error( + message=f'Bot with bot_uuid {bot_uuid} not found', + ) + + await bot.adapter.send_message( + target_type, + target_id, + message_chain_obj, + ) + + return handler.ActionResponse.success( + data={}, + ) + + @self.action(PluginToRuntimeAction.GET_LLM_MODELS) + async def get_llm_models(data: dict[str, Any]) -> handler.ActionResponse: + """Get llm models""" + llm_models = await self.ap.model_service.get_llm_models(include_secret=False) + return handler.ActionResponse.success( + data={ + 'llm_models': llm_models, + }, + ) + + @self.action(PluginToRuntimeAction.INVOKE_LLM) + async def invoke_llm(data: dict[str, Any]) -> handler.ActionResponse: + """Invoke llm""" + llm_model_uuid = data['llm_model_uuid'] + messages = data['messages'] + funcs = data.get('funcs', []) + extra_args = data.get('extra_args', {}) + + llm_model = await self.ap.model_mgr.get_model_by_uuid(llm_model_uuid) + if llm_model is None: + return handler.ActionResponse.error( + message=f'LLM model with llm_model_uuid {llm_model_uuid} not found', + ) + + messages_obj = [provider_message.Message.model_validate(message) for message in messages] + funcs_obj = [resource_tool.LLMTool.model_validate(func) for func in funcs] + + result = await llm_model.requester.invoke_llm( + query=None, + model=llm_model, + messages=messages_obj, + funcs=funcs_obj, + extra_args=extra_args, + ) + + return handler.ActionResponse.success( + data={ + 'message': result.model_dump(), + }, + ) + async def ping(self) -> dict[str, Any]: """Ping the runtime""" return await self.call_action( From 7237294008783c082152ffb485fdab0ea558dacb Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 20:48:15 +0800 Subject: [PATCH 25/78] perf: longer timeout for emit_event --- pkg/plugin/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index c3fd9fe1..8da1bc7f 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -280,7 +280,7 @@ class RuntimeConnectionHandler(handler.Handler): { 'event_context': event_context, }, - timeout=10, + timeout=30, ) return result @@ -324,7 +324,7 @@ class RuntimeConnectionHandler(handler.Handler): { 'command_context': command_context, }, - timeout=10, + timeout=30, ) async for ret in gen: From 65814a464433644e01d01200847c5d6030dd06c2 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 21:39:33 +0800 Subject: [PATCH 26/78] feat: binary storage api --- pkg/entity/persistence/bstorage.py | 22 +++++++ pkg/plugin/handler.py | 102 +++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 pkg/entity/persistence/bstorage.py diff --git a/pkg/entity/persistence/bstorage.py b/pkg/entity/persistence/bstorage.py new file mode 100644 index 00000000..674dee29 --- /dev/null +++ b/pkg/entity/persistence/bstorage.py @@ -0,0 +1,22 @@ +import sqlalchemy + +from .base import Base + + +class BinaryStorage(Base): + """Current for plugin use only""" + + __tablename__ = 'binary_storages' + + unique_key = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True) + key = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + owner_type = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + owner = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) + value = sqlalchemy.Column(sqlalchemy.LargeBinary, nullable=False) + 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/plugin/handler.py b/pkg/plugin/handler.py index 8da1bc7f..d0905f1f 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -2,6 +2,7 @@ from __future__ import annotations import typing from typing import Any +import base64 import sqlalchemy @@ -18,6 +19,7 @@ import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.builtin.resource.tool as resource_tool from ..entity.persistence import plugin as persistence_plugin +from ..entity.persistence import bstorage as persistence_bstorage from ..core import app from ..utils import constants @@ -252,6 +254,106 @@ class RuntimeConnectionHandler(handler.Handler): }, ) + @self.action(RuntimeToLangBotAction.SET_BINARY_STORAGE) + async def set_binary_storage(data: dict[str, Any]) -> handler.ActionResponse: + """Set binary storage""" + key = data['key'] + owner_type = data['owner_type'] + owner = data['owner'] + value = base64.b64decode(data['value_base64']) + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_bstorage.BinaryStorage) + .where(persistence_bstorage.BinaryStorage.key == key) + .where(persistence_bstorage.BinaryStorage.owner_type == owner_type) + .where(persistence_bstorage.BinaryStorage.owner == owner) + ) + + if result.first() is not None: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_bstorage.BinaryStorage) + .where(persistence_bstorage.BinaryStorage.key == key) + .where(persistence_bstorage.BinaryStorage.owner_type == owner_type) + .where(persistence_bstorage.BinaryStorage.owner == owner) + .values(value=value) + ) + else: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_bstorage.BinaryStorage).values( + unique_key=f'{owner_type}:{owner}:{key}', + key=key, + owner_type=owner_type, + owner=owner, + value=value, + ) + ) + + return handler.ActionResponse.success( + data={}, + ) + + @self.action(RuntimeToLangBotAction.GET_BINARY_STORAGE) + async def get_binary_storage(data: dict[str, Any]) -> handler.ActionResponse: + """Get binary storage""" + key = data['key'] + owner_type = data['owner_type'] + owner = data['owner'] + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_bstorage.BinaryStorage) + .where(persistence_bstorage.BinaryStorage.key == key) + .where(persistence_bstorage.BinaryStorage.owner_type == owner_type) + .where(persistence_bstorage.BinaryStorage.owner == owner) + ) + + storage = result.first() + if storage is None: + return handler.ActionResponse.error( + message=f'Storage with key {key} not found', + ) + + return handler.ActionResponse.success( + data={ + 'value_base64': base64.b64encode(storage.value).decode('utf-8'), + }, + ) + + @self.action(RuntimeToLangBotAction.DELETE_BINARY_STORAGE) + async def delete_binary_storage(data: dict[str, Any]) -> handler.ActionResponse: + """Delete binary storage""" + key = data['key'] + owner_type = data['owner_type'] + owner = data['owner'] + + await self.ap.persistence_mgr.execute_async( + sqlalchemy.delete(persistence_bstorage.BinaryStorage) + .where(persistence_bstorage.BinaryStorage.key == key) + .where(persistence_bstorage.BinaryStorage.owner_type == owner_type) + .where(persistence_bstorage.BinaryStorage.owner == owner) + ) + + return handler.ActionResponse.success( + data={}, + ) + + @self.action(RuntimeToLangBotAction.GET_BINARY_STORAGE_KEYS) + async def get_binary_storage_keys(data: dict[str, Any]) -> handler.ActionResponse: + """Get binary storage keys""" + owner_type = data['owner_type'] + owner = data['owner'] + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_bstorage.BinaryStorage.key) + .where(persistence_bstorage.BinaryStorage.owner_type == owner_type) + .where(persistence_bstorage.BinaryStorage.owner == owner) + ) + + return handler.ActionResponse.success( + data={ + 'keys': result.scalars().all(), + }, + ) + async def ping(self) -> dict[str, Any]: """Ping the runtime""" return await self.call_action( From 4380041c7f01067627496627169b6bbfdd64348c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 22:03:47 +0800 Subject: [PATCH 27/78] feat(ui): list plugins --- .../PluginInstalledComponent.tsx | 19 +++++++---- .../plugin-form/PluginForm.tsx | 25 ++++++++++---- web/src/app/infra/entities/api/index.ts | 33 ++++++++++--------- web/src/app/infra/entities/common.ts | 15 +++++++++ web/src/app/infra/entities/plugin/index.ts | 17 ++++++++++ 5 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 web/src/app/infra/entities/plugin/index.ts diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 3e5dd9b5..b108b312 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -43,15 +43,20 @@ const PluginInstalledComponent = forwardRef( setPluginList( value.plugins.map((plugin) => { return new PluginCardVO({ - author: plugin.author, - description: i18nObj(plugin.description), + author: plugin.manifest.manifest.metadata.author ?? '', + description: i18nObj( + plugin.manifest.manifest.metadata.description ?? { + en_US: '', + zh_Hans: '', + }, + ), enabled: plugin.enabled, - name: plugin.name, - version: plugin.version, + name: plugin.manifest.manifest.metadata.name, + version: plugin.manifest.manifest.metadata.version ?? '', status: plugin.status, - tools: plugin.tools, - event_handlers: plugin.event_handlers, - repository: plugin.repository, + tools: [], + event_handlers: {}, + repository: plugin.manifest.manifest.metadata.repository ?? '', priority: plugin.priority, }); }), diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 7ffd6796..0e7b5ac5 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import { ApiRespPluginConfig, Plugin } from '@/app/infra/entities/api'; +import { ApiRespPluginConfig } from '@/app/infra/entities/api'; +import { Plugin } from '@/app/infra/entities/plugin'; import { httpClient } from '@/app/infra/http/HttpClient'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import { Button } from '@/components/ui/button'; @@ -183,13 +184,22 @@ export default function PluginForm({
-
{pluginInfo.name}
-
- {i18nObj(pluginInfo.description)} +
+ {i18nObj(pluginInfo.manifest.manifest.metadata.label)}
- {pluginInfo.config_schema.length > 0 && ( +
+ {i18nObj( + pluginInfo.manifest.manifest.metadata.description ?? { + en_US: '', + zh_Hans: '', + }, + )} +
+ {/* @ts-ignore */} + {pluginInfo.manifest.manifest.spec.config.length > 0 && ( } onSubmit={(values) => { let config = pluginConfig.config; @@ -203,7 +213,8 @@ export default function PluginForm({ }} /> )} - {pluginInfo.config_schema.length === 0 && ( + {/* @ts-ignore */} + {pluginInfo.manifest.manifest.spec.config.length === 0 && (
{t('plugins.pluginNoConfig')}
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index d86a8be0..c49ab861 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -2,6 +2,7 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { PipelineConfigTab } from '@/app/infra/entities/pipeline'; import { I18nLabel } from '@/app/infra/entities/common'; import { Message } from '@/app/infra/entities/message'; +import { Plugin } from '@/app/infra/entities/plugin'; export interface ApiResponse { code: number; @@ -119,22 +120,22 @@ export interface ApiRespPlugin { plugin: Plugin; } -export interface Plugin { - author: string; - name: string; - description: I18nLabel; - label: I18nLabel; - version: string; - enabled: boolean; - priority: number; - status: string; - tools: object[]; - event_handlers: object; - main_file: string; - pkg_path: string; - repository: string; - config_schema: IDynamicFormItemSchema[]; -} +// export interface Plugin { +// author: string; +// name: string; +// description: I18nLabel; +// label: I18nLabel; +// version: string; +// enabled: boolean; +// priority: number; +// status: string; +// tools: object[]; +// event_handlers: object; +// main_file: string; +// pkg_path: string; +// repository: string; +// config_schema: IDynamicFormItemSchema[]; +// } export interface ApiRespPluginConfig { config: object; diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts index 02cd99f8..9805d377 100644 --- a/web/src/app/infra/entities/common.ts +++ b/web/src/app/infra/entities/common.ts @@ -3,3 +3,18 @@ export interface I18nLabel { zh_Hans: string; ja_JP?: string; } + +export interface ComponentManifest { + apiVersion: string; + kind: string; + metadata: { + name: string; + label: I18nLabel; + description?: I18nLabel; + icon?: string; + repository?: string; + version?: string; + author?: string; + }; + spec: object; +} diff --git a/web/src/app/infra/entities/plugin/index.ts b/web/src/app/infra/entities/plugin/index.ts new file mode 100644 index 00000000..96469107 --- /dev/null +++ b/web/src/app/infra/entities/plugin/index.ts @@ -0,0 +1,17 @@ +import { ComponentManifest } from '@/app/infra/entities/common'; + +export interface Plugin { + status: 'intialized' | 'mounted' | 'unmounted'; + priority: number; + plugin_config: object; + manifest: { + manifest: ComponentManifest; + }; + enabled: boolean; + components: { + component_config: object; + manifest: { + manifest: ComponentManifest; + }; + }; +} From e0abd196362dd6700ea7cc154eb32ab1994af346 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 13 Jul 2025 22:14:22 +0800 Subject: [PATCH 28/78] feat: get plugin info --- pkg/api/http/controller/groups/plugins.py | 8 ++++---- pkg/plugin/connector.py | 3 +++ pkg/plugin/handler.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 86ad25e8..8f59986d 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -50,10 +50,10 @@ class PluginsRouterGroup(group.RouterGroup): ) async def _(author: str, plugin_name: str) -> str: if quart.request.method == 'GET': - plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) + plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name) if plugin is None: return self.http_status(404, -1, 'plugin not found') - return self.success(data={'plugin': plugin.model_dump()}) + return self.success(data={'plugin': plugin}) elif quart.request.method == 'DELETE': ctx = taskmgr.TaskContext.new() wrapper = self.ap.task_mgr.create_user_task( @@ -72,11 +72,11 @@ class PluginsRouterGroup(group.RouterGroup): 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) + plugin = await self.ap.plugin_connector.get_plugin_info(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}) + return self.success(data={'config': plugin['plugin_config']}) elif quart.request.method == 'PUT': data = await quart.request.json diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index b46302d5..370b7618 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -98,6 +98,9 @@ class PluginRuntimeConnector: async def list_plugins(self) -> list[dict[str, Any]]: return await self.handler.list_plugins() + async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]: + return await self.handler.get_plugin_info(author, plugin_name) + async def emit_event( self, event: events.BaseEventModel, diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index d0905f1f..22cfe5c9 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -372,6 +372,18 @@ class RuntimeConnectionHandler(handler.Handler): return result['plugins'] + async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]: + """Get plugin""" + result = await self.call_action( + LangBotToRuntimeAction.GET_PLUGIN_INFO, + { + 'author': author, + 'plugin_name': plugin_name, + }, + timeout=10, + ) + return result['plugin'] + async def emit_event( self, event_context: dict[str, Any], From f5b893cfe0a11b1f56e99ebc13147db6a954af55 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 16 Jul 2025 22:43:39 +0800 Subject: [PATCH 29/78] feat: kill runtime process when exit in stdio mode --- pkg/core/app.py | 3 +++ pkg/core/boot.py | 2 +- pkg/plugin/connector.py | 14 ++++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pkg/core/app.py b/pkg/core/app.py index 4b3e3b82..cd140413 100644 --- a/pkg/core/app.py +++ b/pkg/core/app.py @@ -156,6 +156,9 @@ class Application: self.logger.error(f'应用运行致命异常: {e}') self.logger.debug(f'Traceback: {traceback.format_exc()}') + def dispose(self): + self.plugin_connector.dispose() + async def print_web_access_info(self): """打印访问 webui 的提示""" diff --git a/pkg/core/boot.py b/pkg/core/boot.py index aff117e6..548a203e 100644 --- a/pkg/core/boot.py +++ b/pkg/core/boot.py @@ -52,7 +52,7 @@ async def main(loop: asyncio.AbstractEventLoop): def signal_handler(sig, frame): print('[Signal] 程序退出.') - # ap.shutdown() + app_inst.dispose() os._exit(0) signal.signal(signal.SIGINT, signal_handler) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 370b7618..c17de9ae 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -30,6 +30,8 @@ class PluginRuntimeConnector: stdio_client_controller: stdio_client_controller.StdioClientController + ctrl: stdio_client_controller.StdioClientController | ws_client_controller.WebSocketClientController + runtime_disconnect_callback: typing.Callable[ [PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None] ] @@ -73,22 +75,22 @@ class PluginRuntimeConnector: self.ap.logger.error('Failed to connect to plugin runtime, trying to reconnect...') await self.runtime_disconnect_callback(self) - ctrl = ws_client_controller.WebSocketClientController( + self.ctrl = ws_client_controller.WebSocketClientController( ws_url=ws_url, make_connection_failed_callback=make_connection_failed_callback, ) - task = ctrl.run(new_connection_callback) + task = self.ctrl.run(new_connection_callback) else: # stdio self.ap.logger.info('use stdio to connect to plugin runtime') # cmd: lbp rt -s python_path = sys.executable env = os.environ.copy() - ctrl = stdio_client_controller.StdioClientController( + self.ctrl = stdio_client_controller.StdioClientController( command=python_path, args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'], env=env, ) - task = ctrl.run(new_connection_callback) + task = self.ctrl.run(new_connection_callback) asyncio.create_task(task) @@ -135,3 +137,7 @@ class PluginRuntimeConnector: cmd_ret = command_context.CommandReturn.model_validate(ret) yield cmd_ret + + def dispose(self): + if isinstance(self.ctrl, stdio_client_controller.StdioClientController): + self.ctrl.process.terminate() From 41650b585a7305d6481975b7aaa8012bf2c7159a Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 2 Aug 2025 23:54:06 +0800 Subject: [PATCH 30/78] perf: dispose process --- pkg/core/boot.py | 2 +- pkg/plugin/connector.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/core/boot.py b/pkg/core/boot.py index 548a203e..8aa403ad 100644 --- a/pkg/core/boot.py +++ b/pkg/core/boot.py @@ -51,8 +51,8 @@ async def main(loop: asyncio.AbstractEventLoop): import signal def signal_handler(sig, frame): - print('[Signal] 程序退出.') app_inst.dispose() + print('[Signal] 程序退出.') os._exit(0) signal.signal(signal.SIGINT, signal_handler) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index c17de9ae..7103c13e 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -140,4 +140,5 @@ class PluginRuntimeConnector: def dispose(self): if isinstance(self.ctrl, stdio_client_controller.StdioClientController): + self.ap.logger.info('Terminating plugin runtime process...') self.ctrl.process.terminate() From 0b60ef0d06c56a003edd918d80e3a09a0cf94081 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 9 Aug 2025 21:06:31 +0800 Subject: [PATCH 31/78] chore: bump langbot-plugin version to 0.1.1a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 40f172c0..c371f1c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.0a6", + "langbot-plugin==0.1.1a1", ] keywords = [ "bot", From 621f1301b347b4986a764392b245db42c972f74d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 11 Aug 2025 17:24:57 +0800 Subject: [PATCH 32/78] fix: message chain init --- pkg/pipeline/wrapper/wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/pipeline/wrapper/wrapper.py b/pkg/pipeline/wrapper/wrapper.py index f3323622..439595e9 100644 --- a/pkg/pipeline/wrapper/wrapper.py +++ b/pkg/pipeline/wrapper/wrapper.py @@ -97,7 +97,7 @@ class ResponseWrapper(stage.PipelineStage): reply_text = f'调用函数 {".".join(function_names)}...' query.resp_message_chain.append( - platform_message.MessageChain([platform_message.Plain(reply_text)]) + platform_message.MessageChain([platform_message.Plain(text=reply_text)]) ) if query.pipeline_config['output']['misc']['track-function-calls']: @@ -125,12 +125,12 @@ class ResponseWrapper(stage.PipelineStage): else: if event_ctx.event.reply is not None: query.resp_message_chain.append( - platform_message.MessageChain(event_ctx.event.reply) + platform_message.MessageChain(text=event_ctx.event.reply) ) else: query.resp_message_chain.append( - platform_message.MessageChain([platform_message.Plain(reply_text)]) + platform_message.MessageChain([platform_message.Plain(text=reply_text)]) ) yield entities.StageProcessResult( From 39c50d3c120bfbb2f8cc78b6c88cc0e45cd8ba1d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 13 Aug 2025 20:54:43 +0800 Subject: [PATCH 33/78] feat: `get_bot_info` api --- pkg/api/http/service/bot.py | 12 +++++++++++- pkg/plugin/handler.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pkg/api/http/service/bot.py b/pkg/api/http/service/bot.py index a3987ba5..0bdb8e68 100644 --- a/pkg/api/http/service/bot.py +++ b/pkg/api/http/service/bot.py @@ -44,7 +44,17 @@ class BotService: if not include_secret: masked_columns = ['adapter_config'] - return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) + adapter_runtime_values = {} + + runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid) + if runtime_bot is not None: + adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id + + persistence_bot_data = self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) + + persistence_bot_data['adapter_runtime_values'] = adapter_runtime_values + + return persistence_bot_data async def create_bot(self, bot_data: dict) -> str: """创建机器人""" diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 22cfe5c9..7441ae5c 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -187,6 +187,17 @@ class RuntimeConnectionHandler(handler.Handler): }, ) + @self.action(PluginToRuntimeAction.GET_BOT_INFO) + async def get_bot_info(data: dict[str, Any]) -> handler.ActionResponse: + """Get bot info""" + bot_uuid = data['bot_uuid'] + bot = await self.ap.bot_service.get_bot(bot_uuid, include_secret=False) + return handler.ActionResponse.success( + data={ + 'bot': bot, + }, + ) + @self.action(PluginToRuntimeAction.SEND_MESSAGE) async def send_message(data: dict[str, Any]) -> handler.ActionResponse: """Send message""" From ebe0b68e8fb6a7dc35b04f158de103288216c4fe Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 14 Aug 2025 23:42:57 +0800 Subject: [PATCH 34/78] feat: set cloud_service_url --- pkg/api/http/controller/groups/system.py | 11 ++++++++++- templates/config.yaml | 3 ++- web/src/app/infra/entities/api/index.ts | 1 + web/src/app/infra/http/HttpClient.ts | 19 ++++++++++--------- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 2db910b9..9e85e3ad 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -14,6 +14,11 @@ class SystemRouterGroup(group.RouterGroup): 'version': constants.semantic_version, 'debug': constants.debug_mode, 'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()), + 'cloud_service_url': ( + self.ap.instance_config.data['plugin']['cloud_service_url'] + if 'cloud_service_url' in self.ap.instance_config.data['plugin'] + else 'https://space.langbot.app' + ), } ) @@ -57,7 +62,11 @@ class SystemRouterGroup(group.RouterGroup): data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters']) ) - @self.route('/debug/plugin/action', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route( + '/debug/plugin/action', + methods=['POST'], + auth_type=group.AuthType.USER_TOKEN, + ) async def _() -> str: if not constants.debug_mode: return self.http_status(403, 403, 'Forbidden') diff --git a/templates/config.yaml b/templates/config.yaml index c896417c..dd35e39c 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -19,4 +19,5 @@ system: expire: 604800 secret: '' plugin: - runtime_ws_url: 'ws://plugin-runtime:5400/control/ws' \ No newline at end of file + runtime_ws_url: 'ws://plugin-runtime:5400/control/ws' + cloud_service_url: 'https://space.langbot.app' \ No newline at end of file diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index c49ab861..a1c8d8cb 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -151,6 +151,7 @@ export interface PluginReorderElement { export interface ApiRespSystemInfo { debug: boolean; version: string; + cloud_service_url: string; } export interface ApiRespAsyncTasks { diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index a86cdbe8..e5192063 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -53,7 +53,11 @@ export interface RequestConfig extends AxiosRequestConfig { retry?: number; // 重试次数 } -export let systemInfo: ApiRespSystemInfo | null = null; +export let systemInfo: ApiRespSystemInfo = { + debug: false, + version: '', + cloud_service_url: '', +}; class HttpClient { private instance: AxiosInstance; @@ -74,7 +78,10 @@ class HttpClient { this.disableToken = disableToken || false; this.initInterceptors(); - if (systemInfo === null && baseURL != 'https://space.langbot.app') { + if ( + systemInfo.cloud_service_url === '' && + baseURL != 'https://space.langbot.app' + ) { this.getSystemInfo().then((res) => { systemInfo = res; }); @@ -86,12 +93,6 @@ class HttpClient { return this.baseURL; } - // 获取Session - private async getSession() { - // NOT IMPLEMENT - return ''; - } - // 同步获取Session private getSessionSync() { // NOT IMPLEMENT @@ -505,4 +506,4 @@ const getBaseURL = (): string => { export const httpClient = new HttpClient(getBaseURL()); // 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 -export const spaceClient = new HttpClient('https://space.langbot.app'); +export const spaceClient = new HttpClient(systemInfo.cloud_service_url); From bf2bc70794174d5c6aea23e3464ab377940e038f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 14 Aug 2025 23:55:14 +0800 Subject: [PATCH 35/78] feat: refactor webui httpclient --- .../plugin-market/PluginMarketComponent.tsx | 6 +- web/src/app/infra/http/BackendClient.ts | 292 ++++++++++ web/src/app/infra/http/BaseHttpClient.ts | 195 +++++++ web/src/app/infra/http/CloudServiceClient.ts | 39 ++ web/src/app/infra/http/HttpClient.ts | 526 +----------------- web/src/app/infra/http/README.md | 68 +++ web/src/app/infra/http/index.ts | 86 +++ 7 files changed, 701 insertions(+), 511 deletions(-) create mode 100644 web/src/app/infra/http/BackendClient.ts create mode 100644 web/src/app/infra/http/BaseHttpClient.ts create mode 100644 web/src/app/infra/http/CloudServiceClient.ts create mode 100644 web/src/app/infra/http/README.md create mode 100644 web/src/app/infra/http/index.ts diff --git a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx index 3db0b290..054836e1 100644 --- a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx @@ -4,7 +4,7 @@ import { useEffect, useState, useRef } from 'react'; import styles from '@/app/home/plugins/plugins.module.css'; import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent'; -import { spaceClient } from '@/app/infra/http/HttpClient'; +import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { Input } from '@/components/ui/input'; import { @@ -41,6 +41,8 @@ export default function PluginMarketComponent({ const searchTimeout = useRef(null); const pageSize = 10; + const cloudServiceClient = getCloudServiceClientSync(); + useEffect(() => { initData(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -72,7 +74,7 @@ export default function PluginMarketComponent({ sortOrder: string = sortOrderValue, ) { setLoading(true); - spaceClient + cloudServiceClient .getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder) .then((res) => { setMarketPluginList( diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts new file mode 100644 index 00000000..a30a5380 --- /dev/null +++ b/web/src/app/infra/http/BackendClient.ts @@ -0,0 +1,292 @@ +import { BaseHttpClient } from './BaseHttpClient'; +import { + ApiRespProviderRequesters, + ApiRespProviderRequester, + ApiRespProviderLLMModels, + ApiRespProviderLLMModel, + LLMModel, + ApiRespPipelines, + Pipeline, + ApiRespPlatformAdapters, + ApiRespPlatformAdapter, + ApiRespPlatformBots, + ApiRespPlatformBot, + Bot, + ApiRespPlugins, + ApiRespPlugin, + ApiRespPluginConfig, + PluginReorderElement, + AsyncTaskCreatedResp, + ApiRespSystemInfo, + ApiRespAsyncTasks, + ApiRespUserToken, + GetPipelineResponseData, + GetPipelineMetadataResponseData, + AsyncTask, + ApiRespWebChatMessage, + ApiRespWebChatMessages, +} from '@/app/infra/entities/api'; +import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; +import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; + +/** + * 后端服务客户端 + * 负责与后端 API 的所有交互 + */ +export class BackendClient extends BaseHttpClient { + constructor(baseURL: string) { + super(baseURL, false); + } + + // ============ Provider API ============ + public getProviderRequesters(): Promise { + return this.get('/api/v1/provider/requesters'); + } + + public getProviderRequester(name: string): Promise { + return this.get(`/api/v1/provider/requesters/${name}`); + } + + public getProviderRequesterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/provider/requesters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + + `/api/v1/provider/requesters/${name}/icon` + ); + } + + // ============ Provider Model LLM ============ + public getProviderLLMModels(): Promise { + return this.get('/api/v1/provider/models/llm'); + } + + public getProviderLLMModel(uuid: string): Promise { + return this.get(`/api/v1/provider/models/llm/${uuid}`); + } + + public createProviderLLMModel(model: LLMModel): Promise { + return this.post('/api/v1/provider/models/llm', model); + } + + public deleteProviderLLMModel(uuid: string): Promise { + return this.delete(`/api/v1/provider/models/llm/${uuid}`); + } + + public updateProviderLLMModel( + uuid: string, + model: LLMModel, + ): Promise { + return this.put(`/api/v1/provider/models/llm/${uuid}`, model); + } + + public testLLMModel(uuid: string, model: LLMModel): Promise { + return this.post(`/api/v1/provider/models/llm/${uuid}/test`, model); + } + + // ============ Pipeline API ============ + public getGeneralPipelineMetadata(): Promise { + // as designed, this method will be deprecated, and only for developer to check the prefered config schema + return this.get('/api/v1/pipelines/_/metadata'); + } + + public getPipelines(): Promise { + return this.get('/api/v1/pipelines'); + } + + public getPipeline(uuid: string): Promise { + return this.get(`/api/v1/pipelines/${uuid}`); + } + + public createPipeline(pipeline: Pipeline): Promise<{ + uuid: string; + }> { + return this.post('/api/v1/pipelines', pipeline); + } + + public updatePipeline(uuid: string, pipeline: Pipeline): Promise { + return this.put(`/api/v1/pipelines/${uuid}`, pipeline); + } + + public deletePipeline(uuid: string): Promise { + return this.delete(`/api/v1/pipelines/${uuid}`); + } + + // ============ Debug WebChat API ============ + public sendWebChatMessage( + sessionType: string, + messageChain: object[], + pipelineId: string, + timeout: number = 15000, + ): Promise { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/send`, + { + session_type: sessionType, + message: messageChain, + }, + { + timeout, + }, + ); + } + + public getWebChatHistoryMessages( + pipelineId: string, + sessionType: string, + ): Promise { + return this.get( + `/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`, + ); + } + + public resetWebChatSession( + pipelineId: string, + sessionType: string, + ): Promise<{ message: string }> { + return this.post( + `/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`, + ); + } + + // ============ Platform API ============ + public getAdapters(): Promise { + return this.get('/api/v1/platform/adapters'); + } + + public getAdapter(name: string): Promise { + return this.get(`/api/v1/platform/adapters/${name}`); + } + + public getAdapterIconURL(name: string): string { + if (this.instance.defaults.baseURL === '/') { + // 获取用户访问的URL + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/platform/adapters/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + `/api/v1/platform/adapters/${name}/icon` + ); + } + + // ============ Platform Bots ============ + public getBots(): Promise { + return this.get('/api/v1/platform/bots'); + } + + public getBot(uuid: string): Promise { + return this.get(`/api/v1/platform/bots/${uuid}`); + } + + public createBot(bot: Bot): Promise<{ uuid: string }> { + return this.post('/api/v1/platform/bots', bot); + } + + public updateBot(uuid: string, bot: Bot): Promise { + return this.put(`/api/v1/platform/bots/${uuid}`, bot); + } + + public deleteBot(uuid: string): Promise { + return this.delete(`/api/v1/platform/bots/${uuid}`); + } + + public getBotLogs( + botId: string, + request: GetBotLogsRequest, + ): Promise { + return this.post(`/api/v1/platform/bots/${botId}/logs`, request); + } + + // ============ Plugins API ============ + public getPlugins(): Promise { + return this.get('/api/v1/plugins'); + } + + public getPlugin(author: string, name: string): Promise { + return this.get(`/api/v1/plugins/${author}/${name}`); + } + + public getPluginConfig( + author: string, + name: string, + ): Promise { + return this.get(`/api/v1/plugins/${author}/${name}/config`); + } + + public updatePluginConfig( + author: string, + name: string, + config: object, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/config`, config); + } + + public togglePlugin( + author: string, + name: string, + target_enabled: boolean, + ): Promise { + return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { + target_enabled, + }); + } + + public reorderPlugins(plugins: PluginReorderElement[]): Promise { + return this.put('/api/v1/plugins/reorder', { plugins }); + } + + public updatePlugin( + author: string, + name: string, + ): Promise { + return this.post(`/api/v1/plugins/${author}/${name}/update`); + } + + public installPluginFromGithub( + source: string, + ): Promise { + return this.post('/api/v1/plugins/install/github', { source }); + } + + public removePlugin( + author: string, + name: string, + ): Promise { + return this.delete(`/api/v1/plugins/${author}/${name}`); + } + + // ============ System API ============ + public getSystemInfo(): Promise { + return this.get('/api/v1/system/info'); + } + + public getAsyncTasks(): Promise { + return this.get('/api/v1/system/tasks'); + } + + public getAsyncTask(id: number): Promise { + return this.get(`/api/v1/system/tasks/${id}`); + } + + // ============ User API ============ + public checkIfInited(): Promise<{ initialized: boolean }> { + return this.get('/api/v1/user/init'); + } + + public initUser(user: string, password: string): Promise { + return this.post('/api/v1/user/init', { user, password }); + } + + public authUser(user: string, password: string): Promise { + return this.post('/api/v1/user/auth', { user, password }); + } + + public checkUserToken(): Promise { + return this.get('/api/v1/user/check-token'); + } +} diff --git a/web/src/app/infra/http/BaseHttpClient.ts b/web/src/app/infra/http/BaseHttpClient.ts new file mode 100644 index 00000000..dc440799 --- /dev/null +++ b/web/src/app/infra/http/BaseHttpClient.ts @@ -0,0 +1,195 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + AxiosError, +} from 'axios'; + +type JSONValue = string | number | boolean | JSONObject | JSONArray | null; +interface JSONObject { + [key: string]: JSONValue; +} +type JSONArray = Array; + +export interface ResponseData { + code: number; + message: string; + data: T; + timestamp: number; +} + +export interface RequestConfig extends AxiosRequestConfig { + isSSR?: boolean; // 服务端渲染标识 + retry?: number; // 重试次数 +} + +/** + * 基础 HTTP 客户端类 + * 提供通用的 HTTP 请求方法和拦截器配置 + */ +export abstract class BaseHttpClient { + protected instance: AxiosInstance; + protected disableToken: boolean = false; + protected baseURL: string; + + constructor(baseURL: string, disableToken?: boolean) { + this.baseURL = baseURL; + this.disableToken = disableToken || false; + + this.instance = axios.create({ + baseURL: baseURL, + timeout: 15000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.initInterceptors(); + } + + // 外部获取baseURL的方法 + public getBaseUrl(): string { + return this.baseURL; + } + + // 更新 baseURL + public updateBaseURL(newBaseURL: string): void { + this.baseURL = newBaseURL; + this.instance.defaults.baseURL = newBaseURL; + } + + // 同步获取Session + protected getSessionSync(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('token'); + } + return null; + } + + // 拦截器配置 + protected initInterceptors(): void { + // 请求拦截 + this.instance.interceptors.request.use( + async (config) => { + // 客户端添加认证头 + if (typeof window !== 'undefined' && !this.disableToken) { + const session = this.getSessionSync(); + if (session) { + config.headers.Authorization = `Bearer ${session}`; + } + } + + return config; + }, + (error) => Promise.reject(error), + ); + + // 响应拦截 + this.instance.interceptors.response.use( + (response: AxiosResponse) => { + return response; + }, + (error: AxiosError) => { + // 统一错误处理 + if (error.response) { + const { status, data } = error.response; + const errMessage = data?.message || error.message; + + switch (status) { + case 401: + console.log('401 error: ', errMessage, error.request); + console.log('responseURL', error.request.responseURL); + if (typeof window !== 'undefined') { + localStorage.removeItem('token'); + if (!error.request.responseURL.includes('/check-token')) { + window.location.href = '/login'; + } + } + break; + case 403: + console.error('Permission denied:', errMessage); + break; + case 500: + console.error('Server error:', errMessage); + break; + } + + return Promise.reject({ + code: data?.code || status, + message: errMessage, + data: data?.data || null, + }); + } + + return Promise.reject({ + code: -1, + message: error.message || 'Network Error', + data: null, + }); + }, + ); + } + + // 转换下划线为驼峰 + protected convertKeysToCamel(obj: JSONValue): JSONValue { + if (Array.isArray(obj)) { + return obj.map((v) => this.convertKeysToCamel(v)); + } else if (obj !== null && typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const camelKey = key.replace(/_([a-z])/g, (_, letter) => + letter.toUpperCase(), + ); + acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]); + return acc; + }, {} as JSONObject); + } + return obj; + } + + // 错误处理 + protected handleError(error: object): never { + if (axios.isCancel(error)) { + throw { code: -2, message: 'Request canceled', data: null }; + } + throw error; + } + + // 核心请求方法 + public async request(config: RequestConfig): Promise { + try { + const response = await this.instance.request>(config); + return response.data.data; + } catch (error) { + return this.handleError(error as object); + } + } + + // 快捷方法 + public get( + url: string, + params?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'get', url, params, ...config }); + } + + public post( + url: string, + data?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'post', url, data, ...config }); + } + + public put( + url: string, + data?: object, + config?: RequestConfig, + ): Promise { + return this.request({ method: 'put', url, data, ...config }); + } + + public delete(url: string, config?: RequestConfig): Promise { + return this.request({ method: 'delete', url, ...config }); + } +} diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts new file mode 100644 index 00000000..668808ab --- /dev/null +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -0,0 +1,39 @@ +import { BaseHttpClient } from './BaseHttpClient'; +import { MarketPluginResponse } from '@/app/infra/entities/api'; + +/** + * 云服务客户端 + * 负责与 cloud service 的所有交互 + */ +export class CloudServiceClient extends BaseHttpClient { + constructor(baseURL: string = '') { + // cloud service 不需要 token 认证 + super(baseURL, true); + } + + /** + * 获取插件市场插件列表 + * @param page 页码 + * @param page_size 每页大小 + * @param query 搜索关键词 + * @param sort_by 排序字段 + * @param sort_order 排序顺序 + */ + public getMarketPlugins( + page: number, + page_size: number, + query: string, + sort_by: string = 'stars', + sort_order: string = 'DESC', + ): Promise { + return this.post(`/api/v1/market/plugins`, { + page, + page_size, + query, + sort_by, + sort_order, + }); + } + + // 未来可以在这里添加更多 cloud service 相关的方法 +} diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index e5192063..4e6f864f 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -1,509 +1,17 @@ -import axios, { - AxiosInstance, - AxiosRequestConfig, - AxiosResponse, - AxiosError, -} from 'axios'; -import { - ApiRespProviderRequesters, - ApiRespProviderRequester, - ApiRespProviderLLMModels, - ApiRespProviderLLMModel, - LLMModel, - ApiRespPipelines, - Pipeline, - ApiRespPlatformAdapters, - ApiRespPlatformAdapter, - ApiRespPlatformBots, - ApiRespPlatformBot, - Bot, - ApiRespPlugins, - ApiRespPlugin, - ApiRespPluginConfig, - PluginReorderElement, - AsyncTaskCreatedResp, - ApiRespSystemInfo, - ApiRespAsyncTasks, - ApiRespUserToken, - MarketPluginResponse, - GetPipelineResponseData, - GetPipelineMetadataResponseData, - AsyncTask, - ApiRespWebChatMessage, - ApiRespWebChatMessages, -} from '@/app/infra/entities/api'; -import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest'; -import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse'; - -type JSONValue = string | number | boolean | JSONObject | JSONArray | null; -interface JSONObject { - [key: string]: JSONValue; -} -type JSONArray = Array; - -export interface ResponseData { - code: number; - message: string; - data: T; - timestamp: number; -} - -export interface RequestConfig extends AxiosRequestConfig { - isSSR?: boolean; // 服务端渲染标识 - retry?: number; // 重试次数 -} - -export let systemInfo: ApiRespSystemInfo = { - debug: false, - version: '', - cloud_service_url: '', -}; - -class HttpClient { - private instance: AxiosInstance; - private disableToken: boolean = false; - private baseURL: string; - // 暂不需要SSR - // private ssrInstance: AxiosInstance | null = null - - constructor(baseURL: string, disableToken?: boolean) { - this.baseURL = baseURL; - this.instance = axios.create({ - baseURL: baseURL, - timeout: 15000, - headers: { - 'Content-Type': 'application/json', - }, - }); - this.disableToken = disableToken || false; - this.initInterceptors(); - - if ( - systemInfo.cloud_service_url === '' && - baseURL != 'https://space.langbot.app' - ) { - this.getSystemInfo().then((res) => { - systemInfo = res; - }); - } - } - - // 外部获取baseURL的方法 - getBaseUrl(): string { - return this.baseURL; - } - - // 同步获取Session - private getSessionSync() { - // NOT IMPLEMENT - return localStorage.getItem('token'); - } - - // 拦截器配置 - private initInterceptors() { - // 请求拦截 - this.instance.interceptors.request.use( - async (config) => { - // 服务端请求自动携带 cookie, Langbot暂时用不到SSR相关 - // if (typeof window === 'undefined' && config.isSSR) { } - // cookie not required - // const { cookies } = await import('next/headers') - // config.headers.Cookie = cookies().toString() - - // 客户端添加认证头 - if (typeof window !== 'undefined' && !this.disableToken) { - const session = this.getSessionSync(); - config.headers.Authorization = `Bearer ${session}`; - } - - return config; - }, - (error) => Promise.reject(error), - ); - - // 响应拦截 - this.instance.interceptors.response.use( - (response: AxiosResponse) => { - // 响应拦截处理写在这里,暂无业务需要 - - return response; - }, - (error: AxiosError) => { - // 统一错误处理 - if (error.response) { - const { status, data } = error.response; - const errMessage = data?.message || error.message; - - switch (status) { - case 401: - console.log('401 error: ', errMessage, error.request); - console.log('responseURL', error.request.responseURL); - localStorage.removeItem('token'); - if (!error.request.responseURL.includes('/check-token')) { - window.location.href = '/login'; - } - break; - case 403: - console.error('Permission denied:', errMessage); - break; - case 500: - // NOTE: move to component layer for customized message? - // toast.error(errMessage); - console.error('Server error:', errMessage); - break; - } - - return Promise.reject({ - code: data?.code || status, - message: errMessage, - data: data?.data || null, - }); - } - - return Promise.reject({ - code: -1, - message: error.message || 'Network Error', - data: null, - }); - }, - ); - } - - // 转换下划线为驼峰 - private convertKeysToCamel(obj: JSONValue): JSONValue { - if (Array.isArray(obj)) { - return obj.map((v) => this.convertKeysToCamel(v)); - } else if (obj !== null && typeof obj === 'object') { - return Object.keys(obj).reduce((acc, key) => { - const camelKey = key.replace(/_([a-z])/g, (_, letter) => - letter.toUpperCase(), - ); - acc[camelKey] = this.convertKeysToCamel((obj as JSONObject)[key]); - return acc; - }, {} as JSONObject); - } - return obj; - } - - // 核心请求方法 - public async request(config: RequestConfig): Promise { - try { - // 这里未来如果需要SSR可以将前面替换为SSR的instance - const instance = config.isSSR ? this.instance : this.instance; - const response = await instance.request>(config); - return response.data.data; - } catch (error) { - return this.handleError(error as object); - } - } - - private handleError(error: object): never { - if (axios.isCancel(error)) { - throw { code: -2, message: 'Request canceled', data: null }; - } - throw error; - } - - // 快捷方法 - public get( - url: string, - params?: object, - config?: RequestConfig, - ) { - return this.request({ method: 'get', url, params, ...config }); - } - - public post(url: string, data?: object, config?: RequestConfig) { - return this.request({ method: 'post', url, data, ...config }); - } - - public put(url: string, data?: object, config?: RequestConfig) { - return this.request({ method: 'put', url, data, ...config }); - } - - public delete(url: string, config?: RequestConfig) { - return this.request({ method: 'delete', url, ...config }); - } - - // real api request implementation - // ============ Provider API ============ - public getProviderRequesters(): Promise { - return this.get('/api/v1/provider/requesters'); - } - - public getProviderRequester(name: string): Promise { - return this.get(`/api/v1/provider/requesters/${name}`); - } - - public getProviderRequesterIconURL(name: string): string { - if (this.instance.defaults.baseURL === '/') { - // 获取用户访问的URL - const url = window.location.href; - const baseURL = url.split('/').slice(0, 3).join('/'); - return `${baseURL}/api/v1/provider/requesters/${name}/icon`; - } - return ( - this.instance.defaults.baseURL + - `/api/v1/provider/requesters/${name}/icon` - ); - } - - // ============ Provider Model LLM ============ - public getProviderLLMModels(): Promise { - return this.get('/api/v1/provider/models/llm'); - } - - public getProviderLLMModel(uuid: string): Promise { - return this.get(`/api/v1/provider/models/llm/${uuid}`); - } - - public createProviderLLMModel(model: LLMModel): Promise { - return this.post('/api/v1/provider/models/llm', model); - } - - public deleteProviderLLMModel(uuid: string): Promise { - return this.delete(`/api/v1/provider/models/llm/${uuid}`); - } - - public updateProviderLLMModel( - uuid: string, - model: LLMModel, - ): Promise { - return this.put(`/api/v1/provider/models/llm/${uuid}`, model); - } - - public testLLMModel(uuid: string, model: LLMModel): Promise { - return this.post(`/api/v1/provider/models/llm/${uuid}/test`, model); - } - - // ============ Pipeline API ============ - public getGeneralPipelineMetadata(): Promise { - // as designed, this method will be deprecated, and only for developer to check the prefered config schema - return this.get('/api/v1/pipelines/_/metadata'); - } - - public getPipelines(): Promise { - return this.get('/api/v1/pipelines'); - } - - public getPipeline(uuid: string): Promise { - return this.get(`/api/v1/pipelines/${uuid}`); - } - - public createPipeline(pipeline: Pipeline): Promise<{ - uuid: string; - }> { - return this.post('/api/v1/pipelines', pipeline); - } - - public updatePipeline(uuid: string, pipeline: Pipeline): Promise { - return this.put(`/api/v1/pipelines/${uuid}`, pipeline); - } - - public deletePipeline(uuid: string): Promise { - return this.delete(`/api/v1/pipelines/${uuid}`); - } - - // ============ Debug WebChat API ============ - public sendWebChatMessage( - sessionType: string, - messageChain: object[], - pipelineId: string, - timeout: number = 15000, - ): Promise { - return this.post( - `/api/v1/pipelines/${pipelineId}/chat/send`, - { - session_type: sessionType, - message: messageChain, - }, - { - timeout, - }, - ); - } - - public getWebChatHistoryMessages( - pipelineId: string, - sessionType: string, - ): Promise { - return this.get( - `/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`, - ); - } - - public resetWebChatSession( - pipelineId: string, - sessionType: string, - ): Promise<{ message: string }> { - return this.post( - `/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`, - ); - } - - // ============ Platform API ============ - public getAdapters(): Promise { - return this.get('/api/v1/platform/adapters'); - } - - public getAdapter(name: string): Promise { - return this.get(`/api/v1/platform/adapters/${name}`); - } - - public getAdapterIconURL(name: string): string { - if (this.instance.defaults.baseURL === '/') { - // 获取用户访问的URL - const url = window.location.href; - const baseURL = url.split('/').slice(0, 3).join('/'); - return `${baseURL}/api/v1/platform/adapters/${name}/icon`; - } - return ( - this.instance.defaults.baseURL + `/api/v1/platform/adapters/${name}/icon` - ); - } - - // ============ Platform Bots ============ - public getBots(): Promise { - return this.get('/api/v1/platform/bots'); - } - - public getBot(uuid: string): Promise { - return this.get(`/api/v1/platform/bots/${uuid}`); - } - - public createBot(bot: Bot): Promise<{ uuid: string }> { - return this.post('/api/v1/platform/bots', bot); - } - - public updateBot(uuid: string, bot: Bot): Promise { - return this.put(`/api/v1/platform/bots/${uuid}`, bot); - } - - public deleteBot(uuid: string): Promise { - return this.delete(`/api/v1/platform/bots/${uuid}`); - } - - public getBotLogs( - botId: string, - request: GetBotLogsRequest, - ): Promise { - return this.post(`/api/v1/platform/bots/${botId}/logs`, request); - } - - // ============ Plugins API ============ - public getPlugins(): Promise { - return this.get('/api/v1/plugins'); - } - - public getPlugin(author: string, name: string): Promise { - return this.get(`/api/v1/plugins/${author}/${name}`); - } - - public getPluginConfig( - author: string, - name: string, - ): Promise { - return this.get(`/api/v1/plugins/${author}/${name}/config`); - } - - public updatePluginConfig( - author: string, - name: string, - config: object, - ): Promise { - return this.put(`/api/v1/plugins/${author}/${name}/config`, config); - } - - public togglePlugin( - author: string, - name: string, - target_enabled: boolean, - ): Promise { - return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { - target_enabled, - }); - } - - public reorderPlugins(plugins: PluginReorderElement[]): Promise { - return this.put('/api/v1/plugins/reorder', { plugins }); - } - - public updatePlugin( - author: string, - name: string, - ): Promise { - return this.post(`/api/v1/plugins/${author}/${name}/update`); - } - - public getMarketPlugins( - page: number, - page_size: number, - query: string, - sort_by: string = 'stars', - sort_order: string = 'DESC', - ): Promise { - return this.post(`/api/v1/market/plugins`, { - page, - page_size, - query, - sort_by, - sort_order, - }); - } - - public installPluginFromGithub( - source: string, - ): Promise { - return this.post('/api/v1/plugins/install/github', { source }); - } - - public removePlugin( - author: string, - name: string, - ): Promise { - return this.delete(`/api/v1/plugins/${author}/${name}`); - } - - // ============ System API ============ - public getSystemInfo(): Promise { - return this.get('/api/v1/system/info'); - } - - public getAsyncTasks(): Promise { - return this.get('/api/v1/system/tasks'); - } - - public getAsyncTask(id: number): Promise { - return this.get(`/api/v1/system/tasks/${id}`); - } - - // ============ User API ============ - public checkIfInited(): Promise<{ initialized: boolean }> { - return this.get('/api/v1/user/init'); - } - - public initUser(user: string, password: string): Promise { - return this.post('/api/v1/user/init', { user, password }); - } - - public authUser(user: string, password: string): Promise { - return this.post('/api/v1/user/auth', { user, password }); - } - - public checkUserToken(): Promise { - return this.get('/api/v1/user/check-token'); - } -} - -const getBaseURL = (): string => { - if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) { - return process.env.NEXT_PUBLIC_API_BASE_URL; - } - - return '/'; -}; - -export const httpClient = new HttpClient(getBaseURL()); - -// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 -export const spaceClient = new HttpClient(systemInfo.cloud_service_url); +/** + * @deprecated 此文件仅用于向后兼容。请使用新的 client: + * - import { backendClient } from '@/app/infra/http' + * - import { getCloudServiceClient } from '@/app/infra/http' + */ + +// 重新导出新的客户端实现,保持向后兼容 +export { + backendClient as httpClient, + systemInfo, + type ResponseData, + type RequestConfig, +} from './index'; + +// 为了兼容性,重新导出 BackendClient 作为 HttpClient +import { BackendClient } from './BackendClient'; +export const HttpClient = BackendClient; diff --git a/web/src/app/infra/http/README.md b/web/src/app/infra/http/README.md new file mode 100644 index 00000000..b305d8a9 --- /dev/null +++ b/web/src/app/infra/http/README.md @@ -0,0 +1,68 @@ +# HTTP Client 架构说明 + +## 概述 + +HTTP Client 已经重构为更清晰的架构,将通用方法与业务逻辑分离,并为不同的服务创建了独立的客户端。 + +## 文件结构 + +- **BaseHttpClient.ts** - 基础 HTTP 客户端类,包含所有通用的 HTTP 方法和拦截器配置 +- **BackendClient.ts** - 后端服务客户端,处理与后端 API 的所有交互 +- **CloudServiceClient.ts** - 云服务客户端,处理与 cloud service 的交互(如插件市场) +- **index.ts** - 主入口文件,管理客户端实例的创建和导出 +- **HttpClient.ts** - 仅用于向后兼容的文件(已废弃) + +## 使用方法 + +### 新的推荐用法 + +```typescript +// 使用后端客户端 +import { backendClient } from '@/app/infra/http'; + +// 获取模型列表 +const models = await backendClient.getProviderLLMModels(); + +// 使用云服务客户端(异步方式,确保 URL 已初始化) +import { getCloudServiceClient } from '@/app/infra/http'; + +const cloudClient = await getCloudServiceClient(); +const marketPlugins = await cloudClient.getMarketPlugins(1, 10, 'search term'); + +// 使用云服务客户端(同步方式,可能使用默认 URL) +import { cloudServiceClient } from '@/app/infra/http'; + +const marketPlugins = await cloudServiceClient.getMarketPlugins(1, 10, 'search term'); +``` + +### 向后兼容(不推荐) + +```typescript +// 旧的用法仍然可以工作 +import { httpClient, spaceClient } from '@/app/infra/http/HttpClient'; + +// httpClient 现在指向 backendClient +const models = await httpClient.getProviderLLMModels(); + +// spaceClient 现在指向 cloudServiceClient +const marketPlugins = await spaceClient.getMarketPlugins(1, 10, 'search term'); +``` + +## 特点 + +1. **清晰的职责分离** + - BaseHttpClient:通用 HTTP 功能 + - BackendClient:后端 API 业务逻辑 + - CloudServiceClient:云服务 API 业务逻辑 + +2. **自动初始化** + - 应用启动时自动从后端获取 cloud service URL + - 云服务客户端会自动更新 baseURL + +3. **类型安全** + - 所有方法都有完整的 TypeScript 类型定义 + - 请求和响应类型都从 `@/app/infra/entities/api` 导入 + +4. **向后兼容** + - 旧代码无需修改即可继续工作 + - 逐步迁移到新的 API diff --git a/web/src/app/infra/http/index.ts b/web/src/app/infra/http/index.ts new file mode 100644 index 00000000..dad2b68c --- /dev/null +++ b/web/src/app/infra/http/index.ts @@ -0,0 +1,86 @@ +import { BackendClient } from './BackendClient'; +import { CloudServiceClient } from './CloudServiceClient'; +import { ApiRespSystemInfo } from '@/app/infra/entities/api'; + +// 系统信息 +export let systemInfo: ApiRespSystemInfo = { + debug: false, + version: '', + cloud_service_url: '', +}; + +/** + * 获取基础 URL + */ +const getBaseURL = (): string => { + if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) { + return process.env.NEXT_PUBLIC_API_BASE_URL; + } + return '/'; +}; + +// 创建后端客户端实例 +export const backendClient = new BackendClient(getBaseURL()); + +// 创建云服务客户端实例(初始化时使用默认 URL) +export const cloudServiceClient = new CloudServiceClient( + 'https://space.langbot.app', +); + +// 应用启动时自动初始化系统信息 +if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') { + backendClient + .getSystemInfo() + .then((info) => { + systemInfo = info; + cloudServiceClient.updateBaseURL(info.cloud_service_url); + }) + .catch((error) => { + console.error('Failed to initialize system info on startup:', error); + }); +} + +/** + * 获取云服务客户端 + * 如果 cloud service URL 尚未初始化,会自动从后端获取 + */ +export const getCloudServiceClient = async (): Promise => { + if (systemInfo.cloud_service_url === '') { + try { + systemInfo = await backendClient.getSystemInfo(); + // 更新 cloud service client 的 baseURL + cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url); + } catch (error) { + console.error('Failed to get system info:', error); + // 如果获取失败,继续使用默认 URL + } + } + return cloudServiceClient; +}; + +/** + * 获取云服务客户端(同步版本) + * 注意:如果 cloud service URL 尚未初始化,将使用默认 URL + */ +export const getCloudServiceClientSync = (): CloudServiceClient => { + return cloudServiceClient; +}; + +/** + * 手动初始化系统信息 + * 可以在应用启动时调用此方法预先获取系统信息 + */ +export const initializeSystemInfo = async (): Promise => { + try { + systemInfo = await backendClient.getSystemInfo(); + cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url); + } catch (error) { + console.error('Failed to initialize system info:', error); + } +}; + +// 导出类型,以便其他地方使用 +export type { ResponseData, RequestConfig } from './BaseHttpClient'; +export { BaseHttpClient } from './BaseHttpClient'; +export { BackendClient } from './BackendClient'; +export { CloudServiceClient } from './CloudServiceClient'; From 2b8eb5f01cc4d39f7465baef17c62493f99c91ce Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 15 Aug 2025 17:02:00 +0800 Subject: [PATCH 36/78] fix: bot switching --- pkg/api/http/service/bot.py | 14 ++++++++++---- pkg/plugin/handler.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/api/http/service/bot.py b/pkg/api/http/service/bot.py index 0bdb8e68..8d43965f 100644 --- a/pkg/api/http/service/bot.py +++ b/pkg/api/http/service/bot.py @@ -44,17 +44,23 @@ class BotService: if not include_secret: masked_columns = ['adapter_config'] + return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) + + async def get_runtime_bot_info(self, bot_uuid: str, include_secret: bool = True) -> dict: + """获取机器人运行时信息""" + persistence_bot = await self.get_bot(bot_uuid, include_secret) + if persistence_bot is None: + raise Exception('Bot not found') + adapter_runtime_values = {} runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid) if runtime_bot is not None: adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id - persistence_bot_data = self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) + persistence_bot['adapter_runtime_values'] = adapter_runtime_values - persistence_bot_data['adapter_runtime_values'] = adapter_runtime_values - - return persistence_bot_data + return persistence_bot async def create_bot(self, bot_data: dict) -> str: """创建机器人""" diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 7441ae5c..32cf859a 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -191,7 +191,7 @@ class RuntimeConnectionHandler(handler.Handler): async def get_bot_info(data: dict[str, Any]) -> handler.ActionResponse: """Get bot info""" bot_uuid = data['bot_uuid'] - bot = await self.ap.bot_service.get_bot(bot_uuid, include_secret=False) + bot = await self.ap.bot_service.get_runtime_bot_info(bot_uuid, include_secret=False) return handler.ActionResponse.success( data={ 'bot': bot, From e1a78e8ff918113f0f6a56772917105f416b920d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 15 Aug 2025 19:11:49 +0800 Subject: [PATCH 37/78] feat: tag debugging plugins in webui --- web/package.json | 1 + web/src/app/home/plugins/page.tsx | 51 +++- .../plugins/plugin-installed/PluginCardVO.ts | 3 + .../PluginInstalledComponent.tsx | 1 + .../plugin-card/PluginCardComponent.tsx | 8 + web/src/app/infra/entities/plugin/index.ts | 1 + web/src/components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++ web/src/i18n/locales/en-US.ts | 2 + web/src/i18n/locales/ja-JP.ts | 2 + web/src/i18n/locales/zh-Hans.ts | 2 + 10 files changed, 314 insertions(+), 14 deletions(-) create mode 100644 web/src/components/ui/dropdown-menu.tsx diff --git a/web/package.json b/web/package.json index 17516ac4..74967598 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.13", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-popover": "^1.1.14", diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 9935c0db..2d3ce125 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -7,7 +7,13 @@ import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { PlusIcon } from 'lucide-react'; +import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, @@ -32,6 +38,7 @@ export default function PluginConfigPage() { const { t } = useTranslation(); const [modalOpen, setModalOpen] = useState(false); const [sortModalOpen, setSortModalOpen] = useState(false); + const [activeTab, setActiveTab] = useState('installed'); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); @@ -83,7 +90,7 @@ export default function PluginConfigPage() { return (
- +
@@ -104,18 +111,34 @@ export default function PluginConfigPage() { > {t('plugins.arrange')} - + + + + + + { + // TODO: 本地上传功能待实现 + console.log('本地上传功能待实现'); + }} + > + + {t('plugins.uploadLocal')} + + { + setActiveTab('market'); + }} + > + + {t('plugins.marketplace')} + + +
diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts index 0e880543..4e1b68f2 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -9,6 +9,7 @@ export interface IPluginCardVO { tools: object[]; event_handlers: object; repository: string; + debug: boolean; } export class PluginCardVO implements IPluginCardVO { @@ -18,6 +19,7 @@ export class PluginCardVO implements IPluginCardVO { version: string; enabled: boolean; priority: number; + debug: boolean; status: string; tools: object[]; event_handlers: object; @@ -34,5 +36,6 @@ export class PluginCardVO implements IPluginCardVO { this.status = prop.status; this.tools = prop.tools; this.version = prop.version; + this.debug = prop.debug; } } diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index b108b312..3c496c1b 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -50,6 +50,7 @@ const PluginInstalledComponent = forwardRef( zh_Hans: '', }, ), + debug: plugin.debug, enabled: plugin.enabled, name: plugin.manifest.manifest.metadata.name, version: plugin.manifest.manifest.metadata.version ?? '', diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index 119a82bb..b83720dc 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -58,6 +58,14 @@ export default function PluginCardComponent({ v{cardVO.version} + {cardVO.debug && ( + + {t('plugins.debugging')} + + )} diff --git a/web/src/app/infra/entities/plugin/index.ts b/web/src/app/infra/entities/plugin/index.ts index 96469107..ba239d0a 100644 --- a/web/src/app/infra/entities/plugin/index.ts +++ b/web/src/app/infra/entities/plugin/index.ts @@ -7,6 +7,7 @@ export interface Plugin { manifest: { manifest: ComponentManifest; }; + debug: boolean; enabled: boolean; components: { component_config: object; diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..26027549 --- /dev/null +++ b/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 0e171e4b..032ee70c 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -179,6 +179,8 @@ const enUS = { eventCount: 'Events: {{count}}', toolCount: 'Tools: {{count}}', starCount: 'Stars: {{count}}', + uploadLocal: 'Upload Local', + debugging: 'Debugging', }, pipelines: { title: 'Pipelines', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index f1783a35..e17a5988 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -179,6 +179,8 @@ const jaJP = { eventCount: 'イベント:{{count}}', toolCount: 'ツール:{{count}}', starCount: 'スター:{{count}}', + uploadLocal: 'ローカルアップロード', + debugging: 'デバッグ中', }, pipelines: { title: 'パイプライン', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 2a960131..3a723e12 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -175,6 +175,8 @@ const zhHans = { eventCount: '事件:{{count}}', toolCount: '工具:{{count}}', starCount: '星标:{{count}}', + uploadLocal: '本地上传', + debugging: '调试中', }, pipelines: { title: '流水线', From b464d238c5ceaa9cd6867e1c9f810cc75ce6c31f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 15 Aug 2025 21:30:26 +0800 Subject: [PATCH 38/78] feat: plugin installation --- pkg/api/http/controller/groups/plugins.py | 45 ++++++++++++++++++++++- pkg/plugin/connector.py | 10 +++++ pkg/plugin/handler.py | 11 ++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 8f59986d..4ae03042 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -1,10 +1,11 @@ from __future__ import annotations - +import base64 import quart from .....core import taskmgr from .. import group +from langbot_plugin.runtime.plugin.mgr import PluginInstallSource @group.group_class('plugins', '/api/v1/plugins') @@ -100,7 +101,47 @@ class PluginsRouterGroup(group.RouterGroup): self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx), kind='plugin-operation', name='plugin-install-github', - label=f'安装插件 ...{short_source_str}', + label=f'Installing plugin from github ...{short_source_str}', + context=ctx, + ) + + return self.success(data={'task_id': wrapper.id}) + + @self.route('/install/marketplace', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + async def _() -> str: + data = await quart.request.json + + ctx = taskmgr.TaskContext.new() + wrapper = self.ap.task_mgr.create_user_task( + self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx), + kind='plugin-operation', + name='plugin-install-marketplace', + label=f'Installing plugin from marketplace ...{data}', + context=ctx, + ) + + return self.success(data={'task_id': wrapper.id}) + + @self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + async def _() -> str: + file = (await quart.request.files).get('file') + if file is None: + return self.http_status(400, -1, 'file is required') + + file_bytes = file.read() + + file_base64 = base64.b64encode(file_bytes).decode('utf-8') + + data = { + 'plugin_file': file_base64, + } + + ctx = taskmgr.TaskContext.new() + wrapper = self.ap.task_mgr.create_user_task( + self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx), + kind='plugin-operation', + name='plugin-install-local', + label=f'Installing plugin from local ...{file.filename}', context=ctx, ) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 7103c13e..cf363c7c 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -17,6 +17,8 @@ from langbot_plugin.api.entities import context import langbot_plugin.runtime.io.connection as base_connection from langbot_plugin.api.definition.components.manifest import ComponentManifest from langbot_plugin.api.entities.builtin.command import context as command_context +from langbot_plugin.runtime.plugin.mgr import PluginInstallSource +from ..core import taskmgr class PluginRuntimeConnector: @@ -97,6 +99,14 @@ class PluginRuntimeConnector: async def initialize_plugins(self): pass + async def install_plugin( + self, + install_source: PluginInstallSource, + install_info: dict[str, Any], + task_context: taskmgr.TaskContext | None = None, + ): + return await self.handler.install_plugin(install_source.value, install_info) + async def list_plugins(self) -> list[dict[str, Any]]: return await self.handler.list_plugins() diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 32cf859a..502db9cb 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -373,6 +373,17 @@ class RuntimeConnectionHandler(handler.Handler): timeout=10, ) + async def install_plugin(self, install_source: str, install_info: dict[str, Any]) -> dict[str, Any]: + """Install plugin""" + return await self.call_action( + LangBotToRuntimeAction.INSTALL_PLUGIN, + { + 'install_source': install_source, + 'install_info': install_info, + }, + timeout=10, + ) + async def list_plugins(self) -> list[dict[str, Any]]: """List plugins""" result = await self.call_action( From 288b2941484e0d1c33d1cd33785715a5e23c78cc Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 15 Aug 2025 22:05:39 +0800 Subject: [PATCH 39/78] feat: plugin installation webui --- web/src/app/home/plugins/page.tsx | 128 ++++++++++++++++-- .../PluginUploadDialog.tsx | 69 ++++++++++ web/src/app/infra/http/BackendClient.ts | 6 + web/src/app/infra/http/BaseHttpClient.ts | 16 +++ web/src/i18n/locales/en-US.ts | 8 ++ web/src/i18n/locales/ja-JP.ts | 8 ++ web/src/i18n/locales/zh-Hans.ts | 7 + 7 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 2d3ce125..b2776132 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -4,6 +4,9 @@ import PluginInstalledComponent, { } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; +import PluginUploadDialog, { + UploadModalStatus, +} from '@/app/home/plugins/plugin-upload-dialog/PluginUploadDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; @@ -23,7 +26,7 @@ import { } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { GithubIcon } from 'lucide-react'; -import { useState, useRef } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; @@ -43,7 +46,14 @@ export default function PluginConfigPage() { useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); + const [uploadModalOpen, setUploadModalOpen] = useState(false); + const [uploadStatus, setUploadStatus] = useState( + UploadModalStatus.UPLOADING, + ); + const [uploadError, setUploadError] = useState(null); + const [isDragOver, setIsDragOver] = useState(false); const pluginInstalledRef = useRef(null); + const fileInputRef = useRef(null); function handleModalConfirm() { installPlugin(githubURL); @@ -88,8 +98,93 @@ export default function PluginConfigPage() { }); } + const validateFileType = (file: File): boolean => { + const allowedExtensions = ['.lbpkg', '.zip']; + const fileName = file.name.toLowerCase(); + return allowedExtensions.some((ext) => fileName.endsWith(ext)); + }; + + const uploadPluginFile = useCallback( + async (file: File) => { + if (!validateFileType(file)) { + toast.error(t('plugins.unsupportedFileType')); + return; + } + + setUploadModalOpen(true); + setUploadStatus(UploadModalStatus.UPLOADING); + setUploadError(null); + + try { + // 暂时直接显示成功,等后续实现进度显示 + setTimeout(() => { + setUploadStatus(UploadModalStatus.SUCCESS); + toast.success(t('plugins.uploadSuccess')); + pluginInstalledRef.current?.refreshPluginList(); + }, 1000); + } catch (err: unknown) { + setUploadError((err as Error)?.message || t('plugins.uploadFailed')); + setUploadStatus(UploadModalStatus.ERROR); + } + }, + [t], + ); + + const handleFileSelect = useCallback(() => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }, []); + + const handleFileChange = useCallback( + (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + uploadPluginFile(file); + } + // 清空input值,以便可以重复选择同一个文件 + event.target.value = ''; + }, + [uploadPluginFile], + ); + + const handleDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(true); + }, []); + + const handleDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(false); + }, []); + + const handleDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + setIsDragOver(false); + + const files = Array.from(event.dataTransfer.files); + if (files.length > 0) { + uploadPluginFile(files[0]); + } + }, + [uploadPluginFile], + ); + return ( -
+
+
@@ -120,12 +215,7 @@ export default function PluginConfigPage() { - { - // TODO: 本地上传功能待实现 - console.log('本地上传功能待实现'); - }} - > + {t('plugins.uploadLocal')} @@ -206,6 +296,28 @@ export default function PluginConfigPage() { + {/* 上传状态弹窗 */} + + + {/* 拖拽提示覆盖层 */} + {isDragOver && ( +
+
+
+ +

+ {t('plugins.dragToUpload')} +

+
+
+
+ )} + void; + status: UploadModalStatus; + error?: string | null; +} + +export default function PluginUploadDialog({ + open, + onOpenChange, + status, + error, +}: PluginUploadDialogProps) { + const { t } = useTranslation(); + + return ( + + + + + + {t('plugins.uploadLocalPlugin')} + + +
+ {status === UploadModalStatus.UPLOADING && ( +

{t('plugins.uploadingPlugin')}

+ )} + {status === UploadModalStatus.SUCCESS && ( +

{t('plugins.uploadSuccess')}

+ )} + {status === UploadModalStatus.ERROR && ( + <> +

{t('plugins.uploadFailed')}

+

{error}

+ + )} +
+ + {(status === UploadModalStatus.SUCCESS || + status === UploadModalStatus.ERROR) && ( + + )} + +
+
+ ); +} diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index a30a5380..529bd5de 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -253,6 +253,12 @@ export class BackendClient extends BaseHttpClient { return this.post('/api/v1/plugins/install/github', { source }); } + public installPluginFromLocal(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + return this.postFile('/api/v1/plugins/install/local', formData); + } + public removePlugin( author: string, name: string, diff --git a/web/src/app/infra/http/BaseHttpClient.ts b/web/src/app/infra/http/BaseHttpClient.ts index dc440799..019a54e6 100644 --- a/web/src/app/infra/http/BaseHttpClient.ts +++ b/web/src/app/infra/http/BaseHttpClient.ts @@ -192,4 +192,20 @@ export abstract class BaseHttpClient { public delete(url: string, config?: RequestConfig): Promise { return this.request({ method: 'delete', url, ...config }); } + + public postFile( + url: string, + formData: FormData, + config?: RequestConfig, + ): Promise { + return this.request({ + method: 'post', + url, + data: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + ...config, + }); + } } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 032ee70c..85c0c07c 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -181,6 +181,14 @@ const enUS = { starCount: 'Stars: {{count}}', uploadLocal: 'Upload Local', debugging: 'Debugging', + uploadLocalPlugin: 'Upload Local Plugin', + dragToUpload: 'Drag plugin file here to upload', + unsupportedFileType: + 'Unsupported file type, only .lbpkg and .zip files are supported', + uploadingPlugin: 'Uploading plugin...', + uploadSuccess: 'Upload successful', + uploadFailed: 'Upload failed', + selectFileToUpload: 'Select plugin file to upload', }, pipelines: { title: 'Pipelines', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index e17a5988..6613d118 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -181,6 +181,14 @@ const jaJP = { starCount: 'スター:{{count}}', uploadLocal: 'ローカルアップロード', debugging: 'デバッグ中', + uploadLocalPlugin: 'ローカルプラグインのアップロード', + dragToUpload: 'ファイルをここにドラッグしてアップロード', + unsupportedFileType: + 'サポートされていないファイルタイプです。.lbpkg と .zip ファイルのみサポートされています', + uploadingPlugin: 'プラグインをアップロード中...', + uploadSuccess: 'アップロード成功', + uploadFailed: 'アップロード失敗', + selectFileToUpload: 'アップロードするプラグインファイルを選択', }, pipelines: { title: 'パイプライン', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 3a723e12..821cdaec 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -177,6 +177,13 @@ const zhHans = { starCount: '星标:{{count}}', uploadLocal: '本地上传', debugging: '调试中', + uploadLocalPlugin: '上传本地插件', + dragToUpload: '拖拽文件到此处上传', + unsupportedFileType: '不支持的文件类型,仅支持 .lbpkg 和 .zip 文件', + uploadingPlugin: '正在上传插件...', + uploadSuccess: '上传成功', + uploadFailed: '上传失败', + selectFileToUpload: '选择要上传的插件文件', }, pipelines: { title: '流水线', From 5179b3e53a1ab165a48f9649ee9796b7af85f4a2 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 16 Aug 2025 15:42:49 +0800 Subject: [PATCH 40/78] feat: trace plugin installation --- pkg/entity/persistence/plugin.py | 2 + .../dbm005_plugin_install_source.py | 25 +++ pkg/plugin/connector.py | 11 +- pkg/plugin/handler.py | 52 +++++- pkg/utils/constants.py | 2 +- web/src/app/home/plugins/page.tsx | 152 +++++++++--------- .../PluginUploadDialog.tsx | 69 -------- web/src/i18n/locales/en-US.ts | 2 +- web/src/i18n/locales/ja-JP.ts | 2 +- web/src/i18n/locales/zh-Hans.ts | 2 +- 10 files changed, 167 insertions(+), 152 deletions(-) create mode 100644 pkg/persistence/migrations/dbm005_plugin_install_source.py delete mode 100644 web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx diff --git a/pkg/entity/persistence/plugin.py b/pkg/entity/persistence/plugin.py index 30db6bd6..aebed2f0 100644 --- a/pkg/entity/persistence/plugin.py +++ b/pkg/entity/persistence/plugin.py @@ -13,6 +13,8 @@ class PluginSetting(Base): 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) + install_source = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, default='github') + install_info = 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, diff --git a/pkg/persistence/migrations/dbm005_plugin_install_source.py b/pkg/persistence/migrations/dbm005_plugin_install_source.py new file mode 100644 index 00000000..11547f88 --- /dev/null +++ b/pkg/persistence/migrations/dbm005_plugin_install_source.py @@ -0,0 +1,25 @@ +import sqlalchemy +from .. import migration + + +@migration.migration_class(5) +class DBMigratePluginInstallSource(migration.DBMigration): + """插件安装来源""" + + async def upgrade(self): + """升级""" + # add new column install_source, use default value 'github', via alter table + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text( + "ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'" + ) + ) + + # add new column install_info, use default value {}, via alter table + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'") + ) + + async def downgrade(self): + """降级""" + pass diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index cf363c7c..92c26c28 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -105,7 +105,16 @@ class PluginRuntimeConnector: install_info: dict[str, Any], task_context: taskmgr.TaskContext | None = None, ): - return await self.handler.install_plugin(install_source.value, install_info) + async for ret in self.handler.install_plugin(install_source.value, install_info): + current_action = ret.get('current_action', None) + if current_action is not None: + if task_context is not None: + task_context.set_current_action(current_action) + + trace = ret.get('trace', None) + if trace is not None: + if task_context is not None: + task_context.trace(trace) async def list_plugins(self) -> list[dict[str, Any]]: return await self.handler.list_plugins() diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 502db9cb..bc151321 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -39,6 +39,43 @@ class RuntimeConnectionHandler(handler.Handler): super().__init__(connection, disconnect_callback) self.ap = ap + @self.action(RuntimeToLangBotAction.INITIALIZE_PLUGIN_SETTINGS) + async def initialize_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: + """Initialize plugin settings""" + # check if exists plugin setting + plugin_author = data['plugin_author'] + plugin_name = data['plugin_name'] + install_source = data['install_source'] + install_info = data['install_info'] + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) + + if result.first() is not None: + # delete plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.delete(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) + + # create plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_plugin.PluginSetting).values( + plugin_author=plugin_author, + plugin_name=plugin_name, + install_source=install_source, + install_info=install_info, + ) + ) + + return handler.ActionResponse.success( + data={}, + ) + @self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS) async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: """Get plugin settings""" @@ -56,6 +93,8 @@ class RuntimeConnectionHandler(handler.Handler): 'enabled': False, 'priority': 0, 'plugin_config': {}, + 'install_source': 'local', + 'install_info': {}, } setting = result.first() @@ -64,6 +103,8 @@ class RuntimeConnectionHandler(handler.Handler): data['enabled'] = setting.enabled data['priority'] = setting.priority data['plugin_config'] = setting.config + data['install_source'] = setting.install_source + data['install_info'] = setting.install_info return handler.ActionResponse.success( data=data, @@ -373,17 +414,22 @@ class RuntimeConnectionHandler(handler.Handler): timeout=10, ) - async def install_plugin(self, install_source: str, install_info: dict[str, Any]) -> dict[str, Any]: + async def install_plugin( + self, install_source: str, install_info: dict[str, Any] + ) -> typing.AsyncGenerator[dict[str, Any], None]: """Install plugin""" - return await self.call_action( + gen = self.call_action_generator( LangBotToRuntimeAction.INSTALL_PLUGIN, { 'install_source': install_source, 'install_info': install_info, }, - timeout=10, + timeout=120, ) + async for ret in gen: + yield ret + async def list_plugins(self) -> list[dict[str, Any]]: """List plugins""" result = await self.call_action( diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index f822b477..19a92715 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.0.7' -required_database_version = 4 +required_database_version = 5 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" debug_mode = False diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index b2776132..4d1bbc46 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -4,13 +4,16 @@ import PluginInstalledComponent, { } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; -import PluginUploadDialog, { - UploadModalStatus, -} from '@/app/home/plugins/plugin-upload-dialog/PluginUploadDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon } from 'lucide-react'; +import { + PlusIcon, + ChevronDownIcon, + UploadIcon, + StoreIcon, + Download, +} from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -25,7 +28,7 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; -import { GithubIcon } from 'lucide-react'; +import { Upload } from 'lucide-react'; import { useState, useRef, useCallback } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; @@ -33,6 +36,7 @@ import { useTranslation } from 'react-i18next'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', + ASK_CONFIRM = 'ask_confirm', INSTALLING = 'installing', ERROR = 'error', } @@ -46,56 +50,72 @@ export default function PluginConfigPage() { useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); - const [uploadModalOpen, setUploadModalOpen] = useState(false); - const [uploadStatus, setUploadStatus] = useState( - UploadModalStatus.UPLOADING, - ); - const [uploadError, setUploadError] = useState(null); const [isDragOver, setIsDragOver] = useState(false); const pluginInstalledRef = useRef(null); const fileInputRef = useRef(null); - function handleModalConfirm() { - installPlugin(githubURL); - } - function installPlugin(url: string) { - setPluginInstallStatus(PluginInstallStatus.INSTALLING); - httpClient - .installPluginFromGithub(url) - .then((resp) => { - const taskId = resp.task_id; + function watchTask(taskId: number) { + let alreadySuccess = false; + console.log('taskId:', taskId); - let alreadySuccess = false; - console.log('taskId:', taskId); - - // 每秒拉取一次任务状态 - const interval = setInterval(() => { - httpClient.getAsyncTask(taskId).then((resp) => { - console.log('task status:', resp); - if (resp.runtime.done) { - clearInterval(interval); - if (resp.runtime.exception) { - setInstallError(resp.runtime.exception); - setPluginInstallStatus(PluginInstallStatus.ERROR); - } else { - // success - if (!alreadySuccess) { - toast.success(t('plugins.installSuccess')); - alreadySuccess = true; - } - setGithubURL(''); - setModalOpen(false); - pluginInstalledRef.current?.refreshPluginList(); - } + // 每秒拉取一次任务状态 + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((resp) => { + console.log('task status:', resp); + if (resp.runtime.done) { + clearInterval(interval); + if (resp.runtime.exception) { + setInstallError(resp.runtime.exception); + setPluginInstallStatus(PluginInstallStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success(t('plugins.installSuccess')); + alreadySuccess = true; } - }); - }, 1000); - }) - .catch((err) => { - console.log('error when install plugin:', err); - setInstallError(err.message); - setPluginInstallStatus(PluginInstallStatus.ERROR); + setGithubURL(''); + setModalOpen(false); + pluginInstalledRef.current?.refreshPluginList(); + } + } }); + }, 1000); + } + + function handleModalConfirm() { + installPlugin('github', { url: githubURL }); + } + + function installPlugin( + installSource: string, + installInfo: Record, + ) { + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + if (installSource === 'github') { + httpClient + .installPluginFromGithub(installInfo.url) + .then((resp) => { + const taskId = resp.task_id; + watchTask(taskId); + }) + .catch((err) => { + console.log('error when install plugin:', err); + setInstallError(err.message); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } else if (installSource === 'local') { + httpClient + .installPluginFromLocal(installInfo.file) + .then((resp) => { + const taskId = resp.task_id; + watchTask(taskId); + }) + .catch((err) => { + console.log('error when install plugin:', err); + setInstallError(err.message); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } } const validateFileType = (file: File): boolean => { @@ -111,21 +131,10 @@ export default function PluginConfigPage() { return; } - setUploadModalOpen(true); - setUploadStatus(UploadModalStatus.UPLOADING); - setUploadError(null); - - try { - // 暂时直接显示成功,等后续实现进度显示 - setTimeout(() => { - setUploadStatus(UploadModalStatus.SUCCESS); - toast.success(t('plugins.uploadSuccess')); - pluginInstalledRef.current?.refreshPluginList(); - }, 1000); - } catch (err: unknown) { - setUploadError((err as Error)?.message || t('plugins.uploadFailed')); - setUploadStatus(UploadModalStatus.ERROR); - } + setModalOpen(true); + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + setInstallError(null); + installPlugin('local', { file }); }, [t], ); @@ -250,8 +259,8 @@ export default function PluginConfigPage() { - - {t('plugins.installFromGithub')} + + {t('plugins.installPlugin')} {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( @@ -277,12 +286,13 @@ export default function PluginConfigPage() {
)} - {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( + {(pluginInstallStatus === PluginInstallStatus.WAIT_INPUT || + pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM) && ( <> - @@ -296,14 +306,6 @@ export default function PluginConfigPage() { - {/* 上传状态弹窗 */} - - {/* 拖拽提示覆盖层 */} {isDragOver && (
diff --git a/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx b/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx deleted file mode 100644 index 6cc0273e..00000000 --- a/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { UploadIcon } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -export enum UploadModalStatus { - UPLOADING = 'uploading', - SUCCESS = 'success', - ERROR = 'error', -} - -interface PluginUploadDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - status: UploadModalStatus; - error?: string | null; -} - -export default function PluginUploadDialog({ - open, - onOpenChange, - status, - error, -}: PluginUploadDialogProps) { - const { t } = useTranslation(); - - return ( - - - - - - {t('plugins.uploadLocalPlugin')} - - -
- {status === UploadModalStatus.UPLOADING && ( -

{t('plugins.uploadingPlugin')}

- )} - {status === UploadModalStatus.SUCCESS && ( -

{t('plugins.uploadSuccess')}

- )} - {status === UploadModalStatus.ERROR && ( - <> -

{t('plugins.uploadFailed')}

-

{error}

- - )} -
- - {(status === UploadModalStatus.SUCCESS || - status === UploadModalStatus.ERROR) && ( - - )} - -
-
- ); -} diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 85c0c07c..1a7f4499 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -142,7 +142,7 @@ const enUS = { marketplace: 'Marketplace', arrange: 'Sort Plugins', install: 'Install', - installFromGithub: 'Install Plugin from GitHub', + installPlugin: 'Install Plugin', onlySupportGithub: 'Currently only supports installation from GitHub', enterGithubLink: 'Enter GitHub link of the plugin', installing: 'Installing plugin...', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 6613d118..1576500f 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -142,7 +142,7 @@ const jaJP = { marketplace: 'プラグインマーケット', arrange: '並び替え', install: 'インストール', - installFromGithub: 'GitHubからプラグインをインストール', + installPlugin: 'プラグインをインストール', onlySupportGithub: '現在はGitHubからのインストールのみサポートしています', enterGithubLink: 'プラグインのGitHubリンクを入力してください', installing: 'プラグインをインストール中...', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 821cdaec..867410f1 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -139,7 +139,7 @@ const zhHans = { marketplace: '插件市场', arrange: '编排', install: '安装', - installFromGithub: '从 GitHub 安装插件', + installPlugin: '安装插件', onlySupportGithub: '目前仅支持从 GitHub 安装', enterGithubLink: '请输入插件的Github链接', installing: '正在安装插件...', From 28d4b1dd613f3c0f4b544468736128ea04c6e7c0 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 16 Aug 2025 18:05:33 +0800 Subject: [PATCH 41/78] feat: marketplace page --- web/package.json | 1 + .../home/bots/components/bot-form/BotForm.tsx | 6 +- web/src/app/home/bots/page.tsx | 4 +- .../dynamic-form/DynamicFormComponent.tsx | 6 +- .../dynamic-form/DynamicFormItemComponent.tsx | 4 +- .../dynamic-form/DynamicFormItemConfig.ts | 6 +- .../dynamic-form/N8nAuthFormComponent.tsx | 6 +- .../home-sidebar/HomeSidebarChild.tsx | 6 +- .../components/home-titlebar/HomeTitleBar.tsx | 8 +- web/src/app/home/layout.tsx | 4 +- .../models/component/llm-form/LLMForm.tsx | 4 +- web/src/app/home/models/page.tsx | 4 +- .../pipeline-form/PipelineFormComponent.tsx | 20 +- web/src/app/home/plugins/page.tsx | 40 +- .../plugins/plugin-installed/PluginCardVO.ts | 9 +- .../PluginInstalledComponent.tsx | 7 +- .../plugin-card/PluginCardComponent.tsx | 42 ++ .../plugin-form/PluginForm.tsx | 9 +- .../plugin-market/PluginMarketComponent.tsx | 588 +++++++++++------- .../PluginDetailDialog.tsx | 277 +++++++++ .../PluginMarketCardComponent.tsx | 99 ++- .../plugin-market-card/PluginMarketCardVO.ts | 18 +- .../plugins/plugin-sort/PluginSortDialog.tsx | 4 +- web/src/app/infra/entities/api/index.ts | 35 +- web/src/app/infra/entities/common.ts | 8 +- web/src/app/infra/entities/form/dynamic.ts | 8 +- web/src/app/infra/entities/pipeline/index.ts | 8 +- web/src/app/infra/entities/plugin/index.ts | 28 +- web/src/app/infra/http/BackendClient.ts | 12 + web/src/app/infra/http/CloudServiceClient.ts | 75 ++- web/src/app/infra/http/README.md | 6 +- web/src/i18n/I18nProvider.tsx | 4 +- web/src/i18n/locales/en-US.ts | 45 ++ web/src/i18n/locales/ja-JP.ts | 46 ++ web/src/i18n/locales/zh-Hans.ts | 43 ++ 35 files changed, 1095 insertions(+), 395 deletions(-) create mode 100644 web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx diff --git a/web/package.json b/web/package.json index 74967598..36b25dae 100644 --- a/web/package.json +++ b/web/package.json @@ -50,6 +50,7 @@ "react-dom": "^19.0.0", "react-hook-form": "^7.56.3", "react-i18next": "^15.5.1", + "react-markdown": "^10.1.0", "react-photo-view": "^1.2.7", "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index 40a902c2..de42e2ed 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -47,7 +47,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; const getFormSchema = (t: (key: string) => string) => z.object({ @@ -166,7 +166,7 @@ export default function BotForm({ setAdapterNameList( adaptersRes.adapters.map((item) => { return { - label: i18nObj(item.label), + label: extractI18nObject(item.label), value: item.name, }; }), @@ -187,7 +187,7 @@ export default function BotForm({ setAdapterDescriptionList( adaptersRes.adapters.reduce( (acc, item) => { - acc[item.name] = i18nObj(item.description); + acc[item.name] = extractI18nObject(item.description); return acc; }, {} as Record, diff --git a/web/src/app/home/bots/page.tsx b/web/src/app/home/bots/page.tsx index d4305898..df257836 100644 --- a/web/src/app/home/bots/page.tsx +++ b/web/src/app/home/bots/page.tsx @@ -9,7 +9,7 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { Bot, Adapter } from '@/app/infra/entities/api'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; import BotDetailDialog from '@/app/home/bots/BotDetailDialog'; export default function BotConfigPage() { @@ -27,7 +27,7 @@ export default function BotConfigPage() { const adapterListResp = await httpClient.getAdapters(); const adapterList = adapterListResp.adapters.map((adapter: Adapter) => { return { - label: i18nObj(adapter.label), + label: extractI18nObject(adapter.label), value: adapter.name, }; }); diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index f3df9e87..6c97cae4 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -12,7 +12,7 @@ import { } from '@/components/ui/form'; import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent'; import { useEffect } from 'react'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; export default function DynamicFormComponent({ itemConfigList, @@ -142,7 +142,7 @@ export default function DynamicFormComponent({ render={({ field }) => ( - {i18nObj(config.label)}{' '} + {extractI18nObject(config.label)}{' '} {config.required && *} @@ -150,7 +150,7 @@ export default function DynamicFormComponent({ {config.description && (

- {i18nObj(config.description)} + {extractI18nObject(config.description)}

)} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index 28d963d3..b7883bc0 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -24,7 +24,7 @@ import { HoverCardTrigger, } from '@/components/ui/hover-card'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; export default function DynamicFormItemComponent({ config, @@ -124,7 +124,7 @@ export default function DynamicFormItemComponent({ {config.options?.map((option) => ( - {i18nObj(option.label)} + {extractI18nObject(option.label)} ))} diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts index 74fd4a0b..6b52ece0 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemConfig.ts @@ -3,16 +3,16 @@ import { DynamicFormItemType, IDynamicFormItemOption, } from '@/app/infra/entities/form/dynamic'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; export class DynamicFormItemConfig implements IDynamicFormItemSchema { id: string; name: string; default: string | number | boolean | Array; - label: I18nLabel; + label: I18nObject; required: boolean; type: DynamicFormItemType; - description?: I18nLabel; + description?: I18nObject; options?: IDynamicFormItemOption[]; constructor(params: IDynamicFormItemSchema) { diff --git a/web/src/app/home/components/dynamic-form/N8nAuthFormComponent.tsx b/web/src/app/home/components/dynamic-form/N8nAuthFormComponent.tsx index 1c71befc..5605cae2 100644 --- a/web/src/app/home/components/dynamic-form/N8nAuthFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/N8nAuthFormComponent.tsx @@ -12,7 +12,7 @@ import { } from '@/components/ui/form'; import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; /** * N8n认证表单组件 @@ -182,7 +182,7 @@ export default function N8nAuthFormComponent({ render={({ field }) => ( - {i18nObj(config.label)}{' '} + {extractI18nObject(config.label)}{' '} {config.required && *} @@ -190,7 +190,7 @@ export default function N8nAuthFormComponent({ {config.description && (

- {i18nObj(config.description)} + {extractI18nObject(config.description)}

)} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx b/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx index 8529d410..031bc8db 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebarChild.tsx @@ -1,5 +1,5 @@ import styles from './HomeSidebar.module.css'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; export interface ISidebarChildVO { id: string; @@ -7,7 +7,7 @@ export interface ISidebarChildVO { name: string; route: string; description: string; - helpLink: I18nLabel; + helpLink: I18nObject; } export class SidebarChildVO { @@ -16,7 +16,7 @@ export class SidebarChildVO { name: string; route: string; description: string; - helpLink: I18nLabel; + helpLink: I18nObject; constructor(props: ISidebarChildVO) { this.id = props.id; diff --git a/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx b/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx index 56e849fa..0749b8fe 100644 --- a/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx +++ b/web/src/app/home/components/home-titlebar/HomeTitleBar.tsx @@ -1,6 +1,6 @@ -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; import styles from './HomeTittleBar.module.css'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; export default function HomeTitleBar({ title, @@ -9,7 +9,7 @@ export default function HomeTitleBar({ }: { title: string; subtitle: string; - helpLink: I18nLabel; + helpLink: I18nObject; }) { return (
@@ -19,7 +19,7 @@ export default function HomeTitleBar({
{ - window.open(i18nObj(helpLink), '_blank'); + window.open(extractI18nObject(helpLink), '_blank'); }} className="cursor-pointer" > diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx index 7dd68b25..d84eb6af 100644 --- a/web/src/app/home/layout.tsx +++ b/web/src/app/home/layout.tsx @@ -5,7 +5,7 @@ import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar'; import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar'; import React, { useState } from 'react'; import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; export default function HomeLayout({ children, @@ -14,7 +14,7 @@ export default function HomeLayout({ }>) { const [title, setTitle] = useState(''); const [subtitle, setSubtitle] = useState(''); - const [helpLink, setHelpLink] = useState({ + const [helpLink, setHelpLink] = useState({ en_US: '', zh_Hans: '', }); diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index f483f183..3f023336 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -39,7 +39,7 @@ import { } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; const getExtraArgSchema = (t: (key: string) => string) => z @@ -201,7 +201,7 @@ export default function LLMForm({ setRequesterNameList( requesterNameList.requesters.map((item) => { return { - label: i18nObj(item.label), + label: extractI18nObject(item.label), value: item.name, }; }), diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx index 3ccec486..5b23622d 100644 --- a/web/src/app/home/models/page.tsx +++ b/web/src/app/home/models/page.tsx @@ -16,7 +16,7 @@ import { } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; export default function LLMConfigPage() { const { t } = useTranslation(); @@ -33,7 +33,7 @@ export default function LLMConfigPage() { const requesterNameListResp = await httpClient.getProviderRequesters(); const requesterNameList = requesterNameListResp.requesters.map((item) => { return { - label: i18nObj(item.label), + label: extractI18nObject(item.label), value: item.name, }; }); diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index c92b553d..d7dd4c07 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -31,7 +31,7 @@ import { } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; export default function PipelineFormComponent({ initValues, @@ -220,10 +220,12 @@ export default function PipelineFormComponent({ if (stage.name === 'runner') { return (
-
{i18nObj(stage.label)}
+
+ {extractI18nObject(stage.label)} +
{stage.description && (
- {i18nObj(stage.description)} + {extractI18nObject(stage.description)}
)} -
{i18nObj(stage.label)}
+
+ {extractI18nObject(stage.label)} +
{stage.description && (
- {i18nObj(stage.description)} + {extractI18nObject(stage.description)}
)} -
{i18nObj(stage.label)}
+
+ {extractI18nObject(stage.label)} +
{stage.description && (
- {i18nObj(stage.description)} + {extractI18nObject(stage.description)}
)} ('local'); + const [installInfo, setInstallInfo] = useState>({}); // eslint-disable-line @typescript-eslint/no-explicit-any const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); @@ -83,12 +85,12 @@ export default function PluginConfigPage() { } function handleModalConfirm() { - installPlugin('github', { url: githubURL }); + installPlugin(installSource, installInfo as Record); // eslint-disable-line @typescript-eslint/no-explicit-any } function installPlugin( installSource: string, - installInfo: Record, + installInfo: Record, // eslint-disable-line @typescript-eslint/no-explicit-any ) { setPluginInstallStatus(PluginInstallStatus.INSTALLING); if (installSource === 'github') { @@ -115,6 +117,17 @@ export default function PluginConfigPage() { setInstallError(err.message); setPluginInstallStatus(PluginInstallStatus.ERROR); }); + } else if (installSource === 'marketplace') { + httpClient + .installPluginFromMarketplace( + installInfo.plugin_author, + installInfo.plugin_name, + installInfo.plugin_version, + ) + .then((resp) => { + const taskId = resp.task_id; + watchTask(taskId); + }); } } @@ -244,12 +257,16 @@ export default function PluginConfigPage() { - { - setGithubURL(githubURL); + { + setInstallSource('marketplace'); + setInstallInfo({ + plugin_author: plugin.author, + plugin_name: plugin.name, + plugin_version: plugin.latest_version, + }); + setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); setModalOpen(true); - setPluginInstallStatus(PluginInstallStatus.WAIT_INPUT); - setInstallError(null); }} /> @@ -274,6 +291,11 @@ export default function PluginConfigPage() { />
)} + {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && ( +
+

{t('plugins.askConfirm')}

+
+ )} {pluginInstallStatus === PluginInstallStatus.INSTALLING && (

{t('plugins.installing')}

diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts index 4e1b68f2..11ce2154 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -5,10 +5,11 @@ export interface IPluginCardVO { version: string; enabled: boolean; priority: number; + install_source: string; + install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; tools: object[]; event_handlers: object; - repository: string; debug: boolean; } @@ -20,10 +21,11 @@ export class PluginCardVO implements IPluginCardVO { enabled: boolean; priority: number; debug: boolean; + install_source: string; + install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; tools: object[]; event_handlers: object; - repository: string; constructor(prop: IPluginCardVO) { this.author = prop.author; @@ -32,10 +34,11 @@ export class PluginCardVO implements IPluginCardVO { this.event_handlers = prop.event_handlers; this.name = prop.name; this.priority = prop.priority; - this.repository = prop.repository; this.status = prop.status; this.tools = prop.tools; this.version = prop.version; this.debug = prop.debug; + this.install_source = prop.install_source; + this.install_info = prop.install_info; } } diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 3c496c1b..732e15ab 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -13,7 +13,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; export interface PluginInstalledComponentRef { refreshPluginList: () => void; @@ -44,7 +44,7 @@ const PluginInstalledComponent = forwardRef( value.plugins.map((plugin) => { return new PluginCardVO({ author: plugin.manifest.manifest.metadata.author ?? '', - description: i18nObj( + description: extractI18nObject( plugin.manifest.manifest.metadata.description ?? { en_US: '', zh_Hans: '', @@ -57,8 +57,9 @@ const PluginInstalledComponent = forwardRef( status: plugin.status, tools: [], event_handlers: {}, - repository: plugin.manifest.manifest.metadata.repository ?? '', priority: plugin.priority, + install_source: plugin.install_source, + install_info: plugin.install_info, }); }), ); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index b83720dc..bcbc02d4 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -5,6 +5,8 @@ import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; +import { ExternalLink } from 'lucide-react'; +import { getCloudServiceClientSync } from '@/app/infra/http'; export default function PluginCardComponent({ cardVO, @@ -66,6 +68,46 @@ export default function PluginCardComponent({ {t('plugins.debugging')} )} + {cardVO.install_source === 'github' && ( + { + e.stopPropagation(); + window.open(cardVO.install_info.github_url, '_blank'); + }} + > + {t('plugins.fromGithub')} + + + )} + {cardVO.install_source === 'local' && ( + + {t('plugins.fromLocal')} + + )} + {cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + window.open( + getCloudServiceClientSync().getPluginMarketplaceURL( + cardVO.author, + cardVO.name, + ), + '_blank', + ); + }} + > + {t('plugins.fromMarketplace')} + + + )}
diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 0e7b5ac5..081253b6 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -13,7 +13,7 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { toast } from 'sonner'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; import { useTranslation } from 'react-i18next'; enum PluginRemoveStatus { @@ -185,20 +185,18 @@ export default function PluginForm({
- {i18nObj(pluginInfo.manifest.manifest.metadata.label)} + {extractI18nObject(pluginInfo.manifest.manifest.metadata.label)}
- {i18nObj( + {extractI18nObject( pluginInfo.manifest.manifest.metadata.description ?? { en_US: '', zh_Hans: '', }, )}
- {/* @ts-ignore */} {pluginInfo.manifest.manifest.spec.config.length > 0 && ( } onSubmit={(values) => { @@ -213,7 +211,6 @@ export default function PluginForm({ }} /> )} - {/* @ts-ignore */} {pluginInfo.manifest.manifest.spec.config.length === 0 && (
{t('plugins.pluginNoConfig')} diff --git a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx index 054836e1..27b181b8 100644 --- a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx @@ -1,20 +1,8 @@ 'use client'; -import { useEffect, useState, useRef } from 'react'; -import styles from '@/app/home/plugins/plugins.module.css'; -import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; -import PluginMarketCardComponent from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent'; -import { getCloudServiceClientSync } from '@/app/infra/http'; -import { useTranslation } from 'react-i18next'; +import { useState, useEffect, useCallback, useRef, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; import { Input } from '@/components/ui/input'; -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from '@/components/ui/pagination'; import { Select, SelectContent, @@ -22,232 +10,402 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Search, Loader2 } from 'lucide-react'; +import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent'; +import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO'; +import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog'; +import { getCloudServiceClientSync } from '@/app/infra/http'; +import { useTranslation } from 'react-i18next'; +import { PluginV4 } from '@/app/infra/entities/plugin'; +import { extractI18nObject } from '@/i18n/I18nProvider'; +import { toast } from 'sonner'; +import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api'; -export default function PluginMarketComponent({ - askInstallPlugin, +interface SortOption { + value: string; + label: string; + sortBy: string; + sortOrder: string; +} + +// 内部组件,用于处理搜索参数 +function MarketPageContent({ + installPlugin, }: { - askInstallPlugin: (githubURL: string) => void; + installPlugin: (plugin: PluginV4) => void; }) { const { t } = useTranslation(); - const [marketPluginList, setMarketPluginList] = useState< - PluginMarketCardVO[] - >([]); - const [totalCount, setTotalCount] = useState(0); - const [nowPage, setNowPage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const [sortByValue, setSortByValue] = useState('pushed_at'); - const [sortOrderValue, setSortOrderValue] = useState('DESC'); - const searchTimeout = useRef(null); - const pageSize = 10; + const searchParams = useSearchParams(); - const cloudServiceClient = getCloudServiceClientSync(); + const [searchQuery, setSearchQuery] = useState(''); + const [plugins, setPlugins] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + const [sortOption, setSortOption] = useState('install_count_desc'); + // Plugin detail dialog state + const [selectedPluginAuthor, setSelectedPluginAuthor] = useState< + string | null + >(null); + const [selectedPluginName, setSelectedPluginName] = useState( + null, + ); + const [dialogOpen, setDialogOpen] = useState(false); + + const pageSize = 16; // 每页16个,4行x4列 + const searchTimeoutRef = useRef(null); + + // 排序选项 + const sortOptions: SortOption[] = [ + { + value: 'created_at_desc', + label: t('market.sort.recentlyAdded'), + sortBy: 'created_at', + sortOrder: 'DESC', + }, + { + value: 'updated_at_desc', + label: t('market.sort.recentlyUpdated'), + sortBy: 'updated_at', + sortOrder: 'DESC', + }, + { + value: 'install_count_desc', + label: t('market.sort.mostDownloads'), + sortBy: 'install_count', + sortOrder: 'DESC', + }, + { + value: 'install_count_asc', + label: t('market.sort.leastDownloads'), + sortBy: 'install_count', + sortOrder: 'ASC', + }, + ]; + + // 获取当前排序参数 + const getCurrentSort = useCallback(() => { + const option = sortOptions.find((opt) => opt.value === sortOption); + return option + ? { sortBy: option.sortBy, sortOrder: option.sortOrder } + : { sortBy: 'install_count', sortOrder: 'DESC' }; + }, [sortOption]); + + // 将API响应转换为VO对象 + const transformToVO = useCallback((plugin: PluginV4): PluginMarketCardVO => { + return new PluginMarketCardVO({ + pluginId: plugin.author + ' / ' + plugin.name, + author: plugin.author, + pluginName: plugin.name, + label: extractI18nObject(plugin.label), + description: + extractI18nObject(plugin.description) || t('market.noDescription'), + installCount: plugin.install_count, + iconURL: getCloudServiceClientSync().getPluginIconURL( + plugin.author, + plugin.name, + ), + githubURL: plugin.repository, + version: plugin.latest_version, + }); + }, []); + + // 获取插件列表 + const fetchPlugins = useCallback( + async (page: number, isSearch: boolean = false, reset: boolean = false) => { + if (page === 1) { + setIsLoading(true); + } else { + setIsLoadingMore(true); + } + + try { + let response; + const { sortBy, sortOrder } = getCurrentSort(); + + if (isSearch && searchQuery.trim()) { + response = await getCloudServiceClientSync().searchMarketplacePlugins( + searchQuery.trim(), + page, + pageSize, + sortBy, + sortOrder, + ); + } else { + response = await getCloudServiceClientSync().getMarketplacePlugins( + page, + pageSize, + sortBy, + sortOrder, + ); + } + + const data: ApiRespMarketplacePlugins = response; + const newPlugins = data.plugins.map(transformToVO); + const total = data.total; + + if (reset || page === 1) { + setPlugins(newPlugins); + } else { + setPlugins((prev) => [...prev, ...newPlugins]); + } + + setTotal(total); + setHasMore( + data.plugins.length === pageSize && + plugins.length + newPlugins.length < total, + ); + } catch (error) { + console.error('Failed to fetch plugins:', error); + toast.error(t('market.loadFailed')); + } finally { + setIsLoading(false); + setIsLoadingMore(false); + } + }, + [searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort], + ); + + // 初始加载 useEffect(() => { - initData(); + fetchPlugins(1, false, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - function initData() { - getPluginList(); - } + // 搜索功能 + const handleSearch = useCallback( + (query: string) => { + setSearchQuery(query); + setCurrentPage(1); + setPlugins([]); + fetchPlugins(1, !!query.trim(), true); + }, + [fetchPlugins], + ); - function onInputSearchKeyword(keyword: string) { - setSearchKeyword(keyword); + // 防抖搜索 + const handleSearchInputChange = useCallback( + (value: string) => { + setSearchQuery(value); - // 清除之前的定时器 - if (searchTimeout.current) { - clearTimeout(searchTimeout.current); + // 清除之前的定时器 + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + // 设置新的定时器 + searchTimeoutRef.current = setTimeout(() => { + handleSearch(value); + }, 300); + }, + [handleSearch], + ); + + // 排序选项变化处理 + const handleSortChange = useCallback((value: string) => { + setSortOption(value); + setCurrentPage(1); + setPlugins([]); + // fetchPlugins will be called by useEffect when sortOption changes + }, []); + + // 当排序选项变化时重新加载数据 + useEffect(() => { + fetchPlugins(1, !!searchQuery.trim(), true); + }, [sortOption]); + + // 处理URL参数,检查是否需要打开插件详情对话框 + useEffect(() => { + const author = searchParams.get('author'); + const pluginName = searchParams.get('plugin'); + + if (author && pluginName) { + setSelectedPluginAuthor(author); + setSelectedPluginName(pluginName); + setDialogOpen(true); } + }, [searchParams]); - // 设置新的定时器 - searchTimeout.current = setTimeout(() => { - setNowPage(1); - getPluginList(1, keyword); - }, 500); - } + // 插件详情对话框处理函数 + const handlePluginClick = useCallback( + (author: string, pluginName: string) => { + setSelectedPluginAuthor(author); + setSelectedPluginName(pluginName); + setDialogOpen(true); + }, + [], + ); - function getPluginList( - page: number = nowPage, - keyword: string = searchKeyword, - sortBy: string = sortByValue, - sortOrder: string = sortOrderValue, - ) { - setLoading(true); - cloudServiceClient - .getMarketPlugins(page, pageSize, keyword, sortBy, sortOrder) - .then((res) => { - setMarketPluginList( - res.plugins.map((marketPlugin) => { - let repository = marketPlugin.repository; - if (repository.startsWith('https://github.com/')) { - repository = repository.replace('https://github.com/', ''); - } + const handleDialogClose = useCallback(() => { + setDialogOpen(false); + setSelectedPluginAuthor(null); + setSelectedPluginName(null); + }, []); - if (repository.startsWith('github.com/')) { - repository = repository.replace('github.com/', ''); - } + // 清理定时器 + useEffect(() => { + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; + }, []); - const author = repository.split('/')[0]; - const name = repository.split('/')[1]; - return new PluginMarketCardVO({ - author: author, - description: marketPlugin.description, - githubURL: `https://github.com/${repository}`, - name: name, - pluginId: String(marketPlugin.ID), - starCount: marketPlugin.stars, - version: - 'version' in marketPlugin - ? String(marketPlugin.version) - : '1.0.0', // Default version if not provided - }); - }), - ); - setTotalCount(res.total); - setLoading(false); - console.log('market plugins:', res); - }) - .catch((error) => { - console.error(t('plugins.getPluginListError'), error); - setLoading(false); - }); - } + // 加载更多 + const loadMore = useCallback(() => { + if (!isLoadingMore && hasMore) { + const nextPage = currentPage + 1; + setCurrentPage(nextPage); + fetchPlugins(nextPage, !!searchQuery.trim()); + } + }, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]); - function handlePageChange(page: number) { - setNowPage(page); - getPluginList(page); - } + // 监听滚动事件 + useEffect(() => { + const handleScroll = () => { + if ( + window.innerHeight + document.documentElement.scrollTop >= + document.documentElement.offsetHeight - 100 + ) { + loadMore(); + } + }; - function handleSortChange(value: string) { - const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim()); - setSortByValue(newSortBy); - setSortOrderValue(newSortOrder); - setNowPage(1); - getPluginList(1, searchKeyword, newSortBy, newSortOrder); - } + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [loadMore]); + + // 安装插件 + // const handleInstallPlugin = (plugin: PluginV4) => { + // console.log('install plugin', plugin); + // }; return ( -
-
- onInputSearchKeyword(e.target.value)} - /> - - - -
- {totalCount > 0 && ( - - - - handlePageChange(nowPage - 1)} - className={ - nowPage <= 1 ? 'pointer-events-none opacity-50' : '' - } - /> - - - {/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */} - {(() => { - const totalPages = Math.ceil(totalCount / pageSize); - const maxVisiblePages = 5; - let startPage = Math.max( - 1, - nowPage - Math.floor(maxVisiblePages / 2), - ); - const endPage = Math.min( - totalPages, - startPage + maxVisiblePages - 1, - ); - - if (endPage - startPage + 1 < maxVisiblePages) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); - } - - return Array.from( - { length: endPage - startPage + 1 }, - (_, i) => { - const pageNum = startPage + i; - return ( - - handlePageChange(pageNum)} - > - - {pageNum} - - - - ); - }, - ); - })()} - - - handlePageChange(nowPage + 1)} - className={ - nowPage >= Math.ceil(totalCount / pageSize) - ? 'pointer-events-none opacity-50' - : '' - } - /> - - - - )} +
+ {/* 搜索框 */} +
+
+ + handleSearchInputChange(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + // 立即搜索,清除防抖定时器 + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + handleSearch(searchQuery); + } + }} + className="pl-10 pr-4" + />
-
- {loading ? ( -
- {t('plugins.loading')} -
- ) : marketPluginList.length === 0 ? ( -
- {t('plugins.noMatchingPlugins')} -
- ) : ( - marketPluginList.map((vo, index) => ( -
- { - askInstallPlugin(githubURL); - }} - /> -
- )) - )} + {/* 排序下拉框 */} +
+
+ + {t('market.sortBy')}: + + +
+ + {/* 搜索结果统计 */} + {total > 0 && ( +
+ {searchQuery + ? t('market.searchResults', { count: total }) + : t('market.totalPlugins', { count: total })} +
+ )} + + {/* 插件列表 */} + {isLoading ? ( +
+ + {t('cloud.loading')} +
+ ) : plugins.length === 0 ? ( +
+
+ {searchQuery ? t('market.noResults') : t('market.noPlugins')} +
+
+ ) : ( +
+ {plugins.map((plugin) => ( + + ))} +
+ )} + + {/* 加载更多指示器 */} + {isLoadingMore && ( +
+ + {t('market.loadingMore')} +
+ )} + + {/* 没有更多数据提示 */} + {!hasMore && plugins.length > 0 && ( +
+ {t('market.allLoaded')} +
+ )} + + {/* 插件详情对话框 */} +
); } + +// 主组件,包装在 Suspense 中 +export default function MarketPage({ + installPlugin, +}: { + installPlugin: (plugin: PluginV4) => void; +}) { + return ( + +
+ + 加载中... +
+
+ } + > + + + ); +} diff --git a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx new file mode 100644 index 00000000..79e4a698 --- /dev/null +++ b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Loader2, Download, Users } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import { PluginV4 } from '@/app/infra/entities/plugin'; +import { extractI18nObject } from '@/i18n/I18nProvider'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import { getCloudServiceClientSync } from '@/app/infra/http'; + +interface PluginDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + author: string | null; + pluginName: string | null; + installPlugin: (plugin: PluginV4) => void; +} + +export default function PluginDetailDialog({ + open, + onOpenChange, + author, + pluginName, + installPlugin, +}: PluginDetailDialogProps) { + const { t } = useTranslation(); + const [plugin, setPlugin] = useState(null); + const [readme, setReadme] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingReadme, setIsLoadingReadme] = useState(false); + + // 获取插件详情和README + useEffect(() => { + if (open && author && pluginName) { + fetchPluginData(); + } + }, [open, author, pluginName]); + + const fetchPluginData = async () => { + if (!author || !pluginName) return; + + setIsLoading(true); + try { + // 获取插件详情 + const detailResponse = await getCloudServiceClientSync().getPluginDetail( + author, + pluginName, + ); + console.log('detailResponse', detailResponse); + setPlugin(detailResponse.plugin); + + // 获取README + setIsLoadingReadme(true); + try { + const readmeResponse = + await getCloudServiceClientSync().getPluginREADME(author, pluginName); + console.log('readmeResponse', readmeResponse); + setReadme(readmeResponse.readme); + } catch (error) { + console.warn('Failed to load README:', error); + setReadme(t('market.noReadme')); + } finally { + setIsLoadingReadme(false); + } + } catch (error) { + console.error('Failed to fetch plugin details:', error); + toast.error(t('market.loadFailed')); + onOpenChange(false); + } finally { + setIsLoading(false); + } + }; + + if (!open) return null; + + return ( + + + {isLoading ? ( +
+ + {t('cloud.loading')} +
+ ) : plugin ? ( +
+ {/* 左侧:插件基本信息 */} +
+ {/* 插件图标和标题 */} +
+ {plugin.name} +
+

+ {extractI18nObject(plugin.label) || plugin.name} +

+
+ + + {plugin.author} / {plugin.name} + +
+ +
+ + v{plugin.latest_version} + + + + + + {plugin.install_count.toLocaleString()}{' '} + {t('market.downloads')} + + + + {plugin.repository && ( + { + e.stopPropagation(); + window.open(plugin.repository, '_blank'); + }} + > + + + )} +
+
+
+ + {/* 插件描述 */} +
+

+ {t('market.description')} +

+

+ {extractI18nObject(plugin.description) || + t('market.noDescription')} +

+
+ + {/* 标签 */} + {plugin.tags && plugin.tags.length > 0 && ( +
+

+ {t('market.tags')} +

+
+ {plugin.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} + + {/* 操作按钮 */} +
+ + {/* {plugin.repository && ( + + )} */} +
+
+ + {/* 右侧:README内容 */} +
+
+ {isLoadingReadme ? ( +
+ + + {t('cloud.loading')} + +
+ ) : ( +
+ ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + p: ({ children }) => ( +

+ {children} +

+ ), + ul: ({ children }) => ( +
    + {children} +
+ ), + ol: ({ children }) => ( +
    + {children} +
+ ), + li: ({ children }) => ( +
  • {children}
  • + ), + code: ({ children }) => ( + + {children} + + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + a: ({ href, children }) => ( + + {children} + + ), + }} + > + {readme} +
    +
    + )} +
    +
    +
    + ) : null} +
    +
    + ); +} diff --git a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx index bd62cdec..4d961433 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx @@ -1,40 +1,33 @@ -import { PluginMarketCardVO } from '@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO'; -import { Button } from '@/components/ui/button'; -import { useTranslation } from 'react-i18next'; +import { PluginMarketCardVO } from './PluginMarketCardVO'; export default function PluginMarketCardComponent({ cardVO, - installPlugin, + onPluginClick, }: { cardVO: PluginMarketCardVO; - installPlugin: (pluginURL: string) => void; + onPluginClick?: (author: string, pluginName: string) => void; }) { - const { t } = useTranslation(); - - function handleInstallClick(pluginURL: string) { - installPlugin(pluginURL); + function handleCardClick() { + if (onPluginClick) { + onPluginClick(cardVO.author, cardVO.pluginName); + } } return ( -
    -
    - - - +
    +
    + {/* 上部分:插件信息 */} +
    + plugin icon -
    -
    +
    -
    - {cardVO.author} /{' '} -
    +
    {cardVO.pluginId}
    -
    {cardVO.name}
    +
    {cardVO.label}
    @@ -43,42 +36,40 @@ export default function PluginMarketCardComponent({
    -
    -
    +
    + {cardVO.githubURL && ( - - -
    - {t('plugins.starCount', { count: cardVO.starCount })} -
    -
    - -
    - window.open(cardVO.githubURL, '_blank')} + onClick={(e) => { + e.stopPropagation(); + window.open(cardVO.githubURL, '_blank'); + }} > - -
    + )} +
    +
    + + {/* 下部分:下载量 */} +
    + + + + + +
    + {cardVO.installCount.toLocaleString()}
    diff --git a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts index fe0a1e75..b4c38bbe 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts +++ b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO.ts @@ -1,9 +1,11 @@ export interface IPluginMarketCardVO { pluginId: string; author: string; - name: string; + pluginName: string; + label: string; description: string; - starCount: number; + installCount: number; + iconURL: string; githubURL: string; version: string; } @@ -11,18 +13,22 @@ export interface IPluginMarketCardVO { export class PluginMarketCardVO implements IPluginMarketCardVO { pluginId: string; description: string; - name: string; + label: string; author: string; + pluginName: string; + iconURL: string; githubURL: string; - starCount: number; + installCount: number; version: string; constructor(prop: IPluginMarketCardVO) { this.description = prop.description; - this.name = prop.name; + this.label = prop.label; this.author = prop.author; + this.pluginName = prop.pluginName; + this.iconURL = prop.iconURL; this.githubURL = prop.githubURL; - this.starCount = prop.starCount; + this.installCount = prop.installCount; this.pluginId = prop.pluginId; this.version = prop.version; } diff --git a/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx index ad6874eb..acde5741 100644 --- a/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx +++ b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx @@ -32,7 +32,7 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useTranslation } from 'react-i18next'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; interface PluginSortDialogProps { open: boolean; @@ -87,7 +87,7 @@ export default function PluginSortDialog({ value.plugins.map((plugin) => { return new PluginCardVO({ author: plugin.author, - description: i18nObj(plugin.description), + description: extractI18nObject(plugin.description), enabled: plugin.enabled, name: plugin.name, version: plugin.version, diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index a1c8d8cb..accfe5b4 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -1,8 +1,8 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; import { PipelineConfigTab } from '@/app/infra/entities/pipeline'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; import { Message } from '@/app/infra/entities/message'; -import { Plugin } from '@/app/infra/entities/plugin'; +import { Plugin, PluginV4 } from '@/app/infra/entities/plugin'; export interface ApiResponse { code: number; @@ -24,8 +24,8 @@ export interface ApiRespProviderRequester { export interface Requester { name: string; - label: I18nLabel; - description: I18nLabel; + label: I18nObject; + description: I18nObject; icon?: string; spec: { config: IDynamicFormItemSchema[]; @@ -82,8 +82,8 @@ export interface ApiRespPlatformAdapter { export interface Adapter { name: string; - label: I18nLabel; - description: I18nLabel; + label: I18nObject; + description: I18nObject; icon?: string; spec: { config: IDynamicFormItemSchema[]; @@ -183,26 +183,13 @@ export interface ApiRespUserToken { token: string; } -export interface MarketPlugin { - ID: number; - CreatedAt: string; // ISO 8601 格式日期 - UpdatedAt: string; - DeletedAt: string | null; - name: string; - author: string; - description: string; - repository: string; // GitHub 仓库路径 - artifacts_path: string; - stars: number; - downloads: number; - status: 'initialized' | 'mounted'; // 可根据实际状态值扩展联合类型 - synced_at: string; - pushed_at: string; // 最后一次代码推送时间 +export interface ApiRespMarketplacePlugins { + plugins: PluginV4[]; + total: number; } -export interface MarketPluginResponse { - plugins: MarketPlugin[]; - total: number; +export interface ApiRespMarketplacePluginDetail { + plugin: PluginV4; } interface GetPipelineConfig { diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts index 9805d377..35dcc9f7 100644 --- a/web/src/app/infra/entities/common.ts +++ b/web/src/app/infra/entities/common.ts @@ -1,4 +1,4 @@ -export interface I18nLabel { +export interface I18nObject { en_US: string; zh_Hans: string; ja_JP?: string; @@ -9,12 +9,12 @@ export interface ComponentManifest { kind: string; metadata: { name: string; - label: I18nLabel; - description?: I18nLabel; + label: I18nObject; + description?: I18nObject; icon?: string; repository?: string; version?: string; author?: string; }; - spec: object; + spec: Record; // eslint-disable-line @typescript-eslint/no-explicit-any } diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts index 6a185c8b..d46528c9 100644 --- a/web/src/app/infra/entities/form/dynamic.ts +++ b/web/src/app/infra/entities/form/dynamic.ts @@ -1,13 +1,13 @@ -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; export interface IDynamicFormItemSchema { id: string; default: string | number | boolean | Array; - label: I18nLabel; + label: I18nObject; name: string; required: boolean; type: DynamicFormItemType; - description?: I18nLabel; + description?: I18nObject; options?: IDynamicFormItemOption[]; } @@ -25,5 +25,5 @@ export enum DynamicFormItemType { export interface IDynamicFormItemOption { name: string; - label: I18nLabel; + label: I18nObject; } diff --git a/web/src/app/infra/entities/pipeline/index.ts b/web/src/app/infra/entities/pipeline/index.ts index 29a5f6af..cc411c9f 100644 --- a/web/src/app/infra/entities/pipeline/index.ts +++ b/web/src/app/infra/entities/pipeline/index.ts @@ -1,4 +1,4 @@ -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic'; export interface PipelineFormEntity { @@ -11,13 +11,13 @@ export interface PipelineFormEntity { export interface PipelineConfigTab { name: string; - label: I18nLabel; + label: I18nObject; stages: PipelineConfigStage[]; } export interface PipelineConfigStage { name: string; - label: I18nLabel; - description?: I18nLabel; + label: I18nObject; + description?: I18nObject; config: IDynamicFormItemSchema[]; } diff --git a/web/src/app/infra/entities/plugin/index.ts b/web/src/app/infra/entities/plugin/index.ts index ba239d0a..9de4ba4b 100644 --- a/web/src/app/infra/entities/plugin/index.ts +++ b/web/src/app/infra/entities/plugin/index.ts @@ -1,4 +1,4 @@ -import { ComponentManifest } from '@/app/infra/entities/common'; +import { ComponentManifest, I18nObject } from '@/app/infra/entities/common'; export interface Plugin { status: 'intialized' | 'mounted' | 'unmounted'; @@ -9,6 +9,8 @@ export interface Plugin { }; debug: boolean; enabled: boolean; + install_source: string; + install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any components: { component_config: object; manifest: { @@ -16,3 +18,27 @@ export interface Plugin { }; }; } + +// marketplace plugin v4 +export enum PluginV4Status { + Any = 'any', + Live = 'live', + Deleted = 'deleted', +} + +export interface PluginV4 { + id: number; + plugin_id: string; + author: string; + name: string; + label: I18nObject; + description: I18nObject; + icon: string; + repository: string; + tags: string[]; + install_count: number; + latest_version: string; + status: PluginV4Status; + created_at: string; + updated_at: string; +} diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 529bd5de..d077abf6 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -259,6 +259,18 @@ export class BackendClient extends BaseHttpClient { return this.postFile('/api/v1/plugins/install/local', formData); } + public installPluginFromMarketplace( + author: string, + name: string, + version: string, + ): Promise { + return this.post('/api/v1/plugins/install/marketplace', { + plugin_author: author, + plugin_name: name, + plugin_version: version, + }); + } + public removePlugin( author: string, name: string, diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index 668808ab..6d67316e 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -1,5 +1,8 @@ import { BaseHttpClient } from './BaseHttpClient'; -import { MarketPluginResponse } from '@/app/infra/entities/api'; +import { + ApiRespMarketplacePluginDetail, + ApiRespMarketplacePlugins, +} from '@/app/infra/entities/api'; /** * 云服务客户端 @@ -11,29 +14,59 @@ export class CloudServiceClient extends BaseHttpClient { super(baseURL, true); } - /** - * 获取插件市场插件列表 - * @param page 页码 - * @param page_size 每页大小 - * @param query 搜索关键词 - * @param sort_by 排序字段 - * @param sort_order 排序顺序 - */ - public getMarketPlugins( + public getMarketplacePlugins( page: number, page_size: number, - query: string, - sort_by: string = 'stars', - sort_order: string = 'DESC', - ): Promise { - return this.post(`/api/v1/market/plugins`, { - page, - page_size, - query, - sort_by, - sort_order, + sort_by?: string, + sort_order?: string, + ): Promise { + return this.get('/api/v1/marketplace/plugins', { + params: { page, page_size, sort_by, sort_order }, }); } - // 未来可以在这里添加更多 cloud service 相关的方法 + public searchMarketplacePlugins( + query: string, + page: number, + page_size: number, + sort_by?: string, + sort_order?: string, + ): Promise { + return this.post( + '/api/v1/marketplace/plugins/search', + { + query, + page, + page_size, + sort_by, + sort_order, + }, + ); + } + + public getPluginDetail( + author: string, + pluginName: string, + ): Promise { + return this.get( + `/api/v1/marketplace/plugins/${author}/${pluginName}`, + ); + } + + public getPluginREADME( + author: string, + pluginName: string, + ): Promise<{ readme: string }> { + return this.get<{ readme: string }>( + `/api/v1/marketplace/plugins/${author}/${pluginName}/resources/README`, + ); + } + + public getPluginIconURL(author: string, name: string): string { + return `${this.baseURL}/api/v1/marketplace/plugins/${author}/${name}/resources/icon`; + } + + public getPluginMarketplaceURL(author: string, name: string): string { + return `${this.baseURL}/market?author=${author}&plugin=${name}`; + } } diff --git a/web/src/app/infra/http/README.md b/web/src/app/infra/http/README.md index b305d8a9..2a2e976b 100644 --- a/web/src/app/infra/http/README.md +++ b/web/src/app/infra/http/README.md @@ -32,7 +32,11 @@ const marketPlugins = await cloudClient.getMarketPlugins(1, 10, 'search term'); // 使用云服务客户端(同步方式,可能使用默认 URL) import { cloudServiceClient } from '@/app/infra/http'; -const marketPlugins = await cloudServiceClient.getMarketPlugins(1, 10, 'search term'); +const marketPlugins = await cloudServiceClient.getMarketPlugins( + 1, + 10, + 'search term', +); ``` ### 向后兼容(不推荐) diff --git a/web/src/i18n/I18nProvider.tsx b/web/src/i18n/I18nProvider.tsx index ef3ea0b7..2a78941d 100644 --- a/web/src/i18n/I18nProvider.tsx +++ b/web/src/i18n/I18nProvider.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import '@/i18n'; -import { I18nLabel } from '@/app/infra/entities/common'; +import { I18nObject } from '@/app/infra/entities/common'; interface I18nProviderProps { children: ReactNode; @@ -11,7 +11,7 @@ interface I18nProviderProps { export default function I18nProvider({ children }: I18nProviderProps) { return <>{children}; } -export function i18nObj(i18nLabel: I18nLabel): string { +export function extractI18nObject(i18nLabel: I18nObject): string { const language = localStorage.getItem('langbot_language'); if ((language === 'zh-Hans' && i18nLabel.zh_Hans) || !i18nLabel.en_US) { return i18nLabel.zh_Hans; diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 1a7f4499..ec91858c 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -189,6 +189,51 @@ const enUS = { uploadSuccess: 'Upload successful', uploadFailed: 'Upload failed', selectFileToUpload: 'Select plugin file to upload', + fromGithub: 'From GitHub', + fromLocal: 'From Local', + fromMarketplace: 'From Marketplace', + }, + market: { + searchPlaceholder: 'Search plugins...', + searchResults: 'Found {{count}} plugins', + totalPlugins: 'Total {{count}} plugins', + noPlugins: 'No plugins available', + noResults: 'No relevant plugins found', + loadingMore: 'Loading more...', + allLoaded: 'All plugins displayed', + install: 'Install', + installConfirm: + 'Are you sure you want to install plugin "{{name}}" ({{version}})?', + downloadComplete: 'Plugin "{{name}}" download completed', + installFailed: 'Installation failed, please try again later', + loadFailed: 'Failed to get plugin list, please try again later', + noDescription: 'No description available', + notFound: 'Plugin information not found', + sortBy: 'Sort by', + sort: { + recentlyAdded: 'Recently Added', + recentlyUpdated: 'Recently Updated', + mostDownloads: 'Most Downloads', + leastDownloads: 'Least Downloads', + }, + downloads: 'downloads', + download: 'Download', + repository: 'Repository', + downloadFailed: 'Download failed', + noReadme: 'This plugin does not provide README documentation', + description: 'Description', + tags: 'Tags', + submissionTitle: 'You have a plugin submission under review: {{name}}', + submissionPending: 'Your plugin submission is under review: {{name}}', + submissionApproved: 'Your plugin submission has been approved: {{name}}', + submissionRejected: 'Your plugin submission has been rejected: {{name}}', + clickToRevoke: 'Revoke', + revokeSuccess: 'Revoke success', + revokeFailed: 'Revoke failed', + submissionDetails: 'Plugin Submission Details', + markAsRead: 'Mark as Read', + markAsReadSuccess: 'Marked as read', + markAsReadFailed: 'Mark as read failed', }, pipelines: { title: 'Pipelines', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 1576500f..2294a254 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -189,6 +189,52 @@ const jaJP = { uploadSuccess: 'アップロード成功', uploadFailed: 'アップロード失敗', selectFileToUpload: 'アップロードするプラグインファイルを選択', + fromGithub: 'GitHubから', + fromLocal: 'ローカルから', + fromMarketplace: 'プラグインマーケットから', + }, + market: { + searchPlaceholder: 'プラグインを検索...', + searchResults: '{{count}} 個のプラグインが見つかりました', + totalPlugins: '合計 {{count}} 個のプラグイン', + noPlugins: '利用可能なプラグインがありません', + noResults: '関連するプラグインが見つかりません', + loadingMore: 'さらに読み込み中...', + allLoaded: 'すべてのプラグインが表示されました', + install: 'インストール', + installConfirm: + 'プラグイン "{{name}}" ({{version}}) をインストールしますか?', + downloadComplete: 'プラグイン "{{name}}" のダウンロードが完了しました', + installFailed: 'インストールに失敗しました。後でもう一度お試しください', + loadFailed: + 'プラグインリストの取得に失敗しました。後でもう一度お試しください', + noDescription: '説明がありません', + notFound: 'プラグイン情報が見つかりません', + sortBy: '並び順', + sort: { + recentlyAdded: '最近追加', + recentlyUpdated: '最近更新', + mostDownloads: 'ダウンロード数多', + leastDownloads: 'ダウンロード数少', + }, + downloads: '回ダウンロード', + download: 'ダウンロード', + repository: 'リポジトリ', + downloadFailed: 'ダウンロード失敗', + noReadme: 'このプラグインはREADMEドキュメントを提供していません', + description: '説明', + tags: 'タグ', + submissionTitle: 'プラグインの提出が審査中です: {{name}}', + submissionPending: 'プラグインの提出が審査中です: {{name}}', + submissionApproved: 'プラグインの提出が承認されました: {{name}}', + submissionRejected: 'プラグインの提出が拒否されました: {{name}}', + clickToRevoke: '取り消し', + revokeSuccess: '取り消し成功', + revokeFailed: '取り消し失敗', + submissionDetails: 'プラグイン提出詳細', + markAsRead: '既読', + markAsReadSuccess: '既読に設定しました', + markAsReadFailed: '既読に設定に失敗しました', }, pipelines: { title: 'パイプライン', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 867410f1..67e1a9c8 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -184,6 +184,49 @@ const zhHans = { uploadSuccess: '上传成功', uploadFailed: '上传失败', selectFileToUpload: '选择要上传的插件文件', + fromGithub: '来自 GitHub', + fromLocal: '来自本地', + fromMarketplace: '来自市场', + }, + market: { + searchPlaceholder: '搜索插件...', + searchResults: '搜索到 {{count}} 个插件', + totalPlugins: '共 {{count}} 个插件', + noPlugins: '暂无插件', + noResults: '未找到相关插件', + loadingMore: '加载更多...', + allLoaded: '已显示全部插件', + install: '安装', + installConfirm: '确定要安装插件 "{{name}}" ({{version}}) 吗?', + downloadComplete: '插件 "{{name}}" 下载完成', + installFailed: '安装失败,请稍后重试', + loadFailed: '获取插件列表失败,请稍后重试', + noDescription: '暂无描述', + notFound: '插件信息未找到', + sortBy: '排序方式', + sort: { + recentlyAdded: '最近新增', + recentlyUpdated: '最近更新', + mostDownloads: '最多下载', + leastDownloads: '最少下载', + }, + downloads: '次下载', + download: '下载', + repository: '代码仓库', + downloadFailed: '下载失败', + noReadme: '该插件没有提供 README 文档', + description: '描述', + tags: '标签', + submissionTitle: '您有插件提交正在审核中: {{name}}', + submissionApproved: '您的插件提交已通过审核: {{name}}', + submissionRejected: '您的插件提交已被拒绝: {{name}}', + clickToRevoke: '撤回', + revokeSuccess: '撤回成功', + revokeFailed: '撤回失败', + submissionDetails: '插件提交详情', + markAsRead: '已读', + markAsReadSuccess: '已标记为已读', + markAsReadFailed: '标记为已读失败', }, pipelines: { title: '流水线', From 0ea7609ff174d1524ced8b059b4c0e67464dc8c9 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 16 Aug 2025 23:23:24 +0800 Subject: [PATCH 42/78] perf: frontend --- pkg/plugin/handler.py | 51 ++++++----- web/src/app/home/plugins/page.tsx | 7 +- .../plugin-card/PluginCardComponent.tsx | 85 ++++++++++--------- web/src/app/home/plugins/plugins.module.css | 2 +- web/src/app/infra/http/CloudServiceClient.ts | 5 +- web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + 8 files changed, 88 insertions(+), 65 deletions(-) diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index bc151321..5a8f9d9a 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -3,6 +3,7 @@ from __future__ import annotations import typing from typing import Any import base64 +import traceback import sqlalchemy @@ -48,33 +49,39 @@ class RuntimeConnectionHandler(handler.Handler): install_source = data['install_source'] install_info = data['install_info'] - result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_plugin.PluginSetting) - .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) - .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) - ) - - if result.first() is not None: - # delete plugin setting - await self.ap.persistence_mgr.execute_async( - sqlalchemy.delete(persistence_plugin.PluginSetting) + try: + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_plugin.PluginSetting) .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) ) - # create plugin setting - await self.ap.persistence_mgr.execute_async( - sqlalchemy.insert(persistence_plugin.PluginSetting).values( - plugin_author=plugin_author, - plugin_name=plugin_name, - install_source=install_source, - install_info=install_info, - ) - ) + if result.first() is not None: + # delete plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.delete(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) - return handler.ActionResponse.success( - data={}, - ) + # create plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_plugin.PluginSetting).values( + plugin_author=plugin_author, + plugin_name=plugin_name, + install_source=install_source, + install_info=install_info, + ) + ) + + return handler.ActionResponse.success( + data={}, + ) + except Exception as e: + traceback.print_exc() + return handler.ActionResponse.error( + message=f'Failed to initialize plugin settings: {e}', + ) @self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS) async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index e706e6fe..8274d025 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -293,7 +293,12 @@ export default function PluginConfigPage() { )} {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (
    -

    {t('plugins.askConfirm')}

    +

    + {t('plugins.askConfirm', { + name: installInfo.plugin_name, + version: installInfo.plugin_version, + })} +

    )} {pluginInstallStatus === PluginInstallStatus.INSTALLING && ( diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index bcbc02d4..0aae5f39 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { ExternalLink } from 'lucide-react'; +import { BugIcon, ExternalLink } from 'lucide-react'; import { getCloudServiceClientSync } from '@/app/infra/http'; export default function PluginCardComponent({ @@ -65,48 +65,53 @@ export default function PluginCardComponent({ variant="outline" className="text-[0.7rem] border-orange-400 text-orange-400" > + {t('plugins.debugging')} )} - {cardVO.install_source === 'github' && ( - { - e.stopPropagation(); - window.open(cardVO.install_info.github_url, '_blank'); - }} - > - {t('plugins.fromGithub')} - - - )} - {cardVO.install_source === 'local' && ( - - {t('plugins.fromLocal')} - - )} - {cardVO.install_source === 'marketplace' && ( - { - e.stopPropagation(); - window.open( - getCloudServiceClientSync().getPluginMarketplaceURL( - cardVO.author, - cardVO.name, - ), - '_blank', - ); - }} - > - {t('plugins.fromMarketplace')} - - + {!cardVO.debug && ( + <> + {cardVO.install_source === 'github' && ( + { + e.stopPropagation(); + window.open(cardVO.install_info.github_url, '_blank'); + }} + > + {t('plugins.fromGithub')} + + + )} + {cardVO.install_source === 'local' && ( + + {t('plugins.fromLocal')} + + )} + {cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + window.open( + getCloudServiceClientSync().getPluginMarketplaceURL( + cardVO.author, + cardVO.name, + ), + '_blank', + ); + }} + > + {t('plugins.fromMarketplace')} + + + )} + )}
    diff --git a/web/src/app/home/plugins/plugins.module.css b/web/src/app/home/plugins/plugins.module.css index 54ede1c6..a65be354 100644 --- a/web/src/app/home/plugins/plugins.module.css +++ b/web/src/app/home/plugins/plugins.module.css @@ -13,7 +13,7 @@ padding-right: 0.8rem; padding-top: 2rem; display: grid; - grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr)); gap: 2rem; justify-items: stretch; align-items: start; diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index 6d67316e..f7491d5a 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -21,7 +21,10 @@ export class CloudServiceClient extends BaseHttpClient { sort_order?: string, ): Promise { return this.get('/api/v1/marketplace/plugins', { - params: { page, page_size, sort_by, sort_order }, + page, + page_size, + sort_by, + sort_order, }); } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index ec91858c..071930a0 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -189,6 +189,7 @@ const enUS = { uploadSuccess: 'Upload successful', uploadFailed: 'Upload failed', selectFileToUpload: 'Select plugin file to upload', + askConfirm: 'Are you sure to install plugin "{{name}}" ({{version}})?', fromGithub: 'From GitHub', fromLocal: 'From Local', fromMarketplace: 'From Marketplace', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 2294a254..93d48dc1 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -189,6 +189,7 @@ const jaJP = { uploadSuccess: 'アップロード成功', uploadFailed: 'アップロード失敗', selectFileToUpload: 'アップロードするプラグインファイルを選択', + askConfirm: 'プラグイン "{{name}}" ({{version}}) をインストールしますか?', fromGithub: 'GitHubから', fromLocal: 'ローカルから', fromMarketplace: 'プラグインマーケットから', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 67e1a9c8..688458e8 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -184,6 +184,7 @@ const zhHans = { uploadSuccess: '上传成功', uploadFailed: '上传失败', selectFileToUpload: '选择要上传的插件文件', + askConfirm: '确定要安装插件 "{{name}}" ({{version}}) 吗?', fromGithub: '来自 GitHub', fromLocal: '来自本地', fromMarketplace: '来自市场', From 17d997c88e8146534f96e40b2e4a8b36f8ec6745 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 11:43:38 +0800 Subject: [PATCH 43/78] fix: i18n fallback --- web/src/app/infra/entities/common.ts | 1 + web/src/i18n/I18nProvider.tsx | 33 ++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/web/src/app/infra/entities/common.ts b/web/src/app/infra/entities/common.ts index 35dcc9f7..64331738 100644 --- a/web/src/app/infra/entities/common.ts +++ b/web/src/app/infra/entities/common.ts @@ -1,6 +1,7 @@ export interface I18nObject { en_US: string; zh_Hans: string; + zh_Hant?: string; ja_JP?: string; } diff --git a/web/src/i18n/I18nProvider.tsx b/web/src/i18n/I18nProvider.tsx index 2a78941d..55fcd4c8 100644 --- a/web/src/i18n/I18nProvider.tsx +++ b/web/src/i18n/I18nProvider.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; import '@/i18n'; import { I18nObject } from '@/app/infra/entities/common'; +import i18n from 'i18next'; interface I18nProviderProps { children: ReactNode; @@ -11,10 +12,28 @@ interface I18nProviderProps { export default function I18nProvider({ children }: I18nProviderProps) { return <>{children}; } -export function extractI18nObject(i18nLabel: I18nObject): string { - const language = localStorage.getItem('langbot_language'); - if ((language === 'zh-Hans' && i18nLabel.zh_Hans) || !i18nLabel.en_US) { - return i18nLabel.zh_Hans; - } - return i18nLabel.en_US; -} +// export function extractI18nObject(i18nLabel: I18nObject): string { +// const language = localStorage.getItem('langbot_language'); +// if ((language === 'zh-Hans' && i18nLabel.zh_Hans) || !i18nLabel.en_US) { +// return i18nLabel.zh_Hans; +// } +// return i18nLabel.en_US; +// } + +export const extractI18nObject = (i18nObject: I18nObject): string => { + // 根据当前语言返回对应的值, fallback优先级:en_US、zh_Hans、zh_Hant、ja_JP + const language = i18n.language.replace('-', '_'); + console.log('language:', language); + console.log('i18nObject:', i18nObject); + if (language === 'en_US' && i18nObject.en_US) return i18nObject.en_US; + if (language === 'zh_Hans' && i18nObject.zh_Hans) return i18nObject.zh_Hans; + if (language === 'zh_Hant' && i18nObject.zh_Hant) return i18nObject.zh_Hant; + if (language === 'ja_JP' && i18nObject.ja_JP) return i18nObject.ja_JP; + return ( + i18nObject.en_US || + i18nObject.zh_Hans || + i18nObject.zh_Hant || + i18nObject.ja_JP || + '' + ); +}; From a0c42a5f6ee43cad5fbe1d3932510922f98d3ce9 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 16:51:44 +0800 Subject: [PATCH 44/78] feat: plugin operations --- pyproject.toml | 2 +- web/package.json | 1 + .../plugins/plugin-installed/PluginCardVO.ts | 11 +- .../PluginInstalledComponent.tsx | 140 +++++++- .../plugin-card/PluginCardComponent.tsx | 310 +++++++++++------- .../plugin-form/PluginForm.tsx | 131 -------- web/src/app/infra/entities/plugin/index.ts | 12 +- web/src/i18n/locales/zh-Hans.ts | 5 +- 8 files changed, 343 insertions(+), 269 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c371f1c9..d960936e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.1a1", + "langbot-plugin==0.1.1a2", ] keywords = [ "bot", diff --git a/web/package.json b/web/package.json index 36b25dae..952a6fe6 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^5.0.1", "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-dialog": "^1.1.14", diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts index 11ce2154..e230bec4 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -1,3 +1,5 @@ +import { PluginComponent } from '@/app/infra/entities/plugin'; + export interface IPluginCardVO { author: string; name: string; @@ -8,8 +10,7 @@ export interface IPluginCardVO { install_source: string; install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; - tools: object[]; - event_handlers: object; + components: PluginComponent[]; debug: boolean; } @@ -24,18 +25,16 @@ export class PluginCardVO implements IPluginCardVO { install_source: string; install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; - tools: object[]; - event_handlers: object; + components: PluginComponent[]; constructor(prop: IPluginCardVO) { this.author = prop.author; this.description = prop.description; this.enabled = prop.enabled; - this.event_handlers = prop.event_handlers; + this.components = prop.components; this.name = prop.name; this.priority = prop.priority; this.status = prop.status; - this.tools = prop.tools; this.version = prop.version; this.debug = prop.debug; this.install_source = prop.install_source; diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 732e15ab..81428e36 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -11,14 +11,24 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogDescription, + DialogFooter, } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; +import { toast } from 'sonner'; export interface PluginInstalledComponentRef { refreshPluginList: () => void; } +enum PluginRemoveStatus { + WAIT_INPUT = 'WAIT_INPUT', + REMOVING = 'REMOVING', + ERROR = 'ERROR', +} + // eslint-disable-next-line react/display-name const PluginInstalledComponent = forwardRef( (props, ref) => { @@ -28,6 +38,15 @@ const PluginInstalledComponent = forwardRef( const [selectedPlugin, setSelectedPlugin] = useState( null, ); + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const [pluginRemoveStatus, setPluginRemoveStatus] = + useState(PluginRemoveStatus.WAIT_INPUT); + const [pluginRemoveError, setPluginRemoveError] = useState( + null, + ); + const [pluginToDelete, setPluginToDelete] = useState( + null, + ); useEffect(() => { initData(); @@ -55,8 +74,7 @@ const PluginInstalledComponent = forwardRef( name: plugin.manifest.manifest.metadata.name, version: plugin.manifest.manifest.metadata.version ?? '', status: plugin.status, - tools: [], - event_handlers: {}, + components: plugin.components, priority: plugin.priority, install_source: plugin.install_source, install_info: plugin.install_info, @@ -75,8 +93,125 @@ const PluginInstalledComponent = forwardRef( setModalOpen(true); } + function handlePluginDelete(plugin: PluginCardVO) { + setPluginToDelete(plugin); + setShowDeleteConfirmModal(true); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + } + + function deletePlugin() { + setPluginRemoveStatus(PluginRemoveStatus.REMOVING); + httpClient + .removePlugin(pluginToDelete!.author, pluginToDelete!.name) + .then((res) => { + const taskId = res.task_id; + + let alreadySuccess = false; + + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((res) => { + if (res.runtime.done) { + clearInterval(interval); + if (res.runtime.exception) { + setPluginRemoveError(res.runtime.exception); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success('插件删除成功'); + alreadySuccess = true; + } + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + setShowDeleteConfirmModal(false); + } + } + }); + }, 1000); + }) + .catch((error) => { + setPluginRemoveError(error.message); + setPluginRemoveStatus(PluginRemoveStatus.ERROR); + }); + } + return ( <> + { + if (!open) { + setShowDeleteConfirmModal(false); + setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); + setPluginToDelete(null); + } + }} + > + + + {t('plugins.deleteConfirm')} + + + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( +
    + {t('plugins.confirmDeletePlugin', { + author: pluginToDelete?.author ?? '', + name: pluginToDelete?.name ?? '', + })} +
    + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( +
    {t('plugins.deleting')}
    + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( +
    + {t('plugins.deleteError')} +
    {pluginRemoveError}
    +
    + )} +
    + + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( + + )} + {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + + )} + +
    +
    + {pluginList.length === 0 ? (
    ( handlePluginClick(vo)} + onDeleteClick={() => handlePluginDelete(vo)} />
    ); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index 0aae5f39..cb6fb7a7 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -5,15 +5,85 @@ import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { BugIcon, ExternalLink } from 'lucide-react'; +import { TFunction } from 'i18next'; +import { + AudioWaveform, + Wrench, + Hash, + BugIcon, + ExternalLink, + Ellipsis, + Trash, +} from 'lucide-react'; import { getCloudServiceClientSync } from '@/app/infra/http'; +import { PluginComponent } from '@/app/infra/entities/plugin'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +enum PluginRemoveStatus { + WAIT_INPUT = 'WAIT_INPUT', + REMOVING = 'REMOVING', + ERROR = 'ERROR', +} + +function getComponentList(components: PluginComponent[], t: TFunction) { + const componentKindCount: Record = {}; + + for (const component of components) { + const kind = component.manifest.manifest.kind; + if (componentKindCount[kind]) { + componentKindCount[kind]++; + } else { + componentKindCount[kind] = 1; + } + } + + const kindIconMap: Record = { + Tool: , + EventListener: , + Command: , + }; + + const componentKindList = Object.keys(componentKindCount); + + return ( + <> +
    {t('plugins.componentsList')}
    + {componentKindList.length > 0 && ( + <> + {componentKindList.map((kind) => { + return ( +
    + {kindIconMap[kind]} {componentKindCount[kind]} +
    + ); + })} + + )} + + {componentKindList.length === 0 &&
    {t('plugins.noComponents')}
    } + + ); +} export default function PluginCardComponent({ cardVO, onCardClick, + onDeleteClick, }: { cardVO: PluginCardVO; onCardClick: () => void; + onDeleteClick: (cardVO: PluginCardVO) => void; }) { const { t } = useTranslation(); const [enabled, setEnabled] = useState(cardVO.enabled); @@ -34,143 +104,137 @@ export default function PluginCardComponent({ setSwitchEnable(true); }); } + return ( -
    -
    - - - + <> +
    +
    + + + -
    -
    +
    -
    - {cardVO.author} /{' '} -
    -
    -
    {cardVO.name}
    - - v{cardVO.version} - - {cardVO.debug && ( - - - {t('plugins.debugging')} +
    +
    + {cardVO.author} /{' '} +
    +
    +
    {cardVO.name}
    + + v{cardVO.version} - )} - {!cardVO.debug && ( - <> - {cardVO.install_source === 'github' && ( - { - e.stopPropagation(); - window.open(cardVO.install_info.github_url, '_blank'); - }} - > - {t('plugins.fromGithub')} - - - )} - {cardVO.install_source === 'local' && ( - - {t('plugins.fromLocal')} - - )} - {cardVO.install_source === 'marketplace' && ( - { - e.stopPropagation(); - window.open( - getCloudServiceClientSync().getPluginMarketplaceURL( - cardVO.author, - cardVO.name, - ), - '_blank', - ); - }} - > - {t('plugins.fromMarketplace')} - - - )} - - )} + {cardVO.debug && ( + + + {t('plugins.debugging')} + + )} + {!cardVO.debug && ( + <> + {cardVO.install_source === 'github' && ( + { + e.stopPropagation(); + window.open( + cardVO.install_info.github_url, + '_blank', + ); + }} + > + {t('plugins.fromGithub')} + + + )} + {cardVO.install_source === 'local' && ( + + {t('plugins.fromLocal')} + + )} + {cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + window.open( + getCloudServiceClientSync().getPluginMarketplaceURL( + cardVO.author, + cardVO.name, + ), + '_blank', + ); + }} + > + {t('plugins.fromMarketplace')} + + + )} + + )} +
    +
    + +
    + {cardVO.description}
    -
    - {cardVO.description} +
    + {getComponentList(cardVO.components, t)}
    -
    -
    - - - -
    - {t('plugins.eventCount', { - count: Object.keys(cardVO.event_handlers).length, - })} -
    +
    +
    + handleEnable(e)} + disabled={!switchEnable} + />
    -
    - - - -
    - {t('plugins.toolCount', { count: cardVO.tools.length })} -
    +
    + + + + + + { + onDeleteClick(cardVO); + e.stopPropagation(); + }} + > + + {t('plugins.delete')} + + +
    - -
    -
    - handleEnable(e)} - disabled={!switchEnable} - /> -
    - -
    - {/* */} -
    -
    -
    + ); } diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 081253b6..909e87c2 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -16,12 +16,6 @@ import { toast } from 'sonner'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { useTranslation } from 'react-i18next'; -enum PluginRemoveStatus { - WAIT_INPUT = 'WAIT_INPUT', - REMOVING = 'REMOVING', - ERROR = 'ERROR', -} - export default function PluginForm({ pluginAuthor, pluginName, @@ -38,13 +32,6 @@ export default function PluginForm({ const [pluginConfig, setPluginConfig] = useState(); const [isSaving, setIsLoading] = useState(false); - const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); - const [pluginRemoveStatus, setPluginRemoveStatus] = - useState(PluginRemoveStatus.WAIT_INPUT); - const [pluginRemoveError, setPluginRemoveError] = useState( - null, - ); - useEffect(() => { // 获取插件信息 httpClient.getPlugin(pluginAuthor, pluginName).then((res) => { @@ -76,113 +63,8 @@ export default function PluginForm({ return
    {t('plugins.loading')}
    ; } - function deletePlugin() { - setPluginRemoveStatus(PluginRemoveStatus.REMOVING); - httpClient - .removePlugin(pluginAuthor, pluginName) - .then((res) => { - const taskId = res.task_id; - - let alreadySuccess = false; - - const interval = setInterval(() => { - httpClient.getAsyncTask(taskId).then((res) => { - if (res.runtime.done) { - clearInterval(interval); - if (res.runtime.exception) { - setPluginRemoveError(res.runtime.exception); - setPluginRemoveStatus(PluginRemoveStatus.ERROR); - } else { - // success - if (!alreadySuccess) { - toast.success('插件删除成功'); - alreadySuccess = true; - } - setPluginRemoveStatus(PluginRemoveStatus.WAIT_INPUT); - setShowDeleteConfirmModal(false); - onFormSubmit(); - } - } - }); - }, 1000); - }) - .catch((error) => { - setPluginRemoveError(error.message); - setPluginRemoveStatus(PluginRemoveStatus.ERROR); - }); - } - return (
    - - - - {t('plugins.deleteConfirm')} - - - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( -
    - {t('plugins.confirmDeletePlugin', { - author: pluginAuthor, - name: pluginName, - })} -
    - )} - {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( -
    {t('plugins.deleting')}
    - )} - {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( -
    - {t('plugins.deleteError')} -
    {pluginRemoveError}
    -
    - )} -
    - - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( - - )} - {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( - - )} - -
    -
    -
    {extractI18nObject(pluginInfo.manifest.manifest.metadata.label)} @@ -220,19 +102,6 @@ export default function PluginForm({
    - - )} - {pluginRemoveStatus === PluginRemoveStatus.WAIT_INPUT && ( + {asyncTask.status === AsyncTaskStatus.WAIT_INPUT && ( )} - {pluginRemoveStatus === PluginRemoveStatus.REMOVING && ( - )} - {pluginRemoveStatus === PluginRemoveStatus.ERROR && ( + {asyncTask.status === AsyncTaskStatus.ERROR && (
    ); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index cb6fb7a7..9b6152d0 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -14,6 +14,7 @@ import { ExternalLink, Ellipsis, Trash, + ArrowUp, } from 'lucide-react'; import { getCloudServiceClientSync } from '@/app/infra/http'; import { PluginComponent } from '@/app/infra/entities/plugin'; @@ -22,17 +23,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -enum PluginRemoveStatus { - WAIT_INPUT = 'WAIT_INPUT', - REMOVING = 'REMOVING', - ERROR = 'ERROR', -} - function getComponentList(components: PluginComponent[], t: TFunction) { const componentKindCount: Record = {}; @@ -80,14 +73,17 @@ export default function PluginCardComponent({ cardVO, onCardClick, onDeleteClick, + onUpgradeClick, }: { cardVO: PluginCardVO; onCardClick: () => void; onDeleteClick: (cardVO: PluginCardVO) => void; + onUpgradeClick: (cardVO: PluginCardVO) => void; }) { const { t } = useTranslation(); const [enabled, setEnabled] = useState(cardVO.enabled); const [switchEnable, setSwitchEnable] = useState(true); + const [dropdownOpen, setDropdownOpen] = useState(false); function handleEnable(e: React.MouseEvent) { e.stopPropagation(); // 阻止事件冒泡 @@ -212,18 +208,33 @@ export default function PluginCardComponent({
    - + + {/**upgrade */} + {cardVO.install_source === 'marketplace' && ( + { + e.stopPropagation(); + onUpgradeClick(cardVO); + setDropdownOpen(false); + }} + > + + {t('plugins.update')} + + )} { - onDeleteClick(cardVO); e.stopPropagation(); + onDeleteClick(cardVO); + setDropdownOpen(false); }} > diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 909e87c2..82c1a5e8 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -4,14 +4,6 @@ import { Plugin } from '@/app/infra/entities/plugin'; import { httpClient } from '@/app/infra/http/HttpClient'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; import { toast } from 'sonner'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { useTranslation } from 'react-i18next'; @@ -60,7 +52,11 @@ export default function PluginForm({ }; if (!pluginInfo || !pluginConfig) { - return
    {t('plugins.loading')}
    ; + return ( +
    + {t('plugins.loading')} +
    + ); } return ( diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index d077abf6..675a5094 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -240,13 +240,6 @@ export class BackendClient extends BaseHttpClient { return this.put('/api/v1/plugins/reorder', { plugins }); } - public updatePlugin( - author: string, - name: string, - ): Promise { - return this.post(`/api/v1/plugins/${author}/${name}/update`); - } - public installPluginFromGithub( source: string, ): Promise { @@ -278,6 +271,13 @@ export class BackendClient extends BaseHttpClient { return this.delete(`/api/v1/plugins/${author}/${name}`); } + public upgradePlugin( + author: string, + name: string, + ): Promise { + return this.post(`/api/v1/plugins/${author}/${name}/upgrade`); + } + // ============ System API ============ public getSystemInfo(): Promise { return this.get('/api/v1/system/info'); diff --git a/web/src/hooks/useAsyncTask.ts b/web/src/hooks/useAsyncTask.ts new file mode 100644 index 00000000..085c3b97 --- /dev/null +++ b/web/src/hooks/useAsyncTask.ts @@ -0,0 +1,99 @@ +import { useState, useEffect, useRef } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { AsyncTask } from '@/app/infra/entities/api'; + +export enum AsyncTaskStatus { + WAIT_INPUT = 'WAIT_INPUT', + RUNNING = 'RUNNING', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} + +export interface UseAsyncTaskOptions { + onSuccess?: () => void; + onError?: (error: string) => void; + pollInterval?: number; +} + +export interface UseAsyncTaskResult { + status: AsyncTaskStatus; + error: string | null; + startTask: (taskId: number) => void; + reset: () => void; +} + +export function useAsyncTask( + options: UseAsyncTaskOptions = {}, +): UseAsyncTaskResult { + const { onSuccess, onError, pollInterval = 1000 } = options; + + const [status, setStatus] = useState( + AsyncTaskStatus.WAIT_INPUT, + ); + const [error, setError] = useState(null); + const intervalRef = useRef(null); + const alreadySuccessRef = useRef(false); + + const clearPollingInterval = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + const reset = () => { + clearPollingInterval(); + setStatus(AsyncTaskStatus.WAIT_INPUT); + setError(null); + alreadySuccessRef.current = false; + }; + + const startTask = (taskId: number) => { + setStatus(AsyncTaskStatus.RUNNING); + setError(null); + alreadySuccessRef.current = false; + + const interval = setInterval(() => { + httpClient + .getAsyncTask(taskId) + .then((res: AsyncTask) => { + if (res.runtime.done) { + clearPollingInterval(); + if (res.runtime.exception) { + setError(res.runtime.exception); + setStatus(AsyncTaskStatus.ERROR); + onError?.(res.runtime.exception); + } else { + if (!alreadySuccessRef.current) { + alreadySuccessRef.current = true; + setStatus(AsyncTaskStatus.SUCCESS); + onSuccess?.(); + } + } + } + }) + .catch((error) => { + clearPollingInterval(); + const errorMessage = error.message || 'Unknown error'; + setError(errorMessage); + setStatus(AsyncTaskStatus.ERROR); + onError?.(errorMessage); + }); + }, pollInterval); + + intervalRef.current = interval; + }; + + useEffect(() => { + return () => { + clearPollingInterval(); + }; + }, []); + + return { + status, + error, + startTask, + reset, + }; +} diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 071930a0..c0d74898 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -175,6 +175,7 @@ const enUS = { deleteError: 'Delete failed: ', close: 'Close', deleteConfirm: 'Delete Confirmation', + deleteSuccess: 'Delete successful', modifyFailed: 'Modify failed: ', eventCount: 'Events: {{count}}', toolCount: 'Tools: {{count}}', @@ -193,6 +194,17 @@ const enUS = { fromGithub: 'From GitHub', fromLocal: 'From Local', fromMarketplace: 'From Marketplace', + componentsList: 'Components: ', + noComponents: 'No components', + delete: 'Delete Plugin', + update: 'Update Plugin', + updateConfirm: 'Update Confirmation', + confirmUpdatePlugin: + 'Are you sure you want to update the plugin ({{author}}/{{name}})?', + confirmUpdate: 'Confirm Update', + updating: 'Updating...', + updateSuccess: 'Plugin updated successfully', + updateError: 'Update failed: ', }, market: { searchPlaceholder: 'Search plugins...', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 93d48dc1..596ce4eb 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -175,6 +175,7 @@ const jaJP = { deleteError: '削除に失敗しました:', close: '閉じる', deleteConfirm: '削除の確認', + deleteSuccess: '削除に成功しました', modifyFailed: '変更に失敗しました:', eventCount: 'イベント:{{count}}', toolCount: 'ツール:{{count}}', @@ -193,6 +194,17 @@ const jaJP = { fromGithub: 'GitHubから', fromLocal: 'ローカルから', fromMarketplace: 'プラグインマーケットから', + componentsList: '部品:', + noComponents: '部品がありません', + delete: 'プラグインを削除', + update: 'プラグインを更新', + updateConfirm: '更新の確認', + confirmUpdatePlugin: + 'プラグイン「{{author}}/{{name}}」を更新してもよろしいですか?', + confirmUpdate: '更新を確認', + updating: '更新中...', + updateSuccess: 'プラグインの更新に成功しました', + updateError: '更新に失敗しました:', }, market: { searchPlaceholder: 'プラグインを検索...', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 066a277e..2c85d45d 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -171,6 +171,7 @@ const zhHans = { deleteError: '删除失败:', close: '关闭', deleteConfirm: '删除确认', + deleteSuccess: '删除成功', modifyFailed: '修改失败:', eventCount: '事件:{{count}}', toolCount: '工具:{{count}}', @@ -190,7 +191,14 @@ const zhHans = { fromMarketplace: '来自市场', componentsList: '组件: ', noComponents: '无组件', - delete: '删除', + delete: '删除插件', + update: '更新插件', + updateConfirm: '更新确认', + confirmUpdatePlugin: '你确定要更新插件({{author}}/{{name}})吗?', + confirmUpdate: '确认更新', + updating: '更新中...', + updateSuccess: '插件更新成功', + updateError: '更新失败:', }, market: { searchPlaceholder: '搜索插件...', From 8b2480ad3b81f495e8f042703985775976ff0be5 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 17 Aug 2025 21:01:43 +0800 Subject: [PATCH 46/78] feat: setting plugin config --- pkg/api/http/controller/groups/plugins.py | 20 ++----------- pkg/plugin/connector.py | 3 ++ pkg/plugin/handler.py | 23 ++++++++++++++ .../PluginInstalledComponent.tsx | 10 +++++-- .../plugin-card/PluginCardComponent.tsx | 30 +------------------ .../plugin-form/PluginForm.tsx | 13 +++++--- web/src/app/infra/http/BackendClient.ts | 15 ---------- web/src/i18n/locales/en-US.ts | 6 +++- web/src/i18n/locales/ja-JP.ts | 6 +++- web/src/i18n/locales/zh-Hans.ts | 6 +++- 10 files changed, 61 insertions(+), 71 deletions(-) diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 7825c251..670f13ed 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -17,17 +17,6 @@ class PluginsRouterGroup(group.RouterGroup): return self.success(data={'plugins': plugins}) - @self.route( - '///toggle', - methods=['PUT'], - auth_type=group.AuthType.USER_TOKEN, - ) - async def _(author: str, plugin_name: str) -> str: - data = await quart.request.json - target_enabled = data.get('target_enabled') - await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled) - return self.success() - @self.route( '///upgrade', methods=['POST'], @@ -76,21 +65,16 @@ class PluginsRouterGroup(group.RouterGroup): plugin = await self.ap.plugin_connector.get_plugin_info(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) + await self.ap.plugin_connector.set_plugin_config(author, plugin_name, data) return self.success(data={}) - @self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN) - async def _() -> str: - data = await quart.request.json - await self.ap.plugin_mgr.reorder_plugins(data.get('plugins')) - return self.success() - @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: data = await quart.request.json diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 8c2a91ad..dcab8b8c 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -150,6 +150,9 @@ class PluginRuntimeConnector: async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]: return await self.handler.get_plugin_info(author, plugin_name) + async def set_plugin_config(self, plugin_author: str, plugin_name: str, config: dict[str, Any]) -> dict[str, Any]: + return await self.handler.set_plugin_config(plugin_author, plugin_name, config) + async def emit_event( self, event: events.BaseEventModel, diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 4095ea86..5ec36e68 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -486,6 +486,29 @@ class RuntimeConnectionHandler(handler.Handler): ) return result['plugin'] + async def set_plugin_config(self, plugin_author: str, plugin_name: str, config: dict[str, Any]) -> dict[str, Any]: + """Set plugin config""" + # update plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.update(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + .values(config=config) + ) + + # restart plugin + gen = self.call_action_generator( + LangBotToRuntimeAction.RESTART_PLUGIN, + { + 'plugin_author': plugin_author, + 'plugin_name': plugin_name, + }, + ) + async for ret in gen: + pass + + return {} + async def emit_event( self, event_context: dict[str, Any], diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 0f13618f..52c49d7a 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -271,9 +271,15 @@ const PluginInstalledComponent = forwardRef( { + onFormSubmit={(timeout?: number) => { setModalOpen(false); - getPluginList(); + if (timeout) { + setTimeout(() => { + getPluginList(); + }, timeout); + } else { + getPluginList(); + } }} onFormCancel={() => { setModalOpen(false); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index 9b6152d0..5772a3f6 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -1,9 +1,6 @@ import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; import { useState } from 'react'; -import { httpClient } from '@/app/infra/http/HttpClient'; import { Badge } from '@/components/ui/badge'; -import { Switch } from '@/components/ui/switch'; -import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { TFunction } from 'i18next'; import { @@ -81,26 +78,8 @@ export default function PluginCardComponent({ onUpgradeClick: (cardVO: PluginCardVO) => void; }) { const { t } = useTranslation(); - const [enabled, setEnabled] = useState(cardVO.enabled); - const [switchEnable, setSwitchEnable] = useState(true); const [dropdownOpen, setDropdownOpen] = useState(false); - function handleEnable(e: React.MouseEvent) { - e.stopPropagation(); // 阻止事件冒泡 - setSwitchEnable(false); - httpClient - .togglePlugin(cardVO.author, cardVO.name, !enabled) - .then(() => { - setEnabled(!enabled); - }) - .catch((err) => { - toast.error(t('plugins.modifyFailed') + err.message); - }) - .finally(() => { - setSwitchEnable(true); - }); - } - return ( <>
    -
    - handleEnable(e)} - disabled={!switchEnable} - /> -
    +
    diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index 82c1a5e8..09a79d2f 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -16,7 +16,7 @@ export default function PluginForm({ }: { pluginAuthor: string; pluginName: string; - onFormSubmit: () => void; + onFormSubmit: (timeout?: number) => void; onFormCancel: () => void; }) { const { t } = useTranslation(); @@ -37,14 +37,19 @@ export default function PluginForm({ const handleSubmit = async (values: object) => { setIsLoading(true); + const isDebugPlugin = pluginInfo?.debug; httpClient .updatePluginConfig(pluginAuthor, pluginName, values) .then(() => { - onFormSubmit(); - toast.success('保存成功'); + toast.success( + isDebugPlugin + ? t('plugins.saveConfigSuccessDebugPlugin') + : t('plugins.saveConfigSuccessNormal'), + ); + onFormSubmit(1000); }) .catch((error) => { - toast.error('保存失败:' + error.message); + toast.error(t('plugins.saveConfigError') + error.message); }) .finally(() => { setIsLoading(false); diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 675a5094..a74cfd76 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -15,7 +15,6 @@ import { ApiRespPlugins, ApiRespPlugin, ApiRespPluginConfig, - PluginReorderElement, AsyncTaskCreatedResp, ApiRespSystemInfo, ApiRespAsyncTasks, @@ -226,20 +225,6 @@ export class BackendClient extends BaseHttpClient { return this.put(`/api/v1/plugins/${author}/${name}/config`, config); } - public togglePlugin( - author: string, - name: string, - target_enabled: boolean, - ): Promise { - return this.put(`/api/v1/plugins/${author}/${name}/toggle`, { - target_enabled, - }); - } - - public reorderPlugins(plugins: PluginReorderElement[]): Promise { - return this.put('/api/v1/plugins/reorder', { plugins }); - } - public installPluginFromGithub( source: string, ): Promise { diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index c0d74898..8b697df4 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -170,7 +170,7 @@ const enUS = { saveConfig: 'Save Config', saving: 'Saving...', confirmDeletePlugin: - 'Are you sure you want to delete the plugin ({{author}}/{{name}})?', + 'Are you sure you want to delete the plugin ({{author}}/{{name}})? This will also delete the plugin configuration.', confirmDelete: 'Confirm Delete', deleteError: 'Delete failed: ', close: 'Close', @@ -205,6 +205,10 @@ const enUS = { updating: 'Updating...', updateSuccess: 'Plugin updated successfully', updateError: 'Update failed: ', + saveConfigSuccessNormal: 'Configuration saved successfully', + saveConfigSuccessDebugPlugin: + 'Configuration saved successfully, please manually restart the plugin', + saveConfigError: 'Configuration save failed: ', }, market: { searchPlaceholder: 'Search plugins...', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 596ce4eb..a4a02af3 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -170,7 +170,7 @@ const jaJP = { saveConfig: '設定を保存', saving: '保存中...', confirmDeletePlugin: - 'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?', + 'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?この操作により、プラグインの設定も削除されます。', confirmDelete: '削除を確認', deleteError: '削除に失敗しました:', close: '閉じる', @@ -205,6 +205,10 @@ const jaJP = { updating: '更新中...', updateSuccess: 'プラグインの更新に成功しました', updateError: '更新に失敗しました:', + saveConfigSuccessNormal: '設定を保存しました', + saveConfigSuccessDebugPlugin: + '設定を保存しました。手動でプラグインを再起動してください', + saveConfigError: '設定の保存に失敗しました:', }, market: { searchPlaceholder: 'プラグインを検索...', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 2c85d45d..ce37ccc5 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -166,7 +166,8 @@ const zhHans = { cancel: '取消', saveConfig: '保存配置', saving: '保存中...', - confirmDeletePlugin: '你确定要删除插件({{author}}/{{name}})吗?', + confirmDeletePlugin: + '你确定要删除插件({{author}}/{{name}})吗?这将同时删除插件的配置。', confirmDelete: '确认删除', deleteError: '删除失败:', close: '关闭', @@ -199,6 +200,9 @@ const zhHans = { updating: '更新中...', updateSuccess: '插件更新成功', updateError: '更新失败:', + saveConfigSuccessNormal: '保存配置成功', + saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件', + saveConfigError: '保存配置失败:', }, market: { searchPlaceholder: '搜索插件...', From 53ade384ebb421a2808785f36b6334758a529139 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 20 Aug 2025 23:26:32 +0800 Subject: [PATCH 47/78] feat: bump version of langbot-plugin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d960936e..2e2d7c3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.1a2", + "langbot-plugin==0.1.1b1", ] keywords = [ "bot", From 9e9bc88473028416a6d8d436347b5e4f3eb98156 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 21 Aug 2025 10:47:53 +0800 Subject: [PATCH 48/78] chore: remove plugin reorder functionality --- web/src/app/home/plugins/page.tsx | 14 +- .../plugins/plugin-sort/PluginSortDialog.tsx | 390 +++++++++--------- 2 files changed, 205 insertions(+), 199 deletions(-) diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 8274d025..40a666eb 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -3,7 +3,7 @@ import PluginInstalledComponent, { PluginInstalledComponentRef, } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent'; -import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; +// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; @@ -44,7 +44,7 @@ enum PluginInstallStatus { export default function PluginConfigPage() { const { t } = useTranslation(); const [modalOpen, setModalOpen] = useState(false); - const [sortModalOpen, setSortModalOpen] = useState(false); + // const [sortModalOpen, setSortModalOpen] = useState(false); const [activeTab, setActiveTab] = useState('installed'); const [installSource, setInstallSource] = useState('local'); const [installInfo, setInstallInfo] = useState>({}); // eslint-disable-line @typescript-eslint/no-explicit-any @@ -219,15 +219,15 @@ export default function PluginConfigPage() {
    - + */}
    )} - { pluginInstalledRef.current?.refreshPluginList(); }} - /> + /> */} ); } diff --git a/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx index acde5741..998ae93e 100644 --- a/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx +++ b/web/src/app/home/plugins/plugin-sort/PluginSortDialog.tsx @@ -1,209 +1,215 @@ -'use client'; +// 'use client'; -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; -import { httpClient } from '@/app/infra/http/HttpClient'; -import { PluginReorderElement } from '@/app/infra/entities/api'; -import { toast } from 'sonner'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { useTranslation } from 'react-i18next'; -import { extractI18nObject } from '@/i18n/I18nProvider'; +// import * as React from 'react'; +// import { useState, useEffect } from 'react'; +// import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; +// import { httpClient } from '@/app/infra/http/HttpClient'; +// import { PluginReorderElement } from '@/app/infra/entities/api'; +// import { toast } from 'sonner'; +// import { +// Dialog, +// DialogContent, +// DialogHeader, +// DialogTitle, +// DialogFooter, +// } from '@/components/ui/dialog'; +// import { Button } from '@/components/ui/button'; +// import { +// DndContext, +// closestCenter, +// KeyboardSensor, +// PointerSensor, +// useSensor, +// useSensors, +// DragEndEvent, +// } from '@dnd-kit/core'; +// import { +// arrayMove, +// SortableContext, +// sortableKeyboardCoordinates, +// useSortable, +// verticalListSortingStrategy, +// } from '@dnd-kit/sortable'; +// import { CSS } from '@dnd-kit/utilities'; +// import { useTranslation } from 'react-i18next'; +// import { extractI18nObject } from '@/i18n/I18nProvider'; -interface PluginSortDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onSortComplete: () => void; -} +// interface PluginSortDialogProps { +// open: boolean; +// onOpenChange: (open: boolean) => void; +// onSortComplete: () => void; +// } -function SortablePluginItem({ plugin }: { plugin: PluginCardVO }) { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ - id: `${plugin.author}-${plugin.name}`, - }); +// function SortablePluginItem({ plugin }: { plugin: PluginCardVO }) { +// const { attributes, listeners, setNodeRef, transform, transition } = +// useSortable({ +// id: `${plugin.author}-${plugin.name}`, +// }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; +// const style = { +// transform: CSS.Transform.toString(transform), +// transition, +// }; - return ( -
    -
    -
    - {plugin.author} -
    -
    {plugin.name}
    -
    - {plugin.description} -
    -
    -
    - ); -} +// return ( +//
    +//
    +//
    +// {plugin.author} +//
    +//
    {plugin.name}
    +//
    +// {plugin.description} +//
    +//
    +//
    +// ); +// } -export default function PluginSortDialog({ - open, - onOpenChange, - onSortComplete, -}: PluginSortDialogProps) { - const { t } = useTranslation(); - const [sortedPlugins, setSortedPlugins] = useState([]); - const [isLoading, setIsLoading] = useState(false); +// export default function PluginSortDialog({ +// open, +// onOpenChange, +// onSortComplete, +// }: PluginSortDialogProps) { +// const { t } = useTranslation(); +// const [sortedPlugins, setSortedPlugins] = useState([]); +// const [isLoading, setIsLoading] = useState(false); - function getPluginList() { - httpClient.getPlugins().then((value) => { - setSortedPlugins( - value.plugins.map((plugin) => { - return new PluginCardVO({ - author: plugin.author, - description: extractI18nObject(plugin.description), - enabled: plugin.enabled, - name: plugin.name, - version: plugin.version, - status: plugin.status, - tools: plugin.tools, - event_handlers: plugin.event_handlers, - repository: plugin.repository, - priority: plugin.priority, - }); - }), - ); - }); - } +// function getPluginList() { +// httpClient.getPlugins().then((value) => { +// setSortedPlugins( +// value.plugins.map((plugin) => { +// return new PluginCardVO({ +// author: plugin.manifest.manifest.metadata.author ?? '', +// description: extractI18nObject( +// plugin.manifest.manifest.metadata.description ?? { +// en_US: '', +// zh_Hans: '', +// }, +// ), +// enabled: plugin.enabled, +// name: plugin.manifest.manifest.metadata.name, +// version: plugin.manifest.manifest.metadata.version ?? '', +// status: plugin.status, +// components: plugin.components, +// install_source: plugin.install_source, +// install_info: plugin.install_info, +// priority: plugin.priority, +// debug: plugin.debug, +// }); +// }), +// ); +// }); +// } - useEffect(() => { - if (open) { - getPluginList(); - } - }, [open]); +// useEffect(() => { +// if (open) { +// getPluginList(); +// } +// }, [open]); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); +// const sensors = useSensors( +// useSensor(PointerSensor), +// useSensor(KeyboardSensor, { +// coordinateGetter: sortableKeyboardCoordinates, +// }), +// ); - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event; - console.log('Drag end event:', { active, over }); +// function handleDragEnd(event: DragEndEvent) { +// const { active, over } = event; +// console.log('Drag end event:', { active, over }); - if (over && active.id !== over.id) { - setSortedPlugins((items) => { - const oldIndex = items.findIndex( - (item) => `${item.author}-${item.name}` === active.id, - ); - const newIndex = items.findIndex( - (item) => `${item.author}-${item.name}` === over.id, - ); +// if (over && active.id !== over.id) { +// setSortedPlugins((items) => { +// const oldIndex = items.findIndex( +// (item) => `${item.author}-${item.name}` === active.id, +// ); +// const newIndex = items.findIndex( +// (item) => `${item.author}-${item.name}` === over.id, +// ); - const newItems = arrayMove(items, oldIndex, newIndex); +// const newItems = arrayMove(items, oldIndex, newIndex); - return newItems; - }); - } - } +// return newItems; +// }); +// } +// } - function handleSave() { - setIsLoading(true); +// function handleSave() { +// setIsLoading(true); - const reorderElements: PluginReorderElement[] = sortedPlugins.map( - (plugin, index) => ({ - author: plugin.author, - name: plugin.name, - priority: index, - }), - ); +// const reorderElements: PluginReorderElement[] = sortedPlugins.map( +// (plugin, index) => ({ +// author: plugin.author, +// name: plugin.name, +// priority: index, +// }), +// ); - httpClient - .reorderPlugins(reorderElements) - .then(() => { - toast.success(t('plugins.pluginSortSuccess')); - onSortComplete(); - onOpenChange(false); - }) - .catch((err) => { - toast.error(t('plugins.pluginSortError') + err.message); - }) - .finally(() => { - setIsLoading(false); - }); - } +// httpClient +// .reorderPlugins(reorderElements) +// .then(() => { +// toast.success(t('plugins.pluginSortSuccess')); +// onSortComplete(); +// onOpenChange(false); +// }) +// .catch((err) => { +// toast.error(t('plugins.pluginSortError') + err.message); +// }) +// .finally(() => { +// setIsLoading(false); +// }); +// } - return ( - - - - {t('plugins.pluginSort')} - -
    -

    - {t('plugins.pluginSortDescription')} -

    - - `${plugin.author}-${plugin.name}`, - )} - strategy={verticalListSortingStrategy} - > - {sortedPlugins.map((plugin) => ( - - ))} - - -
    - - - - -
    -
    - ); -} +// return ( +// +// +// +// {t('plugins.pluginSort')} +// +//
    +//

    +// {t('plugins.pluginSortDescription')} +//

    +// +// `${plugin.author}-${plugin.name}`, +// )} +// strategy={verticalListSortingStrategy} +// > +// {sortedPlugins.map((plugin) => ( +// +// ))} +// +// +//
    +// +// +// +// +//
    +//
    +// ); +// } From 4012310d99e3a45ebd2e388cb568756f2c2738e6 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 21 Aug 2025 10:49:51 +0800 Subject: [PATCH 49/78] chore: bump version 4.3.0b1 --- pkg/utils/constants.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 19a92715..5bf9ded2 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = 'v4.0.7' +semantic_version = 'v4.3.0.beta1' required_database_version = 5 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" diff --git a/pyproject.toml b/pyproject.toml index 2e2d7c3b..dfecb57d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langbot" -version = "4.0.7" +version = "4.3.0.beta1" description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台" readme = "README.md" requires-python = ">=3.10.1" From e47a5b4e0dec97df6989538c2f7d4a844af20462 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 17:12:29 +0800 Subject: [PATCH 50/78] chore: bump langbot_plugin version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfecb57d..85f8852e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.1b1", + "langbot-plugin==0.1.1b2", ] keywords = [ "bot", From 0155d3b0b97fe64ef3a9b6c56df4ba46da65003e Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 20:05:24 +0800 Subject: [PATCH 51/78] fix: conflict in table `plugin_settings` --- .../dbm005_plugin_install_source.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/persistence/migrations/dbm005_plugin_install_source.py b/pkg/persistence/migrations/dbm005_plugin_install_source.py index 11547f88..414c7196 100644 --- a/pkg/persistence/migrations/dbm005_plugin_install_source.py +++ b/pkg/persistence/migrations/dbm005_plugin_install_source.py @@ -8,17 +8,24 @@ class DBMigratePluginInstallSource(migration.DBMigration): async def upgrade(self): """升级""" - # add new column install_source, use default value 'github', via alter table - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text( - "ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'" - ) - ) + # 查询表结构获取所有列名(异步执行 SQL) + result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(plugin_settings);')) + # fetchall() 是同步方法,无需 await + columns = [row[1] for row in result.fetchall()] - # add new column install_info, use default value {}, via alter table - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'") - ) + # 检查并添加 install_source 列 + if 'install_source' not in columns: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text( + "ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'" + ) + ) + + # 检查并添加 install_info 列 + if 'install_info' not in columns: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'") + ) async def downgrade(self): """降级""" From 8eb1b8759bf10ab2e8bd49b9f65597c6597b3931 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 20:06:19 +0800 Subject: [PATCH 52/78] chore: bump version to '4.3.0b2' --- pkg/utils/constants.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 5bf9ded2..354cffc3 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = 'v4.3.0.beta1' +semantic_version = 'v4.3.0.beta2' required_database_version = 5 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" diff --git a/pyproject.toml b/pyproject.toml index 85f8852e..6a276af8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langbot" -version = "4.3.0.beta1" +version = "4.3.0.beta2" description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台" readme = "README.md" requires-python = ">=3.10.1" From d3a147bbdda86f1a5f4d16a5ee2c711d3419a0a0 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 20:08:29 +0800 Subject: [PATCH 53/78] chore: bump version 4.3.0b3 --- pkg/utils/constants.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 354cffc3..8916a3a3 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = 'v4.3.0.beta2' +semantic_version = 'v4.3.0.beta3' required_database_version = 5 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" diff --git a/pyproject.toml b/pyproject.toml index 6a276af8..096da14e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langbot" -version = "4.3.0.beta2" +version = "4.3.0.beta3" description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台" readme = "README.md" requires-python = ">=3.10.1" From b5e22c6db85b6310951ebd16dba809f71f291971 Mon Sep 17 00:00:00 2001 From: How-Sean Xin Date: Sat, 23 Aug 2025 20:22:25 +0800 Subject: [PATCH 54/78] Update package.json (#1627) --- web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/package.json b/web/package.json index 952a6fe6..68486bce 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev --turbopack", "dev:local": "NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 next dev --turbopack", + "dev:local:win": "set NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 && next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint", From 2742144e1218832a32d6f94f9eb3a716f79a05fa Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 22:57:46 +0800 Subject: [PATCH 55/78] feat: change standalone runtime tag env --- pkg/plugin/connector.py | 4 ++-- pkg/utils/platform.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index dcab8b8c..6afc0661 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -51,7 +51,7 @@ class PluginRuntimeConnector: async def initialize(self): async def new_connection_callback(connection: base_connection.Connection): async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool: - if platform.get_platform() == 'docker': + if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime(): self.ap.logger.error('Disconnected from plugin runtime, trying to reconnect...') await self.runtime_disconnect_callback(self) return False @@ -69,7 +69,7 @@ class PluginRuntimeConnector: task: asyncio.Task | None = None - if platform.get_platform() == 'docker': # use websocket + if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime(): # use websocket self.ap.logger.info('use websocket to connect to plugin runtime') ws_url = self.ap.instance_config.data['plugin']['runtime_ws_url'] diff --git a/pkg/utils/platform.py b/pkg/utils/platform.py index 0145081a..030fd972 100644 --- a/pkg/utils/platform.py +++ b/pkg/utils/platform.py @@ -12,3 +12,9 @@ def get_platform() -> str: return 'docker' return sys.platform + + +def use_websocket_to_connect_plugin_runtime() -> bool: + """是否使用 websocket 连接插件运行时""" + STANDALONE_RUNTIME = os.environ.get('STANDALONE_RUNTIME', 'false') + return STANDALONE_RUNTIME == 'true' From 118ebddae622241ef84689d8d260285cb426d2b1 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 23 Aug 2025 23:03:32 +0800 Subject: [PATCH 56/78] fix: use --standalone-runtime --- main.py | 6 ++++++ pkg/utils/platform.py | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index b7d62e07..d02d527e 100644 --- a/main.py +++ b/main.py @@ -19,8 +19,14 @@ asciiart = r""" async def main_entry(loop: asyncio.AbstractEventLoop): parser = argparse.ArgumentParser(description='LangBot') parser.add_argument('--skip-plugin-deps-check', action='store_true', help='跳过插件依赖项检查', default=False) + parser.add_argument('--standalone-runtime', action='store_true', help='使用独立插件运行时', default=False) args = parser.parse_args() + if args.standalone_runtime: + from pkg.utils import platform + + platform.standalone_runtime = True + print(asciiart) import sys diff --git a/pkg/utils/platform.py b/pkg/utils/platform.py index 030fd972..b3f7a6df 100644 --- a/pkg/utils/platform.py +++ b/pkg/utils/platform.py @@ -14,7 +14,9 @@ def get_platform() -> str: return sys.platform +standalone_runtime = False + + def use_websocket_to_connect_plugin_runtime() -> bool: """是否使用 websocket 连接插件运行时""" - STANDALONE_RUNTIME = os.environ.get('STANDALONE_RUNTIME', 'false') - return STANDALONE_RUNTIME == 'true' + return standalone_runtime From fd9d1c4acc3e6a7bea1f634cb4bd27b8a40a1cbe Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 24 Aug 2025 11:10:05 +0800 Subject: [PATCH 57/78] feat: update docker launch method --- docker-compose.yaml | 28 ++++++++++++++++++++++++---- pyproject.toml | 2 +- templates/config.yaml | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 6f75e85d..f53f205d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,23 @@ version: "3" services: + + langbot_plugin_runtime: + image: rockchin/langbot:refactor-new-plugin-system + container_name: langbot_plugin_runtime + volumes: + - ./data/plugins:/app/data/plugins + ports: + - 5401:5401 + restart: on-failure + environment: + - TZ=Asia/Shanghai + command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"] + networks: + - langbot_network + langbot: - image: rockchin/langbot:latest + image: rockchin/langbot:refactor-new-plugin-system container_name: langbot volumes: - ./data:/app/data @@ -11,6 +26,11 @@ services: environment: - TZ=Asia/Shanghai ports: - - 5300:5300 # 供 WebUI 使用 - - 2280-2290:2280-2290 # 供消息平台适配器方向连接 - # 根据具体环境配置网络 + - 5300:5300 # For web ui + - 2280-2290:2280-2290 # For platform webhook + networks: + - langbot_network + +networks: + langbot_network: + driver: bridge diff --git a/pyproject.toml b/pyproject.toml index 096da14e..d06f0643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pre-commit>=4.2.0", "uv>=0.7.11", "mypy>=1.16.0", - "langbot-plugin==0.1.1b2", + "langbot-plugin==0.1.1b3", ] keywords = [ "bot", diff --git a/templates/config.yaml b/templates/config.yaml index dd35e39c..c26c86df 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -19,5 +19,5 @@ system: expire: 604800 secret: '' plugin: - runtime_ws_url: 'ws://plugin-runtime:5400/control/ws' + runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws' cloud_service_url: 'https://space.langbot.app' \ No newline at end of file From 6113c42014646cfa11744603c85f95ef9c31775b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 24 Aug 2025 11:15:28 +0800 Subject: [PATCH 58/78] fix: change tag of image to `latest` --- docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index f53f205d..107a9e26 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,7 @@ version: "3" services: langbot_plugin_runtime: - image: rockchin/langbot:refactor-new-plugin-system + image: rockchin/langbot:latest container_name: langbot_plugin_runtime volumes: - ./data/plugins:/app/data/plugins @@ -17,7 +17,7 @@ services: - langbot_network langbot: - image: rockchin/langbot:refactor-new-plugin-system + image: rockchin/langbot:latest container_name: langbot volumes: - ./data:/app/data From f2d5c217126f29be6154f49aa72c6c9293917faf Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 24 Aug 2025 19:59:33 +0800 Subject: [PATCH 59/78] perf: inline code display style in markdown --- .../PluginDetailDialog.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx index 79e4a698..4e9f11f2 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx @@ -241,11 +241,20 @@ export default function PluginDetailDialog({ li: ({ children }) => (
  • {children}
  • ), - code: ({ children }) => ( - - {children} - - ), + code: ({ children, node }) => { + const isInline = + node?.children?.length === 1 && + node?.children[0]?.type === 'text'; + return isInline ? ( + + {children} + + ) : ( + + {children} + + ); + }, blockquote: ({ children }) => (
    {children} From 8a370a260e066647d86f51561de8555be9a7457d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 24 Aug 2025 21:46:20 +0800 Subject: [PATCH 60/78] fix: syntax errors --- .../models/component/embedding-form/EmbeddingForm.tsx | 4 ++-- web/src/app/infra/http/BackendClient.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx index eee02b5a..5acb9ac4 100644 --- a/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx +++ b/web/src/app/home/models/component/embedding-form/EmbeddingForm.tsx @@ -38,7 +38,7 @@ import { SelectValue, } from '@/components/ui/select'; import { toast } from 'sonner'; -import { i18nObj } from '@/i18n/I18nProvider'; +import { extractI18nObject } from '@/i18n/I18nProvider'; const getExtraArgSchema = (t: (key: string) => string) => z @@ -184,7 +184,7 @@ export default function EmbeddingForm({ setRequesterNameList( requesterNameList.requesters.map((item) => { return { - label: i18nObj(item.label), + label: extractI18nObject(item.label), value: item.name, }; }), diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index a4ccc23d..ef270258 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -136,8 +136,15 @@ export class BackendClient extends BaseHttpClient { return this.get('/api/v1/pipelines/_/metadata'); } - public getPipelines(): Promise { - return this.get('/api/v1/pipelines'); + public getPipelines( + sortBy?: string, + sortOrder?: string, + ): Promise { + const params = new URLSearchParams(); + if (sortBy) params.append('sort_by', sortBy); + if (sortOrder) params.append('sort_order', sortOrder); + const queryString = params.toString(); + return this.get(`/api/v1/pipelines${queryString ? `?${queryString}` : ''}`); } public getPipeline(uuid: string): Promise { From 55df728471594f91d000ecb206bf35723d40c77a Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 24 Aug 2025 21:47:54 +0800 Subject: [PATCH 61/78] fix: wrong migration target version --- ...plugin_install_source.py => dbm006_plugin_install_source.py} | 2 +- pkg/utils/constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pkg/persistence/migrations/{dbm005_plugin_install_source.py => dbm006_plugin_install_source.py} (97%) diff --git a/pkg/persistence/migrations/dbm005_plugin_install_source.py b/pkg/persistence/migrations/dbm006_plugin_install_source.py similarity index 97% rename from pkg/persistence/migrations/dbm005_plugin_install_source.py rename to pkg/persistence/migrations/dbm006_plugin_install_source.py index 414c7196..37f74929 100644 --- a/pkg/persistence/migrations/dbm005_plugin_install_source.py +++ b/pkg/persistence/migrations/dbm006_plugin_install_source.py @@ -2,7 +2,7 @@ import sqlalchemy from .. import migration -@migration.migration_class(5) +@migration.migration_class(6) class DBMigratePluginInstallSource(migration.DBMigration): """插件安装来源""" diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 8916a3a3..0da3cc9d 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.3.0.beta3' -required_database_version = 5 +required_database_version = 6 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" debug_mode = False From ea6ce2f552eb832c27d59e9a4667c54368f34399 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 25 Aug 2025 20:56:39 +0800 Subject: [PATCH 62/78] fix: set plugin enabled=true as default --- pkg/plugin/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 5ec36e68..a677b9be 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -97,7 +97,7 @@ class RuntimeConnectionHandler(handler.Handler): ) data = { - 'enabled': False, + 'enabled': True, 'priority': 0, 'plugin_config': {}, 'install_source': 'local', From 56183867a7fe6eda78d4455c2a63554665e1b695 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Mon, 25 Aug 2025 23:22:36 +0800 Subject: [PATCH 63/78] fix: replace message_chain.has usage --- pkg/pipeline/resprule/rules/atbot.py | 29 ++++++++++++++++++---------- pkg/platform/sources/discord.py | 9 ++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/pkg/pipeline/resprule/rules/atbot.py b/pkg/pipeline/resprule/rules/atbot.py index cf31cc31..b35fb5e4 100644 --- a/pkg/pipeline/resprule/rules/atbot.py +++ b/pkg/pipeline/resprule/rules/atbot.py @@ -16,17 +16,26 @@ class AtBotRule(rule_model.GroupRespondRule): rule_dict: dict, query: pipeline_query.Query, ) -> entities.RuleJudgeResult: - if message_chain.has(platform_message.At(query.adapter.bot_account_id)) and rule_dict['at']: - message_chain.remove(platform_message.At(query.adapter.bot_account_id)) + def remove_at(message_chain: platform_message.MessageChain): + for component in message_chain.root: + if isinstance(component, platform_message.At) and component.target == query.adapter.bot_account_id: + message_chain.remove(component) + break - if message_chain.has( - platform_message.At(query.adapter.bot_account_id) - ): # 回复消息时会at两次,检查并删除重复的 - message_chain.remove(platform_message.At(query.adapter.bot_account_id)) + remove_at(message_chain) + remove_at(message_chain) # 回复消息时会at两次,检查并删除重复的 - return entities.RuleJudgeResult( - matching=True, - replacement=message_chain, - ) + # if message_chain.has(platform_message.At(query.adapter.bot_account_id)) and rule_dict['at']: + # message_chain.remove(platform_message.At(query.adapter.bot_account_id)) + + # if message_chain.has( + # platform_message.At(query.adapter.bot_account_id) + # ): # 回复消息时会at两次,检查并删除重复的 + # message_chain.remove(platform_message.At(query.adapter.bot_account_id)) + + # return entities.RuleJudgeResult( + # matching=True, + # replacement=message_chain, + # ) return entities.RuleJudgeResult(matching=False, replacement=message_chain) diff --git a/pkg/platform/sources/discord.py b/pkg/platform/sources/discord.py index 75ad287e..933961de 100644 --- a/pkg/platform/sources/discord.py +++ b/pkg/platform/sources/discord.py @@ -1034,7 +1034,14 @@ class DiscordAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): if quote_origin: args['reference'] = message_source.source_platform_object - if message.has(platform_message.At): + has_at = False + + for component in message.root: + if isinstance(component, platform_message.At): + has_at = True + break + + if has_at: args['mention_author'] = True await message_source.source_platform_object.channel.send(**args) From e7fe41810e80aeed991a262f5b90718fca783051 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 26 Aug 2025 22:40:32 +0800 Subject: [PATCH 64/78] fix: dark mode for plugins management page --- .../plugins/plugin-installed/PluginCardVO.ts | 3 + .../PluginInstalledComponent.tsx | 1 + .../plugin-card/PluginCardComponent.tsx | 12 ++-- .../plugin-market/PluginMarketComponent.tsx | 2 +- .../PluginDetailDialog.tsx | 52 +++++++------- .../PluginMarketCardComponent.tsx | 14 ++-- web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + web/src/i18n/locales/zh-Hant.ts | 67 +++++++++++++++++++ 10 files changed, 120 insertions(+), 34 deletions(-) diff --git a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts index e230bec4..9712cead 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/plugin-installed/PluginCardVO.ts @@ -2,6 +2,7 @@ import { PluginComponent } from '@/app/infra/entities/plugin'; export interface IPluginCardVO { author: string; + label: string; name: string; description: string; version: string; @@ -16,6 +17,7 @@ export interface IPluginCardVO { export class PluginCardVO implements IPluginCardVO { author: string; + label: string; name: string; description: string; version: string; @@ -29,6 +31,7 @@ export class PluginCardVO implements IPluginCardVO { constructor(prop: IPluginCardVO) { this.author = prop.author; + this.label = prop.label; this.description = prop.description; this.enabled = prop.enabled; this.components = prop.components; diff --git a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx index 52c49d7a..5581fc7a 100644 --- a/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/PluginInstalledComponent.tsx @@ -74,6 +74,7 @@ const PluginInstalledComponent = forwardRef( value.plugins.map((plugin) => { return new PluginCardVO({ author: plugin.manifest.manifest.metadata.author ?? '', + label: extractI18nObject(plugin.manifest.manifest.metadata.label), description: extractI18nObject( plugin.manifest.manifest.metadata.description ?? { en_US: '', diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index 5772a3f6..a67237b5 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -83,7 +83,7 @@ export default function PluginCardComponent({ return ( <>
    @@ -99,11 +99,13 @@ export default function PluginCardComponent({
    -
    - {cardVO.author} /{' '} +
    + {cardVO.author} / {cardVO.name}
    -
    {cardVO.name}
    +
    + {cardVO.label} +
    v{cardVO.version} @@ -166,7 +168,7 @@ export default function PluginCardComponent({
    -
    +
    {cardVO.description}
    diff --git a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx index 27b181b8..dbde8774 100644 --- a/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/PluginMarketComponent.tsx @@ -341,7 +341,7 @@ function MarketPageContent({ {isLoading ? (
    - {t('cloud.loading')} + {t('market.loading')}
    ) : plugins.length === 0 ? (
    diff --git a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx index 4e9f11f2..83a3994b 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx @@ -79,11 +79,11 @@ export default function PluginDetailDialog({ return ( - + {isLoading ? (
    - {t('cloud.loading')} + {t('market.loading')}
    ) : plugin ? (
    @@ -97,20 +97,20 @@ export default function PluginDetailDialog({ pluginName!, )} alt={plugin.name} - className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0" + className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0 dark:bg-[#1f1f22]" />
    -

    +

    {extractI18nObject(plugin.label) || plugin.name}

    -
    +
    {plugin.author} / {plugin.name}
    -
    +
    v{plugin.latest_version} @@ -128,7 +128,7 @@ export default function PluginDetailDialog({ {plugin.repository && ( -

    +

    {t('market.description')}

    -

    +

    {extractI18nObject(plugin.description) || t('market.noDescription')}

    @@ -158,12 +158,16 @@ export default function PluginDetailDialog({ {/* 标签 */} {plugin.tags && plugin.tags.length > 0 && (
    -

    +

    {t('market.tags')}

    {plugin.tags.map((tag) => ( - + {tag} ))} @@ -200,7 +204,7 @@ export default function PluginDetailDialog({
    - {t('cloud.loading')} + {t('market.loading')}
    ) : ( @@ -209,54 +213,56 @@ export default function PluginDetailDialog({ components={{ // 自定义样式 h1: ({ children }) => ( -

    +

    {children}

    ), h2: ({ children }) => ( -

    +

    {children}

    ), h3: ({ children }) => ( -

    +

    {children}

    ), p: ({ children }) => ( -

    +

    {children}

    ), ul: ({ children }) => ( -
      +
        {children}
      ), ol: ({ children }) => ( -
        +
          {children}
        ), li: ({ children }) => ( -
      1. {children}
      2. +
      3. + {children} +
      4. ), code: ({ children, node }) => { const isInline = node?.children?.length === 1 && node?.children[0]?.type === 'text'; return isInline ? ( - + {children} ) : ( - + {children} ); }, blockquote: ({ children }) => ( -
        +
        {children}
        ), @@ -265,7 +271,7 @@ export default function PluginDetailDialog({ href={href} target="_blank" rel="noopener noreferrer" - className="text-blue-600 hover:text-blue-800 underline" + className="text-blue-600 hover:text-blue-800 underline dark:text-[#29f]" > {children} diff --git a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx index f7a621ca..6fb6618b 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx @@ -15,7 +15,7 @@ export default function PluginMarketCardComponent({ return (
        @@ -25,13 +25,17 @@ export default function PluginMarketCardComponent({
        -
        {cardVO.pluginId}
        +
        + {cardVO.pluginId} +
        -
        {cardVO.label}
        +
        + {cardVO.label} +
        -
        +
        {cardVO.description}
        @@ -39,7 +43,7 @@ export default function PluginMarketCardComponent({
        {cardVO.githubURL && ( Date: Thu, 28 Aug 2025 14:02:56 +0800 Subject: [PATCH 65/78] fix: minor bugs --- pkg/platform/sources/webchat.py | 2 +- pkg/provider/runners/localagent.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pkg/platform/sources/webchat.py b/pkg/platform/sources/webchat.py index 6c148614..7fd54d1e 100644 --- a/pkg/platform/sources/webchat.py +++ b/pkg/platform/sources/webchat.py @@ -235,7 +235,7 @@ class WebChatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp())) if session_type == 'person': - sender = platform_entities.Friend(id='webchatperson', nickname='User') + sender = platform_entities.Friend(id='webchatperson', nickname='User', remark='User') event = platform_events.FriendMessage( sender=sender, message_chain=message_chain, time=datetime.now().timestamp() ) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index f91a145c..4f0e933e 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -94,17 +94,19 @@ class LocalAgentRunner(runner.RequestRunner): except AttributeError: is_stream = False - remove_think = self.pipeline_config['output'].get('misc', '').get('remove-think') + remove_think = query.pipeline_config['output'].get('misc', '').get('remove-think') + + use_llm_model = await self.ap.model_mgr.get_model_by_uuid(query.use_llm_model_uuid) if not is_stream: # 非流式输出,直接请求 - msg = await query.use_llm_model.requester.invoke_llm( + msg = await use_llm_model.requester.invoke_llm( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, remove_think=remove_think, ) yield msg @@ -116,12 +118,12 @@ class LocalAgentRunner(runner.RequestRunner): accumulated_content = '' # 从开始累积的所有内容 last_role = 'assistant' msg_sequence = 1 - async for msg in query.use_llm_model.requester.invoke_llm_stream( + async for msg in use_llm_model.requester.invoke_llm_stream( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, remove_think=remove_think, ): msg_idx = msg_idx + 1 @@ -215,12 +217,12 @@ class LocalAgentRunner(runner.RequestRunner): last_role = 'assistant' msg_sequence = first_end_sequence - async for msg in query.use_llm_model.requester.invoke_llm_stream( + async for msg in use_llm_model.requester.invoke_llm_stream( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, remove_think=remove_think, ): msg_idx += 1 @@ -271,12 +273,12 @@ class LocalAgentRunner(runner.RequestRunner): ) else: # 处理完所有调用,再次请求 - msg = await query.use_llm_model.requester.invoke_llm( + msg = await use_llm_model.requester.invoke_llm( query, - query.use_llm_model, + use_llm_model, req_messages, query.use_funcs, - extra_args=query.use_llm_model.model_entity.extra_args, + extra_args=use_llm_model.model_entity.extra_args, remove_think=remove_think, ) From 4bdd8a021c6bbb65968fd60a247ac3a7721c35aa Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 28 Aug 2025 14:38:10 +0800 Subject: [PATCH 66/78] fix: tool call params in localagent --- pkg/provider/runners/localagent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provider/runners/localagent.py b/pkg/provider/runners/localagent.py index 4f0e933e..7ab1e739 100644 --- a/pkg/provider/runners/localagent.py +++ b/pkg/provider/runners/localagent.py @@ -185,7 +185,7 @@ class LocalAgentRunner(runner.RequestRunner): parameters = json.loads(func.arguments) - func_ret = await self.ap.tool_mgr.execute_func_call(query, func.name, parameters) + func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters) if is_stream: msg = provider_message.MessageChunk( role='tool', From d7fc5283f7db6f4eccbae0d7459f9c8a115bc892 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 28 Aug 2025 14:43:45 +0800 Subject: [PATCH 67/78] chore: bump version 4.3.0b4 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9e847da9..e9603149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langbot" -version = "4.3.0.beta3" +version = "4.3.0.beta4" description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台" readme = "README.md" requires-python = ">=3.10.1" @@ -61,7 +61,7 @@ dependencies = [ "html2text>=2024.2.26", "langchain>=0.2.0", "chromadb>=0.4.24", - "langbot-plugin==0.1.1b4", + "langbot-plugin==0.1.1b5", ] keywords = [ "bot", From 91959527a4edc927b8ac404dbe2287e1153d6f69 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 28 Aug 2025 23:04:21 +0800 Subject: [PATCH 68/78] feat: available for disabling marketplace(offline env) --- pkg/api/http/controller/groups/system.py | 1 + templates/config.yaml | 3 ++- web/src/app/home/plugins/page.tsx | 27 ++++++++++++++---------- web/src/app/infra/entities/api/index.ts | 1 + web/src/app/infra/http/index.ts | 1 + 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/api/http/controller/groups/system.py b/pkg/api/http/controller/groups/system.py index 9e85e3ad..ee107401 100644 --- a/pkg/api/http/controller/groups/system.py +++ b/pkg/api/http/controller/groups/system.py @@ -14,6 +14,7 @@ class SystemRouterGroup(group.RouterGroup): 'version': constants.semantic_version, 'debug': constants.debug_mode, 'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()), + 'enable_marketplace': self.ap.instance_config.data['plugin'].get('enable_marketplace', True), 'cloud_service_url': ( self.ap.instance_config.data['plugin']['cloud_service_url'] if 'cloud_service_url' in self.ap.instance_config.data['plugin'] diff --git a/templates/config.yaml b/templates/config.yaml index 5e9448c8..942ace3f 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -21,4 +21,5 @@ system: secret: '' plugin: runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws' - cloud_service_url: 'https://space.langbot.app' \ No newline at end of file + enable_marketplace: true + cloud_service_url: 'https://space.langbot.app' diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 7172e5bc..fb751c4c 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -33,6 +33,7 @@ import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { PluginV4 } from '@/app/infra/entities/plugin'; +import { systemInfo } from '@/app/infra/http/HttpClient'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', @@ -213,9 +214,11 @@ export default function PluginConfigPage() { {t('plugins.installed')} - - {t('plugins.marketplace')} - + {systemInfo.enable_marketplace && ( + + {t('plugins.marketplace')} + + )}
        @@ -241,14 +244,16 @@ export default function PluginConfigPage() { {t('plugins.uploadLocal')} - { - setActiveTab('market'); - }} - > - - {t('plugins.marketplace')} - + {systemInfo.enable_marketplace && ( + { + setActiveTab('market'); + }} + > + + {t('plugins.marketplace')} + + )}
        diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 055b92b6..5b187cae 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -212,6 +212,7 @@ export interface ApiRespSystemInfo { debug: boolean; version: string; cloud_service_url: string; + enable_marketplace: boolean; } export interface ApiRespAsyncTasks { diff --git a/web/src/app/infra/http/index.ts b/web/src/app/infra/http/index.ts index dad2b68c..df46dca0 100644 --- a/web/src/app/infra/http/index.ts +++ b/web/src/app/infra/http/index.ts @@ -6,6 +6,7 @@ import { ApiRespSystemInfo } from '@/app/infra/entities/api'; export let systemInfo: ApiRespSystemInfo = { debug: false, version: '', + enable_marketplace: true, cloud_service_url: '', }; From 40f1af4434b6a29175fe5e44382c5c68fb9d724b Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 28 Aug 2025 23:50:26 +0800 Subject: [PATCH 69/78] perf: display installed plugin icon --- pkg/api/http/controller/groups/plugins.py | 14 ++++++++++++++ pkg/plugin/connector.py | 6 ++++++ pkg/plugin/handler.py | 11 +++++++++++ .../plugin-card/PluginCardComponent.tsx | 10 ++++++++-- web/src/app/infra/http/BackendClient.ts | 11 +++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 670f13ed..28a3c44c 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -75,6 +75,20 @@ class PluginsRouterGroup(group.RouterGroup): return self.success(data={}) + @self.route( + '///icon', + methods=['GET'], + auth_type=group.AuthType.NONE, + ) + async def _(author: str, plugin_name: str) -> quart.Response: + icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name) + icon_base64 = icon_data['plugin_icon_base64'] + mime_type = icon_data['mime_type'] + + icon_data = base64.b64decode(icon_base64) + + return quart.Response(icon_data, mimetype=mime_type) + @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) async def _() -> str: data = await quart.request.json diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 6afc0661..da7de024 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -7,6 +7,8 @@ import typing import os import sys +from async_lru import alru_cache + from ..core import app from . import handler from ..utils import platform @@ -153,6 +155,10 @@ class PluginRuntimeConnector: async def set_plugin_config(self, plugin_author: str, plugin_name: str, config: dict[str, Any]) -> dict[str, Any]: return await self.handler.set_plugin_config(plugin_author, plugin_name, config) + @alru_cache(ttl=5 * 60) # 5 minutes + async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]: + return await self.handler.get_plugin_icon(plugin_author, plugin_name) + async def emit_event( self, event: events.BaseEventModel, diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index a677b9be..36d11d09 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -534,6 +534,17 @@ class RuntimeConnectionHandler(handler.Handler): return result['tools'] + async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]: + """Get plugin icon""" + result = await self.call_action( + LangBotToRuntimeAction.GET_PLUGIN_ICON, + { + 'plugin_author': plugin_author, + 'plugin_name': plugin_name, + }, + ) + return result + async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: """Call tool""" result = await self.call_action( diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index a67237b5..a3e7596d 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -14,6 +14,7 @@ import { ArrowUp, } from 'lucide-react'; import { getCloudServiceClientSync } from '@/app/infra/http'; +import { httpClient } from '@/app/infra/http/HttpClient'; import { PluginComponent } from '@/app/infra/entities/plugin'; import { Button } from '@/components/ui/button'; import { @@ -87,14 +88,19 @@ export default function PluginCardComponent({ onClick={onCardClick} >
        - - + */} + plugin icon
        diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index ef270258..10f28da2 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -438,6 +438,17 @@ export class BackendClient extends BaseHttpClient { return this.put(`/api/v1/plugins/${author}/${name}/config`, config); } + public getPluginIconURL(author: string, name: string): string { + if (this.instance.defaults.baseURL === '/') { + const url = window.location.href; + const baseURL = url.split('/').slice(0, 3).join('/'); + return `${baseURL}/api/v1/plugins/${author}/${name}/icon`; + } + return ( + this.instance.defaults.baseURL + `/api/v1/plugins/${author}/${name}/icon` + ); + } + public installPluginFromGithub( source: string, ): Promise { From 2194b2975c4cfc270dd85d5cfc9643a6806dcca8 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 30 Aug 2025 17:08:03 +0800 Subject: [PATCH 70/78] refactor: market plugin detail dialog --- web/package.json | 1 + .../PluginDetailDialog.tsx | 381 +++++++++--------- 2 files changed, 192 insertions(+), 190 deletions(-) diff --git a/web/package.json b/web/package.json index 8cce9538..26cb7ca7 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "react-i18next": "^15.5.1", "react-markdown": "^10.1.0", "react-photo-view": "^1.2.7", + "remark-gfm": "^4.0.1", "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.5", diff --git a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx index 83a3994b..2bb29f8d 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx @@ -6,11 +6,12 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Loader2, Download, Users } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; -import { PluginV4 } from '@/app/infra/entities/plugin'; -import { extractI18nObject } from '@/i18n/I18nProvider'; +import remarkGfm from 'remark-gfm'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { PluginV4 } from '@/app/infra/entities/plugin'; import { getCloudServiceClientSync } from '@/app/infra/http'; +import { extractI18nObject } from '@/i18n/I18nProvider'; interface PluginDetailDialogProps { open: boolean; @@ -50,7 +51,6 @@ export default function PluginDetailDialog({ author, pluginName, ); - console.log('detailResponse', detailResponse); setPlugin(detailResponse.plugin); // 获取README @@ -58,7 +58,6 @@ export default function PluginDetailDialog({ try { const readmeResponse = await getCloudServiceClientSync().getPluginREADME(author, pluginName); - console.log('readmeResponse', readmeResponse); setReadme(readmeResponse.readme); } catch (error) { console.warn('Failed to load README:', error); @@ -77,210 +76,212 @@ export default function PluginDetailDialog({ if (!open) return null; + const PluginHeader = () => ( +
        + {plugin!.name} +
        +

        + {extractI18nObject(plugin!.label) || plugin!.name} +

        +
        + + + {plugin!.author} / {plugin!.name} + +
        +
        + v{plugin!.latest_version} + + + {plugin!.install_count.toLocaleString()} {t('market.downloads')} + + {plugin!.repository && ( + + )} +
        +
        +
        + ); + + const PluginDescription = () => ( +
        +

        + {extractI18nObject(plugin!.description) || t('market.noDescription')} +

        +
        + ); + + const PluginOptions = () => ( +
        + +
        + ); + + const ReadmeContent = () => ( +
        + ( +
        + + + ), + thead: ({ ...props }) => , + tbody: ({ ...props }) => ( + + ), + th: ({ ...props }) => ( + + ), + // 删除线支持 + del: ({ ...props }) => ( + + ), + // Todo 列表支持 + input: ({ type, checked, ...props }) => { + if (type === 'checkbox') { + return ( + + ); + } + return ; + }, + ul: ({ ...props }) =>
          , + ol: ({ ...props }) =>
            , + li: ({ ...props }) =>
          1. , + h1: ({ ...props }) => ( +

            + ), + h2: ({ ...props }) => ( +

            + ), + p: ({ ...props }) =>

            , + code: ({ className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + const isCodeBlock = match ? true : false; + + // 如果是代码块(有语言标识),由 pre 标签处理样式,淡灰色底,黑色字 + if (isCodeBlock) { + return ( + + {children} + + ); + } + + // 内联代码样式 - 淡灰色底 + return ( + + {children} + + ); + }, + pre: ({ ...props }) => ( +

            +          ),
            +        }}
            +      >
            +        {readme}
            +      
            +    
            +  );
            +
               return (
                 
            -      
            +      
                     {isLoading ? (
            -          
            +
            - {t('market.loading')} + {t('cloud.loading')}
            ) : plugin ? ( -
            - {/* 左侧:插件基本信息 */} -
            - {/* 插件图标和标题 */} -
            - {plugin.name} -
            -

            - {extractI18nObject(plugin.label) || plugin.name} -

            -
            - - - {plugin.author} / {plugin.name} - -
            - -
            - - v{plugin.latest_version} - - - - - - {plugin.install_count.toLocaleString()}{' '} - {t('market.downloads')} - - - - {plugin.repository && ( - { - e.stopPropagation(); - window.open(plugin.repository, '_blank'); - }} - > - - - )} -
            +
            + {/* 插件信息区域 */} +
            +
            +
            + +
            -
            - - {/* 插件描述 */} -
            -

            - {t('market.description')} -

            -

            - {extractI18nObject(plugin.description) || - t('market.noDescription')} -

            -
            - - {/* 标签 */} - {plugin.tags && plugin.tags.length > 0 && ( -
            -

            - {t('market.tags')} -

            -
            - {plugin.tags.map((tag) => ( - - {tag} - - ))} -
            +
            +
            - )} - - {/* 操作按钮 */} -
            - - {/* {plugin.repository && ( - - )} */}
            - {/* 右侧:README内容 */} -
            -
            + {/* README 区域 */} +
            +
            {isLoadingReadme ? (
            - {t('market.loading')} + {t('cloud.loading')}
            ) : ( -
            - ( -

            - {children} -

            - ), - h2: ({ children }) => ( -

            - {children} -

            - ), - h3: ({ children }) => ( -

            - {children} -

            - ), - p: ({ children }) => ( -

            - {children} -

            - ), - ul: ({ children }) => ( -
              - {children} -
            - ), - ol: ({ children }) => ( -
              - {children} -
            - ), - li: ({ children }) => ( -
          2. - {children} -
          3. - ), - code: ({ children, node }) => { - const isInline = - node?.children?.length === 1 && - node?.children[0]?.type === 'text'; - return isInline ? ( - - {children} - - ) : ( - - {children} - - ); - }, - blockquote: ({ children }) => ( -
            - {children} -
            - ), - a: ({ href, children }) => ( - - {children} - - ), - }} - > - {readme} -
            -
            + )}
            From e701ceeeba661ae30a3980a009a7a589e86f994a Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 30 Aug 2025 17:19:16 +0800 Subject: [PATCH 71/78] perf: dark theme --- .../PluginDetailDialog.tsx | 90 +++++++++++++------ 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx index 2bb29f8d..4421d972 100644 --- a/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx +++ b/web/src/app/home/plugins/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx @@ -84,18 +84,23 @@ export default function PluginDetailDialog({ className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0" />
            -

            +

            {extractI18nObject(plugin!.label) || plugin!.name}

            -
            +
            {plugin!.author} / {plugin!.name}
            - v{plugin!.latest_version} - + + v{plugin!.latest_version} + + {plugin!.install_count.toLocaleString()} {t('market.downloads')} @@ -105,7 +110,7 @@ export default function PluginDetailDialog({ e.stopPropagation(); window.open(plugin!.repository, '_blank'); }} - className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" + className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 cursor-pointer" > @@ -120,7 +125,7 @@ export default function PluginDetailDialog({ const PluginDescription = () => (
            -

            +

            {extractI18nObject(plugin!.description) || t('market.noDescription')}

            @@ -139,38 +144,55 @@ export default function PluginDetailDialog({ ); const ReadmeContent = () => ( -
            +
            ( -
            -

        + ), + td: ({ ...props }) => ( + + ), + tr: ({ ...props }) => ( +
        +
        +
        ), - thead: ({ ...props }) => , + thead: ({ ...props }) => ( + + ), tbody: ({ ...props }) => ( - + ), th: ({ ...props }) => ( + ), // 删除线支持 del: ({ ...props }) => ( - + ), // Todo 列表支持 input: ({ type, checked, ...props }) => { @@ -180,23 +202,35 @@ export default function PluginDetailDialog({ type="checkbox" checked={checked} disabled - className="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-default" + className="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-default dark:border-gray-700" {...props} /> ); } return ; }, - ul: ({ ...props }) =>
          , - ol: ({ ...props }) =>
            , + ul: ({ ...props }) => ( +
              + ), + ol: ({ ...props }) => ( +
                + ), li: ({ ...props }) =>
              1. , h1: ({ ...props }) => ( -

                +

                ), h2: ({ ...props }) => ( -

                +

                + ), + p: ({ ...props }) => ( +

                ), - p: ({ ...props }) =>

                , code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ''); const isCodeBlock = match ? true : false; @@ -205,7 +239,7 @@ export default function PluginDetailDialog({ if (isCodeBlock) { return ( {children} @@ -216,7 +250,7 @@ export default function PluginDetailDialog({ // 内联代码样式 - 淡灰色底 return ( {children} @@ -225,7 +259,7 @@ export default function PluginDetailDialog({ }, pre: ({ ...props }) => (

                             
                -            {t('cloud.loading')}
                +            {t('market.loading')}
                           
                         ) : plugin ? (
                           
                {/* 插件信息区域 */} -
                +
                @@ -272,12 +306,12 @@ export default function PluginDetailDialog({ {/* README 区域 */}
                -
                +
                {isLoadingReadme ? (
                - {t('cloud.loading')} + {t('market.loading')}
                ) : ( From 589f61931adc702054ab0a2b2a698f97be27c235 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 30 Aug 2025 17:27:18 +0800 Subject: [PATCH 72/78] fix: cloudServiceClient api --- web/src/app/home/components/home-sidebar/HomeSidebar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 4009c77a..b9489668 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -9,7 +9,8 @@ import { import { useRouter, usePathname } from 'next/navigation'; import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList'; import langbotIcon from '@/app/assets/langbot-logo.webp'; -import { systemInfo, spaceClient } from '@/app/infra/http/HttpClient'; +import { systemInfo } from '@/app/infra/http/HttpClient'; +import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { Moon, Sun, Monitor } from 'lucide-react'; import { useTheme } from 'next-themes'; @@ -54,7 +55,7 @@ export default function HomeSidebar({ localStorage.setItem('userEmail', 'test@example.com'); } - spaceClient + getCloudServiceClientSync() .get('/api/v1/dist/info/repo') .then((response) => { const data = response as { repo: { stargazers_count: number } }; From 11acd99c103c338a20cc7ca8f4099f5aad0ddee0 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 30 Aug 2025 22:41:07 +0800 Subject: [PATCH 73/78] feat: supports for command return image base64 --- pkg/pipeline/process/handlers/command.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/pipeline/process/handlers/command.py b/pkg/pipeline/process/handlers/command.py index db2187e3..92cebe02 100644 --- a/pkg/pipeline/process/handlers/command.py +++ b/pkg/pipeline/process/handlers/command.py @@ -74,7 +74,7 @@ class CommandHandler(handler.MessageHandler): self.ap.logger.info(f'Command({query.query_id}) error: {self.cut_str(str(ret.error))}') yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) - elif ret.text is not None or ret.image_url is not None: + elif ret.text is not None or ret.image_url is not None or ret.image_base64 is not None: content: list[provider_message.ContentElement] = [] if ret.text is not None: @@ -83,6 +83,9 @@ class CommandHandler(handler.MessageHandler): if ret.image_url is not None: content.append(provider_message.ContentElement.from_image_url(ret.image_url)) + if ret.image_base64 is not None: + content.append(provider_message.ContentElement.from_image_base64(ret.image_base64)) + query.resp_messages.append( provider_message.Message( role='command', From f74502c7117c45e4f7d7bd0a2f220b7fee06560f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 30 Aug 2025 23:15:54 +0800 Subject: [PATCH 74/78] chore: bump langbot_plugin to 0.1.1b6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e9603149..f934f8e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "html2text>=2024.2.26", "langchain>=0.2.0", "chromadb>=0.4.24", - "langbot-plugin==0.1.1b5", + "langbot-plugin==0.1.1b6", ] keywords = [ "bot", From e0d5469ae29c02efec410f96b4529f338ea1ad52 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Sun, 31 Aug 2025 22:18:10 +0800 Subject: [PATCH 75/78] del self.ap error --- pkg/platform/sources/lark.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/platform/sources/lark.py b/pkg/platform/sources/lark.py index 6d04bd94..23257e6f 100644 --- a/pkg/platform/sources/lark.py +++ b/pkg/platform/sources/lark.py @@ -620,15 +620,13 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): f'client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}' ) - self.ap.logger.debug(f'飞书卡片创建成功,卡片ID: {response.data.card_id}') self.card_id_dict[message_id] = response.data.card_id card_id = response.data.card_id return card_id except Exception as e: - self.ap.logger.error(f'飞书卡片创建失败,错误信息: {e}') - + raise e async def create_message_card(self, message_id, event) -> str: """ 创建卡片消息。 From 93319ec2a827765cff8dbc04b9f912cec0911170 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Sun, 31 Aug 2025 22:20:05 +0800 Subject: [PATCH 76/78] fix: dingtalk pydantic.BaseModel norm --- pkg/platform/sources/dingtalk.py | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pkg/platform/sources/dingtalk.py b/pkg/platform/sources/dingtalk.py index f3d3d9f5..eb15775f 100644 --- a/pkg/platform/sources/dingtalk.py +++ b/pkg/platform/sources/dingtalk.py @@ -102,13 +102,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): card_instance_id_dict: ( dict # 回复卡片消息字典,key为消息id,value为回复卡片实例id,用于在流式消息时判断是否发送到指定卡片 ) - seq: int # 消息顺序,直接以seq作为标识 def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - self.card_instance_id_dict = {} - # self.seq = 1 + required_keys = [ 'client_id', 'client_secret', @@ -118,16 +114,23 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): missing_keys = [key for key in required_keys if key not in config] if missing_keys: raise Exception('钉钉缺少相关配置项,请查看文档或联系管理员') + bot = DingTalkClient( + client_id=config['client_id'], + client_secret=config['client_secret'], + robot_name=config['robot_name'], + robot_code=config['robot_code'], + markdown_card=config['markdown_card'], + logger=logger, + ) + bot_account_id = config['robot_name'] + super().__init__( + config=config, + logger=logger, + card_instance_id_dict={}, + bot_account_id=bot_account_id, + bot=bot, + listeners={}, - self.bot_account_id = self.config['robot_name'] - - self.bot = DingTalkClient( - client_id=config['client_id'], - client_secret=config['client_secret'], - robot_name=config['robot_name'], - robot_code=config['robot_code'], - markdown_card=config['markdown_card'], - logger=self.logger, ) async def reply_message( @@ -222,6 +225,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): async def kill(self) -> bool: return False + async def is_muted(self) -> bool: + return False + async def unregister_listener( self, event_type: type, From a95c422de925d9265b3f0f0e6378d29a47515478 Mon Sep 17 00:00:00 2001 From: fdc <2213070223@qq.com> Date: Sun, 31 Aug 2025 22:20:22 +0800 Subject: [PATCH 77/78] fix: wechatpad pydantic.BaseModel norm --- pkg/platform/sources/wechatpad.py | 37 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/pkg/platform/sources/wechatpad.py b/pkg/platform/sources/wechatpad.py index e719b7aa..e35bad63 100644 --- a/pkg/platform/sources/wechatpad.py +++ b/pkg/platform/sources/wechatpad.py @@ -30,10 +30,16 @@ import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConverter): def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger): + self.bot = WeChatPadClient(config['wechatpad_url'], config['token']) self.config = config - self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token']) self.logger = logger + # super().__init__( + # config = config, + # bot = bot, + # logger = logger, + # ) + @staticmethod async def yiri2target(message_chain: platform_message.MessageChain) -> list[dict]: content_list = [] @@ -450,8 +456,13 @@ class WeChatPadMessageConverter(abstract_platform_adapter.AbstractMessageConvert class WeChatPadEventConverter(abstract_platform_adapter.AbstractEventConverter): def __init__(self, config: dict, logger: logging.Logger): self.config = config - self.message_converter = WeChatPadMessageConverter(config, logger) self.logger = logger + self.message_converter = WeChatPadMessageConverter(self.config, self.logger) + # super().__init__( + # config=config, + # message_converter=message_converter, + # logger = logger, + # ) @staticmethod async def yiri2target(event: platform_events.MessageEvent) -> dict: @@ -532,12 +543,24 @@ class WeChatPadAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) ] = {} def __init__(self, config: dict, logger: EventLogger): - self.config = config - self.logger = logger - self.quart_app = quart.Quart(__name__) - self.message_converter = WeChatPadMessageConverter(config, logger) - self.event_converter = WeChatPadEventConverter(config, logger) + quart_app = quart.Quart(__name__) + + message_converter = WeChatPadMessageConverter(config, logger) + event_converter = WeChatPadEventConverter(config, logger) + bot = WeChatPadClient(config['wechatpad_url'], config['token']) + super().__init__( + config=config, + logger = logger, + quart_app = quart_app, + message_converter =message_converter, + event_converter = event_converter, + listeners={}, + bot_account_id ='', + name="WeChatPad", + bot=bot, + + ) async def ws_message(self, data): """处理接收到的消息""" From a9beb66aef4f098713cd5fe47effcd64365cf6fb Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 12 Sep 2025 22:58:51 +0800 Subject: [PATCH 78/78] chore: move docker-compose.yaml for plugin edition --- docker-compose.yaml | 27 ++++----------------------- docker/docker-compose.yaml | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 docker/docker-compose.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml index 107a9e26..e1231d66 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,21 +1,7 @@ +# This file is deprecated, and will be replaced by docker/docker-compose.yaml in next version. version: "3" services: - - langbot_plugin_runtime: - image: rockchin/langbot:latest - container_name: langbot_plugin_runtime - volumes: - - ./data/plugins:/app/data/plugins - ports: - - 5401:5401 - restart: on-failure - environment: - - TZ=Asia/Shanghai - command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"] - networks: - - langbot_network - langbot: image: rockchin/langbot:latest container_name: langbot @@ -26,11 +12,6 @@ services: environment: - TZ=Asia/Shanghai ports: - - 5300:5300 # For web ui - - 2280-2290:2280-2290 # For platform webhook - networks: - - langbot_network - -networks: - langbot_network: - driver: bridge + - 5300:5300 # 供 WebUI 使用 + - 2280-2290:2280-2290 # 供消息平台适配器方向连接 + # 根据具体环境配置网络 \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..107a9e26 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,36 @@ +version: "3" + +services: + + langbot_plugin_runtime: + image: rockchin/langbot:latest + container_name: langbot_plugin_runtime + volumes: + - ./data/plugins:/app/data/plugins + ports: + - 5401:5401 + restart: on-failure + environment: + - TZ=Asia/Shanghai + command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"] + networks: + - langbot_network + + langbot: + image: rockchin/langbot:latest + container_name: langbot + volumes: + - ./data:/app/data + - ./plugins:/app/plugins + restart: on-failure + environment: + - TZ=Asia/Shanghai + ports: + - 5300:5300 # For web ui + - 2280-2290:2280-2290 # For platform webhook + networks: + - langbot_network + +networks: + langbot_network: + driver: bridge

        ), td: ({ ...props }) => ( ), tr: ({ ...props }) => ( -