🐛 fixes #5 實際 MCP 的 timeout 參數會影響到 GUI 及 Web UI 自動關閉

This commit is contained in:
Minidoracat 2025-06-03 22:26:38 +08:00
parent e7283b3610
commit 5d09c3309a
10 changed files with 295 additions and 70 deletions

View File

@ -20,6 +20,6 @@
重構: 模塊化設計
"""
from .main import feedback_ui
from .main import feedback_ui, feedback_ui_with_timeout
__all__ = ['feedback_ui']
__all__ = ['feedback_ui', 'feedback_ui_with_timeout']

View File

@ -7,9 +7,12 @@ GUI 主要入口點
提供 GUI 回饋介面的主要入口點函數
"""
import threading
import time
from typing import Optional
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtGui import QFont
from PySide6.QtCore import QTimer
import sys
from .models import FeedbackResult
@ -51,4 +54,69 @@ def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult
app.exec()
# 返回結果
return window.result
return window.result
def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) -> Optional[FeedbackResult]:
"""
啟動帶超時的回饋收集 GUI 介面
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
timeout: 超時時間
Returns:
Optional[FeedbackResult]: 回饋結果如果用戶取消或超時則返回 None
Raises:
TimeoutError: 當超時時拋出
"""
# 檢查是否已有 QApplication 實例
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 設定全域微軟正黑體字體
font = QFont("Microsoft JhengHei", 11) # 微軟正黑體11pt
app.setFont(font)
# 設定字體回退順序,確保中文字體正確顯示
app.setStyleSheet("""
* {
font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif;
}
""")
# 創建主窗口
window = FeedbackWindow(project_directory, summary)
window.show()
# 創建超時計時器
timeout_timer = QTimer()
timeout_timer.setSingleShot(True)
timeout_timer.timeout.connect(lambda: _handle_timeout(window, app))
timeout_timer.start(timeout * 1000) # 轉換為毫秒
# 運行事件循環直到窗口關閉
app.exec()
# 停止計時器(如果還在運行)
timeout_timer.stop()
# 檢查是否超時
if hasattr(window, '_timeout_occurred'):
raise TimeoutError(f"回饋收集超時({timeout}GUI 介面已自動關閉")
# 返回結果
return window.result
def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None:
"""處理超時事件"""
# 標記超時發生
window._timeout_occurred = True
# 強制關閉視窗
window.force_close()
# 退出應用程式
app.quit()

View File

@ -447,17 +447,16 @@ class FeedbackWindow(QMainWindow):
self.close()
def _cancel_feedback(self) -> None:
"""取消回饋"""
reply = QMessageBox.question(
self, t('app.confirmCancel'),
t('app.confirmCancelMessage'),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.result = None
self.close()
"""取消回饋收集"""
debug_log("取消回饋收集")
self.result = ""
self.close()
def force_close(self) -> None:
"""強制關閉視窗(用於超時處理)"""
debug_log("強制關閉視窗(超時)")
self.result = ""
self.close()
def _refresh_ui_texts(self) -> None:
"""刷新界面文字"""

View File

@ -356,45 +356,37 @@ def process_images(images_data: List[dict]) -> List[MCPImage]:
return mcp_images
def launch_gui(project_dir: str, summary: str) -> dict:
async def launch_gui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict:
"""
啟動 Qt GUI 收集回饋
Args:
project_dir: 專案目錄路徑
summary: AI 工作摘要
Returns:
dict: 收集到的回饋資料
啟動 GUI 模式並處理超時
"""
debug_log("啟動 Qt GUI 介面")
debug_log(f"啟動 GUI 模式(超時:{timeout}秒)")
try:
from .gui import feedback_ui
result = feedback_ui(project_dir, summary)
from .gui import feedback_ui_with_timeout
if result is None:
# 用戶取消
# 直接調用帶超時的 GUI 函數
result = feedback_ui_with_timeout(project_dir, summary, timeout)
if result:
return {
"command_logs": "",
"interactive_feedback": "用戶取消了回饋。",
"logs": f"GUI 模式回饋收集完成",
"interactive_feedback": result.get("interactive_feedback", ""),
"images": result.get("images", [])
}
else:
return {
"logs": "用戶取消了回饋收集",
"interactive_feedback": "",
"images": []
}
# 轉換鍵名以保持向後兼容
return {
"command_logs": result.get("command_logs", ""),
"interactive_feedback": result.get("interactive_feedback", ""),
"images": result.get("images", [])
}
except ImportError as e:
debug_log(f"無法導入 GUI 模組: {e}")
return {
"command_logs": "",
"interactive_feedback": f"Qt GUI 模組導入失敗: {str(e)}",
"images": []
}
except TimeoutError as e:
# 超時異常 - 這是預期的行為
raise e
except Exception as e:
debug_log(f"GUI 啟動失败: {e}")
raise Exception(f"GUI 啟動失败: {e}")
# ===== MCP 工具定義 =====
@ -462,7 +454,7 @@ async def interactive_feedback(
if use_web_ui:
result = await launch_web_ui_with_timeout(project_directory, summary, timeout)
else:
result = launch_gui(project_directory, summary)
result = await launch_gui_with_timeout(project_directory, summary, timeout)
# 處理取消情況
if not result:
@ -517,8 +509,8 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
# 使用新的 web 模組
from .web import launch_web_feedback_ui
# 直接運行 Web UI 會話
return await launch_web_feedback_ui(project_dir, summary)
# 傳遞 timeout 參數給 Web UI
return await launch_web_feedback_ui(project_dir, summary, timeout)
except ImportError as e:
debug_log(f"無法導入 Web UI 模組: {e}")
return {
@ -526,6 +518,13 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
"interactive_feedback": f"Web UI 模組導入失敗: {str(e)}",
"images": []
}
except TimeoutError as e:
debug_log(f"Web UI 超時: {e}")
return {
"command_logs": "",
"interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。",
"images": []
}
except Exception as e:
error_msg = f"Web UI 錯誤: {e}"
debug_log(f"{error_msg}")

View File

@ -147,6 +147,12 @@
"upload": "Upload",
"download": "Download"
},
"session": {
"timeout": "⏰ Session has timed out, interface will close automatically",
"timeoutWarning": "Session is about to timeout",
"timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.",
"closing": "Closing..."
},
"dynamic": {
"aiSummary": "Test Web UI Functionality\n\n🎯 **Test Items:**\n- Web UI server startup and operation\n- WebSocket real-time communication\n- Feedback submission functionality\n- Image upload and preview\n- Command execution functionality\n- Smart Ctrl+V image pasting\n- Multi-language interface functionality\n\n📋 **Test Steps:**\n1. Test image upload (drag-drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart pasting\n3. Try switching languages (Traditional Chinese/Simplified Chinese/English)\n4. Test command execution functionality\n5. Submit feedback and images\n\nPlease test these features and provide feedback!",
"terminalWelcome": "Welcome to Interactive Feedback Terminal\n========================================\nProject Directory: {sessionId}\nEnter commands and press Enter or click Execute button\nSupported commands: ls, dir, pwd, cat, type, etc.\n\n$ "

View File

@ -147,6 +147,12 @@
"upload": "上传",
"download": "下载"
},
"session": {
"timeout": "⏰ 会话已超时,界面将自动关闭",
"timeoutWarning": "会话即将超时",
"timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。",
"closing": "正在关闭..."
},
"dynamic": {
"aiSummary": "测试 Web UI 功能\n\n🎯 **功能测试项目:**\n- Web UI 服务器启动和运行\n- WebSocket 实时通讯\n- 反馈提交功能\n- 图片上传和预览\n- 命令执行功能\n- 智能 Ctrl+V 图片粘贴\n- 多语言界面功能\n\n📋 **测试步骤:**\n1. 测试图片上传(拖拽、选择文件、剪贴板)\n2. 在文本框内按 Ctrl+V 测试智能粘贴\n3. 尝试切换语言(繁中/简中/英文)\n4. 测试命令执行功能\n5. 提交反馈和图片\n\n请测试这些功能并提供反馈",
"terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ "

View File

@ -147,6 +147,12 @@
"upload": "上傳",
"download": "下載"
},
"session": {
"timeout": "⏰ 會話已超時,介面將自動關閉",
"timeoutWarning": "會話即將超時",
"timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。",
"closing": "正在關閉..."
},
"dynamic": {
"aiSummary": "測試 Web UI 功能\n\n🎯 **功能測試項目:**\n- Web UI 服務器啟動和運行\n- WebSocket 即時通訊\n- 回饋提交功能\n- 圖片上傳和預覽\n- 命令執行功能\n- 智能 Ctrl+V 圖片貼上\n- 多語言介面功能\n\n📋 **測試步驟:**\n1. 測試圖片上傳(拖拽、選擇檔案、剪貼簿)\n2. 在文字框內按 Ctrl+V 測試智能貼上\n3. 嘗試切換語言(繁中/簡中/英文)\n4. 測試命令執行功能\n5. 提交回饋和圖片\n\n請測試這些功能並提供回饋",
"terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ "

View File

@ -174,13 +174,14 @@ def get_web_ui_manager() -> WebUIManager:
return _web_ui_manager
async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
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
@ -203,12 +204,19 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
manager.open_browser(feedback_url)
try:
# 等待用戶回饋
result = await session.wait_for_feedback()
# 等待用戶回饋,傳遞 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)

View File

@ -37,13 +37,14 @@ class WebFeedbackSession:
self.feedback_completed = threading.Event()
self.process: Optional[subprocess.Popen] = None
self.command_logs = []
self._cleanup_done = False # 防止重複清理
# 確保臨時目錄存在
TEMP_DIR.mkdir(parents=True, exist_ok=True)
async def wait_for_feedback(self, timeout: int = 600) -> dict:
"""
等待用戶回饋包含圖片
等待用戶回饋包含圖片支援超時自動清理
Args:
timeout: 超時時間
@ -51,21 +52,40 @@ class WebFeedbackSession:
Returns:
dict: 回饋結果
"""
loop = asyncio.get_event_loop()
def wait_in_thread():
return self.feedback_completed.wait(timeout)
completed = await loop.run_in_executor(None, wait_in_thread)
if completed:
return {
"logs": "\n".join(self.command_logs),
"interactive_feedback": self.feedback_result or "",
"images": self.images
}
else:
raise TimeoutError("等待用戶回饋超時")
try:
# 使用比 MCP 超時稍短的時間(提前處理,避免邊界競爭)
# 對於短超時(<30秒提前1秒對於長超時提前5秒
if timeout <= 30:
actual_timeout = max(timeout - 1, 5) # 短超時提前1秒最少5秒
else:
actual_timeout = timeout - 5 # 長超時提前5秒
debug_log(f"會話 {self.session_id} 開始等待回饋,超時時間: {actual_timeout} 秒(原始: {timeout} 秒)")
loop = asyncio.get_event_loop()
def wait_in_thread():
return self.feedback_completed.wait(actual_timeout)
completed = await loop.run_in_executor(None, wait_in_thread)
if completed:
debug_log(f"會話 {self.session_id} 收到用戶回饋")
return {
"logs": "\n".join(self.command_logs),
"interactive_feedback": self.feedback_result or "",
"images": self.images
}
else:
# 超時了,立即清理資源
debug_log(f"會話 {self.session_id}{actual_timeout} 秒後超時,開始清理資源...")
await self._cleanup_resources_on_timeout()
raise TimeoutError(f"等待用戶回饋超時({actual_timeout}秒),介面已自動關閉")
except Exception as e:
# 任何異常都要確保清理資源
debug_log(f"會話 {self.session_id} 發生異常: {e}")
await self._cleanup_resources_on_timeout()
raise
async def submit_feedback(self, feedback: str, images: List[dict]):
"""
@ -224,8 +244,66 @@ class WebFeedbackSession:
except:
pass
async def _cleanup_resources_on_timeout(self):
"""超時時清理所有資源"""
if self._cleanup_done:
return # 避免重複清理
self._cleanup_done = True
debug_log(f"開始清理會話 {self.session_id} 的資源...")
try:
# 1. 關閉 WebSocket 連接
if self.websocket:
try:
# 先通知前端超時
await self.websocket.send_json({
"type": "session_timeout",
"message": "會話已超時,介面將自動關閉"
})
await asyncio.sleep(0.1) # 給前端一點時間處理消息
await self.websocket.close()
debug_log(f"會話 {self.session_id} WebSocket 已關閉")
except Exception as e:
debug_log(f"關閉 WebSocket 時發生錯誤: {e}")
finally:
self.websocket = None
# 2. 終止正在運行的命令進程
if self.process:
try:
self.process.terminate()
try:
self.process.wait(timeout=3)
debug_log(f"會話 {self.session_id} 命令進程已正常終止")
except subprocess.TimeoutExpired:
self.process.kill()
debug_log(f"會話 {self.session_id} 命令進程已強制終止")
except Exception as e:
debug_log(f"終止命令進程時發生錯誤: {e}")
finally:
self.process = None
# 3. 設置完成事件(防止其他地方還在等待)
self.feedback_completed.set()
# 4. 清理臨時數據
self.command_logs.clear()
self.images.clear()
debug_log(f"會話 {self.session_id} 資源清理完成")
except Exception as e:
debug_log(f"清理會話 {self.session_id} 資源時發生錯誤: {e}")
def cleanup(self):
"""清理會話資源"""
"""同步清理會話資源(保持向後兼容)"""
if self._cleanup_done:
return
self._cleanup_done = True
debug_log(f"同步清理會話 {self.session_id} 資源...")
if self.process:
try:
self.process.terminate()
@ -235,4 +313,7 @@ class WebFeedbackSession:
self.process.kill()
except:
pass
self.process = None
self.process = None
# 設置完成事件
self.feedback_completed.set()

View File

@ -242,6 +242,10 @@ class FeedbackApp {
// 顯示成功訊息
this.showSuccessMessage();
break;
case 'session_timeout':
console.log('會話超時:', data.message);
this.handleSessionTimeout(data.message);
break;
default:
console.log('未知的 WebSocket 消息:', data);
}
@ -254,6 +258,54 @@ class FeedbackApp {
this.showMessage(successMessage, 'success');
}
handleSessionTimeout(message) {
console.log('處理會話超時:', message);
// 顯示超時訊息
const timeoutMessage = message || (window.i18nManager ?
window.i18nManager.t('session.timeout', '⏰ 會話已超時,介面將自動關閉') :
'⏰ 會話已超時,介面將自動關閉');
this.showMessage(timeoutMessage, 'warning');
// 禁用所有互動元素
this.disableAllInputs();
// 3秒後自動關閉頁面
setTimeout(() => {
try {
window.close();
} catch (e) {
// 如果無法關閉視窗(可能因為安全限制),重新載入頁面
console.log('無法關閉視窗,重新載入頁面');
window.location.reload();
}
}, 3000);
}
disableAllInputs() {
// 禁用所有輸入元素
const inputs = document.querySelectorAll('input, textarea, button');
inputs.forEach(input => {
input.disabled = true;
input.style.opacity = '0.5';
});
// 特別處理提交和取消按鈕
const submitBtn = document.getElementById('submitBtn');
const cancelBtn = document.getElementById('cancelBtn');
if (submitBtn) {
submitBtn.textContent = '⏰ 已超時';
submitBtn.disabled = true;
}
if (cancelBtn) {
cancelBtn.textContent = '關閉中...';
cancelBtn.disabled = true;
}
}
updateConnectionStatus(connected) {
// 更新連接狀態指示器
const elements = document.querySelectorAll('.connection-indicator');