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
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
"""Top-level coordinator for the desktop pet UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QPoint, QRect, Qt, QTimer
|
|
8
|
+
from PySide6.QtGui import QAction, QColor, QIcon, QPainter, QPen, QPixmap, QPolygon
|
|
9
|
+
from PySide6.QtWidgets import QApplication, QFileDialog, QMenu, QSystemTrayIcon, QWidget
|
|
10
|
+
|
|
11
|
+
from ...chat import GuiChatSubmission
|
|
12
|
+
from ...config import GuiModelConfig, GuiModelConfigStore, pai_config
|
|
13
|
+
from ...llm.factory import create_from_config, create_from_provider_config
|
|
14
|
+
from ...runtime import SessionController, SessionReply
|
|
15
|
+
from .data import (
|
|
16
|
+
BUILTIN_PETS,
|
|
17
|
+
SKIN_PALETTES,
|
|
18
|
+
ChatMessage,
|
|
19
|
+
GuiStateStore,
|
|
20
|
+
PetPackage,
|
|
21
|
+
PetPackageStore,
|
|
22
|
+
gui_model_profile_label,
|
|
23
|
+
)
|
|
24
|
+
from .widgets import PetWidget
|
|
25
|
+
from .windows import ChatWindow, CommandWindow, FloatingActionBar, GuiModelSettingsWindow, PetManagerWindow, PersonalityWindow, StatusCardWidget
|
|
26
|
+
from .workers import SessionWorker
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PetCoordinator(QWidget):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self._gui_model_store = GuiModelConfigStore()
|
|
33
|
+
self._gui_model_config = GuiModelConfig()
|
|
34
|
+
self._gui_model_source = "跟随全局"
|
|
35
|
+
self._gui_model_warning = ""
|
|
36
|
+
self.controller = SessionController(
|
|
37
|
+
llm_client=self._build_initial_gui_llm_client(),
|
|
38
|
+
allow_model_switch_commands=False,
|
|
39
|
+
)
|
|
40
|
+
self._state_store = GuiStateStore()
|
|
41
|
+
self._pet_store = PetPackageStore()
|
|
42
|
+
self._settings_window: GuiModelSettingsWindow | None = None
|
|
43
|
+
self._pet_manager: PetManagerWindow | None = None
|
|
44
|
+
self._personality_window: PersonalityWindow | None = None
|
|
45
|
+
self.current_skin = self._state_store.load_skin()
|
|
46
|
+
self.current_pet_id = self._state_store.load_selected_pet()
|
|
47
|
+
self.current_language = pai_config.active_language
|
|
48
|
+
self._pets: list[PetPackage] = []
|
|
49
|
+
self.worker: SessionWorker | None = None
|
|
50
|
+
self._toolbar_hide_timer = QTimer(self)
|
|
51
|
+
self._toolbar_hide_timer.setSingleShot(True)
|
|
52
|
+
self._toolbar_hide_timer.setInterval(220)
|
|
53
|
+
self._toolbar_hide_timer.timeout.connect(self._hide_toolbar_if_idle)
|
|
54
|
+
|
|
55
|
+
self.pet = PetWidget(self.controller)
|
|
56
|
+
self.chat = ChatWindow(self.controller)
|
|
57
|
+
self.commands = CommandWindow()
|
|
58
|
+
self.toolbar = FloatingActionBar()
|
|
59
|
+
self.status_card = StatusCardWidget()
|
|
60
|
+
self._pet_state = "idle"
|
|
61
|
+
self.pet.toggled.connect(self.toggle_chat)
|
|
62
|
+
self.pet.request_quit.connect(QApplication.instance().quit)
|
|
63
|
+
self.pet.hover_changed.connect(self._on_pet_hover_changed)
|
|
64
|
+
self.pet.position_changed.connect(self._sync_toolbar_position)
|
|
65
|
+
self.pet.position_changed.connect(self._sync_status_card_position)
|
|
66
|
+
self.pet.cycle_pet_requested.connect(self._cycle_pet_action)
|
|
67
|
+
self.pet.context_menu_requested.connect(self._show_pet_context_menu)
|
|
68
|
+
self.chat.submitted.connect(self.submit_line)
|
|
69
|
+
self.chat.attachment_error.connect(self._handle_attachment_error)
|
|
70
|
+
self.commands.submitted.connect(self.submit_line)
|
|
71
|
+
self.toolbar.chat_requested.connect(self.toggle_chat)
|
|
72
|
+
self.toolbar.commands_requested.connect(self.toggle_commands)
|
|
73
|
+
self.toolbar.hover_changed.connect(self._on_toolbar_hover_changed)
|
|
74
|
+
|
|
75
|
+
self.status_card.set_status(
|
|
76
|
+
mode=self.controller.mode,
|
|
77
|
+
model_provider=self.controller.provider_name,
|
|
78
|
+
model_name=self.controller.model_name,
|
|
79
|
+
state=self._pet_state,
|
|
80
|
+
)
|
|
81
|
+
self._reload_pets()
|
|
82
|
+
self._apply_pet(self.current_pet_id, announce=False)
|
|
83
|
+
|
|
84
|
+
self.tray = QSystemTrayIcon(self._build_tray_icon(), self)
|
|
85
|
+
self.tray.setToolTip("Vox Pet")
|
|
86
|
+
self.tray.activated.connect(self._on_tray_activated)
|
|
87
|
+
self._tray_menu = self._build_tray_menu()
|
|
88
|
+
self.tray.setContextMenu(self._tray_menu)
|
|
89
|
+
self.tray.show()
|
|
90
|
+
|
|
91
|
+
self._place_windows()
|
|
92
|
+
self._apply_skin(self.current_skin)
|
|
93
|
+
self._apply_language(self.current_language, announce=False)
|
|
94
|
+
self.pet.show()
|
|
95
|
+
self._sync_toolbar_position()
|
|
96
|
+
self.commands.set_quick_commands(pai_config.quick_commands())
|
|
97
|
+
self._sync_reply_state(SessionReply(text="", mode=self.controller.mode, presentation_mode=self.controller.presentation_mode))
|
|
98
|
+
self.pet.speak(self._text("ready_bubble", "主人,我已经准备好了。"))
|
|
99
|
+
QTimer.singleShot(160, self._initial_reveal)
|
|
100
|
+
|
|
101
|
+
def toggle_chat(self):
|
|
102
|
+
if self.chat.isVisible():
|
|
103
|
+
self.chat.hide()
|
|
104
|
+
self.toolbar.set_active_panel("")
|
|
105
|
+
return
|
|
106
|
+
self.commands.hide()
|
|
107
|
+
self._position_floating_panel(self.chat, x_shift=120, y_gap=34)
|
|
108
|
+
self.chat.show()
|
|
109
|
+
self.chat.raise_()
|
|
110
|
+
self.chat.focus_input()
|
|
111
|
+
self.toolbar.set_active_panel("chat")
|
|
112
|
+
self._show_toolbar()
|
|
113
|
+
|
|
114
|
+
def toggle_commands(self):
|
|
115
|
+
if self.commands.isVisible():
|
|
116
|
+
self.commands.hide()
|
|
117
|
+
self.toolbar.set_active_panel("")
|
|
118
|
+
return
|
|
119
|
+
self.chat.hide()
|
|
120
|
+
self._position_floating_panel(self.commands, x_shift=92, y_gap=34)
|
|
121
|
+
self.commands.show()
|
|
122
|
+
self.commands.raise_()
|
|
123
|
+
self.toolbar.set_active_panel("commands")
|
|
124
|
+
self._show_toolbar()
|
|
125
|
+
|
|
126
|
+
def reveal_pet(self, reset_position: bool = False):
|
|
127
|
+
if reset_position:
|
|
128
|
+
self._place_windows()
|
|
129
|
+
self._sync_toolbar_position()
|
|
130
|
+
self._sync_status_card_position()
|
|
131
|
+
self.pet.show()
|
|
132
|
+
self.pet.raise_()
|
|
133
|
+
self.pet.speak(self._text("reveal_bubble", "主人,我在这里。"))
|
|
134
|
+
|
|
135
|
+
def submit_line(self, line: str | GuiChatSubmission):
|
|
136
|
+
if self.worker is not None and self.worker.isRunning():
|
|
137
|
+
return
|
|
138
|
+
submission = line if isinstance(line, GuiChatSubmission) else GuiChatSubmission(text=line)
|
|
139
|
+
if not self.chat.isVisible():
|
|
140
|
+
self._position_floating_panel(self.chat, x_shift=120, y_gap=34)
|
|
141
|
+
self.chat.show()
|
|
142
|
+
self.chat.raise_()
|
|
143
|
+
self.chat.focus_input()
|
|
144
|
+
self.toolbar.set_active_panel("chat")
|
|
145
|
+
self.chat.append_message(
|
|
146
|
+
ChatMessage("user", submission.text, attachments=submission.attachments)
|
|
147
|
+
)
|
|
148
|
+
self.chat.set_busy(True)
|
|
149
|
+
self._pet_state = "thinking"
|
|
150
|
+
self.pet.set_thinking(True)
|
|
151
|
+
self._update_status_card(state="thinking")
|
|
152
|
+
self.worker = SessionWorker(self.controller, submission)
|
|
153
|
+
self.worker.completed.connect(self._handle_reply)
|
|
154
|
+
self.worker.failed.connect(self._handle_failure)
|
|
155
|
+
self.worker.start()
|
|
156
|
+
|
|
157
|
+
def _handle_reply(self, reply: SessionReply):
|
|
158
|
+
submitted_line = ""
|
|
159
|
+
if self.worker is not None:
|
|
160
|
+
submitted = self.worker._line
|
|
161
|
+
submitted_line = submitted.text.strip() if isinstance(submitted, GuiChatSubmission) else submitted.strip()
|
|
162
|
+
self.chat.set_busy(False)
|
|
163
|
+
self.pet.set_thinking(False)
|
|
164
|
+
self._sync_reply_state(reply)
|
|
165
|
+
|
|
166
|
+
role = "error" if reply.kind == "error" else "assistant"
|
|
167
|
+
if submitted_line == "/clear" and reply.kind != "error":
|
|
168
|
+
self.chat.clear_messages()
|
|
169
|
+
self._pet_state = "celebrate"
|
|
170
|
+
self.pet.speak(reply.text)
|
|
171
|
+
self.pet.celebrate()
|
|
172
|
+
self._update_status_card(state="celebrate", bubble_text=reply.text)
|
|
173
|
+
self.worker = None
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if reply.text:
|
|
177
|
+
if role == "assistant":
|
|
178
|
+
self.chat.start_streaming_message(ChatMessage(role, reply.text))
|
|
179
|
+
else:
|
|
180
|
+
self.chat.append_message(ChatMessage(role, reply.text))
|
|
181
|
+
self.pet.speak(reply.text)
|
|
182
|
+
if reply.kind == "assistant":
|
|
183
|
+
self._pet_state = "celebrate"
|
|
184
|
+
self.pet.celebrate()
|
|
185
|
+
elif reply.kind == "error":
|
|
186
|
+
self._pet_state = "error"
|
|
187
|
+
self.pet.error()
|
|
188
|
+
else:
|
|
189
|
+
self._pet_state = "alert"
|
|
190
|
+
self.pet.alert()
|
|
191
|
+
self._update_status_card(state=self._pet_state, bubble_text=reply.text)
|
|
192
|
+
self._show_toolbar()
|
|
193
|
+
|
|
194
|
+
# Reset to idle after short delay
|
|
195
|
+
QTimer.singleShot(2000, self._reset_pet_state)
|
|
196
|
+
if reply.should_quit:
|
|
197
|
+
QApplication.instance().quit()
|
|
198
|
+
self.worker = None
|
|
199
|
+
|
|
200
|
+
def _reset_pet_state(self):
|
|
201
|
+
self._pet_state = "idle"
|
|
202
|
+
self._update_status_card(state="idle")
|
|
203
|
+
|
|
204
|
+
def _handle_failure(self, message: str):
|
|
205
|
+
self.chat.set_busy(False)
|
|
206
|
+
self.pet.set_thinking(False)
|
|
207
|
+
self.chat.append_message(ChatMessage("error", message))
|
|
208
|
+
self._pet_state = "error"
|
|
209
|
+
self._update_status_card(state="error", bubble_text=message)
|
|
210
|
+
self.pet.speak(message)
|
|
211
|
+
self.pet.error()
|
|
212
|
+
QTimer.singleShot(2000, self._reset_pet_state)
|
|
213
|
+
self.worker = None
|
|
214
|
+
|
|
215
|
+
def _handle_attachment_error(self, message: str):
|
|
216
|
+
self.chat.append_message(ChatMessage("error", message))
|
|
217
|
+
self._pet_state = "error"
|
|
218
|
+
self._update_status_card(state="error", bubble_text=message)
|
|
219
|
+
self.pet.speak(message)
|
|
220
|
+
self.pet.error()
|
|
221
|
+
QTimer.singleShot(2000, self._reset_pet_state)
|
|
222
|
+
|
|
223
|
+
def _apply_controller_reply(self, reply: SessionReply):
|
|
224
|
+
self._sync_reply_state(reply)
|
|
225
|
+
self._refresh_tray_menu()
|
|
226
|
+
self.pet.speak(reply.text)
|
|
227
|
+
self.pet.alert()
|
|
228
|
+
|
|
229
|
+
def _sync_reply_state(self, reply: SessionReply):
|
|
230
|
+
self.pet.update_session_state(reply)
|
|
231
|
+
self.chat.sync_state(reply, self.current_skin)
|
|
232
|
+
self.toolbar.sync_state(
|
|
233
|
+
reply.mode,
|
|
234
|
+
reply.presentation_mode,
|
|
235
|
+
self.current_skin,
|
|
236
|
+
self._current_pet().display_name,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _on_mode_changed(self, mode: str):
|
|
240
|
+
self._apply_controller_reply(self.controller.set_mode(mode))
|
|
241
|
+
|
|
242
|
+
def _on_style_changed(self, style: str):
|
|
243
|
+
self._apply_controller_reply(self.controller.set_presentation_mode(style))
|
|
244
|
+
|
|
245
|
+
def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason):
|
|
246
|
+
if reason in {
|
|
247
|
+
QSystemTrayIcon.ActivationReason.Trigger,
|
|
248
|
+
QSystemTrayIcon.ActivationReason.DoubleClick,
|
|
249
|
+
}:
|
|
250
|
+
self.toggle_chat()
|
|
251
|
+
|
|
252
|
+
def _build_tray_menu(self) -> QMenu:
|
|
253
|
+
menu = QMenu()
|
|
254
|
+
show_pet = QAction("显示桌宠", menu)
|
|
255
|
+
show_pet.triggered.connect(self.reveal_pet)
|
|
256
|
+
menu.addAction(show_pet)
|
|
257
|
+
|
|
258
|
+
reset_pet = QAction("重置桌宠位置", menu)
|
|
259
|
+
reset_pet.triggered.connect(lambda: self.reveal_pet(reset_position=True))
|
|
260
|
+
menu.addAction(reset_pet)
|
|
261
|
+
|
|
262
|
+
show_chat = QAction("打开面板", menu)
|
|
263
|
+
show_chat.triggered.connect(self.toggle_chat)
|
|
264
|
+
menu.addAction(show_chat)
|
|
265
|
+
|
|
266
|
+
show_commands = QAction("快捷命令", menu)
|
|
267
|
+
show_commands.triggered.connect(self.toggle_commands)
|
|
268
|
+
menu.addAction(show_commands)
|
|
269
|
+
|
|
270
|
+
menu.addSeparator()
|
|
271
|
+
model_source = QAction(f"模型来源: {self._gui_model_source}", menu)
|
|
272
|
+
model_source.setEnabled(False)
|
|
273
|
+
menu.addAction(model_source)
|
|
274
|
+
|
|
275
|
+
pet_manager = QAction("宠物管理", menu)
|
|
276
|
+
pet_manager.triggered.connect(self.open_pet_manager)
|
|
277
|
+
menu.addAction(pet_manager)
|
|
278
|
+
|
|
279
|
+
model_settings = QAction("模型设置", menu)
|
|
280
|
+
model_settings.triggered.connect(self.open_model_settings)
|
|
281
|
+
menu.addAction(model_settings)
|
|
282
|
+
|
|
283
|
+
personality = QAction("性格设置", menu)
|
|
284
|
+
personality.triggered.connect(self.open_personality_settings)
|
|
285
|
+
menu.addAction(personality)
|
|
286
|
+
|
|
287
|
+
menu.addSeparator()
|
|
288
|
+
self._populate_configuration_menu(menu)
|
|
289
|
+
|
|
290
|
+
menu.addSeparator()
|
|
291
|
+
quit_action = QAction("退出", menu)
|
|
292
|
+
quit_action.triggered.connect(QApplication.instance().quit)
|
|
293
|
+
menu.addAction(quit_action)
|
|
294
|
+
return menu
|
|
295
|
+
|
|
296
|
+
def _build_tray_icon(self) -> QIcon:
|
|
297
|
+
pixmap = QPixmap(64, 64)
|
|
298
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
299
|
+
painter = QPainter(pixmap)
|
|
300
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
301
|
+
painter.setPen(QPen(QColor(205, 192, 176), 2))
|
|
302
|
+
painter.setBrush(QColor(248, 244, 236))
|
|
303
|
+
painter.drawEllipse(QRect(10, 14, 44, 38))
|
|
304
|
+
painter.drawPolygon(QPolygon([QPoint(18, 20), QPoint(24, 6), QPoint(31, 22)]))
|
|
305
|
+
painter.drawPolygon(QPolygon([QPoint(33, 22), QPoint(40, 6), QPoint(46, 20)]))
|
|
306
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
307
|
+
painter.setBrush(QColor(59, 64, 72))
|
|
308
|
+
painter.drawEllipse(QRect(23, 28, 5, 8))
|
|
309
|
+
painter.drawEllipse(QRect(36, 28, 5, 8))
|
|
310
|
+
painter.setBrush(QColor(239, 164, 145))
|
|
311
|
+
painter.drawEllipse(QRect(29, 34, 6, 4))
|
|
312
|
+
painter.end()
|
|
313
|
+
return QIcon(pixmap)
|
|
314
|
+
|
|
315
|
+
def _place_windows(self):
|
|
316
|
+
screen = QApplication.primaryScreen()
|
|
317
|
+
if screen is None:
|
|
318
|
+
self.pet.move(80, 80)
|
|
319
|
+
self.chat.move(140, 120)
|
|
320
|
+
self.commands.move(120, 80)
|
|
321
|
+
self._sync_toolbar_position()
|
|
322
|
+
return
|
|
323
|
+
area = screen.availableGeometry()
|
|
324
|
+
pet_x = max(area.left() + 16, area.right() - self.pet.width() - 24)
|
|
325
|
+
pet_y = max(area.top() + 16, area.bottom() - self.pet.height() - 24)
|
|
326
|
+
self.pet.move(pet_x, pet_y)
|
|
327
|
+
self._position_floating_panel(self.chat, x_shift=120, y_gap=34)
|
|
328
|
+
self._position_floating_panel(self.commands, x_shift=92, y_gap=34)
|
|
329
|
+
self._sync_toolbar_position()
|
|
330
|
+
|
|
331
|
+
def _position_floating_panel(self, window: QWidget, x_shift: int, y_gap: int):
|
|
332
|
+
screen = QApplication.primaryScreen()
|
|
333
|
+
if screen is None:
|
|
334
|
+
return
|
|
335
|
+
area = screen.availableGeometry()
|
|
336
|
+
pet_pos = self.pet.pos()
|
|
337
|
+
x = pet_pos.x() - window.width() + x_shift
|
|
338
|
+
y = pet_pos.y() - window.height() - y_gap
|
|
339
|
+
x = max(area.left() + 24, min(x, area.right() - window.width() - 24))
|
|
340
|
+
y = max(area.top() + 24, min(y, area.bottom() - window.height() - 24))
|
|
341
|
+
window.move(x, y)
|
|
342
|
+
|
|
343
|
+
def _sync_toolbar_position(self):
|
|
344
|
+
toolbar_size = self.toolbar.sizeHint()
|
|
345
|
+
pet_pos = self.pet.pos()
|
|
346
|
+
x = pet_pos.x() - max(0, (toolbar_size.width() - self.pet.width()) // 2)
|
|
347
|
+
y = pet_pos.y() - toolbar_size.height() - 12
|
|
348
|
+
self.toolbar.move(x, y)
|
|
349
|
+
|
|
350
|
+
def _sync_status_card_position(self):
|
|
351
|
+
card_size = self.status_card.sizeHint()
|
|
352
|
+
toolbar_size = self.toolbar.sizeHint()
|
|
353
|
+
pet_pos = self.pet.pos()
|
|
354
|
+
card_y = pet_pos.y() - card_size.height() - toolbar_size.height() - 18
|
|
355
|
+
card_x = pet_pos.x() - max(0, (card_size.width() - self.pet.width()) // 2)
|
|
356
|
+
screen = QApplication.primaryScreen()
|
|
357
|
+
if screen is not None:
|
|
358
|
+
area = screen.availableGeometry()
|
|
359
|
+
card_x = max(area.left() + 8, min(card_x, area.right() - card_size.width() - 8))
|
|
360
|
+
card_y = max(area.top() + 8, card_y)
|
|
361
|
+
self.status_card.move(card_x, card_y)
|
|
362
|
+
|
|
363
|
+
def _show_toolbar(self):
|
|
364
|
+
self._toolbar_hide_timer.stop()
|
|
365
|
+
self._sync_toolbar_position()
|
|
366
|
+
self.toolbar.show()
|
|
367
|
+
self.toolbar.raise_()
|
|
368
|
+
self._sync_status_card_position()
|
|
369
|
+
self.status_card.show()
|
|
370
|
+
self.status_card.raise_()
|
|
371
|
+
|
|
372
|
+
def _hide_toolbar_if_idle(self):
|
|
373
|
+
if self.chat.isVisible() or self.commands.isVisible():
|
|
374
|
+
return
|
|
375
|
+
if self.pet.underMouse() or self.toolbar.underMouse() or self.status_card.underMouse():
|
|
376
|
+
return
|
|
377
|
+
self.toolbar.hide()
|
|
378
|
+
self.status_card.hide()
|
|
379
|
+
|
|
380
|
+
def _update_status_card(self, state: str = "", bubble_text: str = ""):
|
|
381
|
+
self.status_card.set_status(
|
|
382
|
+
mode=self.controller.mode,
|
|
383
|
+
model_provider=self.controller.provider_name,
|
|
384
|
+
model_name=self.controller.model_name,
|
|
385
|
+
state=state or self._pet_state,
|
|
386
|
+
bubble_text=bubble_text,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def _on_pet_hover_changed(self, hovering: bool):
|
|
390
|
+
if hovering:
|
|
391
|
+
self._show_toolbar()
|
|
392
|
+
return
|
|
393
|
+
self._toolbar_hide_timer.start()
|
|
394
|
+
|
|
395
|
+
def _on_toolbar_hover_changed(self, hovering: bool):
|
|
396
|
+
if hovering:
|
|
397
|
+
self._toolbar_hide_timer.stop()
|
|
398
|
+
return
|
|
399
|
+
self._toolbar_hide_timer.start()
|
|
400
|
+
|
|
401
|
+
def _show_pet_context_menu(self, global_pos: QPoint):
|
|
402
|
+
menu = QMenu()
|
|
403
|
+
open_chat = QAction("打开聊天", menu)
|
|
404
|
+
open_chat.triggered.connect(self.toggle_chat)
|
|
405
|
+
menu.addAction(open_chat)
|
|
406
|
+
|
|
407
|
+
open_commands = QAction("快捷命令", menu)
|
|
408
|
+
open_commands.triggered.connect(self.toggle_commands)
|
|
409
|
+
menu.addAction(open_commands)
|
|
410
|
+
|
|
411
|
+
menu.addSeparator()
|
|
412
|
+
pet_manager = QAction("宠物管理", menu)
|
|
413
|
+
pet_manager.triggered.connect(self.open_pet_manager)
|
|
414
|
+
menu.addAction(pet_manager)
|
|
415
|
+
|
|
416
|
+
model_settings = QAction("模型设置", menu)
|
|
417
|
+
model_settings.triggered.connect(self.open_model_settings)
|
|
418
|
+
menu.addAction(model_settings)
|
|
419
|
+
|
|
420
|
+
personality = QAction("性格设置", menu)
|
|
421
|
+
personality.triggered.connect(self.open_personality_settings)
|
|
422
|
+
menu.addAction(personality)
|
|
423
|
+
|
|
424
|
+
menu.addSeparator()
|
|
425
|
+
pets_menu = menu.addMenu("宠物")
|
|
426
|
+
for package in self._pets:
|
|
427
|
+
action = QAction(package.display_name, pets_menu)
|
|
428
|
+
action.setCheckable(True)
|
|
429
|
+
action.setChecked(package.id == self.current_pet_id)
|
|
430
|
+
action.triggered.connect(lambda _checked=False, value=package.id: self._apply_pet(value))
|
|
431
|
+
pets_menu.addAction(action)
|
|
432
|
+
|
|
433
|
+
import_pet = QAction("导入宠物", menu)
|
|
434
|
+
import_pet.triggered.connect(self._import_pet_action)
|
|
435
|
+
menu.addAction(import_pet)
|
|
436
|
+
|
|
437
|
+
menu.addSeparator()
|
|
438
|
+
reset_pet = QAction("重置桌宠位置", menu)
|
|
439
|
+
reset_pet.triggered.connect(lambda: self.reveal_pet(reset_position=True))
|
|
440
|
+
menu.addAction(reset_pet)
|
|
441
|
+
|
|
442
|
+
hide_pet = QAction("隐藏桌宠", menu)
|
|
443
|
+
hide_pet.triggered.connect(self._hide_pet_action)
|
|
444
|
+
menu.addAction(hide_pet)
|
|
445
|
+
|
|
446
|
+
menu.addSeparator()
|
|
447
|
+
quit_action = QAction("退出", menu)
|
|
448
|
+
quit_action.triggered.connect(QApplication.instance().quit)
|
|
449
|
+
menu.addAction(quit_action)
|
|
450
|
+
menu.exec(global_pos)
|
|
451
|
+
|
|
452
|
+
def _cycle_mode_action(self):
|
|
453
|
+
self._apply_controller_reply(self.controller.cycle_mode())
|
|
454
|
+
|
|
455
|
+
def _toggle_style_action(self):
|
|
456
|
+
next_style = "pet" if self.controller.presentation_mode == "work" else "work"
|
|
457
|
+
self._apply_controller_reply(self.controller.set_presentation_mode(next_style))
|
|
458
|
+
|
|
459
|
+
def _hide_pet_action(self):
|
|
460
|
+
self.toolbar.hide()
|
|
461
|
+
self.status_card.hide()
|
|
462
|
+
self.chat.hide()
|
|
463
|
+
self.commands.hide()
|
|
464
|
+
self.toolbar.set_active_panel("")
|
|
465
|
+
self.pet.hide()
|
|
466
|
+
|
|
467
|
+
def _initial_reveal(self):
|
|
468
|
+
self.reveal_pet(reset_position=True)
|
|
469
|
+
|
|
470
|
+
def _cycle_skin_action(self):
|
|
471
|
+
skins = list(SKIN_PALETTES.keys())
|
|
472
|
+
idx = skins.index(self.current_skin) if self.current_skin in skins else 0
|
|
473
|
+
next_skin = skins[(idx + 1) % len(skins)]
|
|
474
|
+
self._apply_skin(next_skin)
|
|
475
|
+
self.pet.speak(f"换成 {next_skin} 皮肤啦。")
|
|
476
|
+
self.pet.celebrate()
|
|
477
|
+
|
|
478
|
+
def _apply_skin(self, skin: str):
|
|
479
|
+
self.current_skin = skin if skin in SKIN_PALETTES else "glass"
|
|
480
|
+
self._state_store.save_skin(self.current_skin)
|
|
481
|
+
self.pet.set_skin(self.current_skin)
|
|
482
|
+
self.chat.apply_skin(self.current_skin)
|
|
483
|
+
self.commands.apply_skin(self.current_skin)
|
|
484
|
+
self.toolbar.apply_skin(self.current_skin)
|
|
485
|
+
self._sync_reply_state(SessionReply(text="", mode=self.controller.mode, presentation_mode=self.controller.presentation_mode))
|
|
486
|
+
self._refresh_tray_menu()
|
|
487
|
+
|
|
488
|
+
def _apply_language(self, language_id: str, announce: bool = True):
|
|
489
|
+
language = pai_config.get_language(language_id)
|
|
490
|
+
if language is None:
|
|
491
|
+
return
|
|
492
|
+
self.current_language = str(language.get("id", "zh-CN"))
|
|
493
|
+
pai_config.set_active_language(self.current_language)
|
|
494
|
+
texts = dict(language.get("texts", {}))
|
|
495
|
+
self.chat.set_language(texts)
|
|
496
|
+
self.commands.set_language(texts)
|
|
497
|
+
self.commands.set_quick_commands(pai_config.quick_commands())
|
|
498
|
+
self.chat.set_quick_commands(pai_config.quick_commands())
|
|
499
|
+
self.toolbar.set_language(texts)
|
|
500
|
+
self._refresh_tray_menu()
|
|
501
|
+
if announce:
|
|
502
|
+
label = str(language.get("label", self.current_language))
|
|
503
|
+
message = self._format_text("language_switched", label, f"界面语言已切换为 {label}")
|
|
504
|
+
self.pet.speak(message)
|
|
505
|
+
self.pet.alert()
|
|
506
|
+
|
|
507
|
+
def _reload_pets(self):
|
|
508
|
+
ordered: list[PetPackage] = []
|
|
509
|
+
seen: dict[str, int] = {}
|
|
510
|
+
for package in BUILTIN_PETS + self._pet_store.list_imported():
|
|
511
|
+
if package.id in seen:
|
|
512
|
+
ordered[seen[package.id]] = package
|
|
513
|
+
continue
|
|
514
|
+
seen[package.id] = len(ordered)
|
|
515
|
+
ordered.append(package)
|
|
516
|
+
self._pets = ordered or BUILTIN_PETS[:]
|
|
517
|
+
if self._pet_manager is not None and self._pet_manager.isVisible():
|
|
518
|
+
self._pet_manager.load_pets(self._pets, self.current_pet_id, self.current_skin)
|
|
519
|
+
|
|
520
|
+
def _current_pet(self) -> PetPackage:
|
|
521
|
+
for package in self._pets:
|
|
522
|
+
if package.id == self.current_pet_id:
|
|
523
|
+
return package
|
|
524
|
+
return self._pets[0]
|
|
525
|
+
|
|
526
|
+
def _apply_pet(self, pet_id: str, announce: bool = True):
|
|
527
|
+
selected = next((package for package in self._pets if package.id == pet_id), None) or self._pets[0]
|
|
528
|
+
self.current_pet_id = selected.id
|
|
529
|
+
self._state_store.save_selected_pet(self.current_pet_id)
|
|
530
|
+
self.pet.set_pet_package(selected)
|
|
531
|
+
if hasattr(self, "toolbar"):
|
|
532
|
+
self.toolbar.sync_state(
|
|
533
|
+
self.controller.mode,
|
|
534
|
+
self.controller.presentation_mode,
|
|
535
|
+
self.current_skin,
|
|
536
|
+
selected.display_name,
|
|
537
|
+
)
|
|
538
|
+
if hasattr(self, "tray"):
|
|
539
|
+
self._refresh_tray_menu()
|
|
540
|
+
if self._pet_manager is not None and self._pet_manager.isVisible():
|
|
541
|
+
self._pet_manager.load_pets(self._pets, self.current_pet_id, self.current_skin)
|
|
542
|
+
if announce:
|
|
543
|
+
self.pet.speak(self._format_text("pet_changed", selected.display_name, f"{selected.display_name} 来了。"))
|
|
544
|
+
self.pet.celebrate()
|
|
545
|
+
|
|
546
|
+
def _cycle_pet_action(self):
|
|
547
|
+
if not self._pets:
|
|
548
|
+
return
|
|
549
|
+
pet_ids = [package.id for package in self._pets]
|
|
550
|
+
try:
|
|
551
|
+
current_index = pet_ids.index(self.current_pet_id)
|
|
552
|
+
except ValueError:
|
|
553
|
+
current_index = -1
|
|
554
|
+
self._apply_pet(pet_ids[(current_index + 1) % len(pet_ids)])
|
|
555
|
+
|
|
556
|
+
def _import_pet_action(self):
|
|
557
|
+
folder = QFileDialog.getExistingDirectory(
|
|
558
|
+
self.chat if self.chat.isVisible() else None,
|
|
559
|
+
"选择 Petdex / Codex 宠物文件夹",
|
|
560
|
+
str(Path.home()),
|
|
561
|
+
)
|
|
562
|
+
if not folder:
|
|
563
|
+
return
|
|
564
|
+
try:
|
|
565
|
+
imported = self._pet_store.import_folder(folder)
|
|
566
|
+
except Exception as exc:
|
|
567
|
+
message = f"导入失败: {exc}"
|
|
568
|
+
self.pet.speak(message)
|
|
569
|
+
self.pet.error()
|
|
570
|
+
return
|
|
571
|
+
self._reload_pets()
|
|
572
|
+
self._apply_pet(imported.id, announce=False)
|
|
573
|
+
self.pet.speak(f"{imported.display_name} 已导入。")
|
|
574
|
+
self.pet.celebrate()
|
|
575
|
+
|
|
576
|
+
def _populate_configuration_menu(self, menu: QMenu):
|
|
577
|
+
mode_menu = menu.addMenu("运行模式")
|
|
578
|
+
for mode in ("single", "plan", "team"):
|
|
579
|
+
action = QAction(mode, mode_menu)
|
|
580
|
+
action.setCheckable(True)
|
|
581
|
+
action.setChecked(mode == self.controller.mode)
|
|
582
|
+
action.triggered.connect(lambda _checked=False, value=mode: self._set_mode(value))
|
|
583
|
+
mode_menu.addAction(action)
|
|
584
|
+
|
|
585
|
+
style_menu = menu.addMenu("展示模式")
|
|
586
|
+
for style in ("work", "pet"):
|
|
587
|
+
action = QAction(style, style_menu)
|
|
588
|
+
action.setCheckable(True)
|
|
589
|
+
action.setChecked(style == self.controller.presentation_mode)
|
|
590
|
+
action.triggered.connect(lambda _checked=False, value=style: self._set_style(value))
|
|
591
|
+
style_menu.addAction(action)
|
|
592
|
+
|
|
593
|
+
persona_menu = menu.addMenu("人格 Prompt")
|
|
594
|
+
for persona in pai_config.personas():
|
|
595
|
+
persona_id = str(persona.get("id", "")).strip()
|
|
596
|
+
label = str(persona.get("label", persona_id)).strip() or persona_id
|
|
597
|
+
action = QAction(label, persona_menu)
|
|
598
|
+
action.setCheckable(True)
|
|
599
|
+
action.setChecked(persona_id == pai_config.active_persona)
|
|
600
|
+
action.triggered.connect(lambda _checked=False, value=persona_id: self._apply_persona(value))
|
|
601
|
+
persona_menu.addAction(action)
|
|
602
|
+
|
|
603
|
+
language_menu = menu.addMenu("语言")
|
|
604
|
+
for language in pai_config.languages():
|
|
605
|
+
language_id = str(language.get("id", "")).strip()
|
|
606
|
+
label = str(language.get("label", language_id)).strip() or language_id
|
|
607
|
+
action = QAction(label, language_menu)
|
|
608
|
+
action.setCheckable(True)
|
|
609
|
+
action.setChecked(language_id == self.current_language)
|
|
610
|
+
action.triggered.connect(lambda _checked=False, value=language_id: self._apply_language(value))
|
|
611
|
+
language_menu.addAction(action)
|
|
612
|
+
|
|
613
|
+
skin_menu = menu.addMenu("皮肤")
|
|
614
|
+
for skin_id in SKIN_PALETTES:
|
|
615
|
+
action = QAction(skin_id, skin_menu)
|
|
616
|
+
action.setCheckable(True)
|
|
617
|
+
action.setChecked(skin_id == self.current_skin)
|
|
618
|
+
action.triggered.connect(lambda _checked=False, value=skin_id: self._apply_skin_action(value))
|
|
619
|
+
skin_menu.addAction(action)
|
|
620
|
+
|
|
621
|
+
pets_menu = menu.addMenu("宠物")
|
|
622
|
+
for package in self._pets:
|
|
623
|
+
action = QAction(package.display_name, pets_menu)
|
|
624
|
+
action.setCheckable(True)
|
|
625
|
+
action.setChecked(package.id == self.current_pet_id)
|
|
626
|
+
action.triggered.connect(lambda _checked=False, value=package.id: self._apply_pet(value))
|
|
627
|
+
pets_menu.addAction(action)
|
|
628
|
+
|
|
629
|
+
import_pet = QAction("导入 Petdex 宠物", menu)
|
|
630
|
+
import_pet.triggered.connect(self._import_pet_action)
|
|
631
|
+
menu.addAction(import_pet)
|
|
632
|
+
|
|
633
|
+
reset_pet = QAction("重置桌宠位置", menu)
|
|
634
|
+
reset_pet.triggered.connect(lambda: self.reveal_pet(reset_position=True))
|
|
635
|
+
menu.addAction(reset_pet)
|
|
636
|
+
|
|
637
|
+
hide_pet = QAction("隐藏桌宠", menu)
|
|
638
|
+
hide_pet.triggered.connect(self._hide_pet_action)
|
|
639
|
+
menu.addAction(hide_pet)
|
|
640
|
+
|
|
641
|
+
def _set_mode(self, mode: str):
|
|
642
|
+
self._apply_controller_reply(self.controller.set_mode(mode))
|
|
643
|
+
self._update_status_card()
|
|
644
|
+
|
|
645
|
+
def _set_style(self, style: str):
|
|
646
|
+
self._apply_controller_reply(self.controller.set_presentation_mode(style))
|
|
647
|
+
self._update_status_card()
|
|
648
|
+
|
|
649
|
+
def _apply_skin_action(self, skin_id: str):
|
|
650
|
+
self._apply_skin(skin_id)
|
|
651
|
+
self.pet.speak(self._format_text("skin_switched", skin_id, f"皮肤已切换为 {skin_id}"))
|
|
652
|
+
self.pet.celebrate()
|
|
653
|
+
|
|
654
|
+
def _apply_persona(self, persona_id: str):
|
|
655
|
+
persona = pai_config.get_persona(persona_id)
|
|
656
|
+
if persona is None:
|
|
657
|
+
return
|
|
658
|
+
pai_config.set_active_persona(persona_id)
|
|
659
|
+
self._refresh_tray_menu()
|
|
660
|
+
if self._personality_window is not None and self._personality_window.isVisible():
|
|
661
|
+
self._personality_window.load_personas(
|
|
662
|
+
pai_config.personas(), pai_config.active_persona
|
|
663
|
+
)
|
|
664
|
+
label = str(persona.get("label", persona_id)).strip() or persona_id
|
|
665
|
+
message = self._format_text("persona_switched", label, f"人格已切换为 {label}")
|
|
666
|
+
self.pet.speak(message)
|
|
667
|
+
self.pet.alert()
|
|
668
|
+
|
|
669
|
+
def _build_initial_gui_llm_client(self):
|
|
670
|
+
self._gui_model_config = self._load_gui_model_config()
|
|
671
|
+
if self._gui_model_config.enabled:
|
|
672
|
+
try:
|
|
673
|
+
client = self._create_gui_model_client(self._gui_model_config)
|
|
674
|
+
self._gui_model_source = self._format_model_source("独立配置", client.provider_name, client.model_name)
|
|
675
|
+
return client
|
|
676
|
+
except Exception as exc:
|
|
677
|
+
self._gui_model_warning = f"独立 GUI 模型加载失败: {exc}。当前已回退到全局模型。"
|
|
678
|
+
|
|
679
|
+
client = create_from_config()
|
|
680
|
+
if client is None:
|
|
681
|
+
raise RuntimeError(
|
|
682
|
+
"无法创建 LLM 客户端。请至少配置一组模型环境变量,例如 "
|
|
683
|
+
"GLM_API_KEY/GLM_MODEL、DEEPSEEK_API_KEY/DEEPSEEK_MODEL、"
|
|
684
|
+
"QWEN_API_KEY/QWEN_MODEL 或 "
|
|
685
|
+
"OLLAMA_MODEL/OLLAMA_BASE_URL。"
|
|
686
|
+
)
|
|
687
|
+
self._gui_model_source = self._format_model_source("跟随全局", client.provider_name, client.model_name)
|
|
688
|
+
return client
|
|
689
|
+
|
|
690
|
+
def _load_gui_model_config(self) -> GuiModelConfig:
|
|
691
|
+
try:
|
|
692
|
+
return self._gui_model_store.load()
|
|
693
|
+
except Exception as exc:
|
|
694
|
+
self._gui_model_warning = str(exc)
|
|
695
|
+
return GuiModelConfig()
|
|
696
|
+
|
|
697
|
+
def _create_gui_model_client(self, config: GuiModelConfig):
|
|
698
|
+
missing = config.validate()
|
|
699
|
+
if missing:
|
|
700
|
+
raise ValueError(f"缺少字段: {', '.join(missing)}")
|
|
701
|
+
client = create_from_provider_config(config.provider, config.provider_config())
|
|
702
|
+
if client is None:
|
|
703
|
+
raise RuntimeError(f"无法创建 provider={config.provider} 的客户端")
|
|
704
|
+
return client
|
|
705
|
+
|
|
706
|
+
def _format_model_source(self, prefix: str, provider: str, model: str) -> str:
|
|
707
|
+
return f"{prefix} ({gui_model_profile_label(provider)} / {model})"
|
|
708
|
+
|
|
709
|
+
def _current_global_model_summary(self) -> str:
|
|
710
|
+
global_client = create_from_config()
|
|
711
|
+
if global_client is not None:
|
|
712
|
+
return f"{gui_model_profile_label(global_client.provider_name)} / {global_client.model_name}"
|
|
713
|
+
preset = pai_config.get_model_preset(pai_config.active_model_preset)
|
|
714
|
+
if preset is not None:
|
|
715
|
+
return f"{gui_model_profile_label(preset.provider)} / {preset.model}"
|
|
716
|
+
return "未配置"
|
|
717
|
+
|
|
718
|
+
def _seed_gui_model_config(self) -> GuiModelConfig:
|
|
719
|
+
config = self._gui_model_config
|
|
720
|
+
if config.enabled or config.model or config.base_url or config.api_key:
|
|
721
|
+
return config
|
|
722
|
+
return GuiModelConfig(enabled=False, provider="codex", model="", base_url="", api_key="")
|
|
723
|
+
|
|
724
|
+
def open_model_settings(self):
|
|
725
|
+
if self._settings_window is None:
|
|
726
|
+
self._settings_window = GuiModelSettingsWindow()
|
|
727
|
+
self._settings_window.save_requested.connect(self._save_gui_model_settings)
|
|
728
|
+
self._settings_window.load_state(
|
|
729
|
+
self._seed_gui_model_config(),
|
|
730
|
+
self._gui_model_source,
|
|
731
|
+
self._current_global_model_summary(),
|
|
732
|
+
self._gui_model_warning,
|
|
733
|
+
)
|
|
734
|
+
self._settings_window.show()
|
|
735
|
+
self._settings_window.raise_()
|
|
736
|
+
self._settings_window.activateWindow()
|
|
737
|
+
|
|
738
|
+
def _save_gui_model_settings(self, config: GuiModelConfig):
|
|
739
|
+
if not config.enabled:
|
|
740
|
+
self._follow_global_model(config)
|
|
741
|
+
return
|
|
742
|
+
try:
|
|
743
|
+
new_client = self._create_gui_model_client(config)
|
|
744
|
+
self._gui_model_store.save(config)
|
|
745
|
+
self._gui_model_config = config
|
|
746
|
+
self._gui_model_warning = ""
|
|
747
|
+
self.controller.set_llm_client(new_client)
|
|
748
|
+
self._gui_model_source = self._format_model_source(
|
|
749
|
+
"独立配置",
|
|
750
|
+
new_client.provider_name,
|
|
751
|
+
new_client.model_name,
|
|
752
|
+
)
|
|
753
|
+
self._refresh_tray_menu()
|
|
754
|
+
if self._settings_window is not None:
|
|
755
|
+
self._settings_window.update_runtime_labels(
|
|
756
|
+
self._gui_model_source,
|
|
757
|
+
self._current_global_model_summary(),
|
|
758
|
+
self._gui_model_warning,
|
|
759
|
+
)
|
|
760
|
+
self._settings_window.set_status("独立 GUI 模型已保存并应用。")
|
|
761
|
+
self.pet.speak(
|
|
762
|
+
f"桌宠已切换到独立模型 {gui_model_profile_label(new_client.provider_name)} / {new_client.model_name}。"
|
|
763
|
+
)
|
|
764
|
+
self.pet.celebrate()
|
|
765
|
+
self._update_status_card()
|
|
766
|
+
except Exception as exc:
|
|
767
|
+
if self._settings_window is not None:
|
|
768
|
+
self._settings_window.set_status(f"保存失败: {exc}", error=True)
|
|
769
|
+
self.pet.speak(f"模型配置保存失败: {exc}")
|
|
770
|
+
self.pet.error()
|
|
771
|
+
|
|
772
|
+
def open_pet_manager(self):
|
|
773
|
+
if self._pet_manager is None:
|
|
774
|
+
from .windows import PetManagerWindow
|
|
775
|
+
self._pet_manager = PetManagerWindow()
|
|
776
|
+
self._pet_manager.pet_selected.connect(self._apply_pet)
|
|
777
|
+
self._pet_manager.import_requested.connect(self._manager_import_pet)
|
|
778
|
+
self._pet_manager.delete_requested.connect(self._manager_delete_pet)
|
|
779
|
+
self._pet_manager.load_pets(self._pets, self.current_pet_id, self.current_skin)
|
|
780
|
+
self._pet_manager.show()
|
|
781
|
+
self._pet_manager.raise_()
|
|
782
|
+
self._pet_manager.activateWindow()
|
|
783
|
+
|
|
784
|
+
def open_personality_settings(self):
|
|
785
|
+
if self._personality_window is None:
|
|
786
|
+
self._personality_window = PersonalityWindow()
|
|
787
|
+
self._personality_window.load_personas(
|
|
788
|
+
pai_config.personas(), pai_config.active_persona
|
|
789
|
+
)
|
|
790
|
+
self._personality_window.show()
|
|
791
|
+
self._personality_window.raise_()
|
|
792
|
+
self._personality_window.activateWindow()
|
|
793
|
+
|
|
794
|
+
def _manager_import_pet(self):
|
|
795
|
+
"""从 Pet Manager 导入宠物"""
|
|
796
|
+
folder = QFileDialog.getExistingDirectory(
|
|
797
|
+
self._pet_manager if self._pet_manager and self._pet_manager.isVisible() else None,
|
|
798
|
+
"选择宠物文件夹",
|
|
799
|
+
str(Path.home()),
|
|
800
|
+
)
|
|
801
|
+
if not folder:
|
|
802
|
+
return
|
|
803
|
+
try:
|
|
804
|
+
imported = self._pet_store.import_folder(folder)
|
|
805
|
+
except Exception as exc:
|
|
806
|
+
self.pet.speak(f"导入失败: {exc}")
|
|
807
|
+
self.pet.error()
|
|
808
|
+
return
|
|
809
|
+
self._reload_pets()
|
|
810
|
+
self._apply_pet(imported.id, announce=False)
|
|
811
|
+
self.pet.speak(f"{imported.display_name} 已导入。")
|
|
812
|
+
self.pet.celebrate()
|
|
813
|
+
if self._pet_manager is not None:
|
|
814
|
+
self._pet_manager.load_pets(self._pets, self.current_pet_id, self.current_skin)
|
|
815
|
+
|
|
816
|
+
def _manager_delete_pet(self, pet_id: str):
|
|
817
|
+
"""从 Pet Manager 删除已导入宠物"""
|
|
818
|
+
import shutil
|
|
819
|
+
pet_store = self._pet_store
|
|
820
|
+
target_dir = pet_store.root / pet_id
|
|
821
|
+
if target_dir.exists():
|
|
822
|
+
shutil.rmtree(target_dir)
|
|
823
|
+
self._reload_pets()
|
|
824
|
+
if self.current_pet_id == pet_id:
|
|
825
|
+
self._apply_pet(self._pets[0].id if self._pets else "terminal-cat", announce=False)
|
|
826
|
+
if self._pet_manager is not None:
|
|
827
|
+
self._pet_manager.load_pets(self._pets, self.current_pet_id, self.current_skin)
|
|
828
|
+
self.pet.speak(f"{pet_id} 已删除。")
|
|
829
|
+
self.pet.alert()
|
|
830
|
+
|
|
831
|
+
def _follow_global_model(self, config: GuiModelConfig | None = None):
|
|
832
|
+
target_config = config or self._seed_gui_model_config()
|
|
833
|
+
target_config.enabled = False
|
|
834
|
+
reply = self.controller.reload_default_model()
|
|
835
|
+
if reply.kind == "error":
|
|
836
|
+
if self._settings_window is not None:
|
|
837
|
+
self._settings_window.set_status(reply.text, error=True)
|
|
838
|
+
self.pet.speak(reply.text)
|
|
839
|
+
self.pet.error()
|
|
840
|
+
return
|
|
841
|
+
try:
|
|
842
|
+
self._gui_model_store.save(target_config)
|
|
843
|
+
self._gui_model_config = target_config
|
|
844
|
+
self._gui_model_warning = ""
|
|
845
|
+
self._gui_model_source = self._format_model_source(
|
|
846
|
+
"跟随全局",
|
|
847
|
+
self.controller.provider_name,
|
|
848
|
+
self.controller.model_name,
|
|
849
|
+
)
|
|
850
|
+
self._refresh_tray_menu()
|
|
851
|
+
if self._settings_window is not None:
|
|
852
|
+
self._settings_window.update_runtime_labels(
|
|
853
|
+
self._gui_model_source,
|
|
854
|
+
self._current_global_model_summary(),
|
|
855
|
+
self._gui_model_warning,
|
|
856
|
+
)
|
|
857
|
+
self._settings_window.set_status("桌宠已恢复跟随全局模型。")
|
|
858
|
+
self.pet.speak("桌宠已经恢复跟随全局模型。")
|
|
859
|
+
self.pet.alert()
|
|
860
|
+
self._update_status_card()
|
|
861
|
+
except Exception as exc:
|
|
862
|
+
if self._settings_window is not None:
|
|
863
|
+
self._settings_window.set_status(f"保存失败: {exc}", error=True)
|
|
864
|
+
self.pet.speak(f"模型配置保存失败: {exc}")
|
|
865
|
+
self.pet.error()
|
|
866
|
+
|
|
867
|
+
def _text(self, key: str, default: str) -> str:
|
|
868
|
+
return pai_config.active_language_text(key, default)
|
|
869
|
+
|
|
870
|
+
def _format_text(self, key: str, value: str, default: str) -> str:
|
|
871
|
+
template = self._text(key, default)
|
|
872
|
+
try:
|
|
873
|
+
return template.format(value=value)
|
|
874
|
+
except Exception:
|
|
875
|
+
return default
|
|
876
|
+
|
|
877
|
+
def _refresh_tray_menu(self):
|
|
878
|
+
if hasattr(self, "tray"):
|
|
879
|
+
self._tray_menu = self._build_tray_menu()
|
|
880
|
+
self.tray.setContextMenu(self._tray_menu)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def run_pet_app(argv: list[str]) -> int:
|
|
884
|
+
app = QApplication(argv)
|
|
885
|
+
app.setQuitOnLastWindowClosed(False)
|
|
886
|
+
coordinator = PetCoordinator()
|
|
887
|
+
coordinator.hide()
|
|
888
|
+
return app.exec()
|