Merge pull request #130 from agassiz/main

web交互体验修改
This commit is contained in:
Minidoracat 2025-06-27 11:45:13 +08:00 committed by GitHub
commit 788d7b61cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1413 additions and 248 deletions

View File

@ -30,8 +30,8 @@ class I18nManager:
def __init__(self):
self._current_language = None
self._translations = {}
self._supported_languages = ["zh-TW", "en", "zh-CN"]
self._fallback_language = "en"
self._supported_languages = ["zh-CN", "zh-TW", "en"]
self._fallback_language = "zh-CN"
self._config_file = self._get_config_file_path()
self._locales_dir = Path(__file__).parent / "web" / "locales"
@ -139,7 +139,7 @@ class I18nManager:
def get_current_language(self) -> str:
"""獲取當前語言"""
return self._current_language or "zh-TW"
return self._current_language or "zh-CN"
def set_language(self, language: str) -> bool:
"""設定語言"""

View File

@ -156,6 +156,10 @@
"submitted": {
"title": "Submitted",
"message": "Waiting for next MCP call"
},
"completed": {
"title": "Completed",
"message": "Session completed"
}
},
"notifications": {

View File

@ -156,6 +156,10 @@
"submitted": {
"title": "反馈已提交",
"message": "等待下次 MCP 调用"
},
"completed": {
"title": "已完成",
"message": "会话已完成"
}
},
"notifications": {

View File

@ -161,6 +161,10 @@
"submitted": {
"title": "回饋已提交",
"message": "等待下次 MCP 調用"
},
"completed": {
"title": "已完成",
"message": "會話已完成"
}
},
"notifications": {

View File

@ -318,25 +318,46 @@ class WebUIManager:
def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話 - 重構為單一活躍會話模式,保留標籤頁狀態"""
# 保存舊會話的 WebSocket 連接以便發送更新通知
# 保存舊會話的引用和 WebSocket 連接
old_session = self.current_session
old_websocket = None
if self.current_session and self.current_session.websocket:
old_websocket = self.current_session.websocket
if old_session and old_session.websocket:
old_websocket = old_session.websocket
debug_log("保存舊會話的 WebSocket 連接以發送更新通知")
# 如果已有活躍會話,先保存其標籤頁狀態到全局狀態
if self.current_session:
debug_log("保存現有會話的標籤頁狀態並清理會話")
# 保存標籤頁狀態到全局
if hasattr(self.current_session, "active_tabs"):
self._merge_tabs_to_global(self.current_session.active_tabs)
# 同步清理會話資源(但保留 WebSocket 連接)
self.current_session._cleanup_sync()
# 創建新會話
session_id = str(uuid.uuid4())
session = WebFeedbackSession(session_id, project_directory, summary)
# 如果有舊會話,處理狀態轉換和清理
if old_session:
debug_log(f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}")
# 保存標籤頁狀態到全局
if hasattr(old_session, "active_tabs"):
self._merge_tabs_to_global(old_session.active_tabs)
# 如果舊會話是已提交狀態,進入下一步(已完成)
if old_session.status == SessionStatus.FEEDBACK_SUBMITTED:
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},無需轉換")
# 確保舊會話仍在字典中用於API獲取
if old_session.session_id in self.sessions:
debug_log(f"舊會話 {old_session.session_id} 仍在會話字典中")
else:
debug_log(f"⚠️ 舊會話 {old_session.session_id} 不在會話字典中,重新添加")
self.sessions[old_session.session_id] = old_session
# 同步清理會話資源(但保留 WebSocket 連接)
old_session._cleanup_sync()
# 將全局標籤頁狀態繼承到新會話
session.active_tabs = self.global_active_tabs.copy()
@ -348,25 +369,11 @@ class WebUIManager:
debug_log(f"創建新的活躍會話: {session_id}")
debug_log(f"繼承 {len(session.active_tabs)} 個活躍標籤頁")
# 處理會話更新通知
# 處理WebSocket連接轉移
if old_websocket:
# 有舊連接,立即發送會話更新通知並轉移連接
self._old_websocket_for_update = old_websocket
self._new_session_for_update = session
debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知")
# 立即發送會話更新通知
import asyncio
try:
# 在後台任務中發送通知並轉移連接
asyncio.create_task(self._send_immediate_session_update())
except Exception as e:
debug_log(f"創建會話更新任務失敗: {e}")
# 即使任務創建失敗,也要嘗試直接轉移連接
session.websocket = old_websocket
debug_log("任務創建失敗,直接轉移 WebSocket 連接到新會話")
self._pending_session_update = True
# 直接轉移連接到新會話,消息發送由 smart_open_browser 統一處理
session.websocket = old_websocket
debug_log("已將舊 WebSocket 連接轉移到新會話")
else:
# 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時)
self._pending_session_update = True
@ -584,8 +591,14 @@ class WebUIManager:
has_active_tabs = await self._check_active_tabs()
if has_active_tabs:
debug_log("檢測到活躍標籤頁,發送刷新通知")
debug_log(f"向現有標籤頁發送刷新通知:{url}")
# 向現有標籤頁發送刷新通知
refresh_success = await self.notify_existing_tab_to_refresh()
debug_log(f"刷新通知發送結果: {refresh_success}")
debug_log("檢測到活躍標籤頁,不開啟新瀏覽器視窗")
debug_log(f"用戶可以在現有標籤頁中查看更新:{url}")
return True
# 沒有活躍標籤頁,開啟新瀏覽器視窗
@ -676,105 +689,9 @@ class WebUIManager:
else:
debug_log("沒有活躍的桌面應用程式實例")
async def notify_session_update(self, session):
"""向活躍標籤頁發送會話更新通知"""
try:
# 檢查是否有活躍的 WebSocket 連接
if session.websocket:
# 直接通過當前會話的 WebSocket 發送
await session.websocket.send_json(
{
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": session.project_directory,
"summary": session.summary,
"session_id": session.session_id,
},
}
)
debug_log("會話更新通知已通過 WebSocket 發送")
else:
# 沒有活躍連接,設置待更新標記
self._pending_session_update = True
debug_log("沒有活躍 WebSocket 連接,設置待更新標記")
except Exception as e:
debug_log(f"發送會話更新通知失敗: {e}")
# 設置待更新標記作為備用方案
self._pending_session_update = True
async def _send_immediate_session_update(self):
"""立即發送會話更新通知(使用舊的 WebSocket 連接)"""
try:
# 檢查是否有保存的舊 WebSocket 連接
if hasattr(self, "_old_websocket_for_update") and hasattr(
self, "_new_session_for_update"
):
old_websocket = self._old_websocket_for_update
new_session = self._new_session_for_update
# 改進的連接有效性檢查
websocket_valid = False
if old_websocket:
try:
# 檢查 WebSocket 連接狀態
if hasattr(old_websocket, "client_state"):
websocket_valid = (
old_websocket.client_state
!= old_websocket.client_state.DISCONNECTED
)
else:
# 如果沒有 client_state 屬性,嘗試發送測試消息來檢查連接
websocket_valid = True
except Exception as check_error:
debug_log(f"檢查 WebSocket 連接狀態失敗: {check_error}")
websocket_valid = False
if websocket_valid:
try:
# 發送會話更新通知
await old_websocket.send_json(
{
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": new_session.project_directory,
"summary": new_session.summary,
"session_id": new_session.session_id,
},
}
)
debug_log("已通過舊 WebSocket 連接發送會話更新通知")
# 延遲一小段時間讓前端處理消息
await asyncio.sleep(0.2)
# 將 WebSocket 連接轉移到新會話
new_session.websocket = old_websocket
debug_log("已將 WebSocket 連接轉移到新會話")
except Exception as send_error:
debug_log(f"發送會話更新通知失敗: {send_error}")
# 如果發送失敗,仍然嘗試轉移連接
new_session.websocket = old_websocket
debug_log("發送失敗但仍轉移 WebSocket 連接到新會話")
else:
debug_log("舊 WebSocket 連接無效,設置待更新標記")
self._pending_session_update = True
# 清理臨時變數
delattr(self, "_old_websocket_for_update")
delattr(self, "_new_session_for_update")
else:
# 沒有舊連接,設置待更新標記
self._pending_session_update = True
debug_log("沒有舊 WebSocket 連接,設置待更新標記")
except Exception as e:
debug_log(f"立即發送會話更新通知失敗: {e}")
# 回退到待更新標記
self._pending_session_update = True
async def _safe_close_websocket(self, websocket):
"""安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用"""
@ -799,40 +716,67 @@ 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) -> bool:
"""通知現有標籤頁刷新顯示新會話內容
Returns:
bool: True 表示成功發送False 表示失敗
"""
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 False
# 構建刷新通知消息
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.value
}
}
# 發送刷新通知
await self.current_session.websocket.send_json(refresh_message)
debug_log(f"已向現有標籤頁發送刷新通知: {self.current_session.session_id}")
# 簡單等待一下讓消息發送完成
await asyncio.sleep(0.2)
debug_log("刷新通知發送完成")
return True
except Exception as e:
debug_log(f"發送刷新通知失敗: {e}")
return False
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)
# 檢查其他會話的WebSocket連接
active_websockets = 0
for session_id, session in self.sessions.items():
if session.websocket:
active_websockets += 1
debug_log(f"檢測到會話 {session_id} 的WebSocket連接")
# 調用活躍標籤頁 API
import aiohttp
if active_websockets > 0:
debug_log(f"檢測到 {active_websockets} 個活躍的WebSocket連接")
return True
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:
@ -1092,7 +1036,7 @@ async def launch_web_feedback_ui(
"""
manager = get_web_ui_manager()
# 創建或更當前活躍會話
# 創建新會話每次AI調用都應該創建新會話
manager.create_session(project_directory, summary)
session = manager.get_current_session()
@ -1119,10 +1063,9 @@ async def launch_web_feedback_ui(
debug_log(f"[DEBUG] 服務器地址: {feedback_url}")
# 如果檢測到活躍標籤頁但沒有開啟新視窗,立即發送會話更新通知
# 如果檢測到活躍標籤頁,消息已在 smart_open_browser 中發送,無需額外處理
if has_active_tabs:
await manager._send_immediate_session_update()
debug_log("已向活躍標籤頁發送會話更新通知")
debug_log("檢測到活躍標籤頁,會話更新通知已發送")
try:
# 等待用戶回饋,傳遞 timeout 參數

View File

@ -29,15 +29,13 @@ from ...utils.resource_manager import get_resource_manager, register_process
class SessionStatus(Enum):
"""會話狀態枚舉"""
"""會話狀態枚舉 - 單向流轉設計"""
WAITING = "waiting" # 等待中
ACTIVE = "active" # 活躍中
FEEDBACK_SUBMITTED = "feedback_submitted" # 已提交反饋
COMPLETED = "completed" # 已完成
TIMEOUT = "timeout" # 超時
ERROR = "error" # 錯誤
EXPIRED = "expired" # 已過期
ERROR = "error" # 錯誤(終態)
EXPIRED = "expired" # 已過期(終態)
class CleanupReason(Enum):
@ -133,6 +131,7 @@ class WebFeedbackSession:
self.feedback_completed = threading.Event()
self.process: subprocess.Popen | None = None
self.command_logs: list[str] = []
self.user_messages: list[dict] = [] # 用戶消息記錄
self._cleanup_done = False # 防止重複清理
# 新增:會話狀態管理
@ -174,21 +173,79 @@ class WebFeedbackSession:
f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}"
)
def update_status(self, status: SessionStatus, message: str | None = None):
"""更新會話狀態"""
self.status = status
def next_step(self, message: str | None = None) -> bool:
"""進入下一個狀態 - 單向流轉,不可倒退"""
old_status = self.status
# 定義狀態流轉路徑
next_status_map = {
SessionStatus.WAITING: SessionStatus.FEEDBACK_SUBMITTED,
SessionStatus.FEEDBACK_SUBMITTED: SessionStatus.COMPLETED,
SessionStatus.COMPLETED: None, # 終態
SessionStatus.ERROR: 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},無法進入下一步")
return False
# 執行狀態轉換
self.status = next_status
if message:
self.status_message = message
# 統一使用 time.time()
else:
# 默認消息
default_messages = {
SessionStatus.FEEDBACK_SUBMITTED: "用戶已提交反饋",
SessionStatus.COMPLETED: "會話已完成"
}
self.status_message = default_messages.get(next_status, "狀態已更新")
self.last_activity = time.time()
# 如果會話變為活躍狀態,重置清理定時器
if status in [SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]:
# 如果會話變為已提交狀態,重置清理定時器
if next_status == SessionStatus.FEEDBACK_SUBMITTED:
self._schedule_auto_cleanup()
debug_log(
f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}"
f"會話 {self.session_id} 狀態流轉: {old_status.value}{next_status.value} - {self.status_message}"
)
return True
def set_error(self, message: str = "會話發生錯誤") -> bool:
"""設置錯誤狀態(特殊方法,可從任何狀態進入)"""
old_status = self.status
self.status = SessionStatus.ERROR
self.status_message = message
self.last_activity = time.time()
debug_log(
f"❌ 會話 {self.session_id} 設置為錯誤狀態: {old_status.value}{self.status.value} - {message}"
)
return True
def set_expired(self, message: str = "會話已過期") -> bool:
"""設置過期狀態(特殊方法,可從任何狀態進入)"""
old_status = self.status
self.status = SessionStatus.EXPIRED
self.status_message = message
self.last_activity = time.time()
debug_log(
f"⏰ 會話 {self.session_id} 設置為過期狀態: {old_status.value}{self.status.value} - {message}"
)
return True
def can_proceed(self) -> bool:
"""檢查是否可以進入下一步"""
return self.status in [SessionStatus.WAITING, SessionStatus.FEEDBACK_SUBMITTED]
def is_terminal(self) -> bool:
"""檢查是否處於終態"""
return self.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.EXPIRED]
def get_status_info(self) -> dict[str, Any]:
"""獲取會話狀態信息"""
@ -404,10 +461,8 @@ class WebFeedbackSession:
self.settings = settings or {}
self.images = self._process_images(images)
# 更新狀態為已提交反饋
self.update_status(
SessionStatus.FEEDBACK_SUBMITTED, "已送出反饋,等待下次 MCP 調用"
)
# 進入下一步:等待中 → 已提交反饋
self.next_step("已送出反饋,等待下次 MCP 調用")
self.feedback_completed.set()
@ -443,6 +498,22 @@ class WebFeedbackSession:
# 重構:不再自動關閉 WebSocket保持連接以支援頁面持久性
def add_user_message(self, message_data: dict[str, Any]) -> None:
"""添加用戶消息記錄"""
import time
# 創建用戶消息記錄
user_message = {
"timestamp": int(time.time() * 1000), # 毫秒時間戳
"content": message_data.get("content", ""),
"images": message_data.get("images", []),
"submission_method": message_data.get("submission_method", "manual"),
"type": "feedback"
}
self.user_messages.append(user_message)
debug_log(f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}")
def _process_images(self, images: list[dict]) -> list[dict]:
"""
處理圖片數據轉換為統一格式

View File

@ -157,6 +157,68 @@ def setup_routes(manager: "WebUIManager"):
}
)
@manager.app.get("/api/all-sessions")
async def get_all_sessions():
"""獲取所有會話的實時狀態"""
try:
sessions_data = []
# 獲取所有會話的實時狀態
for session_id, session in manager.sessions.items():
session_info = {
"session_id": session.session_id,
"project_directory": session.project_directory,
"summary": session.summary,
"status": session.status.value,
"status_message": session.status_message,
"created_at": int(session.created_at * 1000), # 轉換為毫秒
"last_activity": int(session.last_activity * 1000),
"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 # 包含用戶消息記錄
}
sessions_data.append(session_info)
# 按創建時間排序(最新的在前)
sessions_data.sort(key=lambda x: x["created_at"], reverse=True)
debug_log(f"返回 {len(sessions_data)} 個會話的實時狀態")
return JSONResponse(content={"sessions": sessions_data})
except Exception as e:
debug_log(f"獲取所有會話狀態失敗: {e}")
return JSONResponse(
status_code=500,
content={"error": f"獲取會話狀態失敗: {e!s}"}
)
@manager.app.post("/api/add-user-message")
async def add_user_message(request: Request):
"""添加用戶消息到當前會話"""
try:
data = await request.json()
current_session = manager.get_current_session()
if not current_session:
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": "用戶消息已記錄"})
except Exception as e:
debug_log(f"添加用戶消息失敗: {e}")
return JSONResponse(
status_code=500,
content={"error": f"添加用戶消息失敗: {e!s}"}
)
@manager.app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket 端點 - 重構後移除 session_id 依賴"""
@ -187,6 +249,7 @@ def setup_routes(manager: "WebUIManager"):
await websocket.send_json(
{
"type": "session_updated",
"action": "new_session_created",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": session.project_directory,

View File

@ -753,7 +753,7 @@
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 500px;
max-width: 750px;
width: 90%;
max-height: 80vh;
overflow: hidden;
@ -854,11 +854,149 @@
.detail-value.summary {
background: var(--bg-secondary);
padding: 8px 12px;
padding: 0;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
line-height: 1.4;
margin-top: 4px;
position: relative;
}
.summary-actions {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.btn-copy-summary {
background: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.8;
}
.btn-copy-summary:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.summary-content {
padding: 8px 12px;
padding-right: 50px; /* 为复制按钮留出空间 */
}
/* 为会话详情弹窗中的摘要内容应用与反馈页面相同的 Markdown 样式 */
.detail-value.summary .summary-content h1,
.detail-value.summary .summary-content h2,
.detail-value.summary .summary-content h3,
.detail-value.summary .summary-content h4,
.detail-value.summary .summary-content h5,
.detail-value.summary .summary-content h6 {
color: var(--text-primary);
margin: 16px 0 8px 0;
font-weight: 600;
}
.detail-value.summary .summary-content h1 { font-size: 20px; }
.detail-value.summary .summary-content h2 { font-size: 18px; }
.detail-value.summary .summary-content h3 { font-size: 16px; }
.detail-value.summary .summary-content h4 { font-size: 14px; }
.detail-value.summary .summary-content h5 { font-size: 13px; }
.detail-value.summary .summary-content h6 { font-size: 12px; }
.detail-value.summary .summary-content p {
margin: 8px 0;
line-height: 1.6;
}
.detail-value.summary .summary-content strong {
font-weight: 600;
color: var(--text-primary);
}
.detail-value.summary .summary-content em {
font-style: italic;
color: var(--text-primary);
}
.detail-value.summary .summary-content code {
background: var(--bg-tertiary);
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: var(--accent-color);
}
.detail-value.summary .summary-content pre {
background: var(--bg-tertiary);
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
border-left: 3px solid var(--accent-color);
}
.detail-value.summary .summary-content pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.detail-value.summary .summary-content ul,
.detail-value.summary .summary-content ol {
margin: 8px 0;
padding-left: 20px;
}
.detail-value.summary .summary-content li {
margin: 4px 0;
line-height: 1.5;
}
.detail-value.summary .summary-content blockquote {
border-left: 4px solid var(--accent-color);
margin: 12px 0;
padding: 8px 16px;
background: rgba(0, 122, 204, 0.05);
font-style: italic;
}
/* 复制提示样式 */
.copy-toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: white;
z-index: 10000;
opacity: 0;
transform: translateX(100px);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.copy-toast.show {
opacity: 1;
transform: translateX(0);
}
.copy-toast-success {
background: var(--success-color);
}
.copy-toast-error {
background: var(--error-color);
}
.modal-footer {
@ -924,6 +1062,36 @@
background: var(--bg-secondary);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.message-header .btn-copy-message {
background: var(--accent-color);
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.7;
margin-left: 8px;
}
.message-header .btn-copy-message:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.user-message-item:hover .btn-copy-message {
opacity: 0.9;
}
.message-header {
display: flex;
justify-content: space-between;

View File

@ -369,7 +369,6 @@ body {
/* 容器樣式 */
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<!-- 背景圆形 -->
<circle cx="16" cy="16" r="15" fill="#2563eb" stroke="#1e40af" stroke-width="2"/>
<!-- MCP 字母 M -->
<path d="M6 10 L6 22 L8 22 L8 14 L10 18 L12 14 L12 22 L14 22 L14 10 L11 10 L10 14 L9 10 Z" fill="white"/>
<!-- 反馈图标 - 对话气泡 -->
<path d="M18 8 C20.2 8 22 9.8 22 12 L22 16 C22 18.2 20.2 20 18 20 L16 20 L14 22 L14 20 L16 20 C16 20 18 20 18 20 C19.1 20 20 19.1 20 18 L20 14 C20 12.9 19.1 12 18 12 L16 12 C14.9 12 14 12.9 14 14 L14 16 C14 17.1 14.9 18 16 18" fill="none" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<!-- 反馈箭头 -->
<path d="M24 14 L26 16 L24 18" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- 小点表示活跃状态 -->
<circle cx="19" cy="15" r="1" fill="#10b981"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@ -647,9 +647,25 @@
const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...';
this.updateSummaryStatus(submittedMessage);
// 刷新會話列表以顯示最新狀態
this.refreshSessionList();
console.log('反饋已提交,頁面保持開啟狀態');
};
/**
* 刷新會話列表以顯示最新狀態
*/
FeedbackApp.prototype.refreshSessionList = function() {
// 如果有會話管理器,觸發數據刷新
if (this.sessionManager && this.sessionManager.dataManager) {
console.log('🔄 刷新會話列表以顯示最新狀態');
this.sessionManager.dataManager.loadFromServer();
} else {
console.log('⚠️ 會話管理器未初始化,跳過會話列表刷新');
}
};
/**
* 處理桌面關閉請求
*/
@ -682,7 +698,46 @@
* 處理會話更新原始版本供防抖使用
*/
FeedbackApp.prototype._originalHandleSessionUpdated = function(data) {
console.log('🔄 處理會話更新:', data.session_info);
console.log('🔄 處理會話更新:', data);
console.log('🔍 檢查 action 字段:', data.action);
console.log('🔍 檢查 type 字段:', data.type);
// 檢查是否是新會話創建的通知
if (data.action === 'new_session_created' || data.type === 'new_session_created') {
console.log('🆕 檢測到新會話創建,完全刷新頁面以確保狀態同步');
// 播放音效通知
if (this.audioManager) {
this.audioManager.playNotification();
}
// 顯示新會話通知
window.MCPFeedback.Utils.showMessage(
data.message || '新的 MCP 會話已創建,正在打開新窗口...',
window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS
);
// 使用 window.open 打開新窗口並關閉當前窗口
setTimeout(function() {
console.log('🔄 使用 window.open 打開新窗口');
// 打開新窗口
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();
}
}, 1500);
return; // 提前返回,不執行後續的局部更新邏輯
}
// 播放音效通知
if (this.audioManager) {
@ -758,8 +813,16 @@
}
}
// 重置回饋狀態為等待新回饋
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, newSessionId);
// 檢查當前狀態,只有在非已提交狀態時才重置
const currentState = this.uiManager.getFeedbackState();
if (currentState !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, newSessionId);
console.log('🔄 會話更新:重置回饋狀態為等待新回饋');
} else {
console.log('🔒 會話更新:保護已提交狀態,不重置');
// 更新會話ID但保持已提交狀態
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, newSessionId);
}
// 檢查並啟動自動提交(如果條件滿足)
const self = this;
@ -799,7 +862,16 @@
* 處理狀態更新原始版本供防抖使用
*/
FeedbackApp.prototype._originalHandleStatusUpdate = function(statusInfo) {
console.log('處理狀態更新:', statusInfo);
console.log('📊 處理狀態更新:', statusInfo);
const sessionId = statusInfo.session_id;
console.log('🔍 狀態更新詳情:', {
currentSessionId: this.currentSessionId,
newSessionId: sessionId,
status: statusInfo.status,
message: statusInfo.message,
isNewSession: sessionId !== this.currentSessionId
});
// 更新 SessionManager 的狀態資訊
if (this.sessionManager && this.sessionManager.updateStatusInfo) {
@ -812,36 +884,39 @@
document.title = 'MCP Feedback - ' + projectName;
}
// 提取會話 ID
const sessionId = statusInfo.session_id || this.currentSessionId;
// 使用之前已聲明的 sessionId
// 根據狀態更新 UI
// 前端只管理會話ID所有狀態都從服務器獲取
console.log('📊 收到服務器狀態更新:', statusInfo.status, '會話ID:', sessionId);
// 更新當前會話ID
if (sessionId) {
this.currentSessionId = sessionId;
console.log('🔄 更新當前會話ID:', sessionId.substring(0, 8) + '...');
}
// 刷新會話列表以顯示最新狀態
this.refreshSessionList();
// 根據服務器狀態更新消息顯示(不修改前端狀態)
switch (statusInfo.status) {
case 'feedback_submitted':
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, sessionId);
const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...';
this.updateSummaryStatus(submittedMessage);
break;
case 'active':
case 'waiting':
// 檢查是否是新會話
if (sessionId && sessionId !== this.currentSessionId) {
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionId);
} else if (this.uiManager.getFeedbackState() !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionId);
}
const waitingMessage = window.i18nManager ? window.i18nManager.t('feedback.waitingForUser') : '等待用戶回饋...';
this.updateSummaryStatus(waitingMessage);
if (statusInfo.status === 'waiting') {
const waitingMessage = window.i18nManager ? window.i18nManager.t('feedback.waitingForUser') : '等待用戶回饋...';
this.updateSummaryStatus(waitingMessage);
// 檢查並啟動自動提交(如果條件滿足)
const self = this;
setTimeout(function() {
self.checkAndStartAutoSubmit();
}, 100); // 短暫延遲確保狀態更新完成
}
// 檢查並啟動自動提交(如果條件滿足)
const self = this;
setTimeout(function() {
self.checkAndStartAutoSubmit();
}, 100);
break;
case 'completed':
const completedMessage = window.i18nManager ? window.i18nManager.t('feedback.completed') : '會話已完成';
this.updateSummaryStatus(completedMessage);
break;
}
};
@ -884,10 +959,15 @@
* 檢查是否可以提交回饋
*/
FeedbackApp.prototype.canSubmitFeedback = function() {
return this.webSocketManager &&
this.webSocketManager.isReady() &&
this.uiManager &&
this.uiManager.getFeedbackState() === window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING;
// 簡化檢查只檢查WebSocket連接狀態由服務器端驗證
const wsReady = this.webSocketManager && this.webSocketManager.isReady();
console.log('🔍 提交檢查:', {
wsReady: wsReady,
sessionId: this.currentSessionId
});
return wsReady;
};
/**
@ -1164,14 +1244,9 @@
FeedbackApp.prototype.handleSessionUpdate = function(sessionData) {
console.log('🔄 處理自動檢測到的會話更新:', sessionData);
// 更新當前會話 ID
// 更新當前會話 ID,不管理狀態
this.currentSessionId = sessionData.session_id;
// 重置回饋狀態
if (this.uiManager) {
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
}
// 局部更新頁面內容
this.refreshPageContent();
};
@ -1194,9 +1269,17 @@
.then(function(sessionData) {
console.log('📥 獲取到最新會話資料:', sessionData);
// 重置回饋狀態
// 檢查並保護已提交狀態
if (sessionData.session_id && self.uiManager) {
self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
const currentState = self.uiManager.getFeedbackState();
if (currentState !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
console.log('🔄 局部更新:重置回饋狀態為等待中');
} else {
console.log('🔒 局部更新:保護已提交狀態,不重置');
// 只更新會話ID保持已提交狀態
self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, sessionData.session_id);
}
}
// 更新 AI 摘要內容

View File

@ -8,7 +8,7 @@
class I18nManager {
constructor() {
this.currentLanguage = 'zh-TW';
this.currentLanguage = 'zh-CN';
this.translations = {};
this.loadingPromise = null;
}

View File

@ -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) {

View File

@ -62,9 +62,13 @@
if (this.currentSession && this.currentSession.session_id) {
console.log('📊 檢測到會話 ID 變更,處理舊會話:', this.currentSession.session_id, '->', sessionData.session_id);
// 將舊會話標記為完成並加入歷史記錄
// 將舊會話加入歷史記錄,保持其原有狀態
const oldSession = Object.assign({}, this.currentSession);
oldSession.status = 'completed';
// 完全保持舊會話的原有狀態,不做任何修改
// 讓服務器端負責狀態轉換,前端只負責顯示
console.log('📊 保持舊會話的原有狀態:', oldSession.status);
oldSession.completed_at = TimeUtils.getCurrentTimestamp();
// 計算持續時間
@ -309,6 +313,9 @@
// 記錄用戶最後互動時間
this.currentSession.last_user_interaction = TimeUtils.getCurrentTimestamp();
// 發送用戶消息到服務器端
this.sendUserMessageToServer(userMessage);
// 立即保存當前會話到伺服器
this.saveCurrentSessionToServer();
@ -316,6 +323,29 @@
return true;
};
/**
* 發送用戶消息到服務器端
*/
SessionDataManager.prototype.sendUserMessageToServer = function(userMessage) {
fetch('/api/add-user-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userMessage)
})
.then(function(response) {
if (response.ok) {
console.log('📊 用戶消息已發送到服務器端');
} else {
console.warn('📊 發送用戶消息到服務器端失敗:', response.status);
}
})
.catch(function(error) {
console.warn('📊 發送用戶消息到服務器端出錯:', error);
});
};
/**
* 建立用戶訊息記錄
*/
@ -453,16 +483,24 @@
};
/**
* 根據 ID 查找會話
* 根據 ID 查找會話包含完整的用戶消息數據
*/
SessionDataManager.prototype.findSessionById = function(sessionId) {
// 先檢查當前會話
if (this.currentSession && this.currentSession.session_id === sessionId) {
console.log('📊 從當前會話獲取數據:', sessionId, '用戶消息數量:', this.currentSession.user_messages ? this.currentSession.user_messages.length : 0);
return this.currentSession;
}
// 再檢查歷史記錄
return this.sessionHistory.find(s => s.session_id === sessionId) || null;
const historySession = this.sessionHistory.find(s => s.session_id === sessionId);
if (historySession) {
console.log('📊 從歷史記錄獲取數據:', sessionId, '用戶消息數量:', historySession.user_messages ? historySession.user_messages.length : 0);
return historySession;
}
console.warn('📊 找不到會話:', sessionId);
return null;
};
/**
@ -589,23 +627,25 @@
};
/**
* 從伺服器載入會話歷史
* 從伺服器載入會話歷史包含實時狀態
*/
SessionDataManager.prototype.loadFromServer = function() {
const self = this;
fetch('/api/load-session-history')
// 首先嘗試獲取實時會話狀態
fetch('/api/all-sessions')
.then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('伺服器回應錯誤: ' + response.status);
throw new Error('獲取實時會話狀態失敗: ' + response.status);
}
})
.then(function(data) {
if (data && Array.isArray(data.sessions)) {
// 使用實時會話狀態
self.sessionHistory = data.sessions;
console.log('📊 從伺服器載入', self.sessionHistory.length, '個會話');
console.log('📊 從伺服器載入', self.sessionHistory.length, '個實時會話狀態');
// 載入完成後進行清理和統計更新
self.cleanupExpiredSessions();
@ -621,13 +661,53 @@
self.onDataChanged();
}
} else {
console.warn('📊 伺服器回應格式錯誤:', data);
self.sessionHistory = [];
console.warn('📊 實時會話狀態回應格式錯誤,回退到歷史文件');
self.loadFromHistoryFile();
}
})
.catch(function(error) {
console.warn('📊 獲取實時會話狀態失敗,回退到歷史文件:', error);
self.loadFromHistoryFile();
});
};
// 即使沒有資料也要更新統計
/**
* 從歷史文件載入會話數據備用方案
*/
SessionDataManager.prototype.loadFromHistoryFile = function() {
const self = this;
fetch('/api/load-session-history')
.then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('伺服器回應錯誤: ' + response.status);
}
})
.then(function(data) {
if (data && Array.isArray(data.sessions)) {
self.sessionHistory = data.sessions;
console.log('📊 從歷史文件載入', self.sessionHistory.length, '個會話');
// 載入完成後進行清理和統計更新
self.cleanupExpiredSessions();
self.updateStats();
// 觸發歷史記錄變更回調
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
// 觸發資料變更回調
if (self.onDataChanged) {
self.onDataChanged();
}
} else {
console.warn('📊 歷史文件回應格式錯誤:', data);
self.sessionHistory = [];
self.updateStats();
// 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
@ -638,13 +718,10 @@
}
})
.catch(function(error) {
console.warn('📊 從伺服器載入會話歷史失敗:', error);
console.warn('📊 從歷史文件載入失敗:', error);
self.sessionHistory = [];
// 載入失敗時也要更新統計
self.updateStats();
// 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}

View File

@ -45,6 +45,9 @@
console.log('🔍 顯示會話詳情:', sessionData.session_id);
// 存储当前会话数据,供复制功能使用
this.currentSessionData = sessionData;
// 關閉現有彈窗
this.closeModal();
@ -166,7 +169,12 @@
</div>
<div class="detail-row">
<span class="detail-label">${i18n ? i18n.t('sessionManagement.aiSummary') : 'AI 摘要'}:</span>
<div class="detail-value summary">${this.escapeHtml(details.summary)}</div>
<div class="detail-value summary">
<div class="summary-actions">
<button class="btn-copy-summary" title="复制源码" aria-label="复制源码">📋</button>
</div>
<div class="summary-content">${this.renderMarkdownSafely(details.summary)}</div>
</div>
</div>
${this.createUserMessagesSection(details)}
</div>
@ -241,11 +249,12 @@
}
messagesHtml += `
<div class="user-message-item">
<div class="user-message-item" data-message-index="${index}">
<div class="message-header">
<span class="message-index">#${index + 1}</span>
<span class="message-time">${timestamp}</span>
<span class="message-method">${submissionMethod}</span>
<button class="btn-copy-message" title="复制消息内容" aria-label="复制消息内容" data-message-content="${this.escapeHtml(message.content)}">📋</button>
</div>
${contentHtml}
</div>
@ -310,6 +319,24 @@
};
document.addEventListener('keydown', this.keydownHandler);
}
// 复制摘要按钮
const copyBtn = this.currentModal.querySelector('.btn-copy-summary');
if (copyBtn) {
DOMUtils.addEventListener(copyBtn, 'click', function() {
self.copySummaryToClipboard();
});
}
// 复制用户消息按钮
const copyMessageBtns = this.currentModal.querySelectorAll('.btn-copy-message');
copyMessageBtns.forEach(function(btn) {
DOMUtils.addEventListener(btn, 'click', function(e) {
e.stopPropagation(); // 防止事件冒泡
const messageContent = btn.getAttribute('data-message-content');
self.copyMessageToClipboard(messageContent);
});
});
};
/**
@ -355,12 +382,198 @@
*/
SessionDetailsModal.prototype.escapeHtml = function(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
/**
* 安全地渲染 Markdown 內容
*/
SessionDetailsModal.prototype.renderMarkdownSafely = function(content) {
if (!content) return '';
try {
// 检查 marked 和 DOMPurify 是否可用
if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') {
console.warn('⚠️ Markdown 库未载入,使用纯文字显示');
return this.escapeHtml(content);
}
// 使用 marked 解析 Markdown
const htmlContent = window.marked.parse(content);
// 使用 DOMPurify 清理 HTML
const cleanHtml = window.DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'del', 's', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
ALLOWED_ATTR: ['href', 'title', 'class', 'align', 'style'],
ALLOW_DATA_ATTR: false
});
return cleanHtml;
} catch (error) {
console.error('❌ Markdown 渲染失败:', error);
return this.escapeHtml(content);
}
};
/**
* 复制摘要内容到剪贴板
*/
SessionDetailsModal.prototype.copySummaryToClipboard = function() {
const self = this; // 定义 self 变量
try {
// 获取原始摘要内容Markdown 源码)
const summaryContent = this.currentSessionData && this.currentSessionData.summary ?
this.currentSessionData.summary : '';
if (!summaryContent) {
console.warn('⚠️ 没有摘要内容可复制');
return;
}
// 传统复制方法
const fallbackCopyTextToClipboard = function(text) {
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 {
const successful = document.execCommand('copy');
if (successful) {
console.log('✅ 摘要内容已复制到剪贴板(传统方法)');
self.showToast('✅ 摘要已复制到剪贴板', 'success');
} else {
console.error('❌ 复制失败(传统方法)');
self.showToast('❌ 复制失败,请手动复制', 'error');
}
} catch (err) {
console.error('❌ 复制失败:', err);
self.showToast('❌ 复制失败,请手动复制', 'error');
} finally {
document.body.removeChild(textArea);
}
};
// 使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(summaryContent).then(function() {
console.log('✅ 摘要内容已复制到剪贴板');
self.showToast('✅ 摘要已复制到剪贴板', 'success');
}).catch(function(err) {
console.error('❌ 复制失败:', err);
// 降级到传统方法
fallbackCopyTextToClipboard(summaryContent);
});
} else {
// 降级到传统方法
fallbackCopyTextToClipboard(summaryContent);
}
} catch (error) {
console.error('❌ 复制摘要时发生错误:', error);
this.showToast('❌ 复制失败,请手动复制', 'error');
}
};
/**
* 复制用户消息内容到剪贴板
*/
SessionDetailsModal.prototype.copyMessageToClipboard = function(messageContent) {
if (!messageContent) {
console.warn('⚠️ 没有消息内容可复制');
return;
}
const self = this;
try {
// 使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(messageContent).then(function() {
console.log('✅ 用户消息已复制到剪贴板');
self.showToast('✅ 消息已复制到剪贴板', 'success');
}).catch(function(err) {
console.error('❌ 复制失败:', err);
// 降级到传统方法
fallbackCopyTextToClipboard(messageContent);
});
} else {
// 降级到传统方法
fallbackCopyTextToClipboard(messageContent);
}
// 传统复制方法
const fallbackCopyTextToClipboard = function(text) {
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 {
const successful = document.execCommand('copy');
if (successful) {
console.log('✅ 用户消息已复制到剪贴板(传统方法)');
self.showToast('✅ 消息已复制到剪贴板', 'success');
} else {
console.error('❌ 复制失败(传统方法)');
self.showToast('❌ 复制失败,请手动复制', 'error');
}
} catch (err) {
console.error('❌ 复制失败:', err);
self.showToast('❌ 复制失败,请手动复制', 'error');
} finally {
document.body.removeChild(textArea);
}
};
} catch (error) {
console.error('❌ 复制用户消息时发生错误:', error);
this.showToast('❌ 复制失败,请手动复制', 'error');
}
};
/**
* 显示提示消息
*/
SessionDetailsModal.prototype.showToast = function(message, type) {
// 创建提示元素
const toast = document.createElement('div');
toast.className = 'copy-toast copy-toast-' + type;
toast.textContent = message;
// 添加到弹窗中
if (this.currentModal) {
this.currentModal.appendChild(toast);
// 显示动画
setTimeout(function() {
toast.classList.add('show');
}, 10);
// 自动隐藏
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
};
/**
* 檢查是否有彈窗開啟
*/

View File

@ -486,9 +486,18 @@
// 狀態徽章
const statusContainer = DOMUtils.createElement('div', { className: 'session-status' });
const statusText = StatusUtils.getStatusText(sessionData.status);
// 添加調試信息
console.log('🎨 會話狀態調試:', {
sessionId: sessionData.session_id ? sessionData.session_id.substring(0, 8) + '...' : 'unknown',
rawStatus: sessionData.status,
displayText: statusText
});
const statusBadge = DOMUtils.createElement('span', {
className: 'status-badge ' + (sessionData.status || 'waiting'),
textContent: StatusUtils.getStatusText(sessionData.status)
textContent: statusText
});
statusContainer.appendChild(statusBadge);

View File

@ -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 模組載入完成');
})();

View File

@ -47,7 +47,7 @@
'waiting_for_feedback': 'connectionMonitor.waiting',
'active': 'status.processing.title',
'feedback_submitted': 'status.submitted.title',
'completed': 'status.submitted.title',
'completed': 'status.completed.title',
'timeout': 'session.timeout',
'error': 'status.error',
'expired': 'session.timeout',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,15 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///wAAAAA">
<link rel="apple-touch-icon" href="/static/icon.svg">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/session-management.css">
<link rel="stylesheet" href="/static/css/prompt-management.css">
@ -651,6 +657,8 @@
</div>
<div class="session-actions">
<button class="btn-small" id="viewSessionDetails" data-i18n="sessionManagement.viewDetails">詳細資訊</button>
<button class="btn-small btn-primary" id="copyCurrentSessionContent" title="复制当前会话内容">📋 复制会话内容</button>
<button class="btn-small btn-secondary" id="copyCurrentUserContent" title="复制当前用户发送的内容">📝 复制用户内容</button>
</div>
</div>
</div>
@ -733,8 +741,8 @@
</div>
<div class="language-selector-dropdown">
<select id="settingsLanguageSelect" class="language-setting-select">
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="zh-CN" data-i18n="languages.zh-CN">简体中文</option>
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="en" data-i18n="languages.en">English</option>
</select>
</div>
@ -1049,9 +1057,9 @@
</div>
<!-- WebSocket 和 JavaScript -->
<!-- Markdown 支援庫 -->
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
<!-- Markdown 支援庫 - 本地版本 -->
<script src="/static/js/vendor/marked.min.js"></script>
<script src="/static/js/vendor/purify.min.js"></script>
<script src="/static/js/i18n.js?v=2025010510"></script>
<!-- 載入所有模組 -->
<!-- 核心模組(最先載入) -->

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -129,7 +129,6 @@
/* 主容器 - 有會話時顯示 */
.main-container {
display: none;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;