From f402af58d0b8da12c1c8d0d20ee2f71413e468c0 Mon Sep 17 00:00:00 2001 From: qinglong Date: Sat, 13 Jun 2026 11:02:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(v0.3.4):=20=E6=8F=92=E4=BB=B6=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=20HTTP=20API=20=E8=87=AA=E5=8A=A8=E6=9A=B4=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PluginNetworkBridge.register_command_routes(): - 自动扫描插件 @plugin_command/cmd_* 方法 - 每个命令 → POST /api/plugin/{cmd_name} - JSON body: {"args": [...], "kwargs": {...}} - 返回: {"ok": true, "result": "..."} PluginService.load_plugin() 在插件初始化后自动调用 Co-Authored-By: Claude --- bridges/plugin_network_bridge.py | 72 +++++++++++++++++++++++++++++++- services/plugin_service.py | 10 ++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/bridges/plugin_network_bridge.py b/bridges/plugin_network_bridge.py index aefcad9..f84577c 100644 --- a/bridges/plugin_network_bridge.py +++ b/bridges/plugin_network_bridge.py @@ -3,6 +3,7 @@ import logging import asyncio +import inspect from typing import Dict, List, Callable, Any import json @@ -24,7 +25,76 @@ class PluginNetworkBridge: """检查网络服务是否可用""" return self.internet_service is not None and hasattr(self.internet_service, 'register_plugin_route') - async def register_http_route(self, route_path: str, handler: Callable, + async def register_command_routes(self, plugin_instance, require_auth: bool = True): + """自动扫描插件 @plugin_command/cmd_* 方法并注册 REST 端点 + + 每个命令 → POST /api/plugin/{cmd_name} + 参数以 JSON body 传入: {"args": [...]} + 返回: {"ok": true, "result": "..."} 或 {"ok": false, "error": "..."} + """ + try: + import inspect + + registered = 0 + for attr_name in dir(plugin_instance): + if attr_name.startswith("__"): + continue + + method = getattr(plugin_instance, attr_name, None) + if not callable(method): + continue + + # 识别 @plugin_command 装饰或 cmd_ 前缀 + cmd_name = None + if hasattr(method, "_is_plugin_command"): + cmd_name = getattr(method, "_command_name", attr_name[4:] if attr_name.startswith("cmd_") else attr_name) + elif attr_name.startswith("cmd_"): + cmd_name = attr_name[4:] + + if not cmd_name: + continue + + route_path = f"/api/plugin/{cmd_name}" + + # 创建闭包捕获 method 和 cmd_name + async def _make_handler(_method=method, _cmd_name=cmd_name): + from aiohttp import web + + async def _handler(request): + try: + body = {} + try: + body = await request.json() + except Exception: + pass + args = body.get("args", []) + if isinstance(args, str): + args = [args] + kwargs = body.get("kwargs", {}) + result = _method(*args, **kwargs) + if asyncio.iscoroutine(result): + result = await result + return web.json_response({"ok": True, "result": str(result)}) + except Exception as e: + logger.error(f"命令 {_cmd_name} REST 调用失败: {e}") + return web.json_response({"ok": False, "error": str(e)}, status=500) + + return _handler + + await self.register_http_route( + route_path, await _make_handler(), + methods=["POST"], require_auth=require_auth, + ) + logger.debug(f" 自动暴露 REST: POST {route_path}") + registered += 1 + + if registered: + logger.info(f"插件 {self.plugin_name} 自动暴露 {registered} 个命令 REST 端点") + + except Exception as e: + logger.warning(f"自动注册命令路由失败 {self.plugin_name}: {e}") + + async def register_http_route(self, route_path: str, handler: Callable, methods: List[str] = ["GET"], require_auth: bool = True): """注册HTTP路由""" try: diff --git a/services/plugin_service.py b/services/plugin_service.py index 038c15c..7bf2c37 100644 --- a/services/plugin_service.py +++ b/services/plugin_service.py @@ -314,9 +314,15 @@ class PluginService: else: plugin_instance.initialize() - # 扫描并注册插件命令 + # 扫描并注册 TUI 命令 plugin_commands = await self._scan_and_register_commands(plugin_name, plugin_instance, plugin_config) - + + # 自动暴露插件命令为 REST 端点 + if hasattr(plugin_instance, 'network_bridge') and plugin_instance.network_bridge: + await plugin_instance.network_bridge.register_command_routes( + plugin_instance, require_auth=True + ) + # 注册插件 self.plugins[plugin_name] = plugin_instance