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/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
|
+
)
|