refactor: 重构现代化命令管理为基于配置文件的安全方案

🎯 解决问题:
- 避免推荐用户系统中不存在的工具
- 防止因缺失工具导致的命令执行失败
- 提供更安全、更灵活的现代化命令管理

🔧 主要改进:
- 新增 ai_shell/modern_commands.toml 配置文件
- 智能检测系统中已安装的现代化工具
- 只推荐实际可用的工具,安全回退到原始命令
- 完整的工具描述、分类和安装提示

📦 配置文件特性:
- 28 个命令映射配置
- 20 个工具描述说明
- 8 个工具分类组织
- 6 个详细安装提示

🛠️ 新增管理工具:
- scripts/manage_modern_commands.py 配置管理脚本
- 支持验证、列表、安装建议等功能
- 完整的配置状态检查和报告

🔍 用户体验优化:
- ai --config 显示详细的工具状态
- 区分已启用、保持原样、未安装的工具
- 提供具体的安装命令和说明
- 支持环境变量和配置文件自定义

🛡️ 安全保障:
- 绝不推荐不存在的工具
- 优雅降级到原始命令
- 保持完全向后兼容性

📋 技术实现:
- 添加 tomli 依赖支持 TOML 解析
- 重构配置加载逻辑
- 智能工具检测和状态管理
- 完善的错误处理和回退机制
This commit is contained in:
2025-07-12 22:44:31 +08:00
parent c4d1510ce9
commit afbbb1fbb0
7 changed files with 614 additions and 87 deletions

View File

@ -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("")

View File

@ -81,7 +81,8 @@ def create_parser() -> argparse.ArgumentParser:
def show_config() -> None:
"""Show current configuration"""
from .config import (get_api_key, get_base_url, get_model, get_timeout,
get_max_retries, validate_config, get_available_modern_commands)
get_max_retries, validate_config, get_available_modern_commands,
get_missing_modern_commands, get_installation_hint)
print("AI Shell Configuration:")
print(f" Model: {get_model()}")
@ -115,26 +116,40 @@ def show_config() -> None:
# Show modern commands configuration
modern_commands = get_available_modern_commands()
if modern_commands:
print(f"\n现代化命令替代 ({len(modern_commands)} 个可用):")
missing_commands = get_missing_modern_commands()
print(f"\n现代化命令替代:")
if modern_commands:
# Group commands for better display
active_alternatives = {k: v for k, v in modern_commands.items() if k != v}
unchanged_commands = {k: v for k, v in modern_commands.items() if k == v}
if active_alternatives:
print(" 已启用的替代:")
print(f" 已启用的替代 ({len(active_alternatives)} 个):")
for old_cmd, new_cmd in sorted(active_alternatives.items()):
print(f" {old_cmd}{new_cmd}")
if unchanged_commands:
print(f" 保持原样: {', '.join(sorted(unchanged_commands.keys()))}")
print(f" ➡️ 保持原样: {', '.join(sorted(unchanged_commands.keys()))}")
print("\n 自定义配置格式:")
print(" export AI_SHELL_MODERN_COMMANDS=\"old1:new1,old2:new2\"")
print(" 例如: export AI_SHELL_MODERN_COMMANDS=\"ls:exa,cat:bat,find:fd\"")
else:
print("\n现代化命令替代: 未检测到可用的现代化工具")
if missing_commands:
print(f"\n ⚠️ 可配置但未安装的工具 ({len(missing_commands)} 个):")
for old_cmd, new_cmd in sorted(missing_commands.items()):
hint = get_installation_hint(new_cmd)
if hint:
print(f" {old_cmd}{new_cmd}")
print(f" 安装: {hint.split('#')[0].strip()}")
else:
print(f" {old_cmd}{new_cmd}")
if not modern_commands and not missing_commands:
print(" 未检测到可用的现代化工具配置")
print("\n 配置方法:")
print(" 1. 编辑配置文件: ai_shell/modern_commands.toml")
print(" 2. 环境变量: export AI_SHELL_MODERN_COMMANDS=\"ls:eza,cat:bat\"")
print(" 3. .env 文件: AI_SHELL_MODERN_COMMANDS=\"ls:eza,cat:bat\"")
def main() -> None:
"""Main entry point"""

View File

@ -0,0 +1,98 @@
# AI Shell 现代化命令替代配置文件
# 只有在系统中实际安装了对应工具时,才会启用替代
[commands]
# 文件操作
ls = "eza" # 现代化的 ls支持图标和颜色
cat = "bat" # 带语法高亮的 cat
find = "fd" # 更快更友好的 find
tree = "tree" # 保持原样(已经很现代)
# 文本处理
grep = "rg" # ripgrep - 更快的文本搜索
sed = "sd" # 更简单的 sed 替代
awk = "choose" # 人性化的 awk 替代
cut = "choose" # cut 的替代
diff = "delta" # 带语法高亮的 diff
less = "bat" # 用 bat 进行分页显示
more = "bat" # 用 bat 进行分页显示
# 系统监控
ps = "procs" # 现代化的进程查看器
top = "htop" # 交互式进程监控
du = "ncdu" # 交互式磁盘使用分析
df = "duf" # 更好的磁盘空间显示
# 网络工具
ping = "gping" # 带图形的 ping
curl = "httpie" # 更友好的 HTTP 客户端
wget = "aria2c" # 多连接下载器
# 编辑器
vim = "nvim" # 现代化的 vim
nano = "micro" # 现代化的 nano 替代
# 导航和文件管理
cd = "zoxide" # 智能目录跳转(需要初始化)
rm = "trash" # 安全删除(移到回收站)
# 压缩工具
tar = "ouch" # 通用压缩工具
zip = "ouch" # 通用压缩工具
unzip = "ouch" # 通用压缩工具
# 开发工具
npm = "pnpm" # 更快的 npm 替代
yarn = "pnpm" # 更快的 yarn 替代
scp = "rsync" # 更高效的文件传输
# 保持原样的命令(不需要替代)
# git = "git"
# docker = "docker"
# python = "python"
# node = "node"
# ssh = "ssh"
# rsync = "rsync"
[descriptions]
# 工具描述,用于生成更好的 AI 提示
eza = "现代化的 ls 替代,支持图标、颜色和更好的格式化"
bat = "带语法高亮和行号的 cat 替代"
fd = "更快、更用户友好的 find 替代"
rg = "极快的文本搜索工具,比 grep 更快"
sd = "更简单直观的 sed 替代"
choose = "人性化的字段选择工具,可替代 awk 和 cut"
delta = "带语法高亮的 git diff 查看器"
procs = "现代化的进程查看器,比 ps 更直观"
htop = "交互式的系统监控工具"
ncdu = "交互式的磁盘使用分析工具"
duf = "更美观的磁盘空间显示工具"
gping = "带图形显示的 ping 工具"
httpie = "更友好的 HTTP 客户端"
aria2c = "支持多连接的下载工具"
nvim = "现代化的 vim 编辑器"
micro = "现代化的终端文本编辑器"
zoxide = "智能的目录跳转工具"
trash = "安全的文件删除工具(移到回收站)"
ouch = "通用的压缩/解压工具"
pnpm = "更快、更高效的 Node.js 包管理器"
[categories]
# 工具分类,用于更好的组织和显示
file_operations = ["ls", "cat", "find", "tree", "rm"]
text_processing = ["grep", "sed", "awk", "cut", "diff", "less", "more"]
system_monitoring = ["ps", "top", "du", "df"]
network_tools = ["ping", "curl", "wget"]
editors = ["vim", "nano"]
navigation = ["cd"]
compression = ["tar", "zip", "unzip"]
development = ["npm", "yarn", "scp"]
[installation_hints]
# 安装提示,当工具不存在时可以提供安装建议
eza = "brew install eza # macOS\nsudo apt install eza # Ubuntu\ncargo install eza # Rust"
bat = "brew install bat # macOS\nsudo apt install bat # Ubuntu\ncargo install bat # Rust"
fd = "brew install fd # macOS\nsudo apt install fd-find # Ubuntu\ncargo install fd-find # Rust"
rg = "brew install ripgrep # macOS\nsudo apt install ripgrep # Ubuntu\ncargo install ripgrep # Rust"
ncdu = "brew install ncdu # macOS\nsudo apt install ncdu # Ubuntu"
htop = "brew install htop # macOS\nsudo apt install htop # Ubuntu"