From 9870926054451b56dcdfee82cc5aa175662f8a4a Mon Sep 17 00:00:00 2001 From: wds-a5000 Date: Sat, 23 Aug 2025 20:25:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自动添加历史命令记录以及当前文件夹下面的内容为上下文 --- README.md | 14 +++++ autoterminal/__init__.py | 5 ++ autoterminal/config/loader.py | 2 +- autoterminal/config/manager.py | 42 +++++++++++---- autoterminal/history.py | 73 +++++++++++++++++++++++++ autoterminal/history/__init__.py | 4 ++ autoterminal/history/history.py | 80 +++++++++++++++++++++++++++ autoterminal/llm/client.py | 42 ++++++++++++--- autoterminal/main.py | 92 +++++++++++++++++++++++++++++--- pyproject.toml | 6 +-- setup.py | 2 +- 11 files changed, 333 insertions(+), 29 deletions(-) create mode 100644 autoterminal/history.py create mode 100644 autoterminal/history/__init__.py create mode 100644 autoterminal/history/history.py diff --git a/README.md b/README.md index 79241fe..0f9c17a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ AutoTerminal 是一个基于大语言模型的智能终端工具,可以将自 - ⚙️ 灵活的配置管理 - 🌍 中文支持 - 🔄 支持多种LLM模型 +- 📚 命令历史记录和上下文感知 +- 📁 当前目录内容上下文感知 ## 安装 @@ -50,6 +52,10 @@ sudo pip install . 配置信息会保存在 `config.json` 文件中。 +### 配置选项 + +- `max_history`: 历史命令记录数量(默认:10) + ## 使用方法 ### 方法1:使用uv run @@ -63,6 +69,11 @@ uv pip install -e . at "查看当前目录下的所有文件" ``` +### 使用历史命令上下文 +```bash +at --history-count 5 "基于前面的命令,删除所有.txt文件" +``` + 程序会生成终端命令并显示提示,用户按回车后程序会直接执行该命令。 ## 示例 @@ -92,6 +103,9 @@ autoterminal/ ├── llm/ # LLM相关模块 │ ├── __init__.py # 包初始化文件 │ └── client.py # LLM客户端 +├── history/ # 历史命令管理模块 +│ ├── __init__.py # 包初始化文件 +│ └── history.py # 历史命令管理器 ├── utils/ # 工具函数 │ ├── __init__.py # 包初始化文件 │ └── helpers.py # 辅助函数 diff --git a/autoterminal/__init__.py b/autoterminal/__init__.py index e69de29..232c36d 100644 --- a/autoterminal/__init__.py +++ b/autoterminal/__init__.py @@ -0,0 +1,5 @@ +from . import config, llm, utils +from .history import HistoryManager +from .main import main + +__all__ = ['config', 'llm', 'utils', 'HistoryManager', 'main'] \ No newline at end of file diff --git a/autoterminal/config/loader.py b/autoterminal/config/loader.py index 7c3f439..4fbfa35 100644 --- a/autoterminal/config/loader.py +++ b/autoterminal/config/loader.py @@ -1,6 +1,6 @@ import os import json -from typing import Dict, Optional +from typing import Dict, Optional, Any class ConfigLoader: """配置加载器,支持从文件加载配置""" diff --git a/autoterminal/config/manager.py b/autoterminal/config/manager.py index 60fdd41..54f191f 100644 --- a/autoterminal/config/manager.py +++ b/autoterminal/config/manager.py @@ -20,7 +20,8 @@ class ConfigManager: self.default_config = { 'base_url': 'https://api.openai.com/v1', 'model': 'gpt-4o', - 'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!' + 'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!', + 'max_history': 10 } def save_config(self, config: Dict[str, Any]) -> bool: @@ -51,21 +52,40 @@ class ConfigManager: config = self.default_config.copy() # 获取API密钥 - api_key = input("请输入您的API密钥: ").strip() - if not api_key: - print("错误: API密钥不能为空") + try: + api_key = input("请输入您的API密钥: ").strip() + if not api_key: + print("错误: API密钥不能为空") + return {} + config['api_key'] = api_key + except EOFError: + print("\n配置向导已取消。") + return {} + except Exception as e: + print(f"错误: 无法读取API密钥输入: {e}") return {} - config['api_key'] = api_key # 获取Base URL - base_url = input(f"请输入Base URL (默认: {self.default_config['base_url']}): ").strip() - if base_url: - config['base_url'] = base_url + try: + base_url = input(f"请输入Base URL (默认: {self.default_config['base_url']}): ").strip() + if base_url: + config['base_url'] = base_url + except EOFError: + print("\n配置向导已取消。") + return {} + except Exception as e: + print(f"警告: 无法读取Base URL输入: {e}") # 获取模型名称 - model = input(f"请输入模型名称 (默认: {self.default_config['model']}): ").strip() - if model: - config['model'] = model + try: + model = input(f"请输入模型名称 (默认: {self.default_config['model']}): ").strip() + if model: + config['model'] = model + except EOFError: + print("\n配置向导已取消。") + return {} + except Exception as e: + print(f"警告: 无法读取模型名称输入: {e}") # 保存配置 if self.save_config(config): diff --git a/autoterminal/history.py b/autoterminal/history.py new file mode 100644 index 0000000..ce5854a --- /dev/null +++ b/autoterminal/history.py @@ -0,0 +1,73 @@ +import os +import json +from typing import List, Dict, Any +from datetime import datetime + +class HistoryManager: + """历史命令管理器,用于记录和检索命令历史""" + + def __init__(self, history_file: str = None, max_history: int = 10): + if history_file is None: + # 将历史文件存储在用户主目录下的.autoterminal目录中 + home_dir = os.path.expanduser("~") + config_dir = os.path.join(home_dir, ".autoterminal") + self.history_file = os.path.join(config_dir, "history.json") + else: + self.history_file = history_file + + self.max_history = max_history + self.history = self.load_history() + + def load_history(self) -> List[Dict[str, Any]]: + """从历史文件加载命令历史""" + if os.path.exists(self.history_file): + try: + with open(self.history_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"警告: 无法读取历史文件 {self.history_file}: {e}") + return [] + + def save_history(self) -> bool: + """保存命令历史到文件""" + try: + # 确保目录存在 + os.makedirs(os.path.dirname(self.history_file) if os.path.dirname(self.history_file) else '.', exist_ok=True) + + with open(self.history_file, 'w', encoding='utf-8') as f: + json.dump(self.history, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + print(f"错误: 无法保存历史文件 {self.history_file}: {e}") + return False + + def add_command(self, user_input: str, generated_command: str, executed: bool = True) -> None: + """添加命令到历史记录""" + entry = { + "timestamp": datetime.now().isoformat(), + "user_input": user_input, + "generated_command": generated_command, + "executed": executed + } + + self.history.append(entry) + + # 保持历史记录在最大数量限制内 + if len(self.history) > self.max_history: + self.history = self.history[-self.max_history:] + + # 保存到文件 + self.save_history() + + def get_recent_history(self, count: int = None) -> List[Dict[str, Any]]: + """获取最近的命令历史""" + if count is None: + count = self.max_history + + return self.history[-count:] if self.history else [] + + def get_last_command(self) -> Dict[str, Any]: + """获取最后一条命令""" + if self.history: + return self.history[-1] + return {} \ No newline at end of file diff --git a/autoterminal/history/__init__.py b/autoterminal/history/__init__.py new file mode 100644 index 0000000..759fb41 --- /dev/null +++ b/autoterminal/history/__init__.py @@ -0,0 +1,4 @@ +# History module initialization +from .history import HistoryManager + +__all__ = ['HistoryManager'] \ No newline at end of file diff --git a/autoterminal/history/history.py b/autoterminal/history/history.py new file mode 100644 index 0000000..95cd118 --- /dev/null +++ b/autoterminal/history/history.py @@ -0,0 +1,80 @@ +import os +import json +from typing import List, Dict, Any +from datetime import datetime + +class HistoryManager: + """历史命令管理器,用于记录和检索命令历史""" + + def __init__(self, history_file: str = None, max_history: int = 10): + if history_file is None: + # 将历史文件存储在用户主目录下的.autoterminal目录中 + home_dir = os.path.expanduser("~") + config_dir = os.path.join(home_dir, ".autoterminal") + self.history_file = os.path.join(config_dir, "history.json") + else: + self.history_file = history_file + + self.max_history = max_history + self.history = self.load_history() + + def load_history(self) -> List[Dict[str, Any]]: + """从历史文件加载命令历史""" + if os.path.exists(self.history_file): + try: + with open(self.history_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"警告: 无法读取历史文件 {self.history_file}: {e}") + return [] + + def save_history(self) -> bool: + """保存命令历史到文件""" + try: + # 确保目录存在 + os.makedirs(os.path.dirname(self.history_file) if os.path.dirname(self.history_file) else '.', exist_ok=True) + + with open(self.history_file, 'w', encoding='utf-8') as f: + json.dump(self.history, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + print(f"错误: 无法保存历史文件 {self.history_file}: {e}") + return False + + def add_command(self, user_input: str, generated_command: str, executed: bool = True) -> None: + """添加命令到历史记录""" + entry = { + "timestamp": datetime.now().isoformat(), + "user_input": user_input, + "generated_command": generated_command, + "executed": executed + } + + self.history.append(entry) + + # 保持历史记录在最大数量限制内 + if len(self.history) > self.max_history: + self.history = self.history[-self.max_history:] + + # 保存到文件 + self.save_history() + + def get_last_executed_command(self) -> str: + """获取最后一条已执行的命令""" + for entry in reversed(self.history): + if entry.get("executed", False): + return entry.get("generated_command", "") + return "" + + def get_recent_history(self, count: int = None) -> List[Dict[str, Any]]: + """获取最近的命令历史""" + if count is None: + count = self.max_history + + return self.history[-count:] if self.history else [] + + def get_last_command(self) -> Dict[str, Any]: + """获取最后一条命令""" + if self.history: + return self.history[-1] + return {} \ No newline at end of file diff --git a/autoterminal/llm/client.py b/autoterminal/llm/client.py index 8208127..22353fa 100644 --- a/autoterminal/llm/client.py +++ b/autoterminal/llm/client.py @@ -1,5 +1,6 @@ from openai import OpenAI -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List +import os class LLMClient: """LLM客户端,封装OpenAI API调用""" @@ -11,14 +12,43 @@ class LLMClient: base_url=config.get('base_url') ) - def generate_command(self, user_input: str, prompt: Optional[str] = None) -> str: + def generate_command(self, user_input: str, prompt: Optional[str] = None, + history: Optional[List[Dict[str, Any]]] = None, + current_dir_content: Optional[List[str]] = None, + last_executed_command: str = "") -> str: """根据用户输入生成命令""" - if not prompt: - prompt = self.config.get('default_prompt', '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!') + # 根据用户输入是否为空选择不同的提示词 + if not user_input: + if not prompt: + prompt = self.config.get('recommendation_prompt', '你现在是一个终端助手,根据上下文自动推荐命令:当用户没有输入时,基于最近执行的命令历史和当前目录内容,智能推荐最可能需要的终端命令(仅当有明确上下文线索时);当用户输入命令需求时,生成对应命令。仅输出纯命令文本,不要任何解释或多余内容!') + else: + if not prompt: + prompt = self.config.get('default_prompt', '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!') + + # 构建系统提示,包含上下文信息 + system_prompt = prompt + + # 添加历史命令上下文 + if history: + history_context = "\n最近执行的命令历史:\n" + for i, entry in enumerate(reversed(history), 1): + history_context += f"{i}. 用户输入: {entry.get('user_input', '')} -> 生成命令: {entry.get('generated_command', '')}\n" + system_prompt += history_context + + # 添加当前目录内容上下文 + if current_dir_content: + dir_context = "\n当前目录下的文件和文件夹:\n" + "\n".join(current_dir_content) + system_prompt += dir_context + + # 当用户输入为空时,使用特殊的提示来触发推荐模式 + if not user_input: + user_content = f"根据提供的上下文信息,推荐一个最可能需要的终端命令(仅当有明确的上下文线索时)。如果上下文信息不足以确定一个有用的命令,则返回空。请直接返回一个可执行的终端命令,不要包含任何解释或其他文本。例如:ls -la 或 git status。特别注意:不要使用echo命令来列出文件,应该使用ls命令。推荐命令时请考虑最近执行的命令历史,避免重复推荐相同的命令。最后执行的命令是: {last_executed_command}。如果当前目录有pyproject.toml或setup.py文件,可以考虑使用pip list查看已安装的包。" + else: + user_content = user_input messages = [ - {"role": "system", "content": prompt}, - {"role": "user", "content": user_input} + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_content} ] try: diff --git a/autoterminal/main.py b/autoterminal/main.py index 332cb7e..0b78258 100644 --- a/autoterminal/main.py +++ b/autoterminal/main.py @@ -4,11 +4,13 @@ import sys import os import argparse +import glob from autoterminal.config.loader import ConfigLoader from autoterminal.config.manager import ConfigManager from autoterminal.llm.client import LLMClient from autoterminal.utils.helpers import clean_command +from autoterminal.history import HistoryManager def main(): """主程序入口""" @@ -18,6 +20,7 @@ def main(): parser.add_argument('--api-key', help='API密钥') parser.add_argument('--base-url', help='Base URL') parser.add_argument('--model', help='模型名称') + parser.add_argument('--history-count', type=int, help='历史命令数量') args = parser.parse_args() @@ -36,6 +39,9 @@ def main(): if args.model: config['model'] = args.model + # 获取历史命令数量配置 + history_count = args.history_count or config.get('max_history', 10) + # 如果配置不完整,使用配置管理器初始化 config_manager = ConfigManager() if not all([config.get('api_key'), config.get('base_url'), config.get('model')]): @@ -43,8 +49,22 @@ def main(): if not config: print("错误: 缺少必要的配置参数,请通过命令行参数或配置文件提供API密钥、Base URL和模型名称。") return 1 + # 如果有命令行参数输入,直接处理 if user_input: + # 初始化历史管理器 + history_manager = HistoryManager(max_history=history_count) + + # 获取历史命令 + history = history_manager.get_recent_history(history_count) + + # 获取当前目录内容 + try: + current_dir_content = glob.glob("*") + except Exception as e: + print(f"警告: 无法获取当前目录内容: {e}") + current_dir_content = [] + # 初始化LLM客户端 try: llm_client = LLMClient(config) @@ -54,7 +74,11 @@ def main(): # 调用LLM生成命令 try: - generated_command = llm_client.generate_command(user_input) + generated_command = llm_client.generate_command( + user_input=user_input, + history=history, + current_dir_content=current_dir_content + ) cleaned_command = clean_command(generated_command) # 优化输出格式 @@ -62,10 +86,20 @@ def main(): print("\033[1;37mPress Enter to execute...\033[0m") # 等待用户回车确认执行 - input() - - # 在用户的环境中执行命令 - os.system(cleaned_command) + try: + input() + + # 在用户的环境中执行命令 + os.system(cleaned_command) + + # 记录到历史 + history_manager.add_command(user_input, cleaned_command) + except EOFError: + print("\n输入已取消。") + return 0 + except Exception as exec_e: + print(f"命令执行失败: {exec_e}") + return 1 except Exception as e: print(f"命令生成失败: {e}") @@ -73,8 +107,52 @@ def main(): return 0 else: - print("错误: 请提供要执行的命令,例如: python main.py \"查看当前目录\"") - return 1 + # 处理空输入情况 - 生成基于上下文的推荐命令 + history_manager = HistoryManager(max_history=history_count) + history = history_manager.get_recent_history(history_count) + + try: + current_dir_content = glob.glob("*") + except Exception as e: + print(f"警告: 无法获取当前目录内容: {e}") + current_dir_content = [] + + try: + llm_client = LLMClient(config) + except Exception as e: + print(f"LLM客户端初始化失败: {e}") + return 1 + + # 获取最后执行的命令以避免重复推荐 + last_executed_command = history_manager.get_last_executed_command() + + try: + recommendation = llm_client.generate_command( + user_input="", + history=history, + current_dir_content=current_dir_content, + last_executed_command=last_executed_command + ) + cleaned_recommendation = clean_command(recommendation) + + if cleaned_recommendation.strip(): + print(f"\033[1;34m💡 建议命令:\033[0m {cleaned_recommendation}") + print("\033[1;37mPress Enter to execute, or Ctrl+C to cancel...\033[0m") + try: + input() + os.system(cleaned_recommendation) + history_manager.add_command("自动推荐", cleaned_recommendation) + except EOFError: + print("\n输入已取消。") + return 0 + except Exception as exec_e: + print(f"命令执行失败: {exec_e}") + return 1 + else: + print("没有找到相关的命令建议。") + except Exception as e: + print(f"命令推荐生成失败: {e}") + return 1 if __name__ == "__main__": sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index dd8497c..70d90b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,9 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/wds-dxh/autoterminal" -Repository = "https://github.com/wds-dxh/autoterminal" -Issues = "https://github.com/wds-dxh/autoterminal/issues" +Homepage = "http://cloud-home.dxh-wds.top:20100/w/autoterminal" +Repository = "http://cloud-home.dxh-wds.top:20100/w/autoterminal" +Issues = "http://cloud-home.dxh-wds.top:20100/w/autoterminal/issues" [project.scripts] at = "autoterminal.main:main" diff --git a/setup.py b/setup.py index 0349a89..bc507b0 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( long_description_content_type="text/markdown", author="wds", author_email="wdsnpshy@163.com", - url="https://github.com/wds-dxh/autoterminal", + url="http://cloud-home.dxh-wds.top:20100/w/AutoTerminal", license="MIT", packages=find_packages(), install_requires=[