feat(v0.3.4): 插件命令 HTTP API 自动暴露
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user