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.
Files changed (35) hide show
  1. {aibash_wx-0.1.0/aibash_wx.egg-info → aibash_wx-0.2.0}/PKG-INFO +57 -2
  2. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/README.md +56 -1
  3. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/ollama_agent.py +6 -0
  4. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/openai_agent.py +5 -0
  5. aibash_wx-0.2.0/aibash/automation.py +439 -0
  6. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/config.py +46 -0
  7. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/config_init.py +35 -26
  8. aibash_wx-0.2.0/aibash/i18n.py +389 -0
  9. aibash_wx-0.2.0/aibash/interactive.py +267 -0
  10. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/interfaces/ai_agent.py +1 -0
  11. aibash_wx-0.2.0/aibash/main.py +557 -0
  12. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/prompt.py +71 -0
  13. {aibash_wx-0.1.0 → aibash_wx-0.2.0/aibash_wx.egg-info}/PKG-INFO +57 -2
  14. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/SOURCES.txt +4 -1
  15. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/top_level.txt +1 -0
  16. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/pyproject.toml +1 -1
  17. aibash_wx-0.2.0/venv/bin/activate_this.py +32 -0
  18. aibash_wx-0.1.0/aibash/interactive.py +0 -185
  19. aibash_wx-0.1.0/aibash/main.py +0 -323
  20. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/LICENSE +0 -0
  21. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/__init__.py +0 -0
  22. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/__init__.py +0 -0
  23. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/agents/agent_builder.py +0 -0
  24. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/ai_client.py +0 -0
  25. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/history.py +0 -0
  26. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/interfaces/__init__.py +0 -0
  27. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/resources/__init__.py +0 -0
  28. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/__init__.py +0 -0
  29. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/clipboard.py +0 -0
  30. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/keyboard.py +0 -0
  31. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash/utils/terminal.py +0 -0
  32. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/dependency_links.txt +0 -0
  33. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/entry_points.txt +0 -0
  34. {aibash_wx-0.1.0 → aibash_wx-0.2.0}/aibash_wx.egg-info/requires.txt +0 -0
  35. {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.1.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
+