diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 49970ac..6c15a70 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -585,7 +585,10 @@ class WebUIManager: if has_active_tabs: debug_log("檢測到活躍標籤頁,不開啟新瀏覽器視窗") - debug_log(f"用戶可以在現有標籤頁中查看更新:{url}") + debug_log(f"向現有標籤頁發送刷新通知:{url}") + + # 向現有標籤頁發送刷新通知 + await self.notify_existing_tab_to_refresh() return True # 沒有活躍標籤頁,開啟新瀏覽器視窗 @@ -799,40 +802,46 @@ class WebUIManager: except Exception as e: debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}") - async def _check_active_tabs(self) -> bool: - """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API""" + async def notify_existing_tab_to_refresh(self): + """通知現有標籤頁刷新顯示新會話內容""" try: - # 首先檢查全局標籤頁狀態 - global_count = self.get_global_active_tabs_count() - if global_count > 0: - debug_log(f"檢測到 {global_count} 個全局活躍標籤頁") + if not self.current_session or not self.current_session.websocket: + debug_log("沒有活躍的WebSocket連接,無法發送刷新通知") + return + + # 構建刷新通知消息 + refresh_message = { + "type": "session_updated", + "action": "new_session_created", + "message": "新的 MCP 會話已創建,頁面將自動刷新", + "session_info": { + "session_id": self.current_session.session_id, + "project_directory": self.current_session.project_directory, + "summary": self.current_session.summary, + "status": self.current_session.status + } + } + + # 發送刷新通知 + await self.current_session.websocket.send_json(refresh_message) + debug_log(f"已向現有標籤頁發送刷新通知: {self.current_session.session_id}") + + except Exception as e: + debug_log(f"發送刷新通知失敗: {e}") + + async def _check_active_tabs(self) -> bool: + """檢查是否有活躍標籤頁 - 直接檢查WebSocket連接""" + try: + # 直接檢查是否有活躍的WebSocket連接 + if self.current_session and self.current_session.websocket: + debug_log("檢測到活躍的WebSocket連接") return True - # 如果全局狀態沒有活躍標籤頁,嘗試通過 API 檢查 - # 等待一小段時間讓服務器完全啟動 - await asyncio.sleep(0.5) - - # 調用活躍標籤頁 API - import aiohttp - - timeout = aiohttp.ClientTimeout(total=2) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get( - f"{self.get_server_url()}/api/active-tabs" - ) as response: - if response.status == 200: - data = await response.json() - tab_count = data.get("count", 0) - debug_log(f"API 檢測到 {tab_count} 個活躍標籤頁") - return bool(tab_count > 0) - debug_log(f"檢查活躍標籤頁失敗,狀態碼:{response.status}") - return False - - except TimeoutError: - debug_log("檢查活躍標籤頁超時") + debug_log("沒有檢測到活躍的WebSocket連接") return False + except Exception as e: - debug_log(f"檢查活躍標籤頁時發生錯誤:{e}") + debug_log(f"檢查活躍連接時發生錯誤:{e}") return False def get_server_url(self) -> str: diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 76aa23b..4b29406 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -684,6 +684,30 @@ FeedbackApp.prototype._originalHandleSessionUpdated = function(data) { console.log('🔄 處理會話更新:', data.session_info); + // 檢查是否是新會話創建的通知 + if (data.action === 'new_session_created') { + console.log('🆕 檢測到新會話創建,準備刷新頁面顯示新內容'); + + // 播放音效通知 + if (this.audioManager) { + this.audioManager.playNotification(); + } + + // 顯示新會話通知 + window.MCPFeedback.Utils.showMessage( + data.message || '新的 MCP 會話已創建,正在刷新頁面...', + window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS + ); + + // 延遲一小段時間讓用戶看到通知,然後刷新頁面 + setTimeout(function() { + console.log('🔄 刷新頁面以顯示新會話內容'); + window.location.reload(); + }, 1500); + + return; // 提前返回,不執行後續的局部更新邏輯 + } + // 播放音效通知 if (this.audioManager) { this.audioManager.playNotification(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js index 1bca3f1..a40ad4a 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js @@ -258,6 +258,30 @@ self.showSessionDetails(); }); } + + // 复制当前会话内容按钮 + const copySessionButton = DOMUtils ? + DOMUtils.safeQuerySelector('#copyCurrentSessionContent') : + document.querySelector('#copyCurrentSessionContent'); + if (copySessionButton) { + copySessionButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + self.copyCurrentSessionContent(); + }); + } + + // 复制当前用户内容按钮 + const copyUserButton = DOMUtils ? + DOMUtils.safeQuerySelector('#copyCurrentUserContent') : + document.querySelector('#copyCurrentUserContent'); + if (copyUserButton) { + copyUserButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + self.copyCurrentUserContent(); + }); + } }; /** @@ -648,6 +672,242 @@ } }; + /** + * 复制当前会话内容 + */ + SessionManager.prototype.copyCurrentSessionContent = function() { + console.log('📋 复制当前会话内容...'); + + try { + const currentSession = this.dataManager.getCurrentSession(); + if (!currentSession) { + this.showMessage('没有当前会话数据', 'error'); + return; + } + + const content = this.formatCurrentSessionContent(currentSession); + this.copyToClipboard(content, '当前会话内容已复制到剪贴板'); + } catch (error) { + console.error('复制当前会话内容失败:', error); + this.showMessage('复制失败,请重试', 'error'); + } + }; + + /** + * 复制当前用户发送的内容 + */ + SessionManager.prototype.copyCurrentUserContent = function() { + console.log('📝 复制当前用户发送的内容...'); + console.log('📝 this.dataManager 存在吗?', !!this.dataManager); + + try { + if (!this.dataManager) { + console.log('📝 dataManager 不存在,尝试其他方式获取数据'); + this.showMessage('数据管理器未初始化', 'error'); + return; + } + + const currentSession = this.dataManager.getCurrentSession(); + console.log('📝 当前会话数据:', currentSession); + + if (!currentSession) { + console.log('📝 没有当前会话数据'); + this.showMessage('当前会话没有数据', 'warning'); + return; + } + + console.log('📝 用户消息数组:', currentSession.user_messages); + console.log('📝 用户消息数组长度:', currentSession.user_messages ? currentSession.user_messages.length : 'undefined'); + + if (!currentSession.user_messages || currentSession.user_messages.length === 0) { + console.log('📝 没有用户消息记录'); + this.showMessage('当前会话没有用户消息记录', 'warning'); + return; + } + + // 在这里也添加调试信息 + console.log('📝 准备格式化用户消息,数量:', currentSession.user_messages.length); + console.log('📝 第一条消息内容:', currentSession.user_messages[0]); + + const content = this.formatCurrentUserContent(currentSession.user_messages); + console.log('📝 格式化后的内容长度:', content.length); + console.log('📝 格式化后的内容预览:', content.substring(0, 200)); + + this.copyToClipboard(content, '当前用户内容已复制到剪贴板'); + } catch (error) { + console.error('📝 复制当前用户内容失败:', error); + console.error('📝 错误堆栈:', error.stack); + this.showMessage('复制失败,请重试', 'error'); + } + }; + + /** + * 格式化当前会话内容 + */ + SessionManager.prototype.formatCurrentSessionContent = function(sessionData) { + const lines = []; + lines.push('# MCP Feedback Enhanced - 当前会话内容'); + lines.push(''); + lines.push(`**会话ID**: ${sessionData.session_id || 'N/A'}`); + lines.push(`**项目目录**: ${sessionData.project_directory || 'N/A'}`); + lines.push(`**摘要**: ${sessionData.summary || 'N/A'}`); + lines.push(`**状态**: ${sessionData.status || 'N/A'}`); + lines.push(`**创建时间**: ${sessionData.created_at || 'N/A'}`); + lines.push(`**更新时间**: ${sessionData.updated_at || 'N/A'}`); + lines.push(''); + + if (sessionData.user_messages && sessionData.user_messages.length > 0) { + lines.push('## 用户消息'); + sessionData.user_messages.forEach((msg, index) => { + lines.push(`### 消息 ${index + 1}`); + lines.push(msg); + lines.push(''); + }); + } + + if (sessionData.ai_responses && sessionData.ai_responses.length > 0) { + lines.push('## AI 响应'); + sessionData.ai_responses.forEach((response, index) => { + lines.push(`### 响应 ${index + 1}`); + lines.push(response); + lines.push(''); + }); + } + + return lines.join('\n'); + }; + + /** + * 格式化当前用户内容 + */ + SessionManager.prototype.formatCurrentUserContent = function(userMessages) { + const lines = []; + lines.push('# MCP Feedback Enhanced - 用户发送内容'); + lines.push(''); + + userMessages.forEach((msg, index) => { + lines.push(`## 消息 ${index + 1}`); + + // 调试:输出完整的消息对象 + console.log(`📝 消息 ${index + 1} 完整对象:`, msg); + console.log(`📝 消息 ${index + 1} 所有属性:`, Object.keys(msg)); + + // 添加时间戳信息 - 简化版本,直接使用当前时间 + let timeStr = '未知时间'; + + // 检查是否有时间戳字段 + if (msg.timestamp) { + // 如果时间戳看起来不正常(太小),直接使用当前时间 + if (msg.timestamp < 1000000000) { // 小于2001年的时间戳,可能是相对时间 + timeStr = new Date().toLocaleString('zh-CN'); + console.log('📝 时间戳异常,使用当前时间:', msg.timestamp); + } else { + // 正常处理时间戳 + let timestamp = msg.timestamp; + if (timestamp > 1e12) { + // 毫秒时间戳 + timeStr = new Date(timestamp).toLocaleString('zh-CN'); + } else { + // 秒时间戳 + timeStr = new Date(timestamp * 1000).toLocaleString('zh-CN'); + } + } + } else { + // 没有时间戳,使用当前时间 + timeStr = new Date().toLocaleString('zh-CN'); + console.log('📝 没有时间戳字段,使用当前时间'); + } + + lines.push(`**时间**: ${timeStr}`); + + // 添加提交方式 + if (msg.submission_method) { + const methodText = msg.submission_method === 'auto' ? '自动提交' : '手动提交'; + lines.push(`**提交方式**: ${methodText}`); + } + + // 处理消息内容 + if (msg.content !== undefined) { + // 完整记录模式 - 显示实际内容 + lines.push(`**内容**: ${msg.content}`); + + // 如果有图片,显示图片数量 + if (msg.images && msg.images.length > 0) { + lines.push(`**图片数量**: ${msg.images.length}`); + } + } else if (msg.content_length !== undefined) { + // 基本统计模式 - 显示统计信息 + lines.push(`**内容长度**: ${msg.content_length} 字符`); + lines.push(`**图片数量**: ${msg.image_count || 0}`); + lines.push(`**有内容**: ${msg.has_content ? '是' : '否'}`); + } else if (msg.privacy_note) { + // 隐私保护模式 + lines.push(`**内容**: [内容记录已停用 - 隐私设置]`); + } else { + // 兜底情况 - 尝试显示对象的JSON格式 + lines.push(`**原始数据**: ${JSON.stringify(msg, null, 2)}`); + } + + lines.push(''); + }); + + return lines.join('\n'); + }; + + /** + * 复制到剪贴板 + */ + SessionManager.prototype.copyToClipboard = function(text, successMessage) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(() => { + this.showMessage(successMessage, 'success'); + }).catch(err => { + console.error('复制到剪贴板失败:', err); + this.fallbackCopyToClipboard(text, successMessage); + }); + } else { + this.fallbackCopyToClipboard(text, successMessage); + } + }; + + /** + * 降级复制方法 + */ + SessionManager.prototype.fallbackCopyToClipboard = function(text, successMessage) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + this.showMessage(successMessage, 'success'); + } catch (err) { + console.error('降级复制失败:', err); + this.showMessage('复制失败,请手动复制', 'error'); + } finally { + document.body.removeChild(textArea); + } + }; + + /** + * 显示消息 + */ + SessionManager.prototype.showMessage = function(message, type) { + if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) { + const messageType = type === 'success' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS : + type === 'warning' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING : + window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR; + window.MCPFeedback.Utils.showMessage(message, messageType); + } else { + console.log(`[${type.toUpperCase()}] ${message}`); + } + }; + // 全域匯出會話歷史方法 window.MCPFeedback.SessionManager.exportSessionHistory = function() { if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.sessionManager) { diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js new file mode 100644 index 0000000..66818d2 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js @@ -0,0 +1,235 @@ +/** + * MCP Feedback Enhanced - 標籤頁管理模組 + * ==================================== + * + * 處理多標籤頁狀態同步和智能瀏覽器管理 + */ + +(function() { + 'use strict'; + + // 確保命名空間和依賴存在 + window.MCPFeedback = window.MCPFeedback || {}; + const Utils = window.MCPFeedback.Utils; + + /** + * 標籤頁管理器建構函數 + */ + function TabManager() { + this.tabId = Utils.generateId('tab'); + this.heartbeatInterval = null; + this.heartbeatFrequency = Utils.CONSTANTS.DEFAULT_TAB_HEARTBEAT_FREQUENCY; + this.storageKey = 'mcp_feedback_tabs'; + this.lastActivityKey = 'mcp_feedback_last_activity'; + + this.init(); + } + + /** + * 初始化標籤頁管理器 + */ + TabManager.prototype.init = function() { + // 註冊當前標籤頁 + this.registerTab(); + + // 向服務器註冊標籤頁 + this.registerTabToServer(); + + // 開始心跳 + this.startHeartbeat(); + + // 監聽頁面關閉事件 + const self = this; + window.addEventListener('beforeunload', function() { + self.unregisterTab(); + }); + + // 監聽 localStorage 變化(其他標籤頁的狀態變化) + window.addEventListener('storage', function(e) { + if (e.key === self.storageKey) { + self.handleTabsChange(); + } + }); + + console.log('📋 TabManager 初始化完成,標籤頁 ID: ' + this.tabId); + }; + + /** + * 註冊當前標籤頁 + */ + TabManager.prototype.registerTab = function() { + const tabs = this.getActiveTabs(); + tabs[this.tabId] = { + timestamp: Date.now(), + url: window.location.href, + active: true + }; + + if (Utils.isLocalStorageSupported()) { + localStorage.setItem(this.storageKey, JSON.stringify(tabs)); + } + + this.updateLastActivity(); + console.log('✅ 標籤頁已註冊: ' + this.tabId); + }; + + /** + * 註銷當前標籤頁 + */ + TabManager.prototype.unregisterTab = function() { + const tabs = this.getActiveTabs(); + delete tabs[this.tabId]; + + if (Utils.isLocalStorageSupported()) { + localStorage.setItem(this.storageKey, JSON.stringify(tabs)); + } + + console.log('❌ 標籤頁已註銷: ' + this.tabId); + }; + + /** + * 開始心跳 + */ + TabManager.prototype.startHeartbeat = function() { + const self = this; + this.heartbeatInterval = setInterval(function() { + self.sendHeartbeat(); + }, this.heartbeatFrequency); + }; + + /** + * 發送心跳 + */ + TabManager.prototype.sendHeartbeat = function() { + const tabs = this.getActiveTabs(); + if (tabs[this.tabId]) { + tabs[this.tabId].timestamp = Date.now(); + + if (Utils.isLocalStorageSupported()) { + localStorage.setItem(this.storageKey, JSON.stringify(tabs)); + } + + this.updateLastActivity(); + } + }; + + /** + * 更新最後活動時間 + */ + TabManager.prototype.updateLastActivity = function() { + if (Utils.isLocalStorageSupported()) { + localStorage.setItem(this.lastActivityKey, Date.now().toString()); + } + }; + + /** + * 獲取活躍標籤頁 + */ + TabManager.prototype.getActiveTabs = function() { + if (!Utils.isLocalStorageSupported()) { + return {}; + } + + try { + const stored = localStorage.getItem(this.storageKey); + const tabs = stored ? Utils.safeJsonParse(stored, {}) : {}; + + // 清理過期的標籤頁 + const now = Date.now(); + const expiredThreshold = Utils.CONSTANTS.TAB_EXPIRED_THRESHOLD; + + for (const tabId in tabs) { + if (tabs.hasOwnProperty(tabId)) { + if (now - tabs[tabId].timestamp > expiredThreshold) { + delete tabs[tabId]; + } + } + } + + return tabs; + } catch (error) { + console.error('獲取活躍標籤頁失敗:', error); + return {}; + } + }; + + /** + * 檢查是否有活躍標籤頁 + */ + TabManager.prototype.hasActiveTabs = function() { + const tabs = this.getActiveTabs(); + return Object.keys(tabs).length > 0; + }; + + /** + * 檢查是否為唯一活躍標籤頁 + */ + TabManager.prototype.isOnlyActiveTab = function() { + const tabs = this.getActiveTabs(); + return Object.keys(tabs).length === 1 && tabs[this.tabId]; + }; + + /** + * 處理其他標籤頁狀態變化 + */ + TabManager.prototype.handleTabsChange = function() { + console.log('🔄 檢測到其他標籤頁狀態變化'); + // 可以在這裡添加更多邏輯 + }; + + /** + * 向服務器註冊標籤頁 + */ + TabManager.prototype.registerTabToServer = function() { + const self = this; + + fetch('/api/register-tab', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + tabId: this.tabId + }) + }) + .then(function(response) { + if (response.ok) { + return response.json(); + } else { + console.warn('⚠️ 標籤頁服務器註冊失敗: ' + response.status); + } + }) + .then(function(data) { + if (data) { + console.log('✅ 標籤頁已向服務器註冊: ' + self.tabId); + } + }) + .catch(function(error) { + console.warn('⚠️ 標籤頁服務器註冊錯誤: ' + error); + }); + }; + + /** + * 清理資源 + */ + TabManager.prototype.cleanup = function() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + this.unregisterTab(); + }; + + /** + * 獲取當前標籤頁 ID + */ + TabManager.prototype.getTabId = function() { + return this.tabId; + }; + + // 將 TabManager 加入命名空間 + window.MCPFeedback.TabManager = TabManager; + + console.log('✅ TabManager 模組載入完成'); + +})(); \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index b0f4ac4..5f779a7 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -651,6 +651,8 @@
+ +