From cf646752c57b824c16ed3f3ccaf1754a751a78c6 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 28 Nov 2025 20:13:58 +0800 Subject: [PATCH] feat: add more service api supports --- .../pkg/api/http/controller/groups/files.py | 4 +-- .../http/controller/groups/knowledge/base.py | 6 +++- .../http/controller/groups/platform/bots.py | 29 +++++++++++++++++++ .../pkg/api/http/controller/groups/plugins.py | 20 ++++++------- src/langbot/pkg/api/http/service/bot.py | 26 +++++++++++++++++ 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/files.py b/src/langbot/pkg/api/http/controller/groups/files.py index 30aab87b..408fbfd0 100644 --- a/src/langbot/pkg/api/http/controller/groups/files.py +++ b/src/langbot/pkg/api/http/controller/groups/files.py @@ -28,7 +28,7 @@ class FilesRouterGroup(group.RouterGroup): return quart.Response(image_bytes, mimetype=mime_type) - @self.route('/images', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/images', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def upload_image() -> quart.Response: request = quart.request @@ -76,7 +76,7 @@ class FilesRouterGroup(group.RouterGroup): } ) - @self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def upload_document() -> quart.Response: request = quart.request diff --git a/src/langbot/pkg/api/http/controller/groups/knowledge/base.py b/src/langbot/pkg/api/http/controller/groups/knowledge/base.py index a5bed5df..96ed001c 100644 --- a/src/langbot/pkg/api/http/controller/groups/knowledge/base.py +++ b/src/langbot/pkg/api/http/controller/groups/knowledge/base.py @@ -5,7 +5,7 @@ from ... import group @group.group_class('knowledge_base', '/api/v1/knowledge/bases') class KnowledgeBaseRouterGroup(group.RouterGroup): async def initialize(self) -> None: - @self.route('', methods=['POST', 'GET']) + @self.route('', methods=['POST', 'GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def handle_knowledge_bases() -> quart.Response: if quart.request.method == 'GET': knowledge_bases = await self.ap.knowledge_service.get_knowledge_bases() @@ -21,6 +21,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup): @self.route( '/', methods=['GET', 'DELETE', 'PUT'], + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response: if quart.request.method == 'GET': @@ -47,6 +48,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup): @self.route( '//files', methods=['GET', 'POST'], + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def get_knowledge_base_files(knowledge_base_uuid: str) -> str: if quart.request.method == 'GET': @@ -74,6 +76,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup): @self.route( '//files/', methods=['DELETE'], + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def delete_specific_file_in_kb(file_id: str, knowledge_base_uuid: str) -> str: await self.ap.knowledge_service.delete_file(knowledge_base_uuid, file_id) @@ -82,6 +85,7 @@ class KnowledgeBaseRouterGroup(group.RouterGroup): @self.route( '//retrieve', methods=['POST'], + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def retrieve_knowledge_base(knowledge_base_uuid: str) -> str: json_data = await quart.request.json diff --git a/src/langbot/pkg/api/http/controller/groups/platform/bots.py b/src/langbot/pkg/api/http/controller/groups/platform/bots.py index d943e916..a261eacb 100644 --- a/src/langbot/pkg/api/http/controller/groups/platform/bots.py +++ b/src/langbot/pkg/api/http/controller/groups/platform/bots.py @@ -42,3 +42,32 @@ class BotsRouterGroup(group.RouterGroup): 'total_count': total_count, } ) + + @self.route('//send_message', methods=['POST'], auth_type=group.AuthType.API_KEY) + async def _(bot_uuid: str) -> str: + """Send message to a specific target via bot""" + json_data = await quart.request.json + target_type = json_data.get('target_type') + target_id = json_data.get('target_id') + message_chain_data = json_data.get('message_chain') + + # Validate required fields + if not target_type: + return self.http_status(400, -1, 'target_type is required') + if not target_id: + return self.http_status(400, -1, 'target_id is required') + if not message_chain_data: + return self.http_status(400, -1, 'message_chain is required') + + # Validate target_type + if target_type not in ['person', 'group']: + return self.http_status(400, -1, 'target_type must be either "person" or "group"') + + try: + await self.ap.bot_service.send_message(bot_uuid, target_type, target_id, message_chain_data) + return self.success(data={'sent': True}) + except Exception as e: + import traceback + + traceback.print_exc() + return self.http_status(500, -1, f'Failed to send message: {str(e)}') diff --git a/src/langbot/pkg/api/http/controller/groups/plugins.py b/src/langbot/pkg/api/http/controller/groups/plugins.py index 2f8f0024..e2979598 100644 --- a/src/langbot/pkg/api/http/controller/groups/plugins.py +++ b/src/langbot/pkg/api/http/controller/groups/plugins.py @@ -15,7 +15,7 @@ from langbot_plugin.runtime.plugin.mgr import PluginInstallSource @group.group_class('plugins', '/api/v1/plugins') class PluginsRouterGroup(group.RouterGroup): async def initialize(self) -> None: - @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) + @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def _() -> str: plugins = await self.ap.plugin_connector.list_plugins() @@ -24,7 +24,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '///upgrade', methods=['POST'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _(author: str, plugin_name: str) -> str: ctx = taskmgr.TaskContext.new() @@ -40,7 +40,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '//', methods=['GET', 'DELETE'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _(author: str, plugin_name: str) -> str: if quart.request.method == 'GET': @@ -66,7 +66,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '///config', methods=['GET', 'PUT'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _(author: str, plugin_name: str) -> quart.Response: plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name) @@ -85,7 +85,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '///readme', methods=['GET'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _(author: str, plugin_name: str) -> quart.Response: language = quart.request.args.get('language', 'en') @@ -117,7 +117,7 @@ class PluginsRouterGroup(group.RouterGroup): mime_type = asset_data['mime_type'] return quart.Response(asset_bytes, mimetype=mime_type) - @self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def _() -> str: """Get releases from a GitHub repository URL""" data = await quart.request.json @@ -166,7 +166,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '/github/release-assets', methods=['POST'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _() -> str: """Get assets from a specific GitHub release""" @@ -220,7 +220,7 @@ class PluginsRouterGroup(group.RouterGroup): except httpx.RequestError as e: return self.http_status(500, -1, f'Failed to fetch release assets: {str(e)}') - @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def _() -> str: """Install plugin from GitHub release asset""" data = await quart.request.json @@ -254,7 +254,7 @@ class PluginsRouterGroup(group.RouterGroup): @self.route( '/install/marketplace', methods=['POST'], - auth_type=group.AuthType.USER_TOKEN, + auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, ) async def _() -> str: data = await quart.request.json @@ -270,7 +270,7 @@ class PluginsRouterGroup(group.RouterGroup): return self.success(data={'task_id': wrapper.id}) - @self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) + @self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY) async def _() -> str: file = (await quart.request.files).get('file') if file is None: diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index 3ced0e51..1a65fa87 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -139,3 +139,29 @@ class BotService: logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count) return [log.to_json() for log in logs], total_count + + async def send_message(self, bot_uuid: str, target_type: str, target_id: str, message_chain_data: dict) -> None: + """Send message to a specific target via bot + + Args: + bot_uuid: The UUID of the bot + target_type: The type of the target, can be "group", "person" + target_id: The ID of the target + message_chain_data: The message chain data in dict format + """ + # Import here to avoid circular imports + import langbot_plugin.api.entities.builtin.platform.message as platform_message + + # Get runtime bot + runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid) + if runtime_bot is None: + raise Exception(f'Bot not found: {bot_uuid}') + + # Validate and convert message chain + try: + message_chain = platform_message.MessageChain.model_validate(message_chain_data) + except Exception as e: + raise Exception(f'Invalid message_chain format: {str(e)}') + + # Send message via adapter + await runtime_bot.adapter.send_message(target_type, str(target_id), message_chain)