2025-06-03 06:50:19 +08:00
|
|
|
|
#!/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
|
2025-06-03 15:09:08 +08:00
|
|
|
|
# 優先使用固定端口 8765,確保 localStorage 的一致性
|
|
|
|
|
self.port = port or find_free_port(preferred_port=8765)
|
2025-06-03 20:59:45 +08:00
|
|
|
|
self.app = FastAPI(title="MCP Feedback Enhanced")
|
2025-06-03 06:50:19 +08:00
|
|
|
|
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")
|
2025-06-03 15:09:08 +08:00
|
|
|
|
else:
|
|
|
|
|
raise RuntimeError(f"Static files directory not found: {web_static_path}")
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
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:
|
2025-06-03 15:09:08 +08:00
|
|
|
|
raise RuntimeError(f"Templates directory not found: {web_templates_path}")
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2025-06-03 22:26:38 +08:00
|
|
|
|
async def launch_web_feedback_ui(project_directory: str, summary: str, timeout: int = 600) -> dict:
|
2025-06-03 06:50:19 +08:00
|
|
|
|
"""
|
|
|
|
|
啟動 Web 回饋介面並等待用戶回饋
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
project_directory: 專案目錄路徑
|
|
|
|
|
summary: AI 工作摘要
|
2025-06-03 22:26:38 +08:00
|
|
|
|
timeout: 超時時間(秒)
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: 回饋結果,包含 logs、interactive_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:
|
2025-06-03 22:26:38 +08:00
|
|
|
|
# 等待用戶回饋,傳遞 timeout 參數
|
|
|
|
|
result = await session.wait_for_feedback(timeout)
|
2025-06-03 06:50:19 +08:00
|
|
|
|
debug_log(f"收到用戶回饋,會話: {session_id}")
|
|
|
|
|
return result
|
2025-06-03 22:26:38 +08:00
|
|
|
|
except TimeoutError:
|
|
|
|
|
debug_log(f"會話 {session_id} 超時")
|
|
|
|
|
# 資源已在 wait_for_feedback 中清理,這裡只需要記錄和重新拋出
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
debug_log(f"會話 {session_id} 發生錯誤: {e}")
|
|
|
|
|
raise
|
2025-06-03 06:50:19 +08:00
|
|
|
|
finally:
|
2025-06-03 22:26:38 +08:00
|
|
|
|
# 清理會話(無論成功還是失敗)
|
2025-06-03 06:50:19 +08:00
|
|
|
|
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())
|