refactor: 拆分 ai_cli.py 为模块化 xcli_core 包
将 4130 行的单文件拆分为 13 个职责清晰的模块: - constants.py: 常量 + 可选依赖导入 - sandbox.py: 安全代码沙箱 (AST检查器 + 子进程隔离) - config.py: 配置管理 + 日志 - plugin_manager.py: LiuginManager 插件管理器 - cli_base.py: BaseAICLI 基类 (初始化/引擎/插件/命令) - cli_clawli.py: ClawliMixin 远程模式 - cli_tools.py: ToolMixin 工具调用 - cli_code_exec.py: CodeExecMixin 代码沙箱执行 - cli_display.py: DisplayMixin 显示/输出/图片/帮助 - cli_history.py: HistoryMixin 聊天记录 - cli_core.py: AICLI 主类 + 对话循环 + main() - tui.py: Textual TUI 界面 - __init__.py: 包导出汇总 ai_cli.py 保留为入口文件 (11行), 根目录整洁。 原逻辑完全不变, 仅做结构拆分。
This commit is contained in:
1 parent
7efb6dfe8b
commit
04e2d26809
14 files changed
+3798
-4125
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import os
|
||||
import importlib.util
|
||||
from colorama import Fore, Style
|
||||
from .config import logger
|
||||
|
||||
|
||||
class LiuginManager:
|
||||
"""插件管理器"""
|
||||
|
||||
def __init__(self, liugins_dir="plugins"):
|
||||
self.liugins_dir = liugins_dir
|
||||
self.tools = []
|
||||
|
||||
def get_enabled_liugins(self):
|
||||
"""获取启用的插件列表"""
|
||||
# 检查环境变量中是否有启用的插件列表
|
||||
enabled_liugins_env = os.environ.get("XIAOLI_ENABLED_PLUGINS")
|
||||
if enabled_liugins_env is not None:
|
||||
# 特殊处理:空字符串表示不启用任何插件
|
||||
if enabled_liugins_env == "":
|
||||
return []
|
||||
return enabled_liugins_env.split(",")
|
||||
# 检查是否有插件配置文件
|
||||
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
config_file = os.path.join(project_dir, "plugins_config.py")
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
# 读取配置文件
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config_content = f.read()
|
||||
# 检查是否明确表示不启用任何插件
|
||||
if "# 不启用任何插件" in config_content:
|
||||
return []
|
||||
# 解析启用的插件
|
||||
enabled_liugins = []
|
||||
for line in config_content.split('\n'):
|
||||
if line.startswith('ENABLE_') and '= True' in line:
|
||||
liugin_name = line.split('=')[0].replace('ENABLE_', '').strip().lower()
|
||||
enabled_liugins.append(liugin_name)
|
||||
return enabled_liugins
|
||||
except (FileNotFoundError, PermissionError, UnicodeDecodeError) as e:
|
||||
logger.error(f"读取插件配置文件失败: {e}")
|
||||
print(f"{Fore.RED}读取插件配置文件失败: {e}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
logger.error(f"读取插件配置时发生未知错误: {e}")
|
||||
print(f"{Fore.RED}读取插件配置文件失败: {e}{Style.RESET_ALL}")
|
||||
# 如果没有配置,则返回None表示加载所有插件
|
||||
return None
|
||||
|
||||
def load_liugins(self):
|
||||
"""加载liugin"""
|
||||
self.tools = []
|
||||
# 获取启用的插件列表
|
||||
enabled_liugins = self.get_enabled_liugins()
|
||||
# 加载工具插件
|
||||
if os.path.exists(self.liugins_dir):
|
||||
for filename in os.listdir(self.liugins_dir):
|
||||
if filename.endswith('.py') and filename != '__init__.py':
|
||||
liugin_name = filename[:-3] # 移除.py扩展名
|
||||
# 如果有启用的插件列表,检查当前插件是否在其中
|
||||
if enabled_liugins is not None and liugin_name.lower() not in enabled_liugins:
|
||||
print(f"{Fore.YELLOW}跳过liugin: {liugin_name} (未启用){Style.RESET_ALL}")
|
||||
continue
|
||||
plugin_path = os.path.join(self.liugins_dir, filename)
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(liugin_name, plugin_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# 获取插件信息 - 兼容 Liugin 和 Plugin 两种类名
|
||||
plugin_class = getattr(module, 'Liugin', None) or getattr(module, 'Plugin', None)
|
||||
if plugin_class:
|
||||
plugin_instance = plugin_class()
|
||||
tool_info = plugin_instance.get_tool_info()
|
||||
tool_info['handler'] = plugin_instance.handle
|
||||
self.tools.append(tool_info)
|
||||
print(f"{Fore.GREEN}已加载liugin: {liugin_name}{Style.RESET_ALL}")
|
||||
except (ImportError, AttributeError, TypeError) as e:
|
||||
logger.error(f"加载liugin失败 {filename}: {e}")
|
||||
print(f"{Fore.RED}加载liugin失败 {filename}: {e}{Style.RESET_ALL}")
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"liugin文件未找到 {filename}: {e}")
|
||||
print(f"{Fore.RED}liugin文件未找到 {filename}: {e}{Style.RESET_ALL}")
|
||||
except Exception as e:
|
||||
logger.error(f"加载liugin时发生未知错误 {filename}: {e}")
|
||||
print(f"{Fore.RED}加载liugin失败 {filename}: {e}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}总共加载了 {len(self.tools)} 个工具插件{Style.RESET_ALL}")
|
||||
|
||||
def search_tools(self, query, limit=5):
|
||||
"""
|
||||
搜索工具,支持关键词匹配
|
||||
"""
|
||||
if not self.tools:
|
||||
return []
|
||||
|
||||
query_lower = query.lower()
|
||||
matches = []
|
||||
|
||||
for tool in self.tools:
|
||||
score = 0
|
||||
tool_name = tool.get('name', '').lower()
|
||||
tool_desc = tool.get('description', '').lower()
|
||||
tool_keywords = [kw.lower() for kw in tool.get('keywords', [])]
|
||||
|
||||
if query_lower == tool_name:
|
||||
score += 100
|
||||
if query_lower in tool_name:
|
||||
score += 50
|
||||
if query_lower in tool_desc:
|
||||
score += 30
|
||||
for keyword in tool_keywords:
|
||||
if query_lower in keyword:
|
||||
score += 20
|
||||
break
|
||||
|
||||
if score > 0:
|
||||
matches.append({
|
||||
'score': score,
|
||||
'name': tool.get('name', ''),
|
||||
'description': tool.get('description', ''),
|
||||
'usage': tool.get('usage', ''),
|
||||
'keywords': tool.get('keywords', [])
|
||||
})
|
||||
|
||||
matches.sort(key=lambda x: x['score'], reverse=True)
|
||||
return matches[:limit]
|
||||
|
||||
def get_tool_by_name(self, tool_name):
|
||||
"""根据名称获取工具"""
|
||||
for tool in self.tools:
|
||||
if tool.get('name', '').lower() == tool_name.lower():
|
||||
return tool
|
||||
return None
|
||||
Reference in new issue
Block a user