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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +458 -152
- jarvis/jarvis_agent/agent_manager.py +17 -13
- jarvis/jarvis_agent/builtin_input_handler.py +2 -6
- jarvis/jarvis_agent/config_editor.py +2 -7
- jarvis/jarvis_agent/event_bus.py +82 -12
- jarvis/jarvis_agent/file_context_handler.py +329 -0
- jarvis/jarvis_agent/file_methodology_manager.py +3 -4
- jarvis/jarvis_agent/jarvis.py +628 -55
- jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
- jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
- jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
- jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
- jarvis/jarvis_agent/language_support_info.py +486 -0
- jarvis/jarvis_agent/main.py +34 -10
- jarvis/jarvis_agent/memory_manager.py +7 -16
- jarvis/jarvis_agent/methodology_share_manager.py +10 -16
- jarvis/jarvis_agent/prompt_manager.py +1 -1
- jarvis/jarvis_agent/prompts.py +193 -171
- jarvis/jarvis_agent/protocols.py +8 -12
- jarvis/jarvis_agent/run_loop.py +105 -9
- jarvis/jarvis_agent/session_manager.py +2 -3
- jarvis/jarvis_agent/share_manager.py +20 -22
- jarvis/jarvis_agent/shell_input_handler.py +1 -2
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +31 -6
- jarvis/jarvis_agent/task_manager.py +11 -27
- jarvis/jarvis_agent/tool_executor.py +2 -3
- jarvis/jarvis_agent/tool_share_manager.py +12 -24
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +786 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +575 -0
- jarvis/jarvis_c2rust/collector.py +250 -0
- jarvis/jarvis_c2rust/constants.py +26 -0
- jarvis/jarvis_c2rust/library_replacer.py +1254 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
- jarvis/jarvis_c2rust/loaders.py +207 -0
- jarvis/jarvis_c2rust/models.py +28 -0
- jarvis/jarvis_c2rust/optimizer.py +2157 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2983 -0
- jarvis/jarvis_c2rust/utils.py +385 -0
- jarvis/jarvis_code_agent/build_validation_config.py +132 -0
- jarvis/jarvis_code_agent/code_agent.py +1371 -220
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
- jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
- jarvis/jarvis_code_agent/lint.py +501 -8
- jarvis/jarvis_code_agent/utils.py +141 -0
- jarvis/jarvis_code_analysis/code_review.py +493 -584
- jarvis/jarvis_data/config_schema.json +128 -12
- jarvis/jarvis_git_squash/main.py +4 -5
- jarvis/jarvis_git_utils/git_commiter.py +82 -75
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
- jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
- jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
- jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
- jarvis/jarvis_methodology/main.py +32 -48
- jarvis/jarvis_multi_agent/__init__.py +287 -55
- jarvis/jarvis_multi_agent/main.py +36 -4
- jarvis/jarvis_platform/base.py +524 -202
- jarvis/jarvis_platform/human.py +7 -8
- jarvis/jarvis_platform/kimi.py +30 -36
- jarvis/jarvis_platform/openai.py +88 -25
- jarvis/jarvis_platform/registry.py +26 -10
- jarvis/jarvis_platform/tongyi.py +24 -25
- jarvis/jarvis_platform/yuanbao.py +32 -43
- jarvis/jarvis_platform_manager/main.py +66 -77
- jarvis/jarvis_platform_manager/service.py +8 -13
- jarvis/jarvis_rag/cli.py +53 -55
- jarvis/jarvis_rag/embedding_manager.py +13 -18
- jarvis/jarvis_rag/llm_interface.py +8 -9
- jarvis/jarvis_rag/query_rewriter.py +10 -21
- jarvis/jarvis_rag/rag_pipeline.py +24 -27
- jarvis/jarvis_rag/reranker.py +4 -5
- jarvis/jarvis_rag/retriever.py +28 -30
- jarvis/jarvis_sec/__init__.py +305 -0
- jarvis/jarvis_sec/agents.py +143 -0
- jarvis/jarvis_sec/analysis.py +276 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +139 -0
- jarvis/jarvis_sec/clustering.py +1439 -0
- jarvis/jarvis_sec/file_manager.py +427 -0
- jarvis/jarvis_sec/parsers.py +73 -0
- jarvis/jarvis_sec/prompts.py +268 -0
- jarvis/jarvis_sec/report.py +336 -0
- jarvis/jarvis_sec/review.py +453 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/utils.py +499 -0
- jarvis/jarvis_sec/verification.py +848 -0
- jarvis/jarvis_sec/workflow.py +226 -0
- jarvis/jarvis_smart_shell/main.py +38 -87
- jarvis/jarvis_stats/cli.py +2 -2
- jarvis/jarvis_stats/stats.py +8 -8
- jarvis/jarvis_stats/storage.py +15 -21
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/clear_memory.py +3 -20
- jarvis/jarvis_tools/cli/main.py +21 -23
- jarvis/jarvis_tools/edit_file.py +1019 -132
- jarvis/jarvis_tools/execute_script.py +83 -25
- jarvis/jarvis_tools/file_analyzer.py +6 -9
- jarvis/jarvis_tools/generate_new_tool.py +14 -21
- jarvis/jarvis_tools/lsp_client.py +1552 -0
- jarvis/jarvis_tools/methodology.py +2 -3
- jarvis/jarvis_tools/read_code.py +1736 -35
- jarvis/jarvis_tools/read_symbols.py +140 -0
- jarvis/jarvis_tools/read_webpage.py +12 -13
- jarvis/jarvis_tools/registry.py +427 -200
- jarvis/jarvis_tools/retrieve_memory.py +20 -19
- jarvis/jarvis_tools/rewrite_file.py +72 -158
- jarvis/jarvis_tools/save_memory.py +3 -15
- jarvis/jarvis_tools/search_web.py +18 -18
- jarvis/jarvis_tools/sub_agent.py +36 -43
- jarvis/jarvis_tools/sub_code_agent.py +25 -26
- jarvis/jarvis_tools/virtual_tty.py +55 -33
- jarvis/jarvis_utils/clipboard.py +7 -10
- jarvis/jarvis_utils/config.py +232 -45
- jarvis/jarvis_utils/embedding.py +8 -5
- jarvis/jarvis_utils/fzf.py +8 -8
- jarvis/jarvis_utils/git_utils.py +225 -36
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +99 -48
- jarvis/jarvis_utils/jsonnet_compat.py +465 -0
- jarvis/jarvis_utils/methodology.py +52 -48
- jarvis/jarvis_utils/utils.py +819 -491
- jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
- jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_agent/edit_file_handler.py +0 -296
- jarvis/jarvis_platform/ai8.py +0 -332
- jarvis/jarvis_tools/ask_user.py +0 -54
- jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
jarvis/jarvis_agent/run_loop.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
print("✅ 会话已恢复,并已删除会话文件。")
|
|
65
64
|
except OSError as e:
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
print("⚠️ 无法连接到远程仓库,将跳过更新")
|
|
133
130
|
|
|
134
131
|
def commit_and_push(self, count: int) -> None:
|
|
135
132
|
"""提交并推送更改"""
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
print("⚠️ 无效的选择")
|
|
192
190
|
continue
|
|
193
191
|
return [resources[i - 1] for i in selected_indices]
|
|
194
192
|
|
|
195
193
|
except ValueError:
|
|
196
|
-
|
|
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
|
-
|
|
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
|