257 lines
8.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Web UI 主要管理器
================
基於 FastAPI Web 用戶介面主要管理類參考 GUI 的設計模式重構
專為 SSH 遠端開發環境設計支援現代化界面和多語言
"""
import asyncio
import json
import logging
import os
import socket
import threading
import time
import webbrowser
from pathlib import Path
from typing import Dict, Optional
import uuid
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn
from .models import WebFeedbackSession, FeedbackResult
from .routes import setup_routes
from .utils import find_free_port, get_browser_opener
from ..debug import web_debug_log as debug_log
from ..i18n import get_i18n_manager
class WebUIManager:
"""Web UI 管理器"""
def __init__(self, host: str = "127.0.0.1", port: int = None):
self.host = host
# 優先使用固定端口 8765確保 localStorage 的一致性
self.port = port or find_free_port(preferred_port=8765)
self.app = FastAPI(title="MCP Feedback Enhanced")
self.sessions: Dict[str, WebFeedbackSession] = {}
self.server_thread = None
self.server_process = None
self.i18n = get_i18n_manager()
# 設置靜態文件和模板
self._setup_static_files()
self._setup_templates()
# 設置路由
setup_routes(self)
debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動")
def _setup_static_files(self):
"""設置靜態文件服務"""
# Web UI 靜態文件
web_static_path = Path(__file__).parent / "static"
if web_static_path.exists():
self.app.mount("/static", StaticFiles(directory=str(web_static_path)), name="static")
else:
raise RuntimeError(f"Static files directory not found: {web_static_path}")
def _setup_templates(self):
"""設置模板引擎"""
# Web UI 模板
web_templates_path = Path(__file__).parent / "templates"
if web_templates_path.exists():
self.templates = Jinja2Templates(directory=str(web_templates_path))
else:
raise RuntimeError(f"Templates directory not found: {web_templates_path}")
def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話"""
session_id = str(uuid.uuid4())
session = WebFeedbackSession(session_id, project_directory, summary)
self.sessions[session_id] = session
debug_log(f"創建回饋會話: {session_id}")
return session_id
def get_session(self, session_id: str) -> Optional[WebFeedbackSession]:
"""獲取回饋會話"""
return self.sessions.get(session_id)
def remove_session(self, session_id: str):
"""移除回饋會話"""
if session_id in self.sessions:
session = self.sessions[session_id]
session.cleanup()
del self.sessions[session_id]
debug_log(f"移除回饋會話: {session_id}")
def start_server(self):
"""啟動 Web 伺服器"""
def run_server_with_retry():
max_retries = 5
retry_count = 0
while retry_count < max_retries:
try:
debug_log(f"嘗試啟動伺服器在 {self.host}:{self.port} (嘗試 {retry_count + 1}/{max_retries})")
config = uvicorn.Config(
app=self.app,
host=self.host,
port=self.port,
log_level="warning",
access_log=False
)
server = uvicorn.Server(config)
asyncio.run(server.serve())
break
except OSError as e:
if e.errno == 10048: # Windows: 位址已在使用中
retry_count += 1
if retry_count < max_retries:
debug_log(f"端口 {self.port} 被占用,嘗試下一個端口")
self.port = find_free_port(self.port + 1)
else:
debug_log("已達到最大重試次數,無法啟動伺服器")
break
else:
debug_log(f"伺服器啟動錯誤: {e}")
break
except Exception as e:
debug_log(f"伺服器運行錯誤: {e}")
break
# 在新線程中啟動伺服器
self.server_thread = threading.Thread(target=run_server_with_retry, daemon=True)
self.server_thread.start()
# 等待伺服器啟動
time.sleep(2)
def open_browser(self, url: str):
"""開啟瀏覽器"""
try:
browser_opener = get_browser_opener()
browser_opener(url)
debug_log(f"已開啟瀏覽器:{url}")
except Exception as e:
debug_log(f"無法開啟瀏覽器: {e}")
def get_server_url(self) -> str:
"""獲取伺服器 URL"""
return f"http://{self.host}:{self.port}"
def stop(self):
"""停止 Web UI 服務"""
# 清理所有會話
for session in list(self.sessions.values()):
session.cleanup()
self.sessions.clear()
# 停止伺服器注意uvicorn 的 graceful shutdown 需要額外處理)
if self.server_thread and self.server_thread.is_alive():
debug_log("正在停止 Web UI 服務")
# 全域實例
_web_ui_manager: Optional[WebUIManager] = None
def get_web_ui_manager() -> WebUIManager:
"""獲取 Web UI 管理器實例"""
global _web_ui_manager
if _web_ui_manager is None:
_web_ui_manager = WebUIManager()
return _web_ui_manager
async def launch_web_feedback_ui(project_directory: str, summary: str, timeout: int = 600) -> dict:
"""
啟動 Web 回饋介面並等待用戶回饋
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
timeout: 超時時間
Returns:
dict: 回饋結果包含 logsinteractive_feedback images
"""
manager = get_web_ui_manager()
# 創建會話
session_id = manager.create_session(project_directory, summary)
session = manager.get_session(session_id)
if not session:
raise RuntimeError("無法創建回饋會話")
# 啟動伺服器(如果尚未啟動)
if not manager.server_thread or not manager.server_thread.is_alive():
manager.start_server()
# 構建完整 URL 並開啟瀏覽器
feedback_url = f"{manager.get_server_url()}/session/{session_id}"
manager.open_browser(feedback_url)
try:
# 等待用戶回饋,傳遞 timeout 參數
result = await session.wait_for_feedback(timeout)
debug_log(f"收到用戶回饋,會話: {session_id}")
return result
except TimeoutError:
debug_log(f"會話 {session_id} 超時")
# 資源已在 wait_for_feedback 中清理,這裡只需要記錄和重新拋出
raise
except Exception as e:
debug_log(f"會話 {session_id} 發生錯誤: {e}")
raise
finally:
# 清理會話(無論成功還是失敗)
manager.remove_session(session_id)
def stop_web_ui():
"""停止 Web UI 服務"""
global _web_ui_manager
if _web_ui_manager:
_web_ui_manager.stop()
_web_ui_manager = None
debug_log("Web UI 服務已停止")
# 測試用主函數
if __name__ == "__main__":
async def main():
try:
project_dir = os.getcwd()
summary = "這是一個測試摘要,用於驗證 Web UI 功能。"
print(f"啟動 Web UI 測試...")
print(f"專案目錄: {project_dir}")
print("等待用戶回饋...")
result = await launch_web_feedback_ui(project_dir, summary)
print("收到回饋結果:")
print(f"命令日誌: {result.get('logs', '')}")
print(f"互動回饋: {result.get('interactive_feedback', '')}")
print(f"圖片數量: {len(result.get('images', []))}")
except KeyboardInterrupt:
print("\n用戶取消操作")
except Exception as e:
print(f"錯誤: {e}")
finally:
stop_web_ui()
asyncio.run(main())