vox-code 2.0.0__py3-none-any.whl
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.
- vox_code-2.0.0.dist-info/METADATA +258 -0
- vox_code-2.0.0.dist-info/RECORD +88 -0
- vox_code-2.0.0.dist-info/WHEEL +4 -0
- vox_code-2.0.0.dist-info/entry_points.txt +3 -0
- voxcli/__init__.py +3 -0
- voxcli/__main__.py +5 -0
- voxcli/agent/__init__.py +12 -0
- voxcli/agent/agent.py +449 -0
- voxcli/agent/agent_budget.py +133 -0
- voxcli/agent/agent_orchestrator.py +414 -0
- voxcli/agent/plan_execute_agent.py +514 -0
- voxcli/agent/roles.py +80 -0
- voxcli/agent/sub_agent.py +351 -0
- voxcli/catalog.py +477 -0
- voxcli/chat.py +91 -0
- voxcli/cli/__init__.py +4 -0
- voxcli/cli/main.py +452 -0
- voxcli/cli/parser.py +71 -0
- voxcli/config.py +518 -0
- voxcli/gui/__main__.py +3 -0
- voxcli/gui/main.py +22 -0
- voxcli/gui/pet/__init__.py +5 -0
- voxcli/gui/pet/base.py +62 -0
- voxcli/gui/pet/coordinator.py +888 -0
- voxcli/gui/pet/data.py +430 -0
- voxcli/gui/pet/widgets.py +683 -0
- voxcli/gui/pet/windows.py +2298 -0
- voxcli/gui/pet/workers.py +54 -0
- voxcli/gui/pet_app.py +7 -0
- voxcli/hitl/__init__.py +11 -0
- voxcli/hitl/handler.py +11 -0
- voxcli/hitl/policy.py +32 -0
- voxcli/hitl/request.py +13 -0
- voxcli/hitl/result.py +11 -0
- voxcli/hitl/terminal_handler.py +64 -0
- voxcli/hitl/tool_registry.py +64 -0
- voxcli/llm/base.py +93 -0
- voxcli/llm/factory.py +178 -0
- voxcli/llm/ollama_client.py +137 -0
- voxcli/llm/openai_compatible.py +249 -0
- voxcli/memory/base.py +16 -0
- voxcli/memory/budget.py +53 -0
- voxcli/memory/compressor.py +198 -0
- voxcli/memory/entry.py +36 -0
- voxcli/memory/long_term.py +126 -0
- voxcli/memory/manager.py +101 -0
- voxcli/memory/retriever.py +72 -0
- voxcli/memory/short_term.py +84 -0
- voxcli/memory/tokenizer.py +21 -0
- voxcli/plan/__init__.py +5 -0
- voxcli/plan/execution_plan.py +225 -0
- voxcli/plan/planner.py +198 -0
- voxcli/plan/task.py +123 -0
- voxcli/policy/audit_log.py +111 -0
- voxcli/policy/command_guard.py +34 -0
- voxcli/policy/exception.py +5 -0
- voxcli/policy/path_guard.py +32 -0
- voxcli/prompting/__init__.py +7 -0
- voxcli/prompting/presenter.py +154 -0
- voxcli/rag/__init__.py +16 -0
- voxcli/rag/analyzer.py +89 -0
- voxcli/rag/chunk.py +17 -0
- voxcli/rag/chunker.py +137 -0
- voxcli/rag/embedding.py +75 -0
- voxcli/rag/formatter.py +40 -0
- voxcli/rag/index.py +96 -0
- voxcli/rag/relation.py +14 -0
- voxcli/rag/retriever.py +58 -0
- voxcli/rag/store.py +155 -0
- voxcli/rag/tokenizer.py +26 -0
- voxcli/runtime/__init__.py +6 -0
- voxcli/runtime/session_controller.py +386 -0
- voxcli/tool/__init__.py +3 -0
- voxcli/tool/tool_registry.py +433 -0
- voxcli/util/animation.py +219 -0
- voxcli/util/ansi.py +82 -0
- voxcli/util/markdown.py +98 -0
- voxcli/web/__init__.py +17 -0
- voxcli/web/base.py +20 -0
- voxcli/web/extractor.py +77 -0
- voxcli/web/factory.py +38 -0
- voxcli/web/fetch_result.py +27 -0
- voxcli/web/fetcher.py +42 -0
- voxcli/web/network_policy.py +49 -0
- voxcli/web/result.py +23 -0
- voxcli/web/searxng.py +55 -0
- voxcli/web/serpapi.py +53 -0
- voxcli/web/zhipu.py +55 -0
voxcli/cli/main.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""CLI 主程序 - REPL 循环"""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional, Sequence
|
|
8
|
+
|
|
9
|
+
from ..config import ProviderConfig, pai_config
|
|
10
|
+
from ..llm.factory import create_from_config, default_base_url_for, default_model_for
|
|
11
|
+
from ..agent import Agent, PlanExecuteAgent, AgentOrchestrator
|
|
12
|
+
from ..agent.plan_execute_agent import PlanReviewHandler, PlanReviewDecision, PlanReviewAction
|
|
13
|
+
from ..tool import ToolRegistry
|
|
14
|
+
from ..memory.manager import MemoryManager
|
|
15
|
+
from ..hitl import TerminalHitlHandler, HitlToolRegistry, ApprovalPolicy
|
|
16
|
+
from ..prompting import PresentationMode, ResponsePresenter
|
|
17
|
+
from ..util.ansi import heading, section, subtle, emphasis, success, error
|
|
18
|
+
from ..util.animation import ProgressDots, Typewriter
|
|
19
|
+
from .parser import CliCommandParser, ParsedCommand
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
_SUPPORTED_PROVIDERS = ("glm", "deepseek", "qwen", "ollama")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _init_logging(debug: bool = False):
|
|
26
|
+
level = logging.DEBUG if debug else logging.WARNING
|
|
27
|
+
logging.basicConfig(
|
|
28
|
+
level=level,
|
|
29
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
30
|
+
datefmt="%H:%M:%S",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_repl():
|
|
35
|
+
debug = os.environ.get("VOX_CODE_DEBUG", "").lower() in ("1", "true", "yes")
|
|
36
|
+
_init_logging(debug)
|
|
37
|
+
|
|
38
|
+
_animate_startup()
|
|
39
|
+
|
|
40
|
+
llm_client = create_from_config()
|
|
41
|
+
if llm_client is None:
|
|
42
|
+
print("❌ 无法创建 LLM 客户端。请检查环境变量配置。")
|
|
43
|
+
print(" 至少需要配置一个模型提供商:")
|
|
44
|
+
print(" - GLM: GLM_API_KEY + GLM_MODEL")
|
|
45
|
+
print(" - DeepSeek: DEEPSEEK_API_KEY + DEEPSEEK_MODEL")
|
|
46
|
+
print(" - Qwen: QWEN_API_KEY + QWEN_MODEL")
|
|
47
|
+
print(" - Ollama: OLLAMA_MODEL + OLLAMA_BASE_URL")
|
|
48
|
+
print(" 也可以先运行: vox-code init")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
tool_registry = ToolRegistry()
|
|
52
|
+
memory_manager = MemoryManager(llm_client)
|
|
53
|
+
|
|
54
|
+
# 三种运行模式
|
|
55
|
+
state = {
|
|
56
|
+
"mode": "single",
|
|
57
|
+
"presentation_mode": _default_presentation_mode().value,
|
|
58
|
+
}
|
|
59
|
+
agent = Agent(llm_client, tool_registry)
|
|
60
|
+
plan_agent = PlanExecuteAgent(llm_client, tool_registry, None, memory_manager, None)
|
|
61
|
+
orchestrator = AgentOrchestrator(llm_client, tool_registry, memory_manager)
|
|
62
|
+
presenter = ResponsePresenter(llm_client)
|
|
63
|
+
|
|
64
|
+
terminal_hitl = TerminalHitlHandler()
|
|
65
|
+
hitl_registry = HitlToolRegistry(tool_registry)
|
|
66
|
+
approval_policy = ApprovalPolicy()
|
|
67
|
+
parser = CliCommandParser()
|
|
68
|
+
|
|
69
|
+
tw = Typewriter()
|
|
70
|
+
tw.write_fast(subtle(f" Model: {llm_client.__class__.__name__}\n"))
|
|
71
|
+
tw.write_fast(subtle(f" Mode: {_render_mode_label(state['mode'])}\n"))
|
|
72
|
+
tw.write_fast(subtle(f" Style: {state['presentation_mode']}\n"))
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
while True:
|
|
76
|
+
try:
|
|
77
|
+
line = input(">>> ").strip()
|
|
78
|
+
except (EOFError, KeyboardInterrupt):
|
|
79
|
+
print("\n" + subtle("再见!"))
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Handle commands
|
|
86
|
+
parsed = parser.parse(line)
|
|
87
|
+
if parsed:
|
|
88
|
+
_handle_command(parsed, agent, plan_agent, orchestrator, memory_manager,
|
|
89
|
+
tool_registry, hitl_registry, approval_policy, terminal_hitl,
|
|
90
|
+
llm_client, presenter, lambda: state["mode"],
|
|
91
|
+
lambda m: state.__setitem__("mode", m),
|
|
92
|
+
lambda: state["presentation_mode"],
|
|
93
|
+
lambda m: state.__setitem__("presentation_mode", m))
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Handle normal input
|
|
97
|
+
try:
|
|
98
|
+
mode = state["mode"]
|
|
99
|
+
if mode == "single":
|
|
100
|
+
result = agent.run(line)
|
|
101
|
+
if result:
|
|
102
|
+
_print_presented_result(presenter, state["presentation_mode"], line, result)
|
|
103
|
+
elif mode == "plan":
|
|
104
|
+
result = plan_agent.run(line)
|
|
105
|
+
if result:
|
|
106
|
+
_print_presented_result(presenter, state["presentation_mode"], line, result)
|
|
107
|
+
else:
|
|
108
|
+
result = orchestrator.run(line)
|
|
109
|
+
if result:
|
|
110
|
+
_print_presented_result(presenter, state["presentation_mode"], line, result)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error("Execution failed", exc_info=True)
|
|
113
|
+
print(f"❌ 执行失败: {e}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _handle_command(parsed: ParsedCommand, agent, plan_agent, orchestrator,
|
|
117
|
+
memory_manager, tool_registry, hitl_registry, approval_policy,
|
|
118
|
+
terminal_hitl, llm_client, presenter, get_mode, set_mode,
|
|
119
|
+
get_presentation_mode, set_presentation_mode):
|
|
120
|
+
cmd = parsed.command
|
|
121
|
+
|
|
122
|
+
if cmd == "/exit":
|
|
123
|
+
print(subtle("再见!"))
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
|
|
126
|
+
elif cmd == "/init":
|
|
127
|
+
_cmd_init()
|
|
128
|
+
|
|
129
|
+
elif cmd == "/help":
|
|
130
|
+
CliCommandParser.print_help()
|
|
131
|
+
|
|
132
|
+
elif cmd == "/model":
|
|
133
|
+
_cmd_model(parsed, agent, plan_agent, orchestrator, llm_client, presenter)
|
|
134
|
+
|
|
135
|
+
elif cmd == "/plan":
|
|
136
|
+
print(heading("📋 当前执行计划"))
|
|
137
|
+
print("Plan-and-Execute 模式会在每次任务前自动生成计划。")
|
|
138
|
+
print("使用 /team 切换到多 Agent 团队模式,或保持默认的单 Agent 模式。")
|
|
139
|
+
|
|
140
|
+
elif cmd == "/team":
|
|
141
|
+
current = get_mode()
|
|
142
|
+
modes = ["single", "plan", "team"]
|
|
143
|
+
idx = modes.index(current) if current in modes else 0
|
|
144
|
+
next_mode = modes[(idx + 1) % len(modes)]
|
|
145
|
+
set_mode(next_mode)
|
|
146
|
+
print(f" {success('✓')} {subtle(f'Switched to {_render_mode_label(next_mode)} mode')}")
|
|
147
|
+
|
|
148
|
+
elif cmd == "/style":
|
|
149
|
+
if parsed.args:
|
|
150
|
+
if not PresentationMode.is_valid(parsed.args[0]):
|
|
151
|
+
print(subtle(" Invalid style, options: work, pet"))
|
|
152
|
+
return
|
|
153
|
+
requested = PresentationMode.normalize(parsed.args[0]).value
|
|
154
|
+
set_presentation_mode(requested)
|
|
155
|
+
print(f" {success('✓')} {subtle('Style set to: ' + requested)}")
|
|
156
|
+
else:
|
|
157
|
+
print(subtle(f" Current style: {get_presentation_mode()}"))
|
|
158
|
+
|
|
159
|
+
elif cmd == "/hitl":
|
|
160
|
+
if parsed.args:
|
|
161
|
+
mode = parsed.args[0].lower()
|
|
162
|
+
if mode in ("auto", "always", "never"):
|
|
163
|
+
approval_policy.set_mode(mode)
|
|
164
|
+
print(f" {success('✓')} {subtle('HITL policy set to: ' + mode)}")
|
|
165
|
+
else:
|
|
166
|
+
print(subtle(" Invalid mode, options: auto, always, never"))
|
|
167
|
+
else:
|
|
168
|
+
print(subtle(f" Current HITL policy: {approval_policy.mode}"))
|
|
169
|
+
|
|
170
|
+
elif cmd == "/policy":
|
|
171
|
+
print(heading("🛡️ 安全策略"))
|
|
172
|
+
print(f" 项目根目录: {tool_registry.project_path}")
|
|
173
|
+
print(f" 审批模式: {approval_policy.mode}")
|
|
174
|
+
print(" PathGuard: 路径限制在项目根目录之内")
|
|
175
|
+
print(" CommandGuard: 禁止危险命令")
|
|
176
|
+
print(" 审计日志: ~/.vox-code/audit/")
|
|
177
|
+
|
|
178
|
+
elif cmd == "/audit":
|
|
179
|
+
_cmd_audit(tool_registry)
|
|
180
|
+
|
|
181
|
+
elif cmd == "/index":
|
|
182
|
+
_cmd_index(parsed, tool_registry)
|
|
183
|
+
|
|
184
|
+
elif cmd == "/search":
|
|
185
|
+
_cmd_search(parsed, tool_registry)
|
|
186
|
+
|
|
187
|
+
elif cmd == "/memory":
|
|
188
|
+
print(memory_manager.status_summary())
|
|
189
|
+
|
|
190
|
+
elif cmd == "/clear":
|
|
191
|
+
agent.clear_history()
|
|
192
|
+
print("对话历史已清空")
|
|
193
|
+
|
|
194
|
+
elif cmd == "/context":
|
|
195
|
+
print(agent.get_context_status())
|
|
196
|
+
|
|
197
|
+
elif cmd == "/save":
|
|
198
|
+
_cmd_save(parsed, agent)
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
print(f"未知命令: {cmd},输入 /help 查看可用命令")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cmd_model(parsed, agent, plan_agent, orchestrator, llm_client, presenter):
|
|
205
|
+
from ..llm.factory import create
|
|
206
|
+
target = parsed.args[0] if parsed.args else ""
|
|
207
|
+
if not target:
|
|
208
|
+
print("用法: /model <preset-id|provider[:<model>]>")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
provider, model_name, preset = pai_config.resolve_model_selection(target)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
new_client = create(provider, model_name)
|
|
215
|
+
if new_client is None:
|
|
216
|
+
print(f" {error('✗')} {subtle(f'Failed to create client for provider={provider}')}")
|
|
217
|
+
return
|
|
218
|
+
selected_model = getattr(new_client, "model_name", model_name or "") or (preset.model if preset else "")
|
|
219
|
+
pai_config.persist_model_selection(provider, selected_model)
|
|
220
|
+
agent.set_llm_client(new_client)
|
|
221
|
+
plan_agent._llm = new_client
|
|
222
|
+
orchestrator._llm = new_client
|
|
223
|
+
presenter._llm = new_client
|
|
224
|
+
model_desc = provider + (f" ({selected_model})" if selected_model else "")
|
|
225
|
+
print(f" {success('✓')} {subtle('Model switched to: ' + model_desc)}")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f"切换模型失败: {e}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cmd_audit(tool_registry):
|
|
231
|
+
log = tool_registry.audit_log
|
|
232
|
+
entries = log.recent(10)
|
|
233
|
+
if not entries:
|
|
234
|
+
print("暂无审计记录")
|
|
235
|
+
return
|
|
236
|
+
print(heading("📋 最近审计记录"))
|
|
237
|
+
for entry in entries:
|
|
238
|
+
icon = {"allow": "✅", "deny": "🛡️", "error": "❌"}.get(entry.outcome, "❓")
|
|
239
|
+
print(f" {icon} {entry.tool} ({entry.outcome}) - {entry.reason or '无原因'}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _cmd_index(parsed, tool_registry):
|
|
243
|
+
project_path = tool_registry.project_path
|
|
244
|
+
print(f"正在索引项目: {project_path}")
|
|
245
|
+
try:
|
|
246
|
+
from ..rag.index import CodeIndex
|
|
247
|
+
indexer = CodeIndex(project_path)
|
|
248
|
+
count = indexer.index_project()
|
|
249
|
+
print(f"索引完成,共 {count} 个代码块")
|
|
250
|
+
except ImportError:
|
|
251
|
+
print("索引功能不可用(缺少 RAG 模块依赖)")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"索引失败: {e}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _cmd_search(parsed, tool_registry):
|
|
257
|
+
query = " ".join(parsed.args) if parsed.args else ""
|
|
258
|
+
if not query:
|
|
259
|
+
print("用法: /search <query>")
|
|
260
|
+
return
|
|
261
|
+
try:
|
|
262
|
+
result = tool_registry._search_code(query, 5)
|
|
263
|
+
print(result)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
print(f"搜索失败: {e}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _cmd_save(parsed, agent):
|
|
269
|
+
filename = " ".join(parsed.args) if parsed.args else "conversation.md"
|
|
270
|
+
try:
|
|
271
|
+
history = agent.conversation_history
|
|
272
|
+
lines = []
|
|
273
|
+
for msg in history:
|
|
274
|
+
role = msg.role.upper()
|
|
275
|
+
content = msg.content or ""
|
|
276
|
+
lines.append(f"## {role}\n{content}\n")
|
|
277
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
278
|
+
f.write("\n".join(lines))
|
|
279
|
+
print(f"对话已保存到: {filename}")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
print(f"保存失败: {e}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _cmd_init():
|
|
285
|
+
try:
|
|
286
|
+
print()
|
|
287
|
+
print(heading("⚙️ Vox Code 初始化"))
|
|
288
|
+
print(subtle(f" 配置文件: {pai_config.config_file()}"))
|
|
289
|
+
print(subtle(" 环境变量仍然优先于 config.json。"))
|
|
290
|
+
print()
|
|
291
|
+
|
|
292
|
+
provider = _prompt_provider()
|
|
293
|
+
current = pai_config.providers.get(provider, ProviderConfig())
|
|
294
|
+
default_model = current.model or default_model_for(provider)
|
|
295
|
+
default_base_url = current.base_url or default_base_url_for(provider)
|
|
296
|
+
model = _prompt_text("模型名", default_model, allow_empty=False)
|
|
297
|
+
base_url = _prompt_text("Base URL", default_base_url, allow_empty=False)
|
|
298
|
+
api_key = ""
|
|
299
|
+
if provider != "ollama":
|
|
300
|
+
api_key = _prompt_secret("API Key", current.api_key, required=True)
|
|
301
|
+
|
|
302
|
+
provider_config = ProviderConfig(
|
|
303
|
+
api_key=api_key,
|
|
304
|
+
base_url=base_url,
|
|
305
|
+
model=model,
|
|
306
|
+
)
|
|
307
|
+
pai_config.set_provider_config(provider, provider_config)
|
|
308
|
+
pai_config.persist_model_selection(provider, model)
|
|
309
|
+
|
|
310
|
+
print()
|
|
311
|
+
print(f" {success('✓')} {subtle('配置已保存到: ' + str(pai_config.config_file()))}")
|
|
312
|
+
print(f" {success('✓')} {subtle('默认模型: ' + provider + ' (' + model + ')')}")
|
|
313
|
+
if provider == "ollama":
|
|
314
|
+
print(subtle(" 下一步: 确认本机 Ollama 已启动,然后运行 vox-code"))
|
|
315
|
+
else:
|
|
316
|
+
print(subtle(" 下一步: 运行 vox-code"))
|
|
317
|
+
except (EOFError, KeyboardInterrupt):
|
|
318
|
+
print()
|
|
319
|
+
print(subtle("已取消初始化。"))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _prompt_provider() -> str:
|
|
323
|
+
current = pai_config.default_provider_name
|
|
324
|
+
default_provider = current if current in _SUPPORTED_PROVIDERS else "glm"
|
|
325
|
+
print()
|
|
326
|
+
print("选择模型提供商:")
|
|
327
|
+
for idx, provider in enumerate(_SUPPORTED_PROVIDERS, start=1):
|
|
328
|
+
suffix = " (默认)" if provider == default_provider else ""
|
|
329
|
+
print(f" {idx}. {_provider_label(provider)}{suffix}")
|
|
330
|
+
|
|
331
|
+
while True:
|
|
332
|
+
raw = input(f"提供商 [1-{len(_SUPPORTED_PROVIDERS)} / {default_provider}]: ").strip().lower()
|
|
333
|
+
if not raw:
|
|
334
|
+
return default_provider
|
|
335
|
+
if raw.isdigit():
|
|
336
|
+
index = int(raw) - 1
|
|
337
|
+
if 0 <= index < len(_SUPPORTED_PROVIDERS):
|
|
338
|
+
return _SUPPORTED_PROVIDERS[index]
|
|
339
|
+
if raw in _SUPPORTED_PROVIDERS:
|
|
340
|
+
return raw
|
|
341
|
+
print("请输入 1/2/3/4 或 provider 名称(glm、deepseek、qwen、ollama)。")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _prompt_text(label: str, default: str = "", allow_empty: bool = True) -> str:
|
|
345
|
+
while True:
|
|
346
|
+
suffix = f" [{default}]" if default else ""
|
|
347
|
+
raw = input(f"{label}{suffix}: ").strip()
|
|
348
|
+
if raw:
|
|
349
|
+
return raw
|
|
350
|
+
if default:
|
|
351
|
+
return default
|
|
352
|
+
if allow_empty:
|
|
353
|
+
return ""
|
|
354
|
+
print(f"{label} 不能为空。")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _prompt_secret(label: str, current: str = "", required: bool = False) -> str:
|
|
358
|
+
masked = _mask_secret(current)
|
|
359
|
+
suffix = f" [{masked}]" if masked else ""
|
|
360
|
+
while True:
|
|
361
|
+
raw = getpass.getpass(f"{label}{suffix}: ").strip()
|
|
362
|
+
if raw:
|
|
363
|
+
return raw
|
|
364
|
+
if current:
|
|
365
|
+
return current
|
|
366
|
+
if not required:
|
|
367
|
+
return ""
|
|
368
|
+
print(f"{label} 不能为空。")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _mask_secret(value: str) -> str:
|
|
372
|
+
if not value:
|
|
373
|
+
return ""
|
|
374
|
+
if len(value) <= 8:
|
|
375
|
+
return "*" * len(value)
|
|
376
|
+
return value[:4] + "*" * (len(value) - 8) + value[-4:]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _provider_label(provider: str) -> str:
|
|
380
|
+
return {
|
|
381
|
+
"glm": "GLM",
|
|
382
|
+
"deepseek": "DeepSeek",
|
|
383
|
+
"qwen": "Qwen",
|
|
384
|
+
"ollama": "Ollama",
|
|
385
|
+
}.get(provider, provider.upper())
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _print_cli_usage():
|
|
389
|
+
print("用法:")
|
|
390
|
+
print(" vox-code 启动交互式 REPL")
|
|
391
|
+
print(" vox-code init 初始化模型配置")
|
|
392
|
+
print(" vox-code config-path 显示配置文件路径")
|
|
393
|
+
print(" vox-code --version 显示版本")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main(argv: Optional[Sequence[str]] = None):
|
|
397
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
398
|
+
if not args:
|
|
399
|
+
run_repl()
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
command = args[0].strip().lower()
|
|
403
|
+
if command in {"-h", "--help", "help"}:
|
|
404
|
+
_print_cli_usage()
|
|
405
|
+
return
|
|
406
|
+
if command in {"-v", "--version", "version"}:
|
|
407
|
+
from .. import __version__
|
|
408
|
+
print(__version__)
|
|
409
|
+
return
|
|
410
|
+
if command == "init":
|
|
411
|
+
_cmd_init()
|
|
412
|
+
return
|
|
413
|
+
if command == "config-path":
|
|
414
|
+
print(pai_config.config_file())
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
print(f"未知命令: {args[0]}")
|
|
418
|
+
_print_cli_usage()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _animate_startup():
|
|
422
|
+
"""Claude Code 风格的启动动画"""
|
|
423
|
+
print(heading("╭──────────────────────────────╮"))
|
|
424
|
+
print(heading("│ Vox Code v2.0.0 │"))
|
|
425
|
+
print(heading("│ Web-aware Tool CLI │"))
|
|
426
|
+
print(heading("╰──────────────────────────────╯"))
|
|
427
|
+
dots = ProgressDots("Initializing")
|
|
428
|
+
dots.start()
|
|
429
|
+
import time
|
|
430
|
+
time.sleep(0.4)
|
|
431
|
+
dots.stop(success(" Ready"))
|
|
432
|
+
print(subtle(" /help 查看可用命令, /init 初始化配置, exit 或 /exit 退出"))
|
|
433
|
+
print()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _render_mode_label(mode: str) -> str:
|
|
437
|
+
return "单 Agent" if mode == "single" else "Plan-and-Execute" if mode == "plan" else "多 Agent 团队"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _default_presentation_mode() -> PresentationMode:
|
|
441
|
+
return PresentationMode.normalize(os.environ.get("VOX_CODE_PRESENTATION_MODE", "work"))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _print_presented_result(presenter: ResponsePresenter, presentation_mode: str,
|
|
445
|
+
user_input: str, raw_result: str):
|
|
446
|
+
presented = presenter.present(user_input, raw_result, presentation_mode)
|
|
447
|
+
if presented.display_response:
|
|
448
|
+
print(presented.display_response)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
if __name__ == "__main__":
|
|
452
|
+
main()
|
voxcli/cli/parser.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""CLI 命令解析器 - 解析斜杠命令"""
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ParsedCommand:
|
|
10
|
+
command: str
|
|
11
|
+
args: List[str] = field(default_factory=list)
|
|
12
|
+
raw_args: str = ""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CliCommandParser:
|
|
16
|
+
COMMANDS = {
|
|
17
|
+
"/init": "首次配置模型提供商、API Key 和 URL",
|
|
18
|
+
"/model": "切换 LLM 模型(用法: /model <preset-id|provider[:<model>]>)",
|
|
19
|
+
"/plan": "显示当前执行计划",
|
|
20
|
+
"/team": "切换多 Agent 协作模式",
|
|
21
|
+
"/style": "切换展示模式(work/pet)",
|
|
22
|
+
"/hitl": "设置人工审批模式(auto/always/never)",
|
|
23
|
+
"/policy": "查看或修改安全策略",
|
|
24
|
+
"/audit": "查看审计日志",
|
|
25
|
+
"/index": "索引当前项目代码(供 search_code 检索)",
|
|
26
|
+
"/search": "搜索索引的代码库",
|
|
27
|
+
"/graph": "显示代码关系图",
|
|
28
|
+
"/memory": "查看记忆系统状态",
|
|
29
|
+
"/save": "保存当前对话到文件",
|
|
30
|
+
"/clear": "清空当前对话历史(保留系统提示词)",
|
|
31
|
+
"/context": "查看当前上下文统计",
|
|
32
|
+
"/exit": "退出程序",
|
|
33
|
+
"/help": "显示帮助信息",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def parse(line: str) -> Optional[ParsedCommand]:
|
|
38
|
+
stripped = line.strip()
|
|
39
|
+
if not stripped or not stripped.startswith("/"):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
parts = shlex.split(stripped)
|
|
43
|
+
command = parts[0].lower()
|
|
44
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
45
|
+
|
|
46
|
+
return ParsedCommand(command=command, args=args, raw_args=stripped)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def is_command(line: str) -> bool:
|
|
50
|
+
return line.strip().startswith("/")
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def print_help():
|
|
54
|
+
print("可用命令:")
|
|
55
|
+
print(" /init 初始化模型配置")
|
|
56
|
+
print(" /model <preset-id|provider[:<model>]> 切换模型")
|
|
57
|
+
print(" /plan 查看当前执行计划")
|
|
58
|
+
print(" /team 切换多 Agent 协作模式")
|
|
59
|
+
print(" /style <work|pet> 切换展示模式")
|
|
60
|
+
print(" /hitl <mode> 设置审批模式 (auto/always/never)")
|
|
61
|
+
print(" /policy 查看安全策略")
|
|
62
|
+
print(" /audit 查看审计日志")
|
|
63
|
+
print(" /index 索引项目代码")
|
|
64
|
+
print(" /search <query> 搜索代码库")
|
|
65
|
+
print(" /graph 显示代码关系图")
|
|
66
|
+
print(" /memory 查看记忆状态")
|
|
67
|
+
print(" /save <file> 保存对话到文件")
|
|
68
|
+
print(" /clear 清空对话历史")
|
|
69
|
+
print(" /context 查看上下文统计")
|
|
70
|
+
print(" /exit 退出程序")
|
|
71
|
+
print(" /help 显示此帮助")
|