jarvis-ai-assistant 0.3.28__py3-none-any.whl → 0.3.29__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 (36) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +60 -25
  3. jarvis/jarvis_agent/event_bus.py +2 -2
  4. jarvis/jarvis_agent/events.py +157 -0
  5. jarvis/jarvis_agent/file_methodology_manager.py +17 -4
  6. jarvis/jarvis_agent/jarvis.py +3 -3
  7. jarvis/jarvis_agent/memory_manager.py +4 -3
  8. jarvis/jarvis_agent/prompts.py +2 -2
  9. jarvis/jarvis_agent/protocols.py +4 -1
  10. jarvis/jarvis_agent/run_loop.py +9 -24
  11. jarvis/jarvis_agent/shell_input_handler.py +6 -1
  12. jarvis/jarvis_agent/task_analyzer.py +18 -13
  13. jarvis/jarvis_agent/task_manager.py +6 -4
  14. jarvis/jarvis_agent/utils.py +50 -0
  15. jarvis/jarvis_mcp/sse_mcp_client.py +1 -1
  16. jarvis/jarvis_memory_organizer/memory_organizer.py +4 -4
  17. jarvis/jarvis_platform/kimi.py +1 -1
  18. jarvis/jarvis_platform/tongyi.py +1 -1
  19. jarvis/jarvis_platform/yuanbao.py +1 -1
  20. jarvis/jarvis_rag/retriever.py +3 -3
  21. jarvis/jarvis_stats/stats.py +2 -2
  22. jarvis/jarvis_stats/storage.py +3 -3
  23. jarvis/jarvis_tools/edit_file.py +3 -3
  24. jarvis/jarvis_tools/execute_script.py +2 -2
  25. jarvis/jarvis_tools/sub_agent.py +2 -2
  26. jarvis/jarvis_tools/sub_code_agent.py +2 -2
  27. jarvis/jarvis_utils/config.py +3 -3
  28. jarvis/jarvis_utils/fzf.py +4 -3
  29. jarvis/jarvis_utils/input.py +5 -5
  30. jarvis/jarvis_utils/utils.py +96 -9
  31. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/METADATA +1 -1
  32. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/RECORD +36 -34
  33. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/WHEEL +0 -0
  34. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/entry_points.txt +0 -0
  35. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/licenses/LICENSE +0 -0
  36. {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,8 @@ from jarvis.jarvis_utils.globals import get_interrupt, set_interrupt
8
8
 
9
9
  from jarvis.jarvis_agent.prompts import TASK_ANALYSIS_PROMPT
10
10
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
+ from jarvis.jarvis_agent.utils import join_prompts
12
+ from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL, BEFORE_SUMMARY, TASK_COMPLETED
11
13
 
12
14
 
13
15
  class TaskAnalyzer:
@@ -55,10 +57,7 @@ class TaskAnalyzer:
55
57
 
56
58
  def _prepare_analysis_prompt(self, satisfaction_feedback: str) -> str:
57
59
  """准备分析提示"""
58
- analysis_prompt = TASK_ANALYSIS_PROMPT
59
- if satisfaction_feedback:
60
- analysis_prompt += satisfaction_feedback
61
- return analysis_prompt
60
+ return join_prompts([TASK_ANALYSIS_PROMPT, satisfaction_feedback])
62
61
 
63
62
  def _process_analysis_loop(self):
64
63
  """处理分析循环"""
@@ -74,7 +73,7 @@ class TaskAnalyzer:
74
73
  # 执行工具调用(补充事件:before_tool_call/after_tool_call)
75
74
  try:
76
75
  self.agent.event_bus.emit(
77
- "before_tool_call",
76
+ BEFORE_TOOL_CALL,
78
77
  agent=self.agent,
79
78
  current_response=response,
80
79
  )
@@ -84,7 +83,7 @@ class TaskAnalyzer:
84
83
  self.agent.session.prompt = tool_prompt
85
84
  try:
86
85
  self.agent.event_bus.emit(
87
- "after_tool_call",
86
+ AFTER_TOOL_CALL,
88
87
  agent=self.agent,
89
88
  current_response=response,
90
89
  need_return=need_return,
@@ -130,9 +129,15 @@ class TaskAnalyzer:
130
129
  def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
131
130
  """处理有工具调用时的中断"""
132
131
  if self.agent.user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
133
- return f"被用户中断,用户补充信息为:{user_input}\n\n用户同意继续工具调用。"
132
+ return join_prompts([
133
+ f"被用户中断,用户补充信息为:{user_input}",
134
+ "用户同意继续工具调用。"
135
+ ])
134
136
  else:
135
- return f"被用户中断,用户补充信息为:{user_input}\n\n检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
137
+ return join_prompts([
138
+ f"被用户中断,用户补充信息为:{user_input}",
139
+ "检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
140
+ ])
136
141
 
137
142
  def collect_satisfaction_feedback(self, auto_completed: bool) -> str:
138
143
  """收集满意度反馈"""
@@ -140,18 +145,18 @@ class TaskAnalyzer:
140
145
 
141
146
  if not auto_completed and self.agent.use_analysis:
142
147
  if self.agent.user_confirm("您对本次任务的完成是否满意?", True):
143
- satisfaction_feedback = "\n\n用户对本次任务的完成表示满意。"
148
+ satisfaction_feedback = "用户对本次任务的完成表示满意。"
144
149
  else:
145
150
  feedback = self.agent._multiline_input(
146
151
  "请提供您的反馈意见(可留空直接回车):", False
147
152
  )
148
153
  if feedback:
149
154
  satisfaction_feedback = (
150
- f"\n\n用户对本次任务的完成不满意,反馈意见如下:\n{feedback}"
155
+ f"用户对本次任务的完成不满意,反馈意见如下:\n{feedback}"
151
156
  )
152
157
  else:
153
158
  satisfaction_feedback = (
154
- "\n\n用户对本次任务的完成不满意,未提供具体反馈意见。"
159
+ "用户对本次任务的完成不满意,未提供具体反馈意见。"
155
160
  )
156
161
 
157
162
  return satisfaction_feedback
@@ -162,9 +167,9 @@ class TaskAnalyzer:
162
167
  def _subscribe_events(self) -> None:
163
168
  bus = self.agent.get_event_bus() # type: ignore[attr-defined]
164
169
  # 在生成总结前触发(保持与原顺序一致)
165
- bus.subscribe("before_summary", self._on_before_summary)
170
+ bus.subscribe(BEFORE_SUMMARY, self._on_before_summary)
166
171
  # 当无需总结时,作为兜底触发分析
167
- bus.subscribe("task_completed", self._on_task_completed)
172
+ bus.subscribe(TASK_COMPLETED, self._on_task_completed)
168
173
 
169
174
  def _on_before_summary(self, **payload) -> None:
170
175
  if self._analysis_done:
@@ -14,6 +14,7 @@ from jarvis.jarvis_agent import (
14
14
  get_multiline_input,
15
15
  user_confirm,
16
16
  )
17
+ from jarvis.jarvis_agent.utils import join_prompts
17
18
  from jarvis.jarvis_utils.config import get_data_dir
18
19
  from jarvis.jarvis_utils.fzf import fzf_select
19
20
 
@@ -109,7 +110,7 @@ class TaskManager:
109
110
  if need_additional:
110
111
  additional_input = get_multiline_input("请输入补充信息:")
111
112
  if additional_input:
112
- selected_task = f"{selected_task}\n\n补充信息:\n{additional_input}"
113
+ selected_task = join_prompts([selected_task, f"补充信息:\n{additional_input}"])
113
114
  return selected_task
114
115
  except Exception:
115
116
  # 如果解析失败,则回退到手动输入
@@ -138,9 +139,10 @@ class TaskManager:
138
139
  if need_additional:
139
140
  additional_input = get_multiline_input("请输入补充信息:")
140
141
  if additional_input:
141
- selected_task = (
142
- f"{selected_task}\n\n补充信息:\n{additional_input}"
143
- )
142
+ selected_task = join_prompts([
143
+ selected_task,
144
+ f"补充信息:\n{additional_input}"
145
+ ])
144
146
  return selected_task
145
147
  PrettyOutput.print(
146
148
  "无效的选择。请选择列表中的一个号码。", OutputType.WARNING
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 工具函数(jarvis_agent.utils)
4
+
5
+ - join_prompts: 统一的提示拼接策略(仅拼接非空段落,使用双换行)
6
+ - is_auto_complete: 统一的自动完成标记检测
7
+ """
8
+ from typing import Iterable, List, Any
9
+ from enum import Enum
10
+ from jarvis.jarvis_utils.tag import ot
11
+
12
+ def join_prompts(parts: Iterable[str]) -> str:
13
+ """
14
+ 将多个提示片段按统一规则拼接:
15
+ - 过滤掉空字符串
16
+ - 使用两个换行分隔
17
+ - 不进行额外 strip,保持调用方原样语义
18
+ """
19
+ non_empty: List[str] = [p for p in parts if p]
20
+ return "\n\n".join(non_empty)
21
+
22
+ def is_auto_complete(response: str) -> bool:
23
+ """
24
+ 检测是否包含自动完成标记。
25
+ 当前实现:包含 ot('!!!COMPLETE!!!') 即视为自动完成。
26
+ """
27
+ try:
28
+ return ot("!!!COMPLETE!!!") in response
29
+ except Exception:
30
+ # 防御性处理:即使 ot 出现异常,也不阻塞主流程
31
+ return "!!!COMPLETE!!!" in response
32
+
33
+ def normalize_next_action(next_action: Any) -> str:
34
+ """
35
+ 规范化下一步动作为字符串:
36
+ - 如果是 Enum, 返回其 value(若为字符串)
37
+ - 如果是 str, 原样返回
38
+ - 其他情况返回空字符串
39
+ """
40
+ try:
41
+ if isinstance(next_action, Enum):
42
+ value = getattr(next_action, "value", None)
43
+ return value if isinstance(value, str) else ""
44
+ if isinstance(next_action, str):
45
+ return next_action
46
+ return ""
47
+ except Exception:
48
+ return ""
49
+
50
+ __all__ = ["join_prompts", "is_auto_complete", "normalize_next_action"]
@@ -210,7 +210,7 @@ class SSEMcpClient(McpClient):
210
210
 
211
211
  # 调用已注册的处理器
212
212
  if method in self.notification_handlers:
213
- error_lines: list[str] = []
213
+ error_lines: List[str] = []
214
214
  for handler in self.notification_handlers[method]:
215
215
  try:
216
216
  handler(params)
@@ -63,7 +63,7 @@ class MemoryOrganizer:
63
63
  """加载指定类型的所有记忆"""
64
64
  memories = []
65
65
  memory_files = self._get_memory_files(memory_type)
66
- error_lines: list[str] = []
66
+ error_lines: List[str] = []
67
67
 
68
68
  for memory_file in memory_files:
69
69
  try:
@@ -388,8 +388,8 @@ tags:
388
388
  )
389
389
 
390
390
  # 删除原始记忆文件(先汇总日志,最后统一打印)
391
- info_lines: list[str] = []
392
- warn_lines: list[str] = []
391
+ info_lines: List[str] = []
392
+ warn_lines: List[str] = []
393
393
  for orig_memory in original_memories:
394
394
  if "file_path" in orig_memory:
395
395
  try:
@@ -428,7 +428,7 @@ tags:
428
428
  导出的记忆数量
429
429
  """
430
430
  all_memories = []
431
- progress_lines: list[str] = []
431
+ progress_lines: List[str] = []
432
432
 
433
433
  for memory_type in memory_types:
434
434
  progress_lines.append(f"正在导出 {memory_type} 类型的记忆...")
@@ -194,7 +194,7 @@ class KimiModel(BasePlatform):
194
194
  uploaded_files = []
195
195
  for index, file_path in enumerate(file_list, 1):
196
196
  file_name = os.path.basename(file_path)
197
- log_lines: list[str] = [f"处理文件 [{index}/{len(file_list)}]: {file_name}"]
197
+ log_lines: List[str] = [f"处理文件 [{index}/{len(file_list)}]: {file_name}"]
198
198
  try:
199
199
  mime_type, _ = mimetypes.guess_type(file_path)
200
200
  action = (
@@ -276,7 +276,7 @@ class TongyiPlatform(BasePlatform):
276
276
 
277
277
  for file_path in file_list:
278
278
  file_name = os.path.basename(file_path)
279
- log_lines: list[str] = []
279
+ log_lines: List[str] = []
280
280
  log_lines.append(f"上传文件 {file_name}")
281
281
  try:
282
282
  if not os.path.exists(file_path):
@@ -133,7 +133,7 @@ class YuanbaoPlatform(BasePlatform):
133
133
 
134
134
  for file_path in file_list:
135
135
  file_name = os.path.basename(file_path)
136
- log_lines: list[str] = []
136
+ log_lines: List[str] = []
137
137
  log_lines.append(f"上传文件 {file_name}")
138
138
  try:
139
139
  # 1. Prepare the file information
@@ -185,7 +185,7 @@ class ChromaRetriever:
185
185
  if not changed and not deleted:
186
186
  return
187
187
  # 为避免在循环中逐条打印,先拼接后统一打印
188
- lines: list[str] = []
188
+ lines: List[str] = []
189
189
  if changed:
190
190
  lines.append(
191
191
  f"检测到 {len(changed)} 个已索引文件发生变化,建议重新索引以保证检索准确性。"
@@ -247,7 +247,7 @@ class ChromaRetriever:
247
247
  return
248
248
 
249
249
  # 先处理删除
250
- delete_errors: list[str] = []
250
+ delete_errors: List[str] = []
251
251
  for src in deleted:
252
252
  try:
253
253
  self.collection.delete(where={"source": src}) # type: ignore[arg-type]
@@ -258,7 +258,7 @@ class ChromaRetriever:
258
258
 
259
259
  # 再处理变更(重建)
260
260
  docs_to_add: List[Document] = []
261
- rebuild_errors: list[str] = []
261
+ rebuild_errors: List[str] = []
262
262
  for src in changed:
263
263
  try:
264
264
  # 删除旧条目
@@ -604,8 +604,8 @@ class StatsManager:
604
604
 
605
605
  @staticmethod
606
606
  def _show_multiple_charts(
607
- start_time: datetime,
608
- end_time: datetime,
607
+ start_time: Optional[datetime],
608
+ end_time: Optional[datetime],
609
609
  aggregation: str,
610
610
  tags: Optional[Dict[str, str]],
611
611
  width: Optional[int] = None,
@@ -8,7 +8,7 @@ import json
8
8
  import os
9
9
  from datetime import datetime, timedelta
10
10
  from pathlib import Path
11
- from typing import Dict, List, Optional, Any
11
+ from typing import Dict, List, Optional, Any, Set
12
12
  from collections import defaultdict
13
13
  import sys
14
14
  import time
@@ -457,7 +457,7 @@ class StatsStorage:
457
457
  metrics_from_meta = set(meta.get("metrics", {}).keys())
458
458
 
459
459
  # 扫描所有数据文件获取实际存在的指标
460
- metrics_from_data: set[str] = set()
460
+ metrics_from_data: Set[str] = set()
461
461
  for data_file in self.data_dir.glob("stats_*.json"):
462
462
  try:
463
463
  data = self._load_json(data_file)
@@ -467,7 +467,7 @@ class StatsStorage:
467
467
  continue
468
468
 
469
469
  # 扫描总量缓存目录中已有的指标文件
470
- metrics_from_totals: set[str] = set()
470
+ metrics_from_totals: Set[str] = set()
471
471
  try:
472
472
  for f in self.totals_dir.glob("*"):
473
473
  if f.is_file():
@@ -14,7 +14,7 @@
14
14
  - 完善的错误处理和回滚机制
15
15
  - 严格的格式保持要求
16
16
  """
17
- from typing import Any, Dict
17
+ from typing import Any, Dict, List
18
18
 
19
19
  from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
20
20
 
@@ -122,7 +122,7 @@ class FileSearchReplaceTool:
122
122
 
123
123
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
124
124
 
125
- stdout_messages: list[str] = []
125
+ stdout_messages: List[str] = []
126
126
  overall_success = False
127
127
  file_results = []
128
128
 
@@ -167,7 +167,7 @@ class FileSearchReplaceTool:
167
167
  )
168
168
 
169
169
  # 整合所有错误信息到stderr
170
- all_stderr: list[str] = []
170
+ all_stderr: List[str] = []
171
171
  for file_result in file_results:
172
172
  if not file_result["success"]:
173
173
  all_stderr.append(f"文件 {file_result['file']} 处理失败: {file_result['stderr']}")
@@ -2,7 +2,7 @@
2
2
  import os
3
3
  import tempfile
4
4
  from pathlib import Path
5
- from typing import Any, Dict
5
+ from typing import Any, Dict, List
6
6
 
7
7
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
8
8
 
@@ -74,7 +74,7 @@ class ScriptTool:
74
74
  stream.feed(data)
75
75
 
76
76
  # 清理每行右侧空格,并过滤空行
77
- cleaned: list[str] = []
77
+ cleaned: List[str] = []
78
78
  for y in range(screen.lines):
79
79
  line = screen.buffer[y]
80
80
  stripped = "".join(char.data for char in line.values()).rstrip()
@@ -8,7 +8,7 @@ sub_agent 工具
8
8
  - 继承父 Agent 的部分配置:model_group、input_handler、execute_tool_confirm、multiline_inputer;其他参数需显式提供
9
9
  - 子Agent必须自动完成(auto_complete=True)且需要summary(need_summary=True)
10
10
  """
11
- from typing import Any, Dict
11
+ from typing import Any, Dict, List
12
12
  import json
13
13
 
14
14
  from jarvis.jarvis_agent import Agent
@@ -107,7 +107,7 @@ class SubAgentTool:
107
107
 
108
108
  # 解析可用工具列表(支持数组或以逗号分隔的字符串)
109
109
  _use_tools = args.get("use_tools", None)
110
- use_tools: list[str] = []
110
+ use_tools: List[str] = []
111
111
  if isinstance(_use_tools, list):
112
112
  use_tools = [str(x).strip() for x in _use_tools if str(x).strip()]
113
113
  elif isinstance(_use_tools, str):
@@ -8,7 +8,7 @@ sub_code_agent 工具
8
8
  - 不依赖父 Agent,所有配置使用系统默认与全局变量
9
9
  - 子Agent必须自动完成(auto_complete=True)且需要summary(need_summary=True)
10
10
  """
11
- from typing import Any, Dict
11
+ from typing import Any, Dict, List
12
12
  import json
13
13
 
14
14
  from jarvis.jarvis_code_agent.code_agent import CodeAgent
@@ -85,7 +85,7 @@ class SubCodeAgentTool:
85
85
  except Exception:
86
86
  parent_agent = None
87
87
  model_group = None
88
- use_tools: list[str] = []
88
+ use_tools: List[str] = []
89
89
  try:
90
90
  if parent_agent is not None:
91
91
  if getattr(parent_agent, "model", None):
@@ -285,7 +285,7 @@ def get_pretty_output() -> bool:
285
285
  if platform.system() == "Windows":
286
286
  return False
287
287
 
288
- return GLOBAL_CONFIG_DATA.get("JARVIS_PRETTY_OUTPUT", False)
288
+ return GLOBAL_CONFIG_DATA.get("JARVIS_PRETTY_OUTPUT", True)
289
289
 
290
290
 
291
291
  def is_use_methodology() -> bool:
@@ -423,9 +423,9 @@ def is_force_save_memory() -> bool:
423
423
  获取是否强制保存记忆。
424
424
 
425
425
  返回:
426
- bool: 如果强制保存记忆则返回True,默认为True
426
+ bool: 如果强制保存记忆则返回True,默认为False
427
427
  """
428
- return GLOBAL_CONFIG_DATA.get("JARVIS_FORCE_SAVE_MEMORY", True) is True
428
+ return GLOBAL_CONFIG_DATA.get("JARVIS_FORCE_SAVE_MEMORY", False) is True
429
429
 
430
430
 
431
431
  def is_enable_static_analysis() -> bool:
@@ -2,10 +2,10 @@
2
2
  """FZF selector utility."""
3
3
  import shutil
4
4
  import subprocess
5
- from typing import List, Optional, Union
5
+ from typing import List, Optional, Union, Dict, Any, cast
6
6
 
7
7
  def fzf_select(
8
- options: Union[List[str], List[dict]],
8
+ options: Union[List[str], List[Dict[str, Any]]],
9
9
  prompt: str = "SELECT> ",
10
10
  key: Optional[str] = None,
11
11
  ) -> Optional[str]:
@@ -29,7 +29,8 @@ def fzf_select(
29
29
  if isinstance(options[0], dict):
30
30
  if key is None:
31
31
  raise ValueError("A key must be provided for a list of dicts.")
32
- input_lines = [str(item.get(key, "")) for item in options]
32
+ options_dict = cast(List[Dict[str, Any]], options)
33
+ input_lines = [str(item.get(key, "")) for item in options_dict]
33
34
  else:
34
35
  input_lines = [str(item) for item in options]
35
36
 
@@ -11,7 +11,7 @@
11
11
  import os
12
12
  import sys
13
13
  import base64
14
- from typing import Iterable, List
14
+ from typing import Iterable, List, Optional
15
15
  import wcwidth
16
16
 
17
17
  from colorama import Fore
@@ -308,7 +308,7 @@ class FileCompleter(Completer):
308
308
  import os as _os
309
309
 
310
310
  if self._all_files_cache is None:
311
- files: list[str] = []
311
+ files: List[str] = []
312
312
  for root, dirs, fnames in _os.walk(".", followlinks=False):
313
313
  # Exclude .git directory
314
314
  dirs[:] = [d for d in dirs if d != ".git"]
@@ -429,7 +429,7 @@ def _show_history_and_copy():
429
429
 
430
430
 
431
431
  def _get_multiline_input_internal(
432
- tip: str, preset: str | None = None, preset_cursor: int | None = None
432
+ tip: str, preset: Optional[str] = None, preset_cursor: Optional[int] = None
433
433
  ) -> str:
434
434
  """
435
435
  Internal function to get multiline input using prompt_toolkit.
@@ -660,8 +660,8 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
660
660
  tip: 提示文本,将显示在底部工具栏中
661
661
  print_on_empty: 当输入为空字符串时,是否打印“输入已取消”提示。默认打印。
662
662
  """
663
- preset: str | None = None
664
- preset_cursor: int | None = None
663
+ preset: Optional[str] = None
664
+ preset_cursor: Optional[int] = None
665
665
  while True:
666
666
  user_input = _get_multiline_input_internal(
667
667
  tip, preset=preset, preset_cursor=preset_cursor
@@ -7,7 +7,7 @@ import subprocess
7
7
  import sys
8
8
  import time
9
9
  from pathlib import Path
10
- from typing import Any, Callable, Dict, List, Optional
10
+ from typing import Any, Callable, Dict, List, Optional, Tuple
11
11
  from datetime import datetime, date
12
12
 
13
13
  import yaml # type: ignore
@@ -97,6 +97,98 @@ def is_rag_installed() -> bool:
97
97
  return len(get_missing_rag_modules()) == 0
98
98
 
99
99
 
100
+ def is_editable_install() -> bool:
101
+ """
102
+ 检测当前 Jarvis 是否以可编辑模式安装(pip/uv install -e .)。
103
+
104
+ 判断顺序:
105
+ 1. 读取 PEP 610 的 direct_url.json(dir_info.editable)
106
+ 2. 兼容旧式 .egg-link 安装
107
+ 3. 启发式回退:源码路径上游存在 .git 且不在 site-packages/dist-packages
108
+ """
109
+ # 优先使用 importlib.metadata 读取 distribution 的 direct_url.json
110
+ try:
111
+ import importlib.metadata as metadata # Python 3.8+
112
+ except Exception:
113
+ metadata = None # type: ignore
114
+
115
+ def _check_direct_url() -> Optional[bool]:
116
+ if metadata is None:
117
+ return None
118
+ candidates = ["jarvis-ai-assistant", "jarvis_ai_assistant"]
119
+ for name in candidates:
120
+ try:
121
+ dist = metadata.distribution(name)
122
+ except Exception:
123
+ continue
124
+ try:
125
+ files = dist.files or []
126
+ for f in files:
127
+ try:
128
+ if f.name == "direct_url.json":
129
+ p = Path(str(dist.locate_file(f)))
130
+ if p.exists():
131
+ with open(p, "r", encoding="utf-8", errors="ignore") as fp:
132
+ info = json.load(fp)
133
+ dir_info = info.get("dir_info") or {}
134
+ if isinstance(dir_info, dict) and bool(dir_info.get("editable")):
135
+ return True
136
+ # 兼容部分工具可能写入顶层 editable 字段
137
+ if bool(info.get("editable")):
138
+ return True
139
+ return False # 找到了 direct_url.json 但未标记 editable
140
+ except Exception:
141
+ continue
142
+ except Exception:
143
+ continue
144
+ return None
145
+
146
+ res = _check_direct_url()
147
+ if res is True:
148
+ return True
149
+ if res is False:
150
+ # 明确不是可编辑安装
151
+ return False
152
+
153
+ # 兼容旧式 .egg-link 可编辑安装
154
+ try:
155
+ module_path = Path(__file__).resolve()
156
+ pkg_root = module_path.parent.parent # jarvis 包根目录
157
+ for entry in sys.path:
158
+ try:
159
+ p = Path(entry)
160
+ if not p.exists() or not p.is_dir():
161
+ continue
162
+ for egg in p.glob("*.egg-link"):
163
+ try:
164
+ text = egg.read_text(encoding="utf-8", errors="ignore")
165
+ first_line = (text.strip().splitlines() or [""])[0]
166
+ if not first_line:
167
+ continue
168
+ src_path = Path(first_line).resolve()
169
+ # 当前包根目录在 egg-link 指向的源码路径下,视为可编辑安装
170
+ if str(pkg_root).startswith(str(src_path)):
171
+ return True
172
+ except Exception:
173
+ continue
174
+ except Exception:
175
+ continue
176
+ except Exception:
177
+ pass
178
+
179
+ # 启发式回退:源码仓库路径
180
+ try:
181
+ parents = list(Path(__file__).resolve().parents)
182
+ has_git = any((d / ".git").exists() for d in parents)
183
+ in_site = any(("site-packages" in str(d)) or ("dist-packages" in str(d)) for d in parents)
184
+ if has_git and not in_site:
185
+ return True
186
+ except Exception:
187
+ pass
188
+
189
+ return False
190
+
191
+
100
192
  def _setup_signal_handler() -> None:
101
193
  """设置SIGINT信号处理函数"""
102
194
  original_sigint = signal.getsignal(signal.SIGINT)
@@ -124,7 +216,7 @@ def _check_pip_updates() -> bool:
124
216
  from packaging import version
125
217
 
126
218
  # 检查上次检查日期
127
- last_check_file = Path(get_data_dir()) / "last_pip_check"
219
+ last_check_file = Path(str(get_data_dir())) / "last_pip_check"
128
220
  today_str = date.today().strftime("%Y-%m-%d")
129
221
 
130
222
  if last_check_file.exists():
@@ -867,7 +959,6 @@ def load_config():
867
959
  _load_and_process_config(str(config_file_path.parent), str(config_file_path))
868
960
 
869
961
 
870
- from typing import Tuple
871
962
 
872
963
 
873
964
  def _load_config_file(config_file: str) -> Tuple[str, dict]:
@@ -1235,11 +1326,7 @@ def _collect_optional_config_interactively(
1235
1326
  " 适用于您希望绕过检查并自行管理仓库状态的场景。"
1236
1327
  )
1237
1328
 
1238
- try:
1239
- # 查找当前模式在选项中的索引
1240
- default_index = choices.index(current_mode)
1241
- except ValueError:
1242
- default_index = 0 # 默认为第一个选项
1329
+
1243
1330
 
1244
1331
  new_mode = get_choice(
1245
1332
  tip,
@@ -1848,7 +1935,7 @@ def daily_check_git_updates(repo_dirs: List[str], repo_type: str):
1848
1935
  repo_dirs (List[str]): 需要检查的git仓库目录列表。
1849
1936
  repo_type (str): 仓库的类型名称,例如 "工具" 或 "方法论",用于日志输出。
1850
1937
  """
1851
- data_dir = Path(get_data_dir())
1938
+ data_dir = Path(str(get_data_dir()))
1852
1939
  last_check_file = data_dir / f"{repo_type}_updates_last_check.txt"
1853
1940
  should_check_for_updates = True
1854
1941
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarvis-ai-assistant
3
- Version: 0.3.28
3
+ Version: 0.3.29
4
4
  Summary: Jarvis: An AI assistant that uses tools to interact with the system
5
5
  Home-page: https://github.com/skyfireitdiy/Jarvis
6
6
  Author: skyfire