File size: 5,522 Bytes
fbd9d3d e0ea7df 0d8512e e0ea7df fbd9d3d 0d8512e e0ea7df 0d8512e fbd9d3d e0ea7df e5e756a fbd9d3d e0ea7df fbd9d3d 0d8512e fbd9d3d 0d8512e e0ea7df fbd9d3d 0d8512e fbd9d3d cc826a1 fbd9d3d 0d8512e e0ea7df cc826a1 e0ea7df 0d8512e e0ea7df 0d8512e e0ea7df 0d8512e cc826a1 0d8512e e0ea7df cc826a1 0d8512e e0ea7df 0d8512e e0ea7df 0d8512e e0ea7df 0d8512e e0ea7df 0d8512e e0ea7df 0d8512e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """
浏览器MCP插件 API 路由(v3.0)。
端点:
- GET / API 根
- GET /status 插件状态
- GET /binary-check Lightpanda 二进制探测
- GET /tools 列出 28 个上游工具(含 schema)
- GET /health 健康检查(二进制 + 子进程 + 漂移校验)
- POST /mcp/{tool} 同步调用 MCP 工具(前端用)
- POST /process/stop 停止 lightpanda 子进程
- GET /process/logs 查看子进程 stderr 最近 N 行
"""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
from fastapi import APIRouter, HTTPException
from .browser.binary_locator import locate_lightpanda
from .browser.client import LightpandaClient
from .browser.tools_meta import TOOLS_META
logger = logging.getLogger(__name__)
router = APIRouter()
# 运行时注入的插件实例(由 plugin_loader 调用 set_plugin_instance 注入)
plugin = None
def set_plugin_instance(plugin_instance):
"""由系统调用,注入插件实例。"""
global plugin
plugin = plugin_instance
@router.get("/")
async def api_root():
"""API 根路径。"""
return {"message": "浏览器MCP插件API", "backend": "lightpanda", "status": "运行中"}
@router.get("/status")
async def get_status():
"""获取插件状态。"""
if plugin is None:
return {
"name": "browser",
"enabled": False,
"message": "插件未加载",
}
return plugin.get_status()
@router.get("/binary-check")
async def binary_check():
"""Lightpanda 二进制结构化探测(路径/版本/install_hints)。"""
return locate_lightpanda().to_dict()
@router.get("/tools")
async def list_tools():
"""列出 28 个上游工具的元数据(含 input_schema,供前端动态表单)。"""
return {
"tools": [
{
"name": f"browser-{m.name}",
"upstream_name": m.name,
"title": m.title,
"summary": m.summary,
"description": m.description,
"input_schema": m.input_schema,
"read_only": m.read_only,
}
for m in TOOLS_META
],
"total": len(TOOLS_META),
}
@router.get("/health")
async def health():
"""健康检查:二进制可用性 + 子进程存活 + 上游工具集漂移校验。"""
check = locate_lightpanda()
client = LightpandaClient.instance()
process_alive = False
drift: Optional[Dict[str, Any]] = None
try:
process_alive = client.process_alive
except Exception:
process_alive = False
# 子进程已启动时校验上游工具集与静态 meta 是否漂移
cached = client.tools_cache
if cached is not None:
upstream_names = {t.get("name") for t in cached if isinstance(t, dict)}
meta_names = {m.name for m in TOOLS_META}
missing = sorted(meta_names - upstream_names) # meta 有、上游无
extra = sorted(upstream_names - meta_names) # 上游有、meta 无
if missing or extra:
drift = {"missing_in_upstream": missing, "extra_in_upstream": extra}
return {
"binary_available": check.available,
"binary_path": check.path,
"binary_version": check.version,
"process_alive": process_alive,
"tools_cached": cached is not None,
"tools_count": len(cached) if cached else 0,
"drift": drift,
}
@router.post("/mcp/{tool_name}")
async def call_mcp_tool(tool_name: str, body: Dict[str, Any]):
"""同步调用 MCP 工具;返回结构化结果(run_id + result/error_code)。
前端轮询 run_log 时用返回的 run_id。
"""
from . import mcp as mcp_module
# tool_name 形如 "browser-goto" 或 "goto",统一查 _mcp_tool_def
func = None
for name in dir(mcp_module):
attr = getattr(mcp_module, name, None)
if callable(attr) and hasattr(attr, "_mcp_tool_def"):
defn = attr._mcp_tool_def
registered = defn.get("name")
if registered == tool_name or registered == f"browser-{tool_name}" or registered.endswith(f"-{tool_name}"):
func = attr
break
if func is None:
raise HTTPException(status_code=404, detail=f"未找到 MCP 工具: {tool_name}")
try:
# 工具签名: async def _tool(params: Optional[dict] = None)
result = await func(body or {})
except Exception as e:
raise HTTPException(status_code=500, detail=f"工具执行失败: {e}")
return result
@router.post("/process/stop")
async def process_stop():
"""停止 lightpanda 子进程(下次工具调用会重新懒启动)。"""
await LightpandaClient.instance().close()
return {"success": True, "message": "lightpanda 子进程已停止"}
@router.get("/process/logs")
async def process_logs(tail: int = 50):
"""查看子进程 stderr 最近 N 行。"""
from pathlib import Path
log_path = Path("data/lightpanda") / "mcp.log"
if not log_path.exists():
return {"lines": [], "message": "日志文件不存在(子进程尚未启动过)"}
try:
data = log_path.read_bytes()
text = data.decode("utf-8", errors="replace")
lines = text.splitlines()[-tail:]
return {"lines": lines, "log_path": str(log_path)}
except OSError as e:
raise HTTPException(status_code=500, detail=f"读取日志失败: {e}")
|