jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.6__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 (181) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +458 -152
  3. jarvis/jarvis_agent/agent_manager.py +17 -13
  4. jarvis/jarvis_agent/builtin_input_handler.py +2 -6
  5. jarvis/jarvis_agent/config_editor.py +2 -7
  6. jarvis/jarvis_agent/event_bus.py +82 -12
  7. jarvis/jarvis_agent/file_context_handler.py +329 -0
  8. jarvis/jarvis_agent/file_methodology_manager.py +3 -4
  9. jarvis/jarvis_agent/jarvis.py +628 -55
  10. jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
  11. jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
  12. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
  13. jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
  14. jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
  15. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
  16. jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
  17. jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
  18. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
  19. jarvis/jarvis_agent/language_support_info.py +486 -0
  20. jarvis/jarvis_agent/main.py +34 -10
  21. jarvis/jarvis_agent/memory_manager.py +7 -16
  22. jarvis/jarvis_agent/methodology_share_manager.py +10 -16
  23. jarvis/jarvis_agent/prompt_manager.py +1 -1
  24. jarvis/jarvis_agent/prompts.py +193 -171
  25. jarvis/jarvis_agent/protocols.py +8 -12
  26. jarvis/jarvis_agent/run_loop.py +105 -9
  27. jarvis/jarvis_agent/session_manager.py +2 -3
  28. jarvis/jarvis_agent/share_manager.py +20 -22
  29. jarvis/jarvis_agent/shell_input_handler.py +1 -2
  30. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  31. jarvis/jarvis_agent/task_analyzer.py +31 -6
  32. jarvis/jarvis_agent/task_manager.py +11 -27
  33. jarvis/jarvis_agent/tool_executor.py +2 -3
  34. jarvis/jarvis_agent/tool_share_manager.py +12 -24
  35. jarvis/jarvis_agent/utils.py +5 -1
  36. jarvis/jarvis_agent/web_bridge.py +189 -0
  37. jarvis/jarvis_agent/web_output_sink.py +53 -0
  38. jarvis/jarvis_agent/web_server.py +786 -0
  39. jarvis/jarvis_c2rust/__init__.py +26 -0
  40. jarvis/jarvis_c2rust/cli.py +575 -0
  41. jarvis/jarvis_c2rust/collector.py +250 -0
  42. jarvis/jarvis_c2rust/constants.py +26 -0
  43. jarvis/jarvis_c2rust/library_replacer.py +1254 -0
  44. jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
  45. jarvis/jarvis_c2rust/loaders.py +207 -0
  46. jarvis/jarvis_c2rust/models.py +28 -0
  47. jarvis/jarvis_c2rust/optimizer.py +2157 -0
  48. jarvis/jarvis_c2rust/scanner.py +1681 -0
  49. jarvis/jarvis_c2rust/transpiler.py +2983 -0
  50. jarvis/jarvis_c2rust/utils.py +385 -0
  51. jarvis/jarvis_code_agent/build_validation_config.py +132 -0
  52. jarvis/jarvis_code_agent/code_agent.py +1371 -220
  53. jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
  54. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
  60. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
  61. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
  62. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
  63. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
  64. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
  65. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
  66. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
  67. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
  68. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  69. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
  70. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  71. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  72. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  73. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  74. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  75. jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
  76. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
  77. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
  78. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
  79. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
  80. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
  81. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
  82. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
  83. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
  84. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
  85. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
  86. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
  87. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
  88. jarvis/jarvis_code_agent/lint.py +501 -8
  89. jarvis/jarvis_code_agent/utils.py +141 -0
  90. jarvis/jarvis_code_analysis/code_review.py +493 -584
  91. jarvis/jarvis_data/config_schema.json +128 -12
  92. jarvis/jarvis_git_squash/main.py +4 -5
  93. jarvis/jarvis_git_utils/git_commiter.py +82 -75
  94. jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
  95. jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
  96. jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
  97. jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
  98. jarvis/jarvis_methodology/main.py +32 -48
  99. jarvis/jarvis_multi_agent/__init__.py +287 -55
  100. jarvis/jarvis_multi_agent/main.py +36 -4
  101. jarvis/jarvis_platform/base.py +524 -202
  102. jarvis/jarvis_platform/human.py +7 -8
  103. jarvis/jarvis_platform/kimi.py +30 -36
  104. jarvis/jarvis_platform/openai.py +88 -25
  105. jarvis/jarvis_platform/registry.py +26 -10
  106. jarvis/jarvis_platform/tongyi.py +24 -25
  107. jarvis/jarvis_platform/yuanbao.py +32 -43
  108. jarvis/jarvis_platform_manager/main.py +66 -77
  109. jarvis/jarvis_platform_manager/service.py +8 -13
  110. jarvis/jarvis_rag/cli.py +53 -55
  111. jarvis/jarvis_rag/embedding_manager.py +13 -18
  112. jarvis/jarvis_rag/llm_interface.py +8 -9
  113. jarvis/jarvis_rag/query_rewriter.py +10 -21
  114. jarvis/jarvis_rag/rag_pipeline.py +24 -27
  115. jarvis/jarvis_rag/reranker.py +4 -5
  116. jarvis/jarvis_rag/retriever.py +28 -30
  117. jarvis/jarvis_sec/__init__.py +305 -0
  118. jarvis/jarvis_sec/agents.py +143 -0
  119. jarvis/jarvis_sec/analysis.py +276 -0
  120. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  121. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  122. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  123. jarvis/jarvis_sec/cli.py +139 -0
  124. jarvis/jarvis_sec/clustering.py +1439 -0
  125. jarvis/jarvis_sec/file_manager.py +427 -0
  126. jarvis/jarvis_sec/parsers.py +73 -0
  127. jarvis/jarvis_sec/prompts.py +268 -0
  128. jarvis/jarvis_sec/report.py +336 -0
  129. jarvis/jarvis_sec/review.py +453 -0
  130. jarvis/jarvis_sec/status.py +264 -0
  131. jarvis/jarvis_sec/types.py +20 -0
  132. jarvis/jarvis_sec/utils.py +499 -0
  133. jarvis/jarvis_sec/verification.py +848 -0
  134. jarvis/jarvis_sec/workflow.py +226 -0
  135. jarvis/jarvis_smart_shell/main.py +38 -87
  136. jarvis/jarvis_stats/cli.py +2 -2
  137. jarvis/jarvis_stats/stats.py +8 -8
  138. jarvis/jarvis_stats/storage.py +15 -21
  139. jarvis/jarvis_stats/visualizer.py +1 -1
  140. jarvis/jarvis_tools/clear_memory.py +3 -20
  141. jarvis/jarvis_tools/cli/main.py +21 -23
  142. jarvis/jarvis_tools/edit_file.py +1019 -132
  143. jarvis/jarvis_tools/execute_script.py +83 -25
  144. jarvis/jarvis_tools/file_analyzer.py +6 -9
  145. jarvis/jarvis_tools/generate_new_tool.py +14 -21
  146. jarvis/jarvis_tools/lsp_client.py +1552 -0
  147. jarvis/jarvis_tools/methodology.py +2 -3
  148. jarvis/jarvis_tools/read_code.py +1736 -35
  149. jarvis/jarvis_tools/read_symbols.py +140 -0
  150. jarvis/jarvis_tools/read_webpage.py +12 -13
  151. jarvis/jarvis_tools/registry.py +427 -200
  152. jarvis/jarvis_tools/retrieve_memory.py +20 -19
  153. jarvis/jarvis_tools/rewrite_file.py +72 -158
  154. jarvis/jarvis_tools/save_memory.py +3 -15
  155. jarvis/jarvis_tools/search_web.py +18 -18
  156. jarvis/jarvis_tools/sub_agent.py +36 -43
  157. jarvis/jarvis_tools/sub_code_agent.py +25 -26
  158. jarvis/jarvis_tools/virtual_tty.py +55 -33
  159. jarvis/jarvis_utils/clipboard.py +7 -10
  160. jarvis/jarvis_utils/config.py +232 -45
  161. jarvis/jarvis_utils/embedding.py +8 -5
  162. jarvis/jarvis_utils/fzf.py +8 -8
  163. jarvis/jarvis_utils/git_utils.py +225 -36
  164. jarvis/jarvis_utils/globals.py +3 -3
  165. jarvis/jarvis_utils/http.py +1 -1
  166. jarvis/jarvis_utils/input.py +99 -48
  167. jarvis/jarvis_utils/jsonnet_compat.py +465 -0
  168. jarvis/jarvis_utils/methodology.py +52 -48
  169. jarvis/jarvis_utils/utils.py +819 -491
  170. jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
  171. jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
  172. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
  173. jarvis/jarvis_agent/config.py +0 -92
  174. jarvis/jarvis_agent/edit_file_handler.py +0 -296
  175. jarvis/jarvis_platform/ai8.py +0 -332
  176. jarvis/jarvis_tools/ask_user.py +0 -54
  177. jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
  178. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  179. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
  180. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
  181. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,13 @@ AgentRunLoop: 承载 Agent 的主运行循环逻辑。
7
7
  - 暂不变更外部调用入口,后续在 Agent._main_loop 中委派到该类
8
8
  - 保持与现有异常处理、工具调用、用户交互完全一致
9
9
  """
10
+ import os
10
11
  from enum import Enum
11
12
  from typing import Any, TYPE_CHECKING
12
13
 
13
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
14
14
  from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL
15
15
  from jarvis.jarvis_agent.utils import join_prompts, is_auto_complete, normalize_next_action
16
+ from jarvis.jarvis_utils.config import get_max_input_token_count, get_conversation_turn_threshold
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  # 仅用于类型标注,避免运行时循环依赖
@@ -22,6 +23,12 @@ if TYPE_CHECKING:
22
23
  class AgentRunLoop:
23
24
  def __init__(self, agent: "Agent") -> None:
24
25
  self.agent = agent
26
+ self.conversation_rounds = 0
27
+ self.tool_reminder_rounds = int(os.environ.get("JARVIS_TOOL_REMINDER_ROUNDS", 20))
28
+ # 基于剩余token数量的自动总结阈值:当剩余token低于输入窗口的20%时触发
29
+ max_input_tokens = get_max_input_token_count(self.agent.model_group)
30
+ self.summary_remaining_token_threshold = int(max_input_tokens * 0.2)
31
+ self.conversation_turn_threshold = get_conversation_turn_threshold()
25
32
 
26
33
  def run(self) -> Any:
27
34
  """主运行循环(委派到传入的 agent 实例的方法与属性)"""
@@ -29,6 +36,28 @@ class AgentRunLoop:
29
36
 
30
37
  while True:
31
38
  try:
39
+ self.conversation_rounds += 1
40
+ if self.conversation_rounds % self.tool_reminder_rounds == 0:
41
+ self.agent.session.addon_prompt = join_prompts(
42
+ [self.agent.session.addon_prompt, self.agent.get_tool_usage_prompt()]
43
+ )
44
+ # 基于剩余token数量或对话轮次的自动总结判断
45
+ remaining_tokens = self.agent.model.get_remaining_token_count()
46
+ should_summarize = (
47
+ remaining_tokens <= self.summary_remaining_token_threshold or
48
+ self.conversation_rounds > self.conversation_turn_threshold
49
+ )
50
+ if should_summarize:
51
+ summary_text = self.agent._summarize_and_clear_history()
52
+ if summary_text:
53
+ # 将摘要作为下一轮的附加提示加入,从而维持上下文连续性
54
+ self.agent.session.addon_prompt = join_prompts(
55
+ [self.agent.session.addon_prompt, summary_text]
56
+ )
57
+ # 重置轮次计数(用于工具提醒)与对话长度计数器(用于摘要触发),开始新一轮周期
58
+ self.conversation_rounds = 0
59
+ self.agent.session.conversation_length = 0
60
+
32
61
  ag = self.agent
33
62
 
34
63
  # 更新输入处理器标志
@@ -48,6 +77,25 @@ class AgentRunLoop:
48
77
  ag.session.prompt = ""
49
78
  run_input_handlers = False
50
79
 
80
+ # 检查是否包含 <!!!SUMMARY!!!> 标记,触发总结并清空历史
81
+ if "<!!!SUMMARY!!!>" in current_response:
82
+ print("ℹ️ 检测到 <!!!SUMMARY!!!> 标记,正在触发总结并清空历史...")
83
+ # 移除标记,避免在后续处理中出现
84
+ current_response = current_response.replace("<!!!SUMMARY!!!>", "").strip()
85
+ # 触发总结并清空历史
86
+ summary_text = ag._summarize_and_clear_history()
87
+ if summary_text:
88
+ # 将摘要作为下一轮的附加提示加入,从而维持上下文连续性
89
+ ag.session.addon_prompt = join_prompts(
90
+ [ag.session.addon_prompt, summary_text]
91
+ )
92
+ # 重置轮次计数(用于工具提醒)与对话长度计数器(用于摘要触发),开始新一轮周期
93
+ self.conversation_rounds = 0
94
+ ag.session.conversation_length = 0
95
+ # 如果响应中还有其他内容,继续处理;否则继续下一轮
96
+ if not current_response:
97
+ continue
98
+
51
99
  # 处理中断
52
100
  interrupt_result = ag._handle_run_interrupt(current_response)
53
101
  if (
@@ -61,7 +109,7 @@ class AgentRunLoop:
61
109
  return interrupt_result
62
110
 
63
111
  # 处理工具调用
64
- # 广播工具调用前事件(不影响主流程)
112
+ # 非关键流程:广播工具调用前事件(用于日志、监控等)
65
113
  try:
66
114
  ag.event_bus.emit(
67
115
  BEFORE_TOOL_CALL,
@@ -72,14 +120,34 @@ class AgentRunLoop:
72
120
  pass
73
121
  need_return, tool_prompt = ag._call_tools(current_response)
74
122
 
75
- # 将上一个提示和工具提示安全地拼接起来
76
- ag.session.prompt = join_prompts([ag.session.prompt, tool_prompt])
77
-
123
+ # 如果工具要求立即返回结果(例如 SEND_MESSAGE 需要将字典返回给上层),直接返回该结果
78
124
  if need_return:
79
- return ag.session.prompt
125
+ ag._no_tool_call_count = 0
126
+ return tool_prompt
80
127
 
128
+ # 将上一个提示和工具提示安全地拼接起来(仅当工具结果为字符串时)
129
+ safe_tool_prompt = tool_prompt if isinstance(tool_prompt, str) else ""
130
+
131
+ ag.session.prompt = join_prompts([ag.session.prompt, safe_tool_prompt])
81
132
 
82
- # 广播工具调用后的事件(不影响主流程)
133
+ # 关键流程:直接调用 after_tool_call 回调函数
134
+ try:
135
+ # 获取所有订阅了 AFTER_TOOL_CALL 事件的回调
136
+ listeners = ag.event_bus._listeners.get(AFTER_TOOL_CALL, [])
137
+ for callback in listeners:
138
+ try:
139
+ callback(
140
+ agent=ag,
141
+ current_response=current_response,
142
+ need_return=need_return,
143
+ tool_prompt=tool_prompt,
144
+ )
145
+ except Exception:
146
+ pass
147
+ except Exception:
148
+ pass
149
+
150
+ # 非关键流程:广播工具调用后的事件(用于日志、监控等)
83
151
  try:
84
152
  ag.event_bus.emit(
85
153
  AFTER_TOOL_CALL,
@@ -93,11 +161,39 @@ class AgentRunLoop:
93
161
 
94
162
  # 检查是否需要继续
95
163
  if ag.session.prompt or ag.session.addon_prompt:
164
+ ag._no_tool_call_count = 0
96
165
  continue
97
166
 
98
167
  # 检查自动完成
99
168
  if ag.auto_complete and is_auto_complete(current_response):
100
- return ag._complete_task(auto_completed=True)
169
+ ag._no_tool_call_count = 0
170
+ # 先运行_complete_task,触发记忆整理/事件等副作用,再决定返回值
171
+ result = ag._complete_task(auto_completed=True)
172
+ # 若不需要summary,则将最后一条LLM输出作为返回值
173
+ if not getattr(ag, "need_summary", True):
174
+ return current_response
175
+ return result
176
+
177
+
178
+ # 检查是否有工具调用:如果tool_prompt不为空,说明有工具被调用
179
+ has_tool_call = bool(safe_tool_prompt and safe_tool_prompt.strip())
180
+
181
+ # 在非交互模式下,跟踪连续没有工具调用的次数
182
+ if ag.non_interactive:
183
+ if has_tool_call:
184
+ # 有工具调用,重置计数器
185
+ ag._no_tool_call_count = 0
186
+ else:
187
+ # 没有工具调用,增加计数器
188
+ ag._no_tool_call_count += 1
189
+ # 如果连续5次没有工具调用,添加工具使用提示
190
+ if ag._no_tool_call_count >= 5:
191
+ tool_usage_prompt = ag.get_tool_usage_prompt()
192
+ ag.session.addon_prompt = join_prompts(
193
+ [ag.session.addon_prompt, tool_usage_prompt]
194
+ )
195
+ # 重置计数器,避免重复添加
196
+ ag._no_tool_call_count = 0
101
197
 
102
198
  # 获取下一步用户输入
103
199
  next_action = ag._get_next_user_action()
@@ -109,5 +205,5 @@ class AgentRunLoop:
109
205
  return ag._complete_task(auto_completed=False)
110
206
 
111
207
  except Exception as e:
112
- PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
208
+ print(f"任务失败: {str(e)}")
113
209
  return f"Task failed: {str(e)}"
@@ -2,7 +2,6 @@
2
2
  import os
3
3
  from typing import Any, Dict, Optional, TYPE_CHECKING
4
4
 
5
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
6
5
 
7
6
  if TYPE_CHECKING:
8
7
  from jarvis.jarvis_platform.base import BasePlatform
@@ -61,9 +60,9 @@ class SessionManager:
61
60
  if self.model.restore(session_file):
62
61
  try:
63
62
  os.remove(session_file)
64
- PrettyOutput.print("会话已恢复,并已删除会话文件。", OutputType.SUCCESS)
63
+ print("会话已恢复,并已删除会话文件。")
65
64
  except OSError as e:
66
- PrettyOutput.print(f"删除会话文件失败: {e}", OutputType.ERROR)
65
+ print(f"删除会话文件失败: {e}")
67
66
  return True
68
67
  return False
69
68
 
@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
7
7
 
8
8
  from prompt_toolkit import prompt
9
9
 
10
- from jarvis.jarvis_agent import OutputType, PrettyOutput, user_confirm
10
+ from jarvis.jarvis_agent import user_confirm
11
11
  from jarvis.jarvis_utils.config import get_data_dir
12
12
 
13
13
 
@@ -49,14 +49,19 @@ class ShareManager(ABC):
49
49
  def __init__(self, central_repo_url: str, repo_name: str):
50
50
  self.central_repo_url = central_repo_url
51
51
  self.repo_name = repo_name
52
- self.repo_path = os.path.join(get_data_dir(), repo_name)
52
+ # 支持将中心仓库配置为本地目录(含git子路径)
53
+ expanded = os.path.expanduser(os.path.expandvars(central_repo_url))
54
+ if os.path.isdir(expanded):
55
+ # 直接使用本地目录作为中心仓库路径(支持git仓库子目录)
56
+ self.repo_path = expanded
57
+ else:
58
+ # 仍按原逻辑使用数据目录中的克隆路径
59
+ self.repo_path = os.path.join(get_data_dir(), repo_name)
53
60
 
54
61
  def update_central_repo(self) -> None:
55
62
  """克隆或更新中心仓库"""
56
63
  if not os.path.exists(self.repo_path):
57
- PrettyOutput.print(
58
- f"正在克隆中心{self.get_resource_type()}仓库...", OutputType.INFO
59
- )
64
+ print(f"ℹ️ 正在克隆中心{self.get_resource_type()}仓库...")
60
65
  subprocess.run(
61
66
  ["git", "clone", self.central_repo_url, self.repo_path], check=True
62
67
  )
@@ -85,9 +90,7 @@ class ShareManager(ABC):
85
90
  )
86
91
  subprocess.run(["git", "push"], cwd=self.repo_path, check=True)
87
92
  else:
88
- PrettyOutput.print(
89
- f"正在更新中心{self.get_resource_type()}仓库...", OutputType.INFO
90
- )
93
+ print(f"ℹ️ 正在更新中心{self.get_resource_type()}仓库...")
91
94
  # 检查是否是空仓库
92
95
  try:
93
96
  # 先尝试获取远程分支信息
@@ -116,24 +119,18 @@ class ShareManager(ABC):
116
119
  ["git", "checkout", "."], cwd=self.repo_path, check=True
117
120
  )
118
121
  else:
119
- PrettyOutput.print(
120
- f"跳过更新 '{self.repo_name}' 以保留未提交的更改。",
121
- OutputType.INFO,
122
- )
122
+ print(f"ℹ️ 跳过更新 '{self.repo_name}' 以保留未提交的更改。")
123
123
  return
124
124
  subprocess.run(["git", "pull"], cwd=self.repo_path, check=True)
125
125
  else:
126
- PrettyOutput.print(
127
- f"中心{self.get_resource_type()}仓库是空的,将初始化为新仓库",
128
- OutputType.INFO,
129
- )
126
+ print(f"ℹ️ 中心{self.get_resource_type()}仓库是空的,将初始化为新仓库")
130
127
  except subprocess.CalledProcessError:
131
128
  # 如果命令失败,可能是网络问题或其他错误
132
- PrettyOutput.print("无法连接到远程仓库,将跳过更新", OutputType.WARNING)
129
+ print("⚠️ 无法连接到远程仓库,将跳过更新")
133
130
 
134
131
  def commit_and_push(self, count: int) -> None:
135
132
  """提交并推送更改"""
136
- PrettyOutput.print("\n正在提交更改...", OutputType.INFO)
133
+ print("ℹ️ 正在提交更改...")
137
134
  subprocess.run(["git", "add", "."], cwd=self.repo_path, check=True)
138
135
 
139
136
  commit_msg = f"Add {count} {self.get_resource_type()}(s) from local collection"
@@ -141,7 +138,7 @@ class ShareManager(ABC):
141
138
  ["git", "commit", "-m", commit_msg], cwd=self.repo_path, check=True
142
139
  )
143
140
 
144
- PrettyOutput.print("正在推送到远程仓库...", OutputType.INFO)
141
+ print("ℹ️ 正在推送到远程仓库...")
145
142
  # 检查是否需要设置上游分支(空仓库的情况)
146
143
  try:
147
144
  # 先尝试普通推送
@@ -172,7 +169,8 @@ class ShareManager(ABC):
172
169
  resource_list.append(f"[{i}] {self.format_resource_display(resource)}")
173
170
 
174
171
  # 一次性打印所有资源
175
- PrettyOutput.print("\n".join(resource_list), OutputType.INFO)
172
+ joined_resources = '\n'.join(resource_list)
173
+ print(f"ℹ️ {joined_resources}")
176
174
 
177
175
  # 让用户选择
178
176
  while True:
@@ -188,12 +186,12 @@ class ShareManager(ABC):
188
186
  else:
189
187
  selected_indices = parse_selection(choice_str, len(resources))
190
188
  if not selected_indices:
191
- PrettyOutput.print("无效的选择", OutputType.WARNING)
189
+ print("⚠️ 无效的选择")
192
190
  continue
193
191
  return [resources[i - 1] for i in selected_indices]
194
192
 
195
193
  except ValueError:
196
- PrettyOutput.print("请输入有效的数字", OutputType.WARNING)
194
+ print("⚠️ 请输入有效的数字")
197
195
 
198
196
  @abstractmethod
199
197
  def get_resource_type(self) -> str:
@@ -1,7 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from typing import Any, Tuple
3
3
 
4
- from jarvis.jarvis_utils.output import OutputType, PrettyOutput
5
4
  from jarvis.jarvis_utils.input import user_confirm
6
5
  from jarvis.jarvis_agent.utils import join_prompts
7
6
 
@@ -24,7 +23,7 @@ def shell_input_handler(user_input: str, agent: Any) -> Tuple[str, bool]:
24
23
 
25
24
  # Build script while stripping the no-confirm marker from each line
26
25
  script = "\n".join([_clean(c) for c in cmdline])
27
- PrettyOutput.print(script, OutputType.CODE, lang="bash")
26
+ print(script)
28
27
 
29
28
  # If any line contains the no-confirm marker, skip the pre-execution confirmation
30
29
  no_confirm = any(marker in c for c in cmdline)
@@ -0,0 +1,295 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Web STDIO 重定向模块:
4
+ - 在 Web 模式下,将 Python 层的标准输出/错误(sys.stdout/sys.stderr)重定向到 WebSocket,通过 WebBridge 广播。
5
+ - 适用于工具或第三方库直接使用 print()/stdout/stderr 的输出,从而不经过 PrettyOutput Sink 的场景。
6
+
7
+ 注意:
8
+ - 这是进程级重定向,可能带来重复输出(PrettyOutput 已通过 Sink 广播一次,console.print 也会走到 stdout)。若需要避免重复,可在前端针对 'stdio' 类型进行独立显示或折叠。
9
+ - 对于子进程输出(subprocess),通常由调用方决定是否捕获和打印;若直接透传到父进程的 stdout/stderr,也会被此重定向捕获。
10
+
11
+ 前端消息结构(通过 WebBridge.broadcast):
12
+ { "type": "stdio", "stream": "stdout" | "stderr", "text": "..." }
13
+
14
+ 使用:
15
+ from jarvis.jarvis_agent.stdio_redirect import enable_web_stdio_redirect, disable_web_stdio_redirect
16
+ enable_web_stdio_redirect()
17
+ # ... 运行期间输出将通过 WS 广播 ...
18
+ disable_web_stdio_redirect()
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ import threading
24
+
25
+ from jarvis.jarvis_agent.web_bridge import WebBridge
26
+
27
+
28
+ _original_stdout = sys.stdout
29
+ _original_stderr = sys.stderr
30
+ _redirect_enabled = False
31
+ _lock = threading.Lock()
32
+
33
+
34
+ class _WebStreamWrapper:
35
+ """文件类兼容包装器,将 write() 的内容通过 WebBridge 广播。"""
36
+
37
+ def __init__(self, stream_name: str) -> None:
38
+ self._stream_name = stream_name
39
+ try:
40
+ self._encoding = getattr(_original_stdout, "encoding", "utf-8")
41
+ except Exception:
42
+ self._encoding = "utf-8"
43
+
44
+ def write(self, s: object) -> int:
45
+ try:
46
+ text = s if isinstance(s, str) else str(s)
47
+ except Exception:
48
+ text = repr(s)
49
+ try:
50
+ WebBridge.instance().broadcast({
51
+ "type": "stdio",
52
+ "stream": self._stream_name,
53
+ "text": text,
54
+ })
55
+ except Exception:
56
+ # 广播异常不影响主流程
57
+ pass
58
+ # 返回写入长度以兼容部分调用方
59
+ try:
60
+ return len(text)
61
+ except Exception:
62
+ return 0
63
+
64
+ def flush(self) -> None:
65
+ # 无需实际刷新;保持接口兼容
66
+ pass
67
+
68
+ def isatty(self) -> bool:
69
+ return False
70
+
71
+ @property
72
+ def encoding(self) -> str:
73
+ return self._encoding
74
+
75
+ def writelines(self, lines) -> None:
76
+ for ln in lines:
77
+ self.write(ln)
78
+
79
+ def __getattr__(self, name: str):
80
+ # 兼容性:必要时委派到原始 stdout/stderr 的属性(尽量避免)
81
+ try:
82
+ return getattr(_original_stdout if self._stream_name == "stdout" else _original_stderr, name)
83
+ except Exception:
84
+ raise AttributeError(name)
85
+
86
+
87
+ def enable_web_stdio_redirect() -> None:
88
+ """启用全局 STDOUT/STDERR 到 WebSocket 的重定向。"""
89
+ global _redirect_enabled
90
+ with _lock:
91
+ if _redirect_enabled:
92
+ return
93
+ try:
94
+ sys.stdout = _WebStreamWrapper("stdout") # type: ignore[assignment]
95
+ sys.stderr = _WebStreamWrapper("stderr") # type: ignore[assignment]
96
+ _redirect_enabled = True
97
+ except Exception:
98
+ # 回退:保持原始输出
99
+ sys.stdout = _original_stdout
100
+ sys.stderr = _original_stderr
101
+ _redirect_enabled = False
102
+
103
+
104
+ def disable_web_stdio_redirect() -> None:
105
+ """禁用全局 STDOUT/STDERR 重定向,恢复原始输出。"""
106
+ global _redirect_enabled
107
+ with _lock:
108
+ try:
109
+ sys.stdout = _original_stdout
110
+ sys.stderr = _original_stderr
111
+ except Exception:
112
+ pass
113
+ _redirect_enabled = False
114
+
115
+
116
+ # ---------------------------
117
+ # Web STDIN 重定向(浏览器 -> 后端)
118
+ # ---------------------------
119
+ # 目的:
120
+ # - 将前端 xterm 的按键数据通过 WS 送回服务端,并作为 sys.stdin 的数据源
121
+ # - 使得 Python 层的 input()/sys.stdin.readline() 等可以从浏览器获得输入
122
+ # - 仅适用于部分交互式场景(非真正 PTY 行为),可满足基础行缓冲输入
123
+ from queue import Queue # noqa: E402
124
+
125
+
126
+ _original_stdin = sys.stdin
127
+ _stdin_enabled = False
128
+ _stdin_wrapper = None # type: ignore[assignment]
129
+
130
+
131
+ class _WebInputWrapper:
132
+ """文件类兼容包装器:作为 sys.stdin 的替身,从队列中读取浏览器送来的数据。"""
133
+
134
+ def __init__(self) -> None:
135
+ self._queue: "Queue[str]" = Queue()
136
+ self._buffer: str = ""
137
+ self._lock = threading.Lock()
138
+ try:
139
+ self._encoding = getattr(_original_stdin, "encoding", "utf-8") # type: ignore[name-defined]
140
+ except Exception:
141
+ self._encoding = "utf-8"
142
+
143
+ # 外部注入:由 WebSocket 端点调用
144
+ def feed(self, data: str) -> None:
145
+ try:
146
+ s = data if isinstance(data, str) else str(data)
147
+ except Exception:
148
+ s = repr(data)
149
+ # 将回车转换为换行,方便基于 readline 的读取
150
+ s = s.replace("\r", "\n")
151
+ self._queue.put_nowait(s)
152
+
153
+ # 基础读取:尽可能兼容常用调用
154
+ def read(self, size: int = -1) -> str:
155
+ # size < 0 表示尽可能多地读取(直到当前缓冲区内容)
156
+ if size == 0:
157
+ return ""
158
+
159
+ while True:
160
+ with self._lock:
161
+ if size > 0 and len(self._buffer) >= size:
162
+ out = self._buffer[:size]
163
+ self._buffer = self._buffer[size:]
164
+ return out
165
+ if size < 0 and self._buffer:
166
+ out = self._buffer
167
+ self._buffer = ""
168
+ return out
169
+ # 需要更多数据,阻塞等待
170
+ try:
171
+ chunk = self._queue.get(timeout=None)
172
+ except Exception:
173
+ chunk = ""
174
+ if not isinstance(chunk, str):
175
+ try:
176
+ chunk = str(chunk)
177
+ except Exception:
178
+ chunk = ""
179
+ with self._lock:
180
+ self._buffer += chunk
181
+
182
+ def readline(self, size: int = -1) -> str:
183
+ # 读取到换行符为止(包含换行),可选 size 限制
184
+ while True:
185
+ with self._lock:
186
+ idx = self._buffer.find("\n")
187
+ if idx != -1:
188
+ # 找到换行
189
+ end_index = idx + 1
190
+ if size > 0:
191
+ end_index = min(end_index, size)
192
+ out = self._buffer[:end_index]
193
+ self._buffer = self._buffer[end_index:]
194
+ return out
195
+ # 未找到换行,但如果指定了 size 且缓冲已有足够数据,则返回
196
+ if size > 0 and len(self._buffer) >= size:
197
+ out = self._buffer[:size]
198
+ self._buffer = self._buffer[size:]
199
+ return out
200
+ # 更多数据
201
+ try:
202
+ chunk = self._queue.get(timeout=None)
203
+ except Exception:
204
+ chunk = ""
205
+ if not isinstance(chunk, str):
206
+ try:
207
+ chunk = str(chunk)
208
+ except Exception:
209
+ chunk = ""
210
+ with self._lock:
211
+ self._buffer += chunk
212
+
213
+ def readlines(self, hint: int = -1):
214
+ lines = []
215
+ total = 0
216
+ while True:
217
+ ln = self.readline()
218
+ if not ln:
219
+ break
220
+ lines.append(ln)
221
+ total += len(ln)
222
+ if hint > 0 and total >= hint:
223
+ break
224
+ return lines
225
+
226
+ def writable(self) -> bool:
227
+ return False
228
+
229
+ def readable(self) -> bool:
230
+ return True
231
+
232
+ def seekable(self) -> bool:
233
+ return False
234
+
235
+ def flush(self) -> None:
236
+ pass
237
+
238
+ def isatty(self) -> bool:
239
+ # 伪装为 TTY,可改善部分库的行为(注意并非真正 PTY)
240
+ return True
241
+
242
+ @property
243
+ def encoding(self) -> str:
244
+ return self._encoding
245
+
246
+ def __getattr__(self, name: str):
247
+ # 尽量代理到原始 stdin 的属性以增强兼容性
248
+ try:
249
+ return getattr(_original_stdin, name)
250
+ except Exception:
251
+ raise AttributeError(name)
252
+
253
+
254
+ def enable_web_stdin_redirect() -> None:
255
+ """启用 Web STDIN 重定向:将 sys.stdin 替换为浏览器数据源。"""
256
+ global _stdin_enabled, _stdin_wrapper, _original_stdin
257
+ with _lock:
258
+ if _stdin_enabled:
259
+ return
260
+ try:
261
+ # 记录原始 stdin(若尚未记录)
262
+ if "_original_stdin" not in globals() or _original_stdin is None:
263
+ _original_stdin = sys.stdin # type: ignore[assignment]
264
+ _stdin_wrapper = _WebInputWrapper()
265
+ sys.stdin = _stdin_wrapper # type: ignore[assignment]
266
+ _stdin_enabled = True
267
+ except Exception:
268
+ # 回退:保持原始输入
269
+ try:
270
+ sys.stdin = _original_stdin # type: ignore[assignment]
271
+ except Exception:
272
+ pass
273
+ _stdin_enabled = False
274
+
275
+
276
+ def disable_web_stdin_redirect() -> None:
277
+ """禁用 Web STDIN 重定向,恢复原始输入。"""
278
+ global _stdin_enabled, _stdin_wrapper
279
+ with _lock:
280
+ try:
281
+ sys.stdin = _original_stdin # type: ignore[assignment]
282
+ except Exception:
283
+ pass
284
+ _stdin_wrapper = None
285
+ _stdin_enabled = False
286
+
287
+
288
+ def feed_web_stdin(data: str) -> None:
289
+ """向 Web STDIN 注入数据(由 WebSocket /stdio 端点调用)。"""
290
+ try:
291
+ if _stdin_enabled and _stdin_wrapper is not None:
292
+ _stdin_wrapper.feed(data) # type: ignore[attr-defined]
293
+ except Exception:
294
+ # 注入失败不影响主流程
295
+ pass