ForcePilot/backend/package/yuxi/channel/hooks/hook_scanner.py
Kris a81279cf61 feat(channel/hooks): 实现完整的钩子系统模块
新增了完整的钩子系统,包括元数据定义、扫描器、解析器、生命周期注册表和加载器,支持从目录扫描HOOK.md配置、验证依赖和系统兼容性,以及通过沙箱加载钩子脚本,实现了事件驱动的钩子回调机制。
2026-05-21 10:26:47 +08:00

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