jarvis-ai-assistant 0.1.222__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 (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,139 @@
1
+ # -*- coding: utf-8 -*-
2
+ """工具分享管理模块"""
3
+ import os
4
+ import glob
5
+ import shutil
6
+ from typing import List, Dict, Any, Set
7
+
8
+ import typer
9
+
10
+ from jarvis.jarvis_agent import OutputType, PrettyOutput, user_confirm
11
+ from jarvis.jarvis_agent.share_manager import ShareManager
12
+ from jarvis.jarvis_utils.config import get_central_tool_repo, get_data_dir
13
+
14
+
15
+ class ToolShareManager(ShareManager):
16
+ """工具分享管理器"""
17
+
18
+ def __init__(self):
19
+ central_repo = get_central_tool_repo()
20
+ if not central_repo:
21
+ PrettyOutput.print(
22
+ "错误:未配置中心工具仓库(JARVIS_CENTRAL_TOOL_REPO)",
23
+ OutputType.ERROR,
24
+ )
25
+ PrettyOutput.print(
26
+ "请在配置文件中设置中心工具仓库的Git地址", OutputType.INFO
27
+ )
28
+ raise typer.Exit(code=1)
29
+
30
+ super().__init__(central_repo, "central_tool_repo")
31
+
32
+ def get_resource_type(self) -> str:
33
+ """获取资源类型名称"""
34
+ return "工具"
35
+
36
+ def format_resource_display(self, resource: Dict[str, Any]) -> str:
37
+ """格式化资源显示"""
38
+ return f"{resource['tool_name']} ({resource['filename']})"
39
+
40
+ def get_existing_resources(self) -> Set[str]:
41
+ """获取中心仓库中已有的工具文件名"""
42
+ existing_tools = set()
43
+ for filepath in glob.glob(os.path.join(self.repo_path, "*.py")):
44
+ existing_tools.add(os.path.basename(filepath))
45
+ return existing_tools
46
+
47
+ def get_local_resources(self) -> List[Dict[str, Any]]:
48
+ """获取本地工具"""
49
+ # 获取中心仓库中已有的工具文件名
50
+ existing_tools = self.get_existing_resources()
51
+
52
+ # 只从数据目录的tools目录获取工具
53
+ local_tools_dir = os.path.join(get_data_dir(), "tools")
54
+ if not os.path.exists(local_tools_dir):
55
+ PrettyOutput.print(
56
+ f"本地工具目录不存在: {local_tools_dir}",
57
+ OutputType.WARNING,
58
+ )
59
+ return []
60
+
61
+ # 收集本地工具文件(排除已存在的)
62
+ tool_files = []
63
+ for filepath in glob.glob(os.path.join(local_tools_dir, "*.py")):
64
+ filename = os.path.basename(filepath)
65
+ # 跳过__init__.py和已存在的文件
66
+ if filename == "__init__.py" or filename in existing_tools:
67
+ continue
68
+
69
+ # 尝试获取工具名称(通过简单解析)
70
+ tool_name = filename[:-3] # 移除.py后缀
71
+ tool_files.append(
72
+ {
73
+ "path": filepath,
74
+ "filename": filename,
75
+ "tool_name": tool_name,
76
+ }
77
+ )
78
+
79
+ return tool_files
80
+
81
+ def share_resources(self, resources: List[Dict[str, Any]]) -> List[str]:
82
+ """分享工具到中心仓库"""
83
+ # 确认操作
84
+ share_list = ["\n将要分享以下工具到中心仓库(注意:文件将被移动而非复制):"]
85
+ for tool in resources:
86
+ share_list.append(f"- {tool['tool_name']} ({tool['filename']})")
87
+ PrettyOutput.print("\n".join(share_list), OutputType.WARNING)
88
+
89
+ if not user_confirm("确认移动这些工具到中心仓库吗?(原文件将被删除)"):
90
+ return []
91
+
92
+ # 移动选中的工具到中心仓库
93
+ moved_list = []
94
+ for tool in resources:
95
+ src_file = tool["path"]
96
+ dst_file = os.path.join(self.repo_path, tool["filename"])
97
+ shutil.move(src_file, dst_file) # 使用move而不是copy
98
+ moved_list.append(f"已移动: {tool['tool_name']}")
99
+
100
+ return moved_list
101
+
102
+ def run(self) -> None:
103
+ """执行工具分享流程"""
104
+ try:
105
+ # 更新中心仓库
106
+ self.update_central_repo()
107
+
108
+ # 获取本地资源
109
+ local_resources = self.get_local_resources()
110
+ if not local_resources:
111
+ PrettyOutput.print(
112
+ "没有找到新的工具文件(所有工具可能已存在于中心仓库)",
113
+ OutputType.WARNING,
114
+ )
115
+ return
116
+
117
+ # 选择要分享的资源
118
+ selected_resources = self.select_resources(local_resources)
119
+ if not selected_resources:
120
+ return
121
+
122
+ # 分享资源
123
+ moved_list = self.share_resources(selected_resources)
124
+ if moved_list:
125
+ # 一次性显示所有移动结果
126
+ PrettyOutput.print("\n".join(moved_list), OutputType.SUCCESS)
127
+
128
+ # 提交并推送
129
+ self.commit_and_push(len(selected_resources))
130
+
131
+ PrettyOutput.print("\n工具已成功分享到中心仓库!", OutputType.SUCCESS)
132
+ PrettyOutput.print(
133
+ f"原文件已从 {os.path.join(get_data_dir(), 'tools')} 移动到中心仓库",
134
+ OutputType.INFO,
135
+ )
136
+
137
+ except Exception as e:
138
+ PrettyOutput.print(f"分享工具时出错: {str(e)}", OutputType.ERROR)
139
+ raise typer.Exit(code=1)
@@ -0,0 +1,42 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ UserInteractionHandler: 抽象用户交互(多行输入与确认)逻辑,便于将来替换为 TUI/WebUI。
4
+
5
+ 阶段一(最小变更):
6
+ - 仅提供封装,不直接修改 Agent 的现有调用
7
+ - 后续步骤在 Agent 中以旁路方式接入,保持向后兼容
8
+ """
9
+ from typing import Callable
10
+
11
+
12
+
13
+ class UserInteractionHandler:
14
+ def __init__(
15
+ self,
16
+ multiline_inputer: Callable[..., str],
17
+ confirm_func: Callable[[str, bool], bool],
18
+ ) -> None:
19
+ """
20
+ 参数:
21
+ - multiline_inputer: 提供多行输入的函数,优先支持 (tip, print_on_empty=bool),兼容仅接受 (tip) 的实现
22
+ - confirm_func: 用户确认函数 (tip: str, default: bool) -> bool
23
+ """
24
+ self._multiline_inputer = multiline_inputer
25
+ self._confirm = confirm_func
26
+
27
+ def multiline_input(self, tip: str, print_on_empty: bool) -> str:
28
+ """
29
+ 多行输入封装:兼容两类签名
30
+ 1) func(tip, print_on_empty=True/False)
31
+ 2) func(tip)
32
+ """
33
+ try:
34
+ return self._multiline_inputer(tip, print_on_empty=print_on_empty) # type: ignore[call-arg]
35
+ except TypeError:
36
+ return self._multiline_inputer(tip) # type: ignore[misc]
37
+
38
+ def confirm(self, tip: str, default: bool = True) -> bool:
39
+ """
40
+ 用户确认封装,直接委派
41
+ """
42
+ return self._confirm(tip, default)
@@ -0,0 +1,54 @@
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
+ try:
20
+ non_empty: List[str] = [p for p in parts if isinstance(p, str) and p]
21
+ except Exception:
22
+ # 防御性处理:若 parts 不可迭代或出现异常,直接返回空字符串
23
+ return ""
24
+ return "\n\n".join(non_empty)
25
+
26
+ def is_auto_complete(response: str) -> bool:
27
+ """
28
+ 检测是否包含自动完成标记。
29
+ 当前实现:包含 ot('!!!COMPLETE!!!') 即视为自动完成。
30
+ """
31
+ try:
32
+ return ot("!!!COMPLETE!!!") in response
33
+ except Exception:
34
+ # 防御性处理:即使 ot 出现异常,也不阻塞主流程
35
+ return "!!!COMPLETE!!!" in response
36
+
37
+ def normalize_next_action(next_action: Any) -> str:
38
+ """
39
+ 规范化下一步动作为字符串:
40
+ - 如果是 Enum, 返回其 value(若为字符串)
41
+ - 如果是 str, 原样返回
42
+ - 其他情况返回空字符串
43
+ """
44
+ try:
45
+ if isinstance(next_action, Enum):
46
+ value = getattr(next_action, "value", None)
47
+ return value if isinstance(value, str) else ""
48
+ if isinstance(next_action, str):
49
+ return next_action
50
+ return ""
51
+ except Exception:
52
+ return ""
53
+
54
+ __all__ = ["join_prompts", "is_auto_complete", "normalize_next_action"]
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WebBridge: WebSocket 交互桥
4
+ - 提供线程安全的广播能力(后续由 WebSocket 服务注册发送函数)
5
+ - 提供阻塞式的多行输入与确认请求(通过 request_* 发起请求,等待浏览器端响应)
6
+ - 适配 Agent 的输入注入接口:web_multiline_input / web_user_confirm
7
+ - 事件约定(发往前端,均为 JSON 对象):
8
+ * {"type":"input_request","mode":"multiline","tip": "...","print_on_empty": true/false,"request_id":"..."}
9
+ * {"type":"confirm_request","tip":"...","default": true/false,"request_id":"..."}
10
+ 后续输出事件由输出Sink负责(使用 PrettyOutput.add_sink 接入),不在本桥内实现。
11
+ - 事件约定(来自前端):
12
+ * {"type":"user_input","request_id":"...","text":"..."}
13
+ * {"type":"confirm_response","request_id":"...","value": true/false}
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import threading
18
+ import uuid
19
+ from queue import Queue, Empty
20
+ from typing import Callable, Dict, Optional, Set, Any
21
+
22
+ DEFAULT_WAIT_TIMEOUT = None # 阻塞等待直到收到响应(可按需改为秒数)
23
+
24
+
25
+ class WebBridge:
26
+ """
27
+ 线程安全的 WebSocket 交互桥。
28
+ - 维护一组客户端发送函数(由Web服务注册),用于广播事件
29
+ - 维护挂起的输入/确认请求队列,按 request_id 匹配响应
30
+ """
31
+
32
+ _instance_lock = threading.Lock()
33
+ _instance: Optional["WebBridge"] = None
34
+
35
+ def __init__(self) -> None:
36
+ self._clients: Set[Callable[[Dict[str, Any]], None]] = set()
37
+ self._clients_lock = threading.Lock()
38
+
39
+ # 按 request_id 等待的阻塞队列
40
+ self._pending_inputs: Dict[str, Queue] = {}
41
+ self._pending_confirms: Dict[str, Queue] = {}
42
+ self._pending_lock = threading.Lock()
43
+
44
+ @classmethod
45
+ def instance(cls) -> "WebBridge":
46
+ with cls._instance_lock:
47
+ if cls._instance is None:
48
+ cls._instance = WebBridge()
49
+ return cls._instance
50
+
51
+ # ---------------------------
52
+ # 客户端管理与广播
53
+ # ---------------------------
54
+ def add_client(self, sender: Callable[[Dict[str, Any]], None]) -> None:
55
+ """
56
+ 注册一个客户端发送函数。发送函数需接受一个 dict,并自行完成异步发送。
57
+ 例如在 FastAPI/WS 中包装成 enqueue 到事件循环的任务。
58
+ """
59
+ with self._clients_lock:
60
+ self._clients.add(sender)
61
+
62
+ def remove_client(self, sender: Callable[[Dict[str, Any]], None]) -> None:
63
+ with self._clients_lock:
64
+ if sender in self._clients:
65
+ self._clients.remove(sender)
66
+
67
+ def broadcast(self, payload: Dict[str, Any]) -> None:
68
+ """
69
+ 广播一条消息给所有客户端。失败的客户端不影响其他客户端。
70
+ """
71
+ with self._clients_lock:
72
+ targets = list(self._clients)
73
+ for send in targets:
74
+ try:
75
+ send(payload)
76
+ except Exception:
77
+ # 静默忽略单个客户端的发送异常
78
+ pass
79
+
80
+ # ---------------------------
81
+ # 输入/确认 请求-响应 管理
82
+ # ---------------------------
83
+ def request_multiline_input(self, tip: str, print_on_empty: bool = True, timeout: Optional[float] = DEFAULT_WAIT_TIMEOUT) -> str:
84
+ """
85
+ 发起一个多行输入请求并阻塞等待浏览器返回。
86
+ 返回用户输入的文本(可能为空字符串,表示取消)。
87
+ """
88
+ req_id = uuid.uuid4().hex
89
+ q: Queue = Queue(maxsize=1)
90
+ with self._pending_lock:
91
+ self._pending_inputs[req_id] = q
92
+
93
+ self.broadcast({
94
+ "type": "input_request",
95
+ "mode": "multiline",
96
+ "tip": tip,
97
+ "print_on_empty": bool(print_on_empty),
98
+ "request_id": req_id,
99
+ })
100
+
101
+ try:
102
+ if timeout is None:
103
+ result = q.get() # 阻塞直到有结果
104
+ else:
105
+ result = q.get(timeout=timeout)
106
+ except Empty:
107
+ result = "" # 超时回退为空
108
+ finally:
109
+ with self._pending_lock:
110
+ self._pending_inputs.pop(req_id, None)
111
+
112
+ # 规范化为字符串
113
+ return str(result or "")
114
+
115
+ def request_confirm(self, tip: str, default: bool = True, timeout: Optional[float] = DEFAULT_WAIT_TIMEOUT) -> bool:
116
+ """
117
+ 发起一个确认请求并阻塞等待浏览器返回。
118
+ 返回 True/False,若超时则回退为 default。
119
+ """
120
+ req_id = uuid.uuid4().hex
121
+ q: Queue = Queue(maxsize=1)
122
+ with self._pending_lock:
123
+ self._pending_confirms[req_id] = q
124
+
125
+ self.broadcast({
126
+ "type": "confirm_request",
127
+ "tip": tip,
128
+ "default": bool(default),
129
+ "request_id": req_id,
130
+ })
131
+
132
+ try:
133
+ if timeout is None:
134
+ result = q.get()
135
+ else:
136
+ result = q.get(timeout=timeout)
137
+ except Empty:
138
+ result = default
139
+ finally:
140
+ with self._pending_lock:
141
+ self._pending_confirms.pop(req_id, None)
142
+
143
+ return bool(result)
144
+
145
+ # ---------------------------
146
+ # 由 Web 服务回调:注入用户响应
147
+ # ---------------------------
148
+ def post_user_input(self, request_id: str, text: str) -> None:
149
+ """
150
+ 注入浏览器端的多行输入响应。
151
+ """
152
+ with self._pending_lock:
153
+ q = self._pending_inputs.get(request_id)
154
+ if q:
155
+ try:
156
+ q.put_nowait(text)
157
+ except Exception:
158
+ pass
159
+
160
+ def post_confirm(self, request_id: str, value: bool) -> None:
161
+ """
162
+ 注入浏览器端的确认响应。
163
+ """
164
+ with self._pending_lock:
165
+ q = self._pending_confirms.get(request_id)
166
+ if q:
167
+ try:
168
+ q.put_nowait(bool(value))
169
+ except Exception:
170
+ pass
171
+
172
+
173
+ # ---------------------------
174
+ # 供 Agent 注入的输入函数
175
+ # ---------------------------
176
+ def web_multiline_input(tip: str, print_on_empty: bool = True) -> str:
177
+ """
178
+ 适配 Agent.multiline_inputer 签名的多行输入函数。
179
+ 在 Web 模式下被注入到 Agent,转由浏览器端输入。
180
+ """
181
+ return WebBridge.instance().request_multiline_input(tip, print_on_empty)
182
+
183
+
184
+ def web_user_confirm(tip: str, default: bool = True) -> bool:
185
+ """
186
+ 适配 Agent.confirm_callback 签名的确认函数。
187
+ 在 Web 模式下被注入到 Agent,转由浏览器端确认。
188
+ """
189
+ return WebBridge.instance().request_confirm(tip, default)
@@ -0,0 +1,53 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ WebSocketOutputSink: 将 PrettyOutput 的输出事件通过 WebBridge 广播给前端(WebSocket 客户端)
4
+
5
+ 用法:
6
+ - 在 Web 模式启动时,注册该 Sink:
7
+ from jarvis.jarvis_utils.output import PrettyOutput
8
+ from jarvis.jarvis_agent.web_output_sink import WebSocketOutputSink
9
+ PrettyOutput.add_sink(WebSocketOutputSink())
10
+
11
+ - Web 端收到的消息结构:
12
+ {
13
+ "type": "output",
14
+ "payload": {
15
+ "text": "...",
16
+ "output_type": "INFO" | "ERROR" | ...,
17
+ "timestamp": true/false,
18
+ "lang": "markdown" | "python" | ... | null,
19
+ "traceback": false,
20
+ "section": null | "标题",
21
+ "context": { ... } | null
22
+ }
23
+ }
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Dict
28
+
29
+ from jarvis.jarvis_utils.output import OutputSink, OutputEvent
30
+ from jarvis.jarvis_agent.web_bridge import WebBridge
31
+
32
+
33
+ class WebSocketOutputSink(OutputSink):
34
+ """将输出事件广播到 WebSocket 前端的 OutputSink 实现。"""
35
+
36
+ def emit(self, event: OutputEvent) -> None:
37
+ try:
38
+ payload: Dict[str, Any] = {
39
+ "type": "output",
40
+ "payload": {
41
+ "text": event.text,
42
+ "output_type": event.output_type.value,
43
+ "timestamp": bool(event.timestamp),
44
+ "lang": event.lang,
45
+ "traceback": bool(event.traceback),
46
+ "section": event.section,
47
+ "context": event.context,
48
+ },
49
+ }
50
+ WebBridge.instance().broadcast(payload)
51
+ except Exception:
52
+ # 广播过程中的异常不应影响其他输出后端
53
+ pass