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/gui/pet/data.py ADDED
@@ -0,0 +1,430 @@
1
+ """Configuration models and persistent state for the desktop pet UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import shutil
11
+
12
+ from PySide6.QtGui import QColor
13
+
14
+ from ...chat import ChatAttachment
15
+ from ...config import GuiModelConfig, pai_config
16
+ from ...llm.factory import default_model_for
17
+
18
+
19
+ @dataclass
20
+ class ChatMessage:
21
+ role: str
22
+ text: str
23
+ attachments: tuple[ChatAttachment, ...] = field(default_factory=tuple)
24
+
25
+
26
+ class PendingAttachmentStore:
27
+ def __init__(self):
28
+ self._attachments: list[ChatAttachment] = []
29
+
30
+ def attachments(self) -> tuple[ChatAttachment, ...]:
31
+ return tuple(self._attachments)
32
+
33
+ def clear(self):
34
+ self._attachments.clear()
35
+
36
+ def remove(self, attachment_id: str):
37
+ self._attachments = [item for item in self._attachments if item.id != attachment_id]
38
+
39
+ def reorder(self, ordered_ids: list[str]):
40
+ lookup = {item.id: item for item in self._attachments}
41
+ reordered = [lookup[item_id] for item_id in ordered_ids if item_id in lookup]
42
+ if len(reordered) == len(self._attachments):
43
+ self._attachments = reordered
44
+
45
+ def add_files(self, paths: list[str]) -> list[ChatAttachment]:
46
+ added: list[ChatAttachment] = []
47
+ existing_paths = {item.file_path for item in self._attachments}
48
+ for path in paths:
49
+ attachment = ChatAttachment.from_path(path)
50
+ if attachment.file_path in existing_paths:
51
+ continue
52
+ self._attachments.append(attachment)
53
+ existing_paths.add(attachment.file_path)
54
+ added.append(attachment)
55
+ return added
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class PetPackage:
60
+ id: str
61
+ display_name: str
62
+ description: str
63
+ kind: str
64
+ spritesheet_path: str = ""
65
+
66
+ @property
67
+ def has_spritesheet(self) -> bool:
68
+ return self.kind == "sprite" and bool(self.spritesheet_path)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class SkinPalette:
73
+ id: str
74
+ bubble_bg_start: str
75
+ bubble_bg_end: str
76
+ bubble_border: str
77
+ bubble_text: str
78
+ panel_gradient_start: str
79
+ panel_gradient_mid: str
80
+ panel_gradient_end: str
81
+ panel_border: str
82
+ panel_text: str
83
+ input_bg: str
84
+ input_border: str
85
+ secondary_text: str
86
+ primary_button_start: str
87
+ primary_button_end: str
88
+ primary_button_border: str
89
+ secondary_button_bg: str
90
+ secondary_button_text: str
91
+ close_button_bg: str
92
+ close_button_border: str
93
+ close_button_text: str
94
+ toolbar_bg_start: str
95
+ toolbar_bg_end: str
96
+ toolbar_border: str
97
+ pet_shadow: QColor
98
+ pet_glow: QColor
99
+ pet_base: QColor
100
+ pet_outline: QColor
101
+ pet_blush: QColor
102
+ pet_eye: QColor
103
+ pet_nose: QColor
104
+ pet_mouth: QColor
105
+ pet_charm: QColor
106
+ pet_highlight: QColor
107
+ mode_bg: str
108
+ mode_border: str
109
+ mode_text: str
110
+
111
+
112
+ _RGBA_RE = re.compile(r"rgba?\(([^)]+)\)")
113
+
114
+
115
+ def _parse_qcolor(value: str) -> QColor:
116
+ text = str(value).strip()
117
+ match = _RGBA_RE.fullmatch(text)
118
+ if match:
119
+ parts = [int(float(part.strip())) for part in match.group(1).split(",")]
120
+ if len(parts) == 3:
121
+ return QColor(parts[0], parts[1], parts[2])
122
+ if len(parts) >= 4:
123
+ return QColor(parts[0], parts[1], parts[2], parts[3])
124
+ color = QColor(text)
125
+ return color if color.isValid() else QColor(255, 255, 255)
126
+
127
+
128
+ def _load_skin_palettes() -> dict[str, SkinPalette]:
129
+ palettes: dict[str, SkinPalette] = {}
130
+ for skin in pai_config.skins():
131
+ skin_id = str(skin.get("id", "")).strip()
132
+ if not skin_id:
133
+ continue
134
+ palettes[skin_id] = SkinPalette(
135
+ id=skin_id,
136
+ bubble_bg_start=str(skin.get("bubble_bg_start", "")),
137
+ bubble_bg_end=str(skin.get("bubble_bg_end", "")),
138
+ bubble_border=str(skin.get("bubble_border", "")),
139
+ bubble_text=str(skin.get("bubble_text", "")),
140
+ panel_gradient_start=str(skin.get("panel_gradient_start", "")),
141
+ panel_gradient_mid=str(skin.get("panel_gradient_mid", "")),
142
+ panel_gradient_end=str(skin.get("panel_gradient_end", "")),
143
+ panel_border=str(skin.get("panel_border", "")),
144
+ panel_text=str(skin.get("panel_text", "")),
145
+ input_bg=str(skin.get("input_bg", "")),
146
+ input_border=str(skin.get("input_border", "")),
147
+ secondary_text=str(skin.get("secondary_text", "")),
148
+ primary_button_start=str(skin.get("primary_button_start", "")),
149
+ primary_button_end=str(skin.get("primary_button_end", "")),
150
+ primary_button_border=str(skin.get("primary_button_border", "")),
151
+ secondary_button_bg=str(skin.get("secondary_button_bg", "")),
152
+ secondary_button_text=str(skin.get("secondary_button_text", "")),
153
+ close_button_bg=str(skin.get("close_button_bg", "")),
154
+ close_button_border=str(skin.get("close_button_border", "")),
155
+ close_button_text=str(skin.get("close_button_text", "")),
156
+ toolbar_bg_start=str(skin.get("toolbar_bg_start", "")),
157
+ toolbar_bg_end=str(skin.get("toolbar_bg_end", "")),
158
+ toolbar_border=str(skin.get("toolbar_border", "")),
159
+ pet_shadow=_parse_qcolor(str(skin.get("pet_shadow", "#ffffff"))),
160
+ pet_glow=_parse_qcolor(str(skin.get("pet_glow", "#ffffff"))),
161
+ pet_base=_parse_qcolor(str(skin.get("pet_base", "#ffffff"))),
162
+ pet_outline=_parse_qcolor(str(skin.get("pet_outline", "#ffffff"))),
163
+ pet_blush=_parse_qcolor(str(skin.get("pet_blush", "#ffffff"))),
164
+ pet_eye=_parse_qcolor(str(skin.get("pet_eye", "#000000"))),
165
+ pet_nose=_parse_qcolor(str(skin.get("pet_nose", "#000000"))),
166
+ pet_mouth=_parse_qcolor(str(skin.get("pet_mouth", "#000000"))),
167
+ pet_charm=_parse_qcolor(str(skin.get("pet_charm", "#ffffff"))),
168
+ pet_highlight=_parse_qcolor(str(skin.get("pet_highlight", "#ffffff"))),
169
+ mode_bg=str(skin.get("mode_bg", "")),
170
+ mode_border=str(skin.get("mode_border", "")),
171
+ mode_text=str(skin.get("mode_text", "")),
172
+ )
173
+ return palettes
174
+
175
+
176
+ def _load_builtin_pets() -> list[PetPackage]:
177
+ packages: list[PetPackage] = []
178
+ for pet in pai_config.builtin_pets():
179
+ pet_id = str(pet.get("id", "")).strip()
180
+ if not pet_id:
181
+ continue
182
+ packages.append(
183
+ PetPackage(
184
+ id=pet_id,
185
+ display_name=str(pet.get("displayName", pet_id)).strip() or pet_id,
186
+ description=str(pet.get("description", "")).strip(),
187
+ kind=str(pet.get("kind", "drawn")).strip() or "drawn",
188
+ spritesheet_path=str(pet.get("spritesheetPath", "")).strip(),
189
+ )
190
+ )
191
+ return packages or [
192
+ PetPackage("terminal-cat", "Terminal Cat", "默认吉祥物,小猫常驻终端旁边。", "drawn")
193
+ ]
194
+
195
+
196
+ SKIN_PALETTES = _load_skin_palettes()
197
+ BUILTIN_PETS = _load_builtin_pets()
198
+ if not SKIN_PALETTES:
199
+ SKIN_PALETTES = {
200
+ "glass": SkinPalette(
201
+ id="glass",
202
+ bubble_bg_start="rgba(255,250,243,245)",
203
+ bubble_bg_end="rgba(248,233,209,235)",
204
+ bubble_border="rgba(211,180,138,205)",
205
+ bubble_text="#4c3825",
206
+ panel_gradient_start="rgba(255,250,243,244)",
207
+ panel_gradient_mid="rgba(250,238,222,236)",
208
+ panel_gradient_end="rgba(238,247,240,228)",
209
+ panel_border="rgba(219,193,160,210)",
210
+ panel_text="#443527",
211
+ input_bg="rgba(255,252,247,224)",
212
+ input_border="rgba(216,199,175,235)",
213
+ secondary_text="rgba(87,72,51,204)",
214
+ primary_button_start="#f0b45b",
215
+ primary_button_end="#df8c42",
216
+ primary_button_border="rgba(197,122,44,242)",
217
+ secondary_button_bg="rgba(255,249,240,209)",
218
+ secondary_button_text="#6a5336",
219
+ close_button_bg="rgba(255,244,238,204)",
220
+ close_button_border="rgba(223,165,145,235)",
221
+ close_button_text="#a04b3b",
222
+ toolbar_bg_start="rgba(255,250,243,240)",
223
+ toolbar_bg_end="rgba(244,232,214,232)",
224
+ toolbar_border="rgba(214,189,156,205)",
225
+ pet_shadow=QColor(66, 53, 40, 34),
226
+ pet_glow=QColor(255, 228, 170, 26),
227
+ pet_base=QColor(251, 247, 241),
228
+ pet_outline=QColor(218, 204, 186),
229
+ pet_blush=QColor(255, 214, 205, 120),
230
+ pet_eye=QColor(61, 64, 72),
231
+ pet_nose=QColor(245, 186, 172),
232
+ pet_mouth=QColor(97, 82, 70),
233
+ pet_charm=QColor(255, 242, 218),
234
+ pet_highlight=QColor(255, 252, 247, 88),
235
+ mode_bg="rgba(255,248,239,210)",
236
+ mode_border="rgba(211,180,138,180)",
237
+ mode_text="#6b5a41",
238
+ )
239
+ }
240
+
241
+
242
+ GUI_MODEL_PROFILES = (
243
+ {
244
+ "id": "codex",
245
+ "label": "OpenAI Compatible",
246
+ "summary": "适合任何 OpenAI 兼容网关,可直接自定义 model。",
247
+ },
248
+ {
249
+ "id": "qwen",
250
+ "label": "Qwen API",
251
+ "summary": "阿里云百炼兼容模式,可直接填写 qwen-* 模型名。",
252
+ },
253
+ )
254
+
255
+
256
+ def gui_model_profile_meta(profile_id: str) -> dict[str, str]:
257
+ normalized = normalize_gui_model_provider(profile_id)
258
+ for profile in GUI_MODEL_PROFILES:
259
+ if profile["id"] == normalized:
260
+ return dict(profile)
261
+ return dict(GUI_MODEL_PROFILES[0])
262
+
263
+
264
+ def gui_model_profile_label(profile_id: str) -> str:
265
+ normalized = normalize_gui_model_provider(profile_id)
266
+ for profile in GUI_MODEL_PROFILES:
267
+ if profile["id"] == normalized:
268
+ return profile["label"]
269
+ return profile_id
270
+
271
+
272
+ def normalize_gui_model_provider(value: str) -> str:
273
+ normalized = value.strip().lower().replace("_", "-")
274
+ aliases = {
275
+ "openai": "codex",
276
+ "openai-compatible": "codex",
277
+ "openai-compatible-api": "codex",
278
+ "claude": "codex",
279
+ "claudecode": "codex",
280
+ "claude-code": "codex",
281
+ "codex": "codex",
282
+ "qwen": "qwen",
283
+ "dashscope": "qwen",
284
+ }
285
+ return aliases.get(normalized, "codex")
286
+
287
+
288
+ def load_gui_model_config_from_json(
289
+ text: str,
290
+ current: GuiModelConfig | None = None,
291
+ ) -> GuiModelConfig:
292
+ fallback = current or GuiModelConfig(enabled=True)
293
+ try:
294
+ payload = json.loads(text)
295
+ except json.JSONDecodeError as exc:
296
+ raise ValueError(f"JSON 解析失败: {exc}") from exc
297
+
298
+ if not isinstance(payload, dict):
299
+ raise ValueError("JSON 顶层必须是对象。")
300
+
301
+ scoped = payload
302
+ for key in ("guiModel", "modelConfig", "config"):
303
+ nested = scoped.get(key)
304
+ if isinstance(nested, dict):
305
+ scoped = nested
306
+ break
307
+
308
+ provider = normalize_gui_model_provider(
309
+ str(
310
+ scoped.get("provider")
311
+ or scoped.get("profile")
312
+ or scoped.get("type")
313
+ or fallback.provider
314
+ or "codex"
315
+ )
316
+ )
317
+ enabled_raw = scoped.get("enabled", True)
318
+ enabled = (
319
+ enabled_raw
320
+ if isinstance(enabled_raw, bool)
321
+ else str(enabled_raw).strip().lower() not in {"0", "false", "no"}
322
+ )
323
+ model = str(scoped.get("model", "")).strip() or fallback.model or default_model_for(provider)
324
+ base_url = normalize_gui_model_base_url(
325
+ str(scoped.get("baseUrl", scoped.get("base_url", fallback.base_url))).strip()
326
+ )
327
+ api_key = str(scoped.get("apiKey", scoped.get("api_key", fallback.api_key))).strip()
328
+ return GuiModelConfig(
329
+ enabled=enabled,
330
+ provider=provider,
331
+ model=model,
332
+ base_url=base_url,
333
+ api_key=api_key,
334
+ )
335
+
336
+
337
+ def normalize_gui_model_base_url(value: str) -> str:
338
+ base_url = value.strip()
339
+ if not base_url:
340
+ return ""
341
+ normalized = base_url.rstrip("/")
342
+ lower = normalized.lower()
343
+ if lower.endswith("/chat/completions") or lower.endswith("/beta/chat/completions"):
344
+ return normalized
345
+ return normalized + "/chat/completions"
346
+
347
+
348
+ def gui_state_root() -> Path:
349
+ override = os.environ.get("VOX_CODE_HOME", "").strip() or os.environ.get("VOX_HOME", "").strip()
350
+ if override:
351
+ return Path(override).expanduser()
352
+ return Path.home() / ".vox-code"
353
+
354
+
355
+ class GuiStateStore:
356
+ def load_skin(self) -> str:
357
+ skin = str(pai_config.active_skin).strip().lower()
358
+ return skin if skin in SKIN_PALETTES else "glass"
359
+
360
+ def save_skin(self, skin: str):
361
+ pai_config.set_active_skin(skin)
362
+
363
+ def load_selected_pet(self) -> str:
364
+ return str(pai_config.active_pet).strip() or "terminal-cat"
365
+
366
+ def save_selected_pet(self, pet_id: str):
367
+ pai_config.set_active_pet(pet_id)
368
+
369
+
370
+ class PetPackageStore:
371
+ def __init__(self):
372
+ self._root = gui_state_root() / "ImportedPets"
373
+
374
+ @property
375
+ def root(self) -> Path:
376
+ self._root.mkdir(parents=True, exist_ok=True)
377
+ return self._root
378
+
379
+ def list_imported(self) -> list[PetPackage]:
380
+ packages: list[PetPackage] = []
381
+ for child in sorted(self.root.iterdir()):
382
+ if not child.is_dir():
383
+ continue
384
+ package = self._load_from_dir(child)
385
+ if package is not None:
386
+ packages.append(package)
387
+ return packages
388
+
389
+ def import_folder(self, folder: str) -> PetPackage:
390
+ source = Path(folder).expanduser().resolve()
391
+ package = self._load_from_dir(source)
392
+ if package is None:
393
+ raise ValueError("所选文件夹不是有效的宠物包,必须包含 pet.json 和 spritesheet.webp")
394
+
395
+ destination = self.root / package.id
396
+ if destination.exists():
397
+ shutil.rmtree(destination)
398
+ shutil.copytree(source, destination)
399
+ imported = self._load_from_dir(destination)
400
+ if imported is None:
401
+ raise ValueError("导入后无法读取宠物包")
402
+ return imported
403
+
404
+ def _load_from_dir(self, folder: Path) -> PetPackage | None:
405
+ config_path = folder / "pet.json"
406
+ if not config_path.exists():
407
+ return None
408
+ try:
409
+ data = json.loads(config_path.read_text(encoding="utf-8"))
410
+ except Exception:
411
+ return None
412
+
413
+ spritesheet_name = str(data.get("spritesheetPath", "spritesheet.webp")).strip() or "spritesheet.webp"
414
+ sprite_path = folder / spritesheet_name
415
+ if not sprite_path.exists():
416
+ fallback = folder / "spritesheet.webp"
417
+ if not fallback.exists():
418
+ return None
419
+ sprite_path = fallback
420
+
421
+ pet_id = str(data.get("id", folder.name)).strip() or folder.name
422
+ display_name = str(data.get("displayName", pet_id)).strip() or pet_id
423
+ description = str(data.get("description", "")).strip()
424
+ return PetPackage(
425
+ id=pet_id,
426
+ display_name=display_name,
427
+ description=description,
428
+ kind="sprite",
429
+ spritesheet_path=str(sprite_path),
430
+ )