"""客户钩子加载器 — 从 customers/{CUSTOMER}/hooks/ 动态加载客户自定义钩子 加载逻辑: 1. 读取 CUSTOMER 环境变量 2. 查找 customers/{CUSTOMER}/hooks/__init__.py 3. 导入并返回 hooks 实例 4. 找不到则返回默认空钩子(所有方法为空操作) """ import importlib import importlib.util import logging import os import sys from functools import lru_cache from typing import Optional from app.hooks.base import CustomerHooks from app.core.config import get_settings logger = logging.getLogger("hooks.loader") _hooks_instance: Optional[CustomerHooks] = None def _find_hooks_dir() -> Optional[str]: """Find the customer hooks directory, searching multiple locations.""" settings = get_settings() customer = settings.CUSTOMER config_path = settings.customer_config_path hooks_dir = os.path.join(config_path, "hooks") if os.path.isdir(hooks_dir) and os.path.exists(os.path.join(hooks_dir, "__init__.py")): return hooks_dir return None def _load_hooks_from_dir(hooks_dir: str) -> Optional[CustomerHooks]: """Load hooks module from a directory path.""" try: init_path = os.path.join(hooks_dir, "__init__.py") spec = importlib.util.spec_from_file_location("customer_hooks", init_path) if spec and spec.loader: # Add parent dir to sys.path so relative imports work parent_dir = os.path.dirname(os.path.dirname(hooks_dir)) if parent_dir not in sys.path: sys.path.insert(0, parent_dir) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) hooks = getattr(module, "hooks", None) if isinstance(hooks, CustomerHooks): return hooks else: logger.warning( f"Customer hooks module loaded but 'hooks' attribute is not a CustomerHooks instance" ) except Exception as e: logger.error(f"Failed to load customer hooks: {e}", exc_info=True) return None def get_hooks() -> CustomerHooks: """获取当前客户的钩子实例。线程安全,全局单例。 Returns: CustomerHooks: 客户钩子实例,如果客户没有自定义钩子则返回默认空钩子 """ global _hooks_instance if _hooks_instance is not None: return _hooks_instance settings = get_settings() hooks_dir = _find_hooks_dir() if hooks_dir: loaded = _load_hooks_from_dir(hooks_dir) if loaded: logger.info(f"Loaded customer hooks for '{settings.CUSTOMER}' from {hooks_dir}") _hooks_instance = loaded return _hooks_instance logger.info(f"No custom hooks for '{settings.CUSTOMER}', using defaults") _hooks_instance = CustomerHooks() return _hooks_instance def reload_hooks() -> CustomerHooks: """重新加载客户钩子(开发时热重载用)""" global _hooks_instance _hooks_instance = None return get_hooks()