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/config.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Configuration and user-extensible catalog management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
from .catalog import DEFAULT_CATALOG, load_catalog_from_file, merge_catalog
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_dotenv_files():
|
|
18
|
+
"""Load .env files from cwd and home without overriding real env vars."""
|
|
19
|
+
for env_file in [Path.cwd() / ".env", Path.home() / ".env"]:
|
|
20
|
+
if env_file.exists():
|
|
21
|
+
load_dotenv(env_file, override=False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_load_dotenv_files()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ProviderConfig:
|
|
29
|
+
api_key: str = ""
|
|
30
|
+
base_url: str = ""
|
|
31
|
+
model: str = ""
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict:
|
|
34
|
+
return {"apiKey": self.api_key, "baseUrl": self.base_url, "model": self.model}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, data: dict) -> "ProviderConfig":
|
|
38
|
+
return cls(
|
|
39
|
+
api_key=str(data.get("apiKey", "")),
|
|
40
|
+
base_url=str(data.get("baseUrl", "")),
|
|
41
|
+
model=str(data.get("model", "")),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def get(self, key: str, default: str = "") -> str:
|
|
45
|
+
mapping = {"api_key": self.api_key, "base_url": self.base_url, "model": self.model}
|
|
46
|
+
return mapping.get(key, default)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class GuiModelConfig:
|
|
51
|
+
enabled: bool = False
|
|
52
|
+
provider: str = "glm"
|
|
53
|
+
model: str = ""
|
|
54
|
+
base_url: str = ""
|
|
55
|
+
api_key: str = ""
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"enabled": self.enabled,
|
|
60
|
+
"provider": self.provider,
|
|
61
|
+
"model": self.model,
|
|
62
|
+
"base_url": self.base_url,
|
|
63
|
+
"api_key": self.api_key,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_dict(cls, data: dict) -> "GuiModelConfig":
|
|
68
|
+
return cls(
|
|
69
|
+
enabled=bool(data.get("enabled", False)),
|
|
70
|
+
provider=str(data.get("provider", "glm")).strip().lower() or "glm",
|
|
71
|
+
model=str(data.get("model", "")).strip(),
|
|
72
|
+
base_url=str(data.get("base_url", "")).strip(),
|
|
73
|
+
api_key=str(data.get("api_key", "")).strip(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def provider_config(self) -> ProviderConfig:
|
|
77
|
+
return ProviderConfig(
|
|
78
|
+
api_key=self.api_key,
|
|
79
|
+
base_url=self.base_url,
|
|
80
|
+
model=self.model,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def validate(self) -> list[str]:
|
|
84
|
+
issues: list[str] = []
|
|
85
|
+
if not self.provider:
|
|
86
|
+
issues.append("provider")
|
|
87
|
+
if not self.base_url:
|
|
88
|
+
issues.append("base_url")
|
|
89
|
+
if self.provider != "ollama" and not self.api_key:
|
|
90
|
+
issues.append("api_key")
|
|
91
|
+
return issues
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class ModelPreset:
|
|
96
|
+
id: str
|
|
97
|
+
label: str
|
|
98
|
+
provider: str
|
|
99
|
+
model: str
|
|
100
|
+
description: str = ""
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, data: dict) -> "ModelPreset":
|
|
104
|
+
return cls(
|
|
105
|
+
id=str(data.get("id", "")).strip(),
|
|
106
|
+
label=str(data.get("label", "")).strip() or str(data.get("id", "")).strip(),
|
|
107
|
+
provider=str(data.get("provider", "")).strip().lower(),
|
|
108
|
+
model=str(data.get("model", "")).strip(),
|
|
109
|
+
description=str(data.get("description", "")).strip(),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class VoxCodeConfig:
|
|
114
|
+
CONFIG_DIR = Path.home() / ".vox-code"
|
|
115
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
116
|
+
CATALOG_FILE = CONFIG_DIR / "catalog.json"
|
|
117
|
+
GUI_MODEL_FILE = CONFIG_DIR / "gui-model.json"
|
|
118
|
+
|
|
119
|
+
_PROVIDER_ENV_KEYS = {
|
|
120
|
+
"glm": (("GLM_API_KEY",), ("GLM_MODEL",), ("GLM_BASE_URL",)),
|
|
121
|
+
"deepseek": (("DEEPSEEK_API_KEY",), ("DEEPSEEK_MODEL",), ("DEEPSEEK_BASE_URL",)),
|
|
122
|
+
"qwen": (
|
|
123
|
+
("QWEN_API_KEY", "DASHSCOPE_API_KEY"),
|
|
124
|
+
("QWEN_MODEL", "DASHSCOPE_MODEL"),
|
|
125
|
+
("QWEN_BASE_URL", "DASHSCOPE_BASE_URL"),
|
|
126
|
+
),
|
|
127
|
+
"ollama": ((), ("OLLAMA_MODEL",), ("OLLAMA_BASE_URL",)),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def __init__(self):
|
|
131
|
+
self.default_provider: str = "glm"
|
|
132
|
+
self.providers: dict[str, ProviderConfig] = {}
|
|
133
|
+
self.active_model_preset: str = "glm-5.1"
|
|
134
|
+
self.active_persona: str = "vox"
|
|
135
|
+
self.active_language: str = "zh-CN"
|
|
136
|
+
self.active_skin: str = "glass"
|
|
137
|
+
self.active_pet: str = "terminal-cat"
|
|
138
|
+
self._catalog: dict = merge_catalog(DEFAULT_CATALOG, {})
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def config_dir(cls) -> Path:
|
|
142
|
+
override = os.environ.get("VOX_CODE_HOME", "").strip() or os.environ.get("VOX_HOME", "").strip()
|
|
143
|
+
if override:
|
|
144
|
+
return Path(override).expanduser()
|
|
145
|
+
return Path.home() / ".vox-code"
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def config_file(cls) -> Path:
|
|
149
|
+
return cls.config_dir() / "config.json"
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def catalog_file(cls) -> Path:
|
|
153
|
+
return cls.config_dir() / "catalog.json"
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def gui_model_file(cls) -> Path:
|
|
157
|
+
return cls.config_dir() / "gui-model.json"
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _read_env(keys: str | tuple[str, ...]) -> Optional[str]:
|
|
161
|
+
if isinstance(keys, str):
|
|
162
|
+
keys = (keys,)
|
|
163
|
+
for key in keys:
|
|
164
|
+
val = os.environ.get(key)
|
|
165
|
+
if val and val.strip():
|
|
166
|
+
return val.strip()
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def _ensure_catalog_loaded(self):
|
|
170
|
+
self._catalog = merge_catalog(DEFAULT_CATALOG, load_catalog_from_file(self.catalog_file()))
|
|
171
|
+
|
|
172
|
+
def save(self):
|
|
173
|
+
config_dir = self.config_dir()
|
|
174
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
data = {
|
|
176
|
+
"defaultProvider": self.default_provider,
|
|
177
|
+
"providers": {k: v.to_dict() for k, v in self.providers.items()},
|
|
178
|
+
"activeModelPreset": self.active_model_preset,
|
|
179
|
+
"activePersona": self.active_persona,
|
|
180
|
+
"activeLanguage": self.active_language,
|
|
181
|
+
"activeSkin": self.active_skin,
|
|
182
|
+
"activePet": self.active_pet,
|
|
183
|
+
}
|
|
184
|
+
self.config_file().write_text(
|
|
185
|
+
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def load(cls) -> "VoxCodeConfig":
|
|
190
|
+
cfg = cls()
|
|
191
|
+
config_file = cls.config_file()
|
|
192
|
+
if config_file.exists():
|
|
193
|
+
try:
|
|
194
|
+
data = json.loads(config_file.read_text(encoding="utf-8"))
|
|
195
|
+
cfg.default_provider = str(data.get("defaultProvider", "glm")).strip() or "glm"
|
|
196
|
+
cfg.active_model_preset = str(data.get("activeModelPreset", "glm-5.1")).strip() or "glm-5.1"
|
|
197
|
+
cfg.active_persona = str(data.get("activePersona", "vox")).strip() or "vox"
|
|
198
|
+
cfg.active_language = str(data.get("activeLanguage", "zh-CN")).strip() or "zh-CN"
|
|
199
|
+
cfg.active_skin = str(data.get("activeSkin", "glass")).strip() or "glass"
|
|
200
|
+
cfg.active_pet = str(data.get("activePet", "terminal-cat")).strip() or "terminal-cat"
|
|
201
|
+
for name, pc_data in data.get("providers", {}).items():
|
|
202
|
+
cfg.providers[str(name).lower()] = ProviderConfig.from_dict(pc_data)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"⚠️ 配置文件读取失败: {e}")
|
|
205
|
+
cfg._ensure_catalog_loaded()
|
|
206
|
+
cfg._normalize_active_ids()
|
|
207
|
+
return cfg
|
|
208
|
+
|
|
209
|
+
def reload_catalog(self):
|
|
210
|
+
self._ensure_catalog_loaded()
|
|
211
|
+
self._normalize_active_ids()
|
|
212
|
+
|
|
213
|
+
def _normalize_active_ids(self):
|
|
214
|
+
if self.get_model_preset(self.active_model_preset) is None:
|
|
215
|
+
presets = self.model_presets()
|
|
216
|
+
if presets:
|
|
217
|
+
self.active_model_preset = presets[0].id
|
|
218
|
+
if self.get_persona(self.active_persona) is None:
|
|
219
|
+
personas = self.personas()
|
|
220
|
+
if personas:
|
|
221
|
+
self.active_persona = personas[0]["id"]
|
|
222
|
+
if self.get_language(self.active_language) is None:
|
|
223
|
+
languages = self.languages()
|
|
224
|
+
if languages:
|
|
225
|
+
self.active_language = languages[0]["id"]
|
|
226
|
+
if self.get_skin(self.active_skin) is None:
|
|
227
|
+
skins = self.skins()
|
|
228
|
+
if skins:
|
|
229
|
+
self.active_skin = skins[0]["id"]
|
|
230
|
+
if self.get_builtin_pet(self.active_pet) is None:
|
|
231
|
+
pets = self.builtin_pets()
|
|
232
|
+
if pets:
|
|
233
|
+
self.active_pet = pets[0]["id"]
|
|
234
|
+
|
|
235
|
+
def get_api_key(self, provider: str) -> Optional[str]:
|
|
236
|
+
provider = provider.lower()
|
|
237
|
+
pc = self.providers.get(provider)
|
|
238
|
+
if pc and pc.api_key:
|
|
239
|
+
return pc.api_key
|
|
240
|
+
keys = self._PROVIDER_ENV_KEYS.get(provider)
|
|
241
|
+
if keys and keys[0]:
|
|
242
|
+
return self._read_env(keys[0])
|
|
243
|
+
return ""
|
|
244
|
+
|
|
245
|
+
def get_model(self, provider: str) -> Optional[str]:
|
|
246
|
+
provider = provider.lower()
|
|
247
|
+
pc = self.providers.get(provider)
|
|
248
|
+
if pc and pc.model:
|
|
249
|
+
return pc.model
|
|
250
|
+
keys = self._PROVIDER_ENV_KEYS.get(provider)
|
|
251
|
+
if keys and keys[1]:
|
|
252
|
+
return self._read_env(keys[1])
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def get_base_url(self, provider: str) -> Optional[str]:
|
|
256
|
+
provider = provider.lower()
|
|
257
|
+
pc = self.providers.get(provider)
|
|
258
|
+
if pc and pc.base_url:
|
|
259
|
+
return pc.base_url
|
|
260
|
+
keys = self._PROVIDER_ENV_KEYS.get(provider)
|
|
261
|
+
if keys and keys[2]:
|
|
262
|
+
return self._read_env(keys[2])
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def get_provider(self, name: str) -> Optional[ProviderConfig]:
|
|
266
|
+
name = name.lower()
|
|
267
|
+
pc = self.providers.get(name)
|
|
268
|
+
if pc and pc.api_key and pc.model:
|
|
269
|
+
return pc
|
|
270
|
+
|
|
271
|
+
keys = self._PROVIDER_ENV_KEYS.get(name)
|
|
272
|
+
if keys is None:
|
|
273
|
+
return None
|
|
274
|
+
api_key = self._read_env(keys[0]) if keys[0] else ""
|
|
275
|
+
model = self._read_env(keys[1]) if keys[1] else None
|
|
276
|
+
base_url = self._read_env(keys[2]) if keys[2] else None
|
|
277
|
+
if model or api_key:
|
|
278
|
+
return ProviderConfig(
|
|
279
|
+
api_key=api_key or "",
|
|
280
|
+
base_url=base_url or "",
|
|
281
|
+
model=model or "",
|
|
282
|
+
)
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def default_provider_name(self) -> str:
|
|
287
|
+
env_default = os.environ.get("VOX_CODE_DEFAULT_PROVIDER", "")
|
|
288
|
+
return env_default.strip() or self.default_provider
|
|
289
|
+
|
|
290
|
+
def catalog(self) -> dict:
|
|
291
|
+
return self._catalog
|
|
292
|
+
|
|
293
|
+
def quick_commands(self) -> list[dict]:
|
|
294
|
+
return [dict(item) for item in self._catalog.get("quickCommands", [])]
|
|
295
|
+
|
|
296
|
+
def languages(self) -> list[dict]:
|
|
297
|
+
return [dict(item) for item in self._catalog.get("languages", [])]
|
|
298
|
+
|
|
299
|
+
def get_language(self, language_id: str) -> Optional[dict]:
|
|
300
|
+
for language in self._catalog.get("languages", []):
|
|
301
|
+
if str(language.get("id", "")).strip() == language_id:
|
|
302
|
+
return dict(language)
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def active_language_text(self, key: str, default: str = "") -> str:
|
|
306
|
+
language = self.get_language(self.active_language) or {}
|
|
307
|
+
texts = language.get("texts", {})
|
|
308
|
+
return str(texts.get(key, default))
|
|
309
|
+
|
|
310
|
+
def skins(self) -> list[dict]:
|
|
311
|
+
return [dict(item) for item in self._catalog.get("skins", [])]
|
|
312
|
+
|
|
313
|
+
def get_skin(self, skin_id: str) -> Optional[dict]:
|
|
314
|
+
for skin in self._catalog.get("skins", []):
|
|
315
|
+
if str(skin.get("id", "")).strip() == skin_id:
|
|
316
|
+
return dict(skin)
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def builtin_pets(self) -> list[dict]:
|
|
320
|
+
return [dict(item) for item in self._catalog.get("pets", [])]
|
|
321
|
+
|
|
322
|
+
def get_builtin_pet(self, pet_id: str) -> Optional[dict]:
|
|
323
|
+
for pet in self._catalog.get("pets", []):
|
|
324
|
+
if str(pet.get("id", "")).strip() == pet_id:
|
|
325
|
+
return dict(pet)
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def personas(self) -> list[dict]:
|
|
329
|
+
return [dict(item) for item in self._catalog.get("personas", [])]
|
|
330
|
+
|
|
331
|
+
def get_persona(self, persona_id: str) -> Optional[dict]:
|
|
332
|
+
for persona in self._catalog.get("personas", []):
|
|
333
|
+
if str(persona.get("id", "")).strip() == persona_id:
|
|
334
|
+
return dict(persona)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
def get_active_persona_prompt(self) -> str:
|
|
338
|
+
persona = self.get_persona(self.active_persona)
|
|
339
|
+
if persona:
|
|
340
|
+
return str(persona.get("prompt", "")).strip()
|
|
341
|
+
fallbacks = self.personas()
|
|
342
|
+
if fallbacks:
|
|
343
|
+
return str(fallbacks[0].get("prompt", "")).strip()
|
|
344
|
+
return ""
|
|
345
|
+
|
|
346
|
+
def model_presets(self) -> list[ModelPreset]:
|
|
347
|
+
presets: list[ModelPreset] = []
|
|
348
|
+
for item in self._catalog.get("modelPresets", []):
|
|
349
|
+
preset = ModelPreset.from_dict(item)
|
|
350
|
+
if preset.id and preset.provider:
|
|
351
|
+
presets.append(preset)
|
|
352
|
+
return presets
|
|
353
|
+
|
|
354
|
+
def find_model_preset(self, provider: str, model: str) -> Optional[ModelPreset]:
|
|
355
|
+
normalized_provider = provider.strip().lower()
|
|
356
|
+
normalized_model = model.strip()
|
|
357
|
+
for preset in self.model_presets():
|
|
358
|
+
if preset.provider == normalized_provider and preset.model == normalized_model:
|
|
359
|
+
return preset
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def get_model_preset(self, preset_id: str) -> Optional[ModelPreset]:
|
|
363
|
+
target = preset_id.strip()
|
|
364
|
+
for preset in self.model_presets():
|
|
365
|
+
if preset.id == target:
|
|
366
|
+
return preset
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def resolve_model_selection(self, value: str) -> tuple[str, Optional[str], Optional[ModelPreset]]:
|
|
370
|
+
target = value.strip()
|
|
371
|
+
preset = self.get_model_preset(target)
|
|
372
|
+
if preset is not None:
|
|
373
|
+
return preset.provider, preset.model, preset
|
|
374
|
+
|
|
375
|
+
provider = target
|
|
376
|
+
model_name = None
|
|
377
|
+
if ":" in target:
|
|
378
|
+
provider, model_name = target.split(":", 1)
|
|
379
|
+
return provider.strip().lower(), (model_name or "").strip() or None, None
|
|
380
|
+
|
|
381
|
+
def set_active_model_preset(self, preset_id: str):
|
|
382
|
+
self.active_model_preset = preset_id
|
|
383
|
+
self.save()
|
|
384
|
+
|
|
385
|
+
def set_provider_config(self, provider: str, config: ProviderConfig):
|
|
386
|
+
self.providers[provider.strip().lower()] = config
|
|
387
|
+
|
|
388
|
+
def persist_model_selection(self, provider: str, model: str):
|
|
389
|
+
normalized_provider = provider.strip().lower()
|
|
390
|
+
normalized_model = model.strip()
|
|
391
|
+
if not normalized_provider:
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
provider_config = self.providers.get(normalized_provider, ProviderConfig())
|
|
395
|
+
if normalized_model:
|
|
396
|
+
provider_config.model = normalized_model
|
|
397
|
+
self.providers[normalized_provider] = provider_config
|
|
398
|
+
self.default_provider = normalized_provider
|
|
399
|
+
|
|
400
|
+
if normalized_model:
|
|
401
|
+
preset = self.find_model_preset(normalized_provider, normalized_model)
|
|
402
|
+
if preset is None:
|
|
403
|
+
preset = self._save_custom_model_preset(normalized_provider, normalized_model)
|
|
404
|
+
self.active_model_preset = preset.id
|
|
405
|
+
|
|
406
|
+
self.save()
|
|
407
|
+
|
|
408
|
+
def set_active_persona(self, persona_id: str):
|
|
409
|
+
self.active_persona = persona_id
|
|
410
|
+
self.save()
|
|
411
|
+
|
|
412
|
+
def set_active_language(self, language_id: str):
|
|
413
|
+
self.active_language = language_id
|
|
414
|
+
self.save()
|
|
415
|
+
|
|
416
|
+
def set_active_skin(self, skin_id: str):
|
|
417
|
+
self.active_skin = skin_id
|
|
418
|
+
self.save()
|
|
419
|
+
|
|
420
|
+
def set_active_pet(self, pet_id: str):
|
|
421
|
+
self.active_pet = pet_id
|
|
422
|
+
self.save()
|
|
423
|
+
|
|
424
|
+
def _save_custom_model_preset(self, provider: str, model: str) -> ModelPreset:
|
|
425
|
+
preset = ModelPreset(
|
|
426
|
+
id=f"custom-{provider}-{self._slugify(model)}",
|
|
427
|
+
label=f"{self._provider_label(provider)} {model}",
|
|
428
|
+
provider=provider,
|
|
429
|
+
model=model,
|
|
430
|
+
description="用户自定义模型。",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
override = load_catalog_from_file(self.catalog_file())
|
|
434
|
+
existing_presets = override.get("modelPresets", [])
|
|
435
|
+
if not isinstance(existing_presets, list):
|
|
436
|
+
existing_presets = []
|
|
437
|
+
|
|
438
|
+
updated = False
|
|
439
|
+
merged_presets: list[dict] = []
|
|
440
|
+
for item in existing_presets:
|
|
441
|
+
if isinstance(item, dict) and str(item.get("id", "")).strip() == preset.id:
|
|
442
|
+
merged_presets.append(
|
|
443
|
+
{
|
|
444
|
+
"id": preset.id,
|
|
445
|
+
"label": preset.label,
|
|
446
|
+
"provider": preset.provider,
|
|
447
|
+
"model": preset.model,
|
|
448
|
+
"description": preset.description,
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
updated = True
|
|
452
|
+
else:
|
|
453
|
+
merged_presets.append(item)
|
|
454
|
+
|
|
455
|
+
if not updated:
|
|
456
|
+
merged_presets.append(
|
|
457
|
+
{
|
|
458
|
+
"id": preset.id,
|
|
459
|
+
"label": preset.label,
|
|
460
|
+
"provider": preset.provider,
|
|
461
|
+
"model": preset.model,
|
|
462
|
+
"description": preset.description,
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
override["modelPresets"] = merged_presets
|
|
467
|
+
self.catalog_file().parent.mkdir(parents=True, exist_ok=True)
|
|
468
|
+
self.catalog_file().write_text(
|
|
469
|
+
json.dumps(override, ensure_ascii=False, indent=2),
|
|
470
|
+
encoding="utf-8",
|
|
471
|
+
)
|
|
472
|
+
self.reload_catalog()
|
|
473
|
+
return preset
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def _slugify(value: str) -> str:
|
|
477
|
+
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
478
|
+
return slug or "model"
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _provider_label(provider: str) -> str:
|
|
482
|
+
return {
|
|
483
|
+
"glm": "GLM",
|
|
484
|
+
"deepseek": "DeepSeek",
|
|
485
|
+
"qwen": "Qwen",
|
|
486
|
+
"ollama": "Ollama",
|
|
487
|
+
}.get(provider, provider.upper())
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class GuiModelConfigStore:
|
|
491
|
+
def __init__(self, path: Path | None = None):
|
|
492
|
+
self._path = path or VoxCodeConfig.gui_model_file()
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def path(self) -> Path:
|
|
496
|
+
return self._path
|
|
497
|
+
|
|
498
|
+
def exists(self) -> bool:
|
|
499
|
+
return self.path.exists()
|
|
500
|
+
|
|
501
|
+
def load(self) -> GuiModelConfig:
|
|
502
|
+
if not self.path.exists():
|
|
503
|
+
return GuiModelConfig()
|
|
504
|
+
try:
|
|
505
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
506
|
+
except Exception as exc:
|
|
507
|
+
raise RuntimeError(f"GUI 模型配置读取失败: {exc}") from exc
|
|
508
|
+
return GuiModelConfig.from_dict(data)
|
|
509
|
+
|
|
510
|
+
def save(self, config: GuiModelConfig):
|
|
511
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
512
|
+
self.path.write_text(
|
|
513
|
+
json.dumps(config.to_dict(), ensure_ascii=False, indent=2),
|
|
514
|
+
encoding="utf-8",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
pai_config = VoxCodeConfig.load()
|
voxcli/gui/__main__.py
ADDED
voxcli/gui/main.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PySide6 desktop pet entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
try:
|
|
10
|
+
from .pet_app import run_pet_app
|
|
11
|
+
except ImportError as exc:
|
|
12
|
+
print(
|
|
13
|
+
"PySide6 未安装,无法启动桌宠界面。\n"
|
|
14
|
+
"请先安装 GUI 依赖,例如: pip install 'vox-code[gui]'"
|
|
15
|
+
)
|
|
16
|
+
raise SystemExit(1) from exc
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
raise SystemExit(run_pet_app(sys.argv))
|
|
20
|
+
except RuntimeError as exc:
|
|
21
|
+
print(str(exc))
|
|
22
|
+
raise SystemExit(1) from exc
|
voxcli/gui/pet/base.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Shared Qt helpers for the desktop pet UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
from PySide6.QtGui import QColor, QMouseEvent
|
|
7
|
+
from PySide6.QtWidgets import QGraphicsDropShadowEffect, QMainWindow, QWidget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_shadow(widget: QWidget, blur: int = 36, y: int = 10, alpha: int = 56):
|
|
11
|
+
shadow = QGraphicsDropShadowEffect(widget)
|
|
12
|
+
shadow.setBlurRadius(blur)
|
|
13
|
+
shadow.setOffset(0, y)
|
|
14
|
+
shadow.setColor(QColor(53, 33, 15, alpha))
|
|
15
|
+
widget.setGraphicsEffect(shadow)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def termi_panel_stylesheet(radius: int = 24) -> str:
|
|
19
|
+
return (
|
|
20
|
+
f"background: rgba(10, 12, 16, 244);"
|
|
21
|
+
f"border: 1px solid rgba(72, 76, 84, 220);"
|
|
22
|
+
f"border-radius: {radius}px;"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FramelessToolWindow(QMainWindow):
|
|
27
|
+
"""Shared drag-to-move, hide-on-close behavior for floating panels."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self._drag_offset = None
|
|
32
|
+
self.setWindowFlags(
|
|
33
|
+
Qt.WindowType.FramelessWindowHint
|
|
34
|
+
| Qt.WindowType.Tool
|
|
35
|
+
| Qt.WindowType.WindowStaysOnTopHint
|
|
36
|
+
)
|
|
37
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
38
|
+
|
|
39
|
+
def closeEvent(self, event):
|
|
40
|
+
event.ignore()
|
|
41
|
+
self.hide()
|
|
42
|
+
|
|
43
|
+
def mousePressEvent(self, event: QMouseEvent):
|
|
44
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
45
|
+
self._drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
|
46
|
+
event.accept()
|
|
47
|
+
return
|
|
48
|
+
super().mousePressEvent(event)
|
|
49
|
+
|
|
50
|
+
def mouseMoveEvent(self, event: QMouseEvent):
|
|
51
|
+
if self._drag_offset is not None and event.buttons() & Qt.MouseButton.LeftButton:
|
|
52
|
+
self.move(event.globalPosition().toPoint() - self._drag_offset)
|
|
53
|
+
event.accept()
|
|
54
|
+
return
|
|
55
|
+
super().mouseMoveEvent(event)
|
|
56
|
+
|
|
57
|
+
def mouseReleaseEvent(self, event: QMouseEvent):
|
|
58
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
59
|
+
self._drag_offset = None
|
|
60
|
+
event.accept()
|
|
61
|
+
return
|
|
62
|
+
super().mouseReleaseEvent(event)
|