jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +289 -87
  3. jarvis/jarvis_agent/agent_manager.py +17 -8
  4. jarvis/jarvis_agent/edit_file_handler.py +374 -86
  5. jarvis/jarvis_agent/event_bus.py +1 -1
  6. jarvis/jarvis_agent/file_context_handler.py +79 -0
  7. jarvis/jarvis_agent/jarvis.py +601 -43
  8. jarvis/jarvis_agent/main.py +32 -2
  9. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  10. jarvis/jarvis_agent/run_loop.py +38 -5
  11. jarvis/jarvis_agent/share_manager.py +8 -1
  12. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  13. jarvis/jarvis_agent/task_analyzer.py +5 -2
  14. jarvis/jarvis_agent/task_planner.py +496 -0
  15. jarvis/jarvis_agent/utils.py +5 -1
  16. jarvis/jarvis_agent/web_bridge.py +189 -0
  17. jarvis/jarvis_agent/web_output_sink.py +53 -0
  18. jarvis/jarvis_agent/web_server.py +751 -0
  19. jarvis/jarvis_c2rust/__init__.py +26 -0
  20. jarvis/jarvis_c2rust/cli.py +613 -0
  21. jarvis/jarvis_c2rust/collector.py +258 -0
  22. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  23. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  24. jarvis/jarvis_c2rust/optimizer.py +960 -0
  25. jarvis/jarvis_c2rust/scanner.py +1681 -0
  26. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  27. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  28. jarvis/jarvis_code_agent/code_agent.py +1171 -94
  29. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  30. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  31. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  32. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  33. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  34. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  35. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  36. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  37. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  38. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  39. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  40. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  41. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  42. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  43. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  44. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  45. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  46. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  47. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  48. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  49. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  50. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  51. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  52. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  53. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  54. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  55. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  56. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  57. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  58. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  59. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  60. jarvis/jarvis_code_agent/lint.py +270 -8
  61. jarvis/jarvis_code_agent/utils.py +142 -0
  62. jarvis/jarvis_code_analysis/code_review.py +483 -569
  63. jarvis/jarvis_data/config_schema.json +97 -8
  64. jarvis/jarvis_git_utils/git_commiter.py +38 -26
  65. jarvis/jarvis_mcp/sse_mcp_client.py +2 -2
  66. jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
  67. jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
  68. jarvis/jarvis_multi_agent/__init__.py +239 -25
  69. jarvis/jarvis_multi_agent/main.py +37 -1
  70. jarvis/jarvis_platform/base.py +103 -51
  71. jarvis/jarvis_platform/openai.py +26 -1
  72. jarvis/jarvis_platform/yuanbao.py +1 -1
  73. jarvis/jarvis_platform_manager/service.py +2 -2
  74. jarvis/jarvis_rag/cli.py +4 -4
  75. jarvis/jarvis_sec/__init__.py +3605 -0
  76. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  77. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  78. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  79. jarvis/jarvis_sec/cli.py +116 -0
  80. jarvis/jarvis_sec/report.py +257 -0
  81. jarvis/jarvis_sec/status.py +264 -0
  82. jarvis/jarvis_sec/types.py +20 -0
  83. jarvis/jarvis_sec/workflow.py +219 -0
  84. jarvis/jarvis_stats/cli.py +1 -1
  85. jarvis/jarvis_stats/stats.py +1 -1
  86. jarvis/jarvis_stats/visualizer.py +1 -1
  87. jarvis/jarvis_tools/cli/main.py +1 -0
  88. jarvis/jarvis_tools/execute_script.py +46 -9
  89. jarvis/jarvis_tools/generate_new_tool.py +3 -1
  90. jarvis/jarvis_tools/read_code.py +275 -12
  91. jarvis/jarvis_tools/read_symbols.py +141 -0
  92. jarvis/jarvis_tools/read_webpage.py +5 -3
  93. jarvis/jarvis_tools/registry.py +73 -35
  94. jarvis/jarvis_tools/search_web.py +15 -11
  95. jarvis/jarvis_tools/sub_agent.py +24 -42
  96. jarvis/jarvis_tools/sub_code_agent.py +14 -13
  97. jarvis/jarvis_tools/virtual_tty.py +1 -1
  98. jarvis/jarvis_utils/config.py +187 -35
  99. jarvis/jarvis_utils/embedding.py +3 -0
  100. jarvis/jarvis_utils/git_utils.py +181 -6
  101. jarvis/jarvis_utils/globals.py +3 -3
  102. jarvis/jarvis_utils/http.py +1 -1
  103. jarvis/jarvis_utils/input.py +78 -2
  104. jarvis/jarvis_utils/methodology.py +25 -19
  105. jarvis/jarvis_utils/utils.py +644 -359
  106. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/METADATA +85 -1
  107. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  108. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +4 -0
  109. jarvis/jarvis_agent/config.py +0 -92
  110. jarvis/jarvis_tools/edit_file.py +0 -179
  111. jarvis/jarvis_tools/rewrite_file.py +0 -191
  112. jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
  113. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  114. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  115. {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ from jarvis.jarvis_agent import Agent
9
9
  from jarvis.jarvis_utils.input import get_multiline_input
10
10
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
11
11
  from jarvis.jarvis_utils.utils import init_env
12
+ from jarvis.jarvis_utils.config import set_config
12
13
 
13
14
  app = typer.Typer(help="Jarvis AI 助手")
14
15
 
@@ -46,16 +47,45 @@ def cli(
46
47
  None, "-c", "--agent-definition", help="代理定义文件路径"
47
48
  ),
48
49
  task: Optional[str] = typer.Option(None, "-T", "--task", help="初始任务内容"),
49
-
50
+
50
51
  model_group: Optional[str] = typer.Option(
51
52
  None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
52
53
  ),
54
+ non_interactive: bool = typer.Option(
55
+ False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
56
+ ),
53
57
  ):
54
58
  """Main entry point for Jarvis agent"""
55
- # Initialize environment
59
+ # CLI 标志:非交互模式(不依赖配置文件)
60
+ if non_interactive:
61
+ try:
62
+ os.environ["JARVIS_NON_INTERACTIVE"] = "true"
63
+ except Exception:
64
+ pass
65
+ try:
66
+ set_config("JARVIS_NON_INTERACTIVE", True)
67
+ except Exception:
68
+ pass
69
+ # 非交互模式要求从命令行传入任务
70
+ if non_interactive and not (task and str(task).strip()):
71
+ PrettyOutput.print(
72
+ "非交互模式已启用:必须使用 --task 传入任务内容,因多行输入不可用。",
73
+ OutputType.ERROR,
74
+ )
75
+ raise typer.Exit(code=2)
76
+ # Initialize环境
56
77
  init_env(
57
78
  "欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=config_file
58
79
  )
80
+ # 在初始化环境后同步 CLI 选项到全局配置,避免被 init_env 覆盖
81
+ try:
82
+ if model_group:
83
+ set_config("JARVIS_LLM_GROUP", str(model_group))
84
+ if non_interactive:
85
+ set_config("JARVIS_NON_INTERACTIVE", True)
86
+ except Exception:
87
+ # 静默忽略同步异常,不影响主流程
88
+ pass
59
89
 
60
90
  # Load configuration
61
91
  config = load_config(agent_definition) if agent_definition else {}
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import re
4
+ from typing import Any, List, Tuple
5
+
6
+ from jarvis.jarvis_agent.output_handler import OutputHandler
7
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
8
+ from jarvis.jarvis_utils.tag import ct, ot
9
+
10
+
11
+ class RewriteFileHandler(OutputHandler):
12
+ """
13
+ 处理整文件重写指令的输出处理器。
14
+
15
+ 指令格式:
16
+ <REWRITE file=文件路径>
17
+ 新的文件完整内容
18
+ </REWRITE>
19
+
20
+ 等价支持以下写法:
21
+ <REWRITE file=文件路径>
22
+ 新的文件完整内容
23
+ </REWRITE>
24
+
25
+ 说明:
26
+ - 该处理器用于完全重写文件内容,适用于新增文件或大范围改写
27
+ - 内部直接执行写入,提供失败回滚能力
28
+ - 支持同一响应中包含多个 REWRITE/REWRITE 块
29
+ """
30
+
31
+ def __init__(self) -> None:
32
+ # 允许 file 参数为单引号、双引号或无引号
33
+ self.rewrite_pattern_file = re.compile(
34
+ ot("REWRITE file=(?:'([^']+)'|\"([^\"]+)\"|([^>]+))")
35
+ + r"\s*"
36
+ + r"(.*?)"
37
+ + r"\s*"
38
+ + r"^"
39
+ + ct("REWRITE"),
40
+ re.DOTALL | re.MULTILINE,
41
+ )
42
+
43
+ def name(self) -> str:
44
+ """获取处理器名称,用于操作列表展示"""
45
+ return "REWRITE"
46
+
47
+ def prompt(self) -> str:
48
+ """返回用户提示,描述使用方法与格式"""
49
+ return f"""文件重写指令格式:
50
+ {ot("REWRITE file=文件路径")}
51
+ 新的文件完整内容
52
+ {ct("REWRITE")}
53
+
54
+ 注意:
55
+ - {ot("REWRITE")}、{ct("REWRITE")} 必须出现在行首,否则不生效(会被忽略)
56
+ - 整文件重写会完全替换文件内容,如需局部修改请使用 PATCH 操作
57
+ - 该操作由处理器直接执行,具备失败回滚能力"""
58
+
59
+ def can_handle(self, response: str) -> bool:
60
+ """判断响应中是否包含 REWRITE/REWRITE 指令"""
61
+ return bool(self.rewrite_pattern_file.search(response))
62
+
63
+ def handle(self, response: str, agent: Any) -> Tuple[bool, str]:
64
+ """解析并执行整文件重写指令"""
65
+ rewrites = self._parse_rewrites(response)
66
+ if not rewrites:
67
+ return False, "未找到有效的文件重写指令"
68
+
69
+ # 记录 REWRITE 操作调用统计
70
+ try:
71
+ from jarvis.jarvis_stats.stats import StatsManager
72
+
73
+ StatsManager.increment("rewrite_file", group="tool")
74
+ except Exception:
75
+ # 统计失败不影响主流程
76
+ pass
77
+
78
+ results: List[str] = []
79
+
80
+ for file_path, content in rewrites:
81
+ abs_path = os.path.abspath(file_path)
82
+ original_content = None
83
+ processed = False
84
+ try:
85
+ file_exists = os.path.exists(abs_path)
86
+ if file_exists:
87
+ with open(abs_path, "r", encoding="utf-8") as rf:
88
+ original_content = rf.read()
89
+ os.makedirs(os.path.dirname(abs_path), exist_ok=True)
90
+ with open(abs_path, "w", encoding="utf-8") as wf:
91
+ wf.write(content)
92
+ processed = True
93
+ results.append(f"✅ 文件 {abs_path} 重写成功")
94
+ # 记录成功处理的文件(使用绝对路径)
95
+ if agent:
96
+ files = agent.get_user_data("files")
97
+ if files:
98
+ if abs_path not in files:
99
+ files.append(abs_path)
100
+ else:
101
+ files = [abs_path]
102
+ agent.set_user_data("files", files)
103
+ except Exception as e:
104
+ # 回滚已修改内容
105
+ try:
106
+ if processed:
107
+ if original_content is None:
108
+ if os.path.exists(abs_path):
109
+ os.remove(abs_path)
110
+ else:
111
+ with open(abs_path, "w", encoding="utf-8") as wf:
112
+ wf.write(original_content)
113
+ except Exception:
114
+ pass
115
+ PrettyOutput.print(f"文件重写失败: {str(e)}", OutputType.ERROR)
116
+ results.append(f"❌ 文件 {abs_path} 重写失败: {str(e)}")
117
+
118
+ summary = "\n".join(results)
119
+ # 按现有 EditFileHandler 约定,始终返回 (False, summary) 以继续主循环
120
+ return False, summary
121
+
122
+ def _parse_rewrites(self, response: str) -> List[Tuple[str, str]]:
123
+ """
124
+ 解析响应中的 REWRITE/REWRITE 指令块。
125
+ 返回列表 [(file_path, content), ...],按在响应中的出现顺序排序
126
+ """
127
+ items: List[Tuple[str, str]] = []
128
+ matches: List[Tuple[int, Any]] = []
129
+ for m in self.rewrite_pattern_file.finditer(response):
130
+ matches.append((m.start(), m))
131
+
132
+ # 按出现顺序排序
133
+ matches.sort(key=lambda x: x[0])
134
+
135
+ for _, m in matches:
136
+ file_path = m.group(1) or m.group(2) or m.group(3) or ""
137
+ file_path = file_path.strip()
138
+ content = m.group(4)
139
+ if file_path:
140
+ items.append((file_path, content))
141
+ return items
@@ -7,12 +7,14 @@ 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
14
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
14
15
  from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL
15
16
  from jarvis.jarvis_agent.utils import join_prompts, is_auto_complete, normalize_next_action
17
+ from jarvis.jarvis_utils.config import get_auto_summary_rounds
16
18
 
17
19
  if TYPE_CHECKING:
18
20
  # 仅用于类型标注,避免运行时循环依赖
@@ -22,6 +24,14 @@ if TYPE_CHECKING:
22
24
  class AgentRunLoop:
23
25
  def __init__(self, agent: "Agent") -> None:
24
26
  self.agent = agent
27
+ self.conversation_rounds = 0
28
+ self.tool_reminder_rounds = int(os.environ.get("JARVIS_TOOL_REMINDER_ROUNDS", 20))
29
+ # 基于轮次的自动总结阈值:优先使用 Agent 入参,否则回落到全局配置(默认20轮)
30
+ self.auto_summary_rounds = (
31
+ self.agent.auto_summary_rounds
32
+ if getattr(self.agent, "auto_summary_rounds", None) is not None
33
+ else get_auto_summary_rounds()
34
+ )
25
35
 
26
36
  def run(self) -> Any:
27
37
  """主运行循环(委派到传入的 agent 实例的方法与属性)"""
@@ -29,6 +39,23 @@ class AgentRunLoop:
29
39
 
30
40
  while True:
31
41
  try:
42
+ self.conversation_rounds += 1
43
+ if self.conversation_rounds % self.tool_reminder_rounds == 0:
44
+ self.agent.session.addon_prompt = join_prompts(
45
+ [self.agent.session.addon_prompt, self.agent.get_tool_usage_prompt()]
46
+ )
47
+ # 基于轮次的自动总结判断:达到阈值后执行一次总结与历史清理
48
+ if self.conversation_rounds >= self.auto_summary_rounds:
49
+ summary_text = self.agent._summarize_and_clear_history()
50
+ if summary_text:
51
+ # 将摘要作为下一轮的附加提示加入,从而维持上下文连续性
52
+ self.agent.session.addon_prompt = join_prompts(
53
+ [self.agent.session.addon_prompt, summary_text]
54
+ )
55
+ # 重置轮次计数与对话长度计数器,开始新一轮周期
56
+ self.conversation_rounds = 0
57
+ self.agent.session.conversation_length = 0
58
+
32
59
  ag = self.agent
33
60
 
34
61
  # 更新输入处理器标志
@@ -72,12 +99,13 @@ class AgentRunLoop:
72
99
  pass
73
100
  need_return, tool_prompt = ag._call_tools(current_response)
74
101
 
75
- # 将上一个提示和工具提示安全地拼接起来
76
- ag.session.prompt = join_prompts([ag.session.prompt, tool_prompt])
77
-
102
+ # 如果工具要求立即返回结果(例如 SEND_MESSAGE 需要将字典返回给上层),直接返回该结果
78
103
  if need_return:
79
- return ag.session.prompt
104
+ return tool_prompt
80
105
 
106
+ # 将上一个提示和工具提示安全地拼接起来(仅当工具结果为字符串时)
107
+ safe_tool_prompt = tool_prompt if isinstance(tool_prompt, str) else ""
108
+ ag.session.prompt = join_prompts([ag.session.prompt, safe_tool_prompt])
81
109
 
82
110
  # 广播工具调用后的事件(不影响主流程)
83
111
  try:
@@ -97,7 +125,12 @@ class AgentRunLoop:
97
125
 
98
126
  # 检查自动完成
99
127
  if ag.auto_complete and is_auto_complete(current_response):
100
- return ag._complete_task(auto_completed=True)
128
+ # 先运行_complete_task,触发记忆整理/事件等副作用,再决定返回值
129
+ result = ag._complete_task(auto_completed=True)
130
+ # 若不需要summary,则将最后一条LLM输出作为返回值
131
+ if not getattr(ag, "need_summary", True):
132
+ return current_response
133
+ return result
101
134
 
102
135
  # 获取下一步用户输入
103
136
  next_action = ag._get_next_user_action()
@@ -49,7 +49,14 @@ 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
  """克隆或更新中心仓库"""
@@ -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
@@ -128,7 +128,7 @@ class TaskAnalyzer:
128
128
 
129
129
  def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
130
130
  """处理有工具调用时的中断"""
131
- if self.agent.user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
131
+ if self.agent.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
132
132
  return join_prompts([
133
133
  f"被用户中断,用户补充信息为:{user_input}",
134
134
  "用户同意继续工具调用。"
@@ -144,7 +144,7 @@ class TaskAnalyzer:
144
144
  satisfaction_feedback = ""
145
145
 
146
146
  if not auto_completed and self.agent.use_analysis:
147
- if self.agent.user_confirm("您对本次任务的完成是否满意?", True):
147
+ if self.agent.confirm_callback("您对本次任务的完成是否满意?", True):
148
148
  satisfaction_feedback = "用户对本次任务的完成表示满意。"
149
149
  else:
150
150
  feedback = self.agent._multiline_input(
@@ -158,6 +158,9 @@ class TaskAnalyzer:
158
158
  satisfaction_feedback = (
159
159
  "用户对本次任务的完成不满意,未提供具体反馈意见。"
160
160
  )
161
+ elif auto_completed and self.agent.use_analysis:
162
+ # 自动完成模式下,仍然执行分析,但不收集用户反馈
163
+ satisfaction_feedback = "任务已自动完成,无需用户反馈。"
161
164
 
162
165
  return satisfaction_feedback
163
166