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:
qinglong
2026-06-13 11:02:45 +08:00
parent d5388057d9
commit f402af58d0
2 changed files with 79 additions and 3 deletions
+71 -1
View File
@@ -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: