aibash-wx 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aibash_wx-0.1.0/aibash_wx.egg-info → aibash_wx-0.2.0}/PKG-INFO +57 -2
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/README.md +56 -1
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/ollama_agent.py +6 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/openai_agent.py +5 -0
- aibash_wx-0.2.0/aibash/automation.py +439 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/config.py +46 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/config_init.py +35 -26
- aibash_wx-0.2.0/aibash/i18n.py +389 -0
- aibash_wx-0.2.0/aibash/interactive.py +267 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/interfaces/ai_agent.py +1 -0
- aibash_wx-0.2.0/aibash/main.py +557 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/prompt.py +71 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0/aibash_wx.egg-info}/PKG-INFO +57 -2
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/SOURCES.txt +4 -1
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/top_level.txt +1 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/pyproject.toml +1 -1
- aibash_wx-0.2.0/venv/bin/activate_this.py +32 -0
- aibash_wx-0.1.0/aibash/interactive.py +0 -185
- aibash_wx-0.1.0/aibash/main.py +0 -323
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/LICENSE +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/__init__.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/__init__.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/agent_builder.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/ai_client.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/history.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/interfaces/__init__.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/resources/__init__.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/__init__.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/clipboard.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/keyboard.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/terminal.py +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/dependency_links.txt +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/entry_points.txt +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/requires.txt +0 -0
- {aibash_wx-0.1.0 → aibash_wx-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aibash-wx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AI-powered shell command generator
|
|
5
5
|
Author: github/W1412X
|
|
6
6
|
License: MIT
|
|
@@ -44,6 +44,9 @@ AIBash 是一个智能命令行工具,能够根据自然语言描述生成对
|
|
|
44
44
|
- ⚙️ **灵活配置**: 支持多种配置选项(模型、密钥、系统信息等)
|
|
45
45
|
- 🌐 **多平台支持**: 支持 macOS、Windows、Linux
|
|
46
46
|
- 🔌 **多模型支持**: 支持 OpenAI API 和 Ollama 本地模型
|
|
47
|
+
- 🧠 **自动化任务**: 通过 `-a` 自动规划多步命令,逐步执行完成复杂任务
|
|
48
|
+
- 🪟 **新终端执行**: 通过 `-new` 在新的终端窗口中运行命令,保持当前窗口清爽
|
|
49
|
+
- 🌏 **双语界面**: 命令行提示支持英文/中文两种语言,可随时切换
|
|
47
50
|
|
|
48
51
|
## 安装
|
|
49
52
|
|
|
@@ -58,7 +61,7 @@ pip install -e .
|
|
|
58
61
|
### 使用 pip 安装
|
|
59
62
|
|
|
60
63
|
```bash
|
|
61
|
-
pip install aibash
|
|
64
|
+
pip install aibash-wx
|
|
62
65
|
```
|
|
63
66
|
|
|
64
67
|
## 快速开始
|
|
@@ -112,6 +115,15 @@ aibash -l "列出当前目录下的所有文件"
|
|
|
112
115
|
# 指定配置文件
|
|
113
116
|
aibash --config /path/to/config.yaml -l "查找包含test的文件"
|
|
114
117
|
|
|
118
|
+
# 自动模式示例(逐步完成任务并逐条确认)
|
|
119
|
+
aibash -a "查看当前项目依赖并生成 requirements.txt"
|
|
120
|
+
|
|
121
|
+
# 自动模式从文件读取任务描述
|
|
122
|
+
aibash -a @/path/to/task_description.txt
|
|
123
|
+
|
|
124
|
+
# 在新的终端窗口中执行命令
|
|
125
|
+
aibash -new -l "运行当前目录下的测试用例"
|
|
126
|
+
|
|
115
127
|
# 查看帮助
|
|
116
128
|
aibash -h
|
|
117
129
|
|
|
@@ -131,7 +143,16 @@ aibash --test
|
|
|
131
143
|
## 命令行选项
|
|
132
144
|
|
|
133
145
|
- `-l, --lang QUERY`: 自然语言描述,用于生成 shell 命令
|
|
146
|
+
- `-a, --auto QUERY`: 自动模式,根据自然语言目标规划并执行多步操作,每一步执行前都会请求确认
|
|
147
|
+
- `-p, --plan-file PATH`: 指定文件作为自动模式的任务描述输入
|
|
148
|
+
- `--auto-approve-all`: 自动模式下自动批准所有动作(无确认)
|
|
149
|
+
- `--auto-approve-commands`: 自动模式下自动批准命令执行
|
|
150
|
+
- `--auto-approve-files`: 自动模式下自动批准文件读取
|
|
151
|
+
- `--auto-approve-web`: 自动模式下自动批准网络请求
|
|
152
|
+
- `--auto-max-steps N`: 自动模式下限制最多执行的步骤数量(默认 20)
|
|
153
|
+
- `--ui-language {en,zh}`: 临时切换界面语言(默认从配置读取,未设置时为英文)
|
|
134
154
|
- `--config PATH`: 指定配置文件路径(默认: ~/.aibash/config.yaml)
|
|
155
|
+
- `-new, --new-terminal`: 将命令在新的终端窗口中执行(未指定时默认在当前终端执行)
|
|
135
156
|
- `--init`: 交互式初始化配置文件
|
|
136
157
|
- `--history`: 查看命令执行历史
|
|
137
158
|
- `--clear-history`: 清空命令执行历史
|
|
@@ -147,6 +168,22 @@ aibash --test
|
|
|
147
168
|
- `[s]` 跳过/放弃 - 不执行命令
|
|
148
169
|
- `[h]` 显示帮助 - 显示帮助信息
|
|
149
170
|
|
|
171
|
+
当命令执行失败时,AIBash 会自动根据最新历史记录和错误输出请求 AI 生成新的命令建议,连同一条中文提示一起展示,帮助你快速迭代。
|
|
172
|
+
|
|
173
|
+
### 自动模式(-a)
|
|
174
|
+
|
|
175
|
+
自动模式会让 AIBash 充当一个“执行代理”,根据你的自然语言描述分步规划并完成任务:
|
|
176
|
+
|
|
177
|
+
- 模型每次只会规划一个动作(运行命令、读取文件、访问网络、向你提问或结束)
|
|
178
|
+
- 每个命令、文件读取或网络访问都会先询问你是否确认执行
|
|
179
|
+
- 执行结果会反馈给模型,帮助其决定下一步操作
|
|
180
|
+
- 适合需要多步协作的任务,如“拉取最新代码、安装依赖并运行测试”等
|
|
181
|
+
- 可使用 `--auto-approve-*` 参数细粒度控制哪些操作无需确认,并可通过 `--auto-max-steps` 限制最多执行的步骤数
|
|
182
|
+
- 如某一步执行失败,自动模式会向模型反馈错误详情,并自动重新规划新的命令或策略
|
|
183
|
+
- 自动模式支持从配置文件预设是否自动确认命令/读文件/访问网络等行为
|
|
184
|
+
|
|
185
|
+
可以与 `-new` 搭配,在新的终端窗口中执行实际命令,确保自动模式界面保持整洁。
|
|
186
|
+
|
|
150
187
|
## 配置说明
|
|
151
188
|
|
|
152
189
|
### 模型配置
|
|
@@ -168,6 +205,24 @@ aibash --test
|
|
|
168
205
|
- `system_info`: 系统信息(用于生成更准确的命令)
|
|
169
206
|
- `custom_prompt`: 自定义 prompt 模板
|
|
170
207
|
- `use_default_prompt`: 是否使用默认 prompt
|
|
208
|
+
- `ui.language`: 界面语言(`en` 或 `zh`,默认 `en`)
|
|
209
|
+
- `automation`: 自动模式默认行为配置
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
automation:
|
|
213
|
+
auto_confirm_all: false
|
|
214
|
+
auto_confirm_commands: false
|
|
215
|
+
auto_confirm_files: false
|
|
216
|
+
auto_confirm_web: false
|
|
217
|
+
max_steps: 20
|
|
218
|
+
|
|
219
|
+
ui:
|
|
220
|
+
enable_colors: true
|
|
221
|
+
single_key_mode: true
|
|
222
|
+
language: en
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
命令行中的 `--ui-language` 仅对当前会话生效,如需长期使用请在配置文件的 `ui.language` 中设置。
|
|
171
226
|
|
|
172
227
|
## 自定义 Prompt
|
|
173
228
|
|
|
@@ -16,6 +16,9 @@ AIBash 是一个智能命令行工具,能够根据自然语言描述生成对
|
|
|
16
16
|
- ⚙️ **灵活配置**: 支持多种配置选项(模型、密钥、系统信息等)
|
|
17
17
|
- 🌐 **多平台支持**: 支持 macOS、Windows、Linux
|
|
18
18
|
- 🔌 **多模型支持**: 支持 OpenAI API 和 Ollama 本地模型
|
|
19
|
+
- 🧠 **自动化任务**: 通过 `-a` 自动规划多步命令,逐步执行完成复杂任务
|
|
20
|
+
- 🪟 **新终端执行**: 通过 `-new` 在新的终端窗口中运行命令,保持当前窗口清爽
|
|
21
|
+
- 🌏 **双语界面**: 命令行提示支持英文/中文两种语言,可随时切换
|
|
19
22
|
|
|
20
23
|
## 安装
|
|
21
24
|
|
|
@@ -30,7 +33,7 @@ pip install -e .
|
|
|
30
33
|
### 使用 pip 安装
|
|
31
34
|
|
|
32
35
|
```bash
|
|
33
|
-
pip install aibash
|
|
36
|
+
pip install aibash-wx
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
## 快速开始
|
|
@@ -84,6 +87,15 @@ aibash -l "列出当前目录下的所有文件"
|
|
|
84
87
|
# 指定配置文件
|
|
85
88
|
aibash --config /path/to/config.yaml -l "查找包含test的文件"
|
|
86
89
|
|
|
90
|
+
# 自动模式示例(逐步完成任务并逐条确认)
|
|
91
|
+
aibash -a "查看当前项目依赖并生成 requirements.txt"
|
|
92
|
+
|
|
93
|
+
# 自动模式从文件读取任务描述
|
|
94
|
+
aibash -a @/path/to/task_description.txt
|
|
95
|
+
|
|
96
|
+
# 在新的终端窗口中执行命令
|
|
97
|
+
aibash -new -l "运行当前目录下的测试用例"
|
|
98
|
+
|
|
87
99
|
# 查看帮助
|
|
88
100
|
aibash -h
|
|
89
101
|
|
|
@@ -103,7 +115,16 @@ aibash --test
|
|
|
103
115
|
## 命令行选项
|
|
104
116
|
|
|
105
117
|
- `-l, --lang QUERY`: 自然语言描述,用于生成 shell 命令
|
|
118
|
+
- `-a, --auto QUERY`: 自动模式,根据自然语言目标规划并执行多步操作,每一步执行前都会请求确认
|
|
119
|
+
- `-p, --plan-file PATH`: 指定文件作为自动模式的任务描述输入
|
|
120
|
+
- `--auto-approve-all`: 自动模式下自动批准所有动作(无确认)
|
|
121
|
+
- `--auto-approve-commands`: 自动模式下自动批准命令执行
|
|
122
|
+
- `--auto-approve-files`: 自动模式下自动批准文件读取
|
|
123
|
+
- `--auto-approve-web`: 自动模式下自动批准网络请求
|
|
124
|
+
- `--auto-max-steps N`: 自动模式下限制最多执行的步骤数量(默认 20)
|
|
125
|
+
- `--ui-language {en,zh}`: 临时切换界面语言(默认从配置读取,未设置时为英文)
|
|
106
126
|
- `--config PATH`: 指定配置文件路径(默认: ~/.aibash/config.yaml)
|
|
127
|
+
- `-new, --new-terminal`: 将命令在新的终端窗口中执行(未指定时默认在当前终端执行)
|
|
107
128
|
- `--init`: 交互式初始化配置文件
|
|
108
129
|
- `--history`: 查看命令执行历史
|
|
109
130
|
- `--clear-history`: 清空命令执行历史
|
|
@@ -119,6 +140,22 @@ aibash --test
|
|
|
119
140
|
- `[s]` 跳过/放弃 - 不执行命令
|
|
120
141
|
- `[h]` 显示帮助 - 显示帮助信息
|
|
121
142
|
|
|
143
|
+
当命令执行失败时,AIBash 会自动根据最新历史记录和错误输出请求 AI 生成新的命令建议,连同一条中文提示一起展示,帮助你快速迭代。
|
|
144
|
+
|
|
145
|
+
### 自动模式(-a)
|
|
146
|
+
|
|
147
|
+
自动模式会让 AIBash 充当一个“执行代理”,根据你的自然语言描述分步规划并完成任务:
|
|
148
|
+
|
|
149
|
+
- 模型每次只会规划一个动作(运行命令、读取文件、访问网络、向你提问或结束)
|
|
150
|
+
- 每个命令、文件读取或网络访问都会先询问你是否确认执行
|
|
151
|
+
- 执行结果会反馈给模型,帮助其决定下一步操作
|
|
152
|
+
- 适合需要多步协作的任务,如“拉取最新代码、安装依赖并运行测试”等
|
|
153
|
+
- 可使用 `--auto-approve-*` 参数细粒度控制哪些操作无需确认,并可通过 `--auto-max-steps` 限制最多执行的步骤数
|
|
154
|
+
- 如某一步执行失败,自动模式会向模型反馈错误详情,并自动重新规划新的命令或策略
|
|
155
|
+
- 自动模式支持从配置文件预设是否自动确认命令/读文件/访问网络等行为
|
|
156
|
+
|
|
157
|
+
可以与 `-new` 搭配,在新的终端窗口中执行实际命令,确保自动模式界面保持整洁。
|
|
158
|
+
|
|
122
159
|
## 配置说明
|
|
123
160
|
|
|
124
161
|
### 模型配置
|
|
@@ -140,6 +177,24 @@ aibash --test
|
|
|
140
177
|
- `system_info`: 系统信息(用于生成更准确的命令)
|
|
141
178
|
- `custom_prompt`: 自定义 prompt 模板
|
|
142
179
|
- `use_default_prompt`: 是否使用默认 prompt
|
|
180
|
+
- `ui.language`: 界面语言(`en` 或 `zh`,默认 `en`)
|
|
181
|
+
- `automation`: 自动模式默认行为配置
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
automation:
|
|
185
|
+
auto_confirm_all: false
|
|
186
|
+
auto_confirm_commands: false
|
|
187
|
+
auto_confirm_files: false
|
|
188
|
+
auto_confirm_web: false
|
|
189
|
+
max_steps: 20
|
|
190
|
+
|
|
191
|
+
ui:
|
|
192
|
+
enable_colors: true
|
|
193
|
+
single_key_mode: true
|
|
194
|
+
language: en
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
命令行中的 `--ui-language` 仅对当前会话生效,如需长期使用请在配置文件的 `ui.language` 中设置。
|
|
143
198
|
|
|
144
199
|
## 自定义 Prompt
|
|
145
200
|
|
|
@@ -56,6 +56,7 @@ class OllamaAgent(AIAgent):
|
|
|
56
56
|
temperature = kwargs.get('temperature', self.temperature)
|
|
57
57
|
max_tokens = kwargs.get('max_tokens', self.max_tokens)
|
|
58
58
|
timeout = kwargs.get('timeout', self.timeout)
|
|
59
|
+
expect_raw = kwargs.get('expect_raw', False)
|
|
59
60
|
|
|
60
61
|
try:
|
|
61
62
|
data = {
|
|
@@ -77,6 +78,8 @@ class OllamaAgent(AIAgent):
|
|
|
77
78
|
result = response.json()
|
|
78
79
|
content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
79
80
|
if content:
|
|
81
|
+
if expect_raw:
|
|
82
|
+
return content.strip()
|
|
80
83
|
return self._extract_command(content)
|
|
81
84
|
except Exception:
|
|
82
85
|
pass # 回退到传统 API
|
|
@@ -108,6 +111,9 @@ class OllamaAgent(AIAgent):
|
|
|
108
111
|
if not content:
|
|
109
112
|
raise Exception("AI returned empty content")
|
|
110
113
|
|
|
114
|
+
if expect_raw:
|
|
115
|
+
return content.strip()
|
|
116
|
+
|
|
111
117
|
return self._extract_command(content)
|
|
112
118
|
|
|
113
119
|
except requests.exceptions.RequestException as e:
|
|
@@ -73,6 +73,8 @@ class OpenAIAgent(AIAgent):
|
|
|
73
73
|
"max_tokens": max_tokens
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
expect_raw = kwargs.get('expect_raw', False)
|
|
77
|
+
|
|
76
78
|
try:
|
|
77
79
|
response = self.session.post(
|
|
78
80
|
url,
|
|
@@ -88,6 +90,9 @@ class OpenAIAgent(AIAgent):
|
|
|
88
90
|
if not content:
|
|
89
91
|
raise Exception("AI returned empty content")
|
|
90
92
|
|
|
93
|
+
if expect_raw:
|
|
94
|
+
return content.strip()
|
|
95
|
+
|
|
91
96
|
return self._extract_command(content)
|
|
92
97
|
|
|
93
98
|
except requests.exceptions.RequestException as e:
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
自动执行模式
|
|
3
|
+
|
|
4
|
+
提供在自动模式下,根据自然语言描述规划并执行多步命令的能力。
|
|
5
|
+
每一步命令或操作都会在执行前请求用户确认,执行结果会反馈给 AI 以便继续规划。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import textwrap
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from .interfaces.ai_agent import AIAgent
|
|
19
|
+
from .history import HistoryManager
|
|
20
|
+
from .interactive import InteractiveSelector
|
|
21
|
+
from .utils.terminal import TerminalOutput, Colors
|
|
22
|
+
from .config import AppConfig
|
|
23
|
+
from .i18n import t
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
AUTOMATION_PROMPT_TEMPLATE = """你是终端自动化助手,任务是根据用户提供的自然语言目标,规划并执行一系列终端操作。
|
|
27
|
+
|
|
28
|
+
必须严格遵守以下规则:
|
|
29
|
+
1. 你只能返回一段 JSON,不能包含任何额外文字或者解释。
|
|
30
|
+
2. JSON 须符合下面的结构(字段顺序不限):
|
|
31
|
+
{{
|
|
32
|
+
"action": "run_command" | "read_file" | "web_request" | "ask_user" | "finish",
|
|
33
|
+
"reason": "<解释你的选择,简洁且不超过120个中文字>",
|
|
34
|
+
"command": "<当 action 为 run_command 时的 shell 命令>",
|
|
35
|
+
"path": "<当 action 为 read_file 时需要读取的绝对或相对路径>",
|
|
36
|
+
"url": "<当 action 为 web_request 时需要访问的 URL>",
|
|
37
|
+
"message": "<当 action 为 ask_user 或 finish 时要告诉用户的话>",
|
|
38
|
+
"expectation": "<当 action 为 run_command、read_file 或 web_request 时,预期获得的结果提示>",
|
|
39
|
+
"summary": "<仅在 action 为 finish 时提供对于整个任务的总结>"
|
|
40
|
+
}}
|
|
41
|
+
3. 所有非当前 action 需要的字段可以省略或设为 null。
|
|
42
|
+
4. 每次最多规划一个动作。
|
|
43
|
+
5. 如果需要更多信息,可以使用 "ask_user"。
|
|
44
|
+
6. 当你认为任务完成时,使用 "finish",并提供 "message" 与 "summary"。
|
|
45
|
+
|
|
46
|
+
系统信息:
|
|
47
|
+
{system_info}
|
|
48
|
+
|
|
49
|
+
当前工作目录:
|
|
50
|
+
{cwd}
|
|
51
|
+
|
|
52
|
+
用户任务:
|
|
53
|
+
{task}
|
|
54
|
+
|
|
55
|
+
历史记录(仅供参考):
|
|
56
|
+
{history}
|
|
57
|
+
|
|
58
|
+
请输出符合要求的 JSON。
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class AutomationStep:
|
|
64
|
+
"""记录自动化执行的步骤"""
|
|
65
|
+
action: str
|
|
66
|
+
status: str
|
|
67
|
+
detail: str
|
|
68
|
+
observation: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AutomationExecutor:
|
|
72
|
+
"""自动执行器"""
|
|
73
|
+
|
|
74
|
+
MAX_STEPS = 20
|
|
75
|
+
MAX_HISTORY_LINES = 12
|
|
76
|
+
MAX_OBSERVATION_LENGTH = 1000
|
|
77
|
+
MAX_REQUEST_TIMEOUT = 10
|
|
78
|
+
JSON_PARSE_RETRIES = 2
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
agent: AIAgent,
|
|
83
|
+
terminal: TerminalOutput,
|
|
84
|
+
history_manager: HistoryManager,
|
|
85
|
+
interactive: InteractiveSelector,
|
|
86
|
+
config: AppConfig,
|
|
87
|
+
use_new_terminal: bool = False,
|
|
88
|
+
auto_options: Optional[Dict[str, Any]] = None
|
|
89
|
+
):
|
|
90
|
+
self.agent = agent
|
|
91
|
+
self.terminal = terminal
|
|
92
|
+
self.history_manager = history_manager
|
|
93
|
+
self.interactive = interactive
|
|
94
|
+
self.config = config
|
|
95
|
+
self.use_new_terminal = use_new_terminal
|
|
96
|
+
self.auto_options = auto_options or {}
|
|
97
|
+
self.auto_confirm_all = bool(self.auto_options.get('auto_confirm_all'))
|
|
98
|
+
self.auto_confirm_commands = bool(self.auto_options.get('auto_confirm_commands'))
|
|
99
|
+
self.auto_confirm_files = bool(self.auto_options.get('auto_confirm_files'))
|
|
100
|
+
self.auto_confirm_web = bool(self.auto_options.get('auto_confirm_web'))
|
|
101
|
+
self.max_steps = int(self.auto_options.get('max_steps') or self.MAX_STEPS)
|
|
102
|
+
self.steps: List[AutomationStep] = []
|
|
103
|
+
|
|
104
|
+
def run(self, task: str):
|
|
105
|
+
"""执行自动化任务"""
|
|
106
|
+
task = task.strip()
|
|
107
|
+
if not task:
|
|
108
|
+
self.terminal.error("✗ " + t("automation_task_empty"))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
self.terminal.print_box(
|
|
112
|
+
title=t("automation_title"),
|
|
113
|
+
content=textwrap.dedent(f"""\
|
|
114
|
+
{t("automation_task", task=task)}
|
|
115
|
+
{t("automation_mode")}
|
|
116
|
+
{t("automation_new_terminal", state=t("state_on") if self.use_new_terminal else t("state_off"))}"""),
|
|
117
|
+
color=Colors.BRIGHT_MAGENTA
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for step_idx in range(1, self.max_steps + 1):
|
|
121
|
+
prompt = self._build_prompt(task)
|
|
122
|
+
action_payload = self._request_action(prompt)
|
|
123
|
+
if not action_payload:
|
|
124
|
+
self.terminal.error("✗ " + t("automation_no_action"))
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
action = action_payload.get("action")
|
|
128
|
+
reason = action_payload.get("reason", "")
|
|
129
|
+
self.terminal.print_box(
|
|
130
|
+
title=t("automation_step_plan", index=step_idx),
|
|
131
|
+
content=textwrap.dedent(f"""\
|
|
132
|
+
{t("automation_step_action", action=action)}
|
|
133
|
+
{t("automation_step_reason", reason=reason or "-")}"""),
|
|
134
|
+
color=Colors.BRIGHT_CYAN
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
continue_running = self._execute_action(action_payload)
|
|
138
|
+
if not continue_running:
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
self.terminal.warning(t("automation_max_steps"))
|
|
142
|
+
|
|
143
|
+
def _build_prompt(self, task: str) -> str:
|
|
144
|
+
"""构建发送给模型的 prompt"""
|
|
145
|
+
system_info = self.config.system_info or "Unknown system"
|
|
146
|
+
cwd = str(Path.cwd())
|
|
147
|
+
history_text = self._format_history_for_prompt()
|
|
148
|
+
|
|
149
|
+
return AUTOMATION_PROMPT_TEMPLATE.format(
|
|
150
|
+
system_info=system_info,
|
|
151
|
+
cwd=cwd,
|
|
152
|
+
task=task,
|
|
153
|
+
history=history_text
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _format_history_for_prompt(self) -> str:
|
|
157
|
+
"""格式化历史步骤供模型参考"""
|
|
158
|
+
if not self.steps:
|
|
159
|
+
return "(暂无历史步骤)"
|
|
160
|
+
|
|
161
|
+
lines: List[str] = []
|
|
162
|
+
recent = self.steps[-self.MAX_HISTORY_LINES :]
|
|
163
|
+
for idx, step in enumerate(recent, start=max(1, len(self.steps) - len(recent) + 1)):
|
|
164
|
+
lines.append(f"[{idx}] action={step.action}, status={step.status}")
|
|
165
|
+
lines.append(f"reason/detail={self._truncate(step.detail)}")
|
|
166
|
+
lines.append(f"observation={self._truncate(step.observation)}")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
def _request_action(self, prompt: str) -> Optional[Dict[str, Any]]:
|
|
170
|
+
"""调用模型获取下一步动作"""
|
|
171
|
+
for attempt in range(self.JSON_PARSE_RETRIES):
|
|
172
|
+
response = self.agent.generate_command(prompt, expect_raw=True)
|
|
173
|
+
payload = self._parse_json_response(response)
|
|
174
|
+
if payload is not None:
|
|
175
|
+
return payload
|
|
176
|
+
prompt = self._augment_prompt_with_error(prompt, response)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def _parse_json_response(self, text: str) -> Optional[Dict[str, Any]]:
|
|
180
|
+
"""解析模型返回的 JSON"""
|
|
181
|
+
text = text.strip()
|
|
182
|
+
try:
|
|
183
|
+
return json.loads(text)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
# 尝试提取首尾括号之间的部分
|
|
186
|
+
start = text.find("{")
|
|
187
|
+
end = text.rfind("}")
|
|
188
|
+
if start != -1 and end != -1 and end > start:
|
|
189
|
+
snippet = text[start : end + 1]
|
|
190
|
+
try:
|
|
191
|
+
return json.loads(snippet)
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
return None
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _augment_prompt_with_error(self, prompt: str, response: str) -> str:
|
|
197
|
+
"""在 prompt 中加入错误反馈,提醒模型输出有效 JSON"""
|
|
198
|
+
feedback = textwrap.dedent(
|
|
199
|
+
t("automation_prompt_invalid_json", response=response)
|
|
200
|
+
)
|
|
201
|
+
return prompt + "\n" + feedback
|
|
202
|
+
|
|
203
|
+
def _execute_action(self, payload: Dict[str, Any]) -> bool:
|
|
204
|
+
"""执行具体动作并记录结果
|
|
205
|
+
|
|
206
|
+
返回值:
|
|
207
|
+
True 继续执行后续步骤
|
|
208
|
+
False 停止自动模式
|
|
209
|
+
"""
|
|
210
|
+
action = payload.get("action", "").lower()
|
|
211
|
+
reason = payload.get("reason") or ""
|
|
212
|
+
|
|
213
|
+
if action == "run_command":
|
|
214
|
+
return self._handle_run_command(payload, reason)
|
|
215
|
+
if action == "read_file":
|
|
216
|
+
return self._handle_read_file(payload, reason)
|
|
217
|
+
if action == "web_request":
|
|
218
|
+
return self._handle_web_request(payload, reason)
|
|
219
|
+
if action == "ask_user":
|
|
220
|
+
return self._handle_ask_user(payload, reason)
|
|
221
|
+
if action == "finish":
|
|
222
|
+
return self._handle_finish(payload, reason)
|
|
223
|
+
|
|
224
|
+
observation = t("automation_invalid_action", action=action)
|
|
225
|
+
self._record_step(action or "unknown", "failed", reason, observation)
|
|
226
|
+
self.terminal.error(f"✗ {observation}")
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def _handle_run_command(self, payload: Dict[str, Any], reason: str) -> bool:
|
|
230
|
+
command = payload.get("command")
|
|
231
|
+
expectation = payload.get("expectation", "")
|
|
232
|
+
if not command:
|
|
233
|
+
observation = t("automation_missing_command")
|
|
234
|
+
self._record_step("run_command", "failed", reason, observation)
|
|
235
|
+
self.terminal.error("✗ " + t("automation_missing_command"))
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
self.terminal.info(t("automation_command_suggestion"))
|
|
239
|
+
self.interactive.show_command(command)
|
|
240
|
+
if expectation:
|
|
241
|
+
self.terminal.dim(t("automation_command_expectation", expectation=expectation))
|
|
242
|
+
|
|
243
|
+
if not self._confirm_with_user(t("automation_confirm_run_command"), action="run_command"):
|
|
244
|
+
observation = t("automation_command_denied")
|
|
245
|
+
self._record_step("run_command", "skipped", reason, observation)
|
|
246
|
+
self.terminal.warning(t("automation_command_denied"))
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
success, output = self.interactive.execute_command(
|
|
250
|
+
command,
|
|
251
|
+
use_new_terminal=self.use_new_terminal
|
|
252
|
+
)
|
|
253
|
+
observation = output or t("automation_no_output_captured")
|
|
254
|
+
status = "success" if success else "failed"
|
|
255
|
+
self._record_step("run_command", status, reason, observation)
|
|
256
|
+
|
|
257
|
+
if self.history_manager.enabled:
|
|
258
|
+
self.history_manager.add_record(
|
|
259
|
+
command=command,
|
|
260
|
+
output=output if self.config.history.include_output else "",
|
|
261
|
+
success=success,
|
|
262
|
+
user_query=""
|
|
263
|
+
)
|
|
264
|
+
if not success:
|
|
265
|
+
self.terminal.warning(t("automation_command_failed"))
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def _handle_read_file(self, payload: Dict[str, Any], reason: str) -> bool:
|
|
269
|
+
path_value = payload.get("path")
|
|
270
|
+
if not path_value:
|
|
271
|
+
observation = t("automation_missing_path")
|
|
272
|
+
self._record_step("read_file", "failed", reason, observation)
|
|
273
|
+
self.terminal.error("✗ " + t("automation_missing_path"))
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
target_path = Path(path_value).expanduser().resolve()
|
|
277
|
+
self.terminal.info(t("automation_file_request", path=target_path))
|
|
278
|
+
|
|
279
|
+
if not self._confirm_with_user(t("automation_confirm_read_file"), action="read_file"):
|
|
280
|
+
observation = t("automation_file_denied")
|
|
281
|
+
self._record_step("read_file", "skipped", reason, observation)
|
|
282
|
+
self.terminal.warning(t("automation_file_denied"))
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
if not target_path.exists():
|
|
286
|
+
observation = t("automation_file_missing", path=target_path)
|
|
287
|
+
self._record_step("read_file", "failed", reason, observation)
|
|
288
|
+
self.terminal.error("✗ " + t("automation_file_missing", path=target_path))
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
content = target_path.read_text(encoding="utf-8")
|
|
293
|
+
preview = self._truncate(content, self.MAX_OBSERVATION_LENGTH)
|
|
294
|
+
self.terminal.print_box(
|
|
295
|
+
title=t("automation_file_preview_title"),
|
|
296
|
+
content=preview,
|
|
297
|
+
color=Colors.BRIGHT_GREEN
|
|
298
|
+
)
|
|
299
|
+
self._record_step("read_file", "success", reason, preview)
|
|
300
|
+
return True
|
|
301
|
+
except UnicodeDecodeError:
|
|
302
|
+
observation = t("automation_file_binary")
|
|
303
|
+
self._record_step("read_file", "failed", reason, observation)
|
|
304
|
+
self.terminal.error("✗ " + t("automation_file_binary"))
|
|
305
|
+
return True
|
|
306
|
+
except Exception as e:
|
|
307
|
+
observation = t("automation_file_error", error=e)
|
|
308
|
+
self._record_step("read_file", "failed", reason, observation)
|
|
309
|
+
self.terminal.error("✗ " + t("automation_file_error", error=e))
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def _handle_web_request(self, payload: Dict[str, Any], reason: str) -> bool:
|
|
313
|
+
url = payload.get("url")
|
|
314
|
+
expectation = payload.get("expectation", "")
|
|
315
|
+
if not url:
|
|
316
|
+
observation = t("automation_missing_url")
|
|
317
|
+
self._record_step("web_request", "failed", reason, observation)
|
|
318
|
+
self.terminal.error("✗ " + t("automation_missing_url"))
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
self.terminal.info(t("automation_web_request", url=url))
|
|
322
|
+
if expectation:
|
|
323
|
+
self.terminal.dim(t("automation_web_expectation", expectation=expectation))
|
|
324
|
+
|
|
325
|
+
if not url.lower().startswith(("http://", "https://")):
|
|
326
|
+
observation = t("automation_web_protocol")
|
|
327
|
+
self._record_step("web_request", "failed", reason, observation)
|
|
328
|
+
self.terminal.error("✗ " + t("automation_web_protocol"))
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
if not self._confirm_with_user(t("automation_confirm_web_request"), action="web_request"):
|
|
332
|
+
observation = t("automation_web_denied")
|
|
333
|
+
self._record_step("web_request", "skipped", reason, observation)
|
|
334
|
+
self.terminal.warning(t("automation_web_denied"))
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
return self._perform_web_request(url, reason, expectation, verify_ssl=True)
|
|
339
|
+
except requests.exceptions.SSLError as ssl_error:
|
|
340
|
+
self.terminal.error("✗ " + t("automation_ssl_error", error=ssl_error))
|
|
341
|
+
if not self._confirm_with_user(t("automation_ssl_prompt", url=url), action="web_request"):
|
|
342
|
+
observation = t("automation_ssl_denied")
|
|
343
|
+
self._record_step("web_request", "skipped", reason, observation)
|
|
344
|
+
self.terminal.warning(observation)
|
|
345
|
+
return True
|
|
346
|
+
try:
|
|
347
|
+
return self._perform_web_request(url, reason, expectation, verify_ssl=False)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
observation = t("automation_web_error", error=e)
|
|
350
|
+
self._record_step("web_request", "failed", reason, observation)
|
|
351
|
+
self.terminal.error("✗ " + t("automation_web_error", error=e))
|
|
352
|
+
return True
|
|
353
|
+
except Exception as e:
|
|
354
|
+
observation = t("automation_web_error", error=e)
|
|
355
|
+
self._record_step("web_request", "failed", reason, observation)
|
|
356
|
+
self.terminal.error("✗ " + t("automation_web_error", error=e))
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
def _handle_ask_user(self, payload: Dict[str, Any], reason: str) -> bool:
|
|
360
|
+
message = payload.get("message") or "模型需要更多信息,请提供。"
|
|
361
|
+
self.terminal.print_box(
|
|
362
|
+
title=t("automation_question_title"),
|
|
363
|
+
content=message,
|
|
364
|
+
color=Colors.BRIGHT_YELLOW
|
|
365
|
+
)
|
|
366
|
+
user_input = input(t("automation_ask_user_prompt")).strip()
|
|
367
|
+
observation = user_input or ""
|
|
368
|
+
self._record_step("ask_user", "success", reason, observation)
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def _handle_finish(self, payload: Dict[str, Any], reason: str) -> bool:
|
|
372
|
+
message = payload.get("message") or "模型认为任务已经完成。"
|
|
373
|
+
summary = payload.get("summary") or ""
|
|
374
|
+
|
|
375
|
+
self._record_step("finish", "success", reason, message + ("\n" + summary if summary else ""))
|
|
376
|
+
self.terminal.print_box(
|
|
377
|
+
title=t("automation_finish_title"),
|
|
378
|
+
content=textwrap.dedent(f"""\
|
|
379
|
+
{t("automation_finish_message", message=message)}
|
|
380
|
+
{t("automation_finish_summary", summary=summary or "-")}"""),
|
|
381
|
+
color=Colors.BRIGHT_GREEN
|
|
382
|
+
)
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
def _record_step(self, action: str, status: str, detail: str, observation: str):
|
|
386
|
+
"""记录步骤信息"""
|
|
387
|
+
self.steps.append(
|
|
388
|
+
AutomationStep(
|
|
389
|
+
action=action,
|
|
390
|
+
status=status,
|
|
391
|
+
detail=detail or "",
|
|
392
|
+
observation=self._truncate(observation)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _truncate(self, text: str, limit: int = 400) -> str:
|
|
397
|
+
"""截断文本"""
|
|
398
|
+
if len(text) <= limit:
|
|
399
|
+
return text
|
|
400
|
+
return text[: limit - 3] + "..."
|
|
401
|
+
|
|
402
|
+
def _confirm_with_user(self, prompt: str, action: str) -> bool:
|
|
403
|
+
"""请求用户确认"""
|
|
404
|
+
if self.auto_confirm_all:
|
|
405
|
+
return True
|
|
406
|
+
if action == "run_command" and self.auto_confirm_commands:
|
|
407
|
+
return True
|
|
408
|
+
if action == "read_file" and self.auto_confirm_files:
|
|
409
|
+
return True
|
|
410
|
+
if action == "web_request" and self.auto_confirm_web:
|
|
411
|
+
return True
|
|
412
|
+
try:
|
|
413
|
+
choice = input(prompt).strip().lower()
|
|
414
|
+
return choice in ("y", "yes")
|
|
415
|
+
except (EOFError, KeyboardInterrupt):
|
|
416
|
+
self.terminal.warning(t("common_operation_cancelled"))
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
def _perform_web_request(self, url: str, reason: str, expectation: str, verify_ssl: bool = True) -> bool:
|
|
420
|
+
"""执行具体的网络请求逻辑"""
|
|
421
|
+
response = requests.get(url, timeout=self.MAX_REQUEST_TIMEOUT, verify=verify_ssl)
|
|
422
|
+
content_type = response.headers.get("Content-Type", "")
|
|
423
|
+
text = response.text
|
|
424
|
+
preview = self._truncate(text, self.MAX_OBSERVATION_LENGTH)
|
|
425
|
+
status_line = f"HTTP {response.status_code} {response.reason}"
|
|
426
|
+
note = ""
|
|
427
|
+
if not verify_ssl:
|
|
428
|
+
note = "\n" + t("automation_ssl_unverified_note")
|
|
429
|
+
observation = f"{status_line}; Content-Type={content_type}; Preview:\n{preview}{note}"
|
|
430
|
+
self._record_step("web_request", "success", reason, observation)
|
|
431
|
+
self.terminal.print_box(
|
|
432
|
+
title="Web Response Preview",
|
|
433
|
+
content=preview,
|
|
434
|
+
color=Colors.BRIGHT_GREEN
|
|
435
|
+
)
|
|
436
|
+
if note:
|
|
437
|
+
self.terminal.warning(t("automation_ssl_unverified_note"))
|
|
438
|
+
return True
|
|
439
|
+
|