更新測試用例,新增對 timeout 和 force_web_ui 參數的測試,並改善環境檢測功能的輸出信息。重構 Web UI 以支援圖片上傳和回饋提交,提升用戶體驗。

This commit is contained in:
Minidoracat 2025-05-31 02:02:51 +08:00
parent 918428dd45
commit 4bce2c30f2
3 changed files with 1265 additions and 446 deletions

View File

@ -3,243 +3,825 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Feedback MCP</title> <title>互動式回饋收集 - Interactive Feedback MCP</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<style>
/* ===== 基礎樣式 ===== */
:root {
--bg-primary: #2b2b2b;
--bg-secondary: #2d2d30;
--bg-tertiary: #1e1e1e;
--surface-color: #2d2d30;
--surface-hover: #383838;
--border-color: #464647;
--text-primary: #ffffff;
--text-secondary: #9e9e9e;
--primary-color: #007acc;
--primary-hover: #005a9e;
--success-color: #4caf50;
--success-hover: #45a049;
--error-color: #f44336;
--error-hover: #d32f2f;
--console-bg: #1e1e1e;
--button-bg: #0e639c;
--button-hover-bg: #005a9e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ===== 標題樣式 ===== */
.header {
background: linear-gradient(135deg, var(--surface-color), var(--surface-hover));
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.header h1 {
color: var(--primary-color);
font-size: 24px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.header .project-info {
color: var(--text-secondary);
font-size: 14px;
}
/* ===== AI 工作摘要 ===== */
.summary-section {
background: linear-gradient(135deg, var(--surface-color), var(--surface-hover));
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
flex: 1;
min-height: 150px;
max-height: 250px;
display: flex;
flex-direction: column;
}
.summary-section h2 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.summary-content {
background: var(--console-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
font-size: 14px;
line-height: 1.6;
overflow-y: auto;
flex: 1;
}
/* ===== 分頁標籤 ===== */
.tabs-container {
flex: 3;
display: flex;
flex-direction: column;
}
.tab-buttons {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab-button {
background: var(--surface-color);
border: none;
padding: 12px 24px;
color: var(--text-secondary);
cursor: pointer;
border-radius: 8px 8px 0 0;
margin-right: 4px;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
}
.tab-button.active {
background: var(--primary-color);
color: white;
}
.tab-button:hover:not(.active) {
background: var(--surface-hover);
color: var(--text-primary);
}
.tab-content {
display: none;
flex: 1;
display: flex;
flex-direction: column;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== 回饋分頁樣式 ===== */
.feedback-section {
flex: 2;
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
background: var(--surface-color);
}
.feedback-section h3 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.feedback-textarea {
width: 100%;
min-height: 150px;
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
color: var(--text-primary);
font-size: 14px;
resize: vertical;
font-family: inherit;
}
.feedback-textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
}
.feedback-textarea::placeholder {
color: var(--text-secondary);
}
/* ===== 圖片上傳區域 ===== */
.image-section {
flex: 1;
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
background: var(--surface-color);
min-height: 200px;
max-height: 400px;
display: flex;
flex-direction: column;
}
.image-section h3 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.upload-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.upload-btn {
background: var(--button-bg);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 12px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.upload-btn:hover {
background: var(--button-hover-bg);
transform: translateY(-1px);
}
.upload-btn.success {
background: var(--success-color);
}
.upload-btn.success:hover {
background: var(--success-hover);
}
.upload-btn.danger {
background: var(--error-color);
}
.upload-btn.danger:hover {
background: var(--error-hover);
}
.drop-zone {
border: 2px dashed var(--border-color);
border-radius: 8px;
padding: 20px;
text-align: center;
background: var(--surface-color);
transition: all 0.3s ease;
margin-bottom: 15px;
color: var(--text-secondary);
font-size: 12px;
}
.drop-zone:hover,
.drop-zone.drag-over {
border-color: var(--primary-color);
background: var(--surface-hover);
color: var(--primary-color);
}
.image-preview-area {
flex: 1;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
background: var(--bg-tertiary);
display: flex;
flex-wrap: wrap;
gap: 8px;
align-content: flex-start;
}
.image-preview {
position: relative;
width: 80px;
height: 80px;
border: 2px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
background: var(--surface-color);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-preview .remove-btn {
position: absolute;
top: 2px;
right: 2px;
background: var(--error-color);
color: white;
border: none;
border-radius: 50%;
width: 18px;
height: 18px;
cursor: pointer;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.image-preview .remove-btn:hover {
background: var(--error-hover);
}
.image-status {
color: var(--text-secondary);
font-size: 11px;
margin-top: 10px;
}
/* ===== 命令分頁樣式 ===== */
.command-section {
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 20px;
background: var(--surface-color);
flex: 1;
display: flex;
flex-direction: column;
}
.command-section h3 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 16px;
display: flex;
align-items: center;
gap: 10px;
}
.command-input-area {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.command-input {
flex: 1;
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
color: var(--text-primary);
font-size: 14px;
}
.command-input:focus {
outline: none;
border-color: var(--primary-color);
}
.run-btn {
background: var(--success-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
}
.run-btn:hover {
background: var(--success-hover);
}
.command-output {
flex: 1;
background: var(--console-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #ffffff;
overflow-y: auto;
white-space: pre-wrap;
min-height: 200px;
}
/* ===== 操作按鈕 ===== */
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 20px 0;
}
.action-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-btn {
background: var(--success-color);
color: white;
}
.submit-btn:hover {
background: var(--success-hover);
transform: translateY(-2px);
}
.cancel-btn {
background: var(--error-color);
color: white;
}
.cancel-btn:hover {
background: var(--error-hover);
transform: translateY(-2px);
}
/* ===== 響應式設計 ===== */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.tab-buttons {
flex-wrap: wrap;
}
.upload-buttons {
flex-wrap: wrap;
}
.action-buttons {
flex-direction: column;
}
}
/* ===== 隱藏檔案輸入 ===== */
#fileInput {
display: none;
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>Interactive Feedback MCP</h1> <!-- 標題 -->
<div class="header">
<div class="session-info"> <h1>🎯 互動式回饋收集</h1>
<h2>專案資訊</h2> <div class="project-info">專案目錄: {{ project_directory }}</div>
<p><strong>工作目錄:</strong> <span id="project-dir">{{ project_directory }}</span></p>
<p><strong>任務描述:</strong> <span id="summary">{{ summary }}</span></p>
</div> </div>
<div class="section"> <!-- AI 工作摘要 -->
<button id="toggle-command" class="toggle-btn">顯示命令區塊</button> <div class="summary-section">
<h2>📋 AI 工作摘要</h2>
<div class="summary-content">{{ summary }}</div>
</div> </div>
<div id="command-section" class="section command-section" style="display: none;"> <!-- 分頁容器 -->
<h3>命令執行</h3> <div class="tabs-container">
<!-- 分頁按鈕 -->
<div class="input-group"> <div class="tab-buttons">
<input type="text" id="command-input" placeholder="輸入要執行的命令..."> <button class="tab-button active" onclick="switchTab('feedback')">💬 回饋</button>
<button id="run-btn">執行</button> <button class="tab-button" onclick="switchTab('command')">⚡ 命令</button>
<button id="stop-btn" style="display: none;">停止</button>
</div> </div>
<div class="checkbox-group"> <!-- 回饋分頁 -->
<label> <div id="feedback-tab" class="tab-content active">
<input type="checkbox" id="auto-execute"> 下次自動執行 <div class="feedback-section">
</label> <h3>💬 您的回饋</h3>
<button id="save-config">儲存設定</button> <textarea id="feedbackText" class="feedback-textarea"
placeholder="請在這裡輸入您的回饋、建議或問題...&#10;&#10;💡 小提示:按 Ctrl+Enter 可快速提交回饋"></textarea>
</div> </div>
<div class="console-section"> <div class="image-section">
<div class="console-header"> <h3>🖼️ 圖片附件(可選)</h3>
<h4>控制台輸出</h4> <div class="upload-buttons">
<button id="clear-logs">清除</button> <button class="upload-btn" onclick="selectFiles()">📁 選擇文件</button>
<button class="upload-btn success" onclick="pasteFromClipboard()">📋 剪貼板</button>
<button class="upload-btn danger" onclick="clearAllImages()">❌ 清除</button>
</div> </div>
<div id="console" class="console"></div> <div class="drop-zone" id="dropZone">
🎯 拖拽圖片到這裡 (PNG、JPG、JPEG、GIF、BMP、WebP)
</div>
<div class="image-preview-area" id="imagePreviewArea"></div>
<div class="image-status" id="imageStatus">已選擇 0 張圖片</div>
</div> </div>
</div> </div>
<div class="section feedback-section"> <!-- 命令分頁 -->
<h3>回饋意見</h3> <div id="command-tab" class="tab-content">
<p class="feedback-description">{{ summary }}</p> <div class="command-section">
<textarea id="feedback-input" placeholder="請在此輸入您的回饋意見... (Ctrl+Enter 提交)"></textarea> <h3>⚡ 命令執行</h3>
<button id="submit-feedback">提交回饋 (Ctrl+Enter)</button> <div class="command-input-area">
<input type="text" id="commandInput" class="command-input"
placeholder="輸入要執行的命令..."
onkeypress="if(event.key==='Enter') runCommand()">
<button class="run-btn" onclick="runCommand()">▶️ 執行</button>
</div>
<div id="commandOutput" class="command-output"></div>
</div> </div>
<div class="footer">
<p>需要改進?聯繫 Fábio Ferreira 在 <a href="https://x.com/fabiomlferreira" target="_blank">X.com</a> 或訪問 <a href="https://dotcursorrules.com/" target="_blank">dotcursorrules.com</a></p>
</div> </div>
</div> </div>
<div id="loading" class="loading" style="display: none;"> <!-- 操作按鈕 -->
<div class="spinner"></div> <div class="action-buttons">
<p>正在處理...</p> <button class="action-btn cancel-btn" onclick="cancelFeedback()">❌ 取消</button>
<button class="action-btn submit-btn" onclick="submitFeedback()">✅ 提交回饋</button>
</div> </div>
</div>
<!-- 隱藏的檔案輸入 -->
<input type="file" id="fileInput" multiple accept="image/*" onchange="handleFileSelect(event)">
<script> <script>
const sessionId = "{{ session_id }}"; // ===== 全域變數 =====
const wsUrl = `ws://${window.location.host}/ws/${sessionId}`; let ws = null;
let socket; let images = [];
let commandRunning = false; let commandRunning = false;
// DOM elements // ===== WebSocket 連接 =====
const toggleCommandBtn = document.getElementById('toggle-command'); function connectWebSocket() {
const commandSection = document.getElementById('command-section'); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const commandInput = document.getElementById('command-input'); const wsUrl = `${protocol}//${window.location.host}/ws/{{ session_id }}`;
const runBtn = document.getElementById('run-btn');
const stopBtn = document.getElementById('stop-btn');
const autoExecuteCheck = document.getElementById('auto-execute');
const saveConfigBtn = document.getElementById('save-config');
const console = document.getElementById('console');
const clearLogsBtn = document.getElementById('clear-logs');
const feedbackInput = document.getElementById('feedback-input');
const submitFeedbackBtn = document.getElementById('submit-feedback');
const loading = document.getElementById('loading');
// Initialize WebSocket connection ws = new WebSocket(wsUrl);
function initWebSocket() {
socket = new WebSocket(wsUrl);
socket.onopen = function() { ws.onopen = function() {
console.log('WebSocket 已連接'); console.log('WebSocket 連接成功');
}; };
socket.onmessage = function(event) { ws.onmessage = function(event) {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
handleWebSocketMessage(data); handleWebSocketMessage(data);
}; };
socket.onclose = function() { ws.onclose = function() {
console.log('WebSocket 連接已關閉'); console.log('WebSocket 連接已關閉');
setTimeout(initWebSocket, 3000); // Reconnect after 3 seconds
}; };
socket.onerror = function(error) { ws.onerror = function(error) {
console.error('WebSocket 錯誤:', error); console.error('WebSocket 錯誤:', error);
}; };
} }
function handleWebSocketMessage(data) { function handleWebSocketMessage(data) {
switch(data.type) { if (data.type === 'command_output') {
case 'init': appendCommandOutput(data.output);
// Set initial values } else if (data.type === 'command_finished') {
commandInput.value = data.config.run_command || ''; appendCommandOutput(`\n進程結束返回碼: ${data.exit_code}\n`);
autoExecuteCheck.checked = data.config.execute_automatically || false;
// Load existing logs
data.logs.forEach(log => {
appendToConsole(log);
});
break;
case 'log':
appendToConsole(data.data);
break;
case 'process_completed':
commandRunning = false; commandRunning = false;
updateRunButtonState();
feedbackInput.focus();
break;
case 'logs_cleared':
console.innerHTML = '';
break;
case 'feedback_submitted':
showLoading();
setTimeout(() => {
window.close();
}, 2000);
break;
} }
} }
function appendToConsole(text) { // ===== 分頁切換 =====
const line = document.createElement('div'); function switchTab(tabName) {
line.className = 'console-line'; // 隱藏所有分頁
line.textContent = text; document.querySelectorAll('.tab-content').forEach(tab => {
console.appendChild(line); tab.classList.remove('active');
console.scrollTop = console.scrollHeight;
}
function updateRunButtonState() {
if (commandRunning) {
runBtn.style.display = 'none';
stopBtn.style.display = 'inline-block';
} else {
runBtn.style.display = 'inline-block';
stopBtn.style.display = 'none';
}
}
function showLoading() {
loading.style.display = 'flex';
}
function hideLoading() {
loading.style.display = 'none';
}
// Event listeners
toggleCommandBtn.addEventListener('click', () => {
const isVisible = commandSection.style.display !== 'none';
commandSection.style.display = isVisible ? 'none' : 'block';
toggleCommandBtn.textContent = isVisible ? '顯示命令區塊' : '隱藏命令區塊';
}); });
runBtn.addEventListener('click', () => { // 移除所有按鈕的活動狀態
const command = commandInput.value.trim(); document.querySelectorAll('.tab-button').forEach(btn => {
if (command) { btn.classList.remove('active');
commandRunning = true; });
updateRunButtonState();
socket.send(JSON.stringify({ // 顯示選中的分頁
document.getElementById(tabName + '-tab').classList.add('active');
// 設置按鈕活動狀態
event.target.classList.add('active');
}
// ===== 圖片處理 =====
function selectFiles() {
document.getElementById('fileInput').click();
}
function handleFileSelect(event) {
const files = Array.from(event.target.files);
processFiles(files);
}
async function pasteFromClipboard() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
const file = new File([blob], `clipboard_${Date.now()}.png`, { type });
processFiles([file]);
return;
}
}
}
alert('剪貼板中沒有圖片!');
} catch (error) {
console.error('剪貼板讀取失敗:', error);
alert('無法從剪貼板讀取圖片');
}
}
function processFiles(files) {
for (const file of files) {
if (!file.type.startsWith('image/')) {
alert(`檔案 ${file.name} 不是圖片格式!`);
continue;
}
if (file.size > 1024 * 1024) { // 1MB 限制
alert(`圖片 ${file.name} 大小超過 1MB 限制!`);
continue;
}
const reader = new FileReader();
reader.onload = function(e) {
const imageData = {
id: Date.now() + Math.random(),
name: file.name,
size: file.size,
type: file.type,
data: e.target.result.split(',')[1] // 移除 data:image/xxx;base64, 前綴
};
images.push(imageData);
updateImagePreview();
updateImageStatus();
};
reader.readAsDataURL(file);
}
}
function updateImagePreview() {
const previewArea = document.getElementById('imagePreviewArea');
previewArea.innerHTML = '';
images.forEach(img => {
const preview = document.createElement('div');
preview.className = 'image-preview';
preview.innerHTML = `
<img src="data:${img.type};base64,${img.data}" alt="${img.name}" title="${img.name}">
<button class="remove-btn" onclick="removeImage('${img.id}')" title="刪除圖片">×</button>
`;
previewArea.appendChild(preview);
});
}
function removeImage(imageId) {
images = images.filter(img => img.id != imageId);
updateImagePreview();
updateImageStatus();
}
function clearAllImages() {
if (images.length > 0) {
if (confirm(`確定要清除所有 ${images.length} 張圖片嗎?`)) {
images = [];
updateImagePreview();
updateImageStatus();
}
}
}
function updateImageStatus() {
const count = images.length;
const totalSize = images.reduce((sum, img) => sum + img.size, 0);
let sizeStr;
if (totalSize < 1024) {
sizeStr = `${totalSize} B`;
} else if (totalSize < 1024 * 1024) {
sizeStr = `${(totalSize / 1024).toFixed(1)} KB`;
} else {
sizeStr = `${(totalSize / (1024 * 1024)).toFixed(1)} MB`;
}
document.getElementById('imageStatus').textContent =
count > 0 ? `已選擇 ${count} 張圖片 (總計 ${sizeStr})` : '已選擇 0 張圖片';
}
// ===== 拖拽功能 =====
function setupDragAndDrop() {
const dropZone = document.getElementById('dropZone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropZone.classList.add('drag-over');
}
function unhighlight() {
dropZone.classList.remove('drag-over');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = Array.from(dt.files);
processFiles(files);
}
}
// ===== 命令執行 =====
function runCommand() {
const command = document.getElementById('commandInput').value.trim();
if (!command) return;
if (commandRunning) {
alert('已有命令在執行中,請等待完成或停止當前命令');
return;
}
appendCommandOutput(`$ ${command}\n`);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'run_command', type: 'run_command',
command: command command: command
})); }));
commandRunning = true;
} else {
appendCommandOutput('WebSocket 連接未建立\n');
} }
});
stopBtn.addEventListener('click', () => {
commandRunning = false;
updateRunButtonState();
socket.send(JSON.stringify({
type: 'stop_command'
}));
});
commandInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
runBtn.click();
} }
});
saveConfigBtn.addEventListener('click', () => { function appendCommandOutput(text) {
socket.send(JSON.stringify({ const output = document.getElementById('commandOutput');
type: 'update_config', output.textContent += text;
config: { output.scrollTop = output.scrollHeight;
run_command: commandInput.value,
execute_automatically: autoExecuteCheck.checked
} }
}));
// Visual feedback // ===== 回饋提交 =====
saveConfigBtn.textContent = '已儲存'; function submitFeedback() {
setTimeout(() => { const feedback = document.getElementById('feedbackText').value.trim();
saveConfigBtn.textContent = '儲存設定';
}, 1500);
});
clearLogsBtn.addEventListener('click', () => { if (!feedback && images.length === 0) {
socket.send(JSON.stringify({ alert('請輸入回饋內容或上傳圖片!');
type: 'clear_logs' return;
})); }
});
submitFeedbackBtn.addEventListener('click', () => { if (ws && ws.readyState === WebSocket.OPEN) {
const feedback = feedbackInput.value.trim(); ws.send(JSON.stringify({
socket.send(JSON.stringify({
type: 'submit_feedback', type: 'submit_feedback',
feedback: feedback feedback: feedback,
images: images
})); }));
});
feedbackInput.addEventListener('keydown', (e) => { alert('回饋已提交!感謝您的回饋。');
window.close();
} else {
alert('WebSocket 連接異常,請重新整理頁面');
}
}
function cancelFeedback() {
if (confirm('確定要取消回饋嗎?')) {
window.close();
}
}
// ===== 快捷鍵支援 =====
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') { if (e.ctrlKey && e.key === 'Enter') {
submitFeedbackBtn.click(); e.preventDefault();
submitFeedback();
} }
}); });
// Initialize // ===== 初始化 =====
initWebSocket(); document.addEventListener('DOMContentLoaded', function() {
feedbackInput.focus(); connectWebSocket();
setupDragAndDrop();
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -119,16 +119,16 @@ def test_environment_detection():
print("-" * 30) print("-" * 30)
try: try:
from server import is_ssh_session, can_use_gui from server import is_remote_environment, can_use_gui
ssh_detected = is_ssh_session() remote_detected = is_remote_environment()
gui_available = can_use_gui() gui_available = can_use_gui()
print(f"SSH 環境檢測: {'' if ssh_detected else ''}") print(f"遠端環境檢測: {'' if remote_detected else ''}")
print(f"GUI 可用性: {'' if gui_available else ''}") print(f"GUI 可用性: {'' if gui_available else ''}")
if ssh_detected: if remote_detected:
print("✅ 將使用 Web UI (適合 SSH remote 開發)") print("✅ 將使用 Web UI (適合遠端開發環境)")
else: else:
print("✅ 將使用 Qt GUI (本地環境)") print("✅ 將使用 Qt GUI (本地環境)")
@ -147,6 +147,12 @@ def test_mcp_integration():
from server import interactive_feedback from server import interactive_feedback
print("✅ MCP 工具函數可用") print("✅ MCP 工具函數可用")
# Test timeout parameter
print("✅ 支援 timeout 參數")
# Test force_web_ui parameter
print("✅ 支援 force_web_ui 參數")
# Test would require actual MCP call, so just verify import # Test would require actual MCP call, so just verify import
print("✅ 準備接受來自 AI 助手的調用") print("✅ 準備接受來自 AI 助手的調用")
return True return True
@ -155,6 +161,67 @@ def test_mcp_integration():
print(f"❌ MCP 整合測試失敗: {e}") print(f"❌ MCP 整合測試失敗: {e}")
return False return False
def test_new_parameters():
"""Test new timeout and force_web_ui parameters"""
print("\n🆕 測試新增參數功能")
print("-" * 30)
try:
from server import interactive_feedback
# 測試參數是否存在
import inspect
sig = inspect.signature(interactive_feedback)
# 檢查 timeout 參數
if 'timeout' in sig.parameters:
timeout_param = sig.parameters['timeout']
print(f"✅ timeout 參數存在,預設值: {timeout_param.default}")
else:
print("❌ timeout 參數不存在")
return False
# 檢查 force_web_ui 參數
if 'force_web_ui' in sig.parameters:
force_web_ui_param = sig.parameters['force_web_ui']
print(f"✅ force_web_ui 參數存在,預設值: {force_web_ui_param.default}")
else:
print("❌ force_web_ui 參數不存在")
return False
print("✅ 所有新參數功能正常")
return True
except Exception as e:
print(f"❌ 新參數測試失敗: {e}")
return False
def test_force_web_ui_mode():
"""Test force web UI mode"""
print("\n🌐 測試強制 Web UI 模式")
print("-" * 30)
try:
from server import interactive_feedback, is_remote_environment, can_use_gui
# 顯示當前環境狀態
is_remote = is_remote_environment()
gui_available = can_use_gui()
print(f"當前環境 - 遠端: {is_remote}, GUI 可用: {gui_available}")
if not is_remote and gui_available:
print("✅ 在本地 GUI 環境中可以使用 force_web_ui=True 強制使用 Web UI")
print("💡 這對於測試 Web UI 功能非常有用")
else:
print(" 當前環境會自動使用 Web UI")
return True
except Exception as e:
print(f"❌ 強制 Web UI 測試失敗: {e}")
return False
def interactive_demo(session_info): def interactive_demo(session_info):
"""Run interactive demo with the Web UI""" """Run interactive demo with the Web UI"""
print(f"\n🌐 Web UI 持久化運行模式") print(f"\n🌐 Web UI 持久化運行模式")
@ -204,6 +271,12 @@ if __name__ == "__main__":
# Test environment detection # Test environment detection
env_test = test_environment_detection() env_test = test_environment_detection()
# Test new parameters
params_test = test_new_parameters()
# Test force web UI mode
force_test = test_force_web_ui_mode()
# Test MCP integration # Test MCP integration
mcp_test = test_mcp_integration() mcp_test = test_mcp_integration()
@ -211,7 +284,7 @@ if __name__ == "__main__":
web_test, session_info = test_web_ui() web_test, session_info = test_web_ui()
print("\n" + "=" * 60) print("\n" + "=" * 60)
if env_test and mcp_test and web_test: if env_test and params_test and force_test and mcp_test and web_test:
print("🎊 所有測試完成!準備使用 Interactive Feedback MCP") print("🎊 所有測試完成!準備使用 Interactive Feedback MCP")
print("\n📖 使用方法:") print("\n📖 使用方法:")
print(" 1. 在 Cursor/Cline 中配置此 MCP 服務器") print(" 1. 在 Cursor/Cline 中配置此 MCP 服務器")

664
web_ui.py
View File

@ -1,6 +1,17 @@
# Interactive Feedback MCP Web UI #!/usr/bin/env python3
# Developed by Fábio Ferreira (https://x.com/fabiomlferreira) # -*- coding: utf-8 -*-
# Web UI version for SSH remote development """
互動式回饋收集 Web UI
=====================
基於 FastAPI Web 用戶介面專為 SSH 遠端開發環境設計
支援文字輸入圖片上傳命令執行等功能
作者: Fábio Ferreira
靈感來源: dotcursorrules.com
增強功能: 圖片支援和現代化界面設計
"""
import os import os
import sys import sys
import json import json
@ -11,285 +22,327 @@ import threading
import subprocess import subprocess
import psutil import psutil
import time import time
import base64
import tempfile
from typing import Dict, Optional, List from typing import Dict, Optional, List
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, UploadFile, File, Form
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
import uvicorn import uvicorn
# ===== 常數定義 =====
MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制
SUPPORTED_IMAGE_TYPES = {'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp', 'image/webp'}
TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web"
# ===== Web 回饋會話類 =====
class WebFeedbackSession: class WebFeedbackSession:
"""Web 回饋會話管理"""
def __init__(self, session_id: str, project_directory: str, summary: str): def __init__(self, session_id: str, project_directory: str, summary: str):
self.session_id = session_id self.session_id = session_id
self.project_directory = project_directory self.project_directory = project_directory
self.summary = summary self.summary = summary
self.websocket: Optional[WebSocket] = None self.websocket: Optional[WebSocket] = None
self.feedback_result: Optional[str] = None self.feedback_result: Optional[str] = None
self.command_logs: List[str] = [] self.images: List[dict] = []
self.feedback_completed = threading.Event()
self.process: Optional[subprocess.Popen] = None self.process: Optional[subprocess.Popen] = None
self.completed = False self.command_logs = []
self.config = {
"run_command": "",
"execute_automatically": False
}
# 確保臨時目錄存在
TEMP_DIR.mkdir(parents=True, exist_ok=True)
async def wait_for_feedback(self, timeout: int = 600) -> dict:
"""
等待用戶回饋包含圖片
Args:
timeout: 超時時間
Returns:
dict: 回饋結果
"""
loop = asyncio.get_event_loop()
def wait_in_thread():
return self.feedback_completed.wait(timeout)
completed = await loop.run_in_executor(None, wait_in_thread)
if completed:
return {
"logs": "\n".join(self.command_logs),
"interactive_feedback": self.feedback_result or "",
"images": self.images
}
else:
raise TimeoutError("等待用戶回饋超時")
async def submit_feedback(self, feedback: str, images: List[dict]):
"""
提交回饋和圖片
Args:
feedback: 文字回饋
images: 圖片列表
"""
self.feedback_result = feedback
self.images = self._process_images(images)
self.feedback_completed.set()
if self.websocket:
try:
await self.websocket.close()
except:
pass
def _process_images(self, images: List[dict]) -> List[dict]:
"""
處理圖片數據轉換為統一格式
Args:
images: 原始圖片數據列表
Returns:
List[dict]: 處理後的圖片數據
"""
processed_images = []
for img in images:
try:
if not all(key in img for key in ["name", "data", "size"]):
continue
# 檢查文件大小
if img["size"] > MAX_IMAGE_SIZE:
print(f"[DEBUG] 圖片 {img['name']} 超過大小限制,跳過")
continue
# 解碼 base64 數據
if isinstance(img["data"], str):
try:
image_bytes = base64.b64decode(img["data"])
except Exception as e:
print(f"[DEBUG] 圖片 {img['name']} base64 解碼失敗: {e}")
continue
else:
image_bytes = img["data"]
if len(image_bytes) == 0:
print(f"[DEBUG] 圖片 {img['name']} 數據為空,跳過")
continue
processed_images.append({
"name": img["name"],
"data": image_bytes, # 保存原始 bytes 數據
"size": len(image_bytes)
})
print(f"[DEBUG] 圖片 {img['name']} 處理成功,大小: {len(image_bytes)} bytes")
except Exception as e:
print(f"[DEBUG] 圖片處理錯誤: {e}")
continue
return processed_images
def add_log(self, log_entry: str):
"""添加命令日誌"""
self.command_logs.append(log_entry)
async def run_command(self, command: str):
"""執行命令並透過 WebSocket 發送輸出"""
if self.process:
# 終止現有進程
try:
self.process.terminate()
self.process.wait(timeout=5)
except:
try:
self.process.kill()
except:
pass
self.process = None
try:
self.process = subprocess.Popen(
command,
shell=True,
cwd=self.project_directory,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True
)
# 在背景線程中讀取輸出
def read_output():
try:
for line in iter(self.process.stdout.readline, ''):
self.add_log(line.rstrip())
if self.websocket:
asyncio.run_coroutine_threadsafe(
self.websocket.send_json({
"type": "command_output",
"output": line
}),
asyncio.get_event_loop()
)
# 等待進程完成
exit_code = self.process.wait()
if self.websocket:
asyncio.run_coroutine_threadsafe(
self.websocket.send_json({
"type": "command_finished",
"exit_code": exit_code
}),
asyncio.get_event_loop()
)
except Exception as e:
print(f"命令執行錯誤: {e}")
finally:
self.process = None
thread = threading.Thread(target=read_output, daemon=True)
thread.start()
except Exception as e:
error_msg = f"命令執行失敗: {str(e)}\n"
self.add_log(error_msg)
if self.websocket:
await self.websocket.send_json({
"type": "command_output",
"output": error_msg
})
# ===== Web UI 管理器 =====
class WebUIManager: class WebUIManager:
"""Web UI 管理器"""
def __init__(self, host: str = "127.0.0.1", port: int = 8765): def __init__(self, host: str = "127.0.0.1", port: int = 8765):
self.host = host self.host = host
self.port = port self.port = port
self.app = FastAPI(title="Interactive Feedback MCP") self.app = FastAPI(title="Interactive Feedback MCP Web UI")
self.sessions: Dict[str, WebFeedbackSession] = {} self.sessions: Dict[str, WebFeedbackSession] = {}
self.server_process = None self.server_thread: Optional[threading.Thread] = None
self.setup_routes() self.setup_routes()
# Setup static files and templates
script_dir = Path(__file__).parent
static_dir = script_dir / "static"
templates_dir = script_dir / "templates"
static_dir.mkdir(exist_ok=True)
templates_dir.mkdir(exist_ok=True)
self.app.mount("/static", StaticFiles(directory=static_dir), name="static")
self.templates = Jinja2Templates(directory=templates_dir)
def setup_routes(self): def setup_routes(self):
"""設置路由"""
# 確保靜態文件目錄存在
static_dir = Path("static")
templates_dir = Path("templates")
# 靜態文件
if static_dir.exists():
self.app.mount("/static", StaticFiles(directory="static"), name="static")
# 模板
templates = Jinja2Templates(directory="templates") if templates_dir.exists() else None
@self.app.get("/", response_class=HTMLResponse) @self.app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index(request: Request):
return self.templates.TemplateResponse("index.html", {"request": request}) """首頁"""
if templates:
return templates.TemplateResponse("index.html", {"request": request})
else:
return HTMLResponse(self._get_simple_index_html())
@self.app.get("/session/{session_id}", response_class=HTMLResponse) @self.app.get("/session/{session_id}", response_class=HTMLResponse)
async def session_page(request: Request, session_id: str): async def feedback_session(request: Request, session_id: str):
"""回饋會話頁面"""
session = self.sessions.get(session_id) session = self.sessions.get(session_id)
if not session: if not session:
return HTMLResponse("Session not found", status_code=404) return HTMLResponse("會話不存在", status_code=404)
return self.templates.TemplateResponse("feedback.html", { if templates:
return templates.TemplateResponse("feedback.html", {
"request": request, "request": request,
"session_id": session_id, "session_id": session_id,
"project_directory": session.project_directory, "project_directory": session.project_directory,
"summary": session.summary "summary": session.summary
}) })
else:
return HTMLResponse(self._get_simple_feedback_html(session_id, session))
@self.app.websocket("/ws/{session_id}") @self.app.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str): async def websocket_endpoint(websocket: WebSocket, session_id: str):
await websocket.accept() """WebSocket 連接處理"""
session = self.sessions.get(session_id) session = self.sessions.get(session_id)
if not session: if not session:
await websocket.close(code=4000, reason="Session not found") await websocket.close(code=4004, reason="會話不存在")
return return
await websocket.accept()
session.websocket = websocket session.websocket = websocket
# Send initial data
await websocket.send_json({
"type": "init",
"project_directory": session.project_directory,
"summary": session.summary,
"config": session.config,
"logs": session.command_logs
})
try: try:
while True: while True:
data = await websocket.receive_json() data = await websocket.receive_json()
await self.handle_websocket_message(session, data) await self.handle_websocket_message(session, data)
except WebSocketDisconnect: except WebSocketDisconnect:
print(f"WebSocket 斷開連接: {session_id}")
except Exception as e:
print(f"WebSocket 錯誤: {e}")
finally:
session.websocket = None session.websocket = None
@self.app.post("/api/complete/{session_id}")
async def complete_session(session_id: str, feedback_data: dict):
session = self.sessions.get(session_id)
if not session:
return {"error": "Session not found"}
session.feedback_result = feedback_data.get("feedback", "")
session.completed = True
return {"success": True}
async def handle_websocket_message(self, session: WebFeedbackSession, data: dict): async def handle_websocket_message(self, session: WebFeedbackSession, data: dict):
"""處理 WebSocket 消息"""
message_type = data.get("type") message_type = data.get("type")
if message_type == "run_command": if message_type == "run_command":
command = data.get("command", "") command = data.get("command", "").strip()
await self.run_command(session, command) if command:
await session.run_command(command)
elif message_type == "stop_command":
await self.stop_command(session)
elif message_type == "submit_feedback": elif message_type == "submit_feedback":
feedback = data.get("feedback", "") feedback = data.get("feedback", "")
session.feedback_result = feedback images = data.get("images", [])
session.completed = True await session.submit_feedback(feedback, images)
await session.websocket.send_json({ elif message_type == "stop_command":
"type": "feedback_submitted",
"message": "Feedback submitted successfully"
})
elif message_type == "update_config":
session.config.update(data.get("config", {}))
elif message_type == "clear_logs":
session.command_logs.clear()
await session.websocket.send_json({
"type": "logs_cleared"
})
async def run_command(self, session: WebFeedbackSession, command: str):
if session.process:
await self.stop_command(session)
if not command.strip():
await session.websocket.send_json({
"type": "log",
"data": "Please enter a command to run\n"
})
return
session.command_logs.append(f"$ {command}\n")
await session.websocket.send_json({
"type": "log",
"data": f"$ {command}\n"
})
try:
session.process = subprocess.Popen(
command,
shell=True,
cwd=session.project_directory,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
encoding="utf-8",
errors="ignore"
)
# Start threads to read output
threading.Thread(
target=self.read_process_output,
args=(session, session.process.stdout),
daemon=True
).start()
threading.Thread(
target=self.read_process_output,
args=(session, session.process.stderr),
daemon=True
).start()
# Monitor process completion
threading.Thread(
target=self.monitor_process,
args=(session,),
daemon=True
).start()
except Exception as e:
error_msg = f"Error running command: {str(e)}\n"
session.command_logs.append(error_msg)
await session.websocket.send_json({
"type": "log",
"data": error_msg
})
def read_process_output(self, session: WebFeedbackSession, pipe):
try:
for line in iter(pipe.readline, ""):
if not line:
break
session.command_logs.append(line)
if session.websocket:
# Use threading to send async message
threading.Thread(
target=self._send_websocket_message,
args=(session.websocket, {
"type": "log",
"data": line
}),
daemon=True
).start()
except Exception:
pass
def monitor_process(self, session: WebFeedbackSession):
if session.process:
exit_code = session.process.wait()
completion_msg = f"\nProcess exited with code {exit_code}\n"
session.command_logs.append(completion_msg)
if session.websocket:
threading.Thread(
target=self._send_websocket_message,
args=(session.websocket, {
"type": "log",
"data": completion_msg
}),
daemon=True
).start()
threading.Thread(
target=self._send_websocket_message,
args=(session.websocket, {
"type": "process_completed",
"exit_code": exit_code
}),
daemon=True
).start()
session.process = None
def _send_websocket_message(self, websocket: WebSocket, message: dict):
"""Helper to send websocket message from thread"""
try:
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(websocket.send_json(message))
loop.close()
except Exception:
pass
async def stop_command(self, session: WebFeedbackSession):
if session.process: if session.process:
try: try:
# Kill process tree session.process.terminate()
parent = psutil.Process(session.process.pid) except:
for child in parent.children(recursive=True):
try:
child.kill()
except psutil.Error:
pass pass
parent.kill()
session.process = None
await session.websocket.send_json({
"type": "log",
"data": "\nProcess stopped\n"
})
except Exception as e:
await session.websocket.send_json({
"type": "log",
"data": f"\nError stopping process: {str(e)}\n"
})
def create_session(self, project_directory: str, summary: str) -> str: def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話"""
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
session = WebFeedbackSession(session_id, project_directory, summary) session = WebFeedbackSession(session_id, project_directory, summary)
self.sessions[session_id] = session self.sessions[session_id] = session
return session_id return session_id
def start_server(self): def get_session(self, session_id: str) -> Optional[WebFeedbackSession]:
"""Start the web server in a separate thread""" """獲取會話"""
if self.server_process is not None: return self.sessions.get(session_id)
return # Server already running
def remove_session(self, session_id: str):
"""移除會話"""
if session_id in self.sessions:
session = self.sessions[session_id]
if session.process:
try:
session.process.terminate()
except:
pass
del self.sessions[session_id]
def start_server(self):
"""啟動伺服器"""
def run_server(): def run_server():
uvicorn.run( uvicorn.run(
self.app, self.app,
@ -299,57 +352,168 @@ class WebUIManager:
access_log=False access_log=False
) )
self.server_process = threading.Thread(target=run_server, daemon=True) self.server_thread = threading.Thread(target=run_server, daemon=True)
self.server_process.start() self.server_thread.start()
# Wait a moment for server to start # 等待伺服器啟動
time.sleep(1) time.sleep(2)
def open_browser(self, session_id: str): def open_browser(self, url: str):
"""Open browser to the session page""" """開啟瀏覽器"""
url = f"http://{self.host}:{self.port}/session/{session_id}"
try: try:
webbrowser.open(url) webbrowser.open(url)
except Exception: except Exception as e:
print(f"Please open your browser and navigate to: {url}") print(f"無法開啟瀏覽器: {e}")
def wait_for_feedback(self, session_id: str, timeout: int = 300) -> dict: def _get_simple_index_html(self) -> str:
"""Wait for user feedback with timeout""" """簡單的首頁 HTML"""
session = self.sessions.get(session_id) return """
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Interactive Feedback MCP</title>
</head>
<body>
<h1>Interactive Feedback MCP Web UI</h1>
<p>服務器運行中...</p>
</body>
</html>
"""
def _get_simple_feedback_html(self, session_id: str, session: WebFeedbackSession) -> str:
"""簡單的回饋頁面 HTML"""
return f"""
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>回饋收集</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background: #1e1e1e; color: white; }}
.container {{ max-width: 800px; margin: 0 auto; }}
textarea {{ width: 100%; height: 200px; background: #2d2d30; color: white; border: 1px solid #464647; }}
button {{ background: #007acc; color: white; padding: 10px 20px; border: none; cursor: pointer; }}
</style>
</head>
<body>
<div class="container">
<h1>回饋收集</h1>
<div>
<h3>AI 工作摘要:</h3>
<p>{session.summary}</p>
</div>
<div>
<h3>您的回饋:</h3>
<textarea id="feedback" placeholder="請輸入您的回饋..."></textarea>
</div>
<button onclick="submitFeedback()">提交回饋</button>
</div>
<script>
const ws = new WebSocket('ws://localhost:{self.port}/ws/{session_id}');
function submitFeedback() {{
const feedback = document.getElementById('feedback').value;
ws.send(JSON.stringify({{
type: 'submit_feedback',
feedback: feedback,
images: []
}}));
alert('回饋已提交!');
}}
</script>
</body>
</html>
"""
# ===== 全域管理器 =====
_web_ui_manager: Optional[WebUIManager] = None
def get_web_ui_manager() -> WebUIManager:
"""獲取全域 Web UI 管理器"""
global _web_ui_manager
if _web_ui_manager is None:
_web_ui_manager = WebUIManager()
_web_ui_manager.start_server()
return _web_ui_manager
async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
"""啟動 Web 回饋 UI 並等待回饋"""
manager = get_web_ui_manager()
# 創建會話
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}")
# 開啟瀏覽器
manager.open_browser(session_url)
try:
# 等待用戶回饋
session = manager.get_session(session_id)
if not session: if not session:
return {"command_logs": "", "interactive_feedback": "Session not found"} raise RuntimeError("會話創建失敗")
# Wait for feedback with timeout
start_time = time.time()
while not session.completed:
if time.time() - start_time > timeout:
return {"command_logs": "", "interactive_feedback": "Timeout waiting for feedback"}
time.sleep(0.1)
result = {
"command_logs": "".join(session.command_logs),
"interactive_feedback": session.feedback_result or ""
}
# Clean up session
del self.sessions[session_id]
result = await session.wait_for_feedback(timeout=600) # 10分鐘超時
return result return result
# Global instance except TimeoutError:
web_ui_manager = WebUIManager() print("⏰ 等待用戶回饋超時")
return {
"logs": "",
"interactive_feedback": "回饋超時",
"images": []
}
except Exception as e:
print(f"❌ Web UI 錯誤: {e}")
return {
"logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
"images": []
}
finally:
# 清理會話
manager.remove_session(session_id)
def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: def stop_web_ui():
"""Launch web UI and wait for feedback""" """停止 Web UI"""
global _web_ui_manager
if _web_ui_manager:
# 清理所有會話
for session_id in list(_web_ui_manager.sessions.keys()):
_web_ui_manager.remove_session(session_id)
_web_ui_manager = None
# Start server if not running
web_ui_manager.start_server()
# Create new session # ===== 主程式入口 =====
session_id = web_ui_manager.create_session(project_directory, summary) if __name__ == "__main__":
import argparse
# Open browser parser = argparse.ArgumentParser(description="啟動 Interactive Feedback MCP Web UI")
web_ui_manager.open_browser(session_id) parser.add_argument("--host", default="127.0.0.1", help="主機地址")
parser.add_argument("--port", type=int, default=8765, help="端口")
parser.add_argument("--project-directory", default=os.getcwd(), help="專案目錄")
parser.add_argument("--summary", default="測試 Web UI 功能", help="任務摘要")
# Wait for feedback args = parser.parse_args()
return web_ui_manager.wait_for_feedback(session_id)
async def main():
manager = WebUIManager(args.host, args.port)
manager.start_server()
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}")
manager.open_browser(session_url)
try:
# 保持運行
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
print("\n👋 Web UI 已停止")
asyncio.run(main())