feat: 自动添加上下文

自动添加历史命令记录以及当前文件夹下面的内容为上下文
This commit is contained in:
wudongsheng 2025-08-23 20:25:42 +08:00
parent c8b615eecc
commit 9870926054
11 changed files with 333 additions and 29 deletions

View File

@ -9,6 +9,8 @@ AutoTerminal 是一个基于大语言模型的智能终端工具,可以将自
- ⚙️ 灵活的配置管理 - ⚙️ 灵活的配置管理
- 🌍 中文支持 - 🌍 中文支持
- 🔄 支持多种LLM模型 - 🔄 支持多种LLM模型
- 📚 命令历史记录和上下文感知
- 📁 当前目录内容上下文感知
## 安装 ## 安装
@ -50,6 +52,10 @@ sudo pip install .
配置信息会保存在 `config.json` 文件中。 配置信息会保存在 `config.json` 文件中。
### 配置选项
- `max_history`: 历史命令记录数量默认10
## 使用方法 ## 使用方法
### 方法1使用uv run ### 方法1使用uv run
@ -63,6 +69,11 @@ uv pip install -e .
at "查看当前目录下的所有文件" at "查看当前目录下的所有文件"
``` ```
### 使用历史命令上下文
```bash
at --history-count 5 "基于前面的命令,删除所有.txt文件"
```
程序会生成终端命令并显示提示,用户按回车后程序会直接执行该命令。 程序会生成终端命令并显示提示,用户按回车后程序会直接执行该命令。
## 示例 ## 示例
@ -92,6 +103,9 @@ autoterminal/
├── llm/ # LLM相关模块 ├── llm/ # LLM相关模块
│ ├── __init__.py # 包初始化文件 │ ├── __init__.py # 包初始化文件
│ └── client.py # LLM客户端 │ └── client.py # LLM客户端
├── history/ # 历史命令管理模块
│ ├── __init__.py # 包初始化文件
│ └── history.py # 历史命令管理器
├── utils/ # 工具函数 ├── utils/ # 工具函数
│ ├── __init__.py # 包初始化文件 │ ├── __init__.py # 包初始化文件
│ └── helpers.py # 辅助函数 │ └── helpers.py # 辅助函数

View File

@ -0,0 +1,5 @@
from . import config, llm, utils
from .history import HistoryManager
from .main import main
__all__ = ['config', 'llm', 'utils', 'HistoryManager', 'main']

View File

@ -1,6 +1,6 @@
import os import os
import json import json
from typing import Dict, Optional from typing import Dict, Optional, Any
class ConfigLoader: class ConfigLoader:
"""配置加载器,支持从文件加载配置""" """配置加载器,支持从文件加载配置"""

View File

@ -20,7 +20,8 @@ class ConfigManager:
self.default_config = { self.default_config = {
'base_url': 'https://api.openai.com/v1', 'base_url': 'https://api.openai.com/v1',
'model': 'gpt-4o', 'model': 'gpt-4o',
'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!' 'default_prompt': '你现在是一个终端助手,用户输入想要生成的命令,你来输出一个命令,不要任何多余的文本!',
'max_history': 10
} }
def save_config(self, config: Dict[str, Any]) -> bool: def save_config(self, config: Dict[str, Any]) -> bool:
@ -51,21 +52,40 @@ class ConfigManager:
config = self.default_config.copy() config = self.default_config.copy()
# 获取API密钥 # 获取API密钥
try:
api_key = input("请输入您的API密钥: ").strip() api_key = input("请输入您的API密钥: ").strip()
if not api_key: if not api_key:
print("错误: API密钥不能为空") print("错误: API密钥不能为空")
return {} return {}
config['api_key'] = api_key config['api_key'] = api_key
except EOFError:
print("\n配置向导已取消。")
return {}
except Exception as e:
print(f"错误: 无法读取API密钥输入: {e}")
return {}
# 获取Base URL # 获取Base URL
try:
base_url = input(f"请输入Base URL (默认: {self.default_config['base_url']}): ").strip() base_url = input(f"请输入Base URL (默认: {self.default_config['base_url']}): ").strip()
if base_url: if base_url:
config['base_url'] = base_url config['base_url'] = base_url
except EOFError:
print("\n配置向导已取消。")
return {}
except Exception as e:
print(f"警告: 无法读取Base URL输入: {e}")
# 获取模型名称 # 获取模型名称
try:
model = input(f"请输入模型名称 (默认: {self.default_config['model']}): ").strip() model = input(f"请输入模型名称 (默认: {self.default_config['model']}): ").strip()
if model: if model:
config['model'] = model config['model'] = model
except EOFError:
print("\n配置向导已取消。")
return {}
except Exception as e:
print(f"警告: 无法读取模型名称输入: {e}")
# 保存配置 # 保存配置
if self.save_config(config): if self.save_config(config):

73
autoterminal/history.py Normal file
View File

@ -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 {}

View File

@ -0,0 +1,4 @@
# History module initialization
from .history import HistoryManager
__all__ = ['HistoryManager']

View File

@ -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 {}

View File

@ -1,5 +1,6 @@
from openai import OpenAI from openai import OpenAI
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, List
import os
class LLMClient: class LLMClient:
"""LLM客户端封装OpenAI API调用""" """LLM客户端封装OpenAI API调用"""
@ -11,14 +12,43 @@ class LLMClient:
base_url=config.get('base_url') 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 user_input:
if not prompt: if not prompt:
prompt = self.config.get('default_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 = [ messages = [
{"role": "system", "content": prompt}, {"role": "system", "content": system_prompt},
{"role": "user", "content": user_input} {"role": "user", "content": user_content}
] ]
try: try:

View File

@ -4,11 +4,13 @@
import sys import sys
import os import os
import argparse import argparse
import glob
from autoterminal.config.loader import ConfigLoader from autoterminal.config.loader import ConfigLoader
from autoterminal.config.manager import ConfigManager from autoterminal.config.manager import ConfigManager
from autoterminal.llm.client import LLMClient from autoterminal.llm.client import LLMClient
from autoterminal.utils.helpers import clean_command from autoterminal.utils.helpers import clean_command
from autoterminal.history import HistoryManager
def main(): def main():
"""主程序入口""" """主程序入口"""
@ -18,6 +20,7 @@ def main():
parser.add_argument('--api-key', help='API密钥') parser.add_argument('--api-key', help='API密钥')
parser.add_argument('--base-url', help='Base URL') parser.add_argument('--base-url', help='Base URL')
parser.add_argument('--model', help='模型名称') parser.add_argument('--model', help='模型名称')
parser.add_argument('--history-count', type=int, help='历史命令数量')
args = parser.parse_args() args = parser.parse_args()
@ -36,6 +39,9 @@ def main():
if args.model: if args.model:
config['model'] = args.model config['model'] = args.model
# 获取历史命令数量配置
history_count = args.history_count or config.get('max_history', 10)
# 如果配置不完整,使用配置管理器初始化 # 如果配置不完整,使用配置管理器初始化
config_manager = ConfigManager() config_manager = ConfigManager()
if not all([config.get('api_key'), config.get('base_url'), config.get('model')]): if not all([config.get('api_key'), config.get('base_url'), config.get('model')]):
@ -43,8 +49,22 @@ def main():
if not config: if not config:
print("错误: 缺少必要的配置参数请通过命令行参数或配置文件提供API密钥、Base URL和模型名称。") print("错误: 缺少必要的配置参数请通过命令行参数或配置文件提供API密钥、Base URL和模型名称。")
return 1 return 1
# 如果有命令行参数输入,直接处理 # 如果有命令行参数输入,直接处理
if user_input: 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客户端 # 初始化LLM客户端
try: try:
llm_client = LLMClient(config) llm_client = LLMClient(config)
@ -54,7 +74,11 @@ def main():
# 调用LLM生成命令 # 调用LLM生成命令
try: 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) cleaned_command = clean_command(generated_command)
# 优化输出格式 # 优化输出格式
@ -62,18 +86,72 @@ def main():
print("\033[1;37mPress Enter to execute...\033[0m") print("\033[1;37mPress Enter to execute...\033[0m")
# 等待用户回车确认执行 # 等待用户回车确认执行
try:
input() input()
# 在用户的环境中执行命令 # 在用户的环境中执行命令
os.system(cleaned_command) 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: except Exception as e:
print(f"命令生成失败: {e}") print(f"命令生成失败: {e}")
return 1 return 1
return 0 return 0
else: else:
print("错误: 请提供要执行的命令,例如: python main.py \"查看当前目录\"") # 处理空输入情况 - 生成基于上下文的推荐命令
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 return 1
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -32,9 +32,9 @@ dependencies = [
] ]
[project.urls] [project.urls]
Homepage = "https://github.com/wds-dxh/autoterminal" Homepage = "http://cloud-home.dxh-wds.top:20100/w/autoterminal"
Repository = "https://github.com/wds-dxh/autoterminal" Repository = "http://cloud-home.dxh-wds.top:20100/w/autoterminal"
Issues = "https://github.com/wds-dxh/autoterminal/issues" Issues = "http://cloud-home.dxh-wds.top:20100/w/autoterminal/issues"
[project.scripts] [project.scripts]
at = "autoterminal.main:main" at = "autoterminal.main:main"

View File

@ -8,7 +8,7 @@ setup(
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
author="wds", author="wds",
author_email="wdsnpshy@163.com", author_email="wdsnpshy@163.com",
url="https://github.com/wds-dxh/autoterminal", url="http://cloud-home.dxh-wds.top:20100/w/AutoTerminal",
license="MIT", license="MIT",
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[