mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-26 10:02:25 +08:00
🐛 修復 WebSocket 狀態檢測導入錯誤 (#78)
This commit is contained in:
parent
de6838c79c
commit
5f9eb6a42e
@ -68,8 +68,8 @@ class DesktopApp:
|
||||
self.web_manager.start_server()
|
||||
|
||||
# 等待服務器啟動
|
||||
max_wait = 10 # 最多等待 10 秒
|
||||
wait_count = 0
|
||||
max_wait = 10.0 # 最多等待 10 秒
|
||||
wait_count = 0.0
|
||||
while wait_count < max_wait:
|
||||
if (
|
||||
self.web_manager.server_thread
|
||||
@ -221,7 +221,8 @@ class DesktopApp:
|
||||
# Windows 下隱藏控制台視窗
|
||||
creation_flags = 0
|
||||
if os.name == "nt":
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW
|
||||
# CREATE_NO_WINDOW 只在 Windows 上存在
|
||||
creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
|
||||
|
||||
self.app_handle = subprocess.Popen(
|
||||
[str(tauri_exe)],
|
||||
|
@ -331,7 +331,9 @@ class WebUIManager:
|
||||
|
||||
# 如果有舊會話,處理狀態轉換和清理
|
||||
if old_session:
|
||||
debug_log(f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}")
|
||||
debug_log(
|
||||
f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}"
|
||||
)
|
||||
|
||||
# 保存標籤頁狀態到全局
|
||||
if hasattr(old_session, "active_tabs"):
|
||||
@ -339,14 +341,18 @@ class WebUIManager:
|
||||
|
||||
# 如果舊會話是已提交狀態,進入下一步(已完成)
|
||||
if old_session.status == SessionStatus.FEEDBACK_SUBMITTED:
|
||||
debug_log(f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成")
|
||||
debug_log(
|
||||
f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成"
|
||||
)
|
||||
success = old_session.next_step("反饋已處理,會話完成")
|
||||
if success:
|
||||
debug_log(f"✅ 舊會話 {old_session.session_id} 成功進入已完成狀態")
|
||||
else:
|
||||
debug_log(f"❌ 舊會話 {old_session.session_id} 無法進入下一步")
|
||||
else:
|
||||
debug_log(f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換")
|
||||
debug_log(
|
||||
f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換"
|
||||
)
|
||||
|
||||
# 確保舊會話仍在字典中(用於API獲取)
|
||||
if old_session.session_id in self.sessions:
|
||||
@ -689,10 +695,6 @@ class WebUIManager:
|
||||
else:
|
||||
debug_log("沒有活躍的桌面應用程式實例")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async def _safe_close_websocket(self, websocket):
|
||||
"""安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用"""
|
||||
if not websocket:
|
||||
@ -736,8 +738,8 @@ class WebUIManager:
|
||||
"session_id": self.current_session.session_id,
|
||||
"project_directory": self.current_session.project_directory,
|
||||
"summary": self.current_session.summary,
|
||||
"status": self.current_session.status.value
|
||||
}
|
||||
"status": self.current_session.status.value,
|
||||
},
|
||||
}
|
||||
|
||||
# 發送刷新通知
|
||||
@ -754,26 +756,61 @@ class WebUIManager:
|
||||
return False
|
||||
|
||||
async def _check_active_tabs(self) -> bool:
|
||||
"""檢查是否有活躍標籤頁 - 檢查所有會話的WebSocket連接"""
|
||||
"""檢查是否有活躍標籤頁 - 使用分層檢測機制"""
|
||||
try:
|
||||
# 檢查當前會話的WebSocket連接
|
||||
if self.current_session and self.current_session.websocket:
|
||||
debug_log("檢測到當前會話的WebSocket連接")
|
||||
# 快速檢測層:檢查 WebSocket 物件是否存在
|
||||
if not self.current_session or not self.current_session.websocket:
|
||||
debug_log("快速檢測:沒有當前會話或 WebSocket 連接")
|
||||
return False
|
||||
|
||||
# 檢查心跳(如果有心跳記錄)
|
||||
last_heartbeat = getattr(self.current_session, "last_heartbeat", None)
|
||||
if last_heartbeat:
|
||||
heartbeat_age = time.time() - last_heartbeat
|
||||
if heartbeat_age > 10: # 超過 10 秒沒有心跳
|
||||
debug_log(f"快速檢測:心跳超時 ({heartbeat_age:.1f}秒)")
|
||||
# 可能連接已死,需要進一步檢測
|
||||
else:
|
||||
debug_log(f"快速檢測:心跳正常 ({heartbeat_age:.1f}秒前)")
|
||||
return True # 心跳正常,認為連接活躍
|
||||
|
||||
# 準確檢測層:實際測試連接是否活著
|
||||
try:
|
||||
# 檢查 WebSocket 連接狀態
|
||||
websocket = self.current_session.websocket
|
||||
|
||||
# 檢查連接是否已關閉
|
||||
if hasattr(websocket, "client_state"):
|
||||
try:
|
||||
# 嘗試從 starlette 導入(FastAPI 基於 Starlette)
|
||||
import starlette.websockets # type: ignore[import-not-found]
|
||||
|
||||
if hasattr(starlette.websockets, "WebSocketState"):
|
||||
WebSocketState = starlette.websockets.WebSocketState
|
||||
if websocket.client_state != WebSocketState.CONNECTED:
|
||||
debug_log(
|
||||
f"準確檢測:WebSocket 狀態不是 CONNECTED,而是 {websocket.client_state}"
|
||||
)
|
||||
# 清理死連接
|
||||
self.current_session.websocket = None
|
||||
return False
|
||||
except ImportError:
|
||||
# 如果導入失敗,使用替代方法
|
||||
debug_log("無法導入 WebSocketState,使用替代方法檢測連接")
|
||||
# 跳過狀態檢查,直接測試連接
|
||||
|
||||
# 如果連接看起來是活的,嘗試發送 ping(非阻塞)
|
||||
# 注意:FastAPI WebSocket 沒有內建的 ping 方法,這裡使用自定義消息
|
||||
await websocket.send_json({"type": "ping", "timestamp": time.time()})
|
||||
debug_log("準確檢測:成功發送 ping 消息,連接是活躍的")
|
||||
return True
|
||||
|
||||
# 檢查其他會話的WebSocket連接
|
||||
active_websockets = 0
|
||||
for session_id, session in self.sessions.items():
|
||||
if session.websocket:
|
||||
active_websockets += 1
|
||||
debug_log(f"檢測到會話 {session_id} 的WebSocket連接")
|
||||
|
||||
if active_websockets > 0:
|
||||
debug_log(f"檢測到 {active_websockets} 個活躍的WebSocket連接")
|
||||
return True
|
||||
|
||||
debug_log("沒有檢測到任何活躍的WebSocket連接")
|
||||
return False
|
||||
except Exception as e:
|
||||
debug_log(f"準確檢測:連接測試失敗 - {e}")
|
||||
# 連接已死,清理它
|
||||
if self.current_session:
|
||||
self.current_session.websocket = None
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"檢查活躍連接時發生錯誤:{e}")
|
||||
|
@ -32,9 +32,11 @@ class SessionStatus(Enum):
|
||||
"""會話狀態枚舉 - 單向流轉設計"""
|
||||
|
||||
WAITING = "waiting" # 等待中
|
||||
ACTIVE = "active" # 活躍狀態
|
||||
FEEDBACK_SUBMITTED = "feedback_submitted" # 已提交反饋
|
||||
COMPLETED = "completed" # 已完成
|
||||
ERROR = "error" # 錯誤(終態)
|
||||
TIMEOUT = "timeout" # 超時(終態)
|
||||
EXPIRED = "expired" # 已過期(終態)
|
||||
|
||||
|
||||
@ -140,6 +142,7 @@ class WebFeedbackSession:
|
||||
# 統一使用 time.time() 以避免時間基準不一致
|
||||
self.created_at = time.time()
|
||||
self.last_activity = self.created_at
|
||||
self.last_heartbeat = None # 記錄最後一次心跳時間
|
||||
|
||||
# 新增:自動清理配置
|
||||
self.auto_cleanup_delay = auto_cleanup_delay # 自動清理延遲時間(秒)
|
||||
@ -179,17 +182,21 @@ class WebFeedbackSession:
|
||||
|
||||
# 定義狀態流轉路徑
|
||||
next_status_map = {
|
||||
SessionStatus.WAITING: SessionStatus.FEEDBACK_SUBMITTED,
|
||||
SessionStatus.WAITING: SessionStatus.ACTIVE,
|
||||
SessionStatus.ACTIVE: SessionStatus.FEEDBACK_SUBMITTED,
|
||||
SessionStatus.FEEDBACK_SUBMITTED: SessionStatus.COMPLETED,
|
||||
SessionStatus.COMPLETED: None, # 終態
|
||||
SessionStatus.ERROR: None, # 終態
|
||||
SessionStatus.EXPIRED: None # 終態
|
||||
SessionStatus.ERROR: None, # 終態
|
||||
SessionStatus.TIMEOUT: None, # 終態
|
||||
SessionStatus.EXPIRED: None, # 終態
|
||||
}
|
||||
|
||||
next_status = next_status_map.get(self.status)
|
||||
|
||||
if next_status is None:
|
||||
debug_log(f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步")
|
||||
debug_log(
|
||||
f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步"
|
||||
)
|
||||
return False
|
||||
|
||||
# 執行狀態轉換
|
||||
@ -199,8 +206,9 @@ class WebFeedbackSession:
|
||||
else:
|
||||
# 默認消息
|
||||
default_messages = {
|
||||
SessionStatus.ACTIVE: "會話已啟動",
|
||||
SessionStatus.FEEDBACK_SUBMITTED: "用戶已提交反饋",
|
||||
SessionStatus.COMPLETED: "會話已完成"
|
||||
SessionStatus.COMPLETED: "會話已完成",
|
||||
}
|
||||
self.status_message = default_messages.get(next_status, "狀態已更新")
|
||||
|
||||
@ -245,7 +253,12 @@ class WebFeedbackSession:
|
||||
|
||||
def is_terminal(self) -> bool:
|
||||
"""檢查是否處於終態"""
|
||||
return self.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.EXPIRED]
|
||||
return self.status in [
|
||||
SessionStatus.COMPLETED,
|
||||
SessionStatus.ERROR,
|
||||
SessionStatus.TIMEOUT,
|
||||
SessionStatus.EXPIRED,
|
||||
]
|
||||
|
||||
def get_status_info(self) -> dict[str, Any]:
|
||||
"""獲取會話狀態信息"""
|
||||
@ -508,11 +521,13 @@ class WebFeedbackSession:
|
||||
"content": message_data.get("content", ""),
|
||||
"images": message_data.get("images", []),
|
||||
"submission_method": message_data.get("submission_method", "manual"),
|
||||
"type": "feedback"
|
||||
"type": "feedback",
|
||||
}
|
||||
|
||||
self.user_messages.append(user_message)
|
||||
debug_log(f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}")
|
||||
debug_log(
|
||||
f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}"
|
||||
)
|
||||
|
||||
def _process_images(self, images: list[dict]) -> list[dict]:
|
||||
"""
|
||||
|
@ -176,7 +176,7 @@ def setup_routes(manager: "WebUIManager"):
|
||||
"feedback_completed": session.feedback_completed.is_set(),
|
||||
"has_websocket": session.websocket is not None,
|
||||
"is_current": session == manager.current_session,
|
||||
"user_messages": session.user_messages # 包含用戶消息記錄
|
||||
"user_messages": session.user_messages, # 包含用戶消息記錄
|
||||
}
|
||||
sessions_data.append(session_info)
|
||||
|
||||
@ -189,8 +189,7 @@ def setup_routes(manager: "WebUIManager"):
|
||||
except Exception as e:
|
||||
debug_log(f"獲取所有會話狀態失敗: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": f"獲取會話狀態失敗: {e!s}"}
|
||||
status_code=500, content={"error": f"獲取會話狀態失敗: {e!s}"}
|
||||
)
|
||||
|
||||
@manager.app.post("/api/add-user-message")
|
||||
@ -201,22 +200,20 @@ def setup_routes(manager: "WebUIManager"):
|
||||
current_session = manager.get_current_session()
|
||||
|
||||
if not current_session:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"error": "沒有活躍會話"}
|
||||
)
|
||||
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
|
||||
|
||||
# 添加用戶消息到會話
|
||||
current_session.add_user_message(data)
|
||||
|
||||
debug_log(f"用戶消息已添加到會話 {current_session.session_id}")
|
||||
return JSONResponse(content={"status": "success", "message": "用戶消息已記錄"})
|
||||
return JSONResponse(
|
||||
content={"status": "success", "message": "用戶消息已記錄"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
debug_log(f"添加用戶消息失敗: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": f"添加用戶消息失敗: {e!s}"}
|
||||
status_code=500, content={"error": f"添加用戶消息失敗: {e!s}"}
|
||||
)
|
||||
|
||||
@manager.app.websocket("/ws")
|
||||
@ -556,6 +553,10 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
|
||||
|
||||
elif message_type == "heartbeat":
|
||||
# WebSocket 心跳處理(簡化版)
|
||||
# 更新心跳時間
|
||||
session.last_heartbeat = time.time()
|
||||
session.last_activity = time.time()
|
||||
|
||||
# 發送心跳回應
|
||||
if session.websocket:
|
||||
try:
|
||||
@ -575,6 +576,11 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
|
||||
await session._cleanup_resources_on_timeout()
|
||||
# 重構:不再自動停止服務器,保持服務器運行以支援持久性
|
||||
|
||||
elif message_type == "pong":
|
||||
# 處理來自前端的 pong 回應(用於連接檢測)
|
||||
debug_log(f"收到 pong 回應,時間戳: {data.get('timestamp', 'N/A')}")
|
||||
# 可以在這裡記錄延遲或更新連接狀態
|
||||
|
||||
else:
|
||||
debug_log(f"未知的消息類型: {message_type}")
|
||||
|
||||
|
@ -704,7 +704,7 @@
|
||||
|
||||
// 檢查是否是新會話創建的通知
|
||||
if (data.action === 'new_session_created' || data.type === 'new_session_created') {
|
||||
console.log('🆕 檢測到新會話創建,完全刷新頁面以確保狀態同步');
|
||||
console.log('🆕 檢測到新會話創建,局部更新頁面內容');
|
||||
|
||||
// 播放音效通知
|
||||
if (this.audioManager) {
|
||||
@ -713,28 +713,40 @@
|
||||
|
||||
// 顯示新會話通知
|
||||
window.MCPFeedback.Utils.showMessage(
|
||||
data.message || '新的 MCP 會話已創建,正在打開新窗口...',
|
||||
data.message || '新的 MCP 會話已創建,正在更新內容...',
|
||||
window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS
|
||||
);
|
||||
|
||||
// 使用 window.open 打開新窗口並關閉當前窗口
|
||||
// 局部更新頁面內容而非開啟新視窗
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
console.log('🔄 使用 window.open 打開新窗口');
|
||||
console.log('🔄 執行局部更新頁面內容');
|
||||
|
||||
// 打開新窗口
|
||||
const newWindow = window.open(window.location.href, '_blank');
|
||||
|
||||
if (newWindow) {
|
||||
console.log('✅ 新窗口打開成功,關閉當前窗口');
|
||||
// 短暫延遲後關閉當前窗口
|
||||
setTimeout(function() {
|
||||
window.close();
|
||||
}, 500);
|
||||
} else {
|
||||
console.warn('❌ window.open 被阻止,回退到頁面刷新');
|
||||
window.location.reload();
|
||||
// 1. 更新會話資訊
|
||||
if (data.session_info) {
|
||||
self.currentSessionId = data.session_info.session_id;
|
||||
console.log('📋 新會話 ID:', self.currentSessionId);
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
// 2. 刷新頁面內容(AI 摘要、表單等)
|
||||
self.refreshPageContent();
|
||||
|
||||
// 3. 重置表單狀態
|
||||
self.clearFeedback();
|
||||
|
||||
// 4. 重置回饋狀態為等待中
|
||||
if (self.uiManager) {
|
||||
self.uiManager.setFeedbackState(
|
||||
window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING,
|
||||
self.currentSessionId
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 檢查並啟動自動提交
|
||||
self.checkAndStartAutoSubmit();
|
||||
|
||||
console.log('✅ 局部更新完成,頁面已準備好接收新的回饋');
|
||||
}, 500);
|
||||
|
||||
return; // 提前返回,不執行後續的局部更新邏輯
|
||||
}
|
||||
|
@ -269,6 +269,14 @@
|
||||
this.connectionMonitor.recordPong();
|
||||
}
|
||||
break;
|
||||
case 'ping':
|
||||
// 處理來自伺服器的 ping 消息(用於連接檢測)
|
||||
console.log('收到伺服器 ping,立即回應 pong');
|
||||
this.send({
|
||||
type: 'pong',
|
||||
timestamp: data.timestamp
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// 其他訊息類型由外部處理
|
||||
break;
|
||||
|
@ -182,8 +182,8 @@ class TestWebFeedbackSessionCleanup:
|
||||
"""測試狀態更新重置定時器"""
|
||||
old_timer = self.session.cleanup_timer
|
||||
|
||||
# 更新狀態為活躍
|
||||
self.session.update_status(SessionStatus.ACTIVE, "測試活躍狀態")
|
||||
# 更新狀態為活躍 - 使用 next_step 方法
|
||||
self.session.next_step("測試活躍狀態")
|
||||
|
||||
# 檢查定時器是否被重置
|
||||
assert self.session.cleanup_timer != old_timer
|
||||
|
@ -108,11 +108,16 @@ class TestWebFeedbackSession:
|
||||
# 測試初始狀態
|
||||
assert session.status == SessionStatus.WAITING
|
||||
|
||||
# 測試狀態更新
|
||||
session.update_status(SessionStatus.FEEDBACK_SUBMITTED, "已提交回饋")
|
||||
# 測試狀態更新 - 使用 next_step 方法
|
||||
# 首先進入 ACTIVE 狀態
|
||||
result = session.next_step("會話已激活")
|
||||
assert result is True
|
||||
assert session.status == SessionStatus.ACTIVE
|
||||
# 然後進入 FEEDBACK_SUBMITTED 狀態
|
||||
result = session.next_step("已提交回饋") # type: ignore[unreachable]
|
||||
assert result is True
|
||||
assert session.status == SessionStatus.FEEDBACK_SUBMITTED
|
||||
# 修復 unreachable 錯誤 - 使用 type: ignore 註解
|
||||
assert session.status_message == "已提交回饋" # type: ignore[unreachable]
|
||||
assert session.status_message == "已提交回饋"
|
||||
|
||||
def test_session_age_and_idle_time(self, test_project_dir):
|
||||
"""測試會話年齡和空閒時間"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user