jarvis-ai-assistant 0.4.0__py3-none-any.whl → 0.4.1__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.
@@ -112,16 +112,6 @@ def _build_app() -> FastAPI:
112
112
  <body>
113
113
  <div id="terminal"></div>
114
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
115
  <!-- xterm.js 与 fit 插件 -->
126
116
  <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
127
117
  <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
@@ -145,7 +135,10 @@ def _build_app() -> FastAPI:
145
135
  try {
146
136
  term.onData((data) => {
147
137
  try {
148
- if (wsStd && wsStd.readyState === WebSocket.OPEN) {
138
+ // 优先将键入数据发送到交互式终端(PTY)通道;若未就绪则回退到 STDIN 重定向通道
139
+ if (typeof wsTerm !== 'undefined' && wsTerm && wsTerm.readyState === WebSocket.OPEN) {
140
+ wsTerm.send(JSON.stringify({ type: 'stdin', data }));
141
+ } else if (wsStd && wsStd.readyState === WebSocket.OPEN) {
149
142
  wsStd.send(JSON.stringify({ type: 'stdin', data }));
150
143
  }
151
144
  } catch (e) {}
@@ -161,6 +154,12 @@ def _build_app() -> FastAPI:
161
154
  wsCtl.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
162
155
  } catch (e) {}
163
156
  }
157
+ // 同步调整交互式终端(PTY)窗口大小
158
+ if (typeof wsTerm !== 'undefined' && wsTerm && wsTerm.readyState === WebSocket.OPEN) {
159
+ try {
160
+ wsTerm.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
161
+ } catch (e) {}
162
+ }
164
163
  } catch (e) {}
165
164
  }
166
165
  fitTerminal();
@@ -175,47 +174,14 @@ def _build_app() -> FastAPI:
175
174
  term.write((text ?? '').toString());
176
175
  }
177
176
 
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 通道
177
+ // WebSocket 通道:STDIO、控制与交互式终端
199
178
  const wsProto = (location.protocol === 'https:') ? 'wss' : 'ws';
200
- const ws = new WebSocket(wsProto + '://' + location.host + '/ws');
201
179
  const wsStd = new WebSocket(wsProto + '://' + location.host + '/stdio');
202
180
  const wsCtl = new WebSocket(wsProto + '://' + location.host + '/control');
181
+ // 交互式终端(PTY)通道:用于真正的交互式命令
182
+ const wsTerm = new WebSocket(wsProto + '://' + location.host + '/terminal');
203
183
  let ctlReady = false;
204
184
 
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
185
 
220
186
  wsStd.onopen = () => { writeLine('STDIO 通道已连接'); };
221
187
  wsStd.onclose = () => { writeLine('STDIO 通道已关闭'); };
@@ -233,48 +199,31 @@ def _build_app() -> FastAPI:
233
199
  ctlReady = false;
234
200
  };
235
201
  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) => {
202
+
203
+ // 终端(PTY)通道
204
+ wsTerm.onopen = () => {
205
+ writeLine('终端通道已连接');
206
+ // 初次连接时上报当前终端尺寸
207
+ try {
208
+ wsTerm.send(JSON.stringify({ type: 'resize', cols: term.cols || 200, rows: term.rows || 24 }));
209
+ } catch (e) {}
210
+ };
211
+ wsTerm.onclose = () => { writeLine('终端通道已关闭'); };
212
+ wsTerm.onerror = (e) => { writeLine('终端通道错误: ' + e); };
213
+ wsTerm.onmessage = (evt) => {
242
214
  try {
243
215
  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 通道重复显示
216
+ if (data.type === 'stdio') {
217
+ const text = data.text || '';
218
+ write(text);
272
219
  }
273
220
  } catch (e) {
274
- writeLine('消息解析失败: ' + e);
221
+ writeLine('消息解析失败: ' + e);
275
222
  }
276
223
  };
277
224
 
225
+
226
+
278
227
  // STDIO 通道消息(原样写入,保留流式体验)
279
228
  wsStd.onmessage = (evt) => {
280
229
  try {
@@ -288,89 +237,8 @@ writeLine('消息解析失败: ' + e);
288
237
  }
289
238
  };
290
239
 
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
240
 
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
241
 
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
242
  </script>
375
243
  </body>
376
244
  </html>
@@ -437,77 +305,6 @@ def start_web_server(agent: Any, host: str = "127.0.0.1", port: int = 8765) -> N
437
305
  except Exception:
438
306
  app.state.agent_manager = None
439
307
 
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
308
  @app.websocket("/stdio")
512
309
  async def websocket_stdio(ws: WebSocket) -> None:
513
310
  await ws.accept()
@@ -603,6 +400,307 @@ def start_web_server(agent: Any, host: str = "127.0.0.1", port: int = 8765) -> N
603
400
  except Exception:
604
401
  pass
605
402
 
403
+ # 交互式终端通道:为前端 xterm 提供真实的 PTY 会话,以支持交互式命令
404
+ @app.websocket("/terminal")
405
+ async def websocket_terminal(ws: WebSocket) -> None:
406
+ await ws.accept()
407
+ # 仅在非 Windows 平台提供 PTY 功能
408
+ import sys as _sys
409
+ if _sys.platform == "win32":
410
+ try:
411
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "当前平台不支持交互式终端(PTY)", "output_type": "ERROR"}}))
412
+ except Exception:
413
+ pass
414
+ try:
415
+ await ws.close()
416
+ except Exception:
417
+ pass
418
+ return
419
+
420
+ import os as _os
421
+ try:
422
+ import pty as _pty # type: ignore
423
+ import fcntl as _fcntl # type: ignore
424
+ import select as _select # type: ignore
425
+ import termios as _termios # type: ignore
426
+ import struct as _struct # type: ignore
427
+ except Exception:
428
+ try:
429
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "服务端缺少 PTY 相关依赖,无法启动交互式终端", "output_type": "ERROR"}}))
430
+ except Exception:
431
+ pass
432
+ try:
433
+ await ws.close()
434
+ except Exception:
435
+ pass
436
+ return
437
+
438
+ def _set_winsize(fd: int, cols: int, rows: int) -> None:
439
+ try:
440
+ if cols > 0 and rows > 0:
441
+ winsz = _struct.pack("HHHH", rows, cols, 0, 0)
442
+ _fcntl.ioctl(fd, _termios.TIOCSWINSZ, winsz)
443
+ except Exception:
444
+ # 调整失败不影响主流程
445
+ pass
446
+
447
+ # 交互式会话状态与启动函数(优先执行 jvs 命令,失败回退到系统 shell)
448
+ session = {"pid": None, "master_fd": None}
449
+ last_cols = 0
450
+ last_rows = 0
451
+ # 会话结束后等待用户按回车再重启
452
+ waiting_for_ack = False
453
+ ack_event = asyncio.Event()
454
+
455
+
456
+ def _spawn_jvs_session() -> bool:
457
+ nonlocal session
458
+ try:
459
+ pid, master_fd = _pty.fork()
460
+ if pid == 0:
461
+ # 子进程:执行 jvs 启动命令(移除 web 相关参数),失败时回退到系统 shell
462
+ try:
463
+ import json as _json
464
+ _cmd_json = _os.environ.get("JARVIS_WEB_LAUNCH_JSON", "")
465
+ if _cmd_json:
466
+ try:
467
+ _argv = _json.loads(_cmd_json)
468
+ except Exception:
469
+ _argv = []
470
+ if isinstance(_argv, list) and len(_argv) > 0 and isinstance(_argv[0], str):
471
+ _os.execvp(_argv[0], _argv)
472
+ except Exception:
473
+ pass
474
+ # 若未配置或执行失败,回退到 /bin/bash 或 /bin/sh
475
+ try:
476
+ _os.execvp("/bin/bash", ["/bin/bash"])
477
+ except Exception:
478
+ try:
479
+ _os.execvp("/bin/sh", ["/bin/sh"])
480
+ except Exception:
481
+ _os._exit(1)
482
+ else:
483
+ # 父进程:设置非阻塞模式并记录状态
484
+ try:
485
+ _fcntl.fcntl(master_fd, _fcntl.F_SETFL, _os.O_NONBLOCK)
486
+ except Exception:
487
+ pass
488
+ session["pid"] = pid
489
+ session["master_fd"] = master_fd
490
+ # 如果已有窗口大小设置,应用到新会话
491
+ try:
492
+ if last_cols > 0 and last_rows > 0:
493
+ winsz = _struct.pack("HHHH", last_rows, last_cols, 0, 0)
494
+ _fcntl.ioctl(master_fd, _termios.TIOCSWINSZ, winsz)
495
+ except Exception:
496
+ pass
497
+ return True
498
+ except Exception:
499
+ return False
500
+ return False
501
+
502
+ # 启动首个会话
503
+ ok = _spawn_jvs_session()
504
+ if not ok:
505
+ try:
506
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "启动交互式终端失败", "output_type": "ERROR"}}))
507
+ except Exception:
508
+ pass
509
+ try:
510
+ await ws.close()
511
+ except Exception:
512
+ pass
513
+ return
514
+
515
+ async def _tty_read_loop() -> None:
516
+ nonlocal waiting_for_ack
517
+ try:
518
+ while True:
519
+ fd = session.get("master_fd")
520
+ if fd is None:
521
+ # 若正在等待用户按回车确认,则暂不重启
522
+ if waiting_for_ack:
523
+ if ack_event.is_set():
524
+ try:
525
+ ack_event.clear()
526
+ except Exception:
527
+ pass
528
+ waiting_for_ack = False
529
+ if _spawn_jvs_session():
530
+ try:
531
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\njvs 会话已重启\r\n"}))
532
+ except Exception:
533
+ pass
534
+ fd = session.get("master_fd")
535
+ else:
536
+ await asyncio.sleep(0.5)
537
+ continue
538
+ # 等待用户按回车
539
+ await asyncio.sleep(0.1)
540
+ continue
541
+ # 非确认流程:自动重启
542
+ if _spawn_jvs_session():
543
+ try:
544
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\njvs 会话已重启\r\n"}))
545
+ except Exception:
546
+ pass
547
+ fd = session.get("master_fd")
548
+ else:
549
+ await asyncio.sleep(0.5)
550
+ continue
551
+ try:
552
+ r, _, _ = _select.select([fd], [], [], 0.1)
553
+ except Exception:
554
+ r = []
555
+ if r:
556
+ try:
557
+ data = _os.read(fd, 4096)
558
+ except BlockingIOError:
559
+ data = b""
560
+ except Exception:
561
+ data = b""
562
+ if data:
563
+ try:
564
+ await ws.send_text(json.dumps({"type": "stdio", "text": data.decode(errors="ignore")}))
565
+ except Exception:
566
+ break
567
+ else:
568
+ # 读取到 EOF,说明子进程已退出;提示后等待用户按回车再重启
569
+ try:
570
+ # 关闭旧 master
571
+ try:
572
+ if session.get("master_fd") is not None:
573
+ _os.close(session["master_fd"]) # type: ignore[index]
574
+ except Exception:
575
+ pass
576
+ session["master_fd"] = None
577
+ session["pid"] = None
578
+ # 标记等待用户回车,并提示
579
+ waiting_for_ack = True
580
+ try:
581
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\nAgent 已结束。按回车继续,系统将重启新的 Agent。\r\n> "}))
582
+ except Exception:
583
+ pass
584
+ # 不立即重启,等待顶部 fd None 分支在收到回车后处理
585
+ await asyncio.sleep(0.1)
586
+ except Exception:
587
+ pass
588
+ # 让出事件循环
589
+ try:
590
+ await asyncio.sleep(0)
591
+ except Exception:
592
+ pass
593
+ except Exception:
594
+ pass
595
+
596
+ # 后台读取任务
597
+ read_task = asyncio.create_task(_tty_read_loop())
598
+
599
+ # 初次连接:尝试根据控制通道设定的列数调整终端大小
600
+ try:
601
+ cols = int(_os.environ.get("COLUMNS", "0"))
602
+ except Exception:
603
+ cols = 0
604
+ try:
605
+ rows = int(_os.environ.get("LINES", "0"))
606
+ except Exception:
607
+ rows = 0
608
+ try:
609
+ if cols > 0 and rows > 0:
610
+ _set_winsize(session["master_fd"], cols, rows)
611
+ last_cols = cols
612
+ last_rows = rows
613
+ except Exception:
614
+ pass
615
+ # 发送就绪提示
616
+ try:
617
+ await ws.send_text(json.dumps({"type": "output", "payload": {"text": "交互式终端已就绪(PTY)", "output_type": "INFO"}}))
618
+ except Exception:
619
+ pass
620
+
621
+ try:
622
+ while True:
623
+ msg = await ws.receive_text()
624
+ try:
625
+ data = json.loads(msg)
626
+ except Exception:
627
+ continue
628
+ mtype = data.get("type")
629
+ if mtype == "stdin":
630
+ # 前端键入数据:若等待回车,则捕获回车;否则透传到 PTY
631
+ try:
632
+ text = data.get("data", "")
633
+ if isinstance(text, str) and text:
634
+ if waiting_for_ack:
635
+ # Enter 键触发继续
636
+ if "\r" in text or "\n" in text:
637
+ try:
638
+ ack_event.set()
639
+ except Exception:
640
+ pass
641
+ else:
642
+ # 非回车输入时轻提示
643
+ try:
644
+ await ws.send_text(json.dumps({"type": "stdio", "text": "\r\n按回车继续。\r\n> "}))
645
+ except Exception:
646
+ pass
647
+ else:
648
+ # 原样写入(保留控制字符);前端可按需发送回车
649
+ _os.write(session.get("master_fd") or -1, text.encode(errors="ignore"))
650
+ except Exception:
651
+ pass
652
+ elif mtype == "resize":
653
+ # 终端窗口大小调整(与控制通道一致,但作用于 PTY)
654
+ try:
655
+ cols = int(data.get("cols") or 0)
656
+ except Exception:
657
+ cols = 0
658
+ try:
659
+ rows = int(data.get("rows") or 0)
660
+ except Exception:
661
+ rows = 0
662
+ try:
663
+ if cols > 0 and rows > 0:
664
+ _set_winsize(session.get("master_fd") or -1, cols, rows)
665
+ last_cols = cols
666
+ last_rows = rows
667
+ except Exception:
668
+ pass
669
+ else:
670
+ # 忽略未知类型
671
+ pass
672
+ except WebSocketDisconnect:
673
+ pass
674
+ except Exception:
675
+ pass
676
+ finally:
677
+ # 清理资源
678
+ try:
679
+ read_task.cancel()
680
+ except Exception:
681
+ pass
682
+ try:
683
+ if session.get("master_fd") is not None:
684
+ try:
685
+ _os.close(session["master_fd"]) # type: ignore[index]
686
+ except Exception:
687
+ pass
688
+ except Exception:
689
+ pass
690
+ try:
691
+ if session.get("pid"):
692
+ import signal as _signal # type: ignore
693
+ try:
694
+ _os.kill(session["pid"], _signal.SIGTERM) # type: ignore[index]
695
+ except Exception:
696
+ pass
697
+ except Exception:
698
+ pass
699
+ try:
700
+ await ws.close()
701
+ except Exception:
702
+ pass
703
+
606
704
  PrettyOutput.print(f"启动 Jarvis Web 服务: http://{host}:{port}", OutputType.SUCCESS)
607
705
  # 在服务端进程内也写入并维护 PID 文件,增强可检测性与可清理性
608
706
  try: