| """ |
| 浏览器MCP插件 MCP 工具定义(v3.0 纯 Lightpanda 透传)。 |
| |
| 28 个工具,每个由工厂从 tools_meta 生成,统一入参 params: dict 透传给 Lightpanda |
| 上游,平台层不定义任何浏览器语义: |
| - 工具名/参数/返回/错误码均由 Lightpanda 上游决定。 |
| - 平台只负责:run_log 埋点、调用透传、错误码归一为 BrowserError。 |
| - 单例长驻子进程,复用会话,支持 goto→click→extract 链式操作。 |
| """ |
|
|
| import logging |
| from typing import Any, Callable, Dict, Optional |
|
|
| from app.mcp.decorators import mcp_tool |
| from app.plugins.run_log import get_run_log_service, EventLevel, RunStatus |
|
|
| from .browser.client import LightpandaClient, DEFAULT_TOOL_TIMEOUT |
| from .browser.errors import BrowserError, BrowserErrorCode |
| from .browser.tools_meta import TOOLS_META, ToolMeta |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def _annotations(meta: ToolMeta) -> Dict[str, Any]: |
| """从上游谓词推导 MCP annotations,让 agent 知道工具是否只读。""" |
| return { |
| "readOnlyHint": meta.read_only, |
| "destructiveHint": meta.destructive, |
| } |
|
|
|
|
| def _summarize_args(args: Optional[dict]) -> str: |
| """生成参数摘要用于 run_log(截断长值,避免日志爆炸)。""" |
| if not args: |
| return "{}" |
| parts = [] |
| for k, v in args.items(): |
| s = v if isinstance(v, str) else repr(v) |
| if len(s) > 80: |
| s = s[:77] + "..." |
| parts.append(f"{k}={s}") |
| return ", ".join(parts) |
|
|
|
|
| async def _run_with_log(tool: str, params: Optional[dict]) -> Dict[str, Any]: |
| """统一执行包装:create_run → 透传调用 → finish_run。 |
| |
| 返回 {success, run_id, tool, result/error_code}。 |
| """ |
| run_service = get_run_log_service() |
| run = run_service.create_run("browser") |
| run_service.add_event(run.run_id, "init", f"调用工具: {tool}") |
| args = params or {} |
| run_service.add_event(run.run_id, "call", f"{tool}({_summarize_args(args)})") |
|
|
| try: |
| text = await LightpandaClient.instance().call(tool, args, timeout=DEFAULT_TOOL_TIMEOUT) |
| except BrowserError as e: |
| _safe_event(run_service, run.run_id, "error", f"{e.code.value}: {e}", level=EventLevel.ERROR) |
| _safe_finish(run_service, run.run_id, RunStatus.FAILED, error=str(e)) |
| return { |
| "success": False, |
| "run_id": run.run_id, |
| "tool": tool, |
| "error": str(e), |
| "error_code": e.code.value, |
| } |
| except Exception as e: |
| |
| _safe_event(run_service, run.run_id, "error", f"未捕获: {e}", level=EventLevel.ERROR) |
| _safe_finish(run_service, run.run_id, RunStatus.FAILED, error=str(e)) |
| return { |
| "success": False, |
| "run_id": run.run_id, |
| "tool": tool, |
| "error": str(e), |
| "error_code": BrowserErrorCode.TOOL_ERROR.value, |
| } |
|
|
| _safe_event(run_service, run.run_id, "complete", f"{tool} 完成") |
| result = { |
| "success": True, |
| "run_id": run.run_id, |
| "tool": tool, |
| "result": text, |
| } |
| |
| _safe_finish(run_service, run.run_id, RunStatus.SUCCEEDED, result=result) |
| return result |
|
|
|
|
| def _safe_event(run_service, run_id: str, stage: str, message: str, level: EventLevel = EventLevel.INFO) -> None: |
| """记录 run_log 事件,失败仅记日志不抛(不阻断主链路)。""" |
| try: |
| run_service.add_event(run_id=run_id, stage=stage, message=message, level=level) |
| except Exception as e: |
| logger.warning("run_log add_event 失败(忽略): %s", e) |
|
|
|
|
| def _safe_finish(run_service, run_id: str, status: RunStatus, result=None, error=None) -> None: |
| """结束 run,失败仅记日志不抛(不阻断主链路)。""" |
| try: |
| run_service.finish_run(run_id, status=status, result=result, error=error) |
| except Exception as e: |
| logger.warning("run_log finish_run 失败(忽略): %s", e) |
|
|
|
|
| def _make_tool(meta: ToolMeta) -> Callable: |
| """为单个工具元数据生成一个 @mcp_tool 装饰的透传函数。 |
| |
| 入参统一 params: dict,透传给上游,校验由 Lightpanda 负责。 |
| """ |
| |
| tool_name = f"browser-{meta.name}" |
|
|
| @mcp_tool( |
| name=tool_name, |
| title=meta.title, |
| description=meta.description, |
| annotations=_annotations(meta), |
| risk_level="low" if meta.read_only else "medium", |
| ) |
| async def _tool(params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: |
| """透传调用 Lightpanda 上游工具。params 为该工具的入参字典。""" |
| return await _run_with_log(meta.name, params) |
|
|
| |
| _tool.__name__ = f"browser_{meta.name}" |
| _tool.__doc__ = meta.description |
| return _tool |
|
|
|
|
| |
| |
| for _meta in TOOLS_META: |
| globals()[f"browser_{_meta.name}"] = _make_tool(_meta) |
|
|
|
|
| def list_tool_defs() -> list[dict]: |
| """列出本模块所有已注册工具的 _mcp_tool_def(供 api.py / 测试用)。""" |
| import sys |
| mod = sys.modules[__name__] |
| defs = [] |
| for attr_name in dir(mod): |
| attr = getattr(mod, attr_name, None) |
| if callable(attr) and hasattr(attr, "_mcp_tool_def"): |
| defs.append(attr._mcp_tool_def) |
| return defs |
|
|