新增了完整的钩子系统,包括元数据定义、扫描器、解析器、生命周期注册表和加载器,支持从目录扫描HOOK.md配置、验证依赖和系统兼容性,以及通过沙箱加载钩子脚本,实现了事件驱动的钩子回调机制。
107 lines
2.9 KiB
Python
107 lines
2.9 KiB
Python
import logging
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from yuxi.channel.hooks.hook_parser import HookParseError, HookValidationError, parse_hook_md
|
|
from yuxi.channel.hooks.types import HookMetadata
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_SKIP_DIRS = frozenset(
|
|
{
|
|
"__pycache__",
|
|
".git",
|
|
".svn",
|
|
".hg",
|
|
".venv",
|
|
"venv",
|
|
".tox",
|
|
".eggs",
|
|
".mypy_cache",
|
|
".pytest_cache",
|
|
".ruff_cache",
|
|
"node_modules",
|
|
".idea",
|
|
".vscode",
|
|
}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class DiscoveredHook:
|
|
hook_dir: Path
|
|
metadata: HookMetadata
|
|
|
|
@property
|
|
def source(self) -> str:
|
|
return str(self.hook_dir)
|
|
|
|
|
|
@dataclass
|
|
class ScanResult:
|
|
hooks: list[DiscoveredHook] = field(default_factory=list)
|
|
errors: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def hook_count(self) -> int:
|
|
return len(self.hooks)
|
|
|
|
@property
|
|
def succeeded(self) -> bool:
|
|
return len(self.errors) == 0
|
|
|
|
|
|
def scan_directory(directory: Path | str, *, recursive: bool = True, max_depth: int = 10) -> ScanResult:
|
|
hook_dirs = _find_hook_dirs(Path(directory), recursive=recursive, max_depth=max_depth)
|
|
return _parse_hook_dirs(hook_dirs)
|
|
|
|
|
|
def _find_hook_dirs(root: Path, *, recursive: bool = True, max_depth: int = 10, _depth: int = 0) -> list[Path]:
|
|
root = root.resolve()
|
|
if not root.is_dir():
|
|
return []
|
|
if _depth > max_depth:
|
|
return []
|
|
|
|
result: list[Path] = []
|
|
|
|
candidate = root / "HOOK.md"
|
|
if candidate.is_file():
|
|
result.append(root)
|
|
|
|
try:
|
|
for entry in sorted(root.iterdir()):
|
|
if not entry.is_dir():
|
|
continue
|
|
if entry.name in _SKIP_DIRS:
|
|
continue
|
|
candidate = entry / "HOOK.md"
|
|
if candidate.is_file():
|
|
result.append(entry)
|
|
elif recursive:
|
|
result.extend(_find_hook_dirs(entry, recursive=recursive, max_depth=max_depth, _depth=_depth + 1))
|
|
except PermissionError:
|
|
logger.warning("Permission denied scanning directory: %s", root)
|
|
|
|
return result
|
|
|
|
|
|
def _parse_hook_dirs(hook_dirs: list[Path]) -> ScanResult:
|
|
result = ScanResult()
|
|
|
|
for hook_dir in hook_dirs:
|
|
hook_md_path = hook_dir / "HOOK.md"
|
|
try:
|
|
content = hook_md_path.read_text(encoding="utf-8")
|
|
metadata = parse_hook_md(content, source_path=hook_md_path)
|
|
result.hooks.append(DiscoveredHook(hook_dir=hook_dir, metadata=metadata))
|
|
logger.debug("Discovered hook: %s from %s", metadata.hook_id, hook_dir)
|
|
except (HookParseError, HookValidationError) as e:
|
|
result.errors.append(str(e))
|
|
logger.warning("Hook discovery failed: %s", e)
|
|
except OSError as e:
|
|
result.errors.append(f"Cannot read {hook_md_path}: {e}")
|
|
logger.warning("Cannot read HOOK.md: %s", e)
|
|
|
|
return result
|