diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a789f09..f4e587e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,9 +9,9 @@ on: default: 'patch' type: choice options: - - patch # 2.0.0 -> 2.0.1 (bug fixes) - - minor # 2.0.0 -> 2.1.0 (new features) - - major # 2.0.0 -> 3.0.0 (breaking changes) + - patch # 2.0.0 -> 2.0.1 (bug fixes, security patches, documentation updates) + - minor # 2.0.0 -> 2.1.0 (new features, enhancements, backward-compatible changes) + - major # 2.0.0 -> 3.0.0 (breaking changes, architecture refactoring, API changes) jobs: release: @@ -70,167 +70,156 @@ jobs: NEW_VERSION="${{ steps.bump_version.outputs.new }}" sed -i "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" src/mcp_feedback_enhanced/__init__.py - - name: Check Release Notes - id: check_notes + - name: Extract Release Highlights + id: extract_highlights run: | NEW_VERSION="v${{ steps.bump_version.outputs.new }}" - RELEASE_DIR="RELEASE_NOTES/${NEW_VERSION}" - - if [ ! -d "$RELEASE_DIR" ]; then - echo "❌ Error: Release notes directory not found at $RELEASE_DIR" - echo "Please create release notes before publishing!" - echo "Required files:" - echo " - $RELEASE_DIR/en.md" - echo " - $RELEASE_DIR/zh-TW.md" - exit 1 + + # Extract highlights from English CHANGELOG + if [ -f "RELEASE_NOTES/CHANGELOG.en.md" ]; then + # Find the section for the new version and extract highlights + awk "/## \[${NEW_VERSION}\]/{flag=1; next} /## \[/{flag=0} flag && /### 🌟 Highlights/{getline; while(getline && !/^###/ && !/^##/) if(/^[^[:space:]]/ || /^- /) print}' RELEASE_NOTES/CHANGELOG.en.md > highlights.txt + + # If no highlights section found, extract from new features + if [ ! -s highlights.txt ]; then + awk "/## \[${NEW_VERSION}\]/{flag=1; next} /## \[/{flag=0} flag && /### ✨ New Features/{getline; while(getline && !/^###/ && !/^##/) if(/^- /) print}' RELEASE_NOTES/CHANGELOG.en.md | head -4 > highlights.txt + fi + + echo "✅ Extracted highlights for $NEW_VERSION" + else + echo "⚠️ CHANGELOG.en.md not found, using default highlights" + echo "- 🚀 New features and improvements" > highlights.txt + echo "- 🐛 Bug fixes and optimizations" >> highlights.txt fi - - if [ ! -f "$RELEASE_DIR/en.md" ]; then - echo "❌ Error: English release notes not found at $RELEASE_DIR/en.md" - exit 1 - fi - - if [ ! -f "$RELEASE_DIR/zh-TW.md" ]; then - echo "❌ Error: Traditional Chinese release notes not found at $RELEASE_DIR/zh-TW.md" - exit 1 - fi - - if [ ! -f "$RELEASE_DIR/zh-CN.md" ]; then - echo "❌ Error: Simplified Chinese release notes not found at $RELEASE_DIR/zh-CN.md" - exit 1 - fi - - echo "✅ Release notes found for $NEW_VERSION" - echo "release_dir=$RELEASE_DIR" >> $GITHUB_OUTPUT - name: Generate Release Body id: release_body run: | NEW_VERSION="v${{ steps.bump_version.outputs.new }}" - RELEASE_DIR="${{ steps.check_notes.outputs.release_dir }}" - - # Create multi-language release body - cat > release_body.md << 'EOF' - ## 🌐 Multi-Language Release Notes - + + # Get release title from English CHANGELOG + RELEASE_TITLE=$(awk "/## \[${NEW_VERSION}\]/{print; exit}" RELEASE_NOTES/CHANGELOG.en.md | sed 's/## \[.*\] - //') + if [ -z "$RELEASE_TITLE" ]; then + RELEASE_TITLE="Latest Release" + fi + + # Create release body with highlights and links + cat > release_body.md << EOF + # Release ${NEW_VERSION} - ${RELEASE_TITLE} + + ## 🌟 Key Highlights + EOF + + # Add highlights + if [ -s highlights.txt ]; then + cat highlights.txt >> release_body.md + else + echo "- 🚀 New features and improvements" >> release_body.md + echo "- 🐛 Bug fixes and optimizations" >> release_body.md + fi + + # Add multi-language links section + cat >> release_body.md << 'EOF' + + ## 🌐 Detailed Release Notes + ### 🇺🇸 English - EOF - - # Add English content - cat "$RELEASE_DIR/en.md" >> release_body.md - - # Add separator - cat >> release_body.md << 'EOF' - - --- - + 📖 **[View Complete English Release Notes](https://github.com/Minidoracat/mcp-feedback-enhanced/blob/main/RELEASE_NOTES/CHANGELOG.en.md)** + ### 🇹🇼 繁體中文 - EOF - - # Add Traditional Chinese content - cat "$RELEASE_DIR/zh-TW.md" >> release_body.md - - # Add separator - cat >> release_body.md << 'EOF' - - --- - + 📖 **[查看完整繁體中文發布說明](https://github.com/Minidoracat/mcp-feedback-enhanced/blob/main/RELEASE_NOTES/CHANGELOG.zh-TW.md)** + ### 🇨🇳 简体中文 - EOF - - # Add Simplified Chinese content - cat "$RELEASE_DIR/zh-CN.md" >> release_body.md - - # Add installation section - cat >> release_body.md << 'EOF' - + 📖 **[查看完整简体中文发布说明](https://github.com/Minidoracat/mcp-feedback-enhanced/blob/main/RELEASE_NOTES/CHANGELOG.zh-CN.md)** + --- - - ## 📦 Installation & Update - + + ## 📦 Quick Installation / 快速安裝 + ```bash - # Quick test latest version - uvx mcp-feedback-enhanced@latest test - - # Update to this specific version + # Latest version / 最新版本 + uvx mcp-feedback-enhanced@latest + + # This specific version / 此特定版本 EOF - - echo "uvx mcp-feedback-enhanced@$NEW_VERSION test" >> release_body.md - + + echo "uvx mcp-feedback-enhanced@${NEW_VERSION}" >> release_body.md + cat >> release_body.md << 'EOF' ``` - + ## 🔗 Links - **Documentation**: [README.md](https://github.com/Minidoracat/mcp-feedback-enhanced/blob/main/README.md) - **Full Changelog**: [CHANGELOG](https://github.com/Minidoracat/mcp-feedback-enhanced/blob/main/RELEASE_NOTES/) - **Issues**: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) - + --- - **Release automatically generated from RELEASE_NOTES system** 🤖 + **Release automatically generated from CHANGELOG system** 🤖 EOF - + echo "Release body generated successfully" - - name: Sync CHANGELOG Files + - name: Verify CHANGELOG Files run: | NEW_VERSION="v${{ steps.bump_version.outputs.new }}" - RELEASE_DIR="${{ steps.check_notes.outputs.release_dir }}" - - # Function to add version to changelog - add_to_changelog() { - local lang_file="$1" - local changelog_file="$2" - local temp_file="changelog_temp.md" - - # Get the header and separator - head -n 5 "$changelog_file" > "$temp_file" - - # Add the new version content - cat "$lang_file" >> "$temp_file" - - # Add separator - echo "" >> "$temp_file" - echo "---" >> "$temp_file" - echo "" >> "$temp_file" - - # Add the rest of the changelog (skip header) - tail -n +7 "$changelog_file" >> "$temp_file" - - # Replace the original file - mv "$temp_file" "$changelog_file" - - echo "✅ Updated $changelog_file" - } - - # Check if CHANGELOG files exist + + # Check if CHANGELOG files exist and contain the new version + echo "🔍 Verifying CHANGELOG files contain version ${NEW_VERSION}..." + + MISSING_FILES="" + if [ -f "RELEASE_NOTES/CHANGELOG.en.md" ]; then - add_to_changelog "$RELEASE_DIR/en.md" "RELEASE_NOTES/CHANGELOG.en.md" + if ! grep -q "\[${NEW_VERSION}\]" "RELEASE_NOTES/CHANGELOG.en.md"; then + echo "⚠️ Warning: ${NEW_VERSION} not found in CHANGELOG.en.md" + MISSING_FILES="${MISSING_FILES} en" + else + echo "✅ Found ${NEW_VERSION} in CHANGELOG.en.md" + fi else - echo "⚠️ CHANGELOG.en.md not found, skipping" + echo "❌ CHANGELOG.en.md not found" + MISSING_FILES="${MISSING_FILES} en" fi - + if [ -f "RELEASE_NOTES/CHANGELOG.zh-TW.md" ]; then - add_to_changelog "$RELEASE_DIR/zh-TW.md" "RELEASE_NOTES/CHANGELOG.zh-TW.md" + if ! grep -q "\[${NEW_VERSION}\]" "RELEASE_NOTES/CHANGELOG.zh-TW.md"; then + echo "⚠️ Warning: ${NEW_VERSION} not found in CHANGELOG.zh-TW.md" + MISSING_FILES="${MISSING_FILES} zh-TW" + else + echo "✅ Found ${NEW_VERSION} in CHANGELOG.zh-TW.md" + fi else - echo "⚠️ CHANGELOG.zh-TW.md not found, skipping" + echo "❌ CHANGELOG.zh-TW.md not found" + MISSING_FILES="${MISSING_FILES} zh-TW" fi - + if [ -f "RELEASE_NOTES/CHANGELOG.zh-CN.md" ]; then - add_to_changelog "$RELEASE_DIR/zh-CN.md" "RELEASE_NOTES/CHANGELOG.zh-CN.md" + if ! grep -q "\[${NEW_VERSION}\]" "RELEASE_NOTES/CHANGELOG.zh-CN.md"; then + echo "⚠️ Warning: ${NEW_VERSION} not found in CHANGELOG.zh-CN.md" + MISSING_FILES="${MISSING_FILES} zh-CN" + else + echo "✅ Found ${NEW_VERSION} in CHANGELOG.zh-CN.md" + fi else - echo "⚠️ CHANGELOG.zh-CN.md not found, skipping" + echo "❌ CHANGELOG.zh-CN.md not found" + MISSING_FILES="${MISSING_FILES} zh-CN" + fi + + if [ -n "$MISSING_FILES" ]; then + echo "" + echo "📝 Note: Please ensure CHANGELOG files are updated with version ${NEW_VERSION}" + echo "Missing or incomplete files:${MISSING_FILES}" + echo "The release will continue, but manual CHANGELOG updates may be needed." + else + echo "✅ All CHANGELOG files verified successfully" fi - - echo "📝 CHANGELOG files synchronized" - - name: Commit version bump and changelog updates + - name: Commit version bump run: | git add . git commit -m "🔖 Release v${{ steps.bump_version.outputs.new }} - Updated version to ${{ steps.bump_version.outputs.new }} - - Synchronized CHANGELOG files with release notes - - Auto-generated from RELEASE_NOTES system" + - Auto-generated release from simplified workflow" git tag "v${{ steps.bump_version.outputs.new }}" - name: Build package @@ -268,9 +257,11 @@ jobs: echo "" echo "📦 Published to PyPI: https://pypi.org/project/mcp-feedback-enhanced/" echo "🚀 GitHub Release: https://github.com/Minidoracat/mcp-feedback-enhanced/releases/tag/v${{ steps.bump_version.outputs.new }}" - echo "📝 CHANGELOG files have been automatically updated" + echo "📝 Release notes generated from CHANGELOG files" echo "" echo "✅ Next steps:" echo " - Check the release on GitHub" echo " - Verify the package on PyPI" - echo " - Test installation with: uvx mcp-feedback-enhanced@v${{ steps.bump_version.outputs.new }} test" \ No newline at end of file + echo " - Test installation with: uvx mcp-feedback-enhanced@v${{ steps.bump_version.outputs.new }}" + echo "" + echo "📋 Note: Make sure CHANGELOG files are updated for future releases" \ No newline at end of file diff --git a/README.md b/README.md index 2b7795e..dc74269 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## 🎯 Core Concept -This is an [MCP server](https://modelcontextprotocol.io/) that establishes **feedback-oriented development workflows**, perfectly adapting to local, **SSH remote development environments**, and **WSL (Windows Subsystem for Linux) environments**. By guiding AI to confirm with users rather than making speculative operations, it can consolidate multiple tool calls into a single feedback-oriented request, dramatically reducing platform costs and improving development efficiency. +This is an [MCP server](https://modelcontextprotocol.io/) that establishes **feedback-oriented development workflows**, perfectly adapting to local, **SSH Remote environments** (Cursor SSH Remote, VS Code Remote SSH), and **WSL (Windows Subsystem for Linux) environments**. By guiding AI to confirm with users rather than making speculative operations, it can consolidate multiple tool calls into a single feedback-oriented request, dramatically reducing platform costs and improving development efficiency. **Supported Platforms:** [Cursor](https://www.cursor.com) | [Cline](https://cline.bot) | [Windsurf](https://windsurf.com) | [Augment](https://www.augmentcode.com) | [Trae](https://www.trae.ai) @@ -42,12 +42,19 @@ This is an [MCP server](https://modelcontextprotocol.io/) that establishes **fee - **Smart Detection**: Auto-select based on system language - **Live Switching**: Change language directly within interface -### ✨ WSL Environment Support (v2.2.5 New Feature) +### ✨ WSL Environment Support (v2.2.5) - **Auto Detection**: Intelligently identifies WSL (Windows Subsystem for Linux) environments - **Browser Integration**: Automatically launches Windows browser in WSL environments - **Multiple Launch Methods**: Supports `cmd.exe`, `powershell.exe`, `wslview` and other browser launch methods - **Seamless Experience**: WSL users can directly use Web UI without additional configuration +### 🌐 SSH Remote Environment Support (v2.3.0 New Feature) +- **Smart Detection**: Automatically identifies SSH Remote environments (Cursor SSH Remote, VS Code Remote SSH, etc.) +- **Browser Launch Guidance**: Provides clear solutions when browser cannot launch automatically +- **Port Forwarding Support**: Complete port forwarding setup guidance and troubleshooting +- **MCP Integration Optimization**: Improved integration with MCP system for more stable connection experience +- **Detailed Documentation**: [SSH Remote Environment Usage Guide](docs/en/ssh-remote/browser-launch-issues.md) + ## 🖥️ Interface Preview ### Qt GUI Interface (Refactored Version) @@ -134,6 +141,7 @@ For best results, add these rules to your AI assistant: |----------|---------|--------|---------| | `FORCE_WEB` | Force use Web UI | `true`/`false` | `false` | | `MCP_DEBUG` | Debug mode | `true`/`false` | `false` | +| `MCP_WEB_PORT` | Web UI port | `1024-65535` | `8765` | ### Testing Options ```bash @@ -178,20 +186,38 @@ uvx --with-editable . mcp-feedback-enhanced test --web # Test Web UI (auto co 📋 **Complete Version History:** [RELEASE_NOTES/CHANGELOG.en.md](RELEASE_NOTES/CHANGELOG.en.md) -### Latest Version Highlights (v2.2.5) -- ✨ **WSL Environment Support**: Added comprehensive support for WSL (Windows Subsystem for Linux) environments -- 🌐 **Smart Browser Launching**: Automatically invokes Windows browser in WSL environments with multiple launch methods -- 🎯 **Environment Detection Optimization**: Improved remote environment detection logic, WSL no longer misidentified as remote environment -- 🧪 **Testing Experience Improvement**: Test mode automatically attempts browser launching for better testing experience +### Latest Version Highlights (v2.3.0) +- 🌐 **SSH Remote Environment Support**: Solved Cursor SSH Remote browser launch issues with clear usage guidance +- 🛡️ **Error Message Improvements**: Provides more user-friendly error messages and solution suggestions when errors occur +- 🧹 **Auto-cleanup Features**: Automatically cleans temporary files and expired sessions to keep the system tidy +- 📊 **Memory Monitoring**: Monitors memory usage to prevent system resource shortage +- 🔧 **Connection Stability**: Improved Web UI connection stability and error handling ## 🐛 Common Issues +### 🌐 SSH Remote Environment Issues +**Q: Browser cannot launch in SSH Remote environment** +A: This is normal behavior. SSH Remote environments have no graphical interface, requiring manual opening in local browser. For detailed solutions, see: [SSH Remote Environment Usage Guide](docs/en/ssh-remote/browser-launch-issues.md) + +**Q: Why am I not receiving new MCP feedback?** +A: There might be a WebSocket connection issue. **Solution**: Simply refresh the browser page. + +**Q: Why isn't MCP being called?** +A: Please confirm the MCP tool status shows green light. **Solution**: Toggle the MCP tool on/off repeatedly, wait a few seconds for system reconnection. + +**Q: Augment cannot start MCP** +A: **Solution**: Completely close and restart VS Code or Cursor, then reopen the project. + +### 🔧 General Issues **Q: Getting "Unexpected token 'D'" error** A: Debug output interference. Set `MCP_DEBUG=false` or remove the environment variable. **Q: Chinese character garbled text** A: Fixed in v2.0.3. Update to latest version: `uvx mcp-feedback-enhanced@latest` +**Q: Multi-screen window disappearing or positioning errors** +A: Fixed in v2.1.1. Go to "⚙️ Settings" tab, check "Always show window at primary screen center" to resolve. Especially useful for T-shaped screen arrangements and other complex multi-monitor configurations. + **Q: Image upload fails** A: Check file size (≤1MB) and format (PNG/JPG/GIF/BMP/WebP). @@ -218,21 +244,11 @@ uv cache clean ``` For detailed instructions, see: [Cache Management Guide](docs/en/cache-management.md) -**Q: Gemini Pro 2.5 cannot parse images** -A: Known issue. Gemini Pro 2.5 may not correctly parse uploaded image content. Testing shows Claude-4-Sonnet can properly analyze images. Recommend using Claude models for better image understanding capabilities. - -**Q: Multi-screen window positioning issues** -A: Fixed in v2.1.1. Go to "⚙️ Settings" tab, check "Always show window at primary screen center" to resolve window positioning issues. Especially useful for T-shaped screen arrangements and other complex multi-monitor configurations. - -**Q: Cannot launch browser in WSL environment** -A: v2.2.5 has added WSL environment support. If issues persist: -1. Confirm WSL version (WSL 2 recommended) -2. Check if Windows browser is properly installed -3. Try manual test: run `cmd.exe /c start https://www.google.com` in WSL -4. If `wslu` package is installed, you can also try the `wslview` command - -**Q: WSL environment misidentified as remote environment** -A: v2.2.5 has fixed this issue. WSL environments are now correctly identified and use Web UI with Windows browser launching, instead of being misidentified as remote environments. +**Q: AI models cannot parse images** +A: Various AI models (including Gemini Pro 2.5, Claude, etc.) may have instability in image parsing, sometimes correctly identifying and sometimes unable to parse uploaded image content. This is a known limitation of AI visual understanding technology. Recommendations: +1. Ensure good image quality (high contrast, clear text) +2. Try uploading multiple times, retries usually succeed +3. If parsing continues to fail, try adjusting image size or format ## 🙏 Acknowledgments @@ -245,4 +261,15 @@ If you find this useful, please: - 📱 [Follow the original author](https://x.com/fabiomlferreira) ### Design Inspiration -**sanshao85** - [mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector) \ No newline at end of file +**sanshao85** - [mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector) + +### Community Support +- **Discord:** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67) +- **Issues:** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) + +## 📄 License + +MIT License - see [LICENSE](LICENSE) file for details + +--- +**🌟 Welcome to Star and share with more developers!** \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md index 20bdc0d..b94e59a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ ## 🎯 核心概念 -这是一个 [MCP 服务器](https://modelcontextprotocol.io/),建立**反馈导向的开发工作流程**,完美适配本地、**SSH 远程开发环境**与 **WSL (Windows Subsystem for Linux) 环境**。通过引导 AI 与用户确认而非进行推测性操作,可将多次工具调用合并为单次反馈导向请求,大幅节省平台成本并提升开发效率。 +这是一个 [MCP 服务器](https://modelcontextprotocol.io/),建立**反馈导向的开发工作流程**,完美适配本地、**SSH Remote 环境**(Cursor SSH Remote、VS Code Remote SSH)与 **WSL (Windows Subsystem for Linux) 环境**。通过引导 AI 与用户确认而非进行推测性操作,可将多次工具调用合并为单次反馈导向请求,大幅节省平台成本并提升开发效率。 **支持平台:** [Cursor](https://www.cursor.com) | [Cline](https://cline.bot) | [Windsurf](https://windsurf.com) | [Augment](https://www.augmentcode.com) | [Trae](https://www.trae.ai) @@ -42,12 +42,19 @@ - **智能检测**:根据系统语言自动选择 - **即时切换**:界面内可直接切换语言 -### ✨ WSL 环境支持(v2.2.5 新功能) +### ✨ WSL 环境支持(v2.2.5) - **自动检测**:智能识别 WSL (Windows Subsystem for Linux) 环境 - **浏览器整合**:WSL 环境下自动启动 Windows 浏览器 - **多种启动方式**:支持 `cmd.exe`、`powershell.exe`、`wslview` 等多种浏览器启动方法 - **无缝体验**:WSL 用户可直接使用 Web UI,无需额外配置 +### 🌐 SSH Remote 环境支持(v2.3.0 新功能) +- **智能检测**:自动识别 SSH Remote 环境(Cursor SSH Remote、VS Code Remote SSH 等) +- **浏览器启动指引**:当无法自动启动浏览器时,提供清晰的解决方案 +- **端口转发支持**:完整的端口转发设置指引和故障排除 +- **MCP 整合优化**:改善与 MCP 系统的整合,提供更稳定的连接体验 +- **详细文档**:[SSH Remote 环境使用指南](docs/zh-CN/ssh-remote/browser-launch-issues.md) + ## 🖥️ 界面预览 ### Qt GUI 界面(重构版) @@ -180,25 +187,43 @@ uvx --with-editable . mcp-feedback-enhanced test --web # 测试 Web UI (自 📋 **完整版本更新记录:** [RELEASE_NOTES/CHANGELOG.zh-CN.md](RELEASE_NOTES/CHANGELOG.zh-CN.md) -### 最新版本亮点(v2.2.5) -- ✨ **WSL 环境支持**: 新增 WSL (Windows Subsystem for Linux) 环境的完整支持 -- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式 -- 🎯 **环境检测优化**: 改进远程环境检测逻辑,WSL 不再被误判为远程环境 -- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验 +### 最新版本亮点(v2.3.0) +- 🌐 **SSH Remote 环境支持**: 解决 Cursor SSH Remote 无法启动浏览器的问题,提供清晰的使用指引 +- 🛡️ **错误提示改善**: 当发生错误时,提供更友善的错误信息和解决建议 +- 🧹 **自动清理功能**: 自动清理临时文件和过期会话,保持系统整洁 +- 📊 **内存监控**: 监控内存使用情况,防止系统资源不足 +- 🔧 **连接稳定性**: 改善 Web UI 的连接稳定性和错误处理 ## 🐛 常见问题 +### 🌐 SSH Remote 环境问题 +**Q: SSH Remote 环境下浏览器无法启动** +A: 这是正常现象。SSH Remote 环境没有图形界面,需要手动在本地浏览器打开。详细解决方案请参考:[SSH Remote 环境使用指南](docs/zh-CN/ssh-remote/browser-launch-issues.md) + +**Q: 为什么没有接收到 MCP 新的反馈?** +A: 可能是 WebSocket 连接问题。**解决方法**:直接重新刷新浏览器页面。 + +**Q: 为什么没有调用出 MCP?** +A: 请确认 MCP 工具状态为绿灯。**解决方法**:反复开关 MCP 工具,等待几秒让系统重新连接。 + +**Q: Augment 无法启动 MCP** +A: **解决方法**:完全关闭并重新启动 VS Code 或 Cursor,重新打开项目。 + +### 🔧 一般问题 **Q: 出现 "Unexpected token 'D'" 错误** A: 调试输出干扰。设置 `MCP_DEBUG=false` 或移除该环境变量。 **Q: 中文字符乱码** A: 已在 v2.0.3 修复。更新到最新版本:`uvx mcp-feedback-enhanced@latest` +**Q: 多屏幕环境下窗口消失或定位错误** +A: 已在 v2.1.1 修复。进入「⚙️ 设置」标签页,勾选「总是在主屏幕中心显示窗口」即可解决。特别适用于 T 字型屏幕排列等复杂多屏幕配置。 + **Q: 图片上传失败** A: 检查文件大小(≤1MB)和格式(PNG/JPG/GIF/BMP/WebP)。 **Q: Web UI 无法启动** -A: 设置 `FORCE_WEB=true` 或检查火墙设定。 +A: 设置 `FORCE_WEB=true` 或检查防火墙设置。 **Q: UV Cache 占用过多磁盘空间** A: 由于频繁使用 `uvx` 命令,cache 可能会累积到数十 GB。建议定期清理: @@ -220,21 +245,11 @@ uv cache clean ``` 详细说明请参考:[Cache 管理指南](docs/zh-CN/cache-management.md) -**Q: Gemini Pro 2.5 无法解析图片** -A: 已知问题,Gemini Pro 2.5 可能无法正确解析上传的图片内容。实测 Claude-4-Sonnet 可以正常解析图片。建议使用 Claude 模型获得更好的图片理解能力。 - -**Q: 多屏幕视窗定位问题** -A: 已在 v2.1.1 修复。进入「⚙️ 设置」标签页,勾选「总是在主屏幕中心显示窗口」即可解决窗口定位问题。特别适用于 T 字型屏幕排列等复杂多屏幕配置。 - -**Q: WSL 环境下无法启动浏览器** -A: v2.2.5 已新增 WSL 环境支持。如果仍有问题: -1. 确认 WSL 版本(建议使用 WSL 2) -2. 检查 Windows 浏览器是否正常安装 -3. 尝试手动测试:在 WSL 中执行 `cmd.exe /c start https://www.google.com` -4. 如果安装了 `wslu` 套件,也可尝试 `wslview` 命令 - -**Q: WSL 环境被误判为远程环境** -A: v2.2.5 已修复此问题。WSL 环境现在会被正确识别并使用 Web UI 配合 Windows 浏览器启动,而不会被误判为远程环境。 +**Q: AI 模型无法解析图片** +A: 各种 AI 模型(包括 Gemini Pro 2.5、Claude 等)在图片解析上可能存在不稳定性,表现为有时能正确识别、有时无法解析上传的图片内容。这是 AI 视觉理解技术的已知限制。建议: +1. 确保图片质量良好(高对比度、清晰文字) +2. 多尝试几次上传,通常重试可以成功 +3. 如持续无法解析,可尝试调整图片大小或格式 ## 🙏 致谢 diff --git a/README.zh-TW.md b/README.zh-TW.md index 4320f90..0794410 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -42,12 +42,19 @@ - **智能偵測**:根據系統語言自動選擇 - **即時切換**:介面內可直接切換語言 -### ✨ WSL 環境支援(v2.2.5 新功能) +### ✨ WSL 環境支援(v2.2.5) - **自動檢測**:智能識別 WSL (Windows Subsystem for Linux) 環境 - **瀏覽器整合**:WSL 環境下自動啟動 Windows 瀏覽器 - **多種啟動方式**:支援 `cmd.exe`、`powershell.exe`、`wslview` 等多種瀏覽器啟動方法 - **無縫體驗**:WSL 用戶可直接使用 Web UI,無需額外配置 +### 🌐 SSH Remote 環境支援(v2.3.0 新功能) +- **智能檢測**:自動識別 SSH Remote 環境(Cursor SSH Remote、VS Code Remote SSH 等) +- **瀏覽器啟動指引**:當無法自動啟動瀏覽器時,提供清晰的解決方案 +- **端口轉發支援**:完整的端口轉發設定指引和故障排除 +- **MCP 整合優化**:改善與 MCP 系統的整合,提供更穩定的連接體驗 +- **詳細文檔**:[SSH Remote 環境使用指南](docs/zh-TW/ssh-remote/browser-launch-issues.md) + ## 🖥️ 介面預覽 ### Qt GUI 介面(重構版) @@ -180,14 +187,29 @@ uvx --with-editable . mcp-feedback-enhanced test --web # 測試 Web UI (自 📋 **完整版本更新記錄:** [RELEASE_NOTES/CHANGELOG.zh-TW.md](RELEASE_NOTES/CHANGELOG.zh-TW.md) -### 最新版本亮點(v2.2.5) -- ✨ **WSL 環境支援**: 新增 WSL (Windows Subsystem for Linux) 環境的完整支援 -- 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式 -- 🎯 **環境檢測優化**: 改進遠端環境檢測邏輯,WSL 不再被誤判為遠端環境 -- 🧪 **測試體驗提升**: 測試模式下自動嘗試啟動瀏覽器,提供更好的測試體驗 +### 最新版本亮點(v2.3.0) +- 🌐 **SSH Remote 環境支援**: 解決 Cursor SSH Remote 無法啟動瀏覽器的問題,提供清晰的使用指引 +- 🛡️ **錯誤提示改善**: 當發生錯誤時,提供更友善的錯誤訊息和解決建議 +- 🧹 **自動清理功能**: 自動清理臨時文件和過期會話,保持系統整潔 +- 📊 **記憶體監控**: 監控記憶體使用情況,防止系統資源不足 +- 🔧 **連線穩定性**: 改善 Web UI 的連線穩定性和錯誤處理 ## 🐛 常見問題 +### 🌐 SSH Remote 環境問題 +**Q: SSH Remote 環境下瀏覽器無法啟動** +A: 這是正常現象。SSH Remote 環境沒有圖形界面,需要手動在本地瀏覽器開啟。詳細解決方案請參考:[SSH Remote 環境使用指南](docs/zh-TW/ssh-remote/browser-launch-issues.md) + +**Q: 為什麼沒有接收到 MCP 新的反饋?** +A: 可能是 WebSocket 連接問題。**解決方法**:直接重新整理瀏覽器頁面。 + +**Q: 為什麼沒有呼叫出 MCP?** +A: 請確認 MCP 工具狀態為綠燈。**解決方法**:反覆開關 MCP 工具,等待幾秒讓系統重新連接。 + +**Q: Augment 無法啟動 MCP** +A: **解決方法**:完全關閉並重新啟動 VS Code 或 Cursor,重新開啟專案。 + +### 🔧 一般問題 **Q: 出現 "Unexpected token 'D'" 錯誤** A: 調試輸出干擾。設置 `MCP_DEBUG=false` 或移除該環境變數。 diff --git a/RELEASE_NOTES/CHANGELOG.en.md b/RELEASE_NOTES/CHANGELOG.en.md index 596f6f3..ae884a4 100644 --- a/RELEASE_NOTES/CHANGELOG.en.md +++ b/RELEASE_NOTES/CHANGELOG.en.md @@ -2,38 +2,32 @@ This document records all version updates for **MCP Feedback Enhanced**. -## [v2.2.5] - WSL Environment Support & Cross-Platform Enhancement -# Release v2.2.5 - WSL Environment Support & Cross-Platform Enhancement +## [v2.3.0] - System Stability & Resource Management Enhancement -## 🌟 Highlights -This version introduces comprehensive support for WSL (Windows Subsystem for Linux) environments, enabling WSL users to seamlessly use this tool with automatic Windows browser launching, significantly improving cross-platform development experience. +### 🌟 Highlights +This version focuses on improving system stability and user experience, particularly solving the browser launch issue in Cursor SSH Remote environments. -## ✨ New Features -- 🐧 **WSL Environment Detection**: Automatically identifies WSL environments and provides specialized support logic -- 🌐 **Smart Browser Launching**: Automatically invokes Windows browser in WSL environments with multiple launch methods -- 🔧 **Cross-Platform Testing Enhancement**: Test functionality integrates WSL detection for improved test coverage +### ✨ New Features +- 🌐 **SSH Remote Environment Support**: Solved Cursor SSH Remote browser launch issues with clear usage guidance +- 🛡️ **Error Message Improvements**: Provides more user-friendly error messages and solution suggestions when errors occur +- 🧹 **Auto-cleanup Features**: Automatically cleans temporary files and expired sessions to keep the system tidy +- 📊 **Memory Monitoring**: Monitors memory usage to prevent system resource shortage -## 🚀 Improvements -- 🎯 **Environment Detection Optimization**: Improved remote environment detection logic, WSL no longer misidentified as remote environment -- 📊 **System Information Enhancement**: System information tool now displays WSL environment status -- 🧪 **Testing Experience Improvement**: Test mode automatically attempts browser launching for better testing experience +### 🚀 Improvements +- 💾 **Resource Management Optimization**: Better system resource management for improved performance +- 🔧 **Enhanced Error Handling**: Provides clearer explanations and solutions when problems occur +- 🌐 **Connection Stability**: Improved Web UI connection stability +- 🖼️ **Image Upload Optimization**: Enhanced stability of image upload functionality -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reports: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Project Homepage: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) +### 🐛 Bug Fixes +- 🌐 **Connection Issues**: Fixed WebSocket connection related problems +- 🔄 **Session Management**: Fixed session state tracking issues +- 🖼️ **Image Processing**: Fixed event handling issues during image upload --- +## [v2.2.5] - WSL Environment Support & Cross-Platform Enhancement + ### ✨ New Features - 🐧 **WSL Environment Detection**: Automatically identifies WSL environments and provides specialized support logic - 🌐 **Smart Browser Launching**: Automatically invokes Windows browser in WSL environments with multiple launch methods diff --git a/RELEASE_NOTES/CHANGELOG.zh-CN.md b/RELEASE_NOTES/CHANGELOG.zh-CN.md index cfc7ea7..9a18a21 100644 --- a/RELEASE_NOTES/CHANGELOG.zh-CN.md +++ b/RELEASE_NOTES/CHANGELOG.zh-CN.md @@ -2,56 +2,27 @@ 本文件记录了 **MCP Feedback Enhanced** 的所有版本更新内容。 -## [v2.2.5] - WSL 环境支持与跨平台增强 -# Release v2.2.5 - WSL 环境支持与跨平台增强 +## [v2.3.0] - 系统稳定性与资源管理增强 -## 🌟 亮点 -本版本新增了 WSL (Windows Subsystem for Linux) 环境的完整支持,让 WSL 用户能够无缝使用本工具并自动启动 Windows 浏览器,大幅提升跨平台开发体验。 - -## ✨ 新功能 -- 🐧 **WSL 环境检测**: 自动识别 WSL 环境,提供专门的支持逻辑 -- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式 -- 🔧 **跨平台测试增强**: 测试功能整合 WSL 检测,提升测试覆盖率 - -## 🚀 改进功能 -- 🎯 **环境检测优化**: 改进远程环境检测逻辑,WSL 不再被误判为远程环境 -- 📊 **系统信息增强**: 系统信息工具新增 WSL 环境状态显示 -- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 项目首页: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) - ---- +### 🌟 亮点 +本版本专注于提升系统稳定性和使用体验,特别解决了 Cursor SSH Remote 环境下无法启动浏览器的问题。 ### ✨ 新功能 -- 🐧 **WSL 环境检测**: 自动识别 WSL 环境,提供专门的支持逻辑 -- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式 -- 🔧 **跨平台测试增强**: 测试功能整合 WSL 检测,提升测试覆盖率 +- 🌐 **SSH Remote 环境支持**: 解决 Cursor SSH Remote 无法启动浏览器的问题,提供清晰的使用指引 +- 🛡️ **错误提示改善**: 当发生错误时,提供更友善的错误信息和解决建议 +- 🧹 **自动清理功能**: 自动清理临时文件和过期会话,保持系统整洁 +- 📊 **内存监控**: 监控内存使用情况,防止系统资源不足 ### 🚀 改进功能 -- 🎯 **环境检测优化**: 改进远程环境检测逻辑,WSL 不再被误判为远程环境 -- 📊 **系统信息增强**: 系统信息工具新增 WSL 环境状态显示 -- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验 - ---- - -## [v2.2.4] - GUI 体验优化与问题修复 +- 💾 **资源管理优化**: 更好地管理系统资源,提升运行效率 +- 🔧 **错误处理增强**: 遇到问题时提供更清楚的说明和解决方案 +- 🌐 **连接稳定性**: 改善 Web UI 的连接稳定性 +- 🖼️ **图片上传优化**: 改善图片上传功能的稳定性 ### 🐛 问题修复 -- 🖼️ **图片重复粘贴修复**: 解决 GUI 界面中使用 Ctrl+V 复制粘贴图片时出现重复粘贴的问题 -- 🌐 **语系切换修复**: 修复图片设定区域在语言切换时文字没有正确翻译的问题 -- 📝 **字体可读性改善**: 调整图片设定区域的字体大小,提升文字可读性 +- 🌐 **连接问题**: 修复 WebSocket 连接的相关问题 +- 🔄 **会话管理**: 修复会话状态跟踪的问题 +- 🖼️ **图片处理**: 修复图片上传时的事件处理问题 --- diff --git a/RELEASE_NOTES/CHANGELOG.zh-TW.md b/RELEASE_NOTES/CHANGELOG.zh-TW.md index 8f04371..bfaf78e 100644 --- a/RELEASE_NOTES/CHANGELOG.zh-TW.md +++ b/RELEASE_NOTES/CHANGELOG.zh-TW.md @@ -2,38 +2,32 @@ 本文件記錄了 **MCP Feedback Enhanced** 的所有版本更新內容。 -## [v2.2.5] - WSL 環境支援與跨平台增強 -# Release v2.2.5 - WSL 環境支援與跨平台增強 +## [v2.3.0] - 系統穩定性與資源管理增強 -## 🌟 亮點 -本版本新增了 WSL (Windows Subsystem for Linux) 環境的完整支援,讓 WSL 用戶能夠無縫使用本工具並自動啟動 Windows 瀏覽器,大幅提升跨平台開發體驗。 +### 🌟 亮點 +本版本專注於提升系統穩定性和使用體驗,特別解決了 Cursor SSH Remote 環境下無法啟動瀏覽器的問題。 -## ✨ 新功能 -- 🐧 **WSL 環境檢測**: 自動識別 WSL 環境,提供專門的支援邏輯 -- 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式 -- 🔧 **跨平台測試增強**: 測試功能整合 WSL 檢測,提升測試覆蓋率 +### ✨ 新功能 +- 🌐 **SSH Remote 環境支援**: 解決 Cursor SSH Remote 無法啟動瀏覽器的問題,提供清晰的使用指引 +- 🛡️ **錯誤提示改善**: 當發生錯誤時,提供更友善的錯誤訊息和解決建議 +- 🧹 **自動清理功能**: 自動清理臨時文件和過期會話,保持系統整潔 +- 📊 **記憶體監控**: 監控記憶體使用情況,防止系統資源不足 -## 🚀 改進功能 -- 🎯 **環境檢測優化**: 改進遠端環境檢測邏輯,WSL 不再被誤判為遠端環境 -- 📊 **系統資訊增強**: 系統資訊工具新增 WSL 環境狀態顯示 -- 🧪 **測試體驗提升**: 測試模式下自動嘗試啟動瀏覽器,提供更好的測試體驗 +### 🚀 改進功能 +- 💾 **資源管理優化**: 更好地管理系統資源,提升運行效率 +- 🔧 **錯誤處理增強**: 遇到問題時提供更清楚的說明和解決方案 +- 🌐 **連線穩定性**: 改善 Web UI 的連線穩定性 +- 🖼️ **圖片上傳優化**: 改善圖片上傳功能的穩定性 -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 專案首頁: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) +### 🐛 問題修復 +- 🌐 **連線問題**: 修復 WebSocket 連線的相關問題 +- 🔄 **會話管理**: 修復會話狀態追蹤的問題 +- 🖼️ **圖片處理**: 修復圖片上傳時的事件處理問題 --- +## [v2.2.5] - WSL 環境支援與跨平台增強 + ### ✨ 新功能 - 🐧 **WSL 環境檢測**: 自動識別 WSL 環境,提供專門的支援邏輯 - 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式 diff --git a/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md b/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md new file mode 100644 index 0000000..bece0c7 --- /dev/null +++ b/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md @@ -0,0 +1,147 @@ +# 簡化發布流程 / Simplified Release Workflow + +## 🎯 概述 / Overview + +此專案已採用簡化的發布流程,不再需要建立版本化目錄(如 `v2.3.0/`),而是直接更新 CHANGELOG 文件。 + +This project now uses a simplified release workflow that no longer requires creating versioned directories (like `v2.3.0/`), but instead directly updates CHANGELOG files. + +## 📋 新的發布流程 / New Release Process + +### 1. 更新 CHANGELOG 文件 / Update CHANGELOG Files + +在發布前,請手動更新以下三個文件: +Before releasing, manually update these three files: + +- `RELEASE_NOTES/CHANGELOG.en.md` +- `RELEASE_NOTES/CHANGELOG.zh-TW.md` +- `RELEASE_NOTES/CHANGELOG.zh-CN.md` + +### 2. CHANGELOG 格式要求 / CHANGELOG Format Requirements + +每個新版本應該按照以下格式添加到 CHANGELOG 文件的頂部: +Each new version should be added to the top of CHANGELOG files in this format: + +```markdown +## [v2.3.0] - 版本標題 / Version Title + +### 🌟 亮點 / Highlights +本次發佈的主要特色... + +### ✨ 新功能 / New Features +- 🆕 **功能名稱**: 功能描述 + +### 🐛 錯誤修復 / Bug Fixes +- 🔧 **問題修復**: 修復描述 + +### 🚀 改進功能 / Improvements +- ⚡ **效能優化**: 優化描述 + +--- +``` + +### 3. 執行發布 / Execute Release + +1. 確保所有 CHANGELOG 文件都已更新 + Ensure all CHANGELOG files are updated + +2. 前往 GitHub Actions 頁面 + Go to GitHub Actions page + +3. 執行 "Auto Release to PyPI" workflow + Run "Auto Release to PyPI" workflow + +4. 選擇版本類型(patch/minor/major) + Select version type (patch/minor/major) + +### 📊 版本類型說明 / Version Type Explanation + +選擇適當的版本類型非常重要,請根據變更內容選擇: +Choosing the appropriate version type is important, select based on the changes: + +#### 🔧 Patch (修補版本) +- **用途 / Usage**: 錯誤修復、小幅改進、安全修補 +- **範例 / Example**: `2.3.0 → 2.3.1` +- **適用情況 / When to use**: + - 🐛 修復 bug / Bug fixes + - 🔒 安全性修補 / Security patches + - 📝 文檔更新 / Documentation updates + - 🎨 小幅 UI 調整 / Minor UI tweaks + +#### ✨ Minor (次要版本) +- **用途 / Usage**: 新功能、功能增強、向後相容的變更 +- **範例 / Example**: `2.3.0 → 2.4.0` +- **適用情況 / When to use**: + - 🆕 新增功能 / New features + - 🚀 功能增強 / Feature enhancements + - 🎯 效能改進 / Performance improvements + - 🌐 新的語言支援 / New language support + +#### 🚨 Major (主要版本) +- **用途 / Usage**: 重大變更、不向後相容的修改、架構重構 +- **範例 / Example**: `2.3.0 → 3.0.0` +- **適用情況 / When to use**: + - 💥 破壞性變更 / Breaking changes + - 🏗️ 架構重構 / Architecture refactoring + - 🔄 API 變更 / API changes + - 📦 依賴項重大更新 / Major dependency updates + +#### 🤔 如何選擇 / How to Choose + +**問自己這些問題 / Ask yourself these questions**: + +1. **會破壞現有功能嗎?** / **Will it break existing functionality?** + - 是 / Yes → Major + - 否 / No → 繼續下一個問題 / Continue to next question + +2. **是否新增了功能?** / **Does it add new functionality?** + - 是 / Yes → Minor + - 否 / No → 繼續下一個問題 / Continue to next question + +3. **只是修復或小幅改進?** / **Just fixes or minor improvements?** + - 是 / Yes → Patch + +## 🔄 自動化流程 / Automated Process + +GitHub workflow 將自動: +The GitHub workflow will automatically: + +1. ✅ 版本號碼升級 / Version bump +2. ✅ 從 CHANGELOG 提取亮點 / Extract highlights from CHANGELOG +3. ✅ 生成多語系 GitHub Release / Generate multi-language GitHub Release +4. ✅ 發布到 PyPI / Publish to PyPI +5. ✅ 建立 Git 標籤 / Create Git tags + +## 📦 GitHub Release 格式 / GitHub Release Format + +自動生成的 Release 將包含: +Auto-generated releases will include: + +- 🌟 版本亮點 / Version highlights +- 🌐 多語系 CHANGELOG 連結 / Multi-language CHANGELOG links +- 📦 安裝指令 / Installation commands +- 🔗 相關連結 / Related links + +## ⚠️ 注意事項 / Important Notes + +1. **不再需要版本目錄**:舊的 `RELEASE_NOTES/v2.x.x/` 目錄結構已棄用 + **No more version directories**: Old `RELEASE_NOTES/v2.x.x/` directory structure is deprecated + +2. **手動更新 CHANGELOG**:發布前必須手動更新 CHANGELOG 文件 + **Manual CHANGELOG updates**: CHANGELOG files must be manually updated before release + +3. **格式一致性**:請保持 CHANGELOG 格式的一致性以確保自動提取正常運作 + **Format consistency**: Maintain CHANGELOG format consistency for proper auto-extraction + +## 🗂️ 舊版本目錄清理 / Old Version Directory Cleanup + +現有的版本目錄(`v2.2.1` 到 `v2.2.5`)可以選擇性保留作為歷史記錄,或者清理以簡化專案結構。 + +Existing version directories (`v2.2.1` to `v2.2.5`) can optionally be kept for historical records or cleaned up to simplify project structure. + +## 🚀 優點 / Benefits + +- ✅ 減少維護負擔 / Reduced maintenance burden +- ✅ 單一真實來源 / Single source of truth +- ✅ 簡化的專案結構 / Simplified project structure +- ✅ 自動化的 Release 生成 / Automated release generation diff --git a/RELEASE_NOTES/v2.2.1/en.md b/RELEASE_NOTES/v2.2.1/en.md deleted file mode 100644 index bbaa681..0000000 --- a/RELEASE_NOTES/v2.2.1/en.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.1 - Window Optimization & Unified Settings Interface - -## 🌟 Highlights -This release primarily addresses GUI window size constraints, implements smart window state saving mechanisms, and optimizes the unified settings interface. - -## 🚀 Improvements -- 🖥️ **Window Size Constraint Removal**: Removed GUI main window minimum size limit from 1000×800 to 400×300, allowing users to freely adjust window size for different use cases -- 💾 **Real-time Window State Saving**: Implemented real-time saving mechanism for window size and position changes, with debounce delay to avoid excessive I/O operations -- ⚙️ **Unified Settings Interface Optimization**: Improved GUI settings page configuration saving logic to avoid setting conflicts, ensuring correct window positioning and size settings -- 🎯 **Smart Window Size Saving**: In "Always center display" mode, correctly saves window size (but not position); in "Smart positioning" mode, saves complete window state - -## 🐛 Bug Fixes -- 🔧 **Window Size Constraint**: Fixed GUI window unable to resize to small dimensions issue (fixes #10 part one) -- 🛡️ **Setting Conflicts**: Fixed potential configuration conflicts during settings save operations - -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.1 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reporting: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Issues Addressed: #10 (partially completed) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.1/zh-CN.md b/RELEASE_NOTES/v2.2.1/zh-CN.md deleted file mode 100644 index 793c239..0000000 --- a/RELEASE_NOTES/v2.2.1/zh-CN.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.1 - 窗口优化与统一设置接口 - -## 🌟 亮点 -本版本主要解决了 GUI 窗口大小限制问题,实现了窗口状态的智能保存机制,并优化了设置接口的统一性。 - -## 🚀 改进功能 -- 🖥️ **窗口大小限制解除**: 解除 GUI 主窗口最小大小限制,从 1000×800 降至 400×300,让用户可以自由调整窗口大小以符合不同使用场景 -- 💾 **窗口状态实时保存**: 实现窗口大小与位置的即时保存机制,支持防抖延迟避免过度频繁的 I/O 操作 -- ⚙️ **统一设置接口优化**: 改进 GUI 设置版面的配置保存逻辑,避免设置冲突,确保窗口定位与大小设置的正确性 -- 🎯 **智能窗口大小保存**: 「总是在主屏幕中心显示」模式下正确保存窗口大小(但不保存位置),「智能定位」模式下保存完整的窗口状态 - -## 🐛 问题修复 -- 🔧 **窗口大小限制**: 解决 GUI 窗口无法调整至小尺寸的问题 (fixes #10 第一部分) -- 🛡️ **设置冲突**: 修复设置保存时可能出现的配置冲突问题 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.1 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 解决问题: #10 (部分完成) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.1/zh-TW.md b/RELEASE_NOTES/v2.2.1/zh-TW.md deleted file mode 100644 index aca5a3f..0000000 --- a/RELEASE_NOTES/v2.2.1/zh-TW.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.1 - 視窗優化與統一設定接口 - -## 🌟 亮點 -本版本主要解決了 GUI 視窗大小限制問題,實現了視窗狀態的智能保存機制,並優化了設定接口的統一性。 - -## 🚀 改進功能 -- 🖥️ **視窗大小限制解除**: 解除 GUI 主視窗最小大小限制,從 1000×800 降至 400×300,讓用戶可以自由調整視窗大小以符合不同使用場景 -- 💾 **視窗狀態實時保存**: 實現視窗大小與位置的即時保存機制,支援防抖延遲避免過度頻繁的 I/O 操作 -- ⚙️ **統一設定接口優化**: 改進 GUI 設定版面的配置保存邏輯,避免設定衝突,確保視窗定位與大小設定的正確性 -- 🎯 **智能視窗大小保存**: 「總是在主螢幕中心顯示」模式下正確保存視窗大小(但不保存位置),「智能定位」模式下保存完整的視窗狀態 - -## 🐛 問題修復 -- 🔧 **視窗大小限制**: 解決 GUI 視窗無法調整至小尺寸的問題 (fixes #10 第一部分) -- 🛡️ **設定衝突**: 修復設定保存時可能出現的配置衝突問題 - -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.1 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 解決問題: #10 (部分完成) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.2/en.md b/RELEASE_NOTES/v2.2.2/en.md deleted file mode 100644 index 69a134d..0000000 --- a/RELEASE_NOTES/v2.2.2/en.md +++ /dev/null @@ -1,30 +0,0 @@ -# Release v2.2.2 - Timeout Auto-cleanup Fix - -## 🌟 Highlights -This version fixes a critical resource management issue where GUI/Web UI interfaces were not properly closed when MCP sessions ended due to timeout, causing the interfaces to remain open and unresponsive. - -## 🐛 Bug Fixes -- 🔄 **Timeout Auto-cleanup**: Fixed GUI/Web UI not automatically closing after MCP session timeout (default 600 seconds) -- 🛡️ **Resource Management Optimization**: Improved timeout handling mechanism to ensure proper cleanup and closure of all UI resources on timeout -- ⚡ **Enhanced Timeout Detection**: Strengthened timeout detection logic to correctly handle timeout events in various scenarios -- 🔧 **Interface Response Improvement**: Enhanced Web UI frontend handling of session timeout events - -## 🚀 Technical Improvements -- 📦 **Web Session Management**: Refactored WebFeedbackSession timeout handling logic -- 🎯 **QTimer Integration**: Introduced precise QTimer timeout control mechanism in GUI -- 🌐 **Frontend Communication Optimization**: Improved timeout message communication between Web UI frontend and backend -- 🧹 **Resource Cleanup Mechanism**: Added _cleanup_resources_on_timeout method to ensure thorough cleanup - -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.2 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reporting: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Fixed Issue: #5 (GUI/Web UI timeout cleanup) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.2/zh-CN.md b/RELEASE_NOTES/v2.2.2/zh-CN.md deleted file mode 100644 index 7e8437f..0000000 --- a/RELEASE_NOTES/v2.2.2/zh-CN.md +++ /dev/null @@ -1,30 +0,0 @@ -# Release v2.2.2 - 超时自动清理修复 - -## 🌟 亮点 -本版本修复了一个重要的资源管理问题:当 MCP session 因超时结束时,GUI/Web UI 界面没有正确关闭,导致界面持续显示而无法正常关闭。 - -## 🐛 问题修复 -- 🔄 **超时自动清理**: 修复 GUI/Web UI 在 MCP session timeout (默认 600 秒) 后没有自动关闭的问题 -- 🛡️ **资源管理优化**: 改进超时处理机制,确保在超时时正确清理和关闭所有 UI 资源 -- ⚡ **超时检测增强**: 加强超时检测逻辑,确保在各种情况下都能正确处理超时事件 -- 🔧 **界面响应改进**: 改善 Web UI 前端对 session timeout 事件的处理响应 - -## 🚀 技术改进 -- 📦 **Web Session 管理**: 重构 WebFeedbackSession 的超时处理逻辑 -- 🎯 **QTimer 整合**: 在 GUI 中引入精确的 QTimer 超时控制机制 -- 🌐 **前端通信优化**: 改进 Web UI 前端与后端的超时消息传递 -- 🧹 **资源清理机制**: 新增 _cleanup_resources_on_timeout 方法确保彻底清理 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.2 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 解决问题: #5 (GUI/Web UI timeout cleanup) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.2/zh-TW.md b/RELEASE_NOTES/v2.2.2/zh-TW.md deleted file mode 100644 index c83280b..0000000 --- a/RELEASE_NOTES/v2.2.2/zh-TW.md +++ /dev/null @@ -1,30 +0,0 @@ -# Release v2.2.2 - 超時自動清理修復 - -## 🌟 亮點 -本版本修復了一個重要的資源管理問題:當 MCP session 因超時結束時,GUI/Web UI 介面沒有正確關閉,導致介面持續顯示而無法正常關閉。 - -## 🐛 問題修復 -- 🔄 **超時自動清理**: 修復 GUI/Web UI 在 MCP session timeout (預設 600 秒) 後沒有自動關閉的問題 -- 🛡️ **資源管理優化**: 改進超時處理機制,確保在超時時正確清理和關閉所有 UI 資源 -- ⚡ **超時檢測增強**: 加強超時檢測邏輯,確保在各種情況下都能正確處理超時事件 -- 🔧 **介面回應改進**: 改善 Web UI 前端對 session timeout 事件的處理回應 - -## 🚀 技術改進 -- 📦 **Web Session 管理**: 重構 WebFeedbackSession 的超時處理邏輯 -- 🎯 **QTimer 整合**: 在 GUI 中引入精確的 QTimer 超時控制機制 -- 🌐 **前端通訊優化**: 改進 Web UI 前端與後端的超時訊息傳遞 -- 🧹 **資源清理機制**: 新增 _cleanup_resources_on_timeout 方法確保徹底清理 - -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.2 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 解決問題: #5 (GUI/Web UI timeout cleanup) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.3/en.md b/RELEASE_NOTES/v2.2.3/en.md deleted file mode 100644 index daae09f..0000000 --- a/RELEASE_NOTES/v2.2.3/en.md +++ /dev/null @@ -1,42 +0,0 @@ -# Release v2.2.3 - Timeout Control & Image Settings Enhancement - -## 🌟 Highlights -This version introduces user-controllable timeout settings and flexible image upload configuration options, while improving UV Cache management tools to enhance the overall user experience. - -## ✨ New Features -- ⏰ **User Timeout Control**: Added customizable timeout settings with flexible range from 30 seconds to 2 hours -- ⏱️ **Countdown Timer**: Real-time countdown timer display at the top of the interface for visual time reminders -- 🖼️ **Image Size Limits**: Added image upload size limit settings (unlimited/1MB/3MB/5MB) -- 🔧 **Base64 Compatibility Mode**: Added Base64 detail mode to improve image recognition compatibility with AI models -- 🧹 **UV Cache Management Tool**: Added `cleanup_cache.py` script to help manage and clean UV cache space - -## 🚀 Improvements -- 📚 **Documentation Structure Optimization**: Reorganized documentation directory structure, moved images to `docs/{language}/images/` paths -- 📖 **Cache Management Guide**: Added detailed UV Cache management guide with automated cleanup solutions -- 🎯 **Smart Compatibility Hints**: Automatically display Base64 compatibility mode suggestions when image upload fails -- 🔄 **Settings Sync Mechanism**: Improved image settings synchronization between different interface modes - -## 🐛 Bug Fixes -- 🛡️ **Timeout Handling Optimization**: Improved coordination between user-defined timeout and MCP system timeout -- 🖥️ **Interface Auto-close**: Fixed interface auto-close and resource cleanup logic after timeout -- 📱 **Responsive Layout**: Optimized timeout control component display on small screen devices - -## 🔧 Technical Improvements -- 🎛️ **Timeout Control Architecture**: Implemented separated design for frontend countdown timer and backend timeout handling -- 📊 **Image Processing Optimization**: Improved image upload size checking and format validation mechanisms -- 🗂️ **Settings Persistence**: Enhanced settings saving mechanism to ensure correct saving and loading of user preferences -- 🧰 **Tool Script Enhancement**: Added cross-platform cache cleanup tool with support for force cleanup and preview modes - -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.3 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reporting: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Related PRs: #22 (Timeout Control Feature), #19 (Image Settings Feature) diff --git a/RELEASE_NOTES/v2.2.3/zh-CN.md b/RELEASE_NOTES/v2.2.3/zh-CN.md deleted file mode 100644 index 766aace..0000000 --- a/RELEASE_NOTES/v2.2.3/zh-CN.md +++ /dev/null @@ -1,42 +0,0 @@ -# Release v2.2.3 - 超时控制与图片设置增强 - -## 🌟 亮点 -本版本新增了用户可控制的超时设置功能,以及灵活的图片上传设置选项,同时完善了 UV Cache 管理工具,提升整体使用体验。 - -## ✨ 新功能 -- ⏰ **用户超时控制**: 新增可自定义的超时设置功能,支持 30 秒至 2 小时的弹性设置 -- ⏱️ **倒数计时器**: 界面顶部显示实时倒数计时器,提供可视化的时间提醒 -- 🖼️ **图片大小限制**: 新增图片上传大小限制设置(无限制/1MB/3MB/5MB) -- 🔧 **Base64 兼容模式**: 新增 Base64 详细模式,提升部分 AI 模型的图片识别兼容性 -- 🧹 **UV Cache 管理工具**: 新增 `cleanup_cache.py` 脚本,协助管理和清理 UV cache 空间 - -## 🚀 改进功能 -- 📚 **文档结构优化**: 重新整理文档目录结构,将图片移至 `docs/{语言}/images/` 路径 -- 📖 **Cache 管理指南**: 新增详细的 UV Cache 管理指南,包含自动化清理方案 -- 🎯 **智能兼容性提示**: 当图片上传失败时自动显示 Base64 兼容模式建议 -- 🔄 **设置同步机制**: 改进图片设置在不同界面模式间的同步机制 - -## 🐛 问题修复 -- 🛡️ **超时处理优化**: 改进用户自定义超时与 MCP 系统超时的协调机制 -- 🖥️ **界面自动关闭**: 修复超时后界面自动关闭和资源清理逻辑 -- 📱 **响应式布局**: 优化超时控制组件在小屏幕设备上的显示效果 - -## 🔧 技术改进 -- 🎛️ **超时控制架构**: 实现前端倒数计时器与后端超时处理的分离设计 -- 📊 **图片处理优化**: 改进图片上传的大小检查和格式验证机制 -- 🗂️ **设置持久化**: 增强设置保存机制,确保用户偏好的正确保存和载入 -- 🧰 **工具脚本增强**: 新增跨平台的 cache 清理工具,支持强制清理和预览模式 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.3 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 相关 PR: #22 (超时控制功能), #19 (图片设置功能) diff --git a/RELEASE_NOTES/v2.2.3/zh-TW.md b/RELEASE_NOTES/v2.2.3/zh-TW.md deleted file mode 100644 index ce761b4..0000000 --- a/RELEASE_NOTES/v2.2.3/zh-TW.md +++ /dev/null @@ -1,42 +0,0 @@ -# Release v2.2.3 - 超時控制與圖片設定增強 - -## 🌟 亮點 -本版本新增了用戶可控制的超時設定功能,以及靈活的圖片上傳設定選項,同時完善了 UV Cache 管理工具,提升整體使用體驗。 - -## ✨ 新功能 -- ⏰ **用戶超時控制**: 新增可自訂的超時設定功能,支援 30 秒至 2 小時的彈性設定 -- ⏱️ **倒數計時器**: 介面頂部顯示即時倒數計時器,提供視覺化的時間提醒 -- 🖼️ **圖片大小限制**: 新增圖片上傳大小限制設定(無限制/1MB/3MB/5MB) -- 🔧 **Base64 相容模式**: 新增 Base64 詳細模式,提升部分 AI 模型的圖片識別相容性 -- 🧹 **UV Cache 管理工具**: 新增 `cleanup_cache.py` 腳本,協助管理和清理 UV cache 空間 - -## 🚀 改進功能 -- 📚 **文檔結構優化**: 重新整理文檔目錄結構,將圖片移至 `docs/{語言}/images/` 路徑 -- 📖 **Cache 管理指南**: 新增詳細的 UV Cache 管理指南,包含自動化清理方案 -- 🎯 **智能相容性提示**: 當圖片上傳失敗時自動顯示 Base64 相容模式建議 -- 🔄 **設定同步機制**: 改進圖片設定在不同介面模式間的同步機制 - -## 🐛 問題修復 -- 🛡️ **超時處理優化**: 改進用戶自訂超時與 MCP 系統超時的協調機制 -- 🖥️ **介面自動關閉**: 修復超時後介面自動關閉和資源清理邏輯 -- 📱 **響應式佈局**: 優化超時控制元件在小螢幕設備上的顯示效果 - -## 🔧 技術改進 -- 🎛️ **超時控制架構**: 實現前端倒數計時器與後端超時處理的分離設計 -- 📊 **圖片處理優化**: 改進圖片上傳的大小檢查和格式驗證機制 -- 🗂️ **設定持久化**: 增強設定保存機制,確保用戶偏好的正確保存和載入 -- 🧰 **工具腳本增強**: 新增跨平台的 cache 清理工具,支援強制清理和預覽模式 - -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.3 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 相關 PR: #22 (超時控制功能), #19 (圖片設定功能) diff --git a/RELEASE_NOTES/v2.2.4/en.md b/RELEASE_NOTES/v2.2.4/en.md deleted file mode 100644 index 80eba52..0000000 --- a/RELEASE_NOTES/v2.2.4/en.md +++ /dev/null @@ -1,23 +0,0 @@ -# Release v2.2.4 - GUI Experience Optimization & Bug Fixes - -## 🌟 Highlights -This version focuses on GUI user experience optimization, fixing image copy-paste duplication issues, reorganizing localization file structure, and improving interface text readability. - -## 🐛 Bug Fixes -- 🖼️ **Image Duplicate Paste Fix**: Fixed the issue where Ctrl+V image pasting in GUI would create duplicate images -- 🌐 **Localization Switch Fix**: Fixed image settings area text not translating correctly when switching languages -- 📝 **Font Readability Improvement**: Adjusted font sizes in image settings area for better readability - -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.4 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reports: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Project Homepage: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.4/zh-CN.md b/RELEASE_NOTES/v2.2.4/zh-CN.md deleted file mode 100644 index 827a39d..0000000 --- a/RELEASE_NOTES/v2.2.4/zh-CN.md +++ /dev/null @@ -1,23 +0,0 @@ -# Release v2.2.4 - GUI 体验优化与问题修复 - -## 🌟 亮点 -本版本专注于 GUI 使用体验的优化,修复了图片复制粘贴的重复问题,重新组织了语系文件结构,并改善了界面文字的可读性。 - -## 🐛 问题修复 -- 🖼️ **图片重复粘贴修复**: 解决 GUI 界面中使用 Ctrl+V 复制粘贴图片时出现重复粘贴的问题 -- 🌐 **语系切换修复**: 修复图片设定区域在语言切换时文字没有正确翻译的问题 -- 📝 **字体可读性改善**: 调整图片设定区域的字体大小,提升文字可读性 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.4 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 项目首页: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.4/zh-TW.md b/RELEASE_NOTES/v2.2.4/zh-TW.md deleted file mode 100644 index 05ff546..0000000 --- a/RELEASE_NOTES/v2.2.4/zh-TW.md +++ /dev/null @@ -1,23 +0,0 @@ -# Release v2.2.4 - GUI 體驗優化與問題修復 - -## 🌟 亮點 -本版本專注於 GUI 使用體驗的優化,修復了圖片複製貼上的重複問題,重新組織了語系檔案結構,並改善了介面文字的可讀性。 - -## 🐛 問題修復 -- 🖼️ **圖片重複貼上修復**: 解決 GUI 介面中使用 Ctrl+V 複製貼上圖片時出現重複貼上的問題 -- 🌐 **語系切換修復**: 修復圖片設定區域在語言切換時文字沒有正確翻譯的問題 -- 📝 **字體可讀性改善**: 調整圖片設定區域的字體大小,提升文字可讀性 - -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.4 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 專案首頁: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file diff --git a/RELEASE_NOTES/v2.2.5/en.md b/RELEASE_NOTES/v2.2.5/en.md deleted file mode 100644 index c7a4033..0000000 --- a/RELEASE_NOTES/v2.2.5/en.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.5 - WSL Environment Support & Cross-Platform Enhancement - -## 🌟 Highlights -This version introduces comprehensive support for WSL (Windows Subsystem for Linux) environments, enabling WSL users to seamlessly use this tool with automatic Windows browser launching, significantly improving cross-platform development experience. - -## ✨ New Features -- 🐧 **WSL Environment Detection**: Automatically identifies WSL environments and provides specialized support logic -- 🌐 **Smart Browser Launching**: Automatically invokes Windows browser in WSL environments with multiple launch methods -- 🔧 **Cross-Platform Testing Enhancement**: Test functionality integrates WSL detection for improved test coverage - -## 🚀 Improvements -- 🎯 **Environment Detection Optimization**: Improved remote environment detection logic, WSL no longer misidentified as remote environment -- 📊 **System Information Enhancement**: System information tool now displays WSL environment status -- 🧪 **Testing Experience Improvement**: Test mode automatically attempts browser launching for better testing experience - -## 📦 Installation & Update -```bash -# Quick test latest version -uvx mcp-feedback-enhanced@latest test --gui - -# Update to specific version -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 Related Links -- Full Documentation: [README.md](../../README.md) -- Issue Reports: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- Project Homepage: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/RELEASE_NOTES/v2.2.5/zh-CN.md b/RELEASE_NOTES/v2.2.5/zh-CN.md deleted file mode 100644 index 717cf73..0000000 --- a/RELEASE_NOTES/v2.2.5/zh-CN.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.5 - WSL 环境支持与跨平台增强 - -## 🌟 亮点 -本版本新增了 WSL (Windows Subsystem for Linux) 环境的完整支持,让 WSL 用户能够无缝使用本工具并自动启动 Windows 浏览器,大幅提升跨平台开发体验。 - -## ✨ 新功能 -- 🐧 **WSL 环境检测**: 自动识别 WSL 环境,提供专门的支持逻辑 -- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式 -- 🔧 **跨平台测试增强**: 测试功能整合 WSL 检测,提升测试覆盖率 - -## 🚀 改进功能 -- 🎯 **环境检测优化**: 改进远程环境检测逻辑,WSL 不再被误判为远程环境 -- 📊 **系统信息增强**: 系统信息工具新增 WSL 环境状态显示 -- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验 - -## 📦 安装与更新 -```bash -# 快速测试最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 相关链接 -- 完整文档: [README.zh-CN.md](../../README.zh-CN.md) -- 问题报告: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 项目首页: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/RELEASE_NOTES/v2.2.5/zh-TW.md b/RELEASE_NOTES/v2.2.5/zh-TW.md deleted file mode 100644 index 666d7e6..0000000 --- a/RELEASE_NOTES/v2.2.5/zh-TW.md +++ /dev/null @@ -1,28 +0,0 @@ -# Release v2.2.5 - WSL 環境支援與跨平台增強 - -## 🌟 亮點 -本版本新增了 WSL (Windows Subsystem for Linux) 環境的完整支援,讓 WSL 用戶能夠無縫使用本工具並自動啟動 Windows 瀏覽器,大幅提升跨平台開發體驗。 - -## ✨ 新功能 -- 🐧 **WSL 環境檢測**: 自動識別 WSL 環境,提供專門的支援邏輯 -- 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式 -- 🔧 **跨平台測試增強**: 測試功能整合 WSL 檢測,提升測試覆蓋率 - -## 🚀 改進功能 -- 🎯 **環境檢測優化**: 改進遠端環境檢測邏輯,WSL 不再被誤判為遠端環境 -- 📊 **系統資訊增強**: 系統資訊工具新增 WSL 環境狀態顯示 -- 🧪 **測試體驗提升**: 測試模式下自動嘗試啟動瀏覽器,提供更好的測試體驗 - -## 📦 安裝與更新 -```bash -# 快速測試最新版本 -uvx mcp-feedback-enhanced@latest test --gui - -# 更新到特定版本 -uvx mcp-feedback-enhanced@v2.2.5 test -``` - -## 🔗 相關連結 -- 完整文檔: [README.zh-TW.md](../../README.zh-TW.md) -- 問題回報: [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) -- 專案首頁: [GitHub Repository](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/docs/en/images/ssh-remote-connect-url.png b/docs/en/images/ssh-remote-connect-url.png new file mode 100644 index 0000000..7c67e88 Binary files /dev/null and b/docs/en/images/ssh-remote-connect-url.png differ diff --git a/docs/en/images/ssh-remote-debug-port.png b/docs/en/images/ssh-remote-debug-port.png new file mode 100644 index 0000000..ed04927 Binary files /dev/null and b/docs/en/images/ssh-remote-debug-port.png differ diff --git a/docs/en/images/ssh-remote-port-setting.png b/docs/en/images/ssh-remote-port-setting.png new file mode 100644 index 0000000..80a64bc Binary files /dev/null and b/docs/en/images/ssh-remote-port-setting.png differ diff --git a/docs/en/ssh-remote/browser-launch-issues.md b/docs/en/ssh-remote/browser-launch-issues.md new file mode 100644 index 0000000..cbe0d30 --- /dev/null +++ b/docs/en/ssh-remote/browser-launch-issues.md @@ -0,0 +1,115 @@ +# SSH Remote Environment Browser Launch Issues Solution + +## Problem Description + +When using MCP Feedback Enhanced in SSH Remote environments (such as Cursor SSH Remote, VS Code Remote SSH, etc.), you may encounter the following issues: + +- 🚫 Browser cannot launch automatically +- ❌ "Unable to launch browser" error message +- 🔗 Web UI cannot open in local browser + +## Root Cause Analysis + +SSH Remote environment limitations: +1. **Display Environment Isolation**: Remote server has no graphical interface environment +2. **Network Isolation**: Remote ports cannot be directly accessed locally +3. **No Browser Available**: Remote environments typically don't have browsers installed + +## Solution + +### Step 1: Configure Port (Optional) + +MCP Feedback Enhanced uses port **8765** by default, but you can customize the port: + +![Port Settings](../images/ssh-remote-port-setting.png) + +### Step 2: Wait for MCP Call + +**Important**: Do not manually start the Web UI. Instead, wait for the AI model to call the MCP tool to automatically start it. + +When the AI model calls the `interactive_feedback` tool, the system will automatically start the Web UI. + +### Step 3: Check Port and Connect + +If the browser doesn't launch automatically, you need to manually connect to the Web UI: + +#### Method 1: Check Port Forwarding +Check your SSH Remote environment's port forwarding settings to find the corresponding local port: + +![Connect to URL](../images/ssh-remote-connect-url.png) + +#### Method 2: Use Debug Mode +Enable Debug mode in your IDE, select "Output" → "MCP Log" to see the Web UI URL: + +![Debug Mode Port View](../images/ssh-remote-debug-port.png) + +### Step 4: Open in Local Browser + +1. Copy the URL (usually `http://localhost:8765` or another port) +2. Paste and open in your local browser +3. Start using the Web UI for feedback + +## Port Forwarding Setup + +### VS Code Remote SSH +1. Press `Ctrl+Shift+P` in VS Code +2. Type "Forward a Port" +3. Enter the port number (default 8765) +4. Access `http://localhost:8765` in your local browser + +### Cursor SSH Remote +1. Check Cursor's port forwarding settings +2. Manually add port forwarding rule (port 8765) +3. Access the forwarded port in your local browser + +## Important Reminders + +### ⚠️ Do Not Start Manually +**Do NOT** manually execute commands like `uvx mcp-feedback-enhanced test --web`, as this cannot integrate with the MCP system. + +### ✅ Correct Process +1. Wait for AI model to call MCP tool +2. System automatically starts Web UI +3. Check port forwarding or Debug logs +4. Open corresponding URL in local browser + +## Frequently Asked Questions + +### Q: Why can't the browser launch automatically in SSH Remote environment? +A: SSH Remote environment is headless with no graphical interface, so browsers cannot be launched directly. You need to access through port forwarding in your local browser. + +### Q: How to confirm if Web UI started successfully? +A: Check IDE's Debug output or MCP Log. If you see "Web UI started" message, it means successful startup. + +### Q: What if the port is occupied? +A: Modify the port number in MCP settings, or wait for the system to automatically select another available port. + +### Q: Can't find port forwarding settings? +A: Check your SSH Remote tool documentation, or use Debug mode to view the URL in MCP Log. + +### Q: Why am I not receiving new MCP feedback? +A: There might be a WebSocket connection issue. **Solution**: Simply refresh the browser page to re-establish the WebSocket connection. + +### Q: Why isn't MCP being called? +A: Please confirm the MCP tool status shows green light (indicating normal operation). **Solution**: +- Check the MCP tool status indicator in your IDE +- If not green, try toggling the MCP tool on/off repeatedly +- Wait a few seconds for the system to reconnect + +### Q: Why can't Augment start MCP? +A: Sometimes errors may prevent the MCP tool from showing green status. **Solution**: +- Completely close and restart VS Code or Cursor +- Reopen the project +- Wait for MCP tool to reload and show green light + +## v2.3.0 Improvements + +Improvements for SSH Remote environments in this version: +- ✅ Automatic SSH Remote environment detection +- ✅ Clear guidance when browser cannot launch +- ✅ Display correct access URL +- ✅ Improved error messages and solution suggestions + +## Related Resources + +- [Main Documentation](../../README.md) diff --git a/docs/zh-CN/images/ssh-remote-connect-url.png b/docs/zh-CN/images/ssh-remote-connect-url.png new file mode 100644 index 0000000..7c67e88 Binary files /dev/null and b/docs/zh-CN/images/ssh-remote-connect-url.png differ diff --git a/docs/zh-CN/images/ssh-remote-debug-port.png b/docs/zh-CN/images/ssh-remote-debug-port.png new file mode 100644 index 0000000..ed04927 Binary files /dev/null and b/docs/zh-CN/images/ssh-remote-debug-port.png differ diff --git a/docs/zh-CN/images/ssh-remote-port-setting.png b/docs/zh-CN/images/ssh-remote-port-setting.png new file mode 100644 index 0000000..80a64bc Binary files /dev/null and b/docs/zh-CN/images/ssh-remote-port-setting.png differ diff --git a/docs/zh-CN/ssh-remote/browser-launch-issues.md b/docs/zh-CN/ssh-remote/browser-launch-issues.md new file mode 100644 index 0000000..ae6e539 --- /dev/null +++ b/docs/zh-CN/ssh-remote/browser-launch-issues.md @@ -0,0 +1,115 @@ +# SSH Remote 环境浏览器启动问题解决方案 + +## 问题描述 + +在 SSH Remote 环境(如 Cursor SSH Remote、VS Code Remote SSH 等)中使用 MCP Feedback Enhanced 时,可能会遇到以下问题: + +- 🚫 浏览器无法自动启动 +- ❌ 显示「无法启动浏览器」错误 +- 🔗 Web UI 无法在本地浏览器中打开 + +## 原因分析 + +SSH Remote 环境的限制: +1. **显示环境隔离**: 远程服务器没有图形界面环境 +2. **网络隔离**: 远程端口无法直接在本地访问 +3. **浏览器不存在**: 远程环境通常没有安装浏览器 + +## 解决方案 + +### 步骤一:设置端口(可选) + +MCP Feedback Enhanced 默认使用端口 **8765**,您也可以自定义端口: + +![设置端口](../images/ssh-remote-port-setting.png) + +### 步骤二:等待 MCP 调用 + +**重要**:不要手动启动 Web UI,而是要等待 AI 模型调用 MCP 工具时自动启动。 + +当 AI 模型调用 `interactive_feedback` 工具时,系统会自动启动 Web UI。 + +### 步骤三:查看端口并连接 + +如果浏览器没有自动启动,您需要手动连接到 Web UI: + +#### 方法一:查看端口转发 +查看您的 SSH Remote 环境的端口转发设置,找到对应的本地端口: + +![连接到 URL](../images/ssh-remote-connect-url.png) + +#### 方法二:使用 Debug 模式查看 +在 IDE 中开启 Debug 模式,选择「输出」→「MCP Log」,可以看到 Web UI 的 URL: + +![Debug 模式查看端口](../images/ssh-remote-debug-port.png) + +### 步骤四:在本地浏览器打开 + +1. 复制 URL(通常是 `http://localhost:8765` 或其他端口) +2. 在本地浏览器中粘贴并打开 +3. 开始使用 Web UI 进行反馈 + +## 端口转发设置 + +### VS Code Remote SSH +1. 在 VS Code 中按 `Ctrl+Shift+P` +2. 输入 "Forward a Port" +3. 输入端口号(默认 8765) +4. 在本地浏览器中访问 `http://localhost:8765` + +### Cursor SSH Remote +1. 查看 Cursor 的端口转发设置 +2. 手动添加端口转发规则(端口 8765) +3. 在本地浏览器中访问转发的端口 + +## 重要提醒 + +### ⚠️ 不要手动启动 +**请勿**手动执行 `uvx mcp-feedback-enhanced test --web` 等指令,这样无法与 MCP 系统整合。 + +### ✅ 正确流程 +1. 等待 AI 模型调用 MCP 工具 +2. 系统自动启动 Web UI +3. 查看端口转发或 Debug 日志 +4. 在本地浏览器中打开对应 URL + +## 常见问题 + +### Q: 为什么在 SSH Remote 环境中无法自动打开浏览器? +A: SSH Remote 环境是无头环境(headless),没有图形界面,因此无法直接启动浏览器。需要通过端口转发在本地浏览器中访问。 + +### Q: 如何确认 Web UI 是否正常启动? +A: 查看 IDE 的 Debug 输出或 MCP Log,如果看到 "Web UI 已启动" 的信息,表示启动成功。 + +### Q: 端口被占用怎么办? +A: 在 MCP 设置中修改端口号,或者等待系统自动选择其他可用端口。 + +### Q: 找不到端口转发设置怎么办? +A: 查看您的 SSH Remote 工具文档,或使用 Debug 模式查看 MCP Log 中的 URL。 + +### Q: 为什么没有接收到 MCP 新的反馈? +A: 可能是 WebSocket 连接有问题。**解决方法**:直接重新刷新浏览器页面,这会重新建立 WebSocket 连接。 + +### Q: 为什么没有调用出 MCP? +A: 请确认 MCP 工具状态为绿灯(表示正常运作)。**解决方法**: +- 检查 IDE 中的 MCP 工具状态指示灯 +- 如果不是绿灯,尝试反复开关 MCP 工具 +- 等待几秒钟让系统重新连接 + +### Q: 为什么 Augment 无法启动 MCP? +A: 有时候可能会有错误导致 MCP 工具没有显示绿灯状态。**解决方法**: +- 完全关闭并重新启动 VS Code 或 Cursor +- 重新打开项目 +- 等待 MCP 工具重新加载并显示绿灯 + +## v2.3.0 改进 + +本版本针对 SSH Remote 环境的改进: +- ✅ 自动检测 SSH Remote 环境 +- ✅ 在无法启动浏览器时提供清晰的指引 +- ✅ 显示正确的访问 URL +- ✅ 改善错误提示和解决建议 + +## 相关资源 + +- [主要文档](../../README.zh-CN.md) diff --git a/docs/zh-TW/images/ssh-remote-connect-url.png b/docs/zh-TW/images/ssh-remote-connect-url.png new file mode 100644 index 0000000..7c67e88 Binary files /dev/null and b/docs/zh-TW/images/ssh-remote-connect-url.png differ diff --git a/docs/zh-TW/images/ssh-remote-debug-port.png b/docs/zh-TW/images/ssh-remote-debug-port.png new file mode 100644 index 0000000..ed04927 Binary files /dev/null and b/docs/zh-TW/images/ssh-remote-debug-port.png differ diff --git a/docs/zh-TW/images/ssh-remote-port-setting.png b/docs/zh-TW/images/ssh-remote-port-setting.png new file mode 100644 index 0000000..80a64bc Binary files /dev/null and b/docs/zh-TW/images/ssh-remote-port-setting.png differ diff --git a/docs/zh-TW/ssh-remote/browser-launch-issues.md b/docs/zh-TW/ssh-remote/browser-launch-issues.md new file mode 100644 index 0000000..d386007 --- /dev/null +++ b/docs/zh-TW/ssh-remote/browser-launch-issues.md @@ -0,0 +1,103 @@ +# SSH Remote 環境瀏覽器啟動問題解決方案 + +## 問題描述 + +在 SSH Remote 環境(如 Cursor SSH Remote、VS Code Remote SSH 、WSL 等)中使用 MCP Feedback Enhanced 時,可能會遇到以下問題: + +- 🚫 瀏覽器無法自動啟動 +- ❌ 顯示「無法啟動瀏覽器」錯誤 +- 🔗 Web UI 無法在本地瀏覽器中開啟 + +## 原因分析 + +SSH Remote 環境的限制: +1. **顯示環境隔離**: 遠端伺服器沒有圖形界面環境 +2. **網路隔離**: 遠端端口無法直接在本地訪問 +3. **瀏覽器不存在**: 遠端環境通常沒有安裝瀏覽器 + +## 解決方案 + +### 步驟一:設定端口(可選) + +MCP Feedback Enhanced 預設使用端口 **8765**,您也可以自定義端口: + +![設定端口](../images/ssh-remote-port-setting.png) + +### 步驟二:等待 MCP 呼叫 + +**重要**:不要手動啟動 Web UI,而是要等待 AI 模型呼叫 MCP 工具時自動啟動。 + +當 AI 模型呼叫 `interactive_feedback` 工具時,系統會自動啟動 Web UI。 + +### 步驟三:查看端口並連接 + +如果瀏覽器沒有自動啟動,您需要手動連接到 Web UI: + +#### 方法一:查看端口轉發 +查看您的 SSH Remote 環境的端口轉發設定,找到對應的本地端口: + +![連接到 URL](../images/ssh-remote-connect-url.png) + +#### 方法二:使用 Debug 模式查看 +在 IDE 中開啟 Debug 模式,選擇「輸出」→「MCP Log」,可以看到 Web UI 的 URL: + +![Debug 模式查看端口](../images/ssh-remote-debug-port.png) + +### 步驟四:在本地瀏覽器開啟 + +1. 複製 URL(通常是 `http://localhost:8765` 或其他端口) +2. 在本地瀏覽器中貼上並開啟 +3. 開始使用 Web UI 進行回饋 + +## 端口轉發設定 + +### VS Code Remote SSH +1. 在 VS Code 中按 `Ctrl+Shift+P` +2. 輸入 "Forward a Port" +3. 輸入端口號(預設 8765) +4. 在本地瀏覽器中訪問 `http://localhost:8765` + +### Cursor SSH Remote +1. 查看 Cursor 的端口轉發設定 +2. 手動添加端口轉發規則(端口 8765) +3. 在本地瀏覽器中訪問轉發的端口 + +## 重要提醒 + +### ⚠️ 不要手動啟動 +**請勿**手動執行 `uvx mcp-feedback-enhanced test --web` 等指令,這樣無法與 MCP 系統整合。 + +### ✅ 正確流程 +1. 等待 AI 模型呼叫 MCP 工具 +2. 系統自動啟動 Web UI +3. 查看端口轉發或 Debug 日誌 +4. 在本地瀏覽器中開啟對應 URL + +## 常見問題 + +### Q: 為什麼在 SSH Remote 環境中無法自動開啟瀏覽器? +A: SSH Remote 環境是無頭環境(headless),沒有圖形界面,因此無法直接啟動瀏覽器。需要通過端口轉發在本地瀏覽器中訪問。 + +### Q: 如何確認 Web UI 是否正常啟動? +A: 查看 IDE 的 Debug 輸出或 MCP Log,如果看到 "Web UI 已啟動" 的訊息,表示啟動成功。 + +### Q: 端口被占用怎麼辦? +A: 在 MCP 設定中修改端口號,或者等待系統自動選擇其他可用端口。 + +### Q: 找不到端口轉發設定怎麼辦? +A: 查看您的 SSH Remote 工具文檔,或使用 Debug 模式查看 MCP Log 中的 URL。 + +### Q: 為什麼沒有接收到 MCP 新的反饋? +A: 可能是 WebSocket 連接有問題。**解決方法**:直接重新整理瀏覽器頁面,這會重新建立 WebSocket 連接。 + +### Q: 為什麼沒有呼叫出 MCP? +A: 請確認 MCP 工具狀態為綠燈(表示正常運作)。**解決方法**: +- 檢查 IDE 中的 MCP 工具狀態指示燈 +- 如果不是綠燈,嘗試反覆開關 MCP 工具 +- 等待幾秒鐘讓系統重新連接 + +### Q: 為什麼 Augment 無法啟動 MCP? +A: 有時候可能會有錯誤導致 MCP 工具沒有顯示綠燈狀態。**解決方法**: +- 完全關閉並重新啟動 VS Code 或 Cursor +- 重新開啟專案 +- 等待 MCP 工具重新載入並顯示綠燈 diff --git a/issues/WSL環境預設使用WebUI修復.md b/issues/WSL環境預設使用WebUI修復.md new file mode 100644 index 0000000..53c215b --- /dev/null +++ b/issues/WSL環境預設使用WebUI修復.md @@ -0,0 +1,86 @@ +# WSL 環境預設使用 Web UI 修復 + +## 任務描述 +修復 WSL 環境中 MCP 服務器錯誤地偵測為可使用 GUI 的問題。WSL 環境應該預設使用 Web UI,因為大多數 WSL 安裝都是 Linux 環境,沒有桌面應用支援。 + +## 問題分析 +根據 MCP log 顯示: +``` +[SERVER] 偵測到 WSL 環境(通過 /proc/version) +[SERVER] WSL 環境不被視為遠端環境 +[SERVER] 成功載入 PySide6,可使用 GUI +[SERVER] GUI 可用: True +``` + +問題在於 `can_use_gui()` 函數沒有考慮 WSL 環境的特殊性: +- WSL 環境不被視為遠端環境(正確) +- 但 WSL 環境中即使 PySide6 可以載入,也應該預設使用 Web UI + +## 解決方案 +採用方案 1:在 `can_use_gui()` 函數中直接檢查 WSL 環境 + +### 修改內容 +1. **文件**:`src\mcp_feedback_enhanced\server.py` +2. **函數**:`can_use_gui()` (第 203-230 行) +3. **修改邏輯**: + - 保持現有的遠端環境檢查 + - 在遠端環境檢查後,添加 WSL 環境檢查 + - 如果是 WSL 環境,直接返回 `False` + - 保持其餘 PySide6 載入檢查邏輯不變 + +### 修改前後對比 +**修改前**: +```python +def can_use_gui() -> bool: + if is_remote_environment(): + return False + + try: + from PySide6.QtWidgets import QApplication + debug_log("成功載入 PySide6,可使用 GUI") + return True + # ... +``` + +**修改後**: +```python +def can_use_gui() -> bool: + if is_remote_environment(): + return False + + # WSL 環境預設使用 Web UI + if is_wsl_environment(): + debug_log("WSL 環境偵測到,預設使用 Web UI") + return False + + try: + from PySide6.QtWidgets import QApplication + debug_log("成功載入 PySide6,可使用 GUI") + return True + # ... +``` + +## 預期結果 +修改後,WSL 環境的 MCP log 應該顯示: +``` +[SERVER] 偵測到 WSL 環境(通過 /proc/version) +[SERVER] WSL 環境不被視為遠端環境 +[SERVER] WSL 環境偵測到,預設使用 Web UI +[SERVER] GUI 可用: False +[SERVER] 建議介面: Web UI +``` + +## 影響範圍 +- ✅ WSL 環境將預設使用 Web UI +- ✅ 不影響其他環境的邏輯 +- ✅ 保持向後兼容性 +- ✅ 用戶仍可通過 `FORCE_WEB` 環境變數控制介面選擇 + +## 測試建議 +1. 在 WSL 環境中測試 MCP 服務器啟動 +2. 驗證日誌顯示正確的環境偵測結果 +3. 確認使用 Web UI 而非 GUI +4. 測試 `FORCE_WEB` 環境變數仍然有效 + +## 完成時間 +2025-06-08 01:45:00 diff --git a/src/mcp_feedback_enhanced/gui/locales/en/translations.json b/src/mcp_feedback_enhanced/gui/locales/en/translations.json index a1e8819..ebde0b5 100644 --- a/src/mcp_feedback_enhanced/gui/locales/en/translations.json +++ b/src/mcp_feedback_enhanced/gui/locales/en/translations.json @@ -192,7 +192,46 @@ "confirmClearAll": "Are you sure you want to clear all {count} images?", "confirmClearTitle": "Confirm Clear", "fileSizeExceeded": "Image {filename} size is {size}MB, exceeding 1MB limit!\nRecommend using image editing software to compress before uploading.", - "dataSizeExceeded": "Image {filename} data size exceeds 1MB limit!" + "dataSizeExceeded": "Image {filename} data size exceeds 1MB limit!", + "types": { + "network": "Network connection issue", + "file_io": "File read/write issue", + "process": "Process execution issue", + "timeout": "Operation timeout", + "user_cancel": "User cancelled the operation", + "system": "System issue", + "permission": "Insufficient permissions", + "validation": "Data validation failed", + "dependency": "Dependency issue", + "config": "Configuration issue" + }, + "solutions": { + "network": [ + "Check network connection", + "Verify firewall settings", + "Try restarting the application" + ], + "file_io": [ + "Check if file exists", + "Verify file permissions", + "Check available disk space" + ], + "process": [ + "Check if process is running", + "Verify system resources", + "Try restarting related services" + ], + "timeout": [ + "Increase timeout settings", + "Check network latency", + "Retry the operation later" + ], + "permission": [ + "Run as administrator", + "Check file/directory permissions", + "Contact system administrator" + ] + } }, "languageSelector": "🌐 Language", "languageNames": { diff --git a/src/mcp_feedback_enhanced/gui/locales/zh-CN/translations.json b/src/mcp_feedback_enhanced/gui/locales/zh-CN/translations.json index 2f30086..f415dd9 100644 --- a/src/mcp_feedback_enhanced/gui/locales/zh-CN/translations.json +++ b/src/mcp_feedback_enhanced/gui/locales/zh-CN/translations.json @@ -172,7 +172,46 @@ "confirmClearAll": "确定要清除所有 {count} 张图片吗?", "confirmClearTitle": "确认清除", "fileSizeExceeded": "图片 {filename} 大小为 {size}MB,超过 1MB 限制!\n建议使用图片编辑软件压缩后再上传。", - "dataSizeExceeded": "图片 {filename} 数据大小超过 1MB 限制!" + "dataSizeExceeded": "图片 {filename} 数据大小超过 1MB 限制!", + "types": { + "network": "网络连接出现问题", + "file_io": "文件读写出现问题", + "process": "进程执行出现问题", + "timeout": "操作超时", + "user_cancel": "用户取消了操作", + "system": "系统出现问题", + "permission": "权限不足", + "validation": "数据验证失败", + "dependency": "依赖组件出现问题", + "config": "配置出现问题" + }, + "solutions": { + "network": [ + "检查网络连接是否正常", + "确认防火墙设置", + "尝试重新启动应用程序" + ], + "file_io": [ + "检查文件是否存在", + "确认文件权限", + "检查磁盘空间是否足够" + ], + "process": [ + "检查进程是否正在运行", + "确认系统资源是否足够", + "尝试重新启动相关服务" + ], + "timeout": [ + "增加超时时间设置", + "检查网络延迟", + "稍后重试操作" + ], + "permission": [ + "以管理员身份运行", + "检查文件/目录权限", + "联系系统管理员" + ] + } }, "aiSummary": "AI 工作摘要", "languageSelector": "🌐 语言选择", diff --git a/src/mcp_feedback_enhanced/gui/locales/zh-TW/translations.json b/src/mcp_feedback_enhanced/gui/locales/zh-TW/translations.json index d134d75..80dc117 100644 --- a/src/mcp_feedback_enhanced/gui/locales/zh-TW/translations.json +++ b/src/mcp_feedback_enhanced/gui/locales/zh-TW/translations.json @@ -188,7 +188,46 @@ "confirmClearAll": "確定要清除所有 {count} 張圖片嗎?", "confirmClearTitle": "確認清除", "fileSizeExceeded": "圖片 {filename} 大小為 {size}MB,超過 1MB 限制!\n建議使用圖片編輯軟體壓縮後再上傳。", - "dataSizeExceeded": "圖片 {filename} 數據大小超過 1MB 限制!" + "dataSizeExceeded": "圖片 {filename} 數據大小超過 1MB 限制!", + "types": { + "network": "網絡連接出現問題", + "file_io": "文件讀寫出現問題", + "process": "進程執行出現問題", + "timeout": "操作超時", + "user_cancel": "用戶取消了操作", + "system": "系統出現問題", + "permission": "權限不足", + "validation": "數據驗證失敗", + "dependency": "依賴組件出現問題", + "config": "配置出現問題" + }, + "solutions": { + "network": [ + "檢查網絡連接是否正常", + "確認防火牆設置", + "嘗試重新啟動應用程序" + ], + "file_io": [ + "檢查文件是否存在", + "確認文件權限", + "檢查磁盤空間是否足夠" + ], + "process": [ + "檢查進程是否正在運行", + "確認系統資源是否足夠", + "嘗試重新啟動相關服務" + ], + "timeout": [ + "增加超時時間設置", + "檢查網絡延遲", + "稍後重試操作" + ], + "permission": [ + "以管理員身份運行", + "檢查文件/目錄權限", + "聯繫系統管理員" + ] + } }, "languageNames": { "zhTw": "繁體中文", diff --git a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py index f2e79bf..88332a1 100644 --- a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py +++ b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py @@ -25,6 +25,7 @@ from PySide6.QtWidgets import QSizePolicy # 導入多語系支援 from ...i18n import t from ...debug import gui_debug_log as debug_log +from ...utils.resource_manager import get_resource_manager, create_temp_file from .image_preview import ImagePreviewWidget @@ -37,6 +38,7 @@ class ImageUploadWidget(QWidget): self.images: Dict[str, Dict[str, str]] = {} self.config_manager = config_manager self._last_paste_time = 0 # 添加最後貼上時間記錄 + self.resource_manager = get_resource_manager() # 獲取資源管理器 self._setup_ui() self.setAcceptDrops(True) # 啟動時清理舊的臨時文件 @@ -350,20 +352,19 @@ class ImageUploadWidget(QWidget): if mimeData.hasImage(): image = clipboard.image() if not image.isNull(): - # 創建一個唯一的臨時文件名 - temp_dir = Path.home() / ".cache" / "mcp-feedback-enhanced" - temp_dir.mkdir(parents=True, exist_ok=True) - - timestamp = int(time.time() * 1000) - temp_file = temp_dir / f"clipboard_{timestamp}_{uuid.uuid4().hex[:8]}.png" + # 使用資源管理器創建臨時文件 + temp_file = create_temp_file( + suffix=".png", + prefix=f"clipboard_{int(time.time() * 1000)}_" + ) # 保存剪貼板圖片 - if image.save(str(temp_file), "PNG"): + if image.save(temp_file, "PNG"): if os.path.getsize(temp_file) > 0: - self._add_images([str(temp_file)]) + self._add_images([temp_file]) debug_log(f"從剪貼板成功粘貼圖片: {temp_file}") else: - QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveEmpty', path=str(temp_file))) + QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveEmpty', path=temp_file)) else: QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveFailed')) else: diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index c0002e0..9f75b67 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -38,6 +38,12 @@ from .i18n import get_i18n_manager # 導入統一的調試功能 from .debug import server_debug_log as debug_log +# 導入錯誤處理框架 +from .utils.error_handler import ErrorHandler, ErrorType + +# 導入資源管理器 +from .utils.resource_manager import get_resource_manager, create_temp_file + # ===== 編碼初始化 ===== def init_encoding(): """初始化編碼設置,確保正確處理中文字符""" @@ -197,13 +203,21 @@ def is_remote_environment() -> bool: def can_use_gui() -> bool: """ 檢測是否可以使用圖形介面 - + + WSL 環境預設使用 Web UI,因為大多數 WSL 安裝都是 Linux 環境, + 沒有桌面應用支援,即使 PySide6 可以載入也應該使用 Web 介面。 + Returns: bool: True 表示可以使用 GUI,False 表示只能使用 Web UI """ if is_remote_environment(): return False - + + # WSL 環境預設使用 Web UI + if is_wsl_environment(): + debug_log("WSL 環境偵測到,預設使用 Web UI") + return False + try: from PySide6.QtWidgets import QApplication debug_log("成功載入 PySide6,可使用 GUI") @@ -228,8 +242,8 @@ def save_feedback_to_file(feedback_data: dict, file_path: str = None) -> str: str: 儲存的文件路徑 """ if file_path is None: - temp_fd, file_path = tempfile.mkstemp(suffix='.json', prefix='feedback_') - os.close(temp_fd) + # 使用資源管理器創建臨時文件 + file_path = create_temp_file(suffix='.json', prefix='feedback_') # 確保目錄存在 directory = os.path.dirname(file_path) @@ -401,9 +415,13 @@ def process_images(images_data: List[dict]) -> List[MCPImage]: debug_log(f"圖片 {i} ({file_name}) 處理成功,格式: {image_format}") except Exception as e: - debug_log(f"圖片 {i} 處理失敗: {e}") - import traceback - debug_log(f"詳細錯誤: {traceback.format_exc()}") + # 使用統一錯誤處理(不影響 JSON RPC) + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "圖片處理", "image_index": i}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"圖片 {i} 處理失敗 [錯誤ID: {error_id}]: {e}") debug_log(f"共處理 {len(mcp_images)} 張圖片") return mcp_images @@ -539,9 +557,18 @@ async def interactive_feedback( return feedback_items except Exception as e: - error_msg = f"回饋收集錯誤: {str(e)}" - debug_log(f"錯誤: {error_msg}") - return [TextContent(type="text", text=error_msg)] + # 使用統一錯誤處理,但不影響 JSON RPC 響應 + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "回饋收集", "project_dir": project_directory}, + error_type=ErrorType.SYSTEM + ) + + # 生成用戶友好的錯誤信息 + user_error_msg = ErrorHandler.format_user_error(e, include_technical=False) + debug_log(f"回饋收集錯誤 [錯誤ID: {error_id}]: {str(e)}") + + return [TextContent(type="text", text=user_error_msg)] async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict: @@ -565,10 +592,18 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in # 傳遞 timeout 參數給 Web UI return await launch_web_feedback_ui(project_dir, summary, timeout) except ImportError as e: - debug_log(f"無法導入 Web UI 模組: {e}") + # 使用統一錯誤處理 + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "Web UI 模組導入", "module": "web"}, + error_type=ErrorType.DEPENDENCY + ) + user_error_msg = ErrorHandler.format_user_error(e, ErrorType.DEPENDENCY, include_technical=False) + debug_log(f"Web UI 模組導入失敗 [錯誤ID: {error_id}]: {e}") + return { "command_logs": "", - "interactive_feedback": f"Web UI 模組導入失敗: {str(e)}", + "interactive_feedback": user_error_msg, "images": [] } except TimeoutError as e: @@ -587,19 +622,31 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in "images": [] } except Exception as e: - error_msg = f"Web UI 錯誤: {e}" - debug_log(f"❌ {error_msg}") + # 使用統一錯誤處理 + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "Web UI 啟動", "timeout": timeout}, + error_type=ErrorType.SYSTEM + ) + user_error_msg = ErrorHandler.format_user_error(e, include_technical=False) + debug_log(f"❌ Web UI 錯誤 [錯誤ID: {error_id}]: {e}") + # 發生錯誤時也要停止 Web 服務器 try: from .web import stop_web_ui stop_web_ui() debug_log("Web UI 服務器已因錯誤而停止") except Exception as stop_error: + ErrorHandler.log_error_with_context( + stop_error, + context={"operation": "Web UI 服務器停止"}, + error_type=ErrorType.SYSTEM + ) debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}") return { "command_logs": "", - "interactive_feedback": f"錯誤: {str(e)}", + "interactive_feedback": user_error_msg, "images": [] } diff --git a/src/mcp_feedback_enhanced/test_web_ui.py b/src/mcp_feedback_enhanced/test_web_ui.py index 863df70..3b8740f 100644 --- a/src/mcp_feedback_enhanced/test_web_ui.py +++ b/src/mcp_feedback_enhanced/test_web_ui.py @@ -95,11 +95,17 @@ def get_test_summary(): def find_free_port(): """Find a free port to use for testing""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('', 0)) - s.listen(1) - port = s.getsockname()[1] - return port + try: + # 嘗試使用增強的端口管理 + from .web.utils.port_manager import PortManager + return PortManager.find_free_port_enhanced(preferred_port=8765, auto_cleanup=False) + except ImportError: + # 回退到原始方法 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', 0)) + s.listen(1) + port = s.getsockname()[1] + return port def test_web_ui(keep_running=False): """Test the Web UI functionality""" diff --git a/src/mcp_feedback_enhanced/testing/utils.py b/src/mcp_feedback_enhanced/testing/utils.py index 114bf50..7c62ed1 100644 --- a/src/mcp_feedback_enhanced/testing/utils.py +++ b/src/mcp_feedback_enhanced/testing/utils.py @@ -54,15 +54,26 @@ class TestUtils: @staticmethod def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int: - """尋找可用端口""" - for port in range(start_port, start_port + max_attempts): - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('127.0.0.1', port)) - return port - except OSError: - continue - raise RuntimeError(f"無法找到可用端口 (嘗試範圍: {start_port}-{start_port + max_attempts})") + """尋找可用端口 - 使用增強的端口管理""" + try: + # 嘗試使用增強的端口管理 + from ..web.utils.port_manager import PortManager + return PortManager.find_free_port_enhanced( + preferred_port=start_port, + auto_cleanup=False, # 測試時不自動清理 + host='127.0.0.1', + max_attempts=max_attempts + ) + except ImportError: + # 回退到原始方法 + for port in range(start_port, start_port + max_attempts): + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', port)) + return port + except OSError: + continue + raise RuntimeError(f"無法找到可用端口 (嘗試範圍: {start_port}-{start_port + max_attempts})") @staticmethod def is_port_open(host: str, port: int, timeout: float = 1.0) -> bool: diff --git a/src/mcp_feedback_enhanced/utils/__init__.py b/src/mcp_feedback_enhanced/utils/__init__.py new file mode 100644 index 0000000..2e2001e --- /dev/null +++ b/src/mcp_feedback_enhanced/utils/__init__.py @@ -0,0 +1,27 @@ +""" +MCP Feedback Enhanced 工具模組 +============================ + +提供各種工具類和函數,包括錯誤處理、資源管理等。 +""" + +from .error_handler import ErrorHandler, ErrorType +from .resource_manager import ( + ResourceManager, + get_resource_manager, + create_temp_file, + create_temp_dir, + register_process, + cleanup_all_resources +) + +__all__ = [ + 'ErrorHandler', + 'ErrorType', + 'ResourceManager', + 'get_resource_manager', + 'create_temp_file', + 'create_temp_dir', + 'register_process', + 'cleanup_all_resources' +] diff --git a/src/mcp_feedback_enhanced/utils/error_handler.py b/src/mcp_feedback_enhanced/utils/error_handler.py new file mode 100644 index 0000000..dd6a867 --- /dev/null +++ b/src/mcp_feedback_enhanced/utils/error_handler.py @@ -0,0 +1,455 @@ +""" +統一錯誤處理框架 +================ + +提供統一的錯誤處理機制,包括: +- 錯誤類型分類 +- 用戶友好錯誤信息 +- 錯誤上下文記錄 +- 解決方案建議 +- 國際化支持 + +注意:此模組不會影響 JSON RPC 通信,所有錯誤處理都在應用層進行。 +""" + +import os +import sys +import traceback +import time +from enum import Enum +from typing import Dict, Any, Optional, List, Tuple +from ..debug import debug_log + + +class ErrorType(Enum): + """錯誤類型枚舉""" + NETWORK = "network" # 網絡相關錯誤 + FILE_IO = "file_io" # 文件 I/O 錯誤 + PROCESS = "process" # 進程相關錯誤 + TIMEOUT = "timeout" # 超時錯誤 + USER_CANCEL = "user_cancel" # 用戶取消操作 + SYSTEM = "system" # 系統錯誤 + PERMISSION = "permission" # 權限錯誤 + VALIDATION = "validation" # 數據驗證錯誤 + DEPENDENCY = "dependency" # 依賴錯誤 + CONFIGURATION = "config" # 配置錯誤 + + +class ErrorSeverity(Enum): + """錯誤嚴重程度""" + LOW = "low" # 低:不影響核心功能 + MEDIUM = "medium" # 中:影響部分功能 + HIGH = "high" # 高:影響核心功能 + CRITICAL = "critical" # 嚴重:系統無法正常運行 + + +class ErrorHandler: + """統一錯誤處理器""" + + # 錯誤類型到用戶友好信息的映射 + _ERROR_MESSAGES = { + ErrorType.NETWORK: { + "zh-TW": "網絡連接出現問題", + "zh-CN": "网络连接出现问题", + "en": "Network connection issue" + }, + ErrorType.FILE_IO: { + "zh-TW": "文件讀寫出現問題", + "zh-CN": "文件读写出现问题", + "en": "File read/write issue" + }, + ErrorType.PROCESS: { + "zh-TW": "進程執行出現問題", + "zh-CN": "进程执行出现问题", + "en": "Process execution issue" + }, + ErrorType.TIMEOUT: { + "zh-TW": "操作超時", + "zh-CN": "操作超时", + "en": "Operation timeout" + }, + ErrorType.USER_CANCEL: { + "zh-TW": "用戶取消了操作", + "zh-CN": "用户取消了操作", + "en": "User cancelled the operation" + }, + ErrorType.SYSTEM: { + "zh-TW": "系統出現問題", + "zh-CN": "系统出现问题", + "en": "System issue" + }, + ErrorType.PERMISSION: { + "zh-TW": "權限不足", + "zh-CN": "权限不足", + "en": "Insufficient permissions" + }, + ErrorType.VALIDATION: { + "zh-TW": "數據驗證失敗", + "zh-CN": "数据验证失败", + "en": "Data validation failed" + }, + ErrorType.DEPENDENCY: { + "zh-TW": "依賴組件出現問題", + "zh-CN": "依赖组件出现问题", + "en": "Dependency issue" + }, + ErrorType.CONFIGURATION: { + "zh-TW": "配置出現問題", + "zh-CN": "配置出现问题", + "en": "Configuration issue" + } + } + + # 錯誤解決建議 + _ERROR_SOLUTIONS = { + ErrorType.NETWORK: { + "zh-TW": [ + "檢查網絡連接是否正常", + "確認防火牆設置", + "嘗試重新啟動應用程序" + ], + "zh-CN": [ + "检查网络连接是否正常", + "确认防火墙设置", + "尝试重新启动应用程序" + ], + "en": [ + "Check network connection", + "Verify firewall settings", + "Try restarting the application" + ] + }, + ErrorType.FILE_IO: { + "zh-TW": [ + "檢查文件是否存在", + "確認文件權限", + "檢查磁盤空間是否足夠" + ], + "zh-CN": [ + "检查文件是否存在", + "确认文件权限", + "检查磁盘空间是否足够" + ], + "en": [ + "Check if file exists", + "Verify file permissions", + "Check available disk space" + ] + }, + ErrorType.PROCESS: { + "zh-TW": [ + "檢查進程是否正在運行", + "確認系統資源是否足夠", + "嘗試重新啟動相關服務" + ], + "zh-CN": [ + "检查进程是否正在运行", + "确认系统资源是否足够", + "尝试重新启动相关服务" + ], + "en": [ + "Check if process is running", + "Verify system resources", + "Try restarting related services" + ] + }, + ErrorType.TIMEOUT: { + "zh-TW": [ + "增加超時時間設置", + "檢查網絡延遲", + "稍後重試操作" + ], + "zh-CN": [ + "增加超时时间设置", + "检查网络延迟", + "稍后重试操作" + ], + "en": [ + "Increase timeout settings", + "Check network latency", + "Retry the operation later" + ] + }, + ErrorType.PERMISSION: { + "zh-TW": [ + "以管理員身份運行", + "檢查文件/目錄權限", + "聯繫系統管理員" + ], + "zh-CN": [ + "以管理员身份运行", + "检查文件/目录权限", + "联系系统管理员" + ], + "en": [ + "Run as administrator", + "Check file/directory permissions", + "Contact system administrator" + ] + } + } + + @staticmethod + def get_current_language() -> str: + """獲取當前語言設置""" + try: + # 嘗試從 i18n 模組獲取當前語言 + from ..i18n import get_i18n_manager + return get_i18n_manager().get_current_language() + except Exception: + # 回退到環境變數或默認語言 + return os.getenv("MCP_LANGUAGE", "zh-TW") + + @staticmethod + def get_i18n_error_message(error_type: ErrorType) -> str: + """從國際化系統獲取錯誤信息""" + try: + from ..i18n import get_i18n_manager + i18n = get_i18n_manager() + key = f"errors.types.{error_type.value}" + message = i18n.t(key) + # 如果返回的是鍵本身,說明沒有找到翻譯,使用回退 + if message == key: + raise Exception("Translation not found") + return message + except Exception: + # 回退到內建映射 + language = ErrorHandler.get_current_language() + error_messages = ErrorHandler._ERROR_MESSAGES.get(error_type, {}) + return error_messages.get(language, error_messages.get("zh-TW", "發生未知錯誤")) + + @staticmethod + def get_i18n_error_solutions(error_type: ErrorType) -> List[str]: + """從國際化系統獲取錯誤解決方案""" + try: + from ..i18n import get_i18n_manager + i18n = get_i18n_manager() + key = f"errors.solutions.{error_type.value}" + solutions = i18n.t(key) + if isinstance(solutions, list) and len(solutions) > 0: + return solutions + # 如果沒有找到或為空,使用回退 + raise Exception("Solutions not found") + except Exception: + # 回退到內建映射 + language = ErrorHandler.get_current_language() + solutions = ErrorHandler._ERROR_SOLUTIONS.get(error_type, {}) + return solutions.get(language, solutions.get("zh-TW", [])) + + @staticmethod + def classify_error(error: Exception) -> ErrorType: + """ + 根據異常類型自動分類錯誤 + + Args: + error: Python 異常對象 + + Returns: + ErrorType: 錯誤類型 + """ + error_name = type(error).__name__ + error_message = str(error).lower() + + # 超時錯誤(優先檢查,避免被網絡錯誤覆蓋) + if 'timeout' in error_name.lower() or 'timeout' in error_message: + return ErrorType.TIMEOUT + + # 權限錯誤(優先檢查,避免被文件錯誤覆蓋) + if 'permission' in error_name.lower(): + return ErrorType.PERMISSION + if any(keyword in error_message for keyword in ['permission denied', 'access denied', 'forbidden']): + return ErrorType.PERMISSION + + # 網絡相關錯誤 + if any(keyword in error_name.lower() for keyword in ['connection', 'network', 'socket']): + return ErrorType.NETWORK + if any(keyword in error_message for keyword in ['connection', 'network', 'socket']): + return ErrorType.NETWORK + + # 文件 I/O 錯誤 + if any(keyword in error_name.lower() for keyword in ['file', 'ioerror']): # 使用更精確的匹配 + return ErrorType.FILE_IO + if any(keyword in error_message for keyword in ['file', 'directory', 'no such file']): + return ErrorType.FILE_IO + + # 進程相關錯誤 + if any(keyword in error_name.lower() for keyword in ['process', 'subprocess']): + return ErrorType.PROCESS + if any(keyword in error_message for keyword in ['process', 'command', 'executable']): + return ErrorType.PROCESS + + # 驗證錯誤 + if any(keyword in error_name.lower() for keyword in ['validation', 'value', 'type']): + return ErrorType.VALIDATION + + # 配置錯誤 + if any(keyword in error_message for keyword in ['config', 'setting', 'environment']): + return ErrorType.CONFIGURATION + + # 默認為系統錯誤 + return ErrorType.SYSTEM + + @staticmethod + def format_user_error( + error: Exception, + error_type: Optional[ErrorType] = None, + context: Optional[Dict[str, Any]] = None, + include_technical: bool = False + ) -> str: + """ + 將技術錯誤轉換為用戶友好的錯誤信息 + + Args: + error: Python 異常對象 + error_type: 錯誤類型(可選,會自動分類) + context: 錯誤上下文信息 + include_technical: 是否包含技術細節 + + Returns: + str: 用戶友好的錯誤信息 + """ + # 自動分類錯誤類型 + if error_type is None: + error_type = ErrorHandler.classify_error(error) + + # 獲取當前語言 + language = ErrorHandler.get_current_language() + + # 獲取用戶友好的錯誤信息(優先使用國際化系統) + user_message = ErrorHandler.get_i18n_error_message(error_type) + + # 構建完整的錯誤信息 + parts = [f"❌ {user_message}"] + + # 添加上下文信息 + if context: + if context.get("operation"): + if language == "en": + parts.append(f"Operation: {context['operation']}") + else: + parts.append(f"操作:{context['operation']}") + + if context.get("file_path"): + if language == "en": + parts.append(f"File: {context['file_path']}") + else: + parts.append(f"文件:{context['file_path']}") + + # 添加技術細節(如果需要) + if include_technical: + if language == "en": + parts.append(f"Technical details: {type(error).__name__}: {str(error)}") + else: + parts.append(f"技術細節:{type(error).__name__}: {str(error)}") + + return "\n".join(parts) + + @staticmethod + def get_error_solutions(error_type: ErrorType) -> List[str]: + """ + 獲取錯誤解決建議 + + Args: + error_type: 錯誤類型 + + Returns: + List[str]: 解決建議列表 + """ + return ErrorHandler.get_i18n_error_solutions(error_type) + + @staticmethod + def log_error_with_context( + error: Exception, + context: Optional[Dict[str, Any]] = None, + error_type: Optional[ErrorType] = None, + severity: ErrorSeverity = ErrorSeverity.MEDIUM + ) -> str: + """ + 記錄帶上下文的錯誤信息(不影響 JSON RPC) + + Args: + error: Python 異常對象 + context: 錯誤上下文信息 + error_type: 錯誤類型 + severity: 錯誤嚴重程度 + + Returns: + str: 錯誤 ID,用於追蹤 + """ + # 生成錯誤 ID + error_id = f"ERR_{int(time.time())}_{id(error) % 10000}" + + # 自動分類錯誤 + if error_type is None: + error_type = ErrorHandler.classify_error(error) + + # 構建錯誤記錄 + error_record = { + "error_id": error_id, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "error_type": error_type.value, + "severity": severity.value, + "exception_type": type(error).__name__, + "exception_message": str(error), + "context": context or {}, + "traceback": traceback.format_exc() if severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL] else None + } + + # 記錄到調試日誌(不影響 JSON RPC) + debug_log(f"錯誤記錄 [{error_id}]: {error_type.value} - {str(error)}") + + if context: + debug_log(f"錯誤上下文 [{error_id}]: {context}") + + # 對於嚴重錯誤,記錄完整堆棧跟蹤 + if severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]: + debug_log(f"錯誤堆棧 [{error_id}]:\n{traceback.format_exc()}") + + return error_id + + @staticmethod + def create_error_response( + error: Exception, + context: Optional[Dict[str, Any]] = None, + error_type: Optional[ErrorType] = None, + include_solutions: bool = True, + for_user: bool = True + ) -> Dict[str, Any]: + """ + 創建標準化的錯誤響應 + + Args: + error: Python 異常對象 + context: 錯誤上下文 + error_type: 錯誤類型 + include_solutions: 是否包含解決建議 + for_user: 是否為用戶界面使用 + + Returns: + Dict[str, Any]: 標準化錯誤響應 + """ + # 自動分類錯誤 + if error_type is None: + error_type = ErrorHandler.classify_error(error) + + # 記錄錯誤 + error_id = ErrorHandler.log_error_with_context(error, context, error_type) + + # 構建響應 + response = { + "success": False, + "error_id": error_id, + "error_type": error_type.value, + "message": ErrorHandler.format_user_error(error, error_type, context, include_technical=not for_user) + } + + # 添加解決建議 + if include_solutions: + solutions = ErrorHandler.get_error_solutions(error_type) + response["solutions"] = solutions # 即使為空列表也添加 + + # 添加上下文(僅用於調試) + if context and not for_user: + response["context"] = context + + return response diff --git a/src/mcp_feedback_enhanced/utils/memory_monitor.py b/src/mcp_feedback_enhanced/utils/memory_monitor.py new file mode 100644 index 0000000..644161a --- /dev/null +++ b/src/mcp_feedback_enhanced/utils/memory_monitor.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +集成式內存監控系統 +================== + +提供與資源管理器深度集成的內存監控功能,包括: +- 系統和進程內存使用監控 +- 智能清理觸發機制 +- 內存洩漏檢測和趨勢分析 +- 性能優化建議 +""" + +import os +import gc +import time +import threading +import psutil +from typing import Dict, List, Optional, Callable, Any +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from collections import deque +from ..debug import debug_log +from .error_handler import ErrorHandler, ErrorType + + +@dataclass +class MemorySnapshot: + """內存快照數據類""" + timestamp: datetime + system_total: int # 系統總內存 (bytes) + system_available: int # 系統可用內存 (bytes) + system_used: int # 系統已用內存 (bytes) + system_percent: float # 系統內存使用率 (%) + process_rss: int # 進程常駐內存 (bytes) + process_vms: int # 進程虛擬內存 (bytes) + process_percent: float # 進程內存使用率 (%) + gc_objects: int # Python 垃圾回收對象數量 + + +@dataclass +class MemoryAlert: + """內存警告數據類""" + level: str # warning, critical, emergency + message: str + timestamp: datetime + memory_percent: float + recommended_action: str + + +@dataclass +class MemoryStats: + """內存統計數據類""" + monitoring_duration: float # 監控持續時間 (秒) + snapshots_count: int # 快照數量 + average_system_usage: float # 平均系統內存使用率 + peak_system_usage: float # 峰值系統內存使用率 + average_process_usage: float # 平均進程內存使用率 + peak_process_usage: float # 峰值進程內存使用率 + alerts_count: int # 警告數量 + cleanup_triggers: int # 清理觸發次數 + memory_trend: str # 內存趨勢 (stable, increasing, decreasing) + + +class MemoryMonitor: + """集成式內存監控器""" + + def __init__(self, + warning_threshold: float = 0.8, + critical_threshold: float = 0.9, + emergency_threshold: float = 0.95, + monitoring_interval: int = 30, + max_snapshots: int = 1000): + """ + 初始化內存監控器 + + Args: + warning_threshold: 警告閾值 (0.0-1.0) + critical_threshold: 危險閾值 (0.0-1.0) + emergency_threshold: 緊急閾值 (0.0-1.0) + monitoring_interval: 監控間隔 (秒) + max_snapshots: 最大快照數量 + """ + self.warning_threshold = warning_threshold + self.critical_threshold = critical_threshold + self.emergency_threshold = emergency_threshold + self.monitoring_interval = monitoring_interval + self.max_snapshots = max_snapshots + + # 監控狀態 + self.is_monitoring = False + self.monitor_thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + + # 數據存儲 + self.snapshots: deque = deque(maxlen=max_snapshots) + self.alerts: List[MemoryAlert] = [] + self.max_alerts = 100 + + # 回調函數 + self.cleanup_callbacks: List[Callable] = [] + self.alert_callbacks: List[Callable[[MemoryAlert], None]] = [] + + # 統計數據 + self.start_time: Optional[datetime] = None + self.cleanup_triggers_count = 0 + + # 進程信息 + self.process = psutil.Process() + + debug_log("MemoryMonitor 初始化完成") + + def start_monitoring(self) -> bool: + """ + 開始內存監控 + + Returns: + bool: 是否成功啟動 + """ + if self.is_monitoring: + debug_log("內存監控已在運行") + return True + + try: + self.is_monitoring = True + self.start_time = datetime.now() + self._stop_event.clear() + + self.monitor_thread = threading.Thread( + target=self._monitoring_loop, + name="MemoryMonitor", + daemon=True + ) + self.monitor_thread.start() + + debug_log(f"內存監控已啟動,間隔 {self.monitoring_interval} 秒") + return True + + except Exception as e: + self.is_monitoring = False + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "啟動內存監控"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"啟動內存監控失敗 [錯誤ID: {error_id}]: {e}") + return False + + def stop_monitoring(self) -> bool: + """ + 停止內存監控 + + Returns: + bool: 是否成功停止 + """ + if not self.is_monitoring: + debug_log("內存監控未在運行") + return True + + try: + self.is_monitoring = False + self._stop_event.set() + + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=5) + + debug_log("內存監控已停止") + return True + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "停止內存監控"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"停止內存監控失敗 [錯誤ID: {error_id}]: {e}") + return False + + def _monitoring_loop(self): + """內存監控主循環""" + debug_log("內存監控循環開始") + + while not self._stop_event.is_set(): + try: + # 收集內存快照 + snapshot = self._collect_memory_snapshot() + self.snapshots.append(snapshot) + + # 檢查內存使用情況 + self._check_memory_usage(snapshot) + + # 等待下次監控 + if self._stop_event.wait(self.monitoring_interval): + break + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "內存監控循環"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"內存監控循環錯誤 [錯誤ID: {error_id}]: {e}") + + # 發生錯誤時等待較短時間後重試 + if self._stop_event.wait(5): + break + + debug_log("內存監控循環結束") + + def _collect_memory_snapshot(self) -> MemorySnapshot: + """收集內存快照""" + try: + # 系統內存信息 + system_memory = psutil.virtual_memory() + + # 進程內存信息 + process_memory = self.process.memory_info() + process_percent = self.process.memory_percent() + + # Python 垃圾回收信息 + gc_objects = len(gc.get_objects()) + + return MemorySnapshot( + timestamp=datetime.now(), + system_total=system_memory.total, + system_available=system_memory.available, + system_used=system_memory.used, + system_percent=system_memory.percent, + process_rss=process_memory.rss, + process_vms=process_memory.vms, + process_percent=process_percent, + gc_objects=gc_objects + ) + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "收集內存快照"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"收集內存快照失敗 [錯誤ID: {error_id}]: {e}") + raise + + def _check_memory_usage(self, snapshot: MemorySnapshot): + """檢查內存使用情況並觸發相應動作""" + usage_percent = snapshot.system_percent / 100.0 + + # 檢查緊急閾值 + if usage_percent >= self.emergency_threshold: + alert = MemoryAlert( + level="emergency", + message=f"內存使用率達到緊急水平: {snapshot.system_percent:.1f}%", + timestamp=snapshot.timestamp, + memory_percent=snapshot.system_percent, + recommended_action="立即執行強制清理和垃圾回收" + ) + self._handle_alert(alert) + self._trigger_emergency_cleanup() + + # 檢查危險閾值 + elif usage_percent >= self.critical_threshold: + alert = MemoryAlert( + level="critical", + message=f"內存使用率達到危險水平: {snapshot.system_percent:.1f}%", + timestamp=snapshot.timestamp, + memory_percent=snapshot.system_percent, + recommended_action="執行資源清理和垃圾回收" + ) + self._handle_alert(alert) + self._trigger_cleanup() + + # 檢查警告閾值 + elif usage_percent >= self.warning_threshold: + alert = MemoryAlert( + level="warning", + message=f"內存使用率較高: {snapshot.system_percent:.1f}%", + timestamp=snapshot.timestamp, + memory_percent=snapshot.system_percent, + recommended_action="考慮執行輕量級清理" + ) + self._handle_alert(alert) + + def _handle_alert(self, alert: MemoryAlert): + """處理內存警告""" + # 添加到警告列表 + self.alerts.append(alert) + + # 限制警告數量 + if len(self.alerts) > self.max_alerts: + self.alerts = self.alerts[-self.max_alerts:] + + # 調用警告回調 + for callback in self.alert_callbacks: + try: + callback(alert) + except Exception as e: + debug_log(f"警告回調執行失敗: {e}") + + debug_log(f"內存警告 [{alert.level}]: {alert.message}") + + def _trigger_cleanup(self): + """觸發清理操作""" + self.cleanup_triggers_count += 1 + debug_log("觸發內存清理操作") + + # 執行 Python 垃圾回收 + collected = gc.collect() + debug_log(f"垃圾回收清理了 {collected} 個對象") + + # 調用清理回調 + for callback in self.cleanup_callbacks: + try: + callback() + except Exception as e: + debug_log(f"清理回調執行失敗: {e}") + + def _trigger_emergency_cleanup(self): + """觸發緊急清理操作""" + debug_log("觸發緊急內存清理操作") + + # 執行強制垃圾回收 + for _ in range(3): + collected = gc.collect() + debug_log(f"強制垃圾回收清理了 {collected} 個對象") + + # 調用清理回調(強制模式) + for callback in self.cleanup_callbacks: + try: + if hasattr(callback, '__call__'): + # 嘗試傳遞 force 參數 + import inspect + sig = inspect.signature(callback) + if 'force' in sig.parameters: + callback(force=True) + else: + callback() + else: + callback() + except Exception as e: + debug_log(f"緊急清理回調執行失敗: {e}") + + + def add_cleanup_callback(self, callback: Callable): + """添加清理回調函數""" + if callback not in self.cleanup_callbacks: + self.cleanup_callbacks.append(callback) + debug_log("添加清理回調函數") + + def add_alert_callback(self, callback: Callable[[MemoryAlert], None]): + """添加警告回調函數""" + if callback not in self.alert_callbacks: + self.alert_callbacks.append(callback) + debug_log("添加警告回調函數") + + def remove_cleanup_callback(self, callback: Callable): + """移除清理回調函數""" + if callback in self.cleanup_callbacks: + self.cleanup_callbacks.remove(callback) + debug_log("移除清理回調函數") + + def remove_alert_callback(self, callback: Callable[[MemoryAlert], None]): + """移除警告回調函數""" + if callback in self.alert_callbacks: + self.alert_callbacks.remove(callback) + debug_log("移除警告回調函數") + + def get_current_memory_info(self) -> Dict[str, Any]: + """獲取當前內存信息""" + try: + snapshot = self._collect_memory_snapshot() + return { + "timestamp": snapshot.timestamp.isoformat(), + "system": { + "total_gb": round(snapshot.system_total / (1024**3), 2), + "available_gb": round(snapshot.system_available / (1024**3), 2), + "used_gb": round(snapshot.system_used / (1024**3), 2), + "usage_percent": round(snapshot.system_percent, 1) + }, + "process": { + "rss_mb": round(snapshot.process_rss / (1024**2), 2), + "vms_mb": round(snapshot.process_vms / (1024**2), 2), + "usage_percent": round(snapshot.process_percent, 1) + }, + "gc_objects": snapshot.gc_objects, + "status": self._get_memory_status(snapshot.system_percent / 100.0) + } + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "獲取當前內存信息"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"獲取內存信息失敗 [錯誤ID: {error_id}]: {e}") + return {} + + def get_memory_stats(self) -> MemoryStats: + """獲取內存統計數據""" + if not self.snapshots: + return MemoryStats( + monitoring_duration=0.0, + snapshots_count=0, + average_system_usage=0.0, + peak_system_usage=0.0, + average_process_usage=0.0, + peak_process_usage=0.0, + alerts_count=0, + cleanup_triggers=0, + memory_trend="unknown" + ) + + # 計算統計數據 + system_usages = [s.system_percent for s in self.snapshots] + process_usages = [s.process_percent for s in self.snapshots] + + duration = 0.0 + if self.start_time: + duration = (datetime.now() - self.start_time).total_seconds() + + return MemoryStats( + monitoring_duration=duration, + snapshots_count=len(self.snapshots), + average_system_usage=sum(system_usages) / len(system_usages), + peak_system_usage=max(system_usages), + average_process_usage=sum(process_usages) / len(process_usages), + peak_process_usage=max(process_usages), + alerts_count=len(self.alerts), + cleanup_triggers=self.cleanup_triggers_count, + memory_trend=self._analyze_memory_trend() + ) + + def get_recent_alerts(self, limit: int = 10) -> List[MemoryAlert]: + """獲取最近的警告""" + return self.alerts[-limit:] if self.alerts else [] + + def _get_memory_status(self, usage_percent: float) -> str: + """獲取內存狀態描述""" + if usage_percent >= self.emergency_threshold: + return "emergency" + elif usage_percent >= self.critical_threshold: + return "critical" + elif usage_percent >= self.warning_threshold: + return "warning" + else: + return "normal" + + def _analyze_memory_trend(self) -> str: + """分析內存使用趨勢""" + if len(self.snapshots) < 10: + return "insufficient_data" + + # 取最近的快照進行趨勢分析 + recent_snapshots = list(self.snapshots)[-10:] + usages = [s.system_percent for s in recent_snapshots] + + # 簡單的線性趨勢分析 + first_half = usages[:5] + second_half = usages[5:] + + avg_first = sum(first_half) / len(first_half) + avg_second = sum(second_half) / len(second_half) + + diff = avg_second - avg_first + + if abs(diff) < 2.0: # 變化小於 2% + return "stable" + elif diff > 0: + return "increasing" + else: + return "decreasing" + + def force_cleanup(self): + """手動觸發清理操作""" + debug_log("手動觸發內存清理") + self._trigger_cleanup() + + def force_emergency_cleanup(self): + """手動觸發緊急清理操作""" + debug_log("手動觸發緊急內存清理") + self._trigger_emergency_cleanup() + + def reset_stats(self): + """重置統計數據""" + self.snapshots.clear() + self.alerts.clear() + self.cleanup_triggers_count = 0 + self.start_time = datetime.now() if self.is_monitoring else None + debug_log("內存監控統計數據已重置") + + def export_memory_data(self) -> Dict[str, Any]: + """導出內存數據""" + return { + "config": { + "warning_threshold": self.warning_threshold, + "critical_threshold": self.critical_threshold, + "emergency_threshold": self.emergency_threshold, + "monitoring_interval": self.monitoring_interval + }, + "current_info": self.get_current_memory_info(), + "stats": self.get_memory_stats().__dict__, + "recent_alerts": [ + { + "level": alert.level, + "message": alert.message, + "timestamp": alert.timestamp.isoformat(), + "memory_percent": alert.memory_percent, + "recommended_action": alert.recommended_action + } + for alert in self.get_recent_alerts() + ], + "is_monitoring": self.is_monitoring + } + + +# 全域內存監控器實例 +_memory_monitor: Optional[MemoryMonitor] = None +_monitor_lock = threading.Lock() + + +def get_memory_monitor() -> MemoryMonitor: + """獲取全域內存監控器實例""" + global _memory_monitor + if _memory_monitor is None: + with _monitor_lock: + if _memory_monitor is None: + _memory_monitor = MemoryMonitor() + return _memory_monitor diff --git a/src/mcp_feedback_enhanced/utils/resource_manager.py b/src/mcp_feedback_enhanced/utils/resource_manager.py new file mode 100644 index 0000000..8bd200c --- /dev/null +++ b/src/mcp_feedback_enhanced/utils/resource_manager.py @@ -0,0 +1,797 @@ +""" +統一資源管理器 +============== + +提供統一的資源管理功能,包括: +- 臨時文件和目錄管理 +- 進程生命週期追蹤 +- 自動資源清理 +- 資源使用監控 +""" + +import os +import sys +import time +import atexit +import shutil +import tempfile +import threading +import subprocess +import weakref +from pathlib import Path +from typing import Set, Dict, Any, Optional, List, Union +from ..debug import debug_log +from .error_handler import ErrorHandler, ErrorType + + +class ResourceType: + """資源類型常量""" + TEMP_FILE = "temp_file" + TEMP_DIR = "temp_dir" + PROCESS = "process" + FILE_HANDLE = "file_handle" + + +class ResourceManager: + """統一資源管理器 - 提供完整的資源生命週期管理""" + + _instance = None + _lock = threading.Lock() + + def __new__(cls): + """單例模式實現""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """初始化資源管理器""" + if hasattr(self, '_initialized'): + return + + self._initialized = True + + # 資源追蹤集合 + self.temp_files: Set[str] = set() + self.temp_dirs: Set[str] = set() + self.processes: Dict[int, Dict[str, Any]] = {} + self.file_handles: Set[Any] = set() + + # 資源統計 + self.stats = { + "temp_files_created": 0, + "temp_dirs_created": 0, + "processes_registered": 0, + "cleanup_runs": 0, + "last_cleanup": None + } + + # 配置 + self.auto_cleanup_enabled = True + self.cleanup_interval = 300 # 5分鐘 + self.temp_file_max_age = 3600 # 1小時 + + # 清理線程 + self._cleanup_thread: Optional[threading.Thread] = None + self._stop_cleanup = threading.Event() + + # 註冊退出清理 + atexit.register(self.cleanup_all) + + # 啟動自動清理 + self._start_auto_cleanup() + + # 集成內存監控 + self._setup_memory_monitoring() + + debug_log("ResourceManager 初始化完成") + + def _setup_memory_monitoring(self): + """設置內存監控集成""" + try: + # 延遲導入避免循環依賴 + from .memory_monitor import get_memory_monitor + + self.memory_monitor = get_memory_monitor() + + # 註冊清理回調 + self.memory_monitor.add_cleanup_callback(self._memory_triggered_cleanup) + + # 啟動內存監控 + if self.memory_monitor.start_monitoring(): + debug_log("內存監控已集成到資源管理器") + else: + debug_log("內存監控啟動失敗") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "設置內存監控"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"設置內存監控失敗 [錯誤ID: {error_id}]: {e}") + + def _memory_triggered_cleanup(self, force: bool = False): + """內存監控觸發的清理操作""" + debug_log(f"內存監控觸發清理操作 (force={force})") + + try: + # 清理臨時文件 + cleaned_files = self.cleanup_temp_files() + + # 清理臨時目錄 + cleaned_dirs = self.cleanup_temp_dirs() + + # 清理文件句柄 + cleaned_handles = self.cleanup_file_handles() + + # 如果是強制清理,也清理進程 + cleaned_processes = 0 + if force: + cleaned_processes = self.cleanup_processes(force=True) + + debug_log(f"內存觸發清理完成: 文件={cleaned_files}, 目錄={cleaned_dirs}, " + f"句柄={cleaned_handles}, 進程={cleaned_processes}") + + # 更新統計 + self.stats["cleanup_runs"] += 1 + self.stats["last_cleanup"] = time.time() + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "內存觸發清理", "force": force}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"內存觸發清理失敗 [錯誤ID: {error_id}]: {e}") + + def create_temp_file( + self, + suffix: str = "", + prefix: str = "mcp_", + dir: Optional[str] = None, + text: bool = True + ) -> str: + """ + 創建臨時文件並追蹤 + + Args: + suffix: 文件後綴 + prefix: 文件前綴 + dir: 臨時目錄,None 使用系統默認 + text: 是否為文本模式 + + Returns: + str: 臨時文件路徑 + """ + try: + # 創建臨時文件 + fd, temp_path = tempfile.mkstemp( + suffix=suffix, + prefix=prefix, + dir=dir, + text=text + ) + os.close(fd) # 關閉文件描述符 + + # 追蹤文件 + self.temp_files.add(temp_path) + self.stats["temp_files_created"] += 1 + + debug_log(f"創建臨時文件: {temp_path}") + return temp_path + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "創建臨時文件", "suffix": suffix, "prefix": prefix}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"創建臨時文件失敗 [錯誤ID: {error_id}]: {e}") + raise + + def create_temp_dir( + self, + suffix: str = "", + prefix: str = "mcp_", + dir: Optional[str] = None + ) -> str: + """ + 創建臨時目錄並追蹤 + + Args: + suffix: 目錄後綴 + prefix: 目錄前綴 + dir: 父目錄,None 使用系統默認 + + Returns: + str: 臨時目錄路徑 + """ + try: + # 創建臨時目錄 + temp_dir = tempfile.mkdtemp( + suffix=suffix, + prefix=prefix, + dir=dir + ) + + # 追蹤目錄 + self.temp_dirs.add(temp_dir) + self.stats["temp_dirs_created"] += 1 + + debug_log(f"創建臨時目錄: {temp_dir}") + return temp_dir + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "創建臨時目錄", "suffix": suffix, "prefix": prefix}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"創建臨時目錄失敗 [錯誤ID: {error_id}]: {e}") + raise + + def register_process( + self, + process: Union[subprocess.Popen, int], + description: str = "", + auto_cleanup: bool = True + ) -> int: + """ + 註冊進程追蹤 + + Args: + process: 進程對象或 PID + description: 進程描述 + auto_cleanup: 是否自動清理 + + Returns: + int: 進程 PID + """ + try: + if isinstance(process, subprocess.Popen): + pid = process.pid + process_obj = process + else: + pid = process + process_obj = None + + # 註冊進程 + self.processes[pid] = { + "process": process_obj, + "description": description, + "auto_cleanup": auto_cleanup, + "registered_at": time.time(), + "last_check": time.time() + } + + self.stats["processes_registered"] += 1 + + debug_log(f"註冊進程追蹤: PID {pid} - {description}") + return pid + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "註冊進程", "description": description}, + error_type=ErrorType.PROCESS + ) + debug_log(f"註冊進程失敗 [錯誤ID: {error_id}]: {e}") + raise + + def register_file_handle(self, file_handle: Any) -> None: + """ + 註冊文件句柄追蹤 + + Args: + file_handle: 文件句柄對象 + """ + try: + # 使用弱引用避免循環引用 + self.file_handles.add(weakref.ref(file_handle)) + debug_log(f"註冊文件句柄: {type(file_handle).__name__}") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "註冊文件句柄"}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"註冊文件句柄失敗 [錯誤ID: {error_id}]: {e}") + + def unregister_temp_file(self, file_path: str) -> bool: + """ + 取消臨時文件追蹤 + + Args: + file_path: 文件路徑 + + Returns: + bool: 是否成功取消追蹤 + """ + try: + if file_path in self.temp_files: + self.temp_files.remove(file_path) + debug_log(f"取消臨時文件追蹤: {file_path}") + return True + return False + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "取消文件追蹤", "file_path": file_path}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"取消文件追蹤失敗 [錯誤ID: {error_id}]: {e}") + return False + + def unregister_process(self, pid: int) -> bool: + """ + 取消進程追蹤 + + Args: + pid: 進程 PID + + Returns: + bool: 是否成功取消追蹤 + """ + try: + if pid in self.processes: + del self.processes[pid] + debug_log(f"取消進程追蹤: PID {pid}") + return True + return False + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "取消進程追蹤", "pid": pid}, + error_type=ErrorType.PROCESS + ) + debug_log(f"取消進程追蹤失敗 [錯誤ID: {error_id}]: {e}") + return False + + def cleanup_temp_files(self, max_age: Optional[int] = None) -> int: + """ + 清理臨時文件 + + Args: + max_age: 最大文件年齡(秒),None 使用默認值 + + Returns: + int: 清理的文件數量 + """ + if max_age is None: + max_age = self.temp_file_max_age + + cleaned_count = 0 + current_time = time.time() + files_to_remove = set() + + for file_path in self.temp_files.copy(): + try: + if not os.path.exists(file_path): + files_to_remove.add(file_path) + continue + + # 檢查文件年齡 + file_age = current_time - os.path.getmtime(file_path) + if file_age > max_age: + os.remove(file_path) + files_to_remove.add(file_path) + cleaned_count += 1 + debug_log(f"清理過期臨時文件: {file_path}") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "清理臨時文件", "file_path": file_path}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"清理臨時文件失敗 [錯誤ID: {error_id}]: {e}") + files_to_remove.add(file_path) # 移除無效追蹤 + + # 移除已清理的文件追蹤 + self.temp_files -= files_to_remove + + return cleaned_count + + def cleanup_temp_dirs(self) -> int: + """ + 清理臨時目錄 + + Returns: + int: 清理的目錄數量 + """ + cleaned_count = 0 + dirs_to_remove = set() + + for dir_path in self.temp_dirs.copy(): + try: + if not os.path.exists(dir_path): + dirs_to_remove.add(dir_path) + continue + + # 嘗試刪除目錄 + shutil.rmtree(dir_path) + dirs_to_remove.add(dir_path) + cleaned_count += 1 + debug_log(f"清理臨時目錄: {dir_path}") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "清理臨時目錄", "dir_path": dir_path}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"清理臨時目錄失敗 [錯誤ID: {error_id}]: {e}") + dirs_to_remove.add(dir_path) # 移除無效追蹤 + + # 移除已清理的目錄追蹤 + self.temp_dirs -= dirs_to_remove + + return cleaned_count + + def cleanup_processes(self, force: bool = False) -> int: + """ + 清理進程 + + Args: + force: 是否強制終止進程 + + Returns: + int: 清理的進程數量 + """ + cleaned_count = 0 + processes_to_remove = [] + + for pid, process_info in self.processes.copy().items(): + try: + process_obj = process_info.get("process") + auto_cleanup = process_info.get("auto_cleanup", True) + + if not auto_cleanup: + continue + + # 檢查進程是否還在運行 + if process_obj and hasattr(process_obj, 'poll'): + if process_obj.poll() is None: # 進程還在運行 + if force: + debug_log(f"強制終止進程: PID {pid}") + process_obj.kill() + else: + debug_log(f"優雅終止進程: PID {pid}") + process_obj.terminate() + + # 等待進程結束 + try: + process_obj.wait(timeout=5) + cleaned_count += 1 + except subprocess.TimeoutExpired: + if not force: + debug_log(f"進程 {pid} 優雅終止超時,強制終止") + process_obj.kill() + process_obj.wait(timeout=3) + cleaned_count += 1 + + processes_to_remove.append(pid) + else: + # 使用 psutil 檢查進程 + try: + import psutil + if psutil.pid_exists(pid): + proc = psutil.Process(pid) + if force: + proc.kill() + else: + proc.terminate() + proc.wait(timeout=5) + cleaned_count += 1 + processes_to_remove.append(pid) + except ImportError: + debug_log("psutil 不可用,跳過進程檢查") + processes_to_remove.append(pid) + except Exception as e: + debug_log(f"清理進程 {pid} 失敗: {e}") + processes_to_remove.append(pid) + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "清理進程", "pid": pid}, + error_type=ErrorType.PROCESS + ) + debug_log(f"清理進程失敗 [錯誤ID: {error_id}]: {e}") + processes_to_remove.append(pid) + + # 移除已清理的進程追蹤 + for pid in processes_to_remove: + self.processes.pop(pid, None) + + return cleaned_count + + def cleanup_file_handles(self) -> int: + """ + 清理文件句柄 + + Returns: + int: 清理的句柄數量 + """ + cleaned_count = 0 + handles_to_remove = set() + + for handle_ref in self.file_handles.copy(): + try: + handle = handle_ref() + if handle is None: + # 弱引用已失效 + handles_to_remove.add(handle_ref) + continue + + # 嘗試關閉文件句柄 + if hasattr(handle, 'close') and not handle.closed: + handle.close() + cleaned_count += 1 + debug_log(f"關閉文件句柄: {type(handle).__name__}") + + handles_to_remove.add(handle_ref) + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "清理文件句柄"}, + error_type=ErrorType.FILE_IO + ) + debug_log(f"清理文件句柄失敗 [錯誤ID: {error_id}]: {e}") + handles_to_remove.add(handle_ref) + + # 移除已清理的句柄追蹤 + self.file_handles -= handles_to_remove + + return cleaned_count + + def cleanup_all(self, force: bool = False) -> Dict[str, int]: + """ + 清理所有資源 + + Args: + force: 是否強制清理 + + Returns: + Dict[str, int]: 清理統計 + """ + debug_log("開始全面資源清理...") + + results = { + "temp_files": 0, + "temp_dirs": 0, + "processes": 0, + "file_handles": 0 + } + + try: + # 清理文件句柄 + results["file_handles"] = self.cleanup_file_handles() + + # 清理進程 + results["processes"] = self.cleanup_processes(force=force) + + # 清理臨時文件 + results["temp_files"] = self.cleanup_temp_files(max_age=0) # 清理所有文件 + + # 清理臨時目錄 + results["temp_dirs"] = self.cleanup_temp_dirs() + + # 更新統計 + self.stats["cleanup_runs"] += 1 + self.stats["last_cleanup"] = time.time() + + total_cleaned = sum(results.values()) + debug_log(f"資源清理完成,共清理 {total_cleaned} 個資源: {results}") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "全面資源清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"全面資源清理失敗 [錯誤ID: {error_id}]: {e}") + + return results + + def _start_auto_cleanup(self) -> None: + """啟動自動清理線程""" + if not self.auto_cleanup_enabled or self._cleanup_thread: + return + + def cleanup_worker(): + """清理工作線程""" + while not self._stop_cleanup.wait(self.cleanup_interval): + try: + # 執行定期清理 + self.cleanup_temp_files() + self._check_process_health() + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "自動清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"自動清理失敗 [錯誤ID: {error_id}]: {e}") + + self._cleanup_thread = threading.Thread( + target=cleanup_worker, + name="ResourceManager-AutoCleanup", + daemon=True + ) + self._cleanup_thread.start() + debug_log("自動清理線程已啟動") + + def _check_process_health(self) -> None: + """檢查進程健康狀態""" + current_time = time.time() + + for pid, process_info in self.processes.items(): + try: + process_obj = process_info.get("process") + last_check = process_info.get("last_check", current_time) + + # 每分鐘檢查一次 + if current_time - last_check < 60: + continue + + # 更新檢查時間 + process_info["last_check"] = current_time + + # 檢查進程是否還在運行 + if process_obj and hasattr(process_obj, 'poll'): + if process_obj.poll() is not None: + # 進程已結束,移除追蹤 + debug_log(f"檢測到進程 {pid} 已結束,移除追蹤") + self.unregister_process(pid) + + except Exception as e: + debug_log(f"檢查進程 {pid} 健康狀態失敗: {e}") + + def stop_auto_cleanup(self) -> None: + """停止自動清理""" + if self._cleanup_thread: + self._stop_cleanup.set() + self._cleanup_thread.join(timeout=5) + self._cleanup_thread = None + debug_log("自動清理線程已停止") + + def get_resource_stats(self) -> Dict[str, Any]: + """ + 獲取資源統計信息 + + Returns: + Dict[str, Any]: 資源統計 + """ + current_stats = self.stats.copy() + current_stats.update({ + "current_temp_files": len(self.temp_files), + "current_temp_dirs": len(self.temp_dirs), + "current_processes": len(self.processes), + "current_file_handles": len(self.file_handles), + "auto_cleanup_enabled": self.auto_cleanup_enabled, + "cleanup_interval": self.cleanup_interval, + "temp_file_max_age": self.temp_file_max_age + }) + + # 添加內存監控統計 + try: + if hasattr(self, 'memory_monitor') and self.memory_monitor: + memory_info = self.memory_monitor.get_current_memory_info() + memory_stats = self.memory_monitor.get_memory_stats() + + current_stats.update({ + "memory_monitoring_enabled": self.memory_monitor.is_monitoring, + "current_memory_usage": memory_info.get("system", {}).get("usage_percent", 0), + "memory_status": memory_info.get("status", "unknown"), + "memory_cleanup_triggers": memory_stats.cleanup_triggers, + "memory_alerts_count": memory_stats.alerts_count + }) + except Exception as e: + debug_log(f"獲取內存統計失敗: {e}") + + return current_stats + + def get_detailed_info(self) -> Dict[str, Any]: + """ + 獲取詳細資源信息 + + Returns: + Dict[str, Any]: 詳細資源信息 + """ + return { + "temp_files": list(self.temp_files), + "temp_dirs": list(self.temp_dirs), + "processes": { + pid: { + "description": info.get("description", ""), + "auto_cleanup": info.get("auto_cleanup", True), + "registered_at": info.get("registered_at", 0), + "last_check": info.get("last_check", 0) + } + for pid, info in self.processes.items() + }, + "file_handles_count": len(self.file_handles), + "stats": self.get_resource_stats() + } + + def configure( + self, + auto_cleanup_enabled: Optional[bool] = None, + cleanup_interval: Optional[int] = None, + temp_file_max_age: Optional[int] = None + ) -> None: + """ + 配置資源管理器 + + Args: + auto_cleanup_enabled: 是否啟用自動清理 + cleanup_interval: 清理間隔(秒) + temp_file_max_age: 臨時文件最大年齡(秒) + """ + if auto_cleanup_enabled is not None: + old_enabled = self.auto_cleanup_enabled + self.auto_cleanup_enabled = auto_cleanup_enabled + + if old_enabled and not auto_cleanup_enabled: + self.stop_auto_cleanup() + elif not old_enabled and auto_cleanup_enabled: + self._start_auto_cleanup() + elif auto_cleanup_enabled and self._cleanup_thread is None: + # 如果啟用了自動清理但線程不存在,重新啟動 + self._start_auto_cleanup() + + if cleanup_interval is not None: + self.cleanup_interval = max(60, cleanup_interval) # 最小1分鐘 + + if temp_file_max_age is not None: + self.temp_file_max_age = max(300, temp_file_max_age) # 最小5分鐘 + + debug_log(f"ResourceManager 配置已更新: auto_cleanup={self.auto_cleanup_enabled}, " + f"interval={self.cleanup_interval}, max_age={self.temp_file_max_age}") + + +# 全局資源管理器實例 +_resource_manager = None + + +def get_resource_manager() -> ResourceManager: + """ + 獲取全局資源管理器實例 + + Returns: + ResourceManager: 資源管理器實例 + """ + global _resource_manager + if _resource_manager is None: + _resource_manager = ResourceManager() + return _resource_manager + + +# 便捷函數 +def create_temp_file(suffix: str = "", prefix: str = "mcp_", **kwargs) -> str: + """創建臨時文件的便捷函數""" + return get_resource_manager().create_temp_file(suffix=suffix, prefix=prefix, **kwargs) + + +def create_temp_dir(suffix: str = "", prefix: str = "mcp_", **kwargs) -> str: + """創建臨時目錄的便捷函數""" + return get_resource_manager().create_temp_dir(suffix=suffix, prefix=prefix, **kwargs) + + +def register_process(process: Union[subprocess.Popen, int], description: str = "", **kwargs) -> int: + """註冊進程的便捷函數""" + return get_resource_manager().register_process(process, description=description, **kwargs) + + +def cleanup_all_resources(force: bool = False) -> Dict[str, int]: + """清理所有資源的便捷函數""" + return get_resource_manager().cleanup_all(force=force) diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index f82e688..a77db81 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -10,7 +10,7 @@ "commands": "⚡ Commands", "command": "⚡ Commands", "settings": "⚙️ Settings", - "combined": "📝 Combined Mode", + "combined": "📝 Workspace", "about": "ℹ️ About" }, "feedback": { @@ -73,7 +73,7 @@ "history": "Command History" }, "combined": { - "description": "Combined mode: AI summary and feedback input are on the same page for easy comparison.", + "description": "AI summary and feedback input are on the same page for easy comparison.", "summaryTitle": "📋 AI Work Summary", "feedbackTitle": "💬 Provide Feedback" }, @@ -86,12 +86,10 @@ "interface": "Interface Settings", "layoutMode": "Interface Layout Mode", "layoutModeDesc": "Select how AI summary and feedback input are displayed", - "separateMode": "Separate Mode", - "separateModeDesc": "AI summary and feedback are in separate tabs", - "combinedVertical": "Combined Mode (Vertical Layout)", - "combinedVerticalDesc": "AI summary on top, feedback input below, both on the same page", - "combinedHorizontal": "Combined Mode (Horizontal Layout)", - "combinedHorizontalDesc": "AI summary on left, feedback input on right, expanding summary viewing area", + "combinedVertical": "Vertical Layout", + "combinedVerticalDesc": "AI summary on top, feedback input below, suitable for standard screens", + "combinedHorizontal": "Horizontal Layout", + "combinedHorizontalDesc": "AI summary on left, feedback input on right, suitable for widescreen displays", "autoClose": "Auto Close Page", "autoCloseDesc": "Automatically close page after submitting feedback", "theme": "Theme", @@ -172,6 +170,15 @@ "timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.", "closing": "Closing..." }, + "autoRefresh": { + "enable": "Auto Detect", + "seconds": "seconds", + "disabled": "Disabled", + "enabled": "Detecting", + "checking": "Checking", + "detected": "Detected", + "error": "Failed" + }, "dynamic": { "aiSummary": "Test Web UI Functionality\n\n🎯 **Test Items:**\n- Web UI server startup and operation\n- WebSocket real-time communication\n- Feedback submission functionality\n- Image upload and preview\n- Command execution functionality\n- Smart Ctrl+V image pasting\n- Multi-language interface functionality\n\n📋 **Test Steps:**\n1. Test image upload (drag-drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart pasting\n3. Try switching languages (Traditional Chinese/Simplified Chinese/English)\n4. Test command execution functionality\n5. Submit feedback and images\n\nPlease test these features and provide feedback!", diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json index ca91af0..e1944df 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -10,7 +10,7 @@ "commands": "⚡ 命令", "command": "⚡ 命令", "settings": "⚙️ 设置", - "combined": "📝 合并模式", + "combined": "📝 工作区", "about": "ℹ️ 关于" }, "feedback": { @@ -73,7 +73,7 @@ "history": "命令历史" }, "combined": { - "description": "合并模式:AI 摘要和反馈输入在同一页面中,方便对照查看。", + "description": "AI 摘要和反馈输入在同一页面中,方便对照查看。", "summaryTitle": "📋 AI 工作摘要", "feedbackTitle": "💬 提供反馈" }, @@ -86,12 +86,10 @@ "interface": "界面设定", "layoutMode": "界面布局模式", "layoutModeDesc": "选择 AI 摘要和反馈输入的显示方式", - "separateMode": "分离模式", - "separateModeDesc": "AI 摘要和反馈分别在不同页签", - "combinedVertical": "合并模式(垂直布局)", - "combinedVerticalDesc": "AI 摘要在上,反馈输入在下,摘要和反馈在同一页面", - "combinedHorizontal": "合并模式(水平布局)", - "combinedHorizontalDesc": "AI 摘要在左,反馈输入在右,增大摘要可视区域", + "combinedVertical": "垂直布局", + "combinedVerticalDesc": "AI 摘要在上,反馈输入在下,适合标准屏幕使用", + "combinedHorizontal": "水平布局", + "combinedHorizontalDesc": "AI 摘要在左,反馈输入在右,适合宽屏幕使用", "autoClose": "自动关闭页面", "autoCloseDesc": "提交回馈后自动关闭页面", "theme": "主题", @@ -172,6 +170,15 @@ "timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。", "closing": "正在关闭..." }, + "autoRefresh": { + "enable": "自动检测", + "seconds": "秒", + "disabled": "停用", + "enabled": "检测中", + "checking": "检查中", + "detected": "已检测", + "error": "失败" + }, "dynamic": { "aiSummary": "测试 Web UI 功能\n\n🎯 **功能测试项目:**\n- Web UI 服务器启动和运行\n- WebSocket 实时通讯\n- 反馈提交功能\n- 图片上传和预览\n- 命令执行功能\n- 智能 Ctrl+V 图片粘贴\n- 多语言界面功能\n\n📋 **测试步骤:**\n1. 测试图片上传(拖拽、选择文件、剪贴板)\n2. 在文本框内按 Ctrl+V 测试智能粘贴\n3. 尝试切换语言(繁中/简中/英文)\n4. 测试命令执行功能\n5. 提交反馈和图片\n\n请测试这些功能并提供反馈!", diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json index c43af12..4cdd204 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -10,7 +10,7 @@ "commands": "⚡ 命令", "command": "⚡ 命令", "settings": "⚙️ 設定", - "combined": "📝 合併模式", + "combined": "📝 工作區", "about": "ℹ️ 關於" }, "feedback": { @@ -73,7 +73,7 @@ "history": "命令歷史" }, "combined": { - "description": "合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。", + "description": "AI 摘要和回饋輸入在同一頁面中,方便對照查看。", "summaryTitle": "📋 AI 工作摘要", "feedbackTitle": "💬 提供回饋" }, @@ -86,12 +86,10 @@ "interface": "介面設定", "layoutMode": "界面佈局模式", "layoutModeDesc": "選擇 AI 摘要和回饋輸入的顯示方式", - "separateMode": "分離模式", - "separateModeDesc": "AI 摘要和回饋分別在不同頁籤", - "combinedVertical": "合併模式(垂直布局)", - "combinedVerticalDesc": "AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面", - "combinedHorizontal": "合併模式(水平布局)", - "combinedHorizontalDesc": "AI 摘要在左,回饋輸入在右,增大摘要可視區域", + "combinedVertical": "垂直佈局", + "combinedVerticalDesc": "AI 摘要在上,回饋輸入在下,適合標準螢幕使用", + "combinedHorizontal": "水平佈局", + "combinedHorizontalDesc": "AI 摘要在左,回饋輸入在右,適合寬螢幕使用", "autoClose": "自動關閉頁面", "autoCloseDesc": "提交回饋後自動關閉頁面", "theme": "主題", @@ -172,6 +170,15 @@ "timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。", "closing": "正在關閉..." }, + "autoRefresh": { + "enable": "自動檢測", + "seconds": "秒", + "disabled": "停用", + "enabled": "檢測中", + "checking": "檢查中", + "detected": "已檢測", + "error": "失敗" + }, "dynamic": { "aiSummary": "測試 Web UI 功能\n\n🎯 **功能測試項目:**\n- Web UI 服務器啟動和運行\n- WebSocket 即時通訊\n- 回饋提交功能\n- 圖片上傳和預覽\n- 命令執行功能\n- 智能 Ctrl+V 圖片貼上\n- 多語言介面功能\n\n📋 **測試步驟:**\n1. 測試圖片上傳(拖拽、選擇檔案、剪貼簿)\n2. 在文字框內按 Ctrl+V 測試智能貼上\n3. 嘗試切換語言(繁中/簡中/英文)\n4. 測試命令執行功能\n5. 提交回饋和圖片\n\n請測試這些功能並提供回饋!", diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 1e940bd..ac110d8 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -17,17 +17,23 @@ import threading import time import webbrowser from pathlib import Path -from typing import Dict, Optional +from typing import Dict, Optional, List +from datetime import datetime import uuid -from fastapi import FastAPI +from fastapi import FastAPI, Request, Response from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +from fastapi.middleware.gzip import GZipMiddleware import uvicorn -from .models import WebFeedbackSession, FeedbackResult +from .models import WebFeedbackSession, FeedbackResult, CleanupReason, SessionStatus from .routes import setup_routes from .utils import find_free_port, get_browser_opener +from .utils.port_manager import PortManager +from .utils.compression_config import get_compression_manager +from ..utils.error_handler import ErrorHandler, ErrorType +from ..utils.memory_monitor import get_memory_monitor from ..debug import web_debug_log as debug_log from ..i18n import get_i18n_manager @@ -56,10 +62,20 @@ class WebUIManager: else: debug_log(f"未設定 MCP_WEB_PORT 環境變數,使用預設端口 {preferred_port}") - # 優先使用指定端口,確保 localStorage 的一致性 - self.port = port or find_free_port(preferred_port=preferred_port) + # 使用增強的端口管理,支持自動清理 + self.port = port or PortManager.find_free_port_enhanced( + preferred_port=preferred_port, + auto_cleanup=True, + host=self.host + ) self.app = FastAPI(title="MCP Feedback Enhanced") + # 設置壓縮和緩存中間件 + self._setup_compression_middleware() + + # 設置內存監控 + self._setup_memory_monitoring() + # 重構:使用單一活躍會話而非會話字典 self.current_session: Optional[WebFeedbackSession] = None self.sessions: Dict[str, WebFeedbackSession] = {} # 保留用於向後兼容 @@ -70,6 +86,17 @@ class WebUIManager: # 會話更新通知標記 self._pending_session_update = False + # 會話清理統計 + self.cleanup_stats = { + "total_cleanups": 0, + "expired_cleanups": 0, + "memory_pressure_cleanups": 0, + "manual_cleanups": 0, + "last_cleanup_time": None, + "total_cleanup_duration": 0.0, + "sessions_cleaned": 0 + } + self.server_thread = None self.server_process = None self.i18n = get_i18n_manager() @@ -83,6 +110,105 @@ class WebUIManager: debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動") + def _setup_compression_middleware(self): + """設置壓縮和緩存中間件""" + # 獲取壓縮管理器 + compression_manager = get_compression_manager() + config = compression_manager.config + + # 添加 Gzip 壓縮中間件 + self.app.add_middleware( + GZipMiddleware, + minimum_size=config.minimum_size + ) + + # 添加緩存和壓縮統計中間件 + @self.app.middleware("http") + async def compression_and_cache_middleware(request: Request, call_next): + """壓縮和緩存中間件""" + response = await call_next(request) + + # 添加緩存頭 + if not config.should_exclude_path(request.url.path): + cache_headers = config.get_cache_headers(request.url.path) + for key, value in cache_headers.items(): + response.headers[key] = value + + # 更新壓縮統計(如果可能) + try: + content_length = int(response.headers.get('content-length', 0)) + content_encoding = response.headers.get('content-encoding', '') + was_compressed = 'gzip' in content_encoding + + if content_length > 0: + # 估算原始大小(如果已壓縮,假設壓縮比為 30%) + original_size = content_length if not was_compressed else int(content_length / 0.7) + compression_manager.update_stats(original_size, content_length, was_compressed) + except (ValueError, TypeError): + # 忽略統計錯誤,不影響正常響應 + pass + + return response + + debug_log("壓縮和緩存中間件設置完成") + + def _setup_memory_monitoring(self): + """設置內存監控""" + try: + self.memory_monitor = get_memory_monitor() + + # 添加 Web 應用特定的警告回調 + def web_memory_alert(alert): + debug_log(f"Web UI 內存警告 [{alert.level}]: {alert.message}") + + # 根據警告級別觸發不同的清理策略 + if alert.level == "critical": + # 危險級別:清理過期會話 + cleaned = self.cleanup_expired_sessions() + debug_log(f"內存危險警告觸發,清理了 {cleaned} 個過期會話") + elif alert.level == "emergency": + # 緊急級別:強制清理會話 + cleaned = self.cleanup_sessions_by_memory_pressure(force=True) + debug_log(f"內存緊急警告觸發,強制清理了 {cleaned} 個會話") + + self.memory_monitor.add_alert_callback(web_memory_alert) + + # 添加會話清理回調到內存監控 + def session_cleanup_callback(force: bool = False): + """內存監控觸發的會話清理回調""" + try: + if force: + # 強制清理:包括內存壓力清理 + cleaned = self.cleanup_sessions_by_memory_pressure(force=True) + debug_log(f"內存監控強制清理了 {cleaned} 個會話") + else: + # 常規清理:只清理過期會話 + cleaned = self.cleanup_expired_sessions() + debug_log(f"內存監控清理了 {cleaned} 個過期會話") + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "內存監控會話清理", "force": force}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"內存監控會話清理失敗 [錯誤ID: {error_id}]: {e}") + + self.memory_monitor.add_cleanup_callback(session_cleanup_callback) + + # 確保內存監控已啟動(ResourceManager 可能已經啟動了) + if not self.memory_monitor.is_monitoring: + self.memory_monitor.start_monitoring() + + debug_log("Web UI 內存監控設置完成,已集成會話清理回調") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "設置 Web UI 內存監控"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"設置 Web UI 內存監控失敗 [錯誤ID: {error_id}]: {e}") + def _setup_static_files(self): """設置靜態文件服務""" # Web UI 靜態文件 @@ -135,7 +261,7 @@ class WebUIManager: # 處理會話更新通知 if old_websocket: - # 有舊連接,立即發送會話更新通知 + # 有舊連接,立即發送會話更新通知並轉移連接 self._old_websocket_for_update = old_websocket self._new_session_for_update = session debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知") @@ -143,10 +269,13 @@ class WebUIManager: # 立即發送會話更新通知 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 else: # 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時) @@ -262,16 +391,44 @@ class WebUIManager: if e.errno == 10048: # Windows: 位址已在使用中 retry_count += 1 if retry_count < max_retries: - debug_log(f"端口 {self.port} 被占用,嘗試下一個端口") - self.port = find_free_port(self.port + 1) + debug_log(f"端口 {self.port} 被占用,使用增強端口管理查找新端口") + # 使用增強的端口管理查找新端口 + try: + self.port = PortManager.find_free_port_enhanced( + preferred_port=self.port + 1, + auto_cleanup=False, # 啟動時不自動清理,避免誤殺其他服務 + host=self.host + ) + debug_log(f"找到新的可用端口: {self.port}") + except RuntimeError as port_error: + # 使用統一錯誤處理 + error_id = ErrorHandler.log_error_with_context( + port_error, + context={"operation": "端口查找", "current_port": self.port}, + error_type=ErrorType.NETWORK + ) + debug_log(f"無法找到可用端口 [錯誤ID: {error_id}]: {port_error}") + break else: debug_log("已達到最大重試次數,無法啟動伺服器") break else: - debug_log(f"伺服器啟動錯誤: {e}") + # 使用統一錯誤處理 + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "伺服器啟動", "host": self.host, "port": self.port}, + error_type=ErrorType.NETWORK + ) + debug_log(f"伺服器啟動錯誤 [錯誤ID: {error_id}]: {e}") break except Exception as e: - debug_log(f"伺服器運行錯誤: {e}") + # 使用統一錯誤處理 + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "伺服器運行", "host": self.host, "port": self.port}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"伺服器運行錯誤 [錯誤ID: {error_id}]: {e}") break # 在新線程中啟動伺服器 @@ -351,8 +508,21 @@ class WebUIManager: old_websocket = self._old_websocket_for_update new_session = self._new_session_for_update - # 檢查舊連接是否仍然有效 - if old_websocket and not old_websocket.client_state.DISCONNECTED: + # 改進的連接有效性檢查 + 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({ @@ -369,11 +539,18 @@ class WebUIManager: # 延遲一小段時間讓前端處理消息 await asyncio.sleep(0.2) + # 將 WebSocket 連接轉移到新會話 + new_session.websocket = old_websocket + debug_log("已將 WebSocket 連接轉移到新會話") + except Exception as send_error: debug_log(f"發送會話更新通知失敗: {send_error}") - - # 安全關閉舊連接 - await self._safe_close_websocket(old_websocket) + # 如果發送失敗,仍然嘗試轉移連接 + new_session.websocket = old_websocket + debug_log("發送失敗但仍轉移 WebSocket 連接到新會話") + else: + debug_log("舊 WebSocket 連接無效,設置待更新標記") + self._pending_session_update = True # 清理臨時變數 delattr(self, '_old_websocket_for_update') @@ -390,29 +567,24 @@ class WebUIManager: self._pending_session_update = True async def _safe_close_websocket(self, websocket): - """安全關閉 WebSocket 連接,避免事件循環衝突""" + """安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用""" if not websocket: return + # 注意:此方法現在主要用於清理,因為連接已經轉移到新會話 + # 只有在確認連接沒有被新會話使用時才關閉 try: # 檢查連接狀態 - if websocket.client_state.DISCONNECTED: + if hasattr(websocket, 'client_state') and websocket.client_state.DISCONNECTED: debug_log("WebSocket 已斷開,跳過關閉操作") return - # 嘗試正常關閉 - await asyncio.wait_for(websocket.close(code=1000, reason="會話更新"), timeout=2.0) - debug_log("已正常關閉舊 WebSocket 連接") + # 由於連接已轉移到新會話,這裡不再主動關閉 + # 讓新會話管理這個連接的生命週期 + debug_log("WebSocket 連接已轉移到新會話,跳過關閉操作") - except asyncio.TimeoutError: - debug_log("WebSocket 關閉超時,強制斷開") - except RuntimeError as e: - if "attached to a different loop" in str(e): - debug_log(f"WebSocket 事件循環衝突,忽略關閉錯誤: {e}") - else: - debug_log(f"WebSocket 關閉時發生運行時錯誤: {e}") except Exception as e: - debug_log(f"關閉 WebSocket 連接時發生未知錯誤: {e}") + debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}") async def _check_active_tabs(self) -> bool: """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API""" @@ -451,13 +623,176 @@ class WebUIManager: """獲取伺服器 URL""" return f"http://{self.host}:{self.port}" + def cleanup_expired_sessions(self) -> int: + """清理過期會話""" + cleanup_start_time = time.time() + expired_sessions = [] + + # 掃描過期會話 + for session_id, session in self.sessions.items(): + if session.is_expired(): + expired_sessions.append(session_id) + + # 批量清理過期會話 + cleaned_count = 0 + for session_id in expired_sessions: + try: + session = self.sessions.get(session_id) + if session: + # 使用增強清理方法 + session._cleanup_sync_enhanced(CleanupReason.EXPIRED) + del self.sessions[session_id] + cleaned_count += 1 + + # 如果清理的是當前活躍會話,清空當前會話 + if self.current_session and self.current_session.session_id == session_id: + self.current_session = None + debug_log("清空過期的當前活躍會話") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"session_id": session_id, "operation": "清理過期會話"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"清理過期會話 {session_id} 失敗 [錯誤ID: {error_id}]: {e}") + + # 更新統計 + cleanup_duration = time.time() - cleanup_start_time + self.cleanup_stats.update({ + "total_cleanups": self.cleanup_stats["total_cleanups"] + 1, + "expired_cleanups": self.cleanup_stats["expired_cleanups"] + 1, + "last_cleanup_time": datetime.now().isoformat(), + "total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration, + "sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + cleaned_count + }) + + if cleaned_count > 0: + debug_log(f"清理了 {cleaned_count} 個過期會話,耗時: {cleanup_duration:.2f}秒") + + return cleaned_count + + def cleanup_sessions_by_memory_pressure(self, force: bool = False) -> int: + """根據內存壓力清理會話""" + cleanup_start_time = time.time() + sessions_to_clean = [] + + # 根據優先級選擇要清理的會話 + # 優先級:已完成 > 已提交反饋 > 錯誤狀態 > 空閒時間最長 + for session_id, session in self.sessions.items(): + # 跳過當前活躍會話(除非強制清理) + if not force and self.current_session and session.session_id == self.current_session.session_id: + continue + + # 優先清理已完成或錯誤狀態的會話 + if session.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.TIMEOUT]: + sessions_to_clean.append((session_id, session, 1)) # 高優先級 + elif session.status == SessionStatus.FEEDBACK_SUBMITTED: + # 已提交反饋但空閒時間較長的會話 + if session.get_idle_time() > 300: # 5分鐘空閒 + sessions_to_clean.append((session_id, session, 2)) # 中優先級 + elif session.get_idle_time() > 600: # 10分鐘空閒 + sessions_to_clean.append((session_id, session, 3)) # 低優先級 + + # 按優先級排序 + sessions_to_clean.sort(key=lambda x: x[2]) + + # 清理會話(限制數量避免過度清理) + max_cleanup = min(len(sessions_to_clean), 5 if not force else len(sessions_to_clean)) + cleaned_count = 0 + + for i in range(max_cleanup): + session_id, session, priority = sessions_to_clean[i] + try: + # 使用增強清理方法 + session._cleanup_sync_enhanced(CleanupReason.MEMORY_PRESSURE) + del self.sessions[session_id] + cleaned_count += 1 + + # 如果清理的是當前活躍會話,清空當前會話 + if self.current_session and self.current_session.session_id == session_id: + self.current_session = None + debug_log("因內存壓力清空當前活躍會話") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"session_id": session_id, "operation": "內存壓力清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"內存壓力清理會話 {session_id} 失敗 [錯誤ID: {error_id}]: {e}") + + # 更新統計 + cleanup_duration = time.time() - cleanup_start_time + self.cleanup_stats.update({ + "total_cleanups": self.cleanup_stats["total_cleanups"] + 1, + "memory_pressure_cleanups": self.cleanup_stats["memory_pressure_cleanups"] + 1, + "last_cleanup_time": datetime.now().isoformat(), + "total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration, + "sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + cleaned_count + }) + + if cleaned_count > 0: + debug_log(f"因內存壓力清理了 {cleaned_count} 個會話,耗時: {cleanup_duration:.2f}秒") + + return cleaned_count + + def get_session_cleanup_stats(self) -> dict: + """獲取會話清理統計""" + stats = self.cleanup_stats.copy() + stats.update({ + "active_sessions": len(self.sessions), + "current_session_id": self.current_session.session_id if self.current_session else None, + "expired_sessions": sum(1 for s in self.sessions.values() if s.is_expired()), + "idle_sessions": sum(1 for s in self.sessions.values() if s.get_idle_time() > 300), + "memory_usage_mb": 0 # 將在下面計算 + }) + + # 計算內存使用(如果可能) + try: + import psutil + process = psutil.Process() + stats["memory_usage_mb"] = round(process.memory_info().rss / (1024 * 1024), 2) + except: + pass + + return stats + + def _scan_expired_sessions(self) -> List[str]: + """掃描過期會話ID列表""" + expired_sessions = [] + for session_id, session in self.sessions.items(): + if session.is_expired(): + expired_sessions.append(session_id) + return expired_sessions + def stop(self): """停止 Web UI 服務""" # 清理所有會話 + cleanup_start_time = time.time() + session_count = len(self.sessions) + for session in list(self.sessions.values()): - session.cleanup() + try: + session._cleanup_sync_enhanced(CleanupReason.SHUTDOWN) + except Exception as e: + debug_log(f"停止服務時清理會話失敗: {e}") + self.sessions.clear() - + self.current_session = None + + # 更新統計 + cleanup_duration = time.time() - cleanup_start_time + self.cleanup_stats.update({ + "total_cleanups": self.cleanup_stats["total_cleanups"] + 1, + "manual_cleanups": self.cleanup_stats["manual_cleanups"] + 1, + "last_cleanup_time": datetime.now().isoformat(), + "total_cleanup_duration": self.cleanup_stats["total_cleanup_duration"] + cleanup_duration, + "sessions_cleaned": self.cleanup_stats["sessions_cleaned"] + session_count + }) + + debug_log(f"停止服務時清理了 {session_count} 個會話,耗時: {cleanup_duration:.2f}秒") + # 停止伺服器(注意:uvicorn 的 graceful shutdown 需要額外處理) if self.server_thread and self.server_thread.is_alive(): debug_log("正在停止 Web UI 服務") diff --git a/src/mcp_feedback_enhanced/web/models/__init__.py b/src/mcp_feedback_enhanced/web/models/__init__.py index d06dd01..36e7108 100644 --- a/src/mcp_feedback_enhanced/web/models/__init__.py +++ b/src/mcp_feedback_enhanced/web/models/__init__.py @@ -7,10 +7,12 @@ Web UI 資料模型模組 定義 Web UI 相關的資料結構和型別。 """ -from .feedback_session import WebFeedbackSession +from .feedback_session import WebFeedbackSession, SessionStatus, CleanupReason from .feedback_result import FeedbackResult __all__ = [ 'WebFeedbackSession', + 'SessionStatus', + 'CleanupReason', 'FeedbackResult' -] \ No newline at end of file +] \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index 746c2f2..05c8d6f 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -11,13 +11,17 @@ import asyncio import base64 import subprocess import threading +import time +from datetime import datetime, timedelta from enum import Enum from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Callable from fastapi import WebSocket from ...debug import web_debug_log as debug_log +from ...utils.resource_manager import get_resource_manager, register_process +from ...utils.error_handler import ErrorHandler, ErrorType class SessionStatus(Enum): @@ -28,6 +32,17 @@ class SessionStatus(Enum): COMPLETED = "completed" # 已完成 TIMEOUT = "timeout" # 超時 ERROR = "error" # 錯誤 + EXPIRED = "expired" # 已過期 + + +class CleanupReason(Enum): + """清理原因枚舉""" + TIMEOUT = "timeout" # 超時清理 + EXPIRED = "expired" # 過期清理 + MEMORY_PRESSURE = "memory_pressure" # 內存壓力清理 + MANUAL = "manual" # 手動清理 + ERROR = "error" # 錯誤清理 + SHUTDOWN = "shutdown" # 系統關閉清理 # 常數定義 MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制 @@ -38,7 +53,8 @@ TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web" 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, + auto_cleanup_delay: int = 3600, max_idle_time: int = 1800): self.session_id = session_id self.project_directory = project_directory self.summary = summary @@ -54,18 +70,49 @@ class WebFeedbackSession: # 新增:會話狀態管理 self.status = SessionStatus.WAITING self.status_message = "等待用戶回饋" - self.created_at = asyncio.get_event_loop().time() + # 統一使用 time.time() 以避免時間基準不一致 + self.created_at = time.time() self.last_activity = self.created_at + # 新增:自動清理配置 + self.auto_cleanup_delay = auto_cleanup_delay # 自動清理延遲時間(秒) + self.max_idle_time = max_idle_time # 最大空閒時間(秒) + self.cleanup_timer: Optional[threading.Timer] = None + self.cleanup_callbacks: List[Callable] = [] # 清理回調函數列表 + + # 新增:清理統計 + self.cleanup_stats = { + "cleanup_count": 0, + "last_cleanup_time": None, + "cleanup_reason": None, + "cleanup_duration": 0.0, + "memory_freed": 0, + "resources_cleaned": 0 + } + # 確保臨時目錄存在 TEMP_DIR.mkdir(parents=True, exist_ok=True) + # 獲取資源管理器實例 + self.resource_manager = get_resource_manager() + + # 啟動自動清理定時器 + self._schedule_auto_cleanup() + + debug_log(f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}秒") + def update_status(self, status: SessionStatus, message: str = None): """更新會話狀態""" self.status = status if message: self.status_message = message - self.last_activity = asyncio.get_event_loop().time() + # 統一使用 time.time() + self.last_activity = time.time() + + # 如果會話變為活躍狀態,重置清理定時器 + if status in [SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]: + self._schedule_auto_cleanup() + debug_log(f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}") def get_status_info(self) -> dict: @@ -86,6 +133,117 @@ class WebFeedbackSession: """檢查會話是否活躍""" return self.status in [SessionStatus.WAITING, SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED] + def is_expired(self) -> bool: + """檢查會話是否已過期""" + # 統一使用 time.time() + current_time = time.time() + + # 檢查是否超過最大空閒時間 + idle_time = current_time - self.last_activity + if idle_time > self.max_idle_time: + debug_log(f"會話 {self.session_id} 空閒時間過長: {idle_time:.1f}秒 > {self.max_idle_time}秒") + return True + + # 檢查是否處於已過期狀態 + if self.status == SessionStatus.EXPIRED: + return True + + # 檢查是否處於錯誤或超時狀態且超過一定時間 + if self.status in [SessionStatus.ERROR, SessionStatus.TIMEOUT]: + error_time = current_time - self.last_activity + if error_time > 300: # 錯誤狀態超過5分鐘視為過期 + debug_log(f"會話 {self.session_id} 錯誤狀態時間過長: {error_time:.1f}秒") + return True + + return False + + def get_age(self) -> float: + """獲取會話年齡(秒)""" + current_time = time.time() + return current_time - self.created_at + + def get_idle_time(self) -> float: + """獲取會話空閒時間(秒)""" + current_time = time.time() + return current_time - self.last_activity + + def _schedule_auto_cleanup(self): + """安排自動清理定時器""" + if self.cleanup_timer: + self.cleanup_timer.cancel() + + def auto_cleanup(): + """自動清理回調""" + try: + if not self._cleanup_done and self.is_expired(): + debug_log(f"會話 {self.session_id} 觸發自動清理(過期)") + # 使用異步方式執行清理 + import asyncio + try: + loop = asyncio.get_event_loop() + loop.create_task(self._cleanup_resources_enhanced(CleanupReason.EXPIRED)) + except RuntimeError: + # 如果沒有事件循環,使用同步清理 + self._cleanup_sync_enhanced(CleanupReason.EXPIRED) + else: + # 如果還沒過期,重新安排定時器 + self._schedule_auto_cleanup() + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"session_id": self.session_id, "operation": "自動清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"自動清理失敗 [錯誤ID: {error_id}]: {e}") + + self.cleanup_timer = threading.Timer(self.auto_cleanup_delay, auto_cleanup) + self.cleanup_timer.daemon = True + self.cleanup_timer.start() + debug_log(f"會話 {self.session_id} 自動清理定時器已設置,{self.auto_cleanup_delay}秒後觸發") + + def extend_cleanup_timer(self, additional_time: int = None): + """延長清理定時器""" + if additional_time is None: + additional_time = self.auto_cleanup_delay + + if self.cleanup_timer: + self.cleanup_timer.cancel() + + self.cleanup_timer = threading.Timer(additional_time, lambda: None) + self.cleanup_timer.daemon = True + self.cleanup_timer.start() + + debug_log(f"會話 {self.session_id} 清理定時器已延長 {additional_time} 秒") + + def add_cleanup_callback(self, callback: Callable): + """添加清理回調函數""" + if callback not in self.cleanup_callbacks: + self.cleanup_callbacks.append(callback) + debug_log(f"會話 {self.session_id} 添加清理回調函數") + + def remove_cleanup_callback(self, callback: Callable): + """移除清理回調函數""" + if callback in self.cleanup_callbacks: + self.cleanup_callbacks.remove(callback) + debug_log(f"會話 {self.session_id} 移除清理回調函數") + + def get_cleanup_stats(self) -> dict: + """獲取清理統計信息""" + stats = self.cleanup_stats.copy() + stats.update({ + "session_id": self.session_id, + "age": self.get_age(), + "idle_time": self.get_idle_time(), + "is_expired": self.is_expired(), + "is_active": self.is_active(), + "status": self.status.value, + "has_websocket": self.websocket is not None, + "has_process": self.process is not None, + "command_logs_count": len(self.command_logs), + "images_count": len(self.images) + }) + return stats + async def wait_for_feedback(self, timeout: int = 600) -> dict: """ 等待用戶回饋,包含圖片,支援超時自動清理 @@ -249,6 +407,13 @@ class WebFeedbackSession: universal_newlines=True ) + # 註冊進程到資源管理器 + register_process( + self.process, + description=f"WebFeedbackSession-{self.session_id}-command", + auto_cleanup=True + ) + # 在背景線程中讀取輸出 async def read_output(): loop = asyncio.get_event_loop() @@ -281,7 +446,10 @@ class WebFeedbackSession: # 等待進程完成 if self.process: exit_code = self.process.wait() - + + # 從資源管理器取消註冊進程 + self.resource_manager.unregister_process(self.process.pid) + # 發送命令完成信號 if self.websocket: try: @@ -307,33 +475,72 @@ class WebFeedbackSession: pass async def _cleanup_resources_on_timeout(self): - """超時時清理所有資源""" + """超時時清理所有資源(保持向後兼容)""" + await self._cleanup_resources_enhanced(CleanupReason.TIMEOUT) + + async def _cleanup_resources_enhanced(self, reason: CleanupReason): + """增強的資源清理方法""" if self._cleanup_done: return # 避免重複清理 - + + cleanup_start_time = time.time() self._cleanup_done = True - debug_log(f"開始清理會話 {self.session_id} 的資源...") - + + debug_log(f"開始清理會話 {self.session_id} 的資源,原因: {reason.value}") + + # 更新清理統計 + self.cleanup_stats["cleanup_count"] += 1 + self.cleanup_stats["cleanup_reason"] = reason.value + self.cleanup_stats["last_cleanup_time"] = datetime.now().isoformat() + + resources_cleaned = 0 + memory_before = 0 + try: - # 1. 關閉 WebSocket 連接 + # 記錄清理前的內存使用(如果可能) + try: + import psutil + process = psutil.Process() + memory_before = process.memory_info().rss + except: + pass + + # 1. 取消自動清理定時器 + if self.cleanup_timer: + self.cleanup_timer.cancel() + self.cleanup_timer = None + resources_cleaned += 1 + + # 2. 關閉 WebSocket 連接 if self.websocket: try: - # 先通知前端超時 + # 根據清理原因發送不同的通知消息 + message_map = { + CleanupReason.TIMEOUT: "會話已超時,介面將自動關閉", + CleanupReason.EXPIRED: "會話已過期,介面將自動關閉", + CleanupReason.MEMORY_PRESSURE: "系統內存不足,會話將被清理", + CleanupReason.MANUAL: "會話已被手動清理", + CleanupReason.ERROR: "會話發生錯誤,將被清理", + CleanupReason.SHUTDOWN: "系統正在關閉,會話將被清理" + } + await self.websocket.send_json({ - "type": "session_timeout", - "message": "會話已超時,介面將自動關閉" + "type": "session_cleanup", + "reason": reason.value, + "message": message_map.get(reason, "會話將被清理") }) await asyncio.sleep(0.1) # 給前端一點時間處理消息 # 安全關閉 WebSocket await self._safe_close_websocket() debug_log(f"會話 {self.session_id} WebSocket 已關閉") + resources_cleaned += 1 except Exception as e: debug_log(f"關閉 WebSocket 時發生錯誤: {e}") finally: self.websocket = None - - # 2. 終止正在運行的命令進程 + + # 3. 終止正在運行的命令進程 if self.process: try: self.process.terminate() @@ -343,67 +550,213 @@ class WebFeedbackSession: except subprocess.TimeoutExpired: self.process.kill() debug_log(f"會話 {self.session_id} 命令進程已強制終止") + resources_cleaned += 1 except Exception as e: debug_log(f"終止命令進程時發生錯誤: {e}") finally: self.process = None - - # 3. 設置完成事件(防止其他地方還在等待) + + # 4. 設置完成事件(防止其他地方還在等待) self.feedback_completed.set() - - # 4. 清理臨時數據 + + # 5. 清理臨時數據 + logs_count = len(self.command_logs) + images_count = len(self.images) + self.command_logs.clear() self.images.clear() - - debug_log(f"會話 {self.session_id} 資源清理完成") - + self.settings.clear() + + if logs_count > 0 or images_count > 0: + resources_cleaned += logs_count + images_count + debug_log(f"清理了 {logs_count} 條日誌和 {images_count} 張圖片") + + # 6. 更新會話狀態 + if reason == CleanupReason.EXPIRED: + self.status = SessionStatus.EXPIRED + elif reason == CleanupReason.TIMEOUT: + self.status = SessionStatus.TIMEOUT + elif reason == CleanupReason.ERROR: + self.status = SessionStatus.ERROR + else: + self.status = SessionStatus.COMPLETED + + # 7. 調用清理回調函數 + for callback in self.cleanup_callbacks: + try: + if asyncio.iscoroutinefunction(callback): + await callback(self, reason) + else: + callback(self, reason) + except Exception as e: + debug_log(f"清理回調執行失敗: {e}") + + # 8. 計算清理效果 + cleanup_duration = time.time() - cleanup_start_time + memory_after = 0 + try: + import psutil + process = psutil.Process() + memory_after = process.memory_info().rss + except: + pass + + memory_freed = max(0, memory_before - memory_after) + + # 更新清理統計 + self.cleanup_stats.update({ + "cleanup_duration": cleanup_duration, + "memory_freed": memory_freed, + "resources_cleaned": resources_cleaned + }) + + debug_log(f"會話 {self.session_id} 資源清理完成,耗時: {cleanup_duration:.2f}秒," + f"清理資源: {resources_cleaned}個,釋放內存: {memory_freed}字節") + except Exception as e: - debug_log(f"清理會話 {self.session_id} 資源時發生錯誤: {e}") + error_id = ErrorHandler.log_error_with_context( + e, + context={ + "session_id": self.session_id, + "cleanup_reason": reason.value, + "operation": "增強資源清理" + }, + error_type=ErrorType.SYSTEM + ) + debug_log(f"清理會話 {self.session_id} 資源時發生錯誤 [錯誤ID: {error_id}]: {e}") + + # 即使發生錯誤也要更新統計 + self.cleanup_stats["cleanup_duration"] = time.time() - cleanup_start_time def _cleanup_sync(self): - """同步清理會話資源(但保留 WebSocket 連接)""" - if self._cleanup_done: + """同步清理會話資源(但保留 WebSocket 連接)- 保持向後兼容""" + self._cleanup_sync_enhanced(CleanupReason.MANUAL, preserve_websocket=True) + + def _cleanup_sync_enhanced(self, reason: CleanupReason, preserve_websocket: bool = False): + """增強的同步清理會話資源""" + if self._cleanup_done and not preserve_websocket: return - debug_log(f"同步清理會話 {self.session_id} 資源(保留 WebSocket)...") + cleanup_start_time = time.time() + debug_log(f"同步清理會話 {self.session_id} 資源,原因: {reason.value},保留WebSocket: {preserve_websocket}") - # 只清理進程,不清理 WebSocket 連接 - if self.process: + # 更新清理統計 + self.cleanup_stats["cleanup_count"] += 1 + self.cleanup_stats["cleanup_reason"] = reason.value + self.cleanup_stats["last_cleanup_time"] = datetime.now().isoformat() + + resources_cleaned = 0 + memory_before = 0 + + try: + # 記錄清理前的內存使用 try: - self.process.terminate() - self.process.wait(timeout=5) + import psutil + process = psutil.Process() + memory_before = process.memory_info().rss except: - try: - self.process.kill() - except: - pass - self.process = None + pass - # 清理臨時數據 - self.command_logs.clear() - # 注意:不設置 _cleanup_done = True,因為還需要清理 WebSocket + # 1. 取消自動清理定時器 + if self.cleanup_timer: + self.cleanup_timer.cancel() + self.cleanup_timer = None + resources_cleaned += 1 + + # 2. 清理進程 + if self.process: + try: + self.process.terminate() + self.process.wait(timeout=5) + debug_log(f"會話 {self.session_id} 命令進程已正常終止") + resources_cleaned += 1 + except: + try: + self.process.kill() + debug_log(f"會話 {self.session_id} 命令進程已強制終止") + resources_cleaned += 1 + except: + pass + self.process = None + + # 3. 清理臨時數據 + logs_count = len(self.command_logs) + images_count = len(self.images) + + self.command_logs.clear() + if not preserve_websocket: + self.images.clear() + self.settings.clear() + resources_cleaned += images_count + + resources_cleaned += logs_count + + # 4. 設置完成事件 + if not preserve_websocket: + self.feedback_completed.set() + + # 5. 更新狀態 + if not preserve_websocket: + if reason == CleanupReason.EXPIRED: + self.status = SessionStatus.EXPIRED + elif reason == CleanupReason.TIMEOUT: + self.status = SessionStatus.TIMEOUT + elif reason == CleanupReason.ERROR: + self.status = SessionStatus.ERROR + else: + self.status = SessionStatus.COMPLETED + + self._cleanup_done = True + + # 6. 調用清理回調函數(同步版本) + for callback in self.cleanup_callbacks: + try: + if not asyncio.iscoroutinefunction(callback): + callback(self, reason) + except Exception as e: + debug_log(f"同步清理回調執行失敗: {e}") + + # 7. 計算清理效果 + cleanup_duration = time.time() - cleanup_start_time + memory_after = 0 + try: + import psutil + process = psutil.Process() + memory_after = process.memory_info().rss + except: + pass + + memory_freed = max(0, memory_before - memory_after) + + # 更新清理統計 + self.cleanup_stats.update({ + "cleanup_duration": cleanup_duration, + "memory_freed": memory_freed, + "resources_cleaned": resources_cleaned + }) + + debug_log(f"會話 {self.session_id} 同步清理完成,耗時: {cleanup_duration:.2f}秒," + f"清理資源: {resources_cleaned}個,釋放內存: {memory_freed}字節") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={ + "session_id": self.session_id, + "cleanup_reason": reason.value, + "preserve_websocket": preserve_websocket, + "operation": "同步資源清理" + }, + error_type=ErrorType.SYSTEM + ) + debug_log(f"同步清理會話 {self.session_id} 資源時發生錯誤 [錯誤ID: {error_id}]: {e}") + + # 即使發生錯誤也要更新統計 + self.cleanup_stats["cleanup_duration"] = time.time() - cleanup_start_time def cleanup(self): """同步清理會話資源(保持向後兼容)""" - if self._cleanup_done: - return - - self._cleanup_done = True - debug_log(f"同步清理會話 {self.session_id} 資源...") - - if self.process: - try: - self.process.terminate() - self.process.wait(timeout=5) - except: - try: - self.process.kill() - except: - pass - self.process = None - - # 設置完成事件 - self.feedback_completed.set() + self._cleanup_sync_enhanced(CleanupReason.MANUAL) async def _safe_close_websocket(self): """安全關閉 WebSocket 連接,避免事件循環衝突""" diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py index a8ca0eb..a1efb7b 100644 --- a/src/mcp_feedback_enhanced/web/routes/main_routes.py +++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py @@ -33,15 +33,15 @@ def load_user_layout_settings() -> str: if settings_file.exists(): with open(settings_file, 'r', encoding='utf-8') as f: settings = json.load(f) - layout_mode = settings.get('layoutMode', 'separate') + layout_mode = settings.get('layoutMode', 'combined-vertical') debug_log(f"從設定檔案載入佈局模式: {layout_mode}") return layout_mode else: - debug_log("設定檔案不存在,使用預設佈局模式: separate") - return 'separate' + debug_log("設定檔案不存在,使用預設佈局模式: combined-vertical") + return 'combined-vertical' except Exception as e: - debug_log(f"載入佈局設定失敗: {e},使用預設佈局模式: separate") - return 'separate' + debug_log(f"載入佈局設定失敗: {e},使用預設佈局模式: combined-vertical") + return 'combined-vertical' def setup_routes(manager: 'WebUIManager'): @@ -140,6 +140,7 @@ def setup_routes(manager: 'WebUIManager'): ) return JSONResponse(content={ + "session_id": current_session.session_id, "project_directory": current_session.project_directory, "summary": current_session.summary, "feedback_completed": current_session.feedback_completed.is_set(), @@ -157,9 +158,13 @@ def setup_routes(manager: 'WebUIManager'): return await websocket.accept() - session.websocket = websocket - debug_log(f"WebSocket 連接建立: 當前活躍會話") + # 檢查會話是否已有 WebSocket 連接 + if session.websocket and session.websocket != websocket: + debug_log("會話已有 WebSocket 連接,替換為新連接") + + session.websocket = websocket + debug_log(f"WebSocket 連接建立: 當前活躍會話 {session.session_id}") # 發送連接成功消息 try: @@ -197,7 +202,14 @@ def setup_routes(manager: 'WebUIManager'): while True: data = await websocket.receive_text() message = json.loads(data) - await handle_websocket_message(manager, session, message) + + # 重新獲取當前會話,以防會話已切換 + current_session = manager.get_current_session() + if current_session and current_session.websocket == websocket: + await handle_websocket_message(manager, current_session, message) + else: + debug_log("會話已切換或 WebSocket 連接不匹配,忽略消息") + break except WebSocketDisconnect: debug_log(f"WebSocket 連接正常斷開") @@ -207,8 +219,9 @@ def setup_routes(manager: 'WebUIManager'): debug_log(f"WebSocket 錯誤: {e}") finally: # 安全清理 WebSocket 連接 - if session.websocket == websocket: - session.websocket = None + current_session = manager.get_current_session() + if current_session and current_session.websocket == websocket: + current_session.websocket = None debug_log("已清理會話中的 WebSocket 連接") @manager.app.post("/api/save-settings") diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css index b268490..518fe87 100644 --- a/src/mcp_feedback_enhanced/web/static/css/styles.css +++ b/src/mcp_feedback_enhanced/web/static/css/styles.css @@ -1265,4 +1265,128 @@ body { .compatibility-hint-btn:hover { background: #f57c00; +} + +/* 自動刷新控制項樣式 */ +.section-header-with-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +/* AI 工作摘要標題樣式 */ +h3.combined-section-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.auto-refresh-controls { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 6px 10px; + transition: all 0.3s ease; +} + +.auto-refresh-controls:hover { + border-color: var(--accent-color); + background: var(--surface-color); +} + +.auto-refresh-toggle { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + user-select: none; +} + +.auto-refresh-toggle input[type="checkbox"] { + appearance: none; + width: 14px; + height: 14px; + border: 1px solid var(--border-color); + border-radius: 2px; + background: var(--bg-primary); + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.auto-refresh-toggle input[type="checkbox"]:checked { + background: var(--accent-color); + border-color: var(--accent-color); +} + +.auto-refresh-toggle input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 9px; + font-weight: bold; +} + +.toggle-label { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + white-space: nowrap; +} + +.auto-refresh-interval { + display: flex; + align-items: center; + gap: 2px; +} + +.auto-refresh-interval input[type="number"] { + width: 50px; + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 3px; + padding: 4px 6px; + font-size: 13px; + text-align: center; +} + +.auto-refresh-interval input[type="number"]:focus { + outline: none; + border-color: var(--accent-color); +} + +.interval-unit { + color: var(--text-secondary); + font-size: 14px; + white-space: nowrap; +} + +.auto-refresh-status { + display: flex; + align-items: center; + gap: 3px; + padding: 1px 4px; + background: var(--bg-primary); + border-radius: 3px; + border: 1px solid var(--border-color); +} + +.auto-refresh-status .status-indicator { + font-size: 13px; +} + +.auto-refresh-status .status-text { + color: var(--text-secondary); + font-size: 14px; + white-space: nowrap; } \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 0b512a6..c91257f 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -170,6 +170,13 @@ class FeedbackApp { this.heartbeatInterval = null; this.heartbeatFrequency = 30000; // 30秒 WebSocket 心跳 + // 新增:WebSocket 連接狀態管理 + this.connectionReady = false; + this.pendingSubmission = null; + this.connectionCheckInterval = null; + this.sessionUpdatePending = false; + this.reconnectDelay = 1000; // 重連延遲,會逐漸增加 + // UI 狀態 this.currentTab = 'feedback'; @@ -185,11 +192,17 @@ class FeedbackApp { // 設定 this.autoClose = false; - this.layoutMode = 'separate'; + this.layoutMode = 'combined-vertical'; // 語言設定 this.currentLanguage = 'zh-TW'; + // 自動刷新設定 + this.autoRefreshEnabled = false; + this.autoRefreshInterval = 5; // 默認5秒 + this.autoRefreshTimer = null; + this.lastKnownSessionId = null; + this.init(); } @@ -223,6 +236,9 @@ class FeedbackApp { // 確保狀態指示器使用正確的翻譯(在國際化系統載入後) this.updateStatusIndicators(); + // 初始化自動刷新功能 + this.initAutoRefresh(); + // 設置頁面關閉時的清理 window.addEventListener('beforeunload', () => { if (this.tabManager) { @@ -231,6 +247,9 @@ class FeedbackApp { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } + if (this.autoRefreshTimer) { + clearInterval(this.autoRefreshTimer); + } }); console.log('MCP Feedback Enhanced 應用程式初始化完成'); @@ -259,6 +278,12 @@ class FeedbackApp { this.commandOutput = document.getElementById('commandOutput'); this.runCommandBtn = document.getElementById('runCommandBtn'); + // 自動刷新相關元素 + this.autoRefreshCheckbox = document.getElementById('autoRefreshEnabled'); + this.autoRefreshIntervalInput = document.getElementById('autoRefreshInterval'); + this.refreshStatusIndicator = document.getElementById('refreshStatusIndicator'); + this.refreshStatusText = document.getElementById('refreshStatusText'); + // 動態初始化圖片相關元素 this.initImageElements(); } @@ -409,15 +434,28 @@ class FeedbackApp { * 設置圖片事件監聽器 */ setupImageEventListeners() { + console.log(`🖼️ 設置圖片事件監聽器 - imageInput: ${this.imageInput?.id}, imageUploadArea: ${this.imageUploadArea?.id}`); + // 文件選擇事件 this.imageChangeHandler = (e) => { + console.log(`📁 文件選擇事件觸發 - input: ${e.target.id}, files: ${e.target.files.length}`); this.handleFileSelect(e.target.files); }; this.imageInput.addEventListener('change', this.imageChangeHandler); - // 點擊上傳區域 - this.imageClickHandler = () => { - this.imageInput.click(); + // 點擊上傳區域 - 使用更安全的方式確保只觸發對應的 input + this.imageClickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // 確保我們觸發的是正確的 input 元素 + const targetInput = this.imageInput; + if (targetInput) { + console.log(`🖱️ 點擊上傳區域 - 觸發 input: ${targetInput.id}`); + targetInput.click(); + } else { + console.warn('⚠️ 沒有找到對應的 input 元素'); + } }; this.imageUploadArea.addEventListener('click', this.imageClickHandler); @@ -451,8 +489,10 @@ class FeedbackApp { // 重新初始化圖片元素(確保使用最新的佈局模式) this.initImageElements(); + console.log(`🔍 檢查圖片元素 - imageUploadArea: ${this.imageUploadArea?.id || 'null'}, imageInput: ${this.imageInput?.id || 'null'}`); + if (!this.imageUploadArea || !this.imageInput) { - console.warn('⚠️ 圖片處理初始化失敗 - 缺少必要元素'); + console.warn(`⚠️ 圖片處理初始化失敗 - imageUploadArea: ${!!this.imageUploadArea}, imageInput: ${!!this.imageInput}`); return; } @@ -486,6 +526,7 @@ class FeedbackApp { * 移除舊的圖片事件監聽器 */ removeImageEventListeners() { + // 移除當前主要元素的事件監聽器 if (this.imageInput && this.imageChangeHandler) { this.imageInput.removeEventListener('change', this.imageChangeHandler); } @@ -503,6 +544,32 @@ class FeedbackApp { this.imageUploadArea.removeEventListener('drop', this.imageDropHandler); } } + + // 額外清理:移除所有可能的圖片上傳區域的 click 事件監聽器 + const allImageUploadAreas = [ + document.getElementById('feedbackImageUploadArea'), + document.getElementById('combinedImageUploadArea') + ].filter(area => area); + + allImageUploadAreas.forEach(area => { + if (area && this.imageClickHandler) { + area.removeEventListener('click', this.imageClickHandler); + console.log(`🧹 已移除 ${area.id} 的 click 事件監聽器`); + } + }); + + // 清理所有可能的 input 元素的 change 事件監聽器 + const allImageInputs = [ + document.getElementById('feedbackImageInput'), + document.getElementById('combinedImageInput') + ].filter(input => input); + + allImageInputs.forEach(input => { + if (input && this.imageChangeHandler) { + input.removeEventListener('change', this.imageChangeHandler); + console.log(`🧹 已移除 ${input.id} 的 change 事件監聽器`); + } + }); } /** @@ -797,11 +864,11 @@ class FeedbackApp { } /** - * 檢查是否可以提交回饋 + * 檢查是否可以提交回饋(舊版本,保持兼容性) */ canSubmitFeedback() { - const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected; - console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, canSubmit=${canSubmit}`); + const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected && this.connectionReady; + console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, connectionReady=${this.connectionReady}, canSubmit=${canSubmit}`); return canSubmit; } @@ -965,11 +1032,13 @@ class FeedbackApp { this.websocket.onopen = () => { this.isConnected = true; + this.connectionReady = false; // 等待連接確認 this.updateConnectionStatus('connected', '已連接'); console.log('WebSocket 連接已建立'); - // 重置重連計數器 + // 重置重連計數器和延遲 this.reconnectAttempts = 0; + this.reconnectDelay = 1000; // 開始 WebSocket 心跳 this.startWebSocketHeartbeat(); @@ -982,6 +1051,23 @@ class FeedbackApp { console.log('🔄 WebSocket 重連後重置處理狀態'); this.setFeedbackState('waiting_for_feedback'); } + + // 如果有待處理的會話更新,處理它 + if (this.sessionUpdatePending) { + console.log('🔄 處理待處理的會話更新'); + this.sessionUpdatePending = false; + } + + // 如果有待提交的回饋,處理它 + if (this.pendingSubmission) { + console.log('🔄 處理待提交的回饋'); + setTimeout(() => { + if (this.connectionReady && this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }, 500); // 等待連接完全就緒 + } }; this.websocket.onmessage = (event) => { @@ -995,6 +1081,7 @@ class FeedbackApp { this.websocket.onclose = (event) => { this.isConnected = false; + this.connectionReady = false; console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason); // 停止心跳 @@ -1012,15 +1099,23 @@ class FeedbackApp { } else { this.updateConnectionStatus('disconnected', '已斷開'); + // 會話更新導致的正常關閉,立即重連 + if (event.code === 1000 && event.reason === '會話更新') { + console.log('🔄 會話更新導致的連接關閉,立即重連...'); + this.sessionUpdatePending = true; + setTimeout(() => { + this.setupWebSocket(); + }, 200); // 短延遲後重連 + } // 只有在非正常關閉時才重連 - if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { + else if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; - const delay = Math.min(3000 * this.reconnectAttempts, 15000); // 最大延遲15秒 - console.log(`${delay/1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`); + this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000); // 指數退避,最大15秒 + console.log(`${this.reconnectDelay / 1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`); setTimeout(() => { console.log(`🔄 開始重連 WebSocket... (第${this.reconnectAttempts}次)`); this.setupWebSocket(); - }, delay); + }, this.reconnectDelay); } else if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('❌ 達到最大重連次數,停止重連'); this.showMessage('WebSocket 連接失敗,請刷新頁面重試', 'error'); @@ -1078,6 +1173,18 @@ class FeedbackApp { switch (data.type) { case 'connection_established': console.log('WebSocket 連接確認'); + this.connectionReady = true; + + // 如果有待提交的回饋,現在可以提交了 + if (this.pendingSubmission) { + console.log('🔄 連接就緒,提交待處理的回饋'); + setTimeout(() => { + if (this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }, 100); + } break; case 'heartbeat_response': // 心跳回應,更新標籤頁活躍狀態 @@ -1149,8 +1256,11 @@ class FeedbackApp { document.title = `MCP Feedback - ${projectName}`; } - // 使用局部更新替代整頁刷新 - this.refreshPageContent(); + // 確保 WebSocket 連接就緒 + this.ensureWebSocketReady(() => { + // 使用局部更新替代整頁刷新 + this.refreshPageContent(); + }); } else { // 如果沒有會話信息,仍然重置狀態 console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態'); @@ -1160,6 +1270,51 @@ class FeedbackApp { console.log('✅ 會話更新處理完成'); } + /** + * 確保 WebSocket 連接就緒 + */ + ensureWebSocketReady(callback, maxWaitTime = 5000) { + const startTime = Date.now(); + + const checkConnection = () => { + if (this.isConnected && this.connectionReady) { + console.log('✅ WebSocket 連接已就緒'); + if (callback) callback(); + return; + } + + const elapsed = Date.now() - startTime; + if (elapsed >= maxWaitTime) { + console.log('⚠️ WebSocket 連接等待超時,強制執行回調'); + if (callback) callback(); + return; + } + + // 如果連接斷開,嘗試重連 + if (!this.isConnected) { + console.log('🔄 WebSocket 未連接,嘗試重連...'); + this.setupWebSocket(); + } + + // 繼續等待 + setTimeout(checkConnection, 200); + }; + + checkConnection(); + } + + /** + * 檢查是否可以提交回饋 + */ + canSubmitFeedback() { + const canSubmit = this.isConnected && + this.connectionReady && + this.feedbackState === 'waiting_for_feedback'; + + console.log(`🔍 檢查提交權限: isConnected=${this.isConnected}, connectionReady=${this.connectionReady}, feedbackState=${this.feedbackState}, canSubmit=${canSubmit}`); + return canSubmit; + } + async refreshPageContent() { console.log('🔄 局部更新頁面內容...'); @@ -1200,16 +1355,22 @@ class FeedbackApp { const sessionData = await response.json(); console.log('📥 獲取到最新會話資料:', sessionData); - // 2. 更新 AI 摘要內容 + // 2. 重置回饋狀態為等待新回饋(使用新的會話 ID) + if (sessionData.session_id) { + this.setFeedbackState('waiting_for_feedback', sessionData.session_id); + console.log('🔄 已重置回饋狀態為等待新回饋'); + } + + // 3. 更新 AI 摘要內容 this.updateAISummaryContent(sessionData.summary); - // 3. 重置回饋表單 + // 4. 重置回饋表單 this.resetFeedbackForm(); - // 4. 更新狀態指示器 + // 5. 更新狀態指示器 this.updateStatusIndicators(); - // 5. 更新頁面標題 + // 6. 更新頁面標題 if (sessionData.project_directory) { const projectName = sessionData.project_directory.split(/[/\\]/).pop(); document.title = `MCP Feedback - ${projectName}`; @@ -1410,9 +1571,9 @@ class FeedbackApp { word-wrap: break-word; `; messageDiv.textContent = message; - + document.body.appendChild(messageDiv); - + // 3秒後自動移除 setTimeout(() => { if (messageDiv.parentNode) { @@ -1450,12 +1611,12 @@ class FeedbackApp { async loadFeedbackInterface(sessionInfo) { if (!this.mainContainer) return; - + this.sessionInfo = sessionInfo; - + // 載入完整的回饋界面 this.mainContainer.innerHTML = await this.generateFeedbackHTML(sessionInfo); - + // 重新設置事件監聽器 this.setupFeedbackEventListeners(); } @@ -1618,22 +1779,46 @@ class FeedbackApp { // 檢查是否可以提交回饋 if (!this.canSubmitFeedback()) { - console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected); + console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected, '連接就緒:', this.connectionReady); if (this.feedbackState === 'feedback_submitted') { this.showMessage('回饋已提交,請等待下次 MCP 調用', 'warning'); } else if (this.feedbackState === 'processing') { this.showMessage('正在處理中,請稍候', 'warning'); - } else if (!this.isConnected) { - this.showMessage('WebSocket 未連接,正在嘗試重連...', 'error'); - // 嘗試重新建立連接 - this.setupWebSocket(); + } else if (!this.isConnected || !this.connectionReady) { + // 收集回饋數據,等待連接就緒後提交 + const feedbackData = this.collectFeedbackData(); + if (feedbackData) { + this.pendingSubmission = feedbackData; + this.showMessage('WebSocket 連接中,回饋將在連接就緒後自動提交...', 'info'); + + // 確保 WebSocket 連接 + this.ensureWebSocketReady(() => { + if (this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }); + } } else { this.showMessage(`當前狀態不允許提交: ${this.feedbackState}`, 'warning'); } return; } + // 收集回饋數據並提交 + const feedbackData = this.collectFeedbackData(); + if (!feedbackData) { + return; + } + + this.submitFeedbackInternal(feedbackData); + } + + /** + * 收集回饋數據 + */ + collectFeedbackData() { // 根據當前佈局模式獲取回饋內容 let feedback = ''; if (this.layoutMode.startsWith('combined')) { @@ -1646,9 +1831,25 @@ class FeedbackApp { if (!feedback && this.images.length === 0) { this.showMessage('請提供回饋文字或上傳圖片', 'warning'); - return; + return null; } + return { + feedback: feedback, + images: [...this.images], // 創建副本 + settings: { + image_size_limit: this.imageSizeLimit, + enable_base64_detail: this.enableBase64Detail + } + }; + } + + /** + * 內部提交回饋方法 + */ + submitFeedbackInternal(feedbackData) { + console.log('📤 內部提交回饋...'); + // 設置處理狀態 this.setFeedbackState('processing'); @@ -1656,12 +1857,9 @@ class FeedbackApp { // 發送回饋 this.websocket.send(JSON.stringify({ type: 'submit_feedback', - feedback: feedback, - images: this.images, - settings: { - image_size_limit: this.imageSizeLimit, - enable_base64_detail: this.enableBase64Detail - } + feedback: feedbackData.feedback, + images: feedbackData.images, + settings: feedbackData.settings })); // 清空表單 @@ -1801,6 +1999,8 @@ class FeedbackApp { this.currentLanguage = settings.language || 'zh-TW'; this.imageSizeLimit = settings.imageSizeLimit || 0; this.enableBase64Detail = settings.enableBase64Detail || false; + this.autoRefreshEnabled = settings.autoRefreshEnabled || false; + this.autoRefreshInterval = settings.autoRefreshInterval || 5; // 處理 activeTab 設定 if (settings.activeTab) { @@ -1846,6 +2046,8 @@ class FeedbackApp { language: this.currentLanguage, imageSizeLimit: this.imageSizeLimit, enableBase64Detail: this.enableBase64Detail, + autoRefreshEnabled: this.autoRefreshEnabled, + autoRefreshInterval: this.autoRefreshInterval, activeTab: this.currentTab }; @@ -1904,6 +2106,15 @@ class FeedbackApp { if (this.enableBase64DetailCheckbox) { this.enableBase64DetailCheckbox.checked = this.enableBase64Detail; } + + // 應用自動刷新設定 + if (this.autoRefreshCheckbox) { + this.autoRefreshCheckbox.checked = this.autoRefreshEnabled; + } + + if (this.autoRefreshIntervalInput) { + this.autoRefreshIntervalInput.value = this.autoRefreshInterval; + } } applyLayoutMode() { @@ -1927,18 +2138,11 @@ class FeedbackApp { // 同步合併佈局和分頁中的內容 this.syncCombinedLayoutContent(); - // 如果是合併模式,確保內容同步 - if (this.layoutMode.startsWith('combined')) { - this.setupCombinedModeSync(); - // 如果當前頁籤不是合併模式,則切換到合併模式頁籤 - if (this.currentTab !== 'combined') { - this.currentTab = 'combined'; - } - } else { - // 分離模式時,如果當前頁籤是合併模式,則切換到回饋頁籤 - if (this.currentTab === 'combined') { - this.currentTab = 'feedback'; - } + // 確保合併模式內容同步 + this.setupCombinedModeSync(); + // 如果當前頁籤不是合併模式,則切換到合併模式頁籤 + if (this.currentTab !== 'combined') { + this.currentTab = 'combined'; } } @@ -1947,17 +2151,10 @@ class FeedbackApp { const feedbackTab = document.querySelector('.tab-button[data-tab="feedback"]'); const summaryTab = document.querySelector('.tab-button[data-tab="summary"]'); - if (this.layoutMode.startsWith('combined')) { - // 合併模式:顯示合併模式頁籤,隱藏回饋和AI摘要頁籤 - if (combinedTab) combinedTab.style.display = 'inline-block'; - if (feedbackTab) feedbackTab.style.display = 'none'; - if (summaryTab) summaryTab.style.display = 'none'; - } else { - // 分離模式:隱藏合併模式頁籤,顯示回饋和AI摘要頁籤 - if (combinedTab) combinedTab.style.display = 'none'; - if (feedbackTab) feedbackTab.style.display = 'inline-block'; - if (summaryTab) summaryTab.style.display = 'inline-block'; - } + // 只使用合併模式:顯示合併模式頁籤,隱藏回饋和AI摘要頁籤 + if (combinedTab) combinedTab.style.display = 'inline-block'; + if (feedbackTab) feedbackTab.style.display = 'none'; + if (summaryTab) summaryTab.style.display = 'none'; } syncCombinedLayoutContent() { @@ -2012,27 +2209,6 @@ class FeedbackApp { } setupCombinedModeSync() { - // 設置文字輸入的雙向同步 - const feedbackText = document.getElementById('feedbackText'); - const combinedFeedbackText = document.getElementById('combinedFeedbackText'); - - if (feedbackText && combinedFeedbackText) { - // 移除舊的事件監聽器(如果存在) - feedbackText.removeEventListener('input', this.syncToCombinetText); - combinedFeedbackText.removeEventListener('input', this.syncToSeparateText); - - // 添加新的事件監聽器 - this.syncToCombinetText = (e) => { - combinedFeedbackText.value = e.target.value; - }; - this.syncToSeparateText = (e) => { - feedbackText.value = e.target.value; - }; - - feedbackText.addEventListener('input', this.syncToCombinetText); - combinedFeedbackText.addEventListener('input', this.syncToSeparateText); - } - // 設置圖片設定的同步 this.setupImageSettingsSync(); @@ -2077,45 +2253,20 @@ class FeedbackApp { setupImageUploadSync() { // 設置合併模式的圖片上傳功能 - const combinedImageInput = document.getElementById('combinedImageInput'); - const combinedImageUploadArea = document.getElementById('combinedImageUploadArea'); - - if (combinedImageInput && combinedImageUploadArea) { - // 簡化的圖片上傳同步 - 只需要基本的事件監聽器 - combinedImageInput.addEventListener('change', (e) => { - this.handleFileSelect(e.target.files); - }); - - combinedImageUploadArea.addEventListener('click', () => { - combinedImageInput.click(); - }); - - // 拖放事件 - combinedImageUploadArea.addEventListener('dragover', (e) => { - e.preventDefault(); - combinedImageUploadArea.classList.add('dragover'); - }); - - combinedImageUploadArea.addEventListener('dragleave', (e) => { - e.preventDefault(); - combinedImageUploadArea.classList.remove('dragover'); - }); - - combinedImageUploadArea.addEventListener('drop', (e) => { - e.preventDefault(); - combinedImageUploadArea.classList.remove('dragover'); - this.handleFileSelect(e.dataTransfer.files); - }); - } + // 注意:所有事件監聽器現在由 setupImageEventListeners() 統一處理 + // 這個函數保留用於未來可能的同步邏輯,但不再設置重複的事件監聽器 + console.log('🔄 setupImageUploadSync: 事件監聽器由 setupImageEventListeners() 統一處理'); } resetSettings() { localStorage.removeItem('mcp-feedback-settings'); - this.layoutMode = 'separate'; + this.layoutMode = 'combined-vertical'; this.autoClose = false; this.currentLanguage = 'zh-TW'; this.imageSizeLimit = 0; this.enableBase64Detail = false; + this.autoRefreshEnabled = false; + this.autoRefreshInterval = 5; this.applySettings(); this.saveSettings(); } @@ -2169,6 +2320,203 @@ class FeedbackApp { // 不需要手動複製,updateStatusIndicator() 會處理所有狀態指示器 } + /** + * 初始化自動刷新功能 + */ + initAutoRefresh() { + console.log('🔄 初始化自動刷新功能...'); + + // 檢查必要元素是否存在 + if (!this.autoRefreshCheckbox || !this.autoRefreshIntervalInput) { + console.warn('⚠️ 自動刷新元素不存在,跳過初始化'); + return; + } + + // 設置開關事件監聽器 + this.autoRefreshCheckbox.addEventListener('change', (e) => { + this.autoRefreshEnabled = e.target.checked; + this.handleAutoRefreshToggle(); + this.saveSettings(); + }); + + // 設置間隔輸入事件監聽器 + this.autoRefreshIntervalInput.addEventListener('change', (e) => { + const newInterval = parseInt(e.target.value); + if (newInterval >= 5 && newInterval <= 300) { + this.autoRefreshInterval = newInterval; + this.saveSettings(); + + // 如果自動刷新已啟用,重新啟動定時器 + if (this.autoRefreshEnabled) { + this.stopAutoRefresh(); + this.startAutoRefresh(); + } + } + }); + + // 從設定中恢復狀態 + this.autoRefreshCheckbox.checked = this.autoRefreshEnabled; + this.autoRefreshIntervalInput.value = this.autoRefreshInterval; + + // 延遲更新狀態指示器,確保 i18n 已完全載入 + setTimeout(() => { + this.updateAutoRefreshStatus(); + + // 如果自動刷新已啟用,啟動自動檢測 + if (this.autoRefreshEnabled) { + console.log('🔄 自動刷新已啟用,啟動自動檢測...'); + this.startAutoRefresh(); + } + }, 100); + + console.log('✅ 自動刷新功能初始化完成'); + } + + /** + * 處理自動刷新開關切換 + */ + handleAutoRefreshToggle() { + if (this.autoRefreshEnabled) { + this.startAutoRefresh(); + } else { + this.stopAutoRefresh(); + } + this.updateAutoRefreshStatus(); + } + + /** + * 啟動自動刷新 + */ + startAutoRefresh() { + if (this.autoRefreshTimer) { + clearInterval(this.autoRefreshTimer); + } + + // 記錄當前會話 ID + this.lastKnownSessionId = this.currentSessionId; + + this.autoRefreshTimer = setInterval(() => { + this.checkForSessionUpdate(); + }, this.autoRefreshInterval * 1000); + + console.log(`🔄 自動刷新已啟動,間隔: ${this.autoRefreshInterval}秒`); + } + + /** + * 停止自動刷新 + */ + stopAutoRefresh() { + if (this.autoRefreshTimer) { + clearInterval(this.autoRefreshTimer); + this.autoRefreshTimer = null; + } + console.log('⏸️ 自動刷新已停止'); + } + + /** + * 檢查會話更新 + */ + async checkForSessionUpdate() { + try { + this.updateAutoRefreshStatus('checking'); + + const response = await fetch('/api/current-session'); + if (!response.ok) { + throw new Error(`API 請求失敗: ${response.status}`); + } + + const sessionData = await response.json(); + + // 檢查會話 ID 是否變化 + if (sessionData.session_id && sessionData.session_id !== this.lastKnownSessionId) { + console.log(`🔄 檢測到新會話: ${this.lastKnownSessionId} -> ${sessionData.session_id}`); + + // 更新記錄的會話 ID + this.lastKnownSessionId = sessionData.session_id; + this.currentSessionId = sessionData.session_id; + + // 觸發局部刷新 + await this.updatePageContentPartially(); + + this.updateAutoRefreshStatus('detected'); + + // 短暫顯示檢測成功狀態,然後恢復為檢測中 + setTimeout(() => { + if (this.autoRefreshEnabled) { + this.updateAutoRefreshStatus('enabled'); + } + }, 2000); + } else { + this.updateAutoRefreshStatus('enabled'); + } + + } catch (error) { + console.error('❌ 自動刷新檢測失敗:', error); + this.updateAutoRefreshStatus('error'); + + // 短暫顯示錯誤狀態,然後恢復 + setTimeout(() => { + if (this.autoRefreshEnabled) { + this.updateAutoRefreshStatus('enabled'); + } + }, 3000); + } + } + + /** + * 更新自動刷新狀態指示器 + */ + updateAutoRefreshStatus(status = null) { + console.log(`🔧 updateAutoRefreshStatus 被調用,status: ${status}`); + console.log(`🔧 refreshStatusIndicator: ${this.refreshStatusIndicator ? 'found' : 'null'}`); + console.log(`🔧 refreshStatusText: ${this.refreshStatusText ? 'found' : 'null'}`); + + if (!this.refreshStatusIndicator || !this.refreshStatusText) { + console.log(`⚠️ 自動檢測狀態元素未找到,跳過更新`); + return; + } + + let indicator = '⏸️'; + let textKey = 'autoRefresh.disabled'; + + if (status === null) { + status = this.autoRefreshEnabled ? 'enabled' : 'disabled'; + } + + switch (status) { + case 'enabled': + indicator = '🔄'; + textKey = 'autoRefresh.enabled'; + break; + case 'checking': + indicator = '🔍'; + textKey = 'autoRefresh.checking'; + break; + case 'detected': + indicator = '✅'; + textKey = 'autoRefresh.detected'; + break; + case 'error': + indicator = '❌'; + textKey = 'autoRefresh.error'; + break; + case 'disabled': + default: + indicator = '⏸️'; + textKey = 'autoRefresh.disabled'; + break; + } + + this.refreshStatusIndicator.textContent = indicator; + + // 使用多語系翻譯 + + const translatedText = window.i18nManager.t(textKey); + console.log(`🔄 自動檢測狀態翻譯: ${textKey} -> ${translatedText} (語言: ${window.i18nManager.currentLanguage})`); + this.refreshStatusText.textContent = translatedText; + + } + } diff --git a/src/mcp_feedback_enhanced/web/static/js/i18n.js b/src/mcp_feedback_enhanced/web/static/js/i18n.js index 9bd4d42..93bcb38 100644 --- a/src/mcp_feedback_enhanced/web/static/js/i18n.js +++ b/src/mcp_feedback_enhanced/web/static/js/i18n.js @@ -178,6 +178,10 @@ class I18nManager { if (window.feedbackApp) { window.feedbackApp.updateUIState(); window.feedbackApp.updateStatusIndicator(); + // 更新自動檢測狀態文字 + if (window.feedbackApp.updateAutoRefreshStatus) { + window.feedbackApp.updateAutoRefreshStatus(); + } } } diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 2318419..4ae16be 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -23,18 +23,8 @@ /* 頁面特定的佈局模式樣式 */ - /* 佈局模式樣式 */ - /* 預設分離模式 - 顯示回饋和AI摘要頁籤,隱藏合併模式頁籤 */ - body.layout-separate .tab-button[data-tab="combined"] { - display: none; - } - - body.layout-separate .tab-button[data-tab="feedback"], - body.layout-separate .tab-button[data-tab="summary"] { - display: inline-block; - } - - /* 合併模式 - 顯示合併模式頁籤,隱藏回饋和AI摘要頁籤 */ + /* 佈局模式樣式 - 工作區模式 */ + /* 工作區模式 - 顯示工作區頁籤,隱藏回饋和AI摘要頁籤 */ body.layout-combined-vertical .tab-button[data-tab="combined"], body.layout-combined-horizontal .tab-button[data-tab="combined"] { display: inline-block; @@ -62,7 +52,7 @@ - /* 合併模式分頁的水平佈局樣式 */ + /* 工作區分頁的水平佈局樣式 */ #tab-combined.active.combined-horizontal .combined-content { display: flex !important; flex-direction: row !important; @@ -99,7 +89,7 @@ min-height: 200px; } - /* 合併模式分頁的垂直佈局樣式 */ + /* 工作區分頁的垂直佈局樣式 */ #tab-combined.active.combined-vertical .combined-content { display: flex !important; flex-direction: column !important; @@ -144,7 +134,7 @@ flex: 1; } - /* 合併模式基礎樣式 */ + /* 工作區基礎樣式 */ .combined-section { background: var(--bg-tertiary); border: 1px solid var(--border-color); @@ -395,9 +385,9 @@
- +
- +
- 合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。 + AI 摘要和回饋輸入在同一頁面中,方便對照查看。
-

📋 AI 工作摘要

+
+

📋 AI 工作摘要

+
+ +
+ + +
+
+ ⏸️ + +
+
+
{{ summary }}
@@ -570,23 +576,16 @@
- - -
-
- +
diff --git a/src/mcp_feedback_enhanced/web/utils/compression_config.py b/src/mcp_feedback_enhanced/web/utils/compression_config.py new file mode 100644 index 0000000..5522c33 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/utils/compression_config.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +壓縮配置管理器 +============== + +管理 Web UI 的 Gzip 壓縮配置和靜態文件緩存策略。 +支援可配置的壓縮參數和性能優化選項。 +""" + +import os +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class CompressionConfig: + """壓縮配置類""" + + # Gzip 壓縮設定 + minimum_size: int = 1000 # 最小壓縮大小(bytes) + compression_level: int = 6 # 壓縮級別 (1-9, 6為平衡點) + + # 緩存設定 + static_cache_max_age: int = 3600 # 靜態文件緩存時間(秒) + api_cache_max_age: int = 0 # API 響應緩存時間(秒,0表示不緩存) + + # 支援的 MIME 類型 + compressible_types: List[str] = None + + # 排除的路徑 + exclude_paths: List[str] = None + + def __post_init__(self): + """初始化後處理""" + if self.compressible_types is None: + self.compressible_types = [ + 'text/html', + 'text/css', + 'text/javascript', + 'text/plain', + 'application/json', + 'application/javascript', + 'application/xml', + 'application/rss+xml', + 'application/atom+xml', + 'image/svg+xml' + ] + + if self.exclude_paths is None: + self.exclude_paths = [ + '/ws', # WebSocket 連接 + '/api/ws', # WebSocket API + '/health', # 健康檢查 + ] + + @classmethod + def from_env(cls) -> 'CompressionConfig': + """從環境變數創建配置""" + return cls( + minimum_size=int(os.getenv('MCP_GZIP_MIN_SIZE', '1000')), + compression_level=int(os.getenv('MCP_GZIP_LEVEL', '6')), + static_cache_max_age=int(os.getenv('MCP_STATIC_CACHE_AGE', '3600')), + api_cache_max_age=int(os.getenv('MCP_API_CACHE_AGE', '0')) + ) + + def should_compress(self, content_type: str, content_length: int) -> bool: + """判斷是否應該壓縮""" + if content_length < self.minimum_size: + return False + + if not content_type: + return False + + # 檢查 MIME 類型 + for mime_type in self.compressible_types: + if content_type.startswith(mime_type): + return True + + return False + + def should_exclude_path(self, path: str) -> bool: + """判斷路徑是否應該排除壓縮""" + for exclude_path in self.exclude_paths: + if path.startswith(exclude_path): + return True + return False + + def get_cache_headers(self, path: str) -> Dict[str, str]: + """獲取緩存頭""" + headers = {} + + if path.startswith('/static/'): + # 靜態文件緩存 + headers['Cache-Control'] = f'public, max-age={self.static_cache_max_age}' + headers['Expires'] = self._get_expires_header(self.static_cache_max_age) + elif path.startswith('/api/') and self.api_cache_max_age > 0: + # API 緩存(如果啟用) + headers['Cache-Control'] = f'public, max-age={self.api_cache_max_age}' + headers['Expires'] = self._get_expires_header(self.api_cache_max_age) + else: + # 其他路徑不緩存 + headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + headers['Pragma'] = 'no-cache' + headers['Expires'] = '0' + + return headers + + def _get_expires_header(self, max_age: int) -> str: + """生成 Expires 頭""" + from datetime import datetime, timedelta + expires_time = datetime.utcnow() + timedelta(seconds=max_age) + return expires_time.strftime('%a, %d %b %Y %H:%M:%S GMT') + + def get_compression_stats(self) -> Dict[str, any]: + """獲取壓縮配置統計""" + return { + 'minimum_size': self.minimum_size, + 'compression_level': self.compression_level, + 'static_cache_max_age': self.static_cache_max_age, + 'compressible_types_count': len(self.compressible_types), + 'exclude_paths_count': len(self.exclude_paths), + 'compressible_types': self.compressible_types, + 'exclude_paths': self.exclude_paths + } + + +class CompressionManager: + """壓縮管理器""" + + def __init__(self, config: Optional[CompressionConfig] = None): + self.config = config or CompressionConfig.from_env() + self._stats = { + 'requests_total': 0, + 'requests_compressed': 0, + 'bytes_original': 0, + 'bytes_compressed': 0, + 'compression_ratio': 0.0 + } + + def update_stats(self, original_size: int, compressed_size: int, was_compressed: bool): + """更新壓縮統計""" + self._stats['requests_total'] += 1 + self._stats['bytes_original'] += original_size + + if was_compressed: + self._stats['requests_compressed'] += 1 + self._stats['bytes_compressed'] += compressed_size + else: + self._stats['bytes_compressed'] += original_size + + # 計算壓縮比率 + if self._stats['bytes_original'] > 0: + self._stats['compression_ratio'] = ( + 1 - self._stats['bytes_compressed'] / self._stats['bytes_original'] + ) * 100 + + def get_stats(self) -> Dict[str, any]: + """獲取壓縮統計""" + stats = self._stats.copy() + stats['compression_percentage'] = ( + self._stats['requests_compressed'] / max(self._stats['requests_total'], 1) * 100 + ) + return stats + + def reset_stats(self): + """重置統計""" + self._stats = { + 'requests_total': 0, + 'requests_compressed': 0, + 'bytes_original': 0, + 'bytes_compressed': 0, + 'compression_ratio': 0.0 + } + + +# 全域壓縮管理器實例 +_compression_manager: Optional[CompressionManager] = None + + +def get_compression_manager() -> CompressionManager: + """獲取全域壓縮管理器實例""" + global _compression_manager + if _compression_manager is None: + _compression_manager = CompressionManager() + return _compression_manager diff --git a/src/mcp_feedback_enhanced/web/utils/compression_monitor.py b/src/mcp_feedback_enhanced/web/utils/compression_monitor.py new file mode 100644 index 0000000..475bada --- /dev/null +++ b/src/mcp_feedback_enhanced/web/utils/compression_monitor.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +壓縮性能監控工具 +================ + +監控 Gzip 壓縮的性能效果,包括壓縮比率、響應時間和文件大小統計。 +提供實時性能數據和優化建議。 +""" + +import time +import threading +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass, field +from datetime import datetime, timedelta +import json + + +@dataclass +class CompressionMetrics: + """壓縮指標數據類""" + timestamp: datetime + path: str + original_size: int + compressed_size: int + compression_ratio: float + response_time: float + content_type: str + was_compressed: bool + + +@dataclass +class CompressionSummary: + """壓縮摘要統計""" + total_requests: int = 0 + compressed_requests: int = 0 + total_original_bytes: int = 0 + total_compressed_bytes: int = 0 + average_compression_ratio: float = 0.0 + average_response_time: float = 0.0 + compression_percentage: float = 0.0 + bandwidth_saved: int = 0 + top_compressed_paths: List[Tuple[str, float]] = field(default_factory=list) + + +class CompressionMonitor: + """壓縮性能監控器""" + + def __init__(self, max_metrics: int = 1000): + self.max_metrics = max_metrics + self.metrics: List[CompressionMetrics] = [] + self.lock = threading.Lock() + self._start_time = datetime.now() + + # 路徑統計 + self.path_stats: Dict[str, Dict] = {} + + # 內容類型統計 + self.content_type_stats: Dict[str, Dict] = {} + + def record_request(self, + path: str, + original_size: int, + compressed_size: int, + response_time: float, + content_type: str = "", + was_compressed: bool = False): + """記錄請求的壓縮數據""" + + compression_ratio = 0.0 + if original_size > 0 and was_compressed: + compression_ratio = (1 - compressed_size / original_size) * 100 + + metric = CompressionMetrics( + timestamp=datetime.now(), + path=path, + original_size=original_size, + compressed_size=compressed_size, + compression_ratio=compression_ratio, + response_time=response_time, + content_type=content_type, + was_compressed=was_compressed + ) + + with self.lock: + self.metrics.append(metric) + + # 限制記錄數量 + if len(self.metrics) > self.max_metrics: + self.metrics = self.metrics[-self.max_metrics:] + + # 更新路徑統計 + self._update_path_stats(metric) + + # 更新內容類型統計 + self._update_content_type_stats(metric) + + def _update_path_stats(self, metric: CompressionMetrics): + """更新路徑統計""" + path = metric.path + if path not in self.path_stats: + self.path_stats[path] = { + 'requests': 0, + 'compressed_requests': 0, + 'total_original_bytes': 0, + 'total_compressed_bytes': 0, + 'total_response_time': 0.0, + 'best_compression_ratio': 0.0 + } + + stats = self.path_stats[path] + stats['requests'] += 1 + stats['total_original_bytes'] += metric.original_size + stats['total_compressed_bytes'] += metric.compressed_size + stats['total_response_time'] += metric.response_time + + if metric.was_compressed: + stats['compressed_requests'] += 1 + stats['best_compression_ratio'] = max( + stats['best_compression_ratio'], + metric.compression_ratio + ) + + def _update_content_type_stats(self, metric: CompressionMetrics): + """更新內容類型統計""" + content_type = metric.content_type or 'unknown' + if content_type not in self.content_type_stats: + self.content_type_stats[content_type] = { + 'requests': 0, + 'compressed_requests': 0, + 'total_original_bytes': 0, + 'total_compressed_bytes': 0, + 'average_compression_ratio': 0.0 + } + + stats = self.content_type_stats[content_type] + stats['requests'] += 1 + stats['total_original_bytes'] += metric.original_size + stats['total_compressed_bytes'] += metric.compressed_size + + if metric.was_compressed: + stats['compressed_requests'] += 1 + + # 重新計算平均壓縮比 + if stats['total_original_bytes'] > 0: + stats['average_compression_ratio'] = ( + 1 - stats['total_compressed_bytes'] / stats['total_original_bytes'] + ) * 100 + + def get_summary(self, time_window: Optional[timedelta] = None) -> CompressionSummary: + """獲取壓縮摘要統計""" + with self.lock: + metrics = self.metrics + + # 如果指定時間窗口,過濾數據 + if time_window: + cutoff_time = datetime.now() - time_window + metrics = [m for m in metrics if m.timestamp >= cutoff_time] + + if not metrics: + return CompressionSummary() + + total_requests = len(metrics) + compressed_requests = sum(1 for m in metrics if m.was_compressed) + total_original_bytes = sum(m.original_size for m in metrics) + total_compressed_bytes = sum(m.compressed_size for m in metrics) + total_response_time = sum(m.response_time for m in metrics) + + # 計算統計數據 + compression_percentage = (compressed_requests / total_requests * 100) if total_requests > 0 else 0 + average_compression_ratio = 0.0 + bandwidth_saved = 0 + + if total_original_bytes > 0: + average_compression_ratio = (1 - total_compressed_bytes / total_original_bytes) * 100 + bandwidth_saved = total_original_bytes - total_compressed_bytes + + average_response_time = total_response_time / total_requests if total_requests > 0 else 0 + + # 獲取壓縮效果最好的路徑 + top_compressed_paths = self._get_top_compressed_paths() + + return CompressionSummary( + total_requests=total_requests, + compressed_requests=compressed_requests, + total_original_bytes=total_original_bytes, + total_compressed_bytes=total_compressed_bytes, + average_compression_ratio=average_compression_ratio, + average_response_time=average_response_time, + compression_percentage=compression_percentage, + bandwidth_saved=bandwidth_saved, + top_compressed_paths=top_compressed_paths + ) + + def _get_top_compressed_paths(self, limit: int = 5) -> List[Tuple[str, float]]: + """獲取壓縮效果最好的路徑""" + path_ratios = [] + + for path, stats in self.path_stats.items(): + if stats['compressed_requests'] > 0 and stats['total_original_bytes'] > 0: + compression_ratio = ( + 1 - stats['total_compressed_bytes'] / stats['total_original_bytes'] + ) * 100 + path_ratios.append((path, compression_ratio)) + + # 按壓縮比排序 + path_ratios.sort(key=lambda x: x[1], reverse=True) + return path_ratios[:limit] + + def get_path_stats(self) -> Dict[str, Dict]: + """獲取路徑統計""" + with self.lock: + return self.path_stats.copy() + + def get_content_type_stats(self) -> Dict[str, Dict]: + """獲取內容類型統計""" + with self.lock: + return self.content_type_stats.copy() + + def get_recent_metrics(self, limit: int = 100) -> List[CompressionMetrics]: + """獲取最近的指標數據""" + with self.lock: + return self.metrics[-limit:] if self.metrics else [] + + def reset_stats(self): + """重置統計數據""" + with self.lock: + self.metrics.clear() + self.path_stats.clear() + self.content_type_stats.clear() + self._start_time = datetime.now() + + def export_stats(self) -> Dict: + """導出統計數據為字典格式""" + summary = self.get_summary() + + return { + 'summary': { + 'total_requests': summary.total_requests, + 'compressed_requests': summary.compressed_requests, + 'compression_percentage': round(summary.compression_percentage, 2), + 'average_compression_ratio': round(summary.average_compression_ratio, 2), + 'bandwidth_saved_mb': round(summary.bandwidth_saved / (1024 * 1024), 2), + 'average_response_time_ms': round(summary.average_response_time * 1000, 2), + 'monitoring_duration_hours': round( + (datetime.now() - self._start_time).total_seconds() / 3600, 2 + ) + }, + 'top_compressed_paths': [ + {'path': path, 'compression_ratio': round(ratio, 2)} + for path, ratio in summary.top_compressed_paths + ], + 'path_stats': { + path: { + 'requests': stats['requests'], + 'compression_percentage': round( + stats['compressed_requests'] / stats['requests'] * 100, 2 + ) if stats['requests'] > 0 else 0, + 'average_response_time_ms': round( + stats['total_response_time'] / stats['requests'] * 1000, 2 + ) if stats['requests'] > 0 else 0, + 'bandwidth_saved_kb': round( + (stats['total_original_bytes'] - stats['total_compressed_bytes']) / 1024, 2 + ) + } + for path, stats in self.path_stats.items() + }, + 'content_type_stats': { + content_type: { + 'requests': stats['requests'], + 'compression_percentage': round( + stats['compressed_requests'] / stats['requests'] * 100, 2 + ) if stats['requests'] > 0 else 0, + 'average_compression_ratio': round(stats['average_compression_ratio'], 2) + } + for content_type, stats in self.content_type_stats.items() + } + } + + +# 全域監控器實例 +_compression_monitor: Optional[CompressionMonitor] = None + + +def get_compression_monitor() -> CompressionMonitor: + """獲取全域壓縮監控器實例""" + global _compression_monitor + if _compression_monitor is None: + _compression_monitor = CompressionMonitor() + return _compression_monitor diff --git a/src/mcp_feedback_enhanced/web/utils/port_manager.py b/src/mcp_feedback_enhanced/web/utils/port_manager.py new file mode 100644 index 0000000..635909d --- /dev/null +++ b/src/mcp_feedback_enhanced/web/utils/port_manager.py @@ -0,0 +1,307 @@ +""" +端口管理工具模組 + +提供增強的端口管理功能,包括: +- 智能端口查找 +- 進程檢測和清理 +- 端口衝突解決 +""" + +import socket +import subprocess +import platform +import psutil +import time +from typing import Optional, Dict, Any, List +from ...debug import debug_log + + +class PortManager: + """端口管理器 - 提供增強的端口管理功能""" + + @staticmethod + def find_process_using_port(port: int) -> Optional[Dict[str, Any]]: + """ + 查找占用指定端口的進程 + + Args: + port: 要檢查的端口號 + + Returns: + Dict[str, Any]: 進程信息字典,包含 pid, name, cmdline 等 + None: 如果沒有進程占用該端口 + """ + try: + for conn in psutil.net_connections(kind='inet'): + if conn.laddr.port == port and conn.status == psutil.CONN_LISTEN: + try: + process = psutil.Process(conn.pid) + return { + 'pid': conn.pid, + 'name': process.name(), + 'cmdline': ' '.join(process.cmdline()), + 'create_time': process.create_time(), + 'status': process.status() + } + except (psutil.NoSuchProcess, psutil.AccessDenied): + # 進程可能已經結束或無權限訪問 + continue + except Exception as e: + debug_log(f"查找端口 {port} 占用進程時發生錯誤: {e}") + + return None + + @staticmethod + def kill_process_on_port(port: int, force: bool = False) -> bool: + """ + 終止占用指定端口的進程 + + Args: + port: 要清理的端口號 + force: 是否強制終止進程 + + Returns: + bool: 是否成功終止進程 + """ + process_info = PortManager.find_process_using_port(port) + if not process_info: + debug_log(f"端口 {port} 沒有被任何進程占用") + return True + + try: + pid = process_info['pid'] + process = psutil.Process(pid) + process_name = process_info['name'] + + debug_log(f"發現進程 {process_name} (PID: {pid}) 占用端口 {port}") + + # 檢查是否是自己的進程(避免誤殺) + if 'mcp-feedback-enhanced' in process_info['cmdline'].lower(): + debug_log(f"檢測到 MCP Feedback Enhanced 相關進程,嘗試優雅終止") + + if force: + debug_log(f"強制終止進程 {process_name} (PID: {pid})") + process.kill() + else: + debug_log(f"優雅終止進程 {process_name} (PID: {pid})") + process.terminate() + + # 等待進程結束 + try: + process.wait(timeout=5) + debug_log(f"成功終止進程 {process_name} (PID: {pid})") + return True + except psutil.TimeoutExpired: + if not force: + debug_log(f"優雅終止超時,強制終止進程 {process_name} (PID: {pid})") + process.kill() + process.wait(timeout=3) + return True + else: + debug_log(f"強制終止進程 {process_name} (PID: {pid}) 失敗") + return False + + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: + debug_log(f"無法終止進程 (PID: {process_info['pid']}): {e}") + return False + except Exception as e: + debug_log(f"終止端口 {port} 占用進程時發生錯誤: {e}") + return False + + @staticmethod + def is_port_available(host: str, port: int) -> bool: + """ + 檢查端口是否可用 + + Args: + host: 主機地址 + port: 端口號 + + Returns: + bool: 端口是否可用 + """ + try: + # 首先嘗試不使用 SO_REUSEADDR 來檢測端口 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind((host, port)) + return True + except OSError: + # 如果綁定失敗,再檢查是否真的有進程在監聽 + # 使用 psutil 檢查是否有進程在監聽該端口 + try: + import psutil + for conn in psutil.net_connections(kind='inet'): + if (conn.laddr.port == port and + conn.laddr.ip in [host, '0.0.0.0', '::'] and + conn.status == psutil.CONN_LISTEN): + return False + # 沒有找到監聽的進程,可能是臨時占用,認為可用 + return True + except Exception: + # 如果 psutil 檢查失敗,保守地認為端口不可用 + return False + + @staticmethod + def find_free_port_enhanced( + preferred_port: int = 8765, + auto_cleanup: bool = True, + host: str = "127.0.0.1", + max_attempts: int = 100 + ) -> int: + """ + 增強的端口查找功能 + + Args: + preferred_port: 偏好端口號 + auto_cleanup: 是否自動清理占用端口的進程 + host: 主機地址 + max_attempts: 最大嘗試次數 + + Returns: + int: 可用的端口號 + + Raises: + RuntimeError: 如果找不到可用端口 + """ + # 首先嘗試偏好端口 + if PortManager.is_port_available(host, preferred_port): + debug_log(f"偏好端口 {preferred_port} 可用") + return preferred_port + + # 如果偏好端口被占用且啟用自動清理 + if auto_cleanup: + debug_log(f"偏好端口 {preferred_port} 被占用,嘗試清理占用進程") + process_info = PortManager.find_process_using_port(preferred_port) + + if process_info: + debug_log(f"端口 {preferred_port} 被進程 {process_info['name']} (PID: {process_info['pid']}) 占用") + + # 詢問用戶是否清理(在實際使用中可能需要配置選項) + if PortManager._should_cleanup_process(process_info): + if PortManager.kill_process_on_port(preferred_port): + # 等待一下讓端口釋放 + time.sleep(1) + if PortManager.is_port_available(host, preferred_port): + debug_log(f"成功清理端口 {preferred_port},現在可用") + return preferred_port + + # 如果偏好端口仍不可用,尋找其他端口 + debug_log(f"偏好端口 {preferred_port} 不可用,尋找其他可用端口") + + for i in range(max_attempts): + port = preferred_port + i + 1 + if PortManager.is_port_available(host, port): + debug_log(f"找到可用端口: {port}") + return port + + # 如果向上查找失敗,嘗試向下查找 + for i in range(1, min(preferred_port - 1024, max_attempts)): + port = preferred_port - i + if port < 1024: # 避免使用系統保留端口 + break + if PortManager.is_port_available(host, port): + debug_log(f"找到可用端口: {port}") + return port + + raise RuntimeError( + f"無法在 {preferred_port}±{max_attempts} 範圍內找到可用端口。" + f"請檢查是否有過多進程占用端口,或手動指定其他端口。" + ) + + @staticmethod + def _should_cleanup_process(process_info: Dict[str, Any]) -> bool: + """ + 判斷是否應該清理指定進程 + + Args: + process_info: 進程信息字典 + + Returns: + bool: 是否應該清理該進程 + """ + # 檢查是否是 MCP Feedback Enhanced 相關進程 + cmdline = process_info.get('cmdline', '').lower() + process_name = process_info.get('name', '').lower() + + # 如果是自己的進程,允許清理 + if any(keyword in cmdline for keyword in ['mcp-feedback-enhanced', 'mcp_feedback_enhanced']): + return True + + # 如果是 Python 進程且命令行包含相關關鍵字 + if 'python' in process_name and any(keyword in cmdline for keyword in ['uvicorn', 'fastapi']): + return True + + # 其他情況下,為了安全起見,不自動清理 + debug_log(f"進程 {process_info['name']} (PID: {process_info['pid']}) 不是 MCP 相關進程,跳過自動清理") + return False + + @staticmethod + def get_port_status(port: int, host: str = "127.0.0.1") -> Dict[str, Any]: + """ + 獲取端口狀態信息 + + Args: + port: 端口號 + host: 主機地址 + + Returns: + Dict[str, Any]: 端口狀態信息 + """ + status = { + 'port': port, + 'host': host, + 'available': False, + 'process': None, + 'error': None + } + + try: + # 檢查端口是否可用 + status['available'] = PortManager.is_port_available(host, port) + + # 如果不可用,查找占用進程 + if not status['available']: + status['process'] = PortManager.find_process_using_port(port) + + except Exception as e: + status['error'] = str(e) + debug_log(f"獲取端口 {port} 狀態時發生錯誤: {e}") + + return status + + @staticmethod + def list_listening_ports(start_port: int = 8000, end_port: int = 9000) -> List[Dict[str, Any]]: + """ + 列出指定範圍內正在監聽的端口 + + Args: + start_port: 起始端口 + end_port: 結束端口 + + Returns: + List[Dict[str, Any]]: 監聽端口列表 + """ + listening_ports = [] + + try: + for conn in psutil.net_connections(kind='inet'): + if (conn.status == psutil.CONN_LISTEN and + start_port <= conn.laddr.port <= end_port): + + try: + process = psutil.Process(conn.pid) + port_info = { + 'port': conn.laddr.port, + 'host': conn.laddr.ip, + 'pid': conn.pid, + 'process_name': process.name(), + 'cmdline': ' '.join(process.cmdline()) + } + listening_ports.append(port_info) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + except Exception as e: + debug_log(f"列出監聽端口時發生錯誤: {e}") + + return listening_ports diff --git a/src/mcp_feedback_enhanced/web/utils/session_cleanup_manager.py b/src/mcp_feedback_enhanced/web/utils/session_cleanup_manager.py new file mode 100644 index 0000000..5534db3 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/utils/session_cleanup_manager.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +會話清理管理器 +============== + +統一管理 Web 會話的清理策略、統計和性能監控。 +與內存監控系統深度集成,提供智能清理決策。 +""" + +import time +import threading +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Callable, Any +from dataclasses import dataclass, field +from enum import Enum + +from ...debug import web_debug_log as debug_log +from ...utils.error_handler import ErrorHandler, ErrorType +from ..models.feedback_session import CleanupReason, SessionStatus + + +@dataclass +class CleanupPolicy: + """清理策略配置""" + max_idle_time: int = 1800 # 最大空閒時間(秒) + max_session_age: int = 7200 # 最大會話年齡(秒) + max_sessions: int = 10 # 最大會話數量 + cleanup_interval: int = 300 # 清理間隔(秒) + memory_pressure_threshold: float = 0.8 # 內存壓力閾值 + enable_auto_cleanup: bool = True # 啟用自動清理 + preserve_active_session: bool = True # 保護活躍會話 + + +@dataclass +class CleanupStats: + """清理統計數據""" + total_cleanups: int = 0 + expired_cleanups: int = 0 + memory_pressure_cleanups: int = 0 + manual_cleanups: int = 0 + auto_cleanups: int = 0 + total_sessions_cleaned: int = 0 + total_cleanup_time: float = 0.0 + average_cleanup_time: float = 0.0 + last_cleanup_time: Optional[datetime] = None + cleanup_efficiency: float = 0.0 # 清理效率(清理的會話數/總會話數) + + +class CleanupTrigger(Enum): + """清理觸發器類型""" + AUTO = "auto" # 自動清理 + MEMORY_PRESSURE = "memory_pressure" # 內存壓力 + MANUAL = "manual" # 手動清理 + EXPIRED = "expired" # 過期清理 + CAPACITY = "capacity" # 容量限制 + + +class SessionCleanupManager: + """會話清理管理器""" + + def __init__(self, web_ui_manager, policy: CleanupPolicy = None): + """ + 初始化會話清理管理器 + + Args: + web_ui_manager: WebUIManager 實例 + policy: 清理策略配置 + """ + self.web_ui_manager = web_ui_manager + self.policy = policy or CleanupPolicy() + self.stats = CleanupStats() + + # 清理狀態 + self.is_running = False + self.cleanup_thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + + # 回調函數 + self.cleanup_callbacks: List[Callable] = [] + self.stats_callbacks: List[Callable] = [] + + # 清理歷史記錄 + self.cleanup_history: List[Dict[str, Any]] = [] + self.max_history = 100 + + debug_log("SessionCleanupManager 初始化完成") + + def start_auto_cleanup(self) -> bool: + """啟動自動清理""" + if not self.policy.enable_auto_cleanup: + debug_log("自動清理已禁用") + return False + + if self.is_running: + debug_log("自動清理已在運行") + return True + + try: + self.is_running = True + self._stop_event.clear() + + self.cleanup_thread = threading.Thread( + target=self._auto_cleanup_loop, + name="SessionCleanupManager", + daemon=True + ) + self.cleanup_thread.start() + + debug_log(f"自動清理已啟動,間隔 {self.policy.cleanup_interval} 秒") + return True + + except Exception as e: + self.is_running = False + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "啟動自動清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"啟動自動清理失敗 [錯誤ID: {error_id}]: {e}") + return False + + def stop_auto_cleanup(self) -> bool: + """停止自動清理""" + if not self.is_running: + debug_log("自動清理未在運行") + return True + + try: + self.is_running = False + self._stop_event.set() + + if self.cleanup_thread and self.cleanup_thread.is_alive(): + self.cleanup_thread.join(timeout=5) + + debug_log("自動清理已停止") + return True + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "停止自動清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"停止自動清理失敗 [錯誤ID: {error_id}]: {e}") + return False + + def _auto_cleanup_loop(self): + """自動清理主循環""" + debug_log("自動清理循環開始") + + while not self._stop_event.is_set(): + try: + # 執行清理檢查 + self._perform_auto_cleanup() + + # 等待下次清理 + if self._stop_event.wait(self.policy.cleanup_interval): + break + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "自動清理循環"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"自動清理循環錯誤 [錯誤ID: {error_id}]: {e}") + + # 發生錯誤時等待較短時間後重試 + if self._stop_event.wait(30): + break + + debug_log("自動清理循環結束") + + def _perform_auto_cleanup(self): + """執行自動清理""" + cleanup_start_time = time.time() + cleaned_sessions = 0 + + try: + # 1. 檢查會話數量限制 + if len(self.web_ui_manager.sessions) > self.policy.max_sessions: + cleaned = self._cleanup_by_capacity() + cleaned_sessions += cleaned + debug_log(f"容量限制清理了 {cleaned} 個會話") + + # 2. 清理過期會話 + cleaned = self._cleanup_expired_sessions() + cleaned_sessions += cleaned + + # 3. 清理空閒會話 + cleaned = self._cleanup_idle_sessions() + cleaned_sessions += cleaned + + # 4. 更新統計 + cleanup_duration = time.time() - cleanup_start_time + self._update_cleanup_stats( + CleanupTrigger.AUTO, + cleaned_sessions, + cleanup_duration + ) + + if cleaned_sessions > 0: + debug_log(f"自動清理完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒") + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "執行自動清理"}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"執行自動清理失敗 [錯誤ID: {error_id}]: {e}") + + def trigger_cleanup(self, trigger: CleanupTrigger, force: bool = False) -> int: + """觸發清理操作""" + cleanup_start_time = time.time() + cleaned_sessions = 0 + + try: + debug_log(f"觸發清理操作,觸發器: {trigger.value},強制: {force}") + + if trigger == CleanupTrigger.MEMORY_PRESSURE: + cleaned_sessions = self.web_ui_manager.cleanup_sessions_by_memory_pressure(force) + elif trigger == CleanupTrigger.EXPIRED: + cleaned_sessions = self.web_ui_manager.cleanup_expired_sessions() + elif trigger == CleanupTrigger.CAPACITY: + cleaned_sessions = self._cleanup_by_capacity() + elif trigger == CleanupTrigger.MANUAL: + # 手動清理:組合多種策略 + cleaned_sessions += self.web_ui_manager.cleanup_expired_sessions() + if force: + cleaned_sessions += self.web_ui_manager.cleanup_sessions_by_memory_pressure(force) + else: + # 自動清理 + self._perform_auto_cleanup() + return 0 # 統計已在 _perform_auto_cleanup 中更新 + + # 更新統計 + cleanup_duration = time.time() - cleanup_start_time + self._update_cleanup_stats(trigger, cleaned_sessions, cleanup_duration) + + debug_log(f"清理操作完成,清理了 {cleaned_sessions} 個會話,耗時: {cleanup_duration:.2f}秒") + return cleaned_sessions + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "觸發清理", "trigger": trigger.value, "force": force}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"觸發清理操作失敗 [錯誤ID: {error_id}]: {e}") + return 0 + + def _cleanup_by_capacity(self) -> int: + """根據容量限制清理會話""" + sessions = self.web_ui_manager.sessions + if len(sessions) <= self.policy.max_sessions: + return 0 + + # 計算需要清理的會話數量 + excess_count = len(sessions) - self.policy.max_sessions + + # 按優先級排序會話(優先清理舊的、非活躍的會話) + session_priorities = [] + for session_id, session in sessions.items(): + # 跳過當前活躍會話(如果啟用保護) + if (self.policy.preserve_active_session and + self.web_ui_manager.current_session and + session.session_id == self.web_ui_manager.current_session.session_id): + continue + + # 計算優先級分數(分數越高越優先清理) + priority_score = 0 + + # 狀態優先級 + if session.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.TIMEOUT]: + priority_score += 100 + elif session.status == SessionStatus.FEEDBACK_SUBMITTED: + priority_score += 50 + + # 年齡優先級 + age = session.get_age() + priority_score += age / 60 # 每分鐘加1分 + + # 空閒時間優先級 + idle_time = session.get_idle_time() + priority_score += idle_time / 30 # 每30秒加1分 + + session_priorities.append((session_id, session, priority_score)) + + # 按優先級排序並清理 + session_priorities.sort(key=lambda x: x[2], reverse=True) + cleaned_count = 0 + + for i in range(min(excess_count, len(session_priorities))): + session_id, session, _ = session_priorities[i] + try: + session._cleanup_sync_enhanced(CleanupReason.MANUAL) + del self.web_ui_manager.sessions[session_id] + cleaned_count += 1 + except Exception as e: + debug_log(f"容量清理會話 {session_id} 失敗: {e}") + + return cleaned_count + + def _cleanup_expired_sessions(self) -> int: + """清理過期會話""" + expired_sessions = [] + current_time = time.time() + + for session_id, session in self.web_ui_manager.sessions.items(): + # 檢查是否過期 + if session.is_expired(): + expired_sessions.append(session_id) + # 檢查是否超過最大年齡 + elif session.get_age() > self.policy.max_session_age: + expired_sessions.append(session_id) + + # 清理過期會話 + cleaned_count = 0 + for session_id in expired_sessions: + try: + session = self.web_ui_manager.sessions.get(session_id) + if session: + session._cleanup_sync_enhanced(CleanupReason.EXPIRED) + del self.web_ui_manager.sessions[session_id] + cleaned_count += 1 + + # 如果清理的是當前活躍會話,清空當前會話 + if (self.web_ui_manager.current_session and + self.web_ui_manager.current_session.session_id == session_id): + self.web_ui_manager.current_session = None + + except Exception as e: + debug_log(f"清理過期會話 {session_id} 失敗: {e}") + + return cleaned_count + + def _cleanup_idle_sessions(self) -> int: + """清理空閒會話""" + idle_sessions = [] + + for session_id, session in self.web_ui_manager.sessions.items(): + # 跳過當前活躍會話(如果啟用保護) + if (self.policy.preserve_active_session and + self.web_ui_manager.current_session and + session.session_id == self.web_ui_manager.current_session.session_id): + continue + + # 檢查是否空閒時間過長 + if session.get_idle_time() > self.policy.max_idle_time: + idle_sessions.append(session_id) + + # 清理空閒會話 + cleaned_count = 0 + for session_id in idle_sessions: + try: + session = self.web_ui_manager.sessions.get(session_id) + if session: + session._cleanup_sync_enhanced(CleanupReason.EXPIRED) + del self.web_ui_manager.sessions[session_id] + cleaned_count += 1 + + except Exception as e: + debug_log(f"清理空閒會話 {session_id} 失敗: {e}") + + return cleaned_count + + def _update_cleanup_stats(self, trigger: CleanupTrigger, cleaned_count: int, duration: float): + """更新清理統計""" + self.stats.total_cleanups += 1 + self.stats.total_sessions_cleaned += cleaned_count + self.stats.total_cleanup_time += duration + self.stats.last_cleanup_time = datetime.now() + + # 更新平均清理時間 + if self.stats.total_cleanups > 0: + self.stats.average_cleanup_time = self.stats.total_cleanup_time / self.stats.total_cleanups + + # 更新清理效率 + total_sessions = len(self.web_ui_manager.sessions) + cleaned_count + if total_sessions > 0: + self.stats.cleanup_efficiency = cleaned_count / total_sessions + + # 根據觸發器類型更新統計 + if trigger == CleanupTrigger.AUTO: + self.stats.auto_cleanups += 1 + elif trigger == CleanupTrigger.MEMORY_PRESSURE: + self.stats.memory_pressure_cleanups += 1 + elif trigger == CleanupTrigger.EXPIRED: + self.stats.expired_cleanups += 1 + elif trigger == CleanupTrigger.MANUAL: + self.stats.manual_cleanups += 1 + + # 記錄清理歷史 + cleanup_record = { + "timestamp": datetime.now().isoformat(), + "trigger": trigger.value, + "cleaned_count": cleaned_count, + "duration": duration, + "total_sessions_before": total_sessions, + "total_sessions_after": len(self.web_ui_manager.sessions) + } + + self.cleanup_history.append(cleanup_record) + + # 限制歷史記錄數量 + if len(self.cleanup_history) > self.max_history: + self.cleanup_history = self.cleanup_history[-self.max_history:] + + # 調用統計回調 + for callback in self.stats_callbacks: + try: + callback(self.stats, cleanup_record) + except Exception as e: + debug_log(f"統計回調執行失敗: {e}") + + def get_cleanup_statistics(self) -> Dict[str, Any]: + """獲取清理統計數據""" + stats_dict = { + "total_cleanups": self.stats.total_cleanups, + "expired_cleanups": self.stats.expired_cleanups, + "memory_pressure_cleanups": self.stats.memory_pressure_cleanups, + "manual_cleanups": self.stats.manual_cleanups, + "auto_cleanups": self.stats.auto_cleanups, + "total_sessions_cleaned": self.stats.total_sessions_cleaned, + "total_cleanup_time": round(self.stats.total_cleanup_time, 2), + "average_cleanup_time": round(self.stats.average_cleanup_time, 2), + "cleanup_efficiency": round(self.stats.cleanup_efficiency, 3), + "last_cleanup_time": self.stats.last_cleanup_time.isoformat() if self.stats.last_cleanup_time else None, + "is_auto_cleanup_running": self.is_running, + "current_sessions": len(self.web_ui_manager.sessions), + "policy": { + "max_idle_time": self.policy.max_idle_time, + "max_session_age": self.policy.max_session_age, + "max_sessions": self.policy.max_sessions, + "cleanup_interval": self.policy.cleanup_interval, + "enable_auto_cleanup": self.policy.enable_auto_cleanup, + "preserve_active_session": self.policy.preserve_active_session + } + } + + return stats_dict + + def get_cleanup_history(self, limit: int = 20) -> List[Dict[str, Any]]: + """獲取清理歷史記錄""" + return self.cleanup_history[-limit:] if self.cleanup_history else [] + + def add_cleanup_callback(self, callback: Callable): + """添加清理回調函數""" + if callback not in self.cleanup_callbacks: + self.cleanup_callbacks.append(callback) + debug_log("添加清理回調函數") + + def add_stats_callback(self, callback: Callable): + """添加統計回調函數""" + if callback not in self.stats_callbacks: + self.stats_callbacks.append(callback) + debug_log("添加統計回調函數") + + def update_policy(self, **kwargs): + """更新清理策略""" + for key, value in kwargs.items(): + if hasattr(self.policy, key): + setattr(self.policy, key, value) + debug_log(f"更新清理策略 {key} = {value}") + else: + debug_log(f"未知的策略參數: {key}") + + def reset_stats(self): + """重置統計數據""" + self.stats = CleanupStats() + self.cleanup_history.clear() + debug_log("清理統計數據已重置") + + def force_cleanup_all(self, exclude_current: bool = True) -> int: + """強制清理所有會話""" + sessions_to_clean = [] + + for session_id, session in self.web_ui_manager.sessions.items(): + # 是否排除當前活躍會話 + if (exclude_current and + self.web_ui_manager.current_session and + session.session_id == self.web_ui_manager.current_session.session_id): + continue + sessions_to_clean.append(session_id) + + # 清理會話 + cleaned_count = 0 + for session_id in sessions_to_clean: + try: + session = self.web_ui_manager.sessions.get(session_id) + if session: + session._cleanup_sync_enhanced(CleanupReason.MANUAL) + del self.web_ui_manager.sessions[session_id] + cleaned_count += 1 + except Exception as e: + debug_log(f"強制清理會話 {session_id} 失敗: {e}") + + # 更新統計 + self._update_cleanup_stats(CleanupTrigger.MANUAL, cleaned_count, 0.0) + + debug_log(f"強制清理完成,清理了 {cleaned_count} 個會話") + return cleaned_count diff --git a/tests/test_error_handler.py b/tests/test_error_handler.py new file mode 100644 index 0000000..3a973ac --- /dev/null +++ b/tests/test_error_handler.py @@ -0,0 +1,253 @@ +""" +錯誤處理框架測試模組 + +測試 ErrorHandler 類的各項功能,包括: +- 錯誤類型自動分類 +- 用戶友好錯誤信息生成 +- 國際化支持 +- 錯誤上下文記錄 +""" + +import pytest +import sys +import os +from unittest.mock import patch, MagicMock + +# 添加 src 目錄到 Python 路徑 +sys.path.insert(0, 'src') + +from mcp_feedback_enhanced.utils.error_handler import ErrorHandler, ErrorType, ErrorSeverity + + +class TestErrorHandler: + """錯誤處理器測試類""" + + def test_classify_error_network(self): + """測試網絡錯誤分類""" + # 測試 ConnectionError + error = ConnectionError("Connection failed") + assert ErrorHandler.classify_error(error) == ErrorType.NETWORK + + # 測試包含網絡關鍵字的錯誤(不包含 timeout) + error = Exception("socket connection failed") + assert ErrorHandler.classify_error(error) == ErrorType.NETWORK + + def test_classify_error_file_io(self): + """測試文件 I/O 錯誤分類""" + # 測試 FileNotFoundError + error = FileNotFoundError("No such file or directory") + assert ErrorHandler.classify_error(error) == ErrorType.FILE_IO + + # 測試包含文件關鍵字的錯誤(不包含權限關鍵字) + error = Exception("file not found") + assert ErrorHandler.classify_error(error) == ErrorType.FILE_IO + + def test_classify_error_timeout(self): + """測試超時錯誤分類""" + error = TimeoutError("Operation timed out") + assert ErrorHandler.classify_error(error) == ErrorType.TIMEOUT + + error = Exception("timeout occurred") + assert ErrorHandler.classify_error(error) == ErrorType.TIMEOUT + + def test_classify_error_permission(self): + """測試權限錯誤分類""" + error = PermissionError("Access denied") + assert ErrorHandler.classify_error(error) == ErrorType.PERMISSION + + error = Exception("access denied") + assert ErrorHandler.classify_error(error) == ErrorType.PERMISSION + + def test_classify_error_validation(self): + """測試驗證錯誤分類""" + error = ValueError("Invalid value") + assert ErrorHandler.classify_error(error) == ErrorType.VALIDATION + + error = TypeError("Wrong type") + assert ErrorHandler.classify_error(error) == ErrorType.VALIDATION + + def test_classify_error_default_system(self): + """測試默認系統錯誤分類""" + error = Exception("Some completely unknown issue") + assert ErrorHandler.classify_error(error) == ErrorType.SYSTEM + + def test_format_user_error_basic(self): + """測試基本用戶友好錯誤信息生成""" + error = ConnectionError("Connection failed") + result = ErrorHandler.format_user_error(error) + + assert "❌" in result + assert "網絡連接出現問題" in result or "网络连接出现问题" in result or "Network connection issue" in result + + def test_format_user_error_with_context(self): + """測試帶上下文的錯誤信息生成""" + error = FileNotFoundError("File not found") + context = { + "operation": "文件讀取", + "file_path": "/path/to/file.txt" + } + + result = ErrorHandler.format_user_error(error, context=context) + + assert "❌" in result + assert "文件讀取" in result or "文件读取" in result or "文件讀取" in result + assert "/path/to/file.txt" in result + + def test_format_user_error_with_technical_details(self): + """測試包含技術細節的錯誤信息""" + error = ValueError("Invalid input") + result = ErrorHandler.format_user_error(error, include_technical=True) + + assert "❌" in result + assert "ValueError" in result + assert "Invalid input" in result + + def test_get_error_solutions(self): + """測試獲取錯誤解決方案""" + solutions = ErrorHandler.get_error_solutions(ErrorType.NETWORK) + + assert isinstance(solutions, list) + assert len(solutions) > 0 + # 應該包含網絡相關的解決方案 + solutions_text = " ".join(solutions).lower() + assert any(keyword in solutions_text for keyword in ["網絡", "网络", "network", "連接", "连接", "connection"]) + + def test_log_error_with_context(self): + """測試帶上下文的錯誤記錄""" + error = Exception("Test error") + context = {"operation": "測試操作", "user": "test_user"} + + error_id = ErrorHandler.log_error_with_context(error, context=context) + + assert isinstance(error_id, str) + assert error_id.startswith("ERR_") + assert len(error_id.split("_")) == 3 # ERR_timestamp_id + + def test_create_error_response(self): + """測試創建標準化錯誤響應""" + error = ConnectionError("Network error") + context = {"operation": "網絡請求"} + + response = ErrorHandler.create_error_response(error, context=context) + + assert isinstance(response, dict) + assert response["success"] is False + assert "error_id" in response + assert "error_type" in response + assert "message" in response + assert response["error_type"] == ErrorType.NETWORK.value + assert "solutions" in response + + def test_create_error_response_for_user(self): + """測試為用戶界面創建錯誤響應""" + error = FileNotFoundError("File not found") + + response = ErrorHandler.create_error_response(error, for_user=True) + + assert response["success"] is False + assert "context" not in response # 用戶界面不應包含技術上下文 + assert "❌" in response["message"] # 應該包含用戶友好的格式 + + @patch('mcp_feedback_enhanced.utils.error_handler.ErrorHandler.get_i18n_error_message') + def test_language_support(self, mock_get_message): + """測試多語言支持""" + error = ConnectionError("Network error") + + # 測試繁體中文 + mock_get_message.return_value = "網絡連接出現問題" + result = ErrorHandler.format_user_error(error) + assert "網絡連接出現問題" in result + + # 測試簡體中文 + mock_get_message.return_value = "网络连接出现问题" + result = ErrorHandler.format_user_error(error) + assert "网络连接出现问题" in result + + # 測試英文 + mock_get_message.return_value = "Network connection issue" + result = ErrorHandler.format_user_error(error) + assert "Network connection issue" in result + + def test_error_severity_logging(self): + """測試錯誤嚴重程度記錄""" + error = Exception("Critical system error") + + # 測試高嚴重程度錯誤 + error_id = ErrorHandler.log_error_with_context( + error, + severity=ErrorSeverity.CRITICAL + ) + + assert isinstance(error_id, str) + assert error_id.startswith("ERR_") + + def test_get_current_language_fallback(self): + """測試語言獲取回退機制""" + # 由於 i18n 系統可能會覆蓋環境變數,我們主要測試函數不會拋出異常 + language = ErrorHandler.get_current_language() + assert isinstance(language, str) + assert len(language) > 0 + + # 測試語言代碼格式 + assert language in ["zh-TW", "zh-CN", "en"] or "-" in language + + def test_i18n_integration(self): + """測試國際化系統集成""" + # 測試當 i18n 系統不可用時的回退 + error_type = ErrorType.NETWORK + + # 測試獲取錯誤信息 + message = ErrorHandler.get_i18n_error_message(error_type) + assert isinstance(message, str) + assert len(message) > 0 + + # 測試獲取解決方案 + solutions = ErrorHandler.get_i18n_error_solutions(error_type) + assert isinstance(solutions, list) + + def test_error_context_preservation(self): + """測試錯誤上下文保存""" + error = Exception("Test error") + context = { + "operation": "測試操作", + "file_path": "/test/path", + "user_id": "test_user", + "timestamp": "2025-01-05" + } + + error_id = ErrorHandler.log_error_with_context(error, context=context) + + # 驗證錯誤 ID 格式 + assert isinstance(error_id, str) + assert error_id.startswith("ERR_") + + # 上下文應該被記錄到調試日誌中(通過 debug_log) + # 這裡我們主要驗證函數不會拋出異常 + + def test_json_rpc_safety(self): + """測試不影響 JSON RPC 通信""" + # 錯誤處理應該只記錄到 stderr(通過 debug_log) + # 不應該影響 stdout 或 JSON RPC 響應 + + error = Exception("Test error for JSON RPC safety") + context = {"operation": "JSON RPC 測試"} + + # 這些操作不應該影響 stdout + error_id = ErrorHandler.log_error_with_context(error, context=context) + user_message = ErrorHandler.format_user_error(error) + response = ErrorHandler.create_error_response(error) + + # 驗證返回值類型正確 + assert isinstance(error_id, str) + assert isinstance(user_message, str) + assert isinstance(response, dict) + + # 驗證不會拋出異常 + assert error_id.startswith("ERR_") + assert "❌" in user_message + assert response["success"] is False + + +if __name__ == '__main__': + # 運行測試 + pytest.main([__file__, '-v']) diff --git a/tests/test_gzip_compression.py b/tests/test_gzip_compression.py new file mode 100644 index 0000000..fc1d337 --- /dev/null +++ b/tests/test_gzip_compression.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Gzip 壓縮功能測試 +================ + +測試 FastAPI Gzip 壓縮中間件的功能,包括: +- 壓縮效果驗證 +- WebSocket 兼容性 +- 靜態文件緩存 +- 性能提升測試 +""" + +import pytest +import asyncio +import gzip +import json +from unittest.mock import Mock, patch +from fastapi.testclient import TestClient +from fastapi import FastAPI, Response +from fastapi.middleware.gzip import GZipMiddleware + +from src.mcp_feedback_enhanced.web.utils.compression_config import ( + CompressionConfig, CompressionManager, get_compression_manager +) +from src.mcp_feedback_enhanced.web.utils.compression_monitor import ( + CompressionMonitor, get_compression_monitor +) + + +class TestCompressionConfig: + """測試壓縮配置類""" + + def test_default_config(self): + """測試預設配置""" + config = CompressionConfig() + + assert config.minimum_size == 1000 + assert config.compression_level == 6 + assert config.static_cache_max_age == 3600 + assert config.api_cache_max_age == 0 + assert 'text/html' in config.compressible_types + assert 'application/json' in config.compressible_types + assert '/ws' in config.exclude_paths + + def test_from_env(self): + """測試從環境變數創建配置""" + with patch.dict('os.environ', { + 'MCP_GZIP_MIN_SIZE': '2000', + 'MCP_GZIP_LEVEL': '9', + 'MCP_STATIC_CACHE_AGE': '7200' + }): + config = CompressionConfig.from_env() + + assert config.minimum_size == 2000 + assert config.compression_level == 9 + assert config.static_cache_max_age == 7200 + + def test_should_compress(self): + """測試壓縮判斷邏輯""" + config = CompressionConfig() + + # 應該壓縮的情況 + assert config.should_compress('text/html', 2000) == True + assert config.should_compress('application/json', 1500) == True + + # 不應該壓縮的情況 + assert config.should_compress('text/html', 500) == False # 太小 + assert config.should_compress('image/jpeg', 2000) == False # 不支援的類型 + assert config.should_compress('', 2000) == False # 無內容類型 + + def test_should_exclude_path(self): + """測試路徑排除邏輯""" + config = CompressionConfig() + + assert config.should_exclude_path('/ws') == True + assert config.should_exclude_path('/api/ws') == True + assert config.should_exclude_path('/health') == True + assert config.should_exclude_path('/static/css/style.css') == False + assert config.should_exclude_path('/api/feedback') == False + + def test_get_cache_headers(self): + """測試緩存頭生成""" + config = CompressionConfig() + + # 靜態文件 + static_headers = config.get_cache_headers('/static/css/style.css') + assert 'Cache-Control' in static_headers + assert 'public, max-age=3600' in static_headers['Cache-Control'] + + # API 路徑(預設不緩存) + api_headers = config.get_cache_headers('/api/feedback') + assert 'no-cache' in api_headers['Cache-Control'] + + # 其他路徑 + other_headers = config.get_cache_headers('/feedback') + assert 'no-cache' in other_headers['Cache-Control'] + + +class TestCompressionManager: + """測試壓縮管理器""" + + def test_manager_initialization(self): + """測試管理器初始化""" + manager = CompressionManager() + + assert manager.config is not None + assert manager._stats['requests_total'] == 0 + assert manager._stats['requests_compressed'] == 0 + + def test_update_stats(self): + """測試統計更新""" + manager = CompressionManager() + + # 測試壓縮請求 + manager.update_stats(1000, 600, True) + stats = manager.get_stats() + + assert stats['requests_total'] == 1 + assert stats['requests_compressed'] == 1 + assert stats['bytes_original'] == 1000 + assert stats['bytes_compressed'] == 600 + assert stats['compression_ratio'] == 40.0 # (1000-600)/1000 * 100 + + # 測試未壓縮請求 + manager.update_stats(500, 500, False) + stats = manager.get_stats() + + assert stats['requests_total'] == 2 + assert stats['requests_compressed'] == 1 + assert stats['compression_percentage'] == 50.0 # 1/2 * 100 + + def test_reset_stats(self): + """測試統計重置""" + manager = CompressionManager() + manager.update_stats(1000, 600, True) + + manager.reset_stats() + stats = manager.get_stats() + + assert stats['requests_total'] == 0 + assert stats['requests_compressed'] == 0 + assert stats['compression_ratio'] == 0.0 + + +class TestCompressionMonitor: + """測試壓縮監控器""" + + def test_monitor_initialization(self): + """測試監控器初始化""" + monitor = CompressionMonitor() + + assert monitor.max_metrics == 1000 + assert len(monitor.metrics) == 0 + assert len(monitor.path_stats) == 0 + + def test_record_request(self): + """測試請求記錄""" + monitor = CompressionMonitor() + + monitor.record_request( + path='/static/css/style.css', + original_size=2000, + compressed_size=1200, + response_time=0.05, + content_type='text/css', + was_compressed=True + ) + + assert len(monitor.metrics) == 1 + metric = monitor.metrics[0] + assert metric.path == '/static/css/style.css' + assert metric.compression_ratio == 40.0 # (2000-1200)/2000 * 100 + + # 檢查路徑統計 + path_stats = monitor.get_path_stats() + assert '/static/css/style.css' in path_stats + assert path_stats['/static/css/style.css']['requests'] == 1 + assert path_stats['/static/css/style.css']['compressed_requests'] == 1 + + def test_get_summary(self): + """測試摘要統計""" + monitor = CompressionMonitor() + + # 記錄多個請求 + monitor.record_request('/static/css/style.css', 2000, 1200, 0.05, 'text/css', True) + monitor.record_request('/static/js/app.js', 3000, 1800, 0.08, 'application/javascript', True) + monitor.record_request('/api/feedback', 500, 500, 0.02, 'application/json', False) + + summary = monitor.get_summary() + + assert summary.total_requests == 3 + assert summary.compressed_requests == 2 + assert abs(summary.compression_percentage - 66.67) < 0.01 # 2/3 * 100 (約) + assert summary.bandwidth_saved == 2000 # (2000-1200) + (3000-1800) + 0 = 800 + 1200 + 0 = 2000 + + def test_export_stats(self): + """測試統計導出""" + monitor = CompressionMonitor() + + monitor.record_request('/static/css/style.css', 2000, 1200, 0.05, 'text/css', True) + + exported = monitor.export_stats() + + assert 'summary' in exported + assert 'top_compressed_paths' in exported + assert 'path_stats' in exported + assert 'content_type_stats' in exported + + assert exported['summary']['total_requests'] == 1 + assert exported['summary']['compressed_requests'] == 1 + + +class TestGzipIntegration: + """測試 Gzip 壓縮集成""" + + def create_test_app(self): + """創建測試應用""" + app = FastAPI() + + # 添加 Gzip 中間件 + app.add_middleware(GZipMiddleware, minimum_size=100) + + @app.get("/test-large") + async def test_large(): + # 返回大於最小壓縮大小的內容 + return {"data": "x" * 1000} + + @app.get("/test-small") + async def test_small(): + # 返回小於最小壓縮大小的內容 + return {"data": "small"} + + @app.get("/test-html") + async def test_html(): + html_content = "" + "content " * 100 + "" + return Response(content=html_content, media_type="text/html") + + return app + + def test_gzip_compression_large_content(self): + """測試大內容的 Gzip 壓縮""" + app = self.create_test_app() + client = TestClient(app) + + # 請求壓縮 + response = client.get("/test-large", headers={"Accept-Encoding": "gzip"}) + + assert response.status_code == 200 + assert response.headers.get("content-encoding") == "gzip" + + # 驗證內容正確性 + data = response.json() + assert "data" in data + assert len(data["data"]) == 1000 + + def test_gzip_compression_small_content(self): + """測試小內容不壓縮""" + app = self.create_test_app() + client = TestClient(app) + + response = client.get("/test-small", headers={"Accept-Encoding": "gzip"}) + + assert response.status_code == 200 + # 小內容不應該被壓縮 + assert response.headers.get("content-encoding") != "gzip" + + def test_gzip_compression_html_content(self): + """測試 HTML 內容壓縮""" + app = self.create_test_app() + client = TestClient(app) + + response = client.get("/test-html", headers={"Accept-Encoding": "gzip"}) + + assert response.status_code == 200 + assert response.headers.get("content-encoding") == "gzip" + assert response.headers.get("content-type") == "text/html; charset=utf-8" + + def test_no_compression_without_accept_encoding(self): + """測試不支援壓縮的客戶端""" + app = self.create_test_app() + client = TestClient(app) + + # FastAPI 的 TestClient 預設會添加 Accept-Encoding,所以我們測試明確拒絕壓縮 + response = client.get("/test-large", headers={"Accept-Encoding": "identity"}) + + assert response.status_code == 200 + # 當明確要求不壓縮時,應該不會有 gzip 編碼 + # 注意:某些情況下 FastAPI 仍可能壓縮,這是正常行為 + + +class TestWebSocketCompatibility: + """測試 WebSocket 兼容性""" + + def test_websocket_not_compressed(self): + """測試 WebSocket 連接不受壓縮影響""" + # 這個測試確保 WebSocket 路徑被正確排除 + config = CompressionConfig() + + # WebSocket 路徑應該被排除 + assert config.should_exclude_path('/ws') == True + assert config.should_exclude_path('/api/ws') == True + + # 確保 WebSocket 不會被壓縮配置影響 + assert not config.should_compress('application/json', 1000) or config.should_exclude_path('/ws') + + +@pytest.mark.asyncio +async def test_compression_performance(): + """測試壓縮性能""" + # 創建測試數據 + test_data = {"message": "test " * 1000} # 大約 5KB 的 JSON + json_data = json.dumps(test_data) + + # 手動壓縮測試 + compressed_data = gzip.compress(json_data.encode('utf-8')) + + # 驗證壓縮效果 + original_size = len(json_data.encode('utf-8')) + compressed_size = len(compressed_data) + compression_ratio = (1 - compressed_size / original_size) * 100 + + # 壓縮比應該大於 50%(JSON 數據通常壓縮效果很好) + assert compression_ratio > 50 + assert compressed_size < original_size + + # 驗證解壓縮正確性 + decompressed_data = gzip.decompress(compressed_data).decode('utf-8') + assert decompressed_data == json_data + + +def test_global_instances(): + """測試全域實例""" + # 測試壓縮管理器全域實例 + manager1 = get_compression_manager() + manager2 = get_compression_manager() + assert manager1 is manager2 + + # 測試壓縮監控器全域實例 + monitor1 = get_compression_monitor() + monitor2 = get_compression_monitor() + assert monitor1 is monitor2 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_memory_monitor.py b/tests/test_memory_monitor.py new file mode 100644 index 0000000..d484c8e --- /dev/null +++ b/tests/test_memory_monitor.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +內存監控系統測試 +================ + +測試集成式內存監控系統的功能,包括: +- 內存監控準確性 +- 警告機制 +- 清理觸發 +- 統計和分析功能 +""" + +import pytest +import time +import threading +from unittest.mock import Mock, patch, MagicMock +from datetime import datetime, timedelta + +from src.mcp_feedback_enhanced.utils.memory_monitor import ( + MemoryMonitor, MemorySnapshot, MemoryAlert, MemoryStats, + get_memory_monitor +) + + +class TestMemorySnapshot: + """測試內存快照數據類""" + + def test_memory_snapshot_creation(self): + """測試內存快照創建""" + snapshot = MemorySnapshot( + timestamp=datetime.now(), + system_total=8 * 1024**3, # 8GB + system_available=4 * 1024**3, # 4GB + system_used=4 * 1024**3, # 4GB + system_percent=50.0, + process_rss=100 * 1024**2, # 100MB + process_vms=200 * 1024**2, # 200MB + process_percent=1.25, + gc_objects=10000 + ) + + assert snapshot.system_total == 8 * 1024**3 + assert snapshot.system_percent == 50.0 + assert snapshot.process_rss == 100 * 1024**2 + assert snapshot.gc_objects == 10000 + + +class TestMemoryAlert: + """測試內存警告數據類""" + + def test_memory_alert_creation(self): + """測試內存警告創建""" + alert = MemoryAlert( + level="warning", + message="內存使用率較高: 85.0%", + timestamp=datetime.now(), + memory_percent=85.0, + recommended_action="考慮執行輕量級清理" + ) + + assert alert.level == "warning" + assert alert.memory_percent == 85.0 + assert "85.0%" in alert.message + + +class TestMemoryMonitor: + """測試內存監控器""" + + def test_monitor_initialization(self): + """測試監控器初始化""" + monitor = MemoryMonitor( + warning_threshold=0.7, + critical_threshold=0.85, + emergency_threshold=0.95, + monitoring_interval=10 + ) + + assert monitor.warning_threshold == 0.7 + assert monitor.critical_threshold == 0.85 + assert monitor.emergency_threshold == 0.95 + assert monitor.monitoring_interval == 10 + assert not monitor.is_monitoring + assert len(monitor.snapshots) == 0 + assert len(monitor.alerts) == 0 + + @patch('src.mcp_feedback_enhanced.utils.memory_monitor.psutil') + def test_collect_memory_snapshot(self, mock_psutil): + """測試內存快照收集""" + # 模擬 psutil 返回值 + mock_virtual_memory = Mock() + mock_virtual_memory.total = 8 * 1024**3 + mock_virtual_memory.available = 4 * 1024**3 + mock_virtual_memory.used = 4 * 1024**3 + mock_virtual_memory.percent = 50.0 + + mock_memory_info = Mock() + mock_memory_info.rss = 100 * 1024**2 + mock_memory_info.vms = 200 * 1024**2 + + mock_process = Mock() + mock_process.memory_info.return_value = mock_memory_info + mock_process.memory_percent.return_value = 1.25 + + mock_psutil.virtual_memory.return_value = mock_virtual_memory + mock_psutil.Process.return_value = mock_process + + monitor = MemoryMonitor() + snapshot = monitor._collect_memory_snapshot() + + assert snapshot.system_total == 8 * 1024**3 + assert snapshot.system_percent == 50.0 + assert snapshot.process_rss == 100 * 1024**2 + assert snapshot.process_percent == 1.25 + + def test_memory_status_classification(self): + """測試內存狀態分類""" + monitor = MemoryMonitor( + warning_threshold=0.8, + critical_threshold=0.9, + emergency_threshold=0.95 + ) + + assert monitor._get_memory_status(0.5) == "normal" + assert monitor._get_memory_status(0.85) == "warning" + assert monitor._get_memory_status(0.92) == "critical" + assert monitor._get_memory_status(0.97) == "emergency" + + def test_callback_management(self): + """測試回調函數管理""" + monitor = MemoryMonitor() + + cleanup_callback = Mock() + alert_callback = Mock() + + # 添加回調 + monitor.add_cleanup_callback(cleanup_callback) + monitor.add_alert_callback(alert_callback) + + assert cleanup_callback in monitor.cleanup_callbacks + assert alert_callback in monitor.alert_callbacks + + # 移除回調 + monitor.remove_cleanup_callback(cleanup_callback) + monitor.remove_alert_callback(alert_callback) + + assert cleanup_callback not in monitor.cleanup_callbacks + assert alert_callback not in monitor.alert_callbacks + + @patch('src.mcp_feedback_enhanced.utils.memory_monitor.gc') + def test_cleanup_triggering(self, mock_gc): + """測試清理觸發""" + monitor = MemoryMonitor() + cleanup_callback = Mock() + monitor.add_cleanup_callback(cleanup_callback) + + mock_gc.collect.return_value = 42 + + # 測試普通清理 + monitor._trigger_cleanup() + + assert monitor.cleanup_triggers_count == 1 + cleanup_callback.assert_called_once() + mock_gc.collect.assert_called() + + # 測試緊急清理 + cleanup_callback.reset_mock() + mock_gc.collect.reset_mock() + + monitor._trigger_emergency_cleanup() + + # 緊急清理會調用多次垃圾回收 + assert mock_gc.collect.call_count == 3 + + @patch('src.mcp_feedback_enhanced.utils.memory_monitor.psutil') + def test_memory_usage_checking(self, mock_psutil): + """測試內存使用檢查和警告觸發""" + monitor = MemoryMonitor( + warning_threshold=0.8, + critical_threshold=0.9, + emergency_threshold=0.95 + ) + + alert_callback = Mock() + cleanup_callback = Mock() + monitor.add_alert_callback(alert_callback) + monitor.add_cleanup_callback(cleanup_callback) + + # 模擬不同的內存使用情況 + test_cases = [ + (75.0, "normal", 0, 0), # 正常情況 + (85.0, "warning", 1, 0), # 警告情況 + (92.0, "critical", 1, 1), # 危險情況 + (97.0, "emergency", 1, 1), # 緊急情況 + ] + + for memory_percent, expected_status, expected_alerts, expected_cleanups in test_cases: + # 重置計數器 + alert_callback.reset_mock() + cleanup_callback.reset_mock() + monitor.alerts.clear() + monitor.cleanup_triggers_count = 0 + + # 創建模擬快照 + snapshot = MemorySnapshot( + timestamp=datetime.now(), + system_total=8 * 1024**3, + system_available=int(8 * 1024**3 * (100 - memory_percent) / 100), + system_used=int(8 * 1024**3 * memory_percent / 100), + system_percent=memory_percent, + process_rss=100 * 1024**2, + process_vms=200 * 1024**2, + process_percent=1.25, + gc_objects=10000 + ) + + # 檢查內存使用 + monitor._check_memory_usage(snapshot) + + # 驗證結果 + assert monitor._get_memory_status(memory_percent / 100.0) == expected_status + + if expected_alerts > 0: + assert len(monitor.alerts) == expected_alerts + assert alert_callback.call_count == expected_alerts + + if expected_cleanups > 0: + assert cleanup_callback.call_count == expected_cleanups + + def test_memory_trend_analysis(self): + """測試內存趨勢分析""" + monitor = MemoryMonitor() + + # 測試數據不足的情況 + assert monitor._analyze_memory_trend() == "insufficient_data" + + # 添加穩定趨勢的快照 + base_time = datetime.now() + for i in range(10): + snapshot = MemorySnapshot( + timestamp=base_time + timedelta(seconds=i * 30), + system_total=8 * 1024**3, + system_available=4 * 1024**3, + system_used=4 * 1024**3, + system_percent=50.0 + (i % 2), # 輕微波動 + process_rss=100 * 1024**2, + process_vms=200 * 1024**2, + process_percent=1.25, + gc_objects=10000 + ) + monitor.snapshots.append(snapshot) + + assert monitor._analyze_memory_trend() == "stable" + + # 清空並添加遞增趨勢的快照 + monitor.snapshots.clear() + for i in range(10): + snapshot = MemorySnapshot( + timestamp=base_time + timedelta(seconds=i * 30), + system_total=8 * 1024**3, + system_available=4 * 1024**3, + system_used=4 * 1024**3, + system_percent=50.0 + i * 2, # 遞增趨勢 + process_rss=100 * 1024**2, + process_vms=200 * 1024**2, + process_percent=1.25, + gc_objects=10000 + ) + monitor.snapshots.append(snapshot) + + assert monitor._analyze_memory_trend() == "increasing" + + @patch('src.mcp_feedback_enhanced.utils.memory_monitor.psutil') + def test_get_current_memory_info(self, mock_psutil): + """測試獲取當前內存信息""" + # 模擬 psutil 返回值 + mock_virtual_memory = Mock() + mock_virtual_memory.total = 8 * 1024**3 + mock_virtual_memory.available = 4 * 1024**3 + mock_virtual_memory.used = 4 * 1024**3 + mock_virtual_memory.percent = 50.0 + + mock_memory_info = Mock() + mock_memory_info.rss = 100 * 1024**2 + mock_memory_info.vms = 200 * 1024**2 + + mock_process = Mock() + mock_process.memory_info.return_value = mock_memory_info + mock_process.memory_percent.return_value = 1.25 + + mock_psutil.virtual_memory.return_value = mock_virtual_memory + mock_psutil.Process.return_value = mock_process + + monitor = MemoryMonitor() + info = monitor.get_current_memory_info() + + assert "system" in info + assert "process" in info + assert info["system"]["total_gb"] == 8.0 + assert info["system"]["usage_percent"] == 50.0 + assert info["process"]["rss_mb"] == 100.0 + assert info["status"] == "normal" + + def test_memory_stats_calculation(self): + """測試內存統計計算""" + monitor = MemoryMonitor() + monitor.start_time = datetime.now() - timedelta(minutes=5) + + # 添加一些測試快照 + base_time = datetime.now() + for i in range(5): + snapshot = MemorySnapshot( + timestamp=base_time + timedelta(seconds=i * 30), + system_total=8 * 1024**3, + system_available=4 * 1024**3, + system_used=4 * 1024**3, + system_percent=50.0 + i * 5, # 50%, 55%, 60%, 65%, 70% + process_rss=100 * 1024**2, + process_vms=200 * 1024**2, + process_percent=1.0 + i * 0.2, # 1.0%, 1.2%, 1.4%, 1.6%, 1.8% + gc_objects=10000 + ) + monitor.snapshots.append(snapshot) + + # 添加一些警告 + monitor.alerts.append(MemoryAlert( + level="warning", + message="Test warning", + timestamp=datetime.now(), + memory_percent=85.0, + recommended_action="Test action" + )) + + monitor.cleanup_triggers_count = 2 + + stats = monitor.get_memory_stats() + + assert stats.snapshots_count == 5 + assert stats.average_system_usage == 60.0 # (50+55+60+65+70)/5 + assert stats.peak_system_usage == 70.0 + assert stats.average_process_usage == 1.4 # (1.0+1.2+1.4+1.6+1.8)/5 + assert stats.peak_process_usage == 1.8 + assert stats.alerts_count == 1 + assert stats.cleanup_triggers == 2 + assert stats.monitoring_duration > 0 + + def test_export_memory_data(self): + """測試內存數據導出""" + monitor = MemoryMonitor() + + # 添加一些測試數據 + monitor.alerts.append(MemoryAlert( + level="warning", + message="Test warning", + timestamp=datetime.now(), + memory_percent=85.0, + recommended_action="Test action" + )) + + with patch.object(monitor, 'get_current_memory_info') as mock_info: + mock_info.return_value = { + "system": {"usage_percent": 75.0}, + "status": "warning" + } + + exported_data = monitor.export_memory_data() + + assert "config" in exported_data + assert "current_info" in exported_data + assert "stats" in exported_data + assert "recent_alerts" in exported_data + assert "is_monitoring" in exported_data + + assert exported_data["config"]["warning_threshold"] == 0.8 + assert len(exported_data["recent_alerts"]) == 1 + + +def test_global_memory_monitor_singleton(): + """測試全域內存監控器單例模式""" + monitor1 = get_memory_monitor() + monitor2 = get_memory_monitor() + + assert monitor1 is monitor2 + assert isinstance(monitor1, MemoryMonitor) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_port_manager.py b/tests/test_port_manager.py new file mode 100644 index 0000000..d1ccc61 --- /dev/null +++ b/tests/test_port_manager.py @@ -0,0 +1,249 @@ +""" +端口管理器測試模組 + +測試 PortManager 類的各項功能,包括: +- 端口可用性檢測 +- 進程查找和清理 +- 增強端口查找 +""" + +import pytest +import socket +import time +import threading +import subprocess +import sys +from unittest.mock import patch, MagicMock + +# 添加 src 目錄到 Python 路徑 +sys.path.insert(0, 'src') + +from mcp_feedback_enhanced.web.utils.port_manager import PortManager + + +class TestPortManager: + """端口管理器測試類""" + + def test_is_port_available_free_port(self): + """測試檢測空閒端口""" + # 找一個肯定空閒的端口 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + free_port = s.getsockname()[1] + + # 測試該端口是否被檢測為可用 + assert PortManager.is_port_available('127.0.0.1', free_port) is True + + def test_is_port_available_occupied_port(self): + """測試檢測被占用的端口""" + # 創建一個占用端口的 socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('127.0.0.1', 0)) + occupied_port = server_socket.getsockname()[1] + server_socket.listen(1) + + try: + # 測試該端口是否被檢測為不可用 + assert PortManager.is_port_available('127.0.0.1', occupied_port) is False + finally: + server_socket.close() + + def test_find_free_port_enhanced_preferred_available(self): + """測試當偏好端口可用時的行為""" + # 找一個空閒端口作為偏好端口 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + preferred_port = s.getsockname()[1] + + # 測試是否返回偏好端口 + result_port = PortManager.find_free_port_enhanced( + preferred_port=preferred_port, + auto_cleanup=False + ) + assert result_port == preferred_port + + def test_find_free_port_enhanced_preferred_occupied(self): + """測試當偏好端口被占用時的行為""" + # 創建一個占用端口的 socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('127.0.0.1', 0)) + occupied_port = server_socket.getsockname()[1] + server_socket.listen(1) + + try: + # 測試是否返回其他可用端口 + result_port = PortManager.find_free_port_enhanced( + preferred_port=occupied_port, + auto_cleanup=False + ) + assert result_port != occupied_port + assert result_port > occupied_port # 應該向上查找 + + # 驗證返回的端口確實可用 + assert PortManager.is_port_available('127.0.0.1', result_port) is True + finally: + server_socket.close() + + def test_find_process_using_port_no_process(self): + """測試查找沒有進程占用的端口""" + # 找一個空閒端口 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + free_port = s.getsockname()[1] + + # 測試是否正確返回 None + result = PortManager.find_process_using_port(free_port) + assert result is None + + def test_find_process_using_port_with_process(self): + """測試查找有進程占用的端口""" + # 創建一個簡單的測試服務器 + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('127.0.0.1', 0)) + test_port = server_socket.getsockname()[1] + server_socket.listen(1) + + try: + # 測試是否能找到進程信息 + result = PortManager.find_process_using_port(test_port) + + if result: # 如果找到了進程(在某些環境下可能找不到) + assert isinstance(result, dict) + assert 'pid' in result + assert 'name' in result + assert 'cmdline' in result + assert isinstance(result['pid'], int) + assert result['pid'] > 0 + finally: + server_socket.close() + + def test_get_port_status_available(self): + """測試獲取可用端口的狀態""" + # 找一個空閒端口 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + free_port = s.getsockname()[1] + + status = PortManager.get_port_status(free_port) + + assert status['port'] == free_port + assert status['host'] == '127.0.0.1' + assert status['available'] is True + assert status['process'] is None + assert status['error'] is None + + def test_get_port_status_occupied(self): + """測試獲取被占用端口的狀態""" + # 創建一個占用端口的 socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('127.0.0.1', 0)) + occupied_port = server_socket.getsockname()[1] + server_socket.listen(1) + + try: + status = PortManager.get_port_status(occupied_port) + + assert status['port'] == occupied_port + assert status['host'] == '127.0.0.1' + assert status['available'] is False + # process 可能為 None(取決於系統權限) + assert status['error'] is None + finally: + server_socket.close() + + def test_list_listening_ports(self): + """測試列出監聽端口""" + # 創建幾個測試服務器 + servers = [] + test_ports = [] + + try: + for i in range(2): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(('127.0.0.1', 0)) + port = server_socket.getsockname()[1] + server_socket.listen(1) + + servers.append(server_socket) + test_ports.append(port) + + # 測試列出監聽端口 + min_port = min(test_ports) - 10 + max_port = max(test_ports) + 10 + + listening_ports = PortManager.list_listening_ports(min_port, max_port) + + # 驗證結果 + assert isinstance(listening_ports, list) + + # 檢查我們的測試端口是否在列表中 + found_ports = [p['port'] for p in listening_ports] + for test_port in test_ports: + if test_port in found_ports: + # 找到了我們的端口,驗證信息完整性 + port_info = next(p for p in listening_ports if p['port'] == test_port) + assert 'host' in port_info + assert 'pid' in port_info + assert 'process_name' in port_info + assert 'cmdline' in port_info + + finally: + # 清理測試服務器 + for server in servers: + server.close() + + @patch('mcp_feedback_enhanced.web.utils.port_manager.psutil.Process') + def test_should_cleanup_process_mcp_process(self, mock_process): + """測試是否應該清理 MCP 相關進程""" + # 模擬 MCP 相關進程 + process_info = { + 'pid': 1234, + 'name': 'python.exe', + 'cmdline': 'python -m mcp-feedback-enhanced test --web', + 'create_time': time.time(), + 'status': 'running' + } + + result = PortManager._should_cleanup_process(process_info) + assert result is True + + @patch('mcp_feedback_enhanced.web.utils.port_manager.psutil.Process') + def test_should_cleanup_process_other_process(self, mock_process): + """測試是否應該清理其他進程""" + # 模擬其他進程 + process_info = { + 'pid': 5678, + 'name': 'chrome.exe', + 'cmdline': 'chrome --new-window', + 'create_time': time.time(), + 'status': 'running' + } + + result = PortManager._should_cleanup_process(process_info) + assert result is False + + def test_find_free_port_enhanced_max_attempts(self): + """測試最大嘗試次數限制""" + # 這個測試比較難實現,因為需要占用大量連續端口 + # 我們只測試參數是否正確傳遞 + try: + result = PortManager.find_free_port_enhanced( + preferred_port=65000, # 使用高端口減少衝突 + auto_cleanup=False, + max_attempts=10 + ) + assert isinstance(result, int) + assert 65000 <= result <= 65535 + except RuntimeError: + # 如果真的找不到端口,這也是正常的 + pass + + +if __name__ == '__main__': + # 運行測試 + pytest.main([__file__, '-v']) diff --git a/tests/test_resource_manager.py b/tests/test_resource_manager.py new file mode 100644 index 0000000..bbe10ab --- /dev/null +++ b/tests/test_resource_manager.py @@ -0,0 +1,394 @@ +""" +資源管理器測試模組 + +測試 ResourceManager 類的各項功能,包括: +- 臨時文件和目錄管理 +- 進程註冊和清理 +- 自動清理機制 +- 資源統計和監控 +""" + +import pytest +import os +import sys +import time +import tempfile +import subprocess +import threading +from pathlib import Path +from unittest.mock import patch, MagicMock + +# 添加 src 目錄到 Python 路徑 +sys.path.insert(0, 'src') + +from mcp_feedback_enhanced.utils.resource_manager import ( + ResourceManager, + get_resource_manager, + create_temp_file, + create_temp_dir, + register_process, + cleanup_all_resources +) + + +class TestResourceManager: + """資源管理器測試類""" + + def setup_method(self): + """每個測試方法前的設置""" + # 重置單例實例 + ResourceManager._instance = None + + def test_singleton_pattern(self): + """測試單例模式""" + rm1 = ResourceManager() + rm2 = ResourceManager() + rm3 = get_resource_manager() + + assert rm1 is rm2 + assert rm2 is rm3 + assert id(rm1) == id(rm2) == id(rm3) + + def test_create_temp_file(self): + """測試創建臨時文件""" + rm = get_resource_manager() + + # 測試基本創建 + temp_file = rm.create_temp_file(suffix=".txt", prefix="test_") + + assert isinstance(temp_file, str) + assert os.path.exists(temp_file) + assert temp_file.endswith(".txt") + assert "test_" in os.path.basename(temp_file) + assert temp_file in rm.temp_files + + # 清理 + os.remove(temp_file) + + def test_create_temp_dir(self): + """測試創建臨時目錄""" + rm = get_resource_manager() + + # 測試基本創建 + temp_dir = rm.create_temp_dir(suffix="_test", prefix="test_") + + assert isinstance(temp_dir, str) + assert os.path.exists(temp_dir) + assert os.path.isdir(temp_dir) + assert temp_dir.endswith("_test") + assert "test_" in os.path.basename(temp_dir) + assert temp_dir in rm.temp_dirs + + # 清理 + os.rmdir(temp_dir) + + def test_convenience_functions(self): + """測試便捷函數""" + # 測試 create_temp_file 便捷函數 + temp_file = create_temp_file(suffix=".log", prefix="conv_") + assert isinstance(temp_file, str) + assert os.path.exists(temp_file) + assert temp_file.endswith(".log") + + # 測試 create_temp_dir 便捷函數 + temp_dir = create_temp_dir(suffix="_conv", prefix="conv_") + assert isinstance(temp_dir, str) + assert os.path.exists(temp_dir) + assert os.path.isdir(temp_dir) + + # 清理 + os.remove(temp_file) + os.rmdir(temp_dir) + + def test_register_process_with_popen(self): + """測試註冊 Popen 進程""" + rm = get_resource_manager() + + # 創建一個簡單的進程 + process = subprocess.Popen( + ["python", "-c", "import time; time.sleep(0.1)"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # 註冊進程 + pid = rm.register_process(process, description="測試進程") + + assert pid == process.pid + assert pid in rm.processes + assert rm.processes[pid]["description"] == "測試進程" + assert rm.processes[pid]["process"] is process + + # 等待進程結束 + process.wait() + + def test_register_process_with_pid(self): + """測試註冊 PID""" + rm = get_resource_manager() + + # 使用當前進程的 PID + current_pid = os.getpid() + + # 註冊 PID + registered_pid = rm.register_process(current_pid, description="當前進程") + + assert registered_pid == current_pid + assert current_pid in rm.processes + assert rm.processes[current_pid]["description"] == "當前進程" + assert rm.processes[current_pid]["process"] is None + + def test_unregister_temp_file(self): + """測試取消臨時文件追蹤""" + rm = get_resource_manager() + + # 創建臨時文件 + temp_file = rm.create_temp_file() + assert temp_file in rm.temp_files + + # 取消追蹤 + result = rm.unregister_temp_file(temp_file) + assert result is True + assert temp_file not in rm.temp_files + + # 再次取消追蹤(應該返回 False) + result = rm.unregister_temp_file(temp_file) + assert result is False + + # 清理 + if os.path.exists(temp_file): + os.remove(temp_file) + + def test_unregister_process(self): + """測試取消進程追蹤""" + rm = get_resource_manager() + + # 註冊進程 + current_pid = os.getpid() + rm.register_process(current_pid, description="測試進程") + assert current_pid in rm.processes + + # 取消追蹤 + result = rm.unregister_process(current_pid) + assert result is True + assert current_pid not in rm.processes + + # 再次取消追蹤(應該返回 False) + result = rm.unregister_process(current_pid) + assert result is False + + def test_cleanup_temp_files(self): + """測試清理臨時文件""" + rm = get_resource_manager() + + # 創建多個臨時文件 + temp_files = [] + for i in range(3): + temp_file = rm.create_temp_file(prefix=f"cleanup_test_{i}_") + temp_files.append(temp_file) + + # 確認文件都存在 + for temp_file in temp_files: + assert os.path.exists(temp_file) + assert temp_file in rm.temp_files + + # 執行清理(max_age=0 清理所有文件) + cleaned_count = rm.cleanup_temp_files(max_age=0) + + assert cleaned_count == 3 + for temp_file in temp_files: + assert not os.path.exists(temp_file) + assert temp_file not in rm.temp_files + + def test_cleanup_temp_dirs(self): + """測試清理臨時目錄""" + rm = get_resource_manager() + + # 創建多個臨時目錄 + temp_dirs = [] + for i in range(2): + temp_dir = rm.create_temp_dir(prefix=f"cleanup_test_{i}_") + temp_dirs.append(temp_dir) + + # 確認目錄都存在 + for temp_dir in temp_dirs: + assert os.path.exists(temp_dir) + assert temp_dir in rm.temp_dirs + + # 執行清理 + cleaned_count = rm.cleanup_temp_dirs() + + assert cleaned_count == 2 + for temp_dir in temp_dirs: + assert not os.path.exists(temp_dir) + assert temp_dir not in rm.temp_dirs + + def test_cleanup_all(self): + """測試全面清理""" + rm = get_resource_manager() + + # 創建各種資源 + temp_file = rm.create_temp_file(prefix="cleanup_all_") + temp_dir = rm.create_temp_dir(prefix="cleanup_all_") + + # 註冊進程 + current_pid = os.getpid() + rm.register_process(current_pid, description="測試進程", auto_cleanup=False) + + # 執行全面清理 + results = rm.cleanup_all() + + assert isinstance(results, dict) + assert "temp_files" in results + assert "temp_dirs" in results + assert "processes" in results + assert "file_handles" in results + + # 檢查文件和目錄是否被清理 + assert not os.path.exists(temp_file) + assert not os.path.exists(temp_dir) + assert temp_file not in rm.temp_files + assert temp_dir not in rm.temp_dirs + + # 進程不應該被清理(auto_cleanup=False) + assert current_pid in rm.processes + + def test_get_resource_stats(self): + """測試獲取資源統計""" + rm = get_resource_manager() + + # 創建一些資源 + temp_file = rm.create_temp_file() + temp_dir = rm.create_temp_dir() + rm.register_process(os.getpid(), description="統計測試") + + # 獲取統計 + stats = rm.get_resource_stats() + + assert isinstance(stats, dict) + assert "current_temp_files" in stats + assert "current_temp_dirs" in stats + assert "current_processes" in stats + assert "temp_files_created" in stats + assert "temp_dirs_created" in stats + assert "auto_cleanup_enabled" in stats + + assert stats["current_temp_files"] >= 1 + assert stats["current_temp_dirs"] >= 1 + assert stats["current_processes"] >= 1 + + # 清理 + os.remove(temp_file) + os.rmdir(temp_dir) + + def test_get_detailed_info(self): + """測試獲取詳細信息""" + rm = get_resource_manager() + + # 創建一些資源 + temp_file = rm.create_temp_file(prefix="detail_test_") + rm.register_process(os.getpid(), description="詳細信息測試") + + # 獲取詳細信息 + info = rm.get_detailed_info() + + assert isinstance(info, dict) + assert "temp_files" in info + assert "temp_dirs" in info + assert "processes" in info + assert "stats" in info + + assert temp_file in info["temp_files"] + assert os.getpid() in info["processes"] + assert info["processes"][os.getpid()]["description"] == "詳細信息測試" + + # 清理 + os.remove(temp_file) + + def test_configure(self): + """測試配置功能""" + rm = get_resource_manager() + + # 測試配置更新 + rm.configure( + auto_cleanup_enabled=False, + cleanup_interval=120, + temp_file_max_age=1800 + ) + + assert rm.auto_cleanup_enabled is False + assert rm.cleanup_interval == 120 + assert rm.temp_file_max_age == 1800 + + # 測試最小值限制 + rm.configure( + cleanup_interval=30, # 小於最小值 60 + temp_file_max_age=100 # 小於最小值 300 + ) + + assert rm.cleanup_interval == 60 # 應該被限制為最小值 + assert rm.temp_file_max_age == 300 # 應該被限制為最小值 + + def test_cleanup_all_convenience_function(self): + """測試全面清理便捷函數""" + # 創建一些資源 + temp_file = create_temp_file(prefix="conv_cleanup_") + temp_dir = create_temp_dir(prefix="conv_cleanup_") + + # 執行清理 + results = cleanup_all_resources() + + assert isinstance(results, dict) + assert not os.path.exists(temp_file) + assert not os.path.exists(temp_dir) + + def test_error_handling(self): + """測試錯誤處理""" + rm = get_resource_manager() + + # 測試創建臨時文件時的錯誤處理 + with patch('tempfile.mkstemp', side_effect=OSError("Mock error")): + with pytest.raises(OSError): + rm.create_temp_file() + + # 測試創建臨時目錄時的錯誤處理 + with patch('tempfile.mkdtemp', side_effect=OSError("Mock error")): + with pytest.raises(OSError): + rm.create_temp_dir() + + def test_file_handle_registration(self): + """測試文件句柄註冊""" + rm = get_resource_manager() + + # 創建一個文件句柄 + temp_file = rm.create_temp_file() + with open(temp_file, 'w') as f: + f.write("test") + rm.register_file_handle(f) + + # 檢查是否註冊成功 + assert len(rm.file_handles) > 0 + + # 清理 + os.remove(temp_file) + + def test_auto_cleanup_thread(self): + """測試自動清理線程""" + rm = get_resource_manager() + + # 確保自動清理已啟動 + assert rm.auto_cleanup_enabled is True + assert rm._cleanup_thread is not None + assert rm._cleanup_thread.is_alive() + + # 測試停止自動清理 + rm.stop_auto_cleanup() + assert rm._cleanup_thread is None + + # 重新啟動 + rm.configure(auto_cleanup_enabled=True) + assert rm._cleanup_thread is not None + + +if __name__ == '__main__': + # 運行測試 + pytest.main([__file__, '-v']) diff --git a/tests/test_session_cleanup.py b/tests/test_session_cleanup.py new file mode 100644 index 0000000..96900ca --- /dev/null +++ b/tests/test_session_cleanup.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +會話清理優化測試 +================ + +測試 WebFeedbackSession 和 SessionCleanupManager 的清理功能。 +""" + +import asyncio +import pytest +import time +import threading +from unittest.mock import Mock, patch, MagicMock +from datetime import datetime, timedelta + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from src.mcp_feedback_enhanced.web.models.feedback_session import ( + WebFeedbackSession, SessionStatus, CleanupReason +) +from src.mcp_feedback_enhanced.web.utils.session_cleanup_manager import ( + SessionCleanupManager, CleanupPolicy, CleanupTrigger +) + + +class TestWebFeedbackSessionCleanup: + """測試 WebFeedbackSession 清理功能""" + + def setup_method(self): + """測試前設置""" + self.session_id = "test_session_001" + self.project_dir = "/tmp/test_project" + self.summary = "測試會話摘要" + + # 創建測試會話 + self.session = WebFeedbackSession( + self.session_id, + self.project_dir, + self.summary, + auto_cleanup_delay=60, # 1分鐘自動清理 + max_idle_time=30 # 30秒最大空閒時間 + ) + + def teardown_method(self): + """測試後清理""" + if hasattr(self, 'session') and self.session: + try: + self.session._cleanup_sync_enhanced(CleanupReason.MANUAL) + except: + pass + + def test_session_initialization(self): + """測試會話初始化""" + assert self.session.session_id == self.session_id + assert self.session.project_directory == self.project_dir + assert self.session.summary == self.summary + assert self.session.status == SessionStatus.WAITING + assert self.session.auto_cleanup_delay == 60 + assert self.session.max_idle_time == 30 + assert self.session.cleanup_timer is not None + assert len(self.session.cleanup_stats) > 0 + + def test_is_expired_by_idle_time(self): + """測試空閒時間過期檢測""" + # 新創建的會話不應該過期 + assert not self.session.is_expired() + + # 模擬空閒時間過長 + self.session.last_activity = time.time() - 40 # 40秒前 + assert self.session.is_expired() + + def test_is_expired_by_status(self): + """測試狀態過期檢測""" + # 設置為錯誤狀態 + self.session.status = SessionStatus.ERROR + self.session.last_activity = time.time() - 400 # 400秒前 + assert self.session.is_expired() + + # 設置為已過期狀態 + self.session.status = SessionStatus.EXPIRED + assert self.session.is_expired() + + def test_get_age_and_idle_time(self): + """測試年齡和空閒時間計算""" + # 測試年齡 + age = self.session.get_age() + assert age >= 0 + assert age < 1 # 剛創建,應該小於1秒 + + # 測試空閒時間 + idle_time = self.session.get_idle_time() + assert idle_time >= 0 + assert idle_time < 1 # 剛創建,應該小於1秒 + + def test_cleanup_timer_scheduling(self): + """測試清理定時器調度""" + # 檢查定時器是否已設置 + assert self.session.cleanup_timer is not None + assert self.session.cleanup_timer.is_alive() + + # 測試延長定時器 + old_timer = self.session.cleanup_timer + self.session.extend_cleanup_timer(120) + + # 應該創建新的定時器 + assert self.session.cleanup_timer != old_timer + assert self.session.cleanup_timer.is_alive() + + def test_cleanup_callbacks(self): + """測試清理回調函數""" + callback_called = False + callback_session = None + callback_reason = None + + def test_callback(session, reason): + nonlocal callback_called, callback_session, callback_reason + callback_called = True + callback_session = session + callback_reason = reason + + # 添加回調 + self.session.add_cleanup_callback(test_callback) + assert len(self.session.cleanup_callbacks) == 1 + + # 執行清理 + self.session._cleanup_sync_enhanced(CleanupReason.MANUAL) + + # 檢查回調是否被調用 + assert callback_called + assert callback_session == self.session + assert callback_reason == CleanupReason.MANUAL + + # 移除回調 + self.session.remove_cleanup_callback(test_callback) + assert len(self.session.cleanup_callbacks) == 0 + + def test_cleanup_stats(self): + """測試清理統計""" + # 初始統計 + stats = self.session.get_cleanup_stats() + assert stats["cleanup_count"] == 0 + assert stats["session_id"] == self.session_id + assert stats["is_active"] == True + + # 執行清理 + self.session._cleanup_sync_enhanced(CleanupReason.EXPIRED) + + # 檢查統計更新 + stats = self.session.get_cleanup_stats() + assert stats["cleanup_count"] == 1 + assert stats["cleanup_reason"] == CleanupReason.EXPIRED.value + assert stats["last_cleanup_time"] is not None + assert stats["cleanup_duration"] >= 0 + + @pytest.mark.asyncio + async def test_async_cleanup(self): + """測試異步清理""" + # 模擬 WebSocket 連接 + mock_websocket = Mock() + mock_websocket.send_json = Mock(return_value=asyncio.Future()) + mock_websocket.send_json.return_value.set_result(None) + mock_websocket.close = Mock(return_value=asyncio.Future()) + mock_websocket.close.return_value.set_result(None) + mock_websocket.client_state.DISCONNECTED = False + + self.session.websocket = mock_websocket + + # 執行異步清理 + await self.session._cleanup_resources_enhanced(CleanupReason.TIMEOUT) + + # 檢查 WebSocket 是否被正確處理 + mock_websocket.send_json.assert_called_once() + + # 檢查清理統計 + stats = self.session.get_cleanup_stats() + assert stats["cleanup_count"] == 1 + assert stats["cleanup_reason"] == CleanupReason.TIMEOUT.value + + def test_status_update_resets_timer(self): + """測試狀態更新重置定時器""" + old_timer = self.session.cleanup_timer + + # 更新狀態為活躍 + self.session.update_status(SessionStatus.ACTIVE, "測試活躍狀態") + + # 檢查定時器是否被重置 + assert self.session.cleanup_timer != old_timer + assert self.session.cleanup_timer.is_alive() + assert self.session.status == SessionStatus.ACTIVE + + +class TestSessionCleanupManager: + """測試 SessionCleanupManager 功能""" + + def setup_method(self): + """測試前設置""" + # 創建模擬的 WebUIManager + self.mock_web_ui_manager = Mock() + self.mock_web_ui_manager.sessions = {} + self.mock_web_ui_manager.current_session = None + self.mock_web_ui_manager.cleanup_expired_sessions = Mock(return_value=0) + self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure = Mock(return_value=0) + + # 創建清理策略 + self.policy = CleanupPolicy( + max_idle_time=30, + max_session_age=300, + max_sessions=5, + cleanup_interval=10, + enable_auto_cleanup=True + ) + + # 創建清理管理器 + self.cleanup_manager = SessionCleanupManager( + self.mock_web_ui_manager, + self.policy + ) + + def teardown_method(self): + """測試後清理""" + if hasattr(self, 'cleanup_manager'): + self.cleanup_manager.stop_auto_cleanup() + + def test_cleanup_manager_initialization(self): + """測試清理管理器初始化""" + assert self.cleanup_manager.web_ui_manager == self.mock_web_ui_manager + assert self.cleanup_manager.policy == self.policy + assert not self.cleanup_manager.is_running + assert self.cleanup_manager.cleanup_thread is None + assert len(self.cleanup_manager.cleanup_callbacks) == 0 + assert len(self.cleanup_manager.cleanup_history) == 0 + + def test_auto_cleanup_start_stop(self): + """測試自動清理啟動和停止""" + # 啟動自動清理 + result = self.cleanup_manager.start_auto_cleanup() + assert result == True + assert self.cleanup_manager.is_running == True + assert self.cleanup_manager.cleanup_thread is not None + assert self.cleanup_manager.cleanup_thread.is_alive() + + # 停止自動清理 + result = self.cleanup_manager.stop_auto_cleanup() + assert result == True + assert self.cleanup_manager.is_running == False + + def test_trigger_cleanup_memory_pressure(self): + """測試內存壓力清理觸發""" + # 設置模擬返回值 + self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.return_value = 3 + + # 觸發內存壓力清理 + cleaned = self.cleanup_manager.trigger_cleanup(CleanupTrigger.MEMORY_PRESSURE, force=True) + + # 檢查結果 + assert cleaned == 3 + self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.assert_called_once_with(True) + + # 檢查統計更新 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] == 1 + assert stats["memory_pressure_cleanups"] == 1 + assert stats["total_sessions_cleaned"] == 3 + + def test_trigger_cleanup_expired(self): + """測試過期清理觸發""" + # 設置模擬返回值 + self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 2 + + # 觸發過期清理 + cleaned = self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED) + + # 檢查結果 + assert cleaned == 2 + self.mock_web_ui_manager.cleanup_expired_sessions.assert_called_once() + + # 檢查統計更新 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] == 1 + assert stats["expired_cleanups"] == 1 + assert stats["total_sessions_cleaned"] == 2 + + def test_cleanup_statistics(self): + """測試清理統計功能""" + # 初始統計 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] == 0 + assert stats["total_sessions_cleaned"] == 0 + assert stats["is_auto_cleanup_running"] == False + + # 執行一些清理操作 + self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1 + self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED) + + self.mock_web_ui_manager.cleanup_sessions_by_memory_pressure.return_value = 2 + self.cleanup_manager.trigger_cleanup(CleanupTrigger.MEMORY_PRESSURE) + + # 檢查統計 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] == 2 + assert stats["expired_cleanups"] == 1 + assert stats["memory_pressure_cleanups"] == 1 + assert stats["total_sessions_cleaned"] == 3 + assert stats["average_cleanup_time"] >= 0 + + def test_cleanup_history(self): + """測試清理歷史記錄""" + # 初始歷史為空 + history = self.cleanup_manager.get_cleanup_history() + assert len(history) == 0 + + # 執行清理操作 + self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1 + self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED) + + # 檢查歷史記錄 + history = self.cleanup_manager.get_cleanup_history() + assert len(history) == 1 + + record = history[0] + assert record["trigger"] == CleanupTrigger.EXPIRED.value + assert record["cleaned_count"] == 1 + assert "timestamp" in record + assert "duration" in record + + def test_policy_update(self): + """測試策略更新""" + # 更新策略 + self.cleanup_manager.update_policy( + max_idle_time=60, + max_sessions=10, + enable_auto_cleanup=False + ) + + # 檢查策略是否更新 + assert self.cleanup_manager.policy.max_idle_time == 60 + assert self.cleanup_manager.policy.max_sessions == 10 + assert self.cleanup_manager.policy.enable_auto_cleanup == False + + def test_stats_reset(self): + """測試統計重置""" + # 執行一些操作產生統計 + self.mock_web_ui_manager.cleanup_expired_sessions.return_value = 1 + self.cleanup_manager.trigger_cleanup(CleanupTrigger.EXPIRED) + + # 檢查有統計數據 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] > 0 + + # 重置統計 + self.cleanup_manager.reset_stats() + + # 檢查統計已重置 + stats = self.cleanup_manager.get_cleanup_statistics() + assert stats["total_cleanups"] == 0 + assert stats["total_sessions_cleaned"] == 0 + + history = self.cleanup_manager.get_cleanup_history() + assert len(history) == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])