jarvis-ai-assistant 0.3.34__py3-none-any.whl → 0.4.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.
@@ -0,0 +1,647 @@
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
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
+ from jarvis.jarvis_utils.output import PrettyOutput, OutputType
31
+
32
+ # ---------------------------
33
+ # 应用与页面
34
+ # ---------------------------
35
+ def _build_app() -> FastAPI:
36
+ app = FastAPI(title="Jarvis Web")
37
+
38
+ # 允许本地简单跨域调试
39
+ app.add_middleware(
40
+ CORSMiddleware,
41
+ allow_origins=["*"],
42
+ allow_credentials=True,
43
+ allow_methods=["*"],
44
+ allow_headers=["*"],
45
+ )
46
+
47
+ @app.get("/", response_class=HTMLResponse)
48
+ async def index() -> str:
49
+ # 上下布局 + xterm.js 终端显示输出;底部输入面板
50
+ return """
51
+ <!doctype html>
52
+ <html>
53
+ <head>
54
+ <meta charset="utf-8" />
55
+ <title>Jarvis Web</title>
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
57
+ <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css" />
58
+ <style>
59
+ html, body { height: 100%; }
60
+ body {
61
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
62
+ margin: 0;
63
+ padding: 0;
64
+ display: flex;
65
+ flex-direction: column; /* 上下布局:上输出,下输入 */
66
+ background: #000;
67
+ color: #eee;
68
+ }
69
+ /* 顶部:终端输出区域(占满剩余空间) */
70
+ #terminal {
71
+ flex: 1;
72
+ background: #000; /* 终端背景 */
73
+ overflow: hidden;
74
+ }
75
+ /* 底部:输入区域(固定高度) */
76
+ #input-panel {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 8px;
80
+ padding: 10px;
81
+ background: #0b0b0b;
82
+ border-top: 1px solid #222;
83
+ }
84
+ #tip { color: #9aa0a6; font-size: 13px; }
85
+ textarea#input {
86
+ width: 100%;
87
+ height: 140px;
88
+ background: #0f0f0f;
89
+ color: #e5e7eb;
90
+ border: 1px solid #333;
91
+ border-radius: 6px;
92
+ padding: 8px;
93
+ resize: vertical;
94
+ outline: none;
95
+ }
96
+ #actions {
97
+ display: flex;
98
+ gap: 10px;
99
+ align-items: center;
100
+ }
101
+ button {
102
+ padding: 8px 12px;
103
+ background: #1f2937;
104
+ color: #e5e7eb;
105
+ border: 1px solid #374151;
106
+ border-radius: 6px;
107
+ cursor: pointer;
108
+ }
109
+ button:hover { background: #374151; }
110
+ </style>
111
+ </head>
112
+ <body>
113
+ <div id="terminal"></div>
114
+
115
+ <div id="input-panel">
116
+ <div id="tip">输入任务或在请求时回复(Ctrl+Enter 提交)</div>
117
+ <textarea id="input" placeholder="在此输入..."></textarea>
118
+ <div id="actions">
119
+ <button id="send">发送为新任务</button>
120
+ <button id="clear">清空输出</button>
121
+ <button id="interrupt">干预</button>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- xterm.js 与 fit 插件 -->
126
+ <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
127
+ <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
128
+
129
+ <script>
130
+ // 初始化 xterm 终端
131
+ const term = new Terminal({
132
+ convertEol: true,
133
+ fontSize: 13,
134
+ fontFamily: '"FiraCode Nerd Font", "JetBrainsMono NF", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
135
+ theme: {
136
+ background: '#000000',
137
+ foreground: '#e5e7eb',
138
+ cursor: '#e5e7eb',
139
+ }
140
+ });
141
+ const fitAddon = new FitAddon.FitAddon();
142
+ term.loadAddon(fitAddon);
143
+ term.open(document.getElementById('terminal'));
144
+ // 捕获前端键入并透传到后端作为 STDIN(与后端 sys.stdin 对接)
145
+ try {
146
+ term.onData((data) => {
147
+ try {
148
+ if (wsStd && wsStd.readyState === WebSocket.OPEN) {
149
+ wsStd.send(JSON.stringify({ type: 'stdin', data }));
150
+ }
151
+ } catch (e) {}
152
+ });
153
+ } catch (e) {}
154
+
155
+ function fitTerminal() {
156
+ try {
157
+ fitAddon.fit();
158
+ // 将终端尺寸通知后端(用于动态调整TTY宽度/高度)
159
+ if (typeof wsCtl !== 'undefined' && wsCtl && wsCtl.readyState === WebSocket.OPEN) {
160
+ try {
161
+ wsCtl.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
162
+ } catch (e) {}
163
+ }
164
+ } catch (e) {}
165
+ }
166
+ fitTerminal();
167
+ window.addEventListener('resize', fitTerminal);
168
+
169
+ // 输出辅助
170
+ function writeLine(text) {
171
+ const lines = (text ?? '').toString().split('\\n');
172
+ for (const ln of lines) term.writeln(ln);
173
+ }
174
+ function write(text) {
175
+ term.write((text ?? '').toString());
176
+ }
177
+
178
+ // 元素引用
179
+ const tip = document.getElementById('tip');
180
+ const input = document.getElementById('input');
181
+ const btnSend = document.getElementById('send');
182
+ const btnClear = document.getElementById('clear');
183
+ const btnInterrupt = document.getElementById('interrupt');
184
+ // 输入可用性开关:Agent 未请求输入时禁用输入框(但允许通过按钮发送空任务)
185
+ let inputEnabled = false;
186
+ function setInputEnabled(flag) {
187
+ inputEnabled = !!flag;
188
+ try {
189
+ input.disabled = !inputEnabled;
190
+ // 根据可用状态更新占位提示
191
+ input.placeholder = inputEnabled ? '在此输入...' : 'Agent正在运行';
192
+ } catch (e) {}
193
+ }
194
+ // 初始化(未请求输入时禁用输入框)
195
+ setInputEnabled(false);
196
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
197
+
198
+ // WebSocket 通道:主通道与 STDIO 通道
199
+ const wsProto = (location.protocol === 'https:') ? 'wss' : 'ws';
200
+ const ws = new WebSocket(wsProto + '://' + location.host + '/ws');
201
+ const wsStd = new WebSocket(wsProto + '://' + location.host + '/stdio');
202
+ const wsCtl = new WebSocket(wsProto + '://' + location.host + '/control');
203
+ let ctlReady = false;
204
+
205
+ ws.onopen = () => {
206
+ writeLine('WebSocket 已连接');
207
+ // 连接成功后,允许直接输入首条任务
208
+ try {
209
+ setInputEnabled(true);
210
+ // 连接成功后,优先聚焦终端以便直接通过 xterm 进行交互
211
+ if (typeof term !== 'undefined' && term) {
212
+ try { term.focus(); } catch (e) {}
213
+ }
214
+ } catch (e) {}
215
+ fitTerminal();
216
+ };
217
+ ws.onclose = () => { writeLine('WebSocket 已关闭'); };
218
+ ws.onerror = (e) => { writeLine('WebSocket 错误: ' + e); };
219
+
220
+ wsStd.onopen = () => { writeLine('STDIO 通道已连接'); };
221
+ wsStd.onclose = () => { writeLine('STDIO 通道已关闭'); };
222
+ wsStd.onerror = (e) => { writeLine('STDIO 通道错误: ' + e); };
223
+ wsCtl.onopen = () => {
224
+ writeLine('控制通道已连接');
225
+ ctlReady = true;
226
+ // 初次连接时立即上报当前终端尺寸
227
+ try {
228
+ wsCtl.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
229
+ } catch (e) {}
230
+ };
231
+ wsCtl.onclose = () => {
232
+ writeLine('控制通道已关闭');
233
+ ctlReady = false;
234
+ };
235
+ wsCtl.onerror = (e) => { writeLine('控制通道错误: ' + e); };
236
+
237
+ let pendingInputRequest = null; // {request_id, tip, print_on_empty}
238
+ let pendingConfirmRequest = null; // {request_id, tip, default}
239
+
240
+ // 主通道消息
241
+ ws.onmessage = (evt) => {
242
+ try {
243
+ const data = JSON.parse(evt.data || '{}');
244
+ if (data.type === 'output') {
245
+ // 忽略通过 Sink 推送的output事件,避免与STDIO通道的输出重复显示
246
+ } else if (data.type === 'input_request') {
247
+ pendingInputRequest = data;
248
+ tip.textContent = '请求输入: ' + (data.tip || '');
249
+ setInputEnabled(true);
250
+ try { btnSend.textContent = '提交输入'; } catch (e) {}
251
+ input.focus();
252
+ } else if (data.type === 'confirm_request') {
253
+ pendingConfirmRequest = data;
254
+ // 确认请求期间不需要文本输入,禁用输入框并更新按钮文字
255
+ setInputEnabled(false);
256
+ try { btnSend.textContent = '确认中…'; } catch (e) {}
257
+ const ok = window.confirm((data.tip || '') + (data.default ? " [Y/n]" : " [y/N]"));
258
+ ws.send(JSON.stringify({
259
+ type: 'confirm_response',
260
+ request_id: data.request_id,
261
+ value: !!ok
262
+ }));
263
+ // 确认已提交,恢复按钮文字为“发送为新任务”
264
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
265
+ pendingConfirmRequest = null;
266
+ } else if (data.type === 'agent_idle') {
267
+ // 任务结束提示,并恢复输入状态
268
+ try { writeLine('当前任务已结束'); } catch (e) {}
269
+ try { setInputEnabled(true); btnSend.textContent = '发送为新任务'; input.focus(); } catch (e) {}
270
+ } else if (data.type === 'stdio') {
271
+ // 忽略主通道的 stdio 以避免与独立 STDIO 通道重复显示
272
+ }
273
+ } catch (e) {
274
+ writeLine('消息解析失败: ' + e);
275
+ }
276
+ };
277
+
278
+ // STDIO 通道消息(原样写入,保留流式体验)
279
+ wsStd.onmessage = (evt) => {
280
+ try {
281
+ const data = JSON.parse(evt.data || '{}');
282
+ if (data.type === 'stdio') {
283
+ const text = data.text || '';
284
+ write(text);
285
+ }
286
+ } catch (e) {
287
+ writeLine('消息解析失败: ' + e);
288
+ }
289
+ };
290
+
291
+ // 操作区
292
+ btnSend.onclick = () => {
293
+ const text = input.value || '';
294
+ // 若当前处于输入请求阶段,则按钮行为为“提交输入”(允许空输入)
295
+ if (pendingInputRequest) {
296
+ // 发送前在终端回显用户输入
297
+ try { writeLine('> ' + text); } catch (e) {}
298
+ ws.send(JSON.stringify({
299
+ type: 'user_input',
300
+ request_id: pendingInputRequest.request_id,
301
+ text
302
+ }));
303
+ tip.textContent = '输入已提交';
304
+ pendingInputRequest = null;
305
+ input.value = '';
306
+ // 提交输入后禁用输入区,等待下一次请求或任务结束通知
307
+ setInputEnabled(false);
308
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
309
+ fitTerminal();
310
+ return;
311
+ }
312
+ // 否则行为为“发送为新任务”(允许空输入)
313
+ // 发送前在终端回显用户输入
314
+ try { writeLine('> ' + text); } catch (e) {}
315
+ ws.send(JSON.stringify({ type: 'run_task', text }));
316
+ input.value = '';
317
+ // 发送新任务后,直到Agent请求输入前禁用输入区
318
+ setInputEnabled(false);
319
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
320
+ fitTerminal();
321
+ };
322
+
323
+ btnClear.onclick = () => {
324
+ try {
325
+ if (typeof term.clear === 'function') term.clear();
326
+ else term.write('\\x1bc'); // 清屏/重置
327
+ } catch (e) {
328
+ term.write('\\x1bc');
329
+ }
330
+ };
331
+ // 干预(发送中断信号)
332
+ btnInterrupt.onclick = () => {
333
+ try {
334
+ wsCtl.send(JSON.stringify({ type: 'interrupt' }));
335
+ writeLine('已发送干预(中断)信号');
336
+ } catch (e) {
337
+ writeLine('发送干预失败: ' + e);
338
+ }
339
+ };
340
+
341
+ // Ctrl+Enter 提交输入或作为新任务
342
+ input.addEventListener('keydown', (e) => {
343
+ if (e.key === 'Enter' && e.ctrlKey) {
344
+ e.preventDefault();
345
+ const text = input.value || '';
346
+ if (pendingInputRequest) {
347
+ // 发送前在终端回显用户输入
348
+ try { writeLine('> ' + text); } catch (e) {}
349
+ ws.send(JSON.stringify({
350
+ type: 'user_input',
351
+ request_id: pendingInputRequest.request_id,
352
+ text
353
+ }));
354
+ tip.textContent = '输入已提交';
355
+ pendingInputRequest = null;
356
+ input.value = '';
357
+ // 提交输入后,直到下一次请求前禁用输入区
358
+ setInputEnabled(false);
359
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
360
+ } else {
361
+ // 仅在输入区启用时允许通过 Ctrl+Enter 发送新任务
362
+ if (inputEnabled) {
363
+ // 发送前在终端回显用户输入
364
+ try { writeLine('> ' + text); } catch (e) {}
365
+ ws.send(JSON.stringify({ type: 'run_task', text }));
366
+ input.value = '';
367
+ setInputEnabled(false);
368
+ try { btnSend.textContent = '发送为新任务'; } catch (e) {}
369
+ }
370
+ }
371
+ fitTerminal();
372
+ }
373
+ });
374
+ </script>
375
+ </body>
376
+ </html>
377
+ """
378
+
379
+ return app
380
+
381
+ # ---------------------------
382
+ # WebSocket 端点
383
+ # ---------------------------
384
+ async def _ws_sender_loop(ws: WebSocket, queue: "asyncio.Queue[Dict[str, Any]]") -> None:
385
+ try:
386
+ while True:
387
+ payload = await queue.get()
388
+ await ws.send_text(json.dumps(payload))
389
+ except Exception:
390
+ # 发送循环异常即退出
391
+ pass
392
+
393
+ def _make_sender(queue: "asyncio.Queue[Dict[str, Any]]") -> Callable[[Dict[str, Any]], None]:
394
+ # 同步函数,供 WebBridge 注册;将消息放入异步队列,由协程发送
395
+ def _sender(payload: Dict[str, Any]) -> None:
396
+ try:
397
+ queue.put_nowait(payload)
398
+ except Exception:
399
+ pass
400
+ return _sender
401
+
402
+ def _make_sender_filtered(queue: "asyncio.Queue[Dict[str, Any]]", allowed_types: Optional[list[str]] = None) -> Callable[[Dict[str, Any]], None]:
403
+ """
404
+ 过滤版 sender:仅将指定类型的payload放入队列(用于单独的STDIO通道)。
405
+ """
406
+ allowed = set(allowed_types or [])
407
+ def _sender(payload: Dict[str, Any]) -> None:
408
+ try:
409
+ ptype = payload.get("type")
410
+ if ptype in allowed:
411
+ queue.put_nowait(payload)
412
+ except Exception:
413
+ pass
414
+ return _sender
415
+
416
+ def _run_and_notify(agent: Any, text: str) -> None:
417
+ try:
418
+ agent.run(text)
419
+ finally:
420
+ try:
421
+ WebBridge.instance().broadcast({"type": "agent_idle"})
422
+ except Exception:
423
+ pass
424
+
425
+ def start_web_server(agent: Any, host: str = "127.0.0.1", port: int = 8765) -> None:
426
+ """
427
+ 启动Web服务,并将Agent绑定到应用上下文。
428
+ - agent: 现有的 Agent 实例(已完成初始化)
429
+ """
430
+ app = _build_app()
431
+ app.state.agent = agent # 供 WS 端点调用
432
+ # 兼容传入 Agent 或 AgentManager:
433
+ # - 若传入的是 AgentManager,则在每个任务开始前通过 initialize() 创建全新 Agent
434
+ # - 若传入的是 Agent 实例,则复用该 Agent(旧行为)
435
+ try:
436
+ app.state.agent_manager = agent if hasattr(agent, "initialize") else None
437
+ except Exception:
438
+ app.state.agent_manager = None
439
+
440
+ @app.websocket("/ws")
441
+ async def websocket_endpoint(ws: WebSocket) -> None:
442
+ await ws.accept()
443
+ queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
444
+ sender = _make_sender_filtered(queue, allowed_types=["input_request", "confirm_request", "agent_idle"])
445
+ bridge = WebBridge.instance()
446
+ bridge.add_client(sender)
447
+
448
+ # 后台发送任务
449
+ send_task = asyncio.create_task(_ws_sender_loop(ws, queue))
450
+
451
+ # 初始欢迎
452
+ try:
453
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "欢迎使用 Jarvis Web", "output_type": "INFO"}}))
454
+ except Exception:
455
+ pass
456
+
457
+ try:
458
+ while True:
459
+ msg = await ws.receive_text()
460
+ try:
461
+ data = json.loads(msg)
462
+ except Exception:
463
+ continue
464
+
465
+ mtype = data.get("type")
466
+ if mtype == "user_input":
467
+ req_id = data.get("request_id")
468
+ text = data.get("text", "") or ""
469
+ if isinstance(req_id, str):
470
+ bridge.post_user_input(req_id, str(text))
471
+ elif mtype == "confirm_response":
472
+ req_id = data.get("request_id")
473
+ val = bool(data.get("value", False))
474
+ if isinstance(req_id, str):
475
+ bridge.post_confirm(req_id, val)
476
+ elif mtype == "run_task":
477
+ # 允许空输入(空输入也具有语义)
478
+ text = data.get("text", "")
479
+ # 在后台线程运行,以避免阻塞事件循环
480
+ loop = asyncio.get_running_loop()
481
+ # 若提供了 AgentManager,则为每个任务创建新的 Agent 实例;否则复用现有 Agent
482
+ try:
483
+ if getattr(app.state, "agent_manager", None) and hasattr(app.state.agent_manager, "initialize"):
484
+ new_agent = app.state.agent_manager.initialize()
485
+ loop.run_in_executor(None, _run_and_notify, new_agent, text)
486
+ else:
487
+ loop.run_in_executor(None, _run_and_notify, app.state.agent, text)
488
+ except Exception:
489
+ # 回退到旧行为,避免因异常导致无法执行任务
490
+ try:
491
+ loop.run_in_executor(None, _run_and_notify, app.state.agent, text)
492
+ except Exception:
493
+ pass
494
+ else:
495
+ # 兼容未知消息类型
496
+ pass
497
+ except WebSocketDisconnect:
498
+ pass
499
+ except Exception:
500
+ pass
501
+ finally:
502
+ try:
503
+ bridge.remove_client(sender)
504
+ except Exception:
505
+ pass
506
+ try:
507
+ send_task.cancel()
508
+ except Exception:
509
+ pass
510
+
511
+ @app.websocket("/stdio")
512
+ async def websocket_stdio(ws: WebSocket) -> None:
513
+ await ws.accept()
514
+ queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
515
+ sender = _make_sender_filtered(queue, allowed_types=["stdio"])
516
+ bridge = WebBridge.instance()
517
+ bridge.add_client(sender)
518
+ send_task = asyncio.create_task(_ws_sender_loop(ws, queue))
519
+ try:
520
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "STDIO 通道已就绪", "output_type": "INFO"}}))
521
+ except Exception:
522
+ pass
523
+ try:
524
+ while True:
525
+ # 接收来自前端 STDIN 数据并注入到后端
526
+ msg = await ws.receive_text()
527
+ try:
528
+ data = json.loads(msg)
529
+ except Exception:
530
+ continue
531
+ mtype = data.get("type")
532
+ if mtype == "stdin":
533
+ try:
534
+ from jarvis.jarvis_agent.stdio_redirect import feed_web_stdin
535
+ text = data.get("data", "")
536
+ if isinstance(text, str) and text:
537
+ feed_web_stdin(text)
538
+ except Exception:
539
+ pass
540
+ else:
541
+ # 忽略未知类型
542
+ pass
543
+ except WebSocketDisconnect:
544
+ pass
545
+ except Exception:
546
+ pass
547
+ pass
548
+ finally:
549
+ try:
550
+ bridge.remove_client(sender)
551
+ except Exception:
552
+ pass
553
+ try:
554
+ send_task.cancel()
555
+ except Exception:
556
+ pass
557
+
558
+ @app.websocket("/control")
559
+ async def websocket_control(ws: WebSocket) -> None:
560
+ await ws.accept()
561
+ try:
562
+ while True:
563
+ msg = await ws.receive_text()
564
+ try:
565
+ data = json.loads(msg)
566
+ except Exception:
567
+ continue
568
+ mtype = data.get("type")
569
+ if mtype == "interrupt":
570
+ try:
571
+ set_interrupt(True)
572
+ # 可选:发送回执
573
+ await ws.send_text(json.dumps({"type": "ack", "cmd": "interrupt"}))
574
+ except Exception:
575
+ pass
576
+ elif mtype == "resize":
577
+ # 动态调整后端TTY宽度(影响 PrettyOutput 和基于终端宽度的逻辑)
578
+ try:
579
+ cols = int(data.get("cols") or 0)
580
+ rows = int(data.get("rows") or 0)
581
+ except Exception:
582
+ cols = 0
583
+ rows = 0
584
+ try:
585
+ if cols > 0:
586
+ os.environ["COLUMNS"] = str(cols)
587
+ try:
588
+ # 覆盖全局 rich Console 的宽度,便于 PrettyOutput 按照前端列数换行
589
+ console._width = cols # type: ignore[attr-defined]
590
+ except Exception:
591
+ pass
592
+ if rows > 0:
593
+ os.environ["LINES"] = str(rows)
594
+ except Exception:
595
+ pass
596
+ except WebSocketDisconnect:
597
+ pass
598
+ except Exception:
599
+ pass
600
+ finally:
601
+ try:
602
+ await ws.close()
603
+ except Exception:
604
+ pass
605
+
606
+ PrettyOutput.print(f"启动 Jarvis Web 服务: http://{host}:{port}", OutputType.SUCCESS)
607
+ # 在服务端进程内也写入并维护 PID 文件,增强可检测性与可清理性
608
+ try:
609
+ pidfile = Path(os.path.expanduser("~/.jarvis")) / f"jarvis_web_{port}.pid"
610
+ try:
611
+ pidfile.parent.mkdir(parents=True, exist_ok=True)
612
+ except Exception:
613
+ pass
614
+ try:
615
+ pidfile.write_text(str(os.getpid()), encoding="utf-8")
616
+ except Exception:
617
+ pass
618
+ # 退出时清理 PID 文件
619
+ def _cleanup_pidfile() -> None:
620
+ try:
621
+ pidfile.unlink(missing_ok=True) # type: ignore[call-arg]
622
+ except Exception:
623
+ pass
624
+ try:
625
+ atexit.register(_cleanup_pidfile)
626
+ except Exception:
627
+ pass
628
+ # 处理 SIGTERM/SIGINT,清理后退出
629
+ def _signal_handler(signum, frame): # type: ignore[no-untyped-def]
630
+ try:
631
+ _cleanup_pidfile()
632
+ finally:
633
+ try:
634
+ os._exit(0)
635
+ except Exception:
636
+ pass
637
+ try:
638
+ signal.signal(signal.SIGTERM, _signal_handler)
639
+ except Exception:
640
+ pass
641
+ try:
642
+ signal.signal(signal.SIGINT, _signal_handler)
643
+ except Exception:
644
+ pass
645
+ except Exception:
646
+ pass
647
+ uvicorn.run(app, host=host, port=port)
@@ -91,11 +91,6 @@ class CodeAgent:
91
91
  auto_complete=False,
92
92
  output_handler=[tool_registry, EditFileHandler()], # type: ignore
93
93
  model_group=model_group,
94
- input_handler=[
95
- shell_input_handler,
96
- file_context_handler,
97
- builtin_input_handler,
98
- ],
99
94
  need_summary=need_summary,
100
95
  use_methodology=False, # 禁用方法论
101
96
  use_analysis=False, # 禁用分析
@@ -23,7 +23,6 @@ def cli(
23
23
  non_interactive: bool = typer.Option(
24
24
  False, "-n", "--non-interactive", help="启用非交互模式:用户无法与命令交互,脚本执行超时限制为5分钟"
25
25
  ),
26
- ),
27
26
  ):
28
27
  """从YAML配置文件初始化并运行多智能体系统"""
29
28
  # CLI 标志:非交互模式(不依赖配置文件)
@@ -223,6 +223,7 @@ class BasePlatform(ABC):
223
223
  duration = end_time - start_time
224
224
  panel.subtitle = f"[bold green]✓ 对话完成耗时: {duration:.2f}秒[/bold green]"
225
225
  live.update(panel)
226
+ console.print()
226
227
  else:
227
228
  # Print a clear prefix line before streaming model output (non-pretty mode)
228
229
  console.print(
@@ -137,14 +137,12 @@ class SubAgentTool:
137
137
  # 基于父Agent(如有)继承部分配置后创建子Agent
138
138
  parent_agent = args.get("agent", None)
139
139
  parent_model_group = None
140
- parent_input_handler = None
141
140
  parent_execute_tool_confirm = None
142
141
  parent_multiline_inputer = None
143
142
  try:
144
143
  if parent_agent is not None:
145
144
  if getattr(parent_agent, "model", None):
146
145
  parent_model_group = getattr(parent_agent.model, "model_group", None)
147
- parent_input_handler = getattr(parent_agent, "input_handler", None)
148
146
  parent_execute_tool_confirm = getattr(parent_agent, "execute_tool_confirm", None)
149
147
  parent_multiline_inputer = getattr(parent_agent, "multiline_inputer", None)
150
148
  except Exception:
@@ -160,7 +158,6 @@ class SubAgentTool:
160
158
  auto_complete=auto_complete,
161
159
  output_handler=None,
162
160
  use_tools=None,
163
- input_handler=parent_input_handler,
164
161
  execute_tool_confirm=parent_execute_tool_confirm,
165
162
  need_summary=need_summary,
166
163
  multiline_inputer=parent_multiline_inputer,