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) # ===== 主程式入口 =====