jarvis-ai-assistant 0.2.3__py3-none-any.whl → 0.2.5__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 (33) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +13 -7
  3. jarvis/jarvis_agent/edit_file_handler.py +4 -0
  4. jarvis/jarvis_agent/jarvis.py +22 -25
  5. jarvis/jarvis_agent/main.py +6 -6
  6. jarvis/jarvis_code_agent/code_agent.py +273 -11
  7. jarvis/jarvis_code_analysis/code_review.py +21 -19
  8. jarvis/jarvis_data/config_schema.json +25 -29
  9. jarvis/jarvis_git_squash/main.py +3 -3
  10. jarvis/jarvis_git_utils/git_commiter.py +32 -11
  11. jarvis/jarvis_mcp/sse_mcp_client.py +4 -6
  12. jarvis/jarvis_mcp/streamable_mcp_client.py +5 -9
  13. jarvis/jarvis_rag/retriever.py +1 -1
  14. jarvis/jarvis_smart_shell/main.py +2 -2
  15. jarvis/jarvis_stats/__init__.py +13 -0
  16. jarvis/jarvis_stats/cli.py +404 -0
  17. jarvis/jarvis_stats/stats.py +538 -0
  18. jarvis/jarvis_stats/storage.py +381 -0
  19. jarvis/jarvis_stats/visualizer.py +282 -0
  20. jarvis/jarvis_tools/cli/main.py +82 -15
  21. jarvis/jarvis_tools/registry.py +32 -16
  22. jarvis/jarvis_tools/search_web.py +3 -3
  23. jarvis/jarvis_tools/virtual_tty.py +315 -26
  24. jarvis/jarvis_utils/config.py +12 -8
  25. jarvis/jarvis_utils/git_utils.py +8 -16
  26. jarvis/jarvis_utils/methodology.py +74 -67
  27. jarvis/jarvis_utils/utils.py +468 -72
  28. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/METADATA +29 -3
  29. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/RECORD +33 -28
  30. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/entry_points.txt +2 -0
  31. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/WHEEL +0 -0
  32. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/licenses/LICENSE +0 -0
  33. {jarvis_ai_assistant-0.2.3.dist-info → jarvis_ai_assistant-0.2.5.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
2
2
  import hashlib
3
3
  import json
4
4
  import os
5
+ import platform
5
6
  import signal
6
7
  import subprocess
7
8
  import sys
@@ -11,6 +12,11 @@ from typing import Any, Callable, Dict, List, Optional
11
12
  from datetime import datetime
12
13
 
13
14
  import yaml # type: ignore
15
+ from rich.align import Align
16
+ from rich.console import Group, RenderableType
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.text import Text
14
20
 
15
21
  from jarvis import __version__
16
22
  from jarvis.jarvis_utils.config import (
@@ -77,6 +83,329 @@ def _check_git_updates() -> bool:
77
83
  return check_and_update_git_repo(str(script_dir))
78
84
 
79
85
 
86
+ def _show_usage_stats() -> None:
87
+ """显示Jarvis使用统计信息"""
88
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
89
+
90
+ try:
91
+ from datetime import datetime
92
+
93
+ from rich.console import Console, Group
94
+ from rich.panel import Panel
95
+ from rich.table import Table
96
+ from rich.text import Text
97
+
98
+ from jarvis.jarvis_stats.stats import StatsManager
99
+
100
+ # 获取所有可用的指标
101
+ all_metrics = StatsManager.list_metrics()
102
+
103
+ # 根据指标名称和标签自动分类
104
+ categorized_stats: Dict[str, Dict[str, Any]] = {
105
+ "tool": {"title": "🔧 工具调用", "metrics": {}, "suffix": "次"},
106
+ "code": {"title": "📝 代码修改", "metrics": {}, "suffix": "次"},
107
+ "lines": {"title": "📊 代码行数", "metrics": {}, "suffix": "行"},
108
+ "commit": {"title": "💾 提交统计", "metrics": {}, "suffix": "个"},
109
+ "command": {"title": "📱 命令使用", "metrics": {}, "suffix": "次"},
110
+ "adoption": {"title": "🎯 采纳情况", "metrics": {}, "suffix": ""},
111
+ }
112
+
113
+ # 遍历所有指标,获取统计数据
114
+ for metric in all_metrics:
115
+ # 获取该指标的所有数据
116
+ stats_data = StatsManager.get_stats(
117
+ metric_name=metric,
118
+ start_time=datetime(2000, 1, 1),
119
+ end_time=datetime.now(),
120
+ )
121
+
122
+ if stats_data and isinstance(stats_data, dict) and "records" in stats_data:
123
+ # 按照标签分组统计
124
+ tag_totals: Dict[str, float] = {}
125
+ for record in stats_data["records"]:
126
+ tags = record.get("tags", {})
127
+ group = tags.get("group", "other")
128
+ tag_totals[group] = tag_totals.get(group, 0) + record["value"]
129
+
130
+ # 根据标签将指标分配到相应类别
131
+ for group, total in tag_totals.items():
132
+ if total > 0:
133
+ if group == "tool":
134
+ categorized_stats["tool"]["metrics"][metric] = int(total)
135
+ elif group == "code_agent":
136
+ # 根据指标名称细分
137
+ if metric.startswith("code_lines_"):
138
+ categorized_stats["lines"]["metrics"][metric] = int(
139
+ total
140
+ )
141
+ elif "commit" in metric:
142
+ categorized_stats["commit"]["metrics"][metric] = int(
143
+ total
144
+ )
145
+ else:
146
+ categorized_stats["code"]["metrics"][metric] = int(
147
+ total
148
+ )
149
+ elif group == "command":
150
+ categorized_stats["command"]["metrics"][metric] = int(total)
151
+
152
+ # 计算采纳率并添加到统计中
153
+ commit_stats = categorized_stats["commit"]["metrics"]
154
+ # 尝试多种可能的指标名称
155
+ generated_commits = commit_stats.get(
156
+ "commits_generated", commit_stats.get("commit_generated", 0)
157
+ )
158
+ accepted_commits = commit_stats.get(
159
+ "commits_accepted",
160
+ commit_stats.get("commit_accepted", commit_stats.get("commit_adopted", 0)),
161
+ )
162
+ rejected_commits = commit_stats.get(
163
+ "commits_rejected", commit_stats.get("commit_rejected", 0)
164
+ )
165
+
166
+ # 如果有 generated 和 accepted,则使用这两个计算采纳率
167
+ if generated_commits > 0 and accepted_commits > 0:
168
+ adoption_rate = (accepted_commits / generated_commits) * 100
169
+ categorized_stats["adoption"]["metrics"][
170
+ "adoption_rate"
171
+ ] = f"{adoption_rate:.1f}%"
172
+ categorized_stats["adoption"]["metrics"][
173
+ "commits_status"
174
+ ] = f"{accepted_commits}/{generated_commits}"
175
+ elif accepted_commits > 0 or rejected_commits > 0:
176
+ # 否则使用 accepted 和 rejected 计算
177
+ total_commits = accepted_commits + rejected_commits
178
+ if total_commits > 0:
179
+ adoption_rate = (accepted_commits / total_commits) * 100
180
+ categorized_stats["adoption"]["metrics"][
181
+ "adoption_rate"
182
+ ] = f"{adoption_rate:.1f}%"
183
+ categorized_stats["adoption"]["metrics"][
184
+ "commits_status"
185
+ ] = f"{accepted_commits}/{total_commits}"
186
+
187
+ # 构建输出
188
+ has_data = False
189
+ stats_output = []
190
+
191
+ for category, data in categorized_stats.items():
192
+ if data["metrics"]:
193
+ has_data = True
194
+ stats_output.append((data["title"], data["metrics"], data["suffix"]))
195
+
196
+ # 显示统计信息
197
+ if has_data:
198
+ # 1. 创建统计表格
199
+ from rich import box
200
+
201
+ table = Table(
202
+ show_header=True,
203
+ header_style="bold magenta",
204
+ title="📊 Jarvis 使用统计",
205
+ title_justify="center",
206
+ box=box.ROUNDED,
207
+ padding=(0, 1),
208
+ )
209
+ table.add_column("分类", style="cyan", no_wrap=True, width=12)
210
+ table.add_column("指标", style="white", width=20)
211
+ table.add_column("数量", style="green", justify="right", width=10)
212
+ table.add_column("分类", style="cyan", no_wrap=True, width=12)
213
+ table.add_column("指标", style="white", width=20)
214
+ table.add_column("数量", style="green", justify="right", width=10)
215
+
216
+ # 收集所有要显示的数据
217
+ all_rows = []
218
+ for title, stats, suffix in stats_output:
219
+ if stats:
220
+ sorted_stats = sorted(
221
+ stats.items(), key=lambda item: item[1], reverse=True
222
+ )
223
+ for i, (metric, count) in enumerate(sorted_stats):
224
+ display_name = metric.replace("_", " ").title()
225
+ category_title = title if i == 0 else ""
226
+ # 处理不同类型的count值
227
+ if isinstance(count, (int, float)):
228
+ count_str = f"{count:,} {suffix}"
229
+ else:
230
+ # 对于字符串类型的count(如百分比或比率),直接使用
231
+ count_str = str(count)
232
+ all_rows.append((category_title, display_name, count_str))
233
+
234
+ # 以3行2列的方式添加数据
235
+ has_content = len(all_rows) > 0
236
+ # 计算需要多少行来显示所有数据
237
+ total_rows = len(all_rows)
238
+ rows_needed = (total_rows + 1) // 2 # 向上取整,因为是2列布局
239
+
240
+ for i in range(rows_needed):
241
+ left_idx = i
242
+ right_idx = i + rows_needed
243
+
244
+ if left_idx < len(all_rows):
245
+ left_row = all_rows[left_idx]
246
+ else:
247
+ left_row = ("", "", "")
248
+
249
+ if right_idx < len(all_rows):
250
+ right_row = all_rows[right_idx]
251
+ else:
252
+ right_row = ("", "", "")
253
+
254
+ table.add_row(
255
+ left_row[0],
256
+ left_row[1],
257
+ left_row[2],
258
+ right_row[0],
259
+ right_row[1],
260
+ right_row[2],
261
+ )
262
+
263
+ # 2. 创建总结面板
264
+ summary_content = []
265
+
266
+ # 总结统计
267
+ total_tools = sum(
268
+ count
269
+ for title, stats, _ in stats_output
270
+ if "工具" in title
271
+ for metric, count in stats.items()
272
+ )
273
+ total_changes = sum(
274
+ count
275
+ for title, stats, _ in stats_output
276
+ if "代码修改" in title
277
+ for metric, count in stats.items()
278
+ )
279
+
280
+ # 统计代码行数
281
+ lines_stats = categorized_stats["lines"]["metrics"]
282
+ total_lines_added = lines_stats.get(
283
+ "code_lines_inserted", lines_stats.get("code_lines_added", 0)
284
+ )
285
+ total_lines_deleted = lines_stats.get("code_lines_deleted", 0)
286
+ total_lines_modified = total_lines_added + total_lines_deleted
287
+
288
+ if total_tools > 0 or total_changes > 0 or total_lines_modified > 0:
289
+ parts = []
290
+ if total_tools > 0:
291
+ parts.append(f"工具调用 {total_tools:,} 次")
292
+ if total_changes > 0:
293
+ parts.append(f"代码修改 {total_changes:,} 次")
294
+ if total_lines_modified > 0:
295
+ parts.append(f"代码行数 {total_lines_modified:,} 行")
296
+
297
+ if parts:
298
+ summary_content.append(f"📈 总计: {', '.join(parts)}")
299
+
300
+ # 计算节省的时间
301
+ time_saved_seconds = 0
302
+ tool_stats = categorized_stats["tool"]["metrics"]
303
+ code_agent_changes = categorized_stats["code"]["metrics"]
304
+ lines_stats = categorized_stats["lines"]["metrics"]
305
+ # commit_stats is already defined above
306
+ command_stats = categorized_stats["command"]["metrics"]
307
+
308
+ # 统一的工具使用时间估算(每次调用节省2分钟)
309
+ DEFAULT_TOOL_TIME_SAVINGS = 2 * 60 # 秒
310
+
311
+ # 计算所有工具的时间节省
312
+ for tool_name, count in tool_stats.items():
313
+ time_saved_seconds += count * DEFAULT_TOOL_TIME_SAVINGS
314
+
315
+ # 其他类型的时间计算
316
+ total_code_agent_calls = sum(code_agent_changes.values())
317
+ time_saved_seconds += total_code_agent_calls * 10 * 60
318
+ time_saved_seconds += lines_stats.get("code_lines_added", 0) * 0.8 * 60
319
+ time_saved_seconds += lines_stats.get("code_lines_deleted", 0) * 0.2 * 60
320
+ time_saved_seconds += sum(commit_stats.values()) * 10 * 60
321
+ time_saved_seconds += sum(command_stats.values()) * 1 * 60
322
+
323
+ time_str = ""
324
+ hours = 0
325
+ if time_saved_seconds > 0:
326
+ total_minutes = int(time_saved_seconds / 60)
327
+ seconds = int(time_saved_seconds % 60)
328
+ hours = total_minutes // 60
329
+ minutes = total_minutes % 60
330
+ # 只显示小时和分钟
331
+ if hours > 0:
332
+ time_str = f"{hours} 小时 {minutes} 分钟"
333
+ elif total_minutes > 0:
334
+ time_str = f"{minutes} 分钟 {seconds} 秒"
335
+ else:
336
+ time_str = f"{seconds} 秒"
337
+
338
+ if summary_content:
339
+ summary_content.append("") # Add a separator line
340
+ summary_content.append(f"⏱️ 节省时间: 约 {time_str}")
341
+
342
+ encouragement = ""
343
+ # 计算各级时间单位
344
+ total_work_days = hours // 8 # 总工作日数
345
+ work_years = total_work_days // 240 # 每年约240个工作日
346
+ remaining_days_after_years = total_work_days % 240
347
+ work_months = remaining_days_after_years // 20 # 每月约20个工作日
348
+ remaining_days_after_months = remaining_days_after_years % 20
349
+ work_days = remaining_days_after_months
350
+ remaining_hours = int(hours % 8) # 剩余不足一个工作日的小时数
351
+
352
+ # 构建时间描述
353
+ time_parts = []
354
+ if work_years > 0:
355
+ time_parts.append(f"{work_years} 年")
356
+ if work_months > 0:
357
+ time_parts.append(f"{work_months} 个月")
358
+ if work_days > 0:
359
+ time_parts.append(f"{work_days} 个工作日")
360
+ if remaining_hours > 0:
361
+ time_parts.append(f"{remaining_hours} 小时")
362
+
363
+ if time_parts:
364
+ time_description = "、".join(time_parts)
365
+ if work_years >= 1:
366
+ encouragement = f"🎉 相当于节省了 {time_description} 的工作时间!"
367
+ elif work_months >= 1:
368
+ encouragement = f"🚀 相当于节省了 {time_description} 的工作时间!"
369
+ elif work_days >= 1:
370
+ encouragement = f"💪 相当于节省了 {time_description} 的工作时间!"
371
+ else:
372
+ encouragement = f"✨ 相当于节省了 {time_description} 的工作时间!"
373
+ elif hours >= 1:
374
+ encouragement = f"⭐ 相当于节省了 {int(hours)} 小时的工作时间,积少成多,继续保持!"
375
+ if encouragement:
376
+ summary_content.append(encouragement)
377
+
378
+ # 3. 组合并打印
379
+ render_items: List[RenderableType] = []
380
+ if has_content:
381
+ # 居中显示表格
382
+ centered_table = Align.center(table)
383
+ render_items.append(centered_table)
384
+
385
+ if summary_content:
386
+ summary_panel = Panel(
387
+ Text("\n".join(summary_content), justify="left"),
388
+ title="✨ 总体表现 ✨",
389
+ title_align="center",
390
+ border_style="green",
391
+ expand=False,
392
+ )
393
+ # 居中显示面板
394
+ centered_panel = Align.center(summary_panel)
395
+ render_items.append(centered_panel)
396
+
397
+ if render_items:
398
+ console = Console()
399
+ render_group = Group(*render_items)
400
+ console.print(render_group)
401
+ except Exception as e:
402
+ # 输出错误信息以便调试
403
+ import traceback
404
+
405
+ PrettyOutput.print(f"统计显示出错: {str(e)}", OutputType.ERROR)
406
+ PrettyOutput.print(traceback.format_exc(), OutputType.ERROR)
407
+
408
+
80
409
  def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
81
410
  """初始化Jarvis环境
82
411
 
@@ -99,7 +428,11 @@ def init_env(welcome_str: str, config_file: Optional[str] = None) -> None:
99
428
  g_config_file = config_file
100
429
  load_config()
101
430
 
102
- # 5. 检查git更新
431
+ # 5. 显示历史统计数据(仅在显示欢迎信息时显示)
432
+ if welcome_str:
433
+ _show_usage_stats()
434
+
435
+ # 6. 检查git更新
103
436
  if _check_git_updates():
104
437
  os.execv(sys.executable, [sys.executable] + sys.argv)
105
438
  sys.exit(0)
@@ -243,14 +576,7 @@ def generate_default_config(schema_path: str, output_path: str) -> None:
243
576
 
244
577
  default_config = _generate_from_schema(schema)
245
578
 
246
- # 添加schema声明
247
- rel_schema_path = Path(
248
- os.path.relpath(
249
- Path(schema_path),
250
- start=Path(output_path).parent,
251
- )
252
- )
253
- content = f"# yaml-language-server: $schema={rel_schema_path}\n"
579
+ content = f"# yaml-language-server: $schema={schema}\n"
254
580
  content += yaml.dump(default_config, allow_unicode=True, sort_keys=False)
255
581
 
256
582
  with open(output_path, "w", encoding="utf-8") as f:
@@ -386,35 +712,54 @@ def get_file_line_count(filename: str) -> int:
386
712
  return 0
387
713
 
388
714
 
389
- def _get_cmd_stats() -> Dict[str, int]:
390
- """从数据目录获取命令调用统计"""
391
- stats_file = Path(get_data_dir()) / "cmd_stat.yaml"
392
- if stats_file.exists():
393
- try:
394
- with open(stats_file, "r", encoding="utf-8") as f:
395
- return yaml.safe_load(f) or {}
396
- except Exception as e:
397
- PrettyOutput.print(f"加载命令调用统计失败: {str(e)}", OutputType.WARNING)
398
- return {}
399
-
400
-
401
- def _update_cmd_stats(cmd_name: str) -> None:
402
- """更新命令调用统计"""
403
- stats = _get_cmd_stats()
404
- stats[cmd_name] = stats.get(cmd_name, 0) + 1
405
- stats_file = Path(get_data_dir()) / "cmd_stat.yaml"
406
- try:
407
- with open(stats_file, "w", encoding="utf-8") as f:
408
- yaml.safe_dump(stats, f, allow_unicode=True)
409
- except Exception as e:
410
- PrettyOutput.print(f"保存命令调用统计失败: {str(e)}", OutputType.WARNING)
411
-
412
-
413
715
  def count_cmd_usage() -> None:
414
716
  """统计当前命令的使用次数"""
415
717
  import sys
718
+ import os
719
+ from jarvis.jarvis_stats.stats import StatsManager
720
+
721
+ # 命令映射关系:将短命令映射到长命令
722
+ command_mapping = {
723
+ # jarvis主命令
724
+ "jvs": "jarvis",
725
+ # 代码代理
726
+ "jca": "jarvis-code-agent",
727
+ # 智能shell
728
+ "jss": "jarvis-smart-shell",
729
+ # 平台管理
730
+ "jpm": "jarvis-platform-manager",
731
+ # Git提交
732
+ "jgc": "jarvis-git-commit",
733
+ # 代码审查
734
+ "jcr": "jarvis-code-review",
735
+ # Git压缩
736
+ "jgs": "jarvis-git-squash",
737
+ # 多代理
738
+ "jma": "jarvis-multi-agent",
739
+ # 代理
740
+ "ja": "jarvis-agent",
741
+ # 工具
742
+ "jt": "jarvis-tool",
743
+ # 方法论
744
+ "jm": "jarvis-methodology",
745
+ # RAG
746
+ "jrg": "jarvis-rag",
747
+ # 统计
748
+ "jst": "jarvis-stats",
749
+ }
750
+
751
+ # 从完整路径中提取命令名称
752
+ cmd_path = sys.argv[0]
753
+ cmd_name = os.path.basename(cmd_path)
754
+
755
+ # 如果是短命令,映射到长命令
756
+ if cmd_name in command_mapping:
757
+ metric_name = command_mapping[cmd_name]
758
+ else:
759
+ metric_name = cmd_name
416
760
 
417
- _update_cmd_stats(sys.argv[0])
761
+ # 使用 StatsManager 记录命令使用统计
762
+ StatsManager.increment(metric_name, group="command")
418
763
 
419
764
 
420
765
  def is_context_overflow(
@@ -433,14 +778,16 @@ def get_loc_stats() -> str:
433
778
  str: loc命令输出的原始字符串,失败时返回空字符串
434
779
  """
435
780
  try:
436
- result = subprocess.run(["loc"], capture_output=True, text=True)
781
+ result = subprocess.run(
782
+ ["loc"], capture_output=True, text=True, encoding="utf-8", errors="replace"
783
+ )
437
784
  return result.stdout if result.returncode == 0 else ""
438
785
  except FileNotFoundError:
439
786
  return ""
440
787
 
441
788
 
442
789
  def copy_to_clipboard(text: str) -> None:
443
- """将文本复制到剪贴板,依次尝试xselxclip (非阻塞)
790
+ """将文本复制到剪贴板,支持Windows、macOSLinux
444
791
 
445
792
  参数:
446
793
  text: 要复制的文本
@@ -448,41 +795,80 @@ def copy_to_clipboard(text: str) -> None:
448
795
  print("--- 剪贴板内容开始 ---")
449
796
  print(text)
450
797
  print("--- 剪贴板内容结束 ---")
451
- # 尝试使用 xsel
452
- try:
453
- process = subprocess.Popen(
454
- ["xsel", "-b", "-i"],
455
- stdin=subprocess.PIPE,
456
- stdout=subprocess.DEVNULL,
457
- stderr=subprocess.DEVNULL,
458
- )
459
- if process.stdin:
460
- process.stdin.write(text.encode("utf-8"))
461
- process.stdin.close()
462
- return
463
- except FileNotFoundError:
464
- pass # xsel 未安装,继续尝试下一个
465
- except Exception as e:
466
- PrettyOutput.print(f"使用xsel时出错: {e}", OutputType.WARNING)
467
798
 
468
- # 尝试使用 xclip
469
- try:
470
- process = subprocess.Popen(
471
- ["xclip", "-selection", "clipboard"],
472
- stdin=subprocess.PIPE,
473
- stdout=subprocess.DEVNULL,
474
- stderr=subprocess.DEVNULL,
475
- )
476
- if process.stdin:
477
- process.stdin.write(text.encode("utf-8"))
478
- process.stdin.close()
479
- return
480
- except FileNotFoundError:
481
- PrettyOutput.print(
482
- "xsel 和 xclip 均未安装, 无法复制到剪贴板", OutputType.WARNING
483
- )
484
- except Exception as e:
485
- PrettyOutput.print(f"使用xclip时出错: {e}", OutputType.WARNING)
799
+ system = platform.system()
800
+
801
+ # Windows系统
802
+ if system == "Windows":
803
+ try:
804
+ # 使用Windows的clip命令
805
+ process = subprocess.Popen(
806
+ ["clip"],
807
+ stdin=subprocess.PIPE,
808
+ stdout=subprocess.DEVNULL,
809
+ stderr=subprocess.DEVNULL,
810
+ shell=True,
811
+ )
812
+ if process.stdin:
813
+ process.stdin.write(text.encode("utf-8"))
814
+ process.stdin.close()
815
+ return
816
+ except Exception as e:
817
+ PrettyOutput.print(f"使用Windows clip命令时出错: {e}", OutputType.WARNING)
818
+
819
+ # macOS系统
820
+ elif system == "Darwin":
821
+ try:
822
+ process = subprocess.Popen(
823
+ ["pbcopy"],
824
+ stdin=subprocess.PIPE,
825
+ stdout=subprocess.DEVNULL,
826
+ stderr=subprocess.DEVNULL,
827
+ )
828
+ if process.stdin:
829
+ process.stdin.write(text.encode("utf-8"))
830
+ process.stdin.close()
831
+ return
832
+ except Exception as e:
833
+ PrettyOutput.print(f"使用macOS pbcopy命令时出错: {e}", OutputType.WARNING)
834
+
835
+ # Linux系统
836
+ else:
837
+ # 尝试使用 xsel
838
+ try:
839
+ process = subprocess.Popen(
840
+ ["xsel", "-b", "-i"],
841
+ stdin=subprocess.PIPE,
842
+ stdout=subprocess.DEVNULL,
843
+ stderr=subprocess.DEVNULL,
844
+ )
845
+ if process.stdin:
846
+ process.stdin.write(text.encode("utf-8"))
847
+ process.stdin.close()
848
+ return
849
+ except FileNotFoundError:
850
+ pass # xsel 未安装,继续尝试下一个
851
+ except Exception as e:
852
+ PrettyOutput.print(f"使用xsel时出错: {e}", OutputType.WARNING)
853
+
854
+ # 尝试使用 xclip
855
+ try:
856
+ process = subprocess.Popen(
857
+ ["xclip", "-selection", "clipboard"],
858
+ stdin=subprocess.PIPE,
859
+ stdout=subprocess.DEVNULL,
860
+ stderr=subprocess.DEVNULL,
861
+ )
862
+ if process.stdin:
863
+ process.stdin.write(text.encode("utf-8"))
864
+ process.stdin.close()
865
+ return
866
+ except FileNotFoundError:
867
+ PrettyOutput.print(
868
+ "xsel 和 xclip 均未安装, 无法复制到剪贴板", OutputType.WARNING
869
+ )
870
+ except Exception as e:
871
+ PrettyOutput.print(f"使用xclip时出错: {e}", OutputType.WARNING)
486
872
 
487
873
 
488
874
  def _pull_git_repo(repo_path: Path, repo_type: str):
@@ -499,6 +885,8 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
499
885
  cwd=repo_path,
500
886
  capture_output=True,
501
887
  text=True,
888
+ encoding="utf-8",
889
+ errors="replace",
502
890
  check=True,
503
891
  timeout=10,
504
892
  )
@@ -515,6 +903,8 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
515
903
  cwd=repo_path,
516
904
  capture_output=True,
517
905
  text=True,
906
+ encoding="utf-8",
907
+ errors="replace",
518
908
  check=True,
519
909
  timeout=10,
520
910
  )
@@ -531,6 +921,8 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
531
921
  cwd=repo_path,
532
922
  capture_output=True,
533
923
  text=True,
924
+ encoding="utf-8",
925
+ errors="replace",
534
926
  check=True,
535
927
  timeout=10,
536
928
  )
@@ -558,11 +950,15 @@ def _pull_git_repo(repo_path: Path, repo_type: str):
558
950
  after_hash = after_hash_result.stdout.strip()
559
951
 
560
952
  if before_hash != after_hash:
561
- PrettyOutput.print(f"{repo_type}库 '{repo_path.name}' 已更新。", OutputType.SUCCESS)
953
+ PrettyOutput.print(
954
+ f"{repo_type}库 '{repo_path.name}' 已更新。", OutputType.SUCCESS
955
+ )
562
956
  if pull_result.stdout.strip():
563
957
  PrettyOutput.print(pull_result.stdout.strip(), OutputType.INFO)
564
958
  else:
565
- PrettyOutput.print(f"{repo_type}库 '{repo_path.name}' 已是最新版本。", OutputType.INFO)
959
+ PrettyOutput.print(
960
+ f"{repo_type}库 '{repo_path.name}' 已是最新版本。", OutputType.INFO
961
+ )
566
962
 
567
963
  except FileNotFoundError:
568
964
  PrettyOutput.print(