🔧 增強調試功能,新增 debug_log 函數以輸出調試訊息,並在多個模組中替換原有的 print 語句。更新伺服器初始化以確保編碼正確性,並在 Windows 環境下設置 stdio 為二進制模式。

This commit is contained in:
Minidoracat 2025-05-31 05:20:46 +08:00
parent 86e55b89f6
commit aa4cb3a136
5 changed files with 564 additions and 483 deletions

View File

@ -41,12 +41,8 @@ def main():
elif args.command == 'version':
show_version()
elif args.command == 'server':
# 明確指定伺服器命令
print("🚀 啟動 Interactive Feedback MCP Enhanced 伺服器...")
run_server()
elif args.command is None:
# 沒有指定命令,啟動伺服器(保持向後兼容)
print("🚀 啟動 Interactive Feedback MCP Enhanced 伺服器...")
run_server()
else:
# 不應該到達這裡

View File

@ -30,6 +30,29 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtGui import QFont, QPixmap, QDragEnterEvent, QDropEvent, QKeySequence, QShortcut
# ===== 調試日誌函數 =====
def debug_log(message: str) -> None:
"""輸出調試訊息到標準錯誤,避免污染標準輸出"""
# 只在啟用調試模式時才輸出,避免干擾 MCP 通信
if not os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"):
return
try:
# 確保消息是字符串類型
if not isinstance(message, str):
message = str(message)
# 安全地輸出到 stderr處理編碼問題
try:
print(f"[GUI_DEBUG] {message}", file=sys.stderr, flush=True)
except UnicodeEncodeError:
# 如果遇到編碼問題,使用 ASCII 安全模式
safe_message = message.encode('ascii', errors='replace').decode('ascii')
print(f"[GUI_DEBUG] {safe_message}", file=sys.stderr, flush=True)
except Exception:
# 最後的備用方案:靜默失敗,不影響主程序
pass
# ===== 型別定義 =====
class FeedbackResult(TypedDict):
"""回饋結果的型別定義"""
@ -265,9 +288,8 @@ class ImageUploadWidget(QWidget):
new_height = int(image.height() * scale)
# 縮放圖片
from PySide6.QtCore import Qt
image = image.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)
print(f"[DEBUG] 圖片已縮放至: {new_width}x{new_height}")
debug_log(f"圖片已縮放至: {new_width}x{new_height}")
# 使用較低的質量保存以減小文件大小
quality = 70 # 降低質量以減小文件大小
@ -275,7 +297,7 @@ class ImageUploadWidget(QWidget):
# 檢查保存後的文件大小
if temp_file.exists():
file_size = temp_file.stat().st_size
print(f"[DEBUG] 剪貼板圖片保存成功: {temp_file}, 大小: {file_size} bytes")
debug_log(f"剪貼板圖片保存成功: {temp_file}, 大小: {file_size} bytes")
# 檢查文件大小是否超過限制
if file_size > 1 * 1024 * 1024: # 1MB 限制
@ -319,34 +341,34 @@ class ImageUploadWidget(QWidget):
if os.path.exists(file_path):
os.remove(file_path)
temp_files_cleaned += 1
print(f"[DEBUG] 已刪除臨時文件: {file_path}")
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
print(f"[DEBUG] 刪除臨時文件失敗: {e}")
debug_log(f"刪除臨時文件失敗: {e}")
# 清除內存中的圖片數據
self.images.clear()
self._refresh_preview()
self._update_status()
self.images_changed.emit()
print(f"[DEBUG] 已清除所有圖片,包括 {temp_files_cleaned} 個臨時文件")
debug_log(f"已清除所有圖片,包括 {temp_files_cleaned} 個臨時文件")
def _add_images(self, file_paths: List[str]) -> None:
"""添加圖片"""
added_count = 0
for file_path in file_paths:
try:
print(f"[DEBUG] 嘗試添加圖片: {file_path}")
debug_log(f"嘗試添加圖片: {file_path}")
if not os.path.exists(file_path):
print(f"[DEBUG] 文件不存在: {file_path}")
debug_log(f"文件不存在: {file_path}")
continue
if not self._is_image_file(file_path):
print(f"[DEBUG] 不是圖片文件: {file_path}")
debug_log(f"不是圖片文件: {file_path}")
continue
file_size = os.path.getsize(file_path)
print(f"[DEBUG] 文件大小: {file_size} bytes")
debug_log(f"文件大小: {file_size} bytes")
# 更嚴格的大小限制1MB
if file_size > 1 * 1024 * 1024:
@ -364,10 +386,10 @@ class ImageUploadWidget(QWidget):
# 讀取圖片原始二進制數據
with open(file_path, 'rb') as f:
raw_data = f.read()
print(f"[DEBUG] 讀取原始數據大小: {len(raw_data)} bytes")
debug_log(f"讀取原始數據大小: {len(raw_data)} bytes")
if len(raw_data) == 0:
print(f"[DEBUG] 讀取的數據為空!")
debug_log(f"讀取的數據為空!")
continue
# 再次檢查內存中的數據大小
@ -386,14 +408,14 @@ class ImageUploadWidget(QWidget):
"size": file_size
}
added_count += 1
print(f"[DEBUG] 圖片添加成功: {os.path.basename(file_path)}, 數據大小: {len(raw_data)} bytes")
debug_log(f"圖片添加成功: {os.path.basename(file_path)}, 數據大小: {len(raw_data)} bytes")
except Exception as e:
print(f"[DEBUG] 添加圖片失敗: {e}")
debug_log(f"添加圖片失敗: {e}")
QMessageBox.warning(self, "錯誤", f"無法載入圖片 {os.path.basename(file_path)}:\n{str(e)}")
print(f"[DEBUG] 共添加 {added_count} 張圖片")
print(f"[DEBUG] 當前總共有 {len(self.images)} 張圖片")
debug_log(f"共添加 {added_count} 張圖片")
debug_log(f"當前總共有 {len(self.images)} 張圖片")
if added_count > 0:
self._refresh_preview()
self._update_status()
@ -432,16 +454,16 @@ class ImageUploadWidget(QWidget):
try:
if os.path.exists(file_path):
os.remove(file_path)
print(f"[DEBUG] 已刪除臨時文件: {file_path}")
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
print(f"[DEBUG] 刪除臨時文件失敗: {e}")
debug_log(f"刪除臨時文件失敗: {e}")
# 從內存中移除圖片數據
del self.images[image_id]
self._refresh_preview()
self._update_status()
self.images_changed.emit()
print(f"[DEBUG] 已移除圖片: {image_info['name']}")
debug_log(f"已移除圖片: {image_info['name']}")
def _update_status(self) -> None:
"""更新狀態標籤"""
@ -464,9 +486,9 @@ class ImageUploadWidget(QWidget):
self.status_label.setText(f"已選擇 {count} 張圖片 (總計 {size_str})")
# 詳細調試信息
print(f"[DEBUG] === 圖片狀態更新 ===")
print(f"[DEBUG] 圖片數量: {count}")
print(f"[DEBUG] 總大小: {total_size} bytes ({size_str})")
debug_log(f"=== 圖片狀態更新 ===")
debug_log(f"圖片數量: {count}")
debug_log(f"總大小: {total_size} bytes ({size_str})")
for i, (image_id, img) in enumerate(self.images.items(), 1):
data_size = len(img["data"]) if isinstance(img["data"], bytes) else 0
# 智能顯示每張圖片的大小
@ -476,8 +498,8 @@ class ImageUploadWidget(QWidget):
data_str = f"{data_size/1024:.1f} KB"
else:
data_str = f"{data_size/(1024*1024):.1f} MB"
print(f"[DEBUG] 圖片 {i}: {img['name']} - 數據大小: {data_str}")
print(f"[DEBUG] ==================")
debug_log(f"圖片 {i}: {img['name']} - 數據大小: {data_str}")
debug_log(f"==================")
def get_images_data(self) -> List[dict]:
"""獲取圖片數據"""
@ -552,11 +574,11 @@ class ImageUploadWidget(QWidget):
temp_file.unlink()
cleaned_count += 1
except Exception as e:
print(f"[DEBUG] 清理舊臨時文件失敗: {e}")
debug_log(f"清理舊臨時文件失敗: {e}")
if cleaned_count > 0:
print(f"[DEBUG] 清理了 {cleaned_count} 個舊的臨時文件")
debug_log(f"清理了 {cleaned_count} 個舊的臨時文件")
except Exception as e:
print(f"[DEBUG] 臨時文件清理過程出錯: {e}")
debug_log(f"臨時文件清理過程出錯: {e}")
# ===== 主要回饋介面 =====
@ -886,11 +908,11 @@ class FeedbackWindow(QMainWindow):
if os.path.exists(file_path):
os.remove(file_path)
temp_files_cleaned += 1
print(f"[DEBUG] 關閉時清理臨時文件: {file_path}")
debug_log(f"關閉時清理臨時文件: {file_path}")
except Exception as e:
print(f"[DEBUG] 關閉時清理臨時文件失敗: {e}")
debug_log(f"關閉時清理臨時文件失敗: {e}")
if temp_files_cleaned > 0:
print(f"[DEBUG] 視窗關閉時清理了 {temp_files_cleaned} 個臨時文件")
debug_log(f"視窗關閉時清理了 {temp_files_cleaned} 個臨時文件")
event.accept()
@ -933,6 +955,6 @@ if __name__ == "__main__":
# 測試用的主程式
result = feedback_ui(".", "測試摘要")
if result:
print("收到回饋:", result)
debug_log(f"收到回饋: {result}")
else:
print("用戶取消了回饋")
debug_log("用戶取消了回饋")

View File

@ -26,6 +26,59 @@ from mcp.server.fastmcp.utilities.types import Image as MCPImage
from mcp.types import TextContent
from pydantic import Field
# ===== 編碼初始化 =====
def init_encoding():
"""初始化編碼設置,確保正確處理中文字符"""
try:
# Windows 特殊處理
if sys.platform == 'win32':
import msvcrt
# 設置為二進制模式
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
# 重新包裝為 UTF-8 文本流,並禁用緩衝
sys.stdin = io.TextIOWrapper(
sys.stdin.detach(),
encoding='utf-8',
errors='replace',
newline=None
)
sys.stdout = io.TextIOWrapper(
sys.stdout.detach(),
encoding='utf-8',
errors='replace',
newline='',
write_through=True # 關鍵:禁用寫入緩衝
)
else:
# 非 Windows 系統的標準設置
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stdin, 'reconfigure'):
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
# 設置 stderr 編碼(用於調試訊息)
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
return True
except Exception as e:
# 如果編碼設置失敗,嘗試基本設置
try:
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stdin, 'reconfigure'):
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except:
pass
return False
# 初始化編碼(在導入時就執行)
_encoding_initialized = init_encoding()
# ===== 常數定義 =====
SERVER_NAME = "互動式回饋收集 MCP"
SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY']
@ -39,7 +92,25 @@ mcp = FastMCP(SERVER_NAME, version=__version__)
# ===== 工具函數 =====
def debug_log(message: str) -> None:
"""輸出調試訊息到標準錯誤,避免污染標準輸出"""
print(f"[DEBUG] {message}", file=sys.stderr)
# 只在啟用調試模式時才輸出,避免干擾 MCP 通信
if not os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"):
return
try:
# 確保消息是字符串類型
if not isinstance(message, str):
message = str(message)
# 安全地輸出到 stderr處理編碼問題
try:
print(f"[DEBUG] {message}", file=sys.stderr, flush=True)
except UnicodeEncodeError:
# 如果遇到編碼問題,使用 ASCII 安全模式
safe_message = message.encode('ascii', errors='replace').decode('ascii')
print(f"[DEBUG] {safe_message}", file=sys.stderr, flush=True)
except Exception:
# 最後的備用方案:靜默失敗,不影響主程序
pass
def is_remote_environment() -> bool:
@ -331,6 +402,10 @@ async def interactive_feedback(
3. 上傳圖片作為回饋
4. 查看 AI 的工作摘要
調試模式
- 設置環境變數 MCP_DEBUG=true 可啟用詳細調試輸出
- 生產環境建議關閉調試模式以避免輸出干擾
Args:
project_directory: 專案目錄路徑
summary: AI 工作完成的摘要說明
@ -453,10 +528,11 @@ async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> d
session_url = f"http://{manager.host}:{manager.port}/session/{session_id}"
debug_log(f"Web UI 已啟動: {session_url}")
try:
print(f"Web UI 已啟動: {session_url}")
except UnicodeEncodeError:
print(f"Web UI launched: {session_url}")
# 注意:不能使用 print() 污染 stdout會破壞 MCP 通信
# try:
# print(f"Web UI 已啟動: {session_url}")
# except UnicodeEncodeError:
# print(f"Web UI launched: {session_url}")
# 開啟瀏覽器
manager.open_browser(session_url)
@ -474,10 +550,11 @@ async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> d
except TimeoutError:
timeout_msg = f"等待用戶回饋超時({timeout} 秒)"
debug_log(f"{timeout_msg}")
try:
print(f"等待用戶回饋超時({timeout} 秒)")
except UnicodeEncodeError:
print(f"Feedback timeout ({timeout} seconds)")
# 注意:不能使用 print() 污染 stdout會破壞 MCP 通信
# try:
# print(f"等待用戶回饋超時({timeout} 秒)")
# except UnicodeEncodeError:
# print(f"Feedback timeout ({timeout} seconds)")
return {
"logs": "",
"interactive_feedback": f"回饋超時({timeout} 秒)",
@ -486,10 +563,11 @@ async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> d
except Exception as e:
error_msg = f"Web UI 錯誤: {e}"
debug_log(f"{error_msg}")
try:
print(f"Web UI 錯誤: {e}")
except UnicodeEncodeError:
print(f"Web UI error: {e}")
# 注意:不能使用 print() 污染 stdout會破壞 MCP 通信
# try:
# print(f"Web UI 錯誤: {e}")
# except UnicodeEncodeError:
# print(f"Web UI error: {e}")
return {
"logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
@ -532,60 +610,34 @@ def get_system_info() -> str:
# ===== 主程式入口 =====
def main():
"""主要入口點,用於套件執行"""
debug_log("🚀 啟動互動式回饋收集 MCP 服務器")
debug_log(f" 遠端環境: {is_remote_environment()}")
debug_log(f" GUI 可用: {can_use_gui()}")
debug_log(f" 建議介面: {'Web UI' if is_remote_environment() or not can_use_gui() else 'Qt GUI'}")
debug_log(" 等待來自 AI 助手的調用...")
# 檢查是否啟用調試模式
debug_enabled = os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")
# Windows 特殊處理:設置 stdio 為二進制模式,避免編碼問題
if sys.platform == 'win32':
debug_log("偵測到 Windows 環境,設置 stdio 二進制模式")
try:
# 設置 stdin/stdout 為二進制模式,避免 Windows 下的編碼問題
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
debug_log("Windows stdio 二進制模式設置成功")
# 重新包裝 stdin/stdout 為 UTF-8 編碼的文本流
sys.stdin = io.TextIOWrapper(sys.stdin.detach(), encoding='utf-8', errors='replace')
sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8', errors='replace', newline='')
debug_log("Windows stdio UTF-8 包裝設置成功")
except Exception as e:
debug_log(f"Windows stdio 設置失敗,使用預設模式: {e}")
else:
# 非 Windows 系統:確保使用 UTF-8 編碼
try:
if hasattr(sys.stdin, 'reconfigure'):
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
debug_log("非 Windows 系統 UTF-8 編碼設置成功")
except Exception as e:
debug_log(f"UTF-8 編碼設置失敗: {e}")
# 確保 stderr 使用 UTF-8 編碼(用於 debug 訊息)
if hasattr(sys.stderr, 'reconfigure'):
try:
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
debug_log("stderr UTF-8 編碼設置成功")
except Exception as e:
debug_log(f"stderr 編碼設置失敗: {e}")
# 強制 stdout 立即刷新,確保 JSON-RPC 消息及時發送
sys.stdout.reconfigure(line_buffering=True) if hasattr(sys.stdout, 'reconfigure') else None
if debug_enabled:
debug_log("🚀 啟動互動式回饋收集 MCP 服務器")
debug_log(f" 服務器名稱: {SERVER_NAME}")
debug_log(f" 版本: {__version__}")
debug_log(f" 平台: {sys.platform}")
debug_log(f" 編碼初始化: {'成功' if _encoding_initialized else '失敗'}")
debug_log(f" 遠端環境: {is_remote_environment()}")
debug_log(f" GUI 可用: {can_use_gui()}")
debug_log(f" 建議介面: {'Web UI' if is_remote_environment() or not can_use_gui() else 'Qt GUI'}")
debug_log(" 等待來自 AI 助手的調用...")
debug_log("準備啟動 MCP 伺服器...")
debug_log("調用 mcp.run()...")
try:
# 使用正確的 FastMCP API
mcp.run()
except KeyboardInterrupt:
debug_log("收到中斷信號,正常退出")
if debug_enabled:
debug_log("收到中斷信號,正常退出")
sys.exit(0)
except Exception as e:
debug_log(f"MCP 服務器啟動失敗: {e}")
import traceback
debug_log(f"詳細錯誤: {traceback.format_exc()}")
if debug_enabled:
debug_log(f"MCP 服務器啟動失敗: {e}")
import traceback
debug_log(f"詳細錯誤: {traceback.format_exc()}")
sys.exit(1)

View File

@ -120,7 +120,7 @@ class WebFeedbackSession:
# 檢查文件大小
if img["size"] > MAX_IMAGE_SIZE:
print(f"[DEBUG] 圖片 {img['name']} 超過大小限制,跳過")
debug_log(f"圖片 {img['name']} 超過大小限制,跳過")
continue
# 解碼 base64 數據
@ -128,13 +128,13 @@ class WebFeedbackSession:
try:
image_bytes = base64.b64decode(img["data"])
except Exception as e:
print(f"[DEBUG] 圖片 {img['name']} base64 解碼失敗: {e}")
debug_log(f"圖片 {img['name']} base64 解碼失敗: {e}")
continue
else:
image_bytes = img["data"]
if len(image_bytes) == 0:
print(f"[DEBUG] 圖片 {img['name']} 數據為空,跳過")
debug_log(f"圖片 {img['name']} 數據為空,跳過")
continue
processed_images.append({
@ -143,10 +143,10 @@ class WebFeedbackSession:
"size": len(image_bytes)
})
print(f"[DEBUG] 圖片 {img['name']} 處理成功,大小: {len(image_bytes)} bytes")
debug_log(f"圖片 {img['name']} 處理成功,大小: {len(image_bytes)} bytes")
except Exception as e:
print(f"[DEBUG] 圖片處理錯誤: {e}")
debug_log(f"圖片處理錯誤: {e}")
continue
return processed_images
@ -207,7 +207,7 @@ class WebFeedbackSession:
)
except Exception as e:
print(f"命令執行錯誤: {e}")
debug_log(f"命令執行錯誤: {e}")
finally:
self.process = None
@ -293,9 +293,9 @@ class WebUIManager:
await self.handle_websocket_message(session, data)
except WebSocketDisconnect:
print(f"WebSocket 斷開連接: {session_id}")
debug_log(f"WebSocket 斷開連接: {session_id}")
except Exception as e:
print(f"WebSocket 錯誤: {e}")
debug_log(f"WebSocket 錯誤: {e}")
finally:
session.websocket = None
@ -364,7 +364,7 @@ class WebUIManager:
try:
webbrowser.open(url)
except Exception as e:
print(f"無法開啟瀏覽器: {e}")
debug_log(f"無法開啟瀏覽器: {e}")
def _get_simple_index_html(self) -> str:
"""簡單的首頁 HTML"""
@ -446,7 +446,7 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
session_id = manager.create_session(project_directory, summary)
session_url = f"http://{manager.host}:{manager.port}/session/{session_id}"
print(f"🌐 Web UI 已啟動: {session_url}")
debug_log(f"🌐 Web UI 已啟動: {session_url}")
# 開啟瀏覽器
manager.open_browser(session_url)
@ -461,14 +461,14 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
return result
except TimeoutError:
print("⏰ 等待用戶回饋超時")
debug_log("⏰ 等待用戶回饋超時")
return {
"logs": "",
"interactive_feedback": "回饋超時",
"images": []
}
except Exception as e:
print(f"❌ Web UI 錯誤: {e}")
debug_log(f"❌ Web UI 錯誤: {e}")
return {
"logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
@ -507,7 +507,7 @@ if __name__ == "__main__":
session_id = manager.create_session(args.project_directory, args.summary)
session_url = f"http://{args.host}:{args.port}/session/{session_id}"
print(f"🌐 Web UI 已啟動: {session_url}")
debug_log(f"🌐 Web UI 已啟動: {session_url}")
manager.open_browser(session_url)
try:
@ -515,6 +515,19 @@ if __name__ == "__main__":
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("\n👋 Web UI 已停止")
debug_log("\n👋 Web UI 已停止")
asyncio.run(main())
# === 工具函數 ===
def debug_log(message: str) -> None:
"""輸出調試訊息到標準錯誤,避免污染標準輸出"""
# 只在啟用調試模式時才輸出,避免干擾 MCP 通信
if not os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"):
return
try:
print(f"[WEB_UI] {message}", file=sys.stderr, flush=True)
except Exception:
# 靜默失敗,不影響主程序
pass

742
uv.lock generated

File diff suppressed because it is too large Load Diff