Initial commit: SenSu Alpha 0.2.0
- 13-service async plugin framework - Textual TUI with CLI fallback - Plugin hot-reload + permission system - Web management panel (aiohttp) - Bridge-based inter-module communication - 10 regression tests Fixes applied: - PBKDF2-SHA256 auth (was plain SHA256) - Auth bypass removed (was allow-all on fail) - Bare excepts replaced with logged errors - CatFramework/DreamSu -> SenSu naming unified - ServiceManager: health checks + startup_order - Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
框架功能集 - 提供各种工具函数和工具类
|
||||
"""
|
||||
|
||||
from .file_utils import FileUtils
|
||||
from .config_utils import ConfigUtils
|
||||
from .validation_utils import ValidationUtils
|
||||
from .network_utils import NetworkUtils
|
||||
from .plugin_utils import PluginUtils
|
||||
|
||||
__all__ = [
|
||||
'FileUtils',
|
||||
'ConfigUtils',
|
||||
'ValidationUtils',
|
||||
'NetworkUtils',
|
||||
'PluginUtils'
|
||||
]
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import yaml
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import copy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ConfigUtils:
|
||||
"""配置工具类"""
|
||||
|
||||
@staticmethod
|
||||
def load_yaml_config(file_path: str, default_config: Dict = None) -> Dict:
|
||||
"""加载YAML配置文件"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
logger.warning(f"YAML配置文件不存在: {file_path}")
|
||||
if default_config:
|
||||
ConfigUtils.save_yaml_config(file_path, default_config)
|
||||
logger.debug(f"已创建默认YAML配置: {file_path}")
|
||||
return default_config or {}
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
logger.debug(f"YAML配置加载成功: {file_path}")
|
||||
return config or {}
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
logger.error(f"YAML配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
|
||||
return default_config or {}
|
||||
except Exception as e:
|
||||
logger.error(f"加载YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return default_config or {}
|
||||
|
||||
@staticmethod
|
||||
def save_yaml_config(file_path: str, config: Dict) -> bool:
|
||||
"""保存YAML配置文件"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
|
||||
logger.debug(f"YAML配置保存成功: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_json_config(file_path: str, default_config: Dict = None) -> Dict:
|
||||
"""加载JSON配置文件"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
logger.warning(f"JSON配置文件不存在: {file_path}")
|
||||
if default_config:
|
||||
ConfigUtils.save_json_config(file_path, default_config)
|
||||
logger.debug(f"已创建默认JSON配置: {file_path}")
|
||||
return default_config or {}
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
logger.debug(f"JSON配置加载成功: {file_path}")
|
||||
return config
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
|
||||
return default_config or {}
|
||||
except Exception as e:
|
||||
logger.error(f"加载JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return default_config or {}
|
||||
|
||||
@staticmethod
|
||||
def save_json_config(file_path: str, config: Dict) -> bool:
|
||||
"""保存JSON配置文件"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.debug(f"JSON配置保存成功: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_nested_value(config: Dict, key_path: str, default: Any = None) -> Any:
|
||||
"""获取嵌套配置值"""
|
||||
try:
|
||||
keys = key_path.split('.')
|
||||
current = config
|
||||
|
||||
for key in keys:
|
||||
if isinstance(current, dict) and key in current:
|
||||
current = current[key]
|
||||
else:
|
||||
logger.debug(f"配置键不存在: {key_path}")
|
||||
return default
|
||||
|
||||
logger.debug(f"获取嵌套配置值: {key_path} -> {current}")
|
||||
return current
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def set_nested_value(config: Dict, key_path: str, value: Any) -> bool:
|
||||
"""设置嵌套配置值"""
|
||||
try:
|
||||
keys = key_path.split('.')
|
||||
current = config
|
||||
|
||||
# 遍历到最后一个键的父级
|
||||
for key in keys[:-1]:
|
||||
if key not in current or not isinstance(current[key], dict):
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
|
||||
# 设置值
|
||||
current[keys[-1]] = value
|
||||
logger.debug(f"设置嵌套配置值: {key_path} -> {value}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"设置嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def merge_configs(base_config: Dict, override_config: Dict) -> Dict:
|
||||
"""合并配置(深度合并)"""
|
||||
try:
|
||||
result = copy.deepcopy(base_config)
|
||||
|
||||
for key, value in override_config.items():
|
||||
if (key in result and
|
||||
isinstance(result[key], dict) and
|
||||
isinstance(value, dict)):
|
||||
# 递归合并字典
|
||||
result[key] = ConfigUtils.merge_configs(result[key], value)
|
||||
else:
|
||||
# 直接覆盖
|
||||
result[key] = copy.deepcopy(value)
|
||||
|
||||
logger.debug("配置合并完成")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"合并配置时出错: {str(e)}", exc_info=True)
|
||||
return base_config
|
||||
|
||||
@staticmethod
|
||||
def validate_config_structure(config: Dict, schema: Dict) -> bool:
|
||||
"""验证配置结构"""
|
||||
try:
|
||||
def _validate(current_config, current_schema, path=""):
|
||||
for key, expected_type in current_schema.items():
|
||||
full_path = f"{path}.{key}" if path else key
|
||||
|
||||
if key not in current_config:
|
||||
logger.error(f"配置缺少必要字段: {full_path}")
|
||||
return False
|
||||
|
||||
actual_value = current_config[key]
|
||||
expected_type_name = expected_type.__name__ if hasattr(expected_type, '__name__') else str(expected_type)
|
||||
|
||||
if not isinstance(actual_value, expected_type):
|
||||
logger.error(f"配置类型错误 {full_path}: 期望 {expected_type_name}, 实际 {type(actual_value).__name__}")
|
||||
return False
|
||||
|
||||
# 如果是字典且schema有嵌套定义,递归验证
|
||||
if (isinstance(expected_type, dict) and
|
||||
isinstance(actual_value, dict)):
|
||||
if not _validate(actual_value, expected_type, full_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
result = _validate(config, schema)
|
||||
if result:
|
||||
logger.debug("配置结构验证通过")
|
||||
else:
|
||||
logger.error("配置结构验证失败")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证配置结构时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
import hashlib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FileUtils:
|
||||
"""文件操作工具类"""
|
||||
|
||||
@staticmethod
|
||||
def ensure_directory(directory_path: str) -> bool:
|
||||
"""确保目录存在"""
|
||||
try:
|
||||
path = Path(directory_path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"目录已确保存在: {directory_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"创建目录时出错 {directory_path}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def safe_write(file_path: str, content: str, backup: bool = True) -> bool:
|
||||
"""安全写入文件(支持备份)"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
# 备份原文件
|
||||
if backup and path.exists():
|
||||
backup_path = path.with_suffix(path.suffix + '.bak')
|
||||
shutil.copy2(path, backup_path)
|
||||
logger.debug(f"文件已备份: {backup_path}")
|
||||
|
||||
# 写入新内容
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
logger.debug(f"文件写入成功: {file_path}, 大小: {len(content)} 字节")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"写入文件时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def safe_read(file_path: str, default: str = "") -> str:
|
||||
"""安全读取文件"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
logger.warning(f"文件不存在: {file_path}")
|
||||
return default
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
logger.debug(f"文件读取成功: {file_path}, 大小: {len(content)} 字节")
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def list_files(directory: str, pattern: str = "*", recursive: bool = False) -> List[Path]:
|
||||
"""列出目录中的文件"""
|
||||
try:
|
||||
path = Path(directory)
|
||||
if not path.exists():
|
||||
logger.warning(f"目录不存在: {directory}")
|
||||
return []
|
||||
|
||||
if recursive:
|
||||
files = list(path.rglob(pattern))
|
||||
else:
|
||||
files = list(path.glob(pattern))
|
||||
|
||||
# 过滤出文件(非目录)
|
||||
files = [f for f in files if f.is_file()]
|
||||
|
||||
logger.debug(f"列出文件: {directory}, 模式: {pattern}, 找到 {len(files)} 个文件")
|
||||
return files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"列出文件时出错 {directory}: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def calculate_file_hash(file_path: str, algorithm: str = "md5") -> Optional[str]:
|
||||
"""计算文件哈希值"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
logger.warning(f"文件不存在: {file_path}")
|
||||
return None
|
||||
|
||||
hash_func = getattr(hashlib, algorithm)()
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_func.update(chunk)
|
||||
|
||||
file_hash = hash_func.hexdigest()
|
||||
logger.debug(f"文件哈希计算完成: {file_path} -> {algorithm}:{file_hash}")
|
||||
return file_hash
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"计算文件哈希时出错 {file_path}: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def cleanup_old_files(directory: str, pattern: str, keep_count: int) -> int:
|
||||
"""清理旧文件,保留指定数量的最新文件"""
|
||||
try:
|
||||
files = FileUtils.list_files(directory, pattern)
|
||||
if len(files) <= keep_count:
|
||||
logger.debug(f"文件数量未超过限制,无需清理: {directory}")
|
||||
return 0
|
||||
|
||||
# 按修改时间排序
|
||||
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
|
||||
# 删除旧文件
|
||||
removed_count = 0
|
||||
for file_to_remove in files[keep_count:]:
|
||||
try:
|
||||
file_to_remove.unlink()
|
||||
removed_count += 1
|
||||
logger.debug(f"删除旧文件: {file_to_remove}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除文件时出错 {file_to_remove}: {str(e)}", exc_info=True)
|
||||
|
||||
logger.info(f"文件清理完成: {directory}, 删除 {removed_count} 个文件")
|
||||
return removed_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理旧文件时出错 {directory}: {str(e)}", exc_info=True)
|
||||
return 0
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
import aiohttp
|
||||
import ssl
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NetworkUtils:
|
||||
"""网络工具类"""
|
||||
|
||||
@staticmethod
|
||||
async def check_port_available(host: str, port: int) -> bool:
|
||||
"""检查端口是否可用"""
|
||||
try:
|
||||
# 尝试创建socket连接
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
logger.debug(f"端口 {host}:{port} 已被占用")
|
||||
return False
|
||||
except (ConnectionRefusedError, asyncio.TimeoutError):
|
||||
logger.debug(f"端口 {host}:{port} 可用")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"检查端口可用性时出错 {host}:{port}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def find_available_port(host: str = "localhost", start_port: int = 8000,
|
||||
max_attempts: int = 100) -> Optional[int]:
|
||||
"""查找可用端口"""
|
||||
try:
|
||||
for port in range(start_port, start_port + max_attempts):
|
||||
if await NetworkUtils.check_port_available(host, port):
|
||||
logger.debug(f"找到可用端口: {host}:{port}")
|
||||
return port
|
||||
logger.warning(f"在范围 {start_port}-{start_port + max_attempts} 内未找到可用端口")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"查找可用端口时出错: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def http_request(url: str, method: str = "GET", headers: dict = None,
|
||||
data: dict = None, timeout: int = 30) -> Tuple[bool, dict]:
|
||||
"""发送HTTP请求"""
|
||||
try:
|
||||
logger.debug(f"发送HTTP请求: {method} {url}")
|
||||
|
||||
timeout_obj = aiohttp.ClientTimeout(total=timeout)
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
|
||||
async with session.request(method, url, headers=headers, json=data) as response:
|
||||
response_data = await response.text()
|
||||
|
||||
result = {
|
||||
"status": response.status,
|
||||
"headers": dict(response.headers),
|
||||
"data": response_data,
|
||||
"url": str(response.url)
|
||||
}
|
||||
|
||||
logger.debug(f"HTTP请求完成: {method} {url} -> 状态 {response.status}")
|
||||
return True, result
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"HTTP请求超时: {method} {url}")
|
||||
return False, {"error": "请求超时"}
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"HTTP客户端错误: {method} {url} -> {str(e)}")
|
||||
return False, {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"HTTP请求时出错: {method} {url} -> {str(e)}", exc_info=True)
|
||||
return False, {"error": str(e)}
|
||||
|
||||
@staticmethod
|
||||
def get_local_ip() -> str:
|
||||
"""获取本地IP地址"""
|
||||
try:
|
||||
# 创建一个socket连接来获取本地IP
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
local_ip = s.getsockname()[0]
|
||||
|
||||
logger.debug(f"获取本地IP: {local_ip}")
|
||||
return local_ip
|
||||
except Exception as e:
|
||||
logger.error(f"获取本地IP时出错: {str(e)}", exc_info=True)
|
||||
return "127.0.0.1"
|
||||
|
||||
@staticmethod
|
||||
def is_valid_hostname(hostname: str) -> bool:
|
||||
"""验证主机名格式"""
|
||||
try:
|
||||
if len(hostname) > 255:
|
||||
return False
|
||||
|
||||
if hostname[-1] == ".":
|
||||
hostname = hostname[:-1]
|
||||
|
||||
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
|
||||
return all(allowed.match(x) for x in hostname.split("."))
|
||||
except Exception as e:
|
||||
logger.error(f"验证主机名时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import inspect
|
||||
import re
|
||||
from typing import Dict, List, Any, Callable
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PluginUtils:
|
||||
"""插件工具类"""
|
||||
|
||||
@staticmethod
|
||||
def validate_plugin_structure(plugin_path: Path) -> bool:
|
||||
"""验证插件结构"""
|
||||
try:
|
||||
logger.debug(f"验证插件结构: {plugin_path}")
|
||||
|
||||
required_files = [
|
||||
"__init__.py",
|
||||
"config.yaml",
|
||||
"permissions.yaml"
|
||||
]
|
||||
|
||||
# 检查必需文件
|
||||
for file_name in required_files:
|
||||
if not (plugin_path / file_name).exists():
|
||||
logger.error(f"插件缺少必需文件: {file_name}")
|
||||
return False
|
||||
|
||||
# 检查主模块是否有Plugin类
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("plugin_module", plugin_path / "__init__.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, 'Plugin'):
|
||||
logger.error("插件主模块缺少Plugin类")
|
||||
return False
|
||||
|
||||
# 检查Plugin类是否有必要方法
|
||||
plugin_class = module.Plugin
|
||||
required_methods = ['initialize', 'shutdown']
|
||||
|
||||
for method_name in required_methods:
|
||||
if not hasattr(plugin_class, method_name):
|
||||
logger.error(f"Plugin类缺少必要方法: {method_name}")
|
||||
return False
|
||||
|
||||
logger.debug(f"插件结构验证通过: {plugin_path.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证插件类时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证插件结构时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_plugin_dependencies(plugin_path: Path) -> List[str]:
|
||||
"""获取插件依赖"""
|
||||
try:
|
||||
config_file = plugin_path / "config.yaml"
|
||||
if not config_file.exists():
|
||||
return []
|
||||
|
||||
import yaml
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
dependencies = config.get('dependencies', [])
|
||||
if isinstance(dependencies, list):
|
||||
logger.debug(f"获取插件依赖: {plugin_path.name} -> {dependencies}")
|
||||
return dependencies
|
||||
else:
|
||||
logger.warning(f"插件依赖格式错误: {plugin_path.name}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取插件依赖时出错: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def scan_plugin_methods(plugin_instance) -> Dict[str, List[str]]:
|
||||
"""扫描插件方法"""
|
||||
try:
|
||||
logger.debug(f"扫描插件方法: {type(plugin_instance).__name__}")
|
||||
|
||||
methods_info = {
|
||||
"public_methods": [],
|
||||
"private_methods": [],
|
||||
"async_methods": [],
|
||||
"event_handlers": []
|
||||
}
|
||||
|
||||
for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod):
|
||||
# 跳过特殊方法
|
||||
if name.startswith('_') and not name.startswith('__'):
|
||||
methods_info["private_methods"].append(name)
|
||||
elif not name.startswith('_'):
|
||||
methods_info["public_methods"].append(name)
|
||||
|
||||
# 检查是否为异步方法
|
||||
if inspect.iscoroutinefunction(method):
|
||||
methods_info["async_methods"].append(name)
|
||||
|
||||
# 检查是否为事件处理器
|
||||
if name.startswith('handle_') or name.startswith('on_'):
|
||||
methods_info["event_handlers"].append(name)
|
||||
|
||||
logger.debug(f"插件方法扫描完成: 公共{len(methods_info['public_methods'])}个, 私有{len(methods_info['private_methods'])}个")
|
||||
return methods_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描插件方法时出错: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def create_plugin_skeleton(plugin_name: str, plugin_path: Path) -> bool:
|
||||
"""创建插件骨架"""
|
||||
try:
|
||||
logger.debug(f"创建插件骨架: {plugin_name} -> {plugin_path}")
|
||||
|
||||
# 创建插件目录
|
||||
plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建主模块文件
|
||||
init_content = '''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Plugin:
|
||||
"""{plugin_name} 插件"""
|
||||
|
||||
def __init__(self, plugin_name: str, config: Dict, bridge):
|
||||
self.plugin_name = plugin_name
|
||||
self.config = config
|
||||
self.bridge = bridge
|
||||
self.is_running = False
|
||||
logger.debug(f"插件初始化: {{plugin_name}}")
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化插件"""
|
||||
try:
|
||||
logger.info(f"初始化插件: {{self.plugin_name}}")
|
||||
|
||||
# 在这里注册事件处理器和命令
|
||||
# 示例: self.bridge.subscribe_plugin(self.plugin_name, "event.name", self.handler)
|
||||
|
||||
self.is_running = True
|
||||
logger.debug(f"插件初始化完成: {{self.plugin_name}}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"初始化插件时出错: {{str(e)}}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def shutdown(self):
|
||||
"""关闭插件"""
|
||||
try:
|
||||
logger.info(f"关闭插件: {{self.plugin_name}}")
|
||||
self.is_running = False
|
||||
|
||||
# 清理资源
|
||||
self.bridge.cleanup_plugin_subscriptions(self.plugin_name)
|
||||
|
||||
logger.debug(f"插件关闭完成: {{self.plugin_name}}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"关闭插件时出错: {{str(e)}}", exc_info=True)
|
||||
|
||||
# 在这里添加你的插件方法
|
||||
async def example_method(self, message: str) -> str:
|
||||
"""示例方法"""
|
||||
try:
|
||||
logger.debug(f"插件方法调用: {{message}}")
|
||||
return f"插件响应: {{message}}"
|
||||
except Exception as e:
|
||||
logger.error(f"插件方法调用出错: {{str(e)}}", exc_info=True)
|
||||
raise
|
||||
'''.format(plugin_name=plugin_name)
|
||||
|
||||
with open(plugin_path / "__init__.py", 'w', encoding='utf-8') as f:
|
||||
f.write(init_content)
|
||||
|
||||
# 创建配置文件
|
||||
config_content = f'''# {plugin_name} 插件配置
|
||||
name: "{plugin_name}"
|
||||
version: "1.0.0"
|
||||
description: "{plugin_name} 插件描述"
|
||||
author: "插件作者"
|
||||
|
||||
# 插件特定配置
|
||||
settings:
|
||||
enabled: true
|
||||
auto_start: true
|
||||
log_level: "INFO"
|
||||
|
||||
# 依赖配置
|
||||
dependencies: []
|
||||
'''
|
||||
|
||||
with open(plugin_path / "config.yaml", 'w', encoding='utf-8') as f:
|
||||
f.write(config_content)
|
||||
|
||||
# 创建权限文件
|
||||
permissions_content = f'''# {plugin_name} 插件权限申请
|
||||
plugin_name: "{plugin_name}"
|
||||
permissions:
|
||||
- "plugin.{plugin_name}.read"
|
||||
- "plugin.{plugin_name}.write"
|
||||
|
||||
# 权限说明
|
||||
permission_descriptions:
|
||||
plugin.{plugin_name}.read: "读取{plugin_name}插件数据"
|
||||
plugin.{plugin_name}.write: "写入{plugin_name}插件数据"
|
||||
'''
|
||||
|
||||
with open(plugin_path / "permissions.yaml", 'w', encoding='utf-8') as f:
|
||||
f.write(permissions_content)
|
||||
|
||||
logger.info(f"插件骨架创建完成: {plugin_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建插件骨架时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_plugin_permissions(plugin_path: Path, requested_permissions: List[str]) -> bool:
|
||||
"""验证插件权限申请"""
|
||||
try:
|
||||
logger.debug(f"验证插件权限: {plugin_path.name}")
|
||||
|
||||
# 检查权限格式
|
||||
for permission in requested_permissions:
|
||||
if not isinstance(permission, str):
|
||||
logger.error(f"权限格式错误: {permission}")
|
||||
return False
|
||||
|
||||
# 检查权限命名规范
|
||||
if not re.match(r'^[a-z][a-z0-9_.]*$', permission):
|
||||
logger.error(f"权限命名不规范: {permission}")
|
||||
return False
|
||||
|
||||
logger.debug(f"插件权限验证通过: {len(requested_permissions)} 个权限")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证插件权限时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
from typing import Any, List, Optional, Callable, Dict
|
||||
from urllib.parse import urlparse
|
||||
import ipaddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ValidationUtils:
|
||||
"""验证工具类"""
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""验证邮箱格式"""
|
||||
try:
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
result = bool(re.match(pattern, email))
|
||||
logger.debug(f"邮箱验证: {email} -> {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"验证邮箱时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_url(url: str) -> bool:
|
||||
"""验证URL格式"""
|
||||
try:
|
||||
result = urlparse(url)
|
||||
is_valid = all([result.scheme, result.netloc])
|
||||
logger.debug(f"URL验证: {url} -> {is_valid}")
|
||||
return is_valid
|
||||
except Exception as e:
|
||||
logger.error(f"验证URL时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_ip(ip: str) -> bool:
|
||||
"""验证IP地址格式"""
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
logger.debug(f"IP地址验证: {ip} -> True")
|
||||
return True
|
||||
except ValueError:
|
||||
logger.debug(f"IP地址验证: {ip} -> False")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"验证IP地址时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_port(port: int) -> bool:
|
||||
"""验证端口号"""
|
||||
try:
|
||||
is_valid = 1 <= port <= 65535
|
||||
logger.debug(f"端口验证: {port} -> {is_valid}")
|
||||
return is_valid
|
||||
except Exception as e:
|
||||
logger.error(f"验证端口时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_string(value: Any, min_length: int = 0, max_length: int = None,
|
||||
pattern: str = None) -> bool:
|
||||
"""验证字符串"""
|
||||
try:
|
||||
if not isinstance(value, str):
|
||||
logger.debug(f"字符串验证失败: 不是字符串类型")
|
||||
return False
|
||||
|
||||
if len(value) < min_length:
|
||||
logger.debug(f"字符串验证失败: 长度小于 {min_length}")
|
||||
return False
|
||||
|
||||
if max_length and len(value) > max_length:
|
||||
logger.debug(f"字符串验证失败: 长度大于 {max_length}")
|
||||
return False
|
||||
|
||||
if pattern and not re.match(pattern, value):
|
||||
logger.debug(f"字符串验证失败: 不匹配模式 {pattern}")
|
||||
return False
|
||||
|
||||
logger.debug(f"字符串验证通过: 长度 {len(value)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证字符串时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_number(value: Any, min_value: float = None, max_value: float = None) -> bool:
|
||||
"""验证数字"""
|
||||
try:
|
||||
if not isinstance(value, (int, float)):
|
||||
# 尝试转换
|
||||
try:
|
||||
value = float(value)
|
||||
except (ValueError, TypeError):
|
||||
logger.debug(f"数字验证失败: 无法转换为数字")
|
||||
return False
|
||||
|
||||
if min_value is not None and value < min_value:
|
||||
logger.debug(f"数字验证失败: 值小于 {min_value}")
|
||||
return False
|
||||
|
||||
if max_value is not None and value > max_value:
|
||||
logger.debug(f"数字验证失败: 值大于 {max_value}")
|
||||
return False
|
||||
|
||||
logger.debug(f"数字验证通过: {value}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证数字时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_list(value: Any, min_length: int = 0, max_length: int = None,
|
||||
item_validator: Callable = None) -> bool:
|
||||
"""验证列表"""
|
||||
try:
|
||||
if not isinstance(value, list):
|
||||
logger.debug(f"列表验证失败: 不是列表类型")
|
||||
return False
|
||||
|
||||
if len(value) < min_length:
|
||||
logger.debug(f"列表验证失败: 长度小于 {min_length}")
|
||||
return False
|
||||
|
||||
if max_length and len(value) > max_length:
|
||||
logger.debug(f"列表验证失败: 长度大于 {max_length}")
|
||||
return False
|
||||
|
||||
if item_validator:
|
||||
for i, item in enumerate(value):
|
||||
if not item_validator(item):
|
||||
logger.debug(f"列表验证失败: 第 {i} 项验证失败")
|
||||
return False
|
||||
|
||||
logger.debug(f"列表验证通过: 长度 {len(value)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证列表时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_dict(value: Any, required_keys: List[str] = None,
|
||||
key_validators: Dict[str, Callable] = None) -> bool:
|
||||
"""验证字典"""
|
||||
try:
|
||||
if not isinstance(value, dict):
|
||||
logger.debug(f"字典验证失败: 不是字典类型")
|
||||
return False
|
||||
|
||||
# 检查必需键
|
||||
if required_keys:
|
||||
for key in required_keys:
|
||||
if key not in value:
|
||||
logger.debug(f"字典验证失败: 缺少必需键 {key}")
|
||||
return False
|
||||
|
||||
# 检查键值验证器
|
||||
if key_validators:
|
||||
for key, validator in key_validators.items():
|
||||
if key in value and not validator(value[key]):
|
||||
logger.debug(f"字典验证失败: 键 {key} 的值验证失败")
|
||||
return False
|
||||
|
||||
logger.debug(f"字典验证通过: 键数 {len(value)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证字典时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_file_path(file_path: str, check_exists: bool = True,
|
||||
check_readable: bool = False, check_writable: bool = False) -> bool:
|
||||
"""验证文件路径"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(file_path)
|
||||
|
||||
if check_exists and not path.exists():
|
||||
logger.debug(f"文件路径验证失败: 文件不存在")
|
||||
return False
|
||||
|
||||
if check_readable and not os.access(path, os.R_OK):
|
||||
logger.debug(f"文件路径验证失败: 文件不可读")
|
||||
return False
|
||||
|
||||
if check_writable and not os.access(path, os.W_OK):
|
||||
logger.debug(f"文件路径验证失败: 文件不可写")
|
||||
return False
|
||||
|
||||
logger.debug(f"文件路径验证通过: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证文件路径时出错: {str(e)}", exc_info=True)
|
||||
return False
|
||||
Reference in New Issue
Block a user