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
@@ -0,0 +1,786 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 基于 FastAPI 的 Web 服务:
4
+ - GET / 返回简易网页(含JS,连接 WebSocket,展示输出,处理输入/确认)
5
+ - WS /ws 建立双向通信:服务端通过 WebBridge 广播输出与输入请求;客户端上行提交 user_input/confirm_response 或 run_task
6
+ - WS /stdio 独立通道:专门接收标准输出/错误(sys.stdout/sys.stderr)重定向的流式文本
7
+
8
+ 集成方式(在 --web 模式下):
9
+ - 注册 WebSocketOutputSink,将 PrettyOutput 事件广播到前端
10
+ - 注入 web_multiline_input 与 web_user_confirm 到 Agent,使输入与确认经由浏览器完成
11
+ - 启动本服务,前端通过页面与 Agent 交互
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import os
18
+ import signal
19
+ import atexit
20
+ from pathlib import Path
21
+ from typing import Any, Dict, Callable, Optional, List
22
+
23
+ import uvicorn
24
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
25
+ from fastapi.responses import HTMLResponse
26
+ from fastapi.middleware.cors import CORSMiddleware
27
+
28
+ from jarvis.jarvis_agent.web_bridge import WebBridge
29
+ from jarvis.jarvis_utils.globals import set_interrupt, console
30
+
31
+ # ---------------------------
32
+ # 应用与页面
33
+ # ---------------------------
34
+ def _build_app() -> FastAPI:
35
+ app = FastAPI(title="Jarvis Web")
36
+
37
+ # 允许本地简单跨域调试
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ @app.get("/", response_class=HTMLResponse)
47
+ async def index() -> str:
48
+ # 上下布局 + xterm.js 终端显示输出;底部输入面板
49
+ return """
50
+ <!doctype html>
51
+ <html>
52
+ <head>
53
+ <meta charset="utf-8" />
54
+ <title>Jarvis Web</title>
55
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
56
+ <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css" />
57
+ <style>
58
+ html, body { height: 100%; }
59
+ body {
60
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
61
+ margin: 0;
62
+ padding: 0;
63
+ display: flex;
64
+ flex-direction: column; /* 上下布局:上输出,下输入 */
65
+ background: #000;
66
+ color: #eee;
67
+ }
68
+ /* 顶部:终端输出区域(占满剩余空间) */
69
+ #terminal {
70
+ flex: 1;
71
+ background: #000; /* 终端背景 */
72
+ overflow: hidden;
73
+ }
74
+ /* 底部:输入区域(固定高度) */
75
+ #input-panel {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 8px;
79
+ padding: 10px;
80
+ background: #0b0b0b;
81
+ border-top: 1px solid #222;
82
+ }
83
+ #tip { color: #9aa0a6; font-size: 13px; }
84
+ textarea#input {
85
+ width: 100%;
86
+ height: 140px;
87
+ background: #0f0f0f;
88
+ color: #e5e7eb;
89
+ border: 1px solid #333;
90
+ border-radius: 6px;
91
+ padding: 8px;
92
+ resize: vertical;
93
+ outline: none;
94
+ }
95
+ #actions {
96
+ display: flex;
97
+ gap: 10px;
98
+ align-items: center;
99
+ }
100
+ button {
101
+ padding: 8px 12px;
102
+ background: #1f2937;
103
+ color: #e5e7eb;
104
+ border: 1px solid #374151;
105
+ border-radius: 6px;
106
+ cursor: pointer;
107
+ }
108
+ button:hover { background: #374151; }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div id="terminal"></div>
113
+
114
+ <!-- xterm.js 与 fit 插件 -->
115
+ <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
116
+ <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
117
+
118
+ <script>
119
+ // 初始化 xterm 终端
120
+ const term = new Terminal({
121
+ convertEol: true,
122
+ fontSize: 13,
123
+ fontFamily: '"FiraCode Nerd Font", "JetBrainsMono NF", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
124
+ theme: {
125
+ background: '#000000',
126
+ foreground: '#e5e7eb',
127
+ cursor: '#e5e7eb',
128
+ }
129
+ });
130
+ const fitAddon = new FitAddon.FitAddon();
131
+ term.loadAddon(fitAddon);
132
+ term.open(document.getElementById('terminal'));
133
+ // 捕获前端键入并透传到后端作为 STDIN(与后端 sys.stdin 对接)
134
+ try {
135
+ term.onData((data) => {
136
+ try {
137
+ // 优先将键入数据发送到交互式终端(PTY)通道;若未就绪则回退到 STDIN 重定向通道
138
+ if (typeof wsTerm !== 'undefined' && wsTerm && wsTerm.readyState === WebSocket.OPEN) {
139
+ wsTerm.send(JSON.stringify({ type: 'stdin', data }));
140
+ } else if (wsStd && wsStd.readyState === WebSocket.OPEN) {
141
+ wsStd.send(JSON.stringify({ type: 'stdin', data }));
142
+ }
143
+ } catch (e) {}
144
+ });
145
+ } catch (e) {}
146
+
147
+ function fitTerminal() {
148
+ try {
149
+ fitAddon.fit();
150
+ // 将终端尺寸通知后端(用于动态调整TTY宽度/高度)
151
+ if (typeof wsCtl !== 'undefined' && wsCtl && wsCtl.readyState === WebSocket.OPEN) {
152
+ try {
153
+ wsCtl.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
154
+ } catch (e) {}
155
+ }
156
+ // 同步调整交互式终端(PTY)窗口大小
157
+ if (typeof wsTerm !== 'undefined' && wsTerm && wsTerm.readyState === WebSocket.OPEN) {
158
+ try {
159
+ wsTerm.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
160
+ } catch (e) {}
161
+ }
162
+ } catch (e) {}
163
+ }
164
+ fitTerminal();
165
+ window.addEventListener('resize', fitTerminal);
166
+
167
+ // 输出辅助
168
+ function writeLine(text) {
169
+ const lines = (text ?? '').toString().split('\\n');
170
+ for (const ln of lines) term.writeln(ln);
171
+ }
172
+ function write(text) {
173
+ term.write((text ?? '').toString());
174
+ }
175
+
176
+ // WebSocket 通道:STDIO、控制与交互式终端
177
+ const wsProto = (location.protocol === 'https:') ? 'wss' : 'ws';
178
+ const wsStd = new WebSocket(wsProto + '://' + location.host + '/stdio');
179
+ const wsCtl = new WebSocket(wsProto + '://' + location.host + '/control');
180
+ // 交互式终端(PTY)通道:用于真正的交互式命令
181
+ const wsTerm = new WebSocket(wsProto + '://' + location.host + '/terminal');
182
+ let ctlReady = false;
183
+
184
+
185
+ wsStd.onopen = () => { writeLine('STDIO 通道已连接'); };
186
+ wsStd.onclose = () => { writeLine('STDIO 通道已关闭'); };
187
+ wsStd.onerror = (e) => { writeLine('STDIO 通道错误: ' + e); };
188
+ wsCtl.onopen = () => {
189
+ writeLine('控制通道已连接');
190
+ ctlReady = true;
191
+ // 初次连接时立即上报当前终端尺寸
192
+ try {
193
+ wsCtl.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
194
+ } catch (e) {}
195
+ };
196
+ wsCtl.onclose = () => {
197
+ writeLine('控制通道已关闭');
198
+ ctlReady = false;
199
+ };
200
+ wsCtl.onerror = (e) => { writeLine('控制通道错误: ' + e); };
201
+
202
+ // 终端(PTY)通道
203
+ wsTerm.onopen = () => {
204
+ writeLine('终端通道已连接');
205
+ // 初次连接时上报当前终端尺寸
206
+ try {
207
+ wsTerm.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
208
+ } catch (e) {}
209
+ };
210
+ wsTerm.onclose = () => { writeLine('终端通道已关闭'); };
211
+ wsTerm.onerror = (e) => { writeLine('终端通道错误: ' + e); };
212
+ wsTerm.onmessage = (evt) => {
213
+ try {
214
+ const data = JSON.parse(evt.data || '{}');
215
+ if (data.type === 'stdio') {
216
+ const text = data.text || '';
217
+ write(text);
218
+ }
219
+ } catch (e) {
220
+ writeLine('消息解析失败: ' + e);
221
+ }
222
+ };
223
+
224
+
225
+
226
+ // STDIO 通道消息(原样写入,保留流式体验)
227
+ wsStd.onmessage = (evt) => {
228
+ try {
229
+ const data = JSON.parse(evt.data || '{}');
230
+ if (data.type === 'stdio') {
231
+ const text = data.text || '';
232
+ write(text);
233
+ }
234
+ } catch (e) {
235
+ writeLine('消息解析失败: ' + e);
236
+ }
237
+ };
238
+
239
+
240
+
241
+ </script>
242
+ </body>
243
+ </html>
244
+ """
245
+
246
+ return app
247
+
248
+ # ---------------------------
249
+ # WebSocket 端点
250
+ # ---------------------------
251
+ async def _ws_sender_loop(ws: WebSocket, queue: "asyncio.Queue[Dict[str, Any]]") -> None:
252
+ try:
253
+ while True:
254
+ payload = await queue.get()
255
+ await ws.send_text(json.dumps(payload))
256
+ except Exception:
257
+ # 发送循环异常即退出
258
+ pass
259
+
260
+ def _make_sender(queue: "asyncio.Queue[Dict[str, Any]]") -> Callable[[Dict[str, Any]], None]:
261
+ # 同步函数,供 WebBridge 注册;将消息放入异步队列,由协程发送
262
+ def _sender(payload: Dict[str, Any]) -> None:
263
+ try:
264
+ queue.put_nowait(payload)
265
+ except Exception:
266
+ pass
267
+ return _sender
268
+
269
+ def _make_sender_filtered(queue: "asyncio.Queue[Dict[str, Any]]", allowed_types: Optional[list[str]] = None) -> Callable[[Dict[str, Any]], None]:
270
+ """
271
+ 过滤版 sender:仅将指定类型的payload放入队列(用于单独的STDIO通道)。
272
+ """
273
+ allowed = set(allowed_types or [])
274
+ def _sender(payload: Dict[str, Any]) -> None:
275
+ try:
276
+ ptype = payload.get("type")
277
+ if ptype in allowed:
278
+ queue.put_nowait(payload)
279
+ except Exception:
280
+ pass
281
+ return _sender
282
+
283
+ def _run_and_notify(agent: Any, text: str) -> None:
284
+ try:
285
+ agent.run(text)
286
+ finally:
287
+ try:
288
+ WebBridge.instance().broadcast({"type": "agent_idle"})
289
+ except Exception:
290
+ pass
291
+
292
+ def start_web_server(agent: Any, host: str = "127.0.0.1", port: int = 8765, launch_command: Optional[List[str]] = None) -> None:
293
+ """
294
+ 启动Web服务,并将Agent绑定到应用上下文。
295
+ - agent: 现有的 Agent 实例(已完成初始化)
296
+ - host: Web 服务主机地址
297
+ - port: Web 服务端口
298
+ - launch_command: 交互式终端启动命令(列表格式,如 ["jvs", "--task", "xxx"]),
299
+ 如果为 None,则从环境变量 JARVIS_WEB_LAUNCH_JSON 读取
300
+ """
301
+ app = _build_app()
302
+ app.state.agent = agent # 供 WS 端点调用
303
+ # 兼容传入 Agent 或 AgentManager:
304
+ # - 若传入的是 AgentManager,则在每个任务开始前通过 initialize() 创建全新 Agent
305
+ # - 若传入的是 Agent 实例,则复用该 Agent(旧行为)
306
+ try:
307
+ app.state.agent_manager = agent if hasattr(agent, "initialize") else None
308
+ except Exception:
309
+ app.state.agent_manager = None
310
+ # 存储启动命令到应用状态
311
+ app.state.launch_command = launch_command
312
+
313
+ @app.websocket("/stdio")
314
+ async def websocket_stdio(ws: WebSocket) -> None:
315
+ await ws.accept()
316
+ queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
317
+ sender = _make_sender_filtered(queue, allowed_types=["stdio"])
318
+ bridge = WebBridge.instance()
319
+ bridge.add_client(sender)
320
+ send_task = asyncio.create_task(_ws_sender_loop(ws, queue))
321
+ try:
322
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "STDIO 通道已就绪", "output_type": "INFO"}}))
323
+ except Exception:
324
+ pass
325
+ try:
326
+ while True:
327
+ # 接收来自前端 STDIN 数据并注入到后端
328
+ msg = await ws.receive_text()
329
+ try:
330
+ data = json.loads(msg)
331
+ except Exception:
332
+ continue
333
+ mtype = data.get("type")
334
+ if mtype == "stdin":
335
+ try:
336
+ from jarvis.jarvis_agent.stdio_redirect import feed_web_stdin
337
+ text = data.get("data", "")
338
+ if isinstance(text, str) and text:
339
+ feed_web_stdin(text)
340
+ except Exception:
341
+ pass
342
+ else:
343
+ # 忽略未知类型
344
+ pass
345
+ except WebSocketDisconnect:
346
+ pass
347
+ except Exception:
348
+ pass
349
+ pass
350
+ finally:
351
+ try:
352
+ bridge.remove_client(sender)
353
+ except Exception:
354
+ pass
355
+ try:
356
+ send_task.cancel()
357
+ except Exception:
358
+ pass
359
+
360
+ @app.websocket("/control")
361
+ async def websocket_control(ws: WebSocket) -> None:
362
+ await ws.accept()
363
+ try:
364
+ while True:
365
+ msg = await ws.receive_text()
366
+ try:
367
+ data = json.loads(msg)
368
+ except Exception:
369
+ continue
370
+ mtype = data.get("type")
371
+ if mtype == "interrupt":
372
+ try:
373
+ set_interrupt(True)
374
+ # 可选:发送回执
375
+ await ws.send_text(json.dumps({"type": "ack", "cmd": "interrupt"}))
376
+ except Exception:
377
+ pass
378
+ elif mtype == "resize":
379
+ # 动态调整后端TTY宽度(影响 PrettyOutput 和基于终端宽度的逻辑)
380
+ try:
381
+ cols = int(data.get("cols") or 0)
382
+ rows = int(data.get("rows") or 0)
383
+ except Exception:
384
+ cols = 0
385
+ rows = 0
386
+ try:
387
+ if cols > 0:
388
+ os.environ["COLUMNS"] = str(cols)
389
+ try:
390
+ # 覆盖全局 rich Console 的宽度,便于 PrettyOutput 按照前端列数换行
391
+ console._width = cols
392
+ except Exception:
393
+ pass
394
+ if rows > 0:
395
+ os.environ["LINES"] = str(rows)
396
+ except Exception:
397
+ pass
398
+ except WebSocketDisconnect:
399
+ pass
400
+ except Exception:
401
+ pass
402
+ finally:
403
+ try:
404
+ await ws.close()
405
+ except Exception:
406
+ pass
407
+
408
+ # 交互式终端通道:为前端 xterm 提供真实的 PTY 会话,以支持交互式命令
409
+ @app.websocket("/terminal")
410
+ async def websocket_terminal(ws: WebSocket) -> None:
411
+ await ws.accept()
412
+ # 仅在非 Windows 平台提供 PTY 功能
413
+ import sys as _sys
414
+ if _sys.platform == "win32":
415
+ try:
416
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "当前平台不支持交互式终端(PTY)", "output_type": "ERROR"}}))
417
+ except Exception:
418
+ pass
419
+ try:
420
+ await ws.close()
421
+ except Exception:
422
+ pass
423
+ return
424
+
425
+ import os as _os
426
+ try:
427
+ import pty as _pty
428
+ import fcntl as _fcntl
429
+ import select as _select
430
+ import termios as _termios
431
+ import struct as _struct
432
+ except Exception:
433
+ try:
434
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "服务端缺少 PTY 相关依赖,无法启动交互式终端", "output_type": "ERROR"}}))
435
+ except Exception:
436
+ pass
437
+ try:
438
+ await ws.close()
439
+ except Exception:
440
+ pass
441
+ return
442
+
443
+ def _set_winsize(fd: int, cols: int, rows: int) -> None:
444
+ try:
445
+ if cols > 0 and rows > 0:
446
+ winsz = _struct.pack("HHHH", rows, cols, 0, 0)
447
+ _fcntl.ioctl(fd, _termios.TIOCSWINSZ, winsz)
448
+ except Exception:
449
+ # 调整失败不影响主流程
450
+ pass
451
+
452
+ # 交互式会话状态与启动函数(优先执行 jvs 命令,失败回退到系统 shell)
453
+ session: Dict[str, Optional[int]] = {"pid": None, "master_fd": None}
454
+ last_cols = 0
455
+ last_rows = 0
456
+ # 会话结束后等待用户按回车再重启
457
+ waiting_for_ack = False
458
+ ack_event = asyncio.Event()
459
+
460
+ # 在 fork 前获取启动命令(避免在子进程中访问 app.state)
461
+ _launch_cmd = None
462
+ try:
463
+ if hasattr(app.state, "launch_command") and app.state.launch_command:
464
+ _launch_cmd = app.state.launch_command
465
+ # 调试输出
466
+ if _os.environ.get("JARVIS_DEBUG_WEB_LAUNCH_CMD") == "1":
467
+ print(f"🔍 Web服务器: 使用传入的启动命令: {_launch_cmd}")
468
+ else:
469
+ # 回退到环境变量
470
+ import json as _json
471
+ _cmd_json = _os.environ.get("JARVIS_WEB_LAUNCH_JSON", "")
472
+ if _cmd_json:
473
+ try:
474
+ _launch_cmd = _json.loads(_cmd_json)
475
+ if _os.environ.get("JARVIS_DEBUG_WEB_LAUNCH_CMD") == "1":
476
+ print(f"🔍 Web服务器: 从环境变量读取启动命令: {_launch_cmd}")
477
+ except Exception:
478
+ _launch_cmd = None
479
+ except Exception:
480
+ _launch_cmd = None
481
+
482
+ def _spawn_jvs_session() -> bool:
483
+ nonlocal session, _launch_cmd
484
+ try:
485
+ pid, master_fd = _pty.fork()
486
+ if pid == 0:
487
+ # 子进程:执行启动命令,失败时回退到系统 shell
488
+ # 使用在 fork 前获取的命令
489
+ _argv = _launch_cmd
490
+
491
+ # 如果获取到有效命令,执行它
492
+ if _argv and isinstance(_argv, list) and len(_argv) > 0 and isinstance(_argv[0], str):
493
+ try:
494
+ if _os.environ.get("JARVIS_DEBUG_WEB_LAUNCH_CMD") == "1":
495
+ print(f"🔍 子进程: 执行命令: {_argv}")
496
+ _os.execvp(_argv[0], _argv)
497
+ except Exception as e:
498
+ if _os.environ.get("JARVIS_DEBUG_WEB_LAUNCH_CMD") == "1":
499
+ print(f"⚠️ 子进程: 执行命令失败: {e}")
500
+ pass
501
+ # 若未配置或执行失败,回退到 /bin/bash 或 /bin/sh
502
+ try:
503
+ _os.execvp("/bin/bash", ["/bin/bash"])
504
+ except Exception:
505
+ try:
506
+ _os.execvp("/bin/sh", ["/bin/sh"])
507
+ except Exception:
508
+ _os._exit(1)
509
+ else:
510
+ # 父进程:设置非阻塞模式并记录状态
511
+ try:
512
+ _fcntl.fcntl(master_fd, _fcntl.F_SETFL, _os.O_NONBLOCK)
513
+ except Exception:
514
+ pass
515
+ session["pid"] = pid
516
+ session["master_fd"] = master_fd
517
+ # 如果已有窗口大小设置,应用到新会话
518
+ try:
519
+ if last_cols > 0 and last_rows > 0:
520
+ winsz = _struct.pack("HHHH", last_rows, last_cols, 0, 0)
521
+ _fcntl.ioctl(master_fd, _termios.TIOCSWINSZ, winsz)
522
+ except Exception:
523
+ pass
524
+ return True
525
+ except Exception:
526
+ return False
527
+ return False
528
+
529
+ # 启动首个会话
530
+ ok = _spawn_jvs_session()
531
+ if not ok:
532
+ try:
533
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "启动交互式终端失败", "output_type": "ERROR"}}))
534
+ except Exception:
535
+ pass
536
+ try:
537
+ await ws.close()
538
+ except Exception:
539
+ pass
540
+ return
541
+
542
+ async def _tty_read_loop() -> None:
543
+ nonlocal waiting_for_ack
544
+ try:
545
+ while True:
546
+ fd = session.get("master_fd")
547
+ if fd is None:
548
+ # 若正在等待用户按回车确认,则暂不重启
549
+ if waiting_for_ack:
550
+ if ack_event.is_set():
551
+ try:
552
+ ack_event.clear()
553
+ except Exception:
554
+ pass
555
+ waiting_for_ack = False
556
+ if _spawn_jvs_session():
557
+ try:
558
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\njvs 会话已重启\r\n"}))
559
+ except Exception:
560
+ pass
561
+ fd = session.get("master_fd")
562
+ else:
563
+ await asyncio.sleep(0.5)
564
+ continue
565
+ # 等待用户按回车
566
+ await asyncio.sleep(0.1)
567
+ continue
568
+ # 非确认流程:自动重启
569
+ if _spawn_jvs_session():
570
+ try:
571
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\njvs 会话已重启\r\n"}))
572
+ except Exception:
573
+ pass
574
+ fd = session.get("master_fd")
575
+ else:
576
+ await asyncio.sleep(0.5)
577
+ continue
578
+ if not isinstance(fd, int):
579
+ await asyncio.sleep(0.1)
580
+ continue
581
+ try:
582
+ r, _, _ = _select.select([fd], [], [], 0.1)
583
+ except Exception:
584
+ r = []
585
+ if r:
586
+ try:
587
+ data = _os.read(fd, 4096)
588
+ except BlockingIOError:
589
+ data = b""
590
+ except Exception:
591
+ data = b""
592
+ if data:
593
+ try:
594
+ await ws.send_text(json.dumps({"type": "stdio", "text": data.decode(errors="ignore")}))
595
+ except Exception:
596
+ break
597
+ else:
598
+ # 读取到 EOF,说明子进程已退出;提示后等待用户按回车再重启
599
+ try:
600
+ # 关闭旧 master
601
+ try:
602
+ fd2 = session.get("master_fd")
603
+ if isinstance(fd2, int):
604
+ _os.close(fd2)
605
+ except Exception:
606
+ pass
607
+ session["master_fd"] = None
608
+ session["pid"] = None
609
+ # 标记等待用户回车,并提示
610
+ waiting_for_ack = True
611
+ try:
612
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\nAgent 已结束。按回车继续,系统将重启新的 Agent。\r\n> "}))
613
+ except Exception:
614
+ pass
615
+ # 不立即重启,等待顶部 fd None 分支在收到回车后处理
616
+ await asyncio.sleep(0.1)
617
+ except Exception:
618
+ pass
619
+ # 让出事件循环
620
+ try:
621
+ await asyncio.sleep(0)
622
+ except Exception:
623
+ pass
624
+ except Exception:
625
+ pass
626
+
627
+ # 后台读取任务
628
+ read_task = asyncio.create_task(_tty_read_loop())
629
+
630
+ # 初次连接:尝试根据控制通道设定的列数调整终端大小
631
+ try:
632
+ cols = int(_os.environ.get("COLUMNS", "0"))
633
+ except Exception:
634
+ cols = 0
635
+ try:
636
+ rows = int(_os.environ.get("LINES", "0"))
637
+ except Exception:
638
+ rows = 0
639
+ try:
640
+ if cols > 0 and rows > 0:
641
+ _set_winsize(session.get("master_fd") or -1, cols, rows)
642
+ last_cols = cols
643
+ last_rows = rows
644
+ except Exception:
645
+ pass
646
+ # 发送就绪提示
647
+ try:
648
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "交互式终端已就绪(PTY)", "output_type": "INFO"}}))
649
+ except Exception:
650
+ pass
651
+
652
+ try:
653
+ while True:
654
+ msg = await ws.receive_text()
655
+ try:
656
+ data = json.loads(msg)
657
+ except Exception:
658
+ continue
659
+ mtype = data.get("type")
660
+ if mtype == "stdin":
661
+ # 前端键入数据:若等待回车,则捕获回车;否则透传到 PTY
662
+ try:
663
+ text = data.get("data", "")
664
+ if isinstance(text, str) and text:
665
+ if waiting_for_ack:
666
+ # Enter 键触发继续
667
+ if "\r" in text or "\n" in text:
668
+ try:
669
+ ack_event.set()
670
+ except Exception:
671
+ pass
672
+ else:
673
+ # 非回车输入时轻提示
674
+ try:
675
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\n按回车继续。\r\n> "}))
676
+ except Exception:
677
+ pass
678
+ else:
679
+ # 原样写入(保留控制字符);前端可按需发送回车
680
+ _os.write(session.get("master_fd") or -1, text.encode(errors="ignore"))
681
+ except Exception:
682
+ pass
683
+ elif mtype == "resize":
684
+ # 终端窗口大小调整(与控制通道一致,但作用于 PTY)
685
+ try:
686
+ cols = int(data.get("cols") or 0)
687
+ except Exception:
688
+ cols = 0
689
+ try:
690
+ rows = int(data.get("rows") or 0)
691
+ except Exception:
692
+ rows = 0
693
+ try:
694
+ if cols > 0 and rows > 0:
695
+ _set_winsize(session.get("master_fd") or -1, cols, rows)
696
+ last_cols = cols
697
+ last_rows = rows
698
+ except Exception:
699
+ pass
700
+ else:
701
+ # 忽略未知类型
702
+ pass
703
+ except WebSocketDisconnect:
704
+ pass
705
+ except Exception:
706
+ pass
707
+ finally:
708
+ # 清理资源
709
+ try:
710
+ read_task.cancel()
711
+ except Exception:
712
+ pass
713
+ try:
714
+ fd3 = session.get("master_fd")
715
+ if isinstance(fd3, int):
716
+ try:
717
+ _os.close(fd3)
718
+ except Exception:
719
+ pass
720
+ except Exception:
721
+ pass
722
+ try:
723
+ pid_val = session.get("pid")
724
+ if isinstance(pid_val, int):
725
+ import signal as _signal
726
+ try:
727
+ _os.kill(pid_val, _signal.SIGTERM)
728
+ except Exception:
729
+ pass
730
+ except Exception:
731
+ pass
732
+ try:
733
+ await ws.close()
734
+ except Exception:
735
+ pass
736
+
737
+ print(f"✅ 启动 Jarvis Web 服务: http://{host}:{port}")
738
+ # 在服务端进程内也写入并维护 PID 文件,增强可检测性与可清理性
739
+ try:
740
+ pidfile = Path(os.path.expanduser("~/.jarvis")) / f"jarvis_web_{port}.pid"
741
+ try:
742
+ pidfile.parent.mkdir(parents=True, exist_ok=True)
743
+ except Exception:
744
+ pass
745
+ try:
746
+ pidfile.write_text(str(os.getpid()), encoding="utf-8")
747
+ except Exception:
748
+ pass
749
+ # 退出时清理 PID 文件
750
+ def _cleanup_pidfile() -> None:
751
+ try:
752
+ pidfile.unlink(missing_ok=True)
753
+ except Exception:
754
+ pass
755
+ try:
756
+ atexit.register(_cleanup_pidfile)
757
+ except Exception:
758
+ pass
759
+ # 处理 SIGTERM/SIGINT,清理后退出
760
+ def _signal_handler(signum: int, frame: Any) -> None:
761
+ try:
762
+ _cleanup_pidfile()
763
+ finally:
764
+ try:
765
+ os._exit(0)
766
+ except Exception:
767
+ pass
768
+ try:
769
+ signal.signal(signal.SIGTERM, _signal_handler)
770
+ except Exception:
771
+ pass
772
+ try:
773
+ signal.signal(signal.SIGINT, _signal_handler)
774
+ except Exception:
775
+ pass
776
+ except Exception:
777
+ pass
778
+ # 配置 uvicorn 日志级别,隐藏连接信息和访问日志
779
+ import logging
780
+ # 禁用 uvicorn 的访问日志
781
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
782
+ # 禁用 uvicorn 的常规日志(连接信息等)
783
+ logging.getLogger("uvicorn").setLevel(logging.WARNING)
784
+ # 禁用 uvicorn.error 的 INFO 级别日志
785
+ logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
786
+ uvicorn.run(app, host=host, port=port, log_level="warning", access_log=False)