mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
🔧 增強調試功能,新增 debug_log 函數以輸出調試訊息,並在多個模組中替換原有的 print 語句。更新伺服器初始化以確保編碼正確性,並在 Windows 環境下設置 stdio 為二進制模式。
This commit is contained in:
parent
86e55b89f6
commit
aa4cb3a136
@ -41,12 +41,8 @@ def main():
|
|||||||
elif args.command == 'version':
|
elif args.command == 'version':
|
||||||
show_version()
|
show_version()
|
||||||
elif args.command == 'server':
|
elif args.command == 'server':
|
||||||
# 明確指定伺服器命令
|
|
||||||
print("🚀 啟動 Interactive Feedback MCP Enhanced 伺服器...")
|
|
||||||
run_server()
|
run_server()
|
||||||
elif args.command is None:
|
elif args.command is None:
|
||||||
# 沒有指定命令,啟動伺服器(保持向後兼容)
|
|
||||||
print("🚀 啟動 Interactive Feedback MCP Enhanced 伺服器...")
|
|
||||||
run_server()
|
run_server()
|
||||||
else:
|
else:
|
||||||
# 不應該到達這裡
|
# 不應該到達這裡
|
||||||
|
@ -30,6 +30,29 @@ from PySide6.QtWidgets import (
|
|||||||
from PySide6.QtCore import Qt, Signal, QTimer
|
from PySide6.QtCore import Qt, Signal, QTimer
|
||||||
from PySide6.QtGui import QFont, QPixmap, QDragEnterEvent, QDropEvent, QKeySequence, QShortcut
|
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):
|
class FeedbackResult(TypedDict):
|
||||||
"""回饋結果的型別定義"""
|
"""回饋結果的型別定義"""
|
||||||
@ -265,9 +288,8 @@ class ImageUploadWidget(QWidget):
|
|||||||
new_height = int(image.height() * scale)
|
new_height = int(image.height() * scale)
|
||||||
|
|
||||||
# 縮放圖片
|
# 縮放圖片
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
image = image.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
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 # 降低質量以減小文件大小
|
quality = 70 # 降低質量以減小文件大小
|
||||||
@ -275,7 +297,7 @@ class ImageUploadWidget(QWidget):
|
|||||||
# 檢查保存後的文件大小
|
# 檢查保存後的文件大小
|
||||||
if temp_file.exists():
|
if temp_file.exists():
|
||||||
file_size = temp_file.stat().st_size
|
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 限制
|
if file_size > 1 * 1024 * 1024: # 1MB 限制
|
||||||
@ -319,34 +341,34 @@ class ImageUploadWidget(QWidget):
|
|||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
temp_files_cleaned += 1
|
temp_files_cleaned += 1
|
||||||
print(f"[DEBUG] 已刪除臨時文件: {file_path}")
|
debug_log(f"已刪除臨時文件: {file_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 刪除臨時文件失敗: {e}")
|
debug_log(f"刪除臨時文件失敗: {e}")
|
||||||
|
|
||||||
# 清除內存中的圖片數據
|
# 清除內存中的圖片數據
|
||||||
self.images.clear()
|
self.images.clear()
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.images_changed.emit()
|
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:
|
def _add_images(self, file_paths: List[str]) -> None:
|
||||||
"""添加圖片"""
|
"""添加圖片"""
|
||||||
added_count = 0
|
added_count = 0
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
try:
|
try:
|
||||||
print(f"[DEBUG] 嘗試添加圖片: {file_path}")
|
debug_log(f"嘗試添加圖片: {file_path}")
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
print(f"[DEBUG] 文件不存在: {file_path}")
|
debug_log(f"文件不存在: {file_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not self._is_image_file(file_path):
|
if not self._is_image_file(file_path):
|
||||||
print(f"[DEBUG] 不是圖片文件: {file_path}")
|
debug_log(f"不是圖片文件: {file_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_size = os.path.getsize(file_path)
|
file_size = os.path.getsize(file_path)
|
||||||
print(f"[DEBUG] 文件大小: {file_size} bytes")
|
debug_log(f"文件大小: {file_size} bytes")
|
||||||
|
|
||||||
# 更嚴格的大小限制(1MB)
|
# 更嚴格的大小限制(1MB)
|
||||||
if file_size > 1 * 1024 * 1024:
|
if file_size > 1 * 1024 * 1024:
|
||||||
@ -364,10 +386,10 @@ class ImageUploadWidget(QWidget):
|
|||||||
# 讀取圖片原始二進制數據
|
# 讀取圖片原始二進制數據
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
raw_data = f.read()
|
raw_data = f.read()
|
||||||
print(f"[DEBUG] 讀取原始數據大小: {len(raw_data)} bytes")
|
debug_log(f"讀取原始數據大小: {len(raw_data)} bytes")
|
||||||
|
|
||||||
if len(raw_data) == 0:
|
if len(raw_data) == 0:
|
||||||
print(f"[DEBUG] 讀取的數據為空!")
|
debug_log(f"讀取的數據為空!")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 再次檢查內存中的數據大小
|
# 再次檢查內存中的數據大小
|
||||||
@ -386,14 +408,14 @@ class ImageUploadWidget(QWidget):
|
|||||||
"size": file_size
|
"size": file_size
|
||||||
}
|
}
|
||||||
added_count += 1
|
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:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 添加圖片失敗: {e}")
|
debug_log(f"添加圖片失敗: {e}")
|
||||||
QMessageBox.warning(self, "錯誤", f"無法載入圖片 {os.path.basename(file_path)}:\n{str(e)}")
|
QMessageBox.warning(self, "錯誤", f"無法載入圖片 {os.path.basename(file_path)}:\n{str(e)}")
|
||||||
|
|
||||||
print(f"[DEBUG] 共添加 {added_count} 張圖片")
|
debug_log(f"共添加 {added_count} 張圖片")
|
||||||
print(f"[DEBUG] 當前總共有 {len(self.images)} 張圖片")
|
debug_log(f"當前總共有 {len(self.images)} 張圖片")
|
||||||
if added_count > 0:
|
if added_count > 0:
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
@ -432,16 +454,16 @@ class ImageUploadWidget(QWidget):
|
|||||||
try:
|
try:
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
print(f"[DEBUG] 已刪除臨時文件: {file_path}")
|
debug_log(f"已刪除臨時文件: {file_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 刪除臨時文件失敗: {e}")
|
debug_log(f"刪除臨時文件失敗: {e}")
|
||||||
|
|
||||||
# 從內存中移除圖片數據
|
# 從內存中移除圖片數據
|
||||||
del self.images[image_id]
|
del self.images[image_id]
|
||||||
self._refresh_preview()
|
self._refresh_preview()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
self.images_changed.emit()
|
self.images_changed.emit()
|
||||||
print(f"[DEBUG] 已移除圖片: {image_info['name']}")
|
debug_log(f"已移除圖片: {image_info['name']}")
|
||||||
|
|
||||||
def _update_status(self) -> None:
|
def _update_status(self) -> None:
|
||||||
"""更新狀態標籤"""
|
"""更新狀態標籤"""
|
||||||
@ -464,9 +486,9 @@ class ImageUploadWidget(QWidget):
|
|||||||
self.status_label.setText(f"已選擇 {count} 張圖片 (總計 {size_str})")
|
self.status_label.setText(f"已選擇 {count} 張圖片 (總計 {size_str})")
|
||||||
|
|
||||||
# 詳細調試信息
|
# 詳細調試信息
|
||||||
print(f"[DEBUG] === 圖片狀態更新 ===")
|
debug_log(f"=== 圖片狀態更新 ===")
|
||||||
print(f"[DEBUG] 圖片數量: {count}")
|
debug_log(f"圖片數量: {count}")
|
||||||
print(f"[DEBUG] 總大小: {total_size} bytes ({size_str})")
|
debug_log(f"總大小: {total_size} bytes ({size_str})")
|
||||||
for i, (image_id, img) in enumerate(self.images.items(), 1):
|
for i, (image_id, img) in enumerate(self.images.items(), 1):
|
||||||
data_size = len(img["data"]) if isinstance(img["data"], bytes) else 0
|
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"
|
data_str = f"{data_size/1024:.1f} KB"
|
||||||
else:
|
else:
|
||||||
data_str = f"{data_size/(1024*1024):.1f} MB"
|
data_str = f"{data_size/(1024*1024):.1f} MB"
|
||||||
print(f"[DEBUG] 圖片 {i}: {img['name']} - 數據大小: {data_str}")
|
debug_log(f"圖片 {i}: {img['name']} - 數據大小: {data_str}")
|
||||||
print(f"[DEBUG] ==================")
|
debug_log(f"==================")
|
||||||
|
|
||||||
def get_images_data(self) -> List[dict]:
|
def get_images_data(self) -> List[dict]:
|
||||||
"""獲取圖片數據"""
|
"""獲取圖片數據"""
|
||||||
@ -552,11 +574,11 @@ class ImageUploadWidget(QWidget):
|
|||||||
temp_file.unlink()
|
temp_file.unlink()
|
||||||
cleaned_count += 1
|
cleaned_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 清理舊臨時文件失敗: {e}")
|
debug_log(f"清理舊臨時文件失敗: {e}")
|
||||||
if cleaned_count > 0:
|
if cleaned_count > 0:
|
||||||
print(f"[DEBUG] 清理了 {cleaned_count} 個舊的臨時文件")
|
debug_log(f"清理了 {cleaned_count} 個舊的臨時文件")
|
||||||
except Exception as e:
|
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):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
temp_files_cleaned += 1
|
temp_files_cleaned += 1
|
||||||
print(f"[DEBUG] 關閉時清理臨時文件: {file_path}")
|
debug_log(f"關閉時清理臨時文件: {file_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 關閉時清理臨時文件失敗: {e}")
|
debug_log(f"關閉時清理臨時文件失敗: {e}")
|
||||||
if temp_files_cleaned > 0:
|
if temp_files_cleaned > 0:
|
||||||
print(f"[DEBUG] 視窗關閉時清理了 {temp_files_cleaned} 個臨時文件")
|
debug_log(f"視窗關閉時清理了 {temp_files_cleaned} 個臨時文件")
|
||||||
|
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
@ -933,6 +955,6 @@ if __name__ == "__main__":
|
|||||||
# 測試用的主程式
|
# 測試用的主程式
|
||||||
result = feedback_ui(".", "測試摘要")
|
result = feedback_ui(".", "測試摘要")
|
||||||
if result:
|
if result:
|
||||||
print("收到回饋:", result)
|
debug_log(f"收到回饋: {result}")
|
||||||
else:
|
else:
|
||||||
print("用戶取消了回饋")
|
debug_log("用戶取消了回饋")
|
@ -26,6 +26,59 @@ from mcp.server.fastmcp.utilities.types import Image as MCPImage
|
|||||||
from mcp.types import TextContent
|
from mcp.types import TextContent
|
||||||
from pydantic import Field
|
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"
|
SERVER_NAME = "互動式回饋收集 MCP"
|
||||||
SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY']
|
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:
|
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:
|
def is_remote_environment() -> bool:
|
||||||
@ -331,6 +402,10 @@ async def interactive_feedback(
|
|||||||
3. 上傳圖片作為回饋
|
3. 上傳圖片作為回饋
|
||||||
4. 查看 AI 的工作摘要
|
4. 查看 AI 的工作摘要
|
||||||
|
|
||||||
|
調試模式:
|
||||||
|
- 設置環境變數 MCP_DEBUG=true 可啟用詳細調試輸出
|
||||||
|
- 生產環境建議關閉調試模式以避免輸出干擾
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_directory: 專案目錄路徑
|
project_directory: 專案目錄路徑
|
||||||
summary: AI 工作完成的摘要說明
|
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}"
|
session_url = f"http://{manager.host}:{manager.port}/session/{session_id}"
|
||||||
|
|
||||||
debug_log(f"Web UI 已啟動: {session_url}")
|
debug_log(f"Web UI 已啟動: {session_url}")
|
||||||
try:
|
# 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
|
||||||
print(f"Web UI 已啟動: {session_url}")
|
# try:
|
||||||
except UnicodeEncodeError:
|
# print(f"Web UI 已啟動: {session_url}")
|
||||||
print(f"Web UI launched: {session_url}")
|
# except UnicodeEncodeError:
|
||||||
|
# print(f"Web UI launched: {session_url}")
|
||||||
|
|
||||||
# 開啟瀏覽器
|
# 開啟瀏覽器
|
||||||
manager.open_browser(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:
|
except TimeoutError:
|
||||||
timeout_msg = f"等待用戶回饋超時({timeout} 秒)"
|
timeout_msg = f"等待用戶回饋超時({timeout} 秒)"
|
||||||
debug_log(f"⏰ {timeout_msg}")
|
debug_log(f"⏰ {timeout_msg}")
|
||||||
try:
|
# 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
|
||||||
print(f"等待用戶回饋超時({timeout} 秒)")
|
# try:
|
||||||
except UnicodeEncodeError:
|
# print(f"等待用戶回饋超時({timeout} 秒)")
|
||||||
print(f"Feedback timeout ({timeout} seconds)")
|
# except UnicodeEncodeError:
|
||||||
|
# print(f"Feedback timeout ({timeout} seconds)")
|
||||||
return {
|
return {
|
||||||
"logs": "",
|
"logs": "",
|
||||||
"interactive_feedback": f"回饋超時({timeout} 秒)",
|
"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:
|
except Exception as e:
|
||||||
error_msg = f"Web UI 錯誤: {e}"
|
error_msg = f"Web UI 錯誤: {e}"
|
||||||
debug_log(f"❌ {error_msg}")
|
debug_log(f"❌ {error_msg}")
|
||||||
try:
|
# 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
|
||||||
print(f"Web UI 錯誤: {e}")
|
# try:
|
||||||
except UnicodeEncodeError:
|
# print(f"Web UI 錯誤: {e}")
|
||||||
print(f"Web UI error: {e}")
|
# except UnicodeEncodeError:
|
||||||
|
# print(f"Web UI error: {e}")
|
||||||
return {
|
return {
|
||||||
"logs": "",
|
"logs": "",
|
||||||
"interactive_feedback": f"錯誤: {str(e)}",
|
"interactive_feedback": f"錯誤: {str(e)}",
|
||||||
@ -532,60 +610,34 @@ def get_system_info() -> str:
|
|||||||
# ===== 主程式入口 =====
|
# ===== 主程式入口 =====
|
||||||
def main():
|
def main():
|
||||||
"""主要入口點,用於套件執行"""
|
"""主要入口點,用於套件執行"""
|
||||||
debug_log("🚀 啟動互動式回饋收集 MCP 服務器")
|
# 檢查是否啟用調試模式
|
||||||
debug_log(f" 遠端環境: {is_remote_environment()}")
|
debug_enabled = os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")
|
||||||
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 助手的調用...")
|
|
||||||
|
|
||||||
# Windows 特殊處理:設置 stdio 為二進制模式,避免編碼問題
|
if debug_enabled:
|
||||||
if sys.platform == 'win32':
|
debug_log("🚀 啟動互動式回饋收集 MCP 服務器")
|
||||||
debug_log("偵測到 Windows 環境,設置 stdio 二進制模式")
|
debug_log(f" 服務器名稱: {SERVER_NAME}")
|
||||||
try:
|
debug_log(f" 版本: {__version__}")
|
||||||
# 設置 stdin/stdout 為二進制模式,避免 Windows 下的編碼問題
|
debug_log(f" 平台: {sys.platform}")
|
||||||
import msvcrt
|
debug_log(f" 編碼初始化: {'成功' if _encoding_initialized else '失敗'}")
|
||||||
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
debug_log(f" 遠端環境: {is_remote_environment()}")
|
||||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
debug_log(f" GUI 可用: {can_use_gui()}")
|
||||||
debug_log("Windows stdio 二進制模式設置成功")
|
debug_log(f" 建議介面: {'Web UI' if is_remote_environment() or not can_use_gui() else 'Qt GUI'}")
|
||||||
|
debug_log(" 等待來自 AI 助手的調用...")
|
||||||
# 重新包裝 stdin/stdout 為 UTF-8 編碼的文本流
|
debug_log("準備啟動 MCP 伺服器...")
|
||||||
sys.stdin = io.TextIOWrapper(sys.stdin.detach(), encoding='utf-8', errors='replace')
|
debug_log("調用 mcp.run()...")
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 使用正確的 FastMCP API
|
||||||
mcp.run()
|
mcp.run()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
debug_log("收到中斷信號,正常退出")
|
if debug_enabled:
|
||||||
|
debug_log("收到中斷信號,正常退出")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"MCP 服務器啟動失敗: {e}")
|
if debug_enabled:
|
||||||
import traceback
|
debug_log(f"MCP 服務器啟動失敗: {e}")
|
||||||
debug_log(f"詳細錯誤: {traceback.format_exc()}")
|
import traceback
|
||||||
|
debug_log(f"詳細錯誤: {traceback.format_exc()}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ class WebFeedbackSession:
|
|||||||
|
|
||||||
# 檢查文件大小
|
# 檢查文件大小
|
||||||
if img["size"] > MAX_IMAGE_SIZE:
|
if img["size"] > MAX_IMAGE_SIZE:
|
||||||
print(f"[DEBUG] 圖片 {img['name']} 超過大小限制,跳過")
|
debug_log(f"圖片 {img['name']} 超過大小限制,跳過")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 解碼 base64 數據
|
# 解碼 base64 數據
|
||||||
@ -128,13 +128,13 @@ class WebFeedbackSession:
|
|||||||
try:
|
try:
|
||||||
image_bytes = base64.b64decode(img["data"])
|
image_bytes = base64.b64decode(img["data"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 圖片 {img['name']} base64 解碼失敗: {e}")
|
debug_log(f"圖片 {img['name']} base64 解碼失敗: {e}")
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
image_bytes = img["data"]
|
image_bytes = img["data"]
|
||||||
|
|
||||||
if len(image_bytes) == 0:
|
if len(image_bytes) == 0:
|
||||||
print(f"[DEBUG] 圖片 {img['name']} 數據為空,跳過")
|
debug_log(f"圖片 {img['name']} 數據為空,跳過")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
processed_images.append({
|
processed_images.append({
|
||||||
@ -143,10 +143,10 @@ class WebFeedbackSession:
|
|||||||
"size": len(image_bytes)
|
"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:
|
except Exception as e:
|
||||||
print(f"[DEBUG] 圖片處理錯誤: {e}")
|
debug_log(f"圖片處理錯誤: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return processed_images
|
return processed_images
|
||||||
@ -207,7 +207,7 @@ class WebFeedbackSession:
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"命令執行錯誤: {e}")
|
debug_log(f"命令執行錯誤: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
@ -293,9 +293,9 @@ class WebUIManager:
|
|||||||
await self.handle_websocket_message(session, data)
|
await self.handle_websocket_message(session, data)
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
print(f"WebSocket 斷開連接: {session_id}")
|
debug_log(f"WebSocket 斷開連接: {session_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"WebSocket 錯誤: {e}")
|
debug_log(f"WebSocket 錯誤: {e}")
|
||||||
finally:
|
finally:
|
||||||
session.websocket = None
|
session.websocket = None
|
||||||
|
|
||||||
@ -364,7 +364,7 @@ class WebUIManager:
|
|||||||
try:
|
try:
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"無法開啟瀏覽器: {e}")
|
debug_log(f"無法開啟瀏覽器: {e}")
|
||||||
|
|
||||||
def _get_simple_index_html(self) -> str:
|
def _get_simple_index_html(self) -> str:
|
||||||
"""簡單的首頁 HTML"""
|
"""簡單的首頁 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_id = manager.create_session(project_directory, summary)
|
||||||
session_url = f"http://{manager.host}:{manager.port}/session/{session_id}"
|
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)
|
manager.open_browser(session_url)
|
||||||
@ -461,14 +461,14 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
print("⏰ 等待用戶回饋超時")
|
debug_log("⏰ 等待用戶回饋超時")
|
||||||
return {
|
return {
|
||||||
"logs": "",
|
"logs": "",
|
||||||
"interactive_feedback": "回饋超時",
|
"interactive_feedback": "回饋超時",
|
||||||
"images": []
|
"images": []
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Web UI 錯誤: {e}")
|
debug_log(f"❌ Web UI 錯誤: {e}")
|
||||||
return {
|
return {
|
||||||
"logs": "",
|
"logs": "",
|
||||||
"interactive_feedback": f"錯誤: {str(e)}",
|
"interactive_feedback": f"錯誤: {str(e)}",
|
||||||
@ -507,7 +507,7 @@ if __name__ == "__main__":
|
|||||||
session_id = manager.create_session(args.project_directory, args.summary)
|
session_id = manager.create_session(args.project_directory, args.summary)
|
||||||
session_url = f"http://{args.host}:{args.port}/session/{session_id}"
|
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)
|
manager.open_browser(session_url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -515,6 +515,19 @@ if __name__ == "__main__":
|
|||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n👋 Web UI 已停止")
|
debug_log("\n👋 Web UI 已停止")
|
||||||
|
|
||||||
asyncio.run(main())
|
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
|
Loading…
x
Reference in New Issue
Block a user