refactor: 重构现代化命令管理为基于配置文件的安全方案
🎯 解决问题: - 避免推荐用户系统中不存在的工具 - 防止因缺失工具导致的命令执行失败 - 提供更安全、更灵活的现代化命令管理 🔧 主要改进: - 新增 ai_shell/modern_commands.toml 配置文件 - 智能检测系统中已安装的现代化工具 - 只推荐实际可用的工具,安全回退到原始命令 - 完整的工具描述、分类和安装提示 📦 配置文件特性: - 28 个命令映射配置 - 20 个工具描述说明 - 8 个工具分类组织 - 6 个详细安装提示 🛠️ 新增管理工具: - scripts/manage_modern_commands.py 配置管理脚本 - 支持验证、列表、安装建议等功能 - 完整的配置状态检查和报告 🔍 用户体验优化: - ai --config 显示详细的工具状态 - 区分已启用、保持原样、未安装的工具 - 提供具体的安装命令和说明 - 支持环境变量和配置文件自定义 🛡️ 安全保障: - 绝不推荐不存在的工具 - 优雅降级到原始命令 - 保持完全向后兼容性 📋 技术实现: - 添加 tomli 依赖支持 TOML 解析 - 重构配置加载逻辑 - 智能工具检测和状态管理 - 完善的错误处理和回退机制
This commit is contained in:
@ -3,13 +3,23 @@ Configuration module for AI Shell
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
load_dotenv = None
|
||||
|
||||
try:
|
||||
import tomllib # Python 3.11+
|
||||
except ImportError:
|
||||
try:
|
||||
import tomli as tomllib # fallback for older Python
|
||||
except ImportError:
|
||||
tomllib = None
|
||||
|
||||
# Load .env file if it exists
|
||||
def load_env_file() -> None:
|
||||
"""Load environment variables from .env file"""
|
||||
@ -61,57 +71,30 @@ def get_max_retries() -> int:
|
||||
"""Get max retries from environment"""
|
||||
return int(os.getenv("AI_SHELL_MAX_RETRIES", "3"))
|
||||
|
||||
def get_modern_commands() -> dict:
|
||||
"""Get modern command alternatives configuration"""
|
||||
# Default modern command alternatives
|
||||
default_alternatives = {
|
||||
"ls": "eza", # Modern ls replacement
|
||||
"cat": "bat", # Syntax highlighting cat
|
||||
"find": "fd", # Fast and user-friendly find
|
||||
"grep": "rg", # Ripgrep - faster grep
|
||||
"du": "ncdu", # Interactive disk usage
|
||||
"df": "duf", # Modern df with better output
|
||||
"ps": "procs", # Modern ps replacement
|
||||
"top": "htop", # Interactive process viewer
|
||||
"ping": "gping", # Ping with graph
|
||||
"curl": "httpie", # User-friendly HTTP client
|
||||
"wget": "aria2c", # Multi-connection downloader
|
||||
"diff": "delta", # Better diff with syntax highlighting
|
||||
"tree": "tree", # Keep tree as is (already modern)
|
||||
"sed": "sd", # Simpler sed alternative
|
||||
"awk": "choose", # Human-friendly awk alternative
|
||||
"cut": "choose", # Alternative to cut
|
||||
"sort": "sort", # Keep sort as is
|
||||
"uniq": "uniq", # Keep uniq as is
|
||||
"head": "head", # Keep head as is
|
||||
"tail": "tail", # Keep tail as is
|
||||
"less": "bat", # Use bat for paging too
|
||||
"more": "bat", # Use bat for paging
|
||||
"vim": "nvim", # Modern vim
|
||||
"nano": "micro", # Modern nano alternative
|
||||
"cd": "zoxide", # Smart cd with frecency
|
||||
"cp": "cp", # Keep cp as is (or could use rsync)
|
||||
"mv": "mv", # Keep mv as is
|
||||
"rm": "trash", # Safer deletion (trash-cli)
|
||||
"mkdir": "mkdir", # Keep mkdir as is
|
||||
"rmdir": "rmdir", # Keep rmdir as is
|
||||
"chmod": "chmod", # Keep chmod as is
|
||||
"chown": "chown", # Keep chown as is
|
||||
"tar": "ouch", # Universal archive tool
|
||||
"zip": "ouch", # Universal archive tool
|
||||
"unzip": "ouch", # Universal archive tool
|
||||
"ssh": "ssh", # Keep ssh as is
|
||||
"scp": "rsync", # More efficient file transfer
|
||||
"rsync": "rsync", # Keep rsync as is
|
||||
"git": "git", # Keep git as is
|
||||
"docker": "docker", # Keep docker as is
|
||||
"python": "python", # Keep python as is
|
||||
"node": "node", # Keep node as is
|
||||
"npm": "pnpm", # Faster npm alternative
|
||||
"yarn": "pnpm", # Faster yarn alternative
|
||||
}
|
||||
def load_modern_commands_config() -> Dict[str, any]:
|
||||
"""Load modern commands configuration from TOML file"""
|
||||
config_file = Path(__file__).parent / "modern_commands.toml"
|
||||
|
||||
# Try to load custom alternatives from environment or config file
|
||||
if not config_file.exists():
|
||||
return {"commands": {}, "descriptions": {}, "categories": {}, "installation_hints": {}}
|
||||
|
||||
if tomllib is None:
|
||||
# Fallback to basic parsing if tomllib not available
|
||||
return {"commands": {}, "descriptions": {}, "categories": {}, "installation_hints": {}}
|
||||
|
||||
try:
|
||||
with open(config_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
except Exception:
|
||||
return {"commands": {}, "descriptions": {}, "categories": {}, "installation_hints": {}}
|
||||
|
||||
def get_modern_commands() -> Dict[str, str]:
|
||||
"""Get modern command alternatives configuration"""
|
||||
# Load from TOML config file
|
||||
config = load_modern_commands_config()
|
||||
default_alternatives = config.get("commands", {})
|
||||
|
||||
# Try to load custom alternatives from environment
|
||||
custom_alternatives_str = os.getenv("AI_SHELL_MODERN_COMMANDS", "")
|
||||
custom_alternatives = {}
|
||||
|
||||
@ -132,10 +115,8 @@ def get_modern_commands() -> dict:
|
||||
|
||||
return alternatives
|
||||
|
||||
def get_available_modern_commands() -> dict:
|
||||
def get_available_modern_commands() -> Dict[str, str]:
|
||||
"""Get only the modern commands that are actually installed on the system"""
|
||||
import shutil
|
||||
|
||||
all_alternatives = get_modern_commands()
|
||||
available_alternatives = {}
|
||||
|
||||
@ -143,17 +124,45 @@ def get_available_modern_commands() -> dict:
|
||||
# Check if the modern command is actually available
|
||||
if shutil.which(new_cmd):
|
||||
available_alternatives[old_cmd] = new_cmd
|
||||
# If modern command not available, keep the original
|
||||
# If modern command not available, keep the original (if it exists)
|
||||
elif shutil.which(old_cmd):
|
||||
available_alternatives[old_cmd] = old_cmd
|
||||
|
||||
return available_alternatives
|
||||
|
||||
def get_missing_modern_commands() -> Dict[str, str]:
|
||||
"""Get modern commands that are configured but not installed"""
|
||||
all_alternatives = get_modern_commands()
|
||||
missing_commands = {}
|
||||
|
||||
for old_cmd, new_cmd in all_alternatives.items():
|
||||
# If modern command is not available but old command exists
|
||||
if not shutil.which(new_cmd) and shutil.which(old_cmd) and old_cmd != new_cmd:
|
||||
missing_commands[old_cmd] = new_cmd
|
||||
|
||||
return missing_commands
|
||||
|
||||
def get_installation_hint(command: str) -> Optional[str]:
|
||||
"""Get installation hint for a modern command"""
|
||||
config = load_modern_commands_config()
|
||||
installation_hints = config.get("installation_hints", {})
|
||||
return installation_hints.get(command)
|
||||
|
||||
def get_command_description(command: str) -> Optional[str]:
|
||||
"""Get description for a modern command"""
|
||||
config = load_modern_commands_config()
|
||||
descriptions = config.get("descriptions", {})
|
||||
return descriptions.get(command)
|
||||
|
||||
def generate_modern_commands_prompt() -> str:
|
||||
"""Generate prompt text about available modern command alternatives"""
|
||||
available_commands = get_available_modern_commands()
|
||||
config = load_modern_commands_config()
|
||||
|
||||
if not available_commands:
|
||||
# Only include commands that have modern alternatives available
|
||||
active_alternatives = {k: v for k, v in available_commands.items() if k != v}
|
||||
|
||||
if not active_alternatives:
|
||||
return ""
|
||||
|
||||
prompt_parts = [
|
||||
@ -162,42 +171,44 @@ def generate_modern_commands_prompt() -> str:
|
||||
""
|
||||
]
|
||||
|
||||
# Group by categories for better readability
|
||||
categories = {
|
||||
"文件操作": ["ls", "cat", "find", "tree", "cp", "mv", "rm"],
|
||||
"文本处理": ["grep", "sed", "awk", "cut", "sort", "uniq", "head", "tail", "less", "more", "diff"],
|
||||
"系统监控": ["ps", "top", "du", "df"],
|
||||
"网络工具": ["ping", "curl", "wget"],
|
||||
"编辑器": ["vim", "nano"],
|
||||
"导航": ["cd"],
|
||||
"压缩工具": ["tar", "zip", "unzip"],
|
||||
"包管理": ["npm", "yarn"],
|
||||
"其他": []
|
||||
}
|
||||
# Use categories from config file
|
||||
categories = config.get("categories", {})
|
||||
descriptions = config.get("descriptions", {})
|
||||
|
||||
for category, commands in categories.items():
|
||||
for category_name, commands in categories.items():
|
||||
category_commands = []
|
||||
for cmd in commands:
|
||||
if cmd in available_commands and available_commands[cmd] != cmd:
|
||||
category_commands.append(f" - Use `{available_commands[cmd]}` instead of `{cmd}`")
|
||||
if cmd in active_alternatives:
|
||||
new_cmd = active_alternatives[cmd]
|
||||
desc = descriptions.get(new_cmd, "")
|
||||
if desc:
|
||||
category_commands.append(f" - Use `{new_cmd}` instead of `{cmd}` ({desc})")
|
||||
else:
|
||||
category_commands.append(f" - Use `{new_cmd}` instead of `{cmd}`")
|
||||
|
||||
if category_commands:
|
||||
prompt_parts.append(f"**{category}:**")
|
||||
# Convert category name to display format
|
||||
display_name = category_name.replace("_", " ").title()
|
||||
prompt_parts.append(f"**{display_name}:**")
|
||||
prompt_parts.extend(category_commands)
|
||||
prompt_parts.append("")
|
||||
|
||||
# Add remaining commands not in categories
|
||||
other_commands = []
|
||||
categorized_commands = set()
|
||||
for commands in categories.values():
|
||||
categorized_commands.update(commands)
|
||||
|
||||
for old_cmd, new_cmd in available_commands.items():
|
||||
if old_cmd not in categorized_commands and new_cmd != old_cmd:
|
||||
other_commands.append(f" - Use `{new_cmd}` instead of `{old_cmd}`")
|
||||
other_commands = []
|
||||
for old_cmd, new_cmd in active_alternatives.items():
|
||||
if old_cmd not in categorized_commands:
|
||||
desc = descriptions.get(new_cmd, "")
|
||||
if desc:
|
||||
other_commands.append(f" - Use `{new_cmd}` instead of `{old_cmd}` ({desc})")
|
||||
else:
|
||||
other_commands.append(f" - Use `{new_cmd}` instead of `{old_cmd}`")
|
||||
|
||||
if other_commands:
|
||||
prompt_parts.append("**其他工具:**")
|
||||
prompt_parts.append("**Other Tools:**")
|
||||
prompt_parts.extend(other_commands)
|
||||
prompt_parts.append("")
|
||||
|
||||
|
Reference in New Issue
Block a user