mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
140 lines
4.9 KiB
Python
140 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import inspect
|
|
import logging
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from oclaw.runtime.tools.base import ToolSpec
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ToolFactory = Callable[[], ToolSpec]
|
|
_PUBLIC_ROOT = Path(__file__).resolve().parent / "public"
|
|
_CACHED_FACTORIES: list[ToolFactory] | None = None
|
|
_CACHED_SPECS: list[ToolSpec] | None = None
|
|
|
|
|
|
def _load_module_from_path(module_path: Path, module_name: str) -> Any | None:
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
if not spec or not spec.loader:
|
|
return None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
except Exception as exc:
|
|
logger.warning("skip public module %s: %s", module_path, exc)
|
|
return None
|
|
|
|
|
|
def discover_public_tool_factories() -> list[ToolFactory]:
|
|
"""Discover public tool factories (naming convention: `*_tool`)."""
|
|
global _CACHED_FACTORIES
|
|
if _CACHED_FACTORIES is not None:
|
|
return list(_CACHED_FACTORIES)
|
|
if not _PUBLIC_ROOT.exists():
|
|
_CACHED_FACTORIES = []
|
|
return []
|
|
factories: list[ToolFactory] = []
|
|
for module_path in sorted(_PUBLIC_ROOT.glob("*.py")):
|
|
if module_path.name == "__init__.py":
|
|
continue
|
|
mod_name = f"oclaw.runtime.tools.public.{module_path.stem}"
|
|
module = _load_module_from_path(module_path, mod_name)
|
|
if module is None:
|
|
continue
|
|
exported = getattr(module, "__all__", None)
|
|
if isinstance(exported, list) and exported:
|
|
for name in sorted(exported):
|
|
value = getattr(module, name, None)
|
|
if callable(value) and name.endswith("_tool"):
|
|
factories.append(value)
|
|
continue
|
|
for name, value in sorted(inspect.getmembers(module)):
|
|
if callable(value) and name.endswith("_tool"):
|
|
factories.append(value)
|
|
_CACHED_FACTORIES = list(factories)
|
|
return list(factories)
|
|
|
|
|
|
def materialize_public_tools() -> list[ToolSpec]:
|
|
global _CACHED_SPECS
|
|
if _CACHED_SPECS is not None:
|
|
return list(_CACHED_SPECS)
|
|
specs: list[ToolSpec] = []
|
|
for factory in discover_public_tool_factories():
|
|
try:
|
|
specs.append(factory())
|
|
except Exception as exc:
|
|
logger.warning("skip public tool factory %s: %s", factory, exc)
|
|
_CACHED_SPECS = list(specs)
|
|
return list(specs)
|
|
|
|
|
|
def clear_public_tool_cache() -> None:
|
|
global _CACHED_FACTORIES, _CACHED_SPECS
|
|
_CACHED_FACTORIES = None
|
|
_CACHED_SPECS = None
|
|
|
|
|
|
def preview_public_tools() -> dict[str, Any]:
|
|
"""Preview public tool load outcome, including skip reasons.
|
|
|
|
This intentionally bypasses caches so Admin UI can reflect current filesystem state.
|
|
"""
|
|
out_tools: list[ToolSpec] = []
|
|
skipped: list[dict[str, str]] = []
|
|
if not _PUBLIC_ROOT.exists():
|
|
return {"tools": [], "skipped": []}
|
|
for module_path in sorted(_PUBLIC_ROOT.glob("*.py")):
|
|
if module_path.name == "__init__.py":
|
|
continue
|
|
mod_name = f"oclaw.runtime.tools.public.{module_path.stem}"
|
|
module = None
|
|
try:
|
|
module = _load_module_from_path(module_path, mod_name)
|
|
except Exception as exc:
|
|
skipped.append({"module": str(module_path), "error_code": "module_load_exception", "error": str(exc)})
|
|
continue
|
|
if module is None:
|
|
skipped.append({"module": str(module_path), "error_code": "module_load_failed", "error": "load_failed"})
|
|
continue
|
|
factories: list[ToolFactory] = []
|
|
exported = getattr(module, "__all__", None)
|
|
if isinstance(exported, list) and exported:
|
|
for name in sorted(exported):
|
|
value = getattr(module, name, None)
|
|
if callable(value) and name.endswith("_tool"):
|
|
factories.append(value)
|
|
else:
|
|
for name, value in sorted(inspect.getmembers(module)):
|
|
if callable(value) and name.endswith("_tool"):
|
|
factories.append(value)
|
|
if not factories:
|
|
continue
|
|
for factory in factories:
|
|
try:
|
|
out_tools.append(factory())
|
|
except Exception as exc:
|
|
skipped.append(
|
|
{
|
|
"module": str(module_path),
|
|
"error_code": "tool_factory_failed",
|
|
"error": str(exc),
|
|
"factory": str(getattr(factory, "__name__", "") or str(factory)),
|
|
}
|
|
)
|
|
return {"tools": out_tools, "skipped": skipped}
|
|
|
|
|
|
__all__ = [
|
|
"ToolFactory",
|
|
"clear_public_tool_cache",
|
|
"discover_public_tool_factories",
|
|
"materialize_public_tools",
|
|
"preview_public_tools",
|
|
]
|
|
|