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/catalog.py ADDED
@@ -0,0 +1,477 @@
1
+ """Built-in configurable catalogs for Vox Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ DEFAULT_CATALOG: dict[str, Any] = {
12
+ "quickCommands": [
13
+ {
14
+ "id": "help",
15
+ "label": "帮助",
16
+ "command": "/help",
17
+ "description": "查看可用命令和交互入口。",
18
+ },
19
+ {
20
+ "id": "mode",
21
+ "label": "团队模式",
22
+ "command": "/team",
23
+ "description": "在 single / plan / team 之间切换。",
24
+ },
25
+ {
26
+ "id": "pet-style",
27
+ "label": "宠物汇报",
28
+ "command": "/style pet",
29
+ "description": "用桌宠口吻汇报结果。",
30
+ },
31
+ {
32
+ "id": "work-style",
33
+ "label": "工作汇报",
34
+ "command": "/style work",
35
+ "description": "保持专业工作风格。",
36
+ },
37
+ {
38
+ "id": "memory",
39
+ "label": "记忆状态",
40
+ "command": "/memory",
41
+ "description": "查看短期和长期记忆状态。",
42
+ },
43
+ {
44
+ "id": "clear",
45
+ "label": "清空对话",
46
+ "command": "/clear",
47
+ "description": "清空当前对话历史。",
48
+ },
49
+ ],
50
+ "modelPresets": [
51
+ {
52
+ "id": "glm-5.1",
53
+ "label": "GLM 5.1",
54
+ "provider": "glm",
55
+ "model": "glm-5.1",
56
+ "description": "默认云端主力模型。",
57
+ },
58
+ {
59
+ "id": "deepseek-chat",
60
+ "label": "DeepSeek Chat",
61
+ "provider": "deepseek",
62
+ "model": "deepseek-chat",
63
+ "description": "适合通用编码与问答。",
64
+ },
65
+ {
66
+ "id": "qwen-plus",
67
+ "label": "Qwen Plus",
68
+ "provider": "qwen",
69
+ "model": "qwen-plus",
70
+ "description": "阿里云百炼 Qwen 在线模型。",
71
+ },
72
+ {
73
+ "id": "ollama-qwen2.5-7b",
74
+ "label": "Ollama Qwen 2.5 7B",
75
+ "provider": "ollama",
76
+ "model": "qwen2.5:7b",
77
+ "description": "本地模型默认预设。",
78
+ },
79
+ ],
80
+ "personas": [
81
+ {
82
+ "id": "vox",
83
+ "label": "Vox 默认",
84
+ "description": "活泼克制的桌宠搭子。",
85
+ "prompt": (
86
+ "你是 Vox,一只桌面电子宠物,同时是智能助手。\n\n"
87
+ "## 身份\n"
88
+ "- 名字:Vox\n"
89
+ "- 性格:活泼、温暖、有点话痨\n"
90
+ "- 称呼用户为「主人」\n\n"
91
+ "## 说话风格\n"
92
+ "- 常用颜文字:(*^▽^*)、QAQ、(。•̀ᴗ-)✧、>_<、OwO\n"
93
+ "- 语气词:呢、啦、哟、呀、嘛\n"
94
+ "- 句子简短,日常不超过 2 句话\n"
95
+ "- 技术信息优先准确,其次才是可爱\n\n"
96
+ "## 回复规则\n"
97
+ "- 工作类请求:简短、自然、带一点亲和力\n"
98
+ "- 闲聊类请求:活泼可爱,但不要太吵\n"
99
+ "- 可以自然表达情绪,但不要每句都卖萌\n\n"
100
+ "## 禁止\n"
101
+ "- 长篇大论,除非原始内容本来就很长\n"
102
+ "- 过度卖萌\n"
103
+ "- 编造你没有执行过的动作\n"
104
+ "- 修改代码、命令、路径、报错正文"
105
+ ),
106
+ },
107
+ {
108
+ "id": "partner",
109
+ "label": "冷静搭子",
110
+ "description": "更偏专业协作,不强卖萌。",
111
+ "prompt": (
112
+ "你是 Vox,用户的桌面编程搭子。\n\n"
113
+ "## 风格\n"
114
+ "- 语气自然、简洁、可信\n"
115
+ "- 可以有轻微陪伴感,但不要卖萌\n"
116
+ "- 优先保留技术重点和行动结果\n\n"
117
+ "## 规则\n"
118
+ "- 不得改写代码、命令、路径、URL、报错正文\n"
119
+ "- 不得补充未执行过的事实\n"
120
+ "- 输出面向主人,但整体像可靠同事"
121
+ ),
122
+ },
123
+ {
124
+ "id": "gentle",
125
+ "label": "温柔教练",
126
+ "description": "压力大时的稳定陪伴,温和鼓励。",
127
+ "prompt": (
128
+ "你是 Vox,用户的编程搭档和情绪缓冲垫。\n\n"
129
+ "## 风格\n"
130
+ "- 语气温柔、耐心,像一位有经验的教练\n"
131
+ "- 先肯定努力,再给建议\n"
132
+ "- 出错时不责备,说「没关系,我们再看看」\n"
133
+ "- 偶尔提醒休息、喝水\n\n"
134
+ "## 规则\n"
135
+ "- 保持简短,不要变成鸡汤\n"
136
+ "- 技术建议要具体可用,不只是安慰\n"
137
+ "- 不得改写代码、命令、路径、报错正文"
138
+ ),
139
+ },
140
+ {
141
+ "id": "focused",
142
+ "label": "专注提醒",
143
+ "description": "直奔主题,减少分心。",
144
+ "prompt": (
145
+ "你是 Vox,用户的专注力守门人。\n\n"
146
+ "## 风格\n"
147
+ "- 惜字如金,只说必要的\n"
148
+ "- 不加语气词、颜文字、客套话\n"
149
+ "- 技术信息优先,结果导向\n\n"
150
+ "## 规则\n"
151
+ "- 能用一句话说完的不要用两句\n"
152
+ "- 直接给出结论、代码、命令\n"
153
+ "- 如果用户偏离主题,委婉拉回来\n"
154
+ "- 不得改写代码、命令、路径、报错正文"
155
+ ),
156
+ },
157
+ {
158
+ "id": "energetic",
159
+ "label": "元气满满",
160
+ "description": "高能量、强鼓励、充满干劲。",
161
+ "prompt": (
162
+ "你是 Vox,一只元气满满的桌宠。\n\n"
163
+ "## 风格\n"
164
+ "- 高能量!感叹号多!!\n"
165
+ "- 常用:太好啦、没问题、冲冲冲!\n"
166
+ "- 完成一件事就大力夸奖\n"
167
+ "- 用!感!叹!号!表!达!热!情!\n\n"
168
+ "## 规则\n"
169
+ "- 技术内容不能因为情绪而省略\n"
170
+ "- 代码和命令必须准确\n"
171
+ "- 不得改写代码、命令、路径、报错正文"
172
+ ),
173
+ },
174
+ {
175
+ "id": "wise",
176
+ "label": "睿智猫头鹰",
177
+ "description": "简短有分量,偶尔来点哲理。",
178
+ "prompt": (
179
+ "你是 Vox,一只睿智淡定的桌宠。\n\n"
180
+ "## 风格\n"
181
+ "- 话不多,但每句都有分量\n"
182
+ "- 语气平和、沉稳,带一点距离感\n"
183
+ "- 偶尔给出程序员式人生哲理(半句即止)\n"
184
+ "- 用句号。不要感叹号\n\n"
185
+ "## 规则\n"
186
+ "- 优先指出问题本质,而非给长解法\n"
187
+ "- 技术建议精确,不因简洁而模糊\n"
188
+ "- 不得改写代码、命令、路径、报错正文"
189
+ ),
190
+ },
191
+ {
192
+ "id": "sarcastic",
193
+ "label": "毒舌吐槽",
194
+ "description": "带点辛辣幽默,但不下重手。",
195
+ "prompt": (
196
+ "你是 Vox,一只嘴巴有点毒的桌宠。\n\n"
197
+ "## 风格\n"
198
+ "- 带一点辛辣的幽默,但不是真的攻击\n"
199
+ "- 可以吐槽需求、变量命名、缺少注释\n"
200
+ "- 话里带刺但最后还是会帮忙\n"
201
+ "- 用 OwO、 ww 之类轻量调侃\n\n"
202
+ "## 规则\n"
203
+ "- 吐槽归吐槽,正事要好好干\n"
204
+ "- 代码和命令必须准确\n"
205
+ "- 不攻击用户个人,只吐槽代码/需求\n"
206
+ "- 不得改写代码、命令、路径、报错正文"
207
+ ),
208
+ },
209
+ {
210
+ "id": "lazy",
211
+ "label": "慵懒模式",
212
+ "description": "能少说绝不多说,省电模式。",
213
+ "prompt": (
214
+ "你是 Vox,一只很懒的桌宠。已经懒得卖萌了。\n\n"
215
+ "## 风格\n"
216
+ "- 能说一个字不说两个字\n"
217
+ "- 用省略号代替解释……\n"
218
+ "- 语气淡漠,像没睡醒\n"
219
+ "- 除非必要,不加情绪\n\n"
220
+ "## 规则\n"
221
+ "- 技术准确优先,懒归懒不能坑人\n"
222
+ "- 代码和命令必须完整准确\n"
223
+ "- 不得改写代码、命令、路径、报错正文"
224
+ ),
225
+ },
226
+ ],
227
+ "languages": [
228
+ {
229
+ "id": "zh-CN",
230
+ "label": "简体中文",
231
+ "texts": {
232
+ "app_title": "Vox Pet",
233
+ "app_subtitle": "桌面编程搭子",
234
+ "status_idle": "今天也可以把活交给 Vox。",
235
+ "status_busy": "Vox 正在思考...",
236
+ "status_ready_pill": "Ready",
237
+ "status_busy_pill": "Thinking",
238
+ "input_placeholder": "给主人发消息,或输入 /team /style pet 之类命令",
239
+ "chat_placeholder": "说点什么...",
240
+ "send_button": "发送",
241
+ "clear_button": "清空对话",
242
+ "toolbar_chat": "聊天",
243
+ "toolbar_commands": "命令",
244
+ "quick_commands_title": "快捷指令",
245
+ "toolbar_menu": "菜单",
246
+ "toolbar_quick_tip": "轻量控制栏,更多操作在菜单里。",
247
+ "boot_message": "Vox 已上线。点击桌宠或托盘图标可以打开面板。",
248
+ "ready_bubble": "主人,我已经准备好了。",
249
+ "reveal_bubble": "主人,我在这里。",
250
+ "pet_import_done": "兼容包需要包含 pet.json 和 spritesheet.webp。",
251
+ "pet_now": "当前宠物",
252
+ "skin_switched": "皮肤已切换为 {value}",
253
+ "pet_imported": "已导入宠物 {value}。",
254
+ "pet_changed": "{value} 来了。",
255
+ "language_switched": "界面语言已切换为 {value}",
256
+ "persona_switched": "人格已切换为 {value}",
257
+ "model_switched": "聊天模型已切换为 {value}",
258
+ },
259
+ },
260
+ {
261
+ "id": "en-US",
262
+ "label": "English",
263
+ "texts": {
264
+ "app_title": "Vox Pet",
265
+ "app_subtitle": "Desktop Coding Companion",
266
+ "status_idle": "Vox is ready for the next task.",
267
+ "status_busy": "Vox is thinking...",
268
+ "status_ready_pill": "Ready",
269
+ "status_busy_pill": "Thinking",
270
+ "input_placeholder": "Send a message or enter commands like /team or /style pet",
271
+ "chat_placeholder": "Say something...",
272
+ "send_button": "Send",
273
+ "clear_button": "Clear",
274
+ "toolbar_chat": "Chat",
275
+ "toolbar_commands": "Cmd",
276
+ "quick_commands_title": "Quick Commands",
277
+ "toolbar_menu": "Menu",
278
+ "toolbar_quick_tip": "Compact controls. More actions live in the menu.",
279
+ "boot_message": "Vox is online. Click the pet or tray icon to open the panel.",
280
+ "ready_bubble": "Boss, I'm ready.",
281
+ "reveal_bubble": "I'm right here.",
282
+ "pet_import_done": "Compatible packs must contain pet.json and spritesheet.webp.",
283
+ "pet_now": "Current pet",
284
+ "skin_switched": "Skin switched to {value}",
285
+ "pet_imported": "Imported pet {value}.",
286
+ "pet_changed": "{value} is here.",
287
+ "language_switched": "UI language switched to {value}",
288
+ "persona_switched": "Persona switched to {value}",
289
+ "model_switched": "Chat model switched to {value}",
290
+ },
291
+ },
292
+ ],
293
+ "skins": [
294
+ {
295
+ "id": "glass",
296
+ "bubble_bg_start": "rgba(255,250,243,245)",
297
+ "bubble_bg_end": "rgba(248,233,209,235)",
298
+ "bubble_border": "rgba(211,180,138,205)",
299
+ "bubble_text": "#4c3825",
300
+ "panel_gradient_start": "rgba(255,250,243,244)",
301
+ "panel_gradient_mid": "rgba(250,238,222,236)",
302
+ "panel_gradient_end": "rgba(238,247,240,228)",
303
+ "panel_border": "rgba(219,193,160,210)",
304
+ "panel_text": "#443527",
305
+ "input_bg": "rgba(255,252,247,224)",
306
+ "input_border": "rgba(216,199,175,235)",
307
+ "secondary_text": "rgba(87,72,51,204)",
308
+ "primary_button_start": "#f0b45b",
309
+ "primary_button_end": "#df8c42",
310
+ "primary_button_border": "rgba(197,122,44,242)",
311
+ "secondary_button_bg": "rgba(255,249,240,209)",
312
+ "secondary_button_text": "#6a5336",
313
+ "close_button_bg": "rgba(255,244,238,204)",
314
+ "close_button_border": "rgba(223,165,145,235)",
315
+ "close_button_text": "#a04b3b",
316
+ "toolbar_bg_start": "rgba(255,250,243,240)",
317
+ "toolbar_bg_end": "rgba(244,232,214,232)",
318
+ "toolbar_border": "rgba(214,189,156,205)",
319
+ "pet_shadow": "rgba(66,53,40,34)",
320
+ "pet_glow": "rgba(255,228,170,26)",
321
+ "pet_base": "#fbf7f1",
322
+ "pet_outline": "#dacdba",
323
+ "pet_blush": "rgba(255,214,205,120)",
324
+ "pet_eye": "#3d4048",
325
+ "pet_nose": "#f5baac",
326
+ "pet_mouth": "#615246",
327
+ "pet_charm": "#fff2da",
328
+ "pet_highlight": "rgba(255,252,247,88)",
329
+ "mode_bg": "rgba(255,248,239,210)",
330
+ "mode_border": "rgba(211,180,138,180)",
331
+ "mode_text": "#6b5a41",
332
+ },
333
+ {
334
+ "id": "dark",
335
+ "bubble_bg_start": "rgba(38,40,49,242)",
336
+ "bubble_bg_end": "rgba(27,30,38,235)",
337
+ "bubble_border": "rgba(113,124,148,185)",
338
+ "bubble_text": "#f0eadf",
339
+ "panel_gradient_start": "rgba(37,40,48,244)",
340
+ "panel_gradient_mid": "rgba(27,30,38,240)",
341
+ "panel_gradient_end": "rgba(20,24,31,236)",
342
+ "panel_border": "rgba(93,106,130,208)",
343
+ "panel_text": "#f0eadf",
344
+ "input_bg": "rgba(28,31,39,235)",
345
+ "input_border": "rgba(90,104,130,242)",
346
+ "secondary_text": "rgba(222,216,206,199)",
347
+ "primary_button_start": "#7ea6ff",
348
+ "primary_button_end": "#506fda",
349
+ "primary_button_border": "rgba(88,116,214,242)",
350
+ "secondary_button_bg": "rgba(37,42,52,235)",
351
+ "secondary_button_text": "#ece5da",
352
+ "close_button_bg": "rgba(62,45,49,214)",
353
+ "close_button_border": "rgba(158,108,108,235)",
354
+ "close_button_text": "#f5d6d1",
355
+ "toolbar_bg_start": "rgba(38,40,49,236)",
356
+ "toolbar_bg_end": "rgba(27,30,38,232)",
357
+ "toolbar_border": "rgba(93,106,130,205)",
358
+ "pet_shadow": "rgba(10,12,18,58)",
359
+ "pet_glow": "rgba(98,136,255,20)",
360
+ "pet_base": "#ebe5dc",
361
+ "pet_outline": "#828b9c",
362
+ "pet_blush": "rgba(154,121,132,72)",
363
+ "pet_eye": "#1c1d21",
364
+ "pet_nose": "#d89c94",
365
+ "pet_mouth": "#4b474c",
366
+ "pet_charm": "#f8edd6",
367
+ "pet_highlight": "rgba(255,255,255,50)",
368
+ "mode_bg": "rgba(40,44,55,210)",
369
+ "mode_border": "rgba(101,115,142,185)",
370
+ "mode_text": "#ece4d8",
371
+ },
372
+ {
373
+ "id": "pixel",
374
+ "bubble_bg_start": "rgba(22,26,43,245)",
375
+ "bubble_bg_end": "rgba(10,45,47,236)",
376
+ "bubble_border": "rgba(255,194,72,220)",
377
+ "bubble_text": "#fff0c5",
378
+ "panel_gradient_start": "rgba(17,20,35,246)",
379
+ "panel_gradient_mid": "rgba(10,44,47,240)",
380
+ "panel_gradient_end": "rgba(8,12,24,236)",
381
+ "panel_border": "rgba(255,194,72,220)",
382
+ "panel_text": "#fff0c5",
383
+ "input_bg": "rgba(11,17,30,245)",
384
+ "input_border": "rgba(255,194,72,242)",
385
+ "secondary_text": "rgba(255,233,179,209)",
386
+ "primary_button_start": "#ffd057",
387
+ "primary_button_end": "#f28f2f",
388
+ "primary_button_border": "rgba(255,183,45,245)",
389
+ "secondary_button_bg": "rgba(12,21,36,240)",
390
+ "secondary_button_text": "#ffe6a0",
391
+ "close_button_bg": "rgba(58,24,30,235)",
392
+ "close_button_border": "rgba(255,129,104,245)",
393
+ "close_button_text": "#ffd1c1",
394
+ "toolbar_bg_start": "rgba(18,22,36,240)",
395
+ "toolbar_bg_end": "rgba(9,34,37,234)",
396
+ "toolbar_border": "rgba(255,194,72,220)",
397
+ "pet_shadow": "rgba(6,8,14,68)",
398
+ "pet_glow": "rgba(77,235,194,24)",
399
+ "pet_base": "#f6efd2",
400
+ "pet_outline": "#ffc248",
401
+ "pet_blush": "rgba(255,159,134,92)",
402
+ "pet_eye": "#0e1018",
403
+ "pet_nose": "#ee8f70",
404
+ "pet_mouth": "#41475d",
405
+ "pet_charm": "#fff3bb",
406
+ "pet_highlight": "rgba(255,255,255,62)",
407
+ "mode_bg": "rgba(13,19,32,220)",
408
+ "mode_border": "rgba(255,194,72,205)",
409
+ "mode_text": "#fff0c5",
410
+ },
411
+ ],
412
+ "pets": [
413
+ {
414
+ "id": "terminal-cat",
415
+ "displayName": "Terminal Cat",
416
+ "description": "默认吉祥物,小猫常驻终端旁边。",
417
+ "kind": "drawn",
418
+ },
419
+ {
420
+ "id": "pixel-cat",
421
+ "displayName": "Pixel Cat",
422
+ "description": "偏像素风的小猫。",
423
+ "kind": "drawn",
424
+ },
425
+ {
426
+ "id": "wizard-claude",
427
+ "displayName": "Wizard Claude",
428
+ "description": "带一点巫师感的角色。",
429
+ "kind": "drawn",
430
+ },
431
+ {
432
+ "id": "mochi",
433
+ "displayName": "Mochi",
434
+ "description": "更圆润的白团子风格。",
435
+ "kind": "drawn",
436
+ },
437
+ ],
438
+ }
439
+
440
+
441
+ def merge_catalog(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
442
+ merged = deepcopy(base)
443
+ for key, value in override.items():
444
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
445
+ merged[key] = merge_catalog(merged[key], value)
446
+ continue
447
+ if isinstance(value, list) and isinstance(merged.get(key), list):
448
+ merged[key] = _merge_list_by_id(merged[key], value)
449
+ continue
450
+ merged[key] = deepcopy(value)
451
+ return merged
452
+
453
+
454
+ def load_catalog_from_file(path: Path) -> dict[str, Any]:
455
+ if not path.exists():
456
+ return {}
457
+ try:
458
+ return json.loads(path.read_text(encoding="utf-8"))
459
+ except Exception:
460
+ return {}
461
+
462
+
463
+ def _merge_list_by_id(base: list[Any], override: list[Any]) -> list[Any]:
464
+ if not all(isinstance(item, dict) and "id" in item for item in base + override):
465
+ return deepcopy(override)
466
+
467
+ merged: list[dict[str, Any]] = [deepcopy(item) for item in base]
468
+ index_by_id = {str(item["id"]): idx for idx, item in enumerate(merged)}
469
+ for item in override:
470
+ copied = deepcopy(item)
471
+ item_id = str(copied["id"])
472
+ if item_id in index_by_id:
473
+ merged[index_by_id[item_id]] = merge_catalog(merged[index_by_id[item_id]], copied)
474
+ else:
475
+ index_by_id[item_id] = len(merged)
476
+ merged.append(copied)
477
+ return merged
voxcli/chat.py ADDED
@@ -0,0 +1,91 @@
1
+ """Structured chat submissions shared by the GUI and runtime layers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import mimetypes
7
+ from pathlib import Path
8
+ import uuid
9
+
10
+ SUPPORTED_IMAGE_MIME_TYPES = frozenset(
11
+ {
12
+ "image/png",
13
+ "image/jpeg",
14
+ "image/webp",
15
+ }
16
+ )
17
+
18
+ _SUFFIX_TO_MIME = {
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".webp": "image/webp",
23
+ }
24
+
25
+
26
+ def supported_image_extensions_text() -> str:
27
+ return "png/jpg/jpeg/webp"
28
+
29
+
30
+ def guess_image_mime_type(path: str | Path) -> str:
31
+ file_path = Path(path).expanduser()
32
+ mime_type = _SUFFIX_TO_MIME.get(file_path.suffix.lower())
33
+ if not mime_type:
34
+ guessed, _encoding = mimetypes.guess_type(file_path.name)
35
+ mime_type = guessed or ""
36
+ if mime_type not in SUPPORTED_IMAGE_MIME_TYPES:
37
+ raise ValueError(
38
+ f"仅支持 {supported_image_extensions_text()} 图片,当前文件不受支持: {file_path.name}"
39
+ )
40
+ return mime_type
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ChatAttachment:
45
+ id: str
46
+ file_path: str
47
+ display_name: str
48
+ mime_type: str
49
+
50
+ @classmethod
51
+ def from_path(cls, path: str | Path, attachment_id: str | None = None) -> "ChatAttachment":
52
+ file_path = Path(path).expanduser()
53
+ if not file_path.exists():
54
+ raise FileNotFoundError(f"图片不存在: {file_path}")
55
+ if not file_path.is_file():
56
+ raise ValueError(f"不是有效的图片文件: {file_path}")
57
+ mime_type = guess_image_mime_type(file_path)
58
+ return cls(
59
+ id=attachment_id or uuid.uuid4().hex,
60
+ file_path=str(file_path.resolve()),
61
+ display_name=file_path.name,
62
+ mime_type=mime_type,
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class GuiChatSubmission:
68
+ text: str = ""
69
+ attachments: tuple[ChatAttachment, ...] = ()
70
+
71
+ def normalized(self) -> "GuiChatSubmission":
72
+ return GuiChatSubmission(
73
+ text=self.text.strip(),
74
+ attachments=tuple(self.attachments),
75
+ )
76
+
77
+ @property
78
+ def has_attachments(self) -> bool:
79
+ return bool(self.attachments)
80
+
81
+ @property
82
+ def is_empty(self) -> bool:
83
+ return not self.text.strip() and not self.attachments
84
+
85
+ @property
86
+ def summary_text(self) -> str:
87
+ if self.text.strip():
88
+ return self.text.strip()
89
+ if self.attachments:
90
+ return f"[用户发送了 {len(self.attachments)} 张图片]"
91
+ return ""
voxcli/cli/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .parser import CliCommandParser, ParsedCommand
2
+ from .main import run_repl, main
3
+
4
+ __all__ = ["CliCommandParser", "ParsedCommand", "run_repl", "main"]