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.
Files changed (88) hide show
  1. vox_code-2.0.0.dist-info/METADATA +258 -0
  2. vox_code-2.0.0.dist-info/RECORD +88 -0
  3. vox_code-2.0.0.dist-info/WHEEL +4 -0
  4. vox_code-2.0.0.dist-info/entry_points.txt +3 -0
  5. voxcli/__init__.py +3 -0
  6. voxcli/__main__.py +5 -0
  7. voxcli/agent/__init__.py +12 -0
  8. voxcli/agent/agent.py +449 -0
  9. voxcli/agent/agent_budget.py +133 -0
  10. voxcli/agent/agent_orchestrator.py +414 -0
  11. voxcli/agent/plan_execute_agent.py +514 -0
  12. voxcli/agent/roles.py +80 -0
  13. voxcli/agent/sub_agent.py +351 -0
  14. voxcli/catalog.py +477 -0
  15. voxcli/chat.py +91 -0
  16. voxcli/cli/__init__.py +4 -0
  17. voxcli/cli/main.py +452 -0
  18. voxcli/cli/parser.py +71 -0
  19. voxcli/config.py +518 -0
  20. voxcli/gui/__main__.py +3 -0
  21. voxcli/gui/main.py +22 -0
  22. voxcli/gui/pet/__init__.py +5 -0
  23. voxcli/gui/pet/base.py +62 -0
  24. voxcli/gui/pet/coordinator.py +888 -0
  25. voxcli/gui/pet/data.py +430 -0
  26. voxcli/gui/pet/widgets.py +683 -0
  27. voxcli/gui/pet/windows.py +2298 -0
  28. voxcli/gui/pet/workers.py +54 -0
  29. voxcli/gui/pet_app.py +7 -0
  30. voxcli/hitl/__init__.py +11 -0
  31. voxcli/hitl/handler.py +11 -0
  32. voxcli/hitl/policy.py +32 -0
  33. voxcli/hitl/request.py +13 -0
  34. voxcli/hitl/result.py +11 -0
  35. voxcli/hitl/terminal_handler.py +64 -0
  36. voxcli/hitl/tool_registry.py +64 -0
  37. voxcli/llm/base.py +93 -0
  38. voxcli/llm/factory.py +178 -0
  39. voxcli/llm/ollama_client.py +137 -0
  40. voxcli/llm/openai_compatible.py +249 -0
  41. voxcli/memory/base.py +16 -0
  42. voxcli/memory/budget.py +53 -0
  43. voxcli/memory/compressor.py +198 -0
  44. voxcli/memory/entry.py +36 -0
  45. voxcli/memory/long_term.py +126 -0
  46. voxcli/memory/manager.py +101 -0
  47. voxcli/memory/retriever.py +72 -0
  48. voxcli/memory/short_term.py +84 -0
  49. voxcli/memory/tokenizer.py +21 -0
  50. voxcli/plan/__init__.py +5 -0
  51. voxcli/plan/execution_plan.py +225 -0
  52. voxcli/plan/planner.py +198 -0
  53. voxcli/plan/task.py +123 -0
  54. voxcli/policy/audit_log.py +111 -0
  55. voxcli/policy/command_guard.py +34 -0
  56. voxcli/policy/exception.py +5 -0
  57. voxcli/policy/path_guard.py +32 -0
  58. voxcli/prompting/__init__.py +7 -0
  59. voxcli/prompting/presenter.py +154 -0
  60. voxcli/rag/__init__.py +16 -0
  61. voxcli/rag/analyzer.py +89 -0
  62. voxcli/rag/chunk.py +17 -0
  63. voxcli/rag/chunker.py +137 -0
  64. voxcli/rag/embedding.py +75 -0
  65. voxcli/rag/formatter.py +40 -0
  66. voxcli/rag/index.py +96 -0
  67. voxcli/rag/relation.py +14 -0
  68. voxcli/rag/retriever.py +58 -0
  69. voxcli/rag/store.py +155 -0
  70. voxcli/rag/tokenizer.py +26 -0
  71. voxcli/runtime/__init__.py +6 -0
  72. voxcli/runtime/session_controller.py +386 -0
  73. voxcli/tool/__init__.py +3 -0
  74. voxcli/tool/tool_registry.py +433 -0
  75. voxcli/util/animation.py +219 -0
  76. voxcli/util/ansi.py +82 -0
  77. voxcli/util/markdown.py +98 -0
  78. voxcli/web/__init__.py +17 -0
  79. voxcli/web/base.py +20 -0
  80. voxcli/web/extractor.py +77 -0
  81. voxcli/web/factory.py +38 -0
  82. voxcli/web/fetch_result.py +27 -0
  83. voxcli/web/fetcher.py +42 -0
  84. voxcli/web/network_policy.py +49 -0
  85. voxcli/web/result.py +23 -0
  86. voxcli/web/searxng.py +55 -0
  87. voxcli/web/serpapi.py +53 -0
  88. 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
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ main()
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
@@ -0,0 +1,5 @@
1
+ """Desktop pet GUI modules."""
2
+
3
+ from .coordinator import PetCoordinator, run_pet_app
4
+
5
+ __all__ = ["PetCoordinator", "run_pet_app"]
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)