From c5a0521411ca5c3bd7de2cca10c46c8f3a8e7f63 Mon Sep 17 00:00:00 2001 From: Minidoracat Date: Sat, 31 May 2025 08:55:53 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E5=A2=9E=E5=BC=B7=20Web=20UI=20?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=20WebSocket=20?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E9=87=8D=E9=80=A3=E6=A9=9F=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E5=96=84=E7=94=A8=E6=88=B6=E5=9B=9E=E9=A5=8B=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=B5=81=E7=A8=8B=EF=BC=8C=E4=B8=A6=E5=84=AA=E5=8C=96?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E8=A8=AD=E8=A8=88=E8=88=87=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=AB=94=E9=A9=97=E3=80=82=E9=87=8D=E6=A7=8B=E4=BB=A3=E7=A2=BC?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E5=A4=9A=E9=80=B2=E7=A8=8B=E7=8D=A8?= =?UTF-8?q?=E7=AB=8B=E7=AE=A1=E7=90=86=E5=99=A8=EF=BC=8C=E4=B8=A6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=80=9A=E7=9F=A5=E7=B3=BB=E7=B5=B1=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E7=94=A8=E6=88=B6=E5=8F=8D=E9=A5=8B=E3=80=82=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=91=BD=E4=BB=A4=E5=9F=B7=E8=A1=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=B7=E9=8C=AF=E8=AA=A4=E8=99=95=E7=90=86?= =?UTF-8?q?=E8=88=87=E7=8B=80=E6=85=8B=E6=8F=90=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../templates/feedback.html | 493 ++++++++++++++---- src/mcp_feedback_enhanced/web_ui.py | 244 +++++++-- 3 files changed, 593 insertions(+), 145 deletions(-) diff --git a/.gitignore b/.gitignore index 6f1b1d9..955d1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ venv*/ .DS_Store .cursor/rules/ +uv.lock \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/templates/feedback.html b/src/mcp_feedback_enhanced/templates/feedback.html index 2bbcaad..c68d4ec 100644 --- a/src/mcp_feedback_enhanced/templates/feedback.html +++ b/src/mcp_feedback_enhanced/templates/feedback.html @@ -720,63 +720,202 @@ \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web_ui.py b/src/mcp_feedback_enhanced/web_ui.py index 5b64f97..34c36ca 100644 --- a/src/mcp_feedback_enhanced/web_ui.py +++ b/src/mcp_feedback_enhanced/web_ui.py @@ -12,21 +12,23 @@ 增強功能: 圖片支援和現代化界面設計 """ -import os -import sys -import json -import uuid import asyncio -import webbrowser -import threading +import json +import logging +import os +import socket import subprocess -import psutil +import sys +import threading import time +import webbrowser +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +import uuid +from datetime import datetime import base64 import tempfile from typing import Dict, Optional, List -from pathlib import Path - from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, UploadFile, File, Form from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, JSONResponse @@ -230,14 +232,32 @@ class WebFeedbackSession: class WebUIManager: """Web UI 管理器""" - def __init__(self, host: str = "127.0.0.1", port: int = 8765): + def __init__(self, host: str = "127.0.0.1", port: int = None): self.host = host - self.port = port + self.port = port or self._find_free_port() self.app = FastAPI(title="Interactive Feedback MCP Web UI") self.sessions: Dict[str, WebFeedbackSession] = {} self.server_thread: Optional[threading.Thread] = None self.setup_routes() + def _find_free_port(self, start_port: int = 8765, max_attempts: int = 100) -> int: + """尋找可用的端口""" + for port in range(start_port, start_port + max_attempts): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((self.host, port)) + debug_log(f"找到可用端口: {port}") + return port + except OSError: + continue + + # 如果沒有找到可用端口,使用系統分配 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((self.host, 0)) + port = s.getsockname()[1] + debug_log(f"使用系統分配端口: {port}") + return port + def setup_routes(self): """設置路由""" @@ -346,20 +366,45 @@ class WebUIManager: def start_server(self): """啟動伺服器""" - def run_server(): - uvicorn.run( - self.app, - host=self.host, - port=self.port, - log_level="error", - access_log=False - ) + max_retries = 10 + retry_count = 0 + + def run_server_with_retry(): + nonlocal retry_count + while retry_count < max_retries: + try: + debug_log(f"嘗試在端口 {self.port} 啟動伺服器(第 {retry_count + 1} 次嘗試)") + uvicorn.run( + self.app, + host=self.host, + port=self.port, + log_level="error", + access_log=False + ) + break # 成功啟動,跳出循環 + except OSError as e: + if "10048" in str(e) or "Address already in use" in str(e): + retry_count += 1 + debug_log(f"端口 {self.port} 被占用,尋找新端口(第 {retry_count} 次重試)") + if retry_count < max_retries: + # 尋找新的可用端口 + self.port = self._find_free_port(self.port + 1) + debug_log(f"切換到新端口: {self.port}") + else: + debug_log(f"已達到最大重試次數 {max_retries},無法啟動伺服器") + raise Exception(f"無法找到可用端口,已嘗試 {max_retries} 次") + else: + debug_log(f"伺服器啟動失敗: {e}") + raise e + except Exception as e: + debug_log(f"伺服器啟動時發生未預期錯誤: {e}") + raise e - self.server_thread = threading.Thread(target=run_server, daemon=True) + self.server_thread = threading.Thread(target=run_server_with_retry, daemon=True) self.server_thread.start() - # 等待伺服器啟動 - time.sleep(2) + # 等待伺服器啟動,並給足夠時間處理重試 + time.sleep(3) def open_browser(self, url: str): """開啟瀏覽器""" @@ -395,8 +440,13 @@ class WebUIManager: @@ -410,19 +460,113 @@ class WebUIManager:

您的回饋:

- + + @@ -430,15 +574,21 @@ class WebUIManager: # ===== 全域管理器 ===== -_web_ui_manager: Optional[WebUIManager] = None +_web_ui_managers: Dict[int, WebUIManager] = {} def get_web_ui_manager() -> WebUIManager: - """獲取全域 Web UI 管理器""" - global _web_ui_manager - if _web_ui_manager is None: - _web_ui_manager = WebUIManager() - _web_ui_manager.start_server() - return _web_ui_manager + """獲取 Web UI 管理器 - 每個進程獲得獨立的實例""" + process_id = os.getpid() + + global _web_ui_managers + if process_id not in _web_ui_managers: + # 為每個進程創建獨立的管理器,使用不同的端口 + manager = WebUIManager() + manager.start_server() + _web_ui_managers[process_id] = manager + debug_log(f"為進程 {process_id} 創建新的 Web UI 管理器,端口: {manager.port}") + + return _web_ui_managers[process_id] async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: """啟動 Web 回饋 UI 並等待回饋""" @@ -482,12 +632,14 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: def stop_web_ui(): """停止 Web UI""" - global _web_ui_manager - if _web_ui_manager: + global _web_ui_managers + if _web_ui_managers: # 清理所有會話 - for session_id in list(_web_ui_manager.sessions.keys()): - _web_ui_manager.remove_session(session_id) - _web_ui_manager = None + for process_id, manager in list(_web_ui_managers.items()): + for session_id in list(manager.sessions.keys()): + manager.remove_session(session_id) + manager.sessions.clear() + _web_ui_managers.pop(process_id) # ===== 主程式入口 =====