Merge pull request #42 from Minidoracat/feature/practical-improvements

Feature/practical improvements
This commit is contained in:
Minidoracat 2025-06-08 03:05:23 +08:00 committed by GitHub
commit edbbf535ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 7626 additions and 1029 deletions

View File

@ -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"
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"

View File

@ -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)
**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!**

View File

@ -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. 如持续无法解析,可尝试调整图片大小或格式
## 🙏 致谢

View File

@ -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` 或移除該環境變數。

View File

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

View File

@ -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 连接的相关问题
- 🔄 **会话管理**: 修复会话状态跟踪的问题
- 🖼️ **图片处理**: 修复图片上传时的事件处理问题
---

View File

@ -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 瀏覽器,支援多種啟動方式

View File

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

View File

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

View File

@ -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 (部分完成)

View File

@ -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 (部分完成)

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (图片设置功能)

View File

@ -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 (圖片設定功能)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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 工具重新載入並顯示綠燈

View File

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

View File

@ -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": {

View File

@ -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": "🌐 语言选择",

View File

@ -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": "繁體中文",

View File

@ -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:

View File

@ -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 表示可以使用 GUIFalse 表示只能使用 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": []
}

View File

@ -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"""

View File

@ -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:

View File

@ -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'
]

View File

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

View File

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

View File

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

View File

@ -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!",

View File

@ -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请测试这些功能并提供反馈",

View File

@ -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請測試這些功能並提供回饋",

View File

@ -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 服務")

View File

@ -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'
]
]

View File

@ -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 連接,避免事件循環衝突"""

View File

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

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -178,6 +178,10 @@ class I18nManager {
if (window.feedbackApp) {
window.feedbackApp.updateUIState();
window.feedbackApp.updateStatusIndicator();
// 更新自動檢測狀態文字
if (window.feedbackApp.updateAutoRefreshStatus) {
window.feedbackApp.updateAutoRefreshStatus();
}
}
}

View File

@ -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 @@
<!-- 分頁導航 -->
<div class="tabs">
<div class="tab-buttons">
<!-- 合併模式分頁 - 移到最左邊第一個 -->
<!-- 工作區分頁 - 移到最左邊第一個 -->
<button class="tab-button hidden" data-tab="combined" data-i18n="tabs.combined">
📝 合併模式
📝 工作區
</button>
<button class="tab-button active" data-tab="feedback" data-i18n="tabs.feedback">
💬 回饋
@ -499,16 +489,32 @@
</div>
</div>
<!-- 合併模式分頁 - 移動到此位置 -->
<!-- 工作區分頁 - 移動到此位置 -->
<div id="tab-combined" class="tab-content">
<div class="section-description" style="margin-bottom: 12px; padding: 8px 12px; font-size: 13px;" data-i18n="combined.description">
合併模式:AI 摘要和回饋輸入在同一頁面中,方便對照查看。
AI 摘要和回饋輸入在同一頁面中,方便對照查看。
</div>
<div class="combined-content">
<!-- AI 摘要區域 -->
<div class="combined-section">
<h3 class="combined-section-title" data-i18n="combined.summaryTitle">📋 AI 工作摘要</h3>
<div class="section-header-with-controls">
<h3 class="combined-section-title" data-i18n="combined.summaryTitle">📋 AI 工作摘要</h3>
<div class="auto-refresh-controls">
<label class="auto-refresh-toggle">
<input type="checkbox" id="autoRefreshEnabled" />
<span class="toggle-label" data-i18n="autoRefresh.enable"></span>
</label>
<div class="auto-refresh-interval">
<input type="number" id="autoRefreshInterval" min="5" max="300" value="5" />
<span class="interval-unit" data-i18n="autoRefresh.seconds"></span>
</div>
<div class="auto-refresh-status" id="autoRefreshStatus">
<span class="status-indicator" id="refreshStatusIndicator">⏸️</span>
<span class="status-text" id="refreshStatusText" data-i18n="autoRefresh.disabled"></span>
</div>
</div>
</div>
<div class="combined-summary">
<div id="combinedSummaryContent" class="text-input" style="min-height: 200px; white-space: pre-wrap !important; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary">{{ summary }}</div>
</div>
@ -570,23 +576,16 @@
</div>
<div class="layout-mode-selector">
<div class="layout-option">
<input type="radio" id="separateMode" name="layoutMode" value="separate" checked>
<label for="separateMode">
<div class="layout-option-title" data-i18n="settings.separateMode">分離模式</div>
<div class="layout-option-desc" data-i18n="settings.separateModeDesc">AI 摘要和回饋分別在不同頁籤</div>
</label>
</div>
<div class="layout-option">
<input type="radio" id="combinedVertical" name="layoutMode" value="combined-vertical">
<input type="radio" id="combinedVertical" name="layoutMode" value="combined-vertical" checked>
<label for="combinedVertical">
<div class="layout-option-title" data-i18n="settings.combinedVertical">合併模式(垂直布局</div>
<div class="layout-option-title" data-i18n="settings.combinedVertical">垂直布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
</label>
</div>
<div class="layout-option">
<input type="radio" id="combinedHorizontal" name="layoutMode" value="combined-horizontal">
<label for="combinedHorizontal">
<div class="layout-option-title" data-i18n="settings.combinedHorizontal">合併模式(水平布局</div>
<div class="layout-option-title" data-i18n="settings.combinedHorizontal">水平布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
</label>
</div>

View File

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

View File

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

View File

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

View File

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

253
tests/test_error_handler.py Normal file
View File

@ -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'])

View File

@ -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 = "<html><body>" + "content " * 100 + "</body></html>"
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"])

View File

@ -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"])

249
tests/test_port_manager.py Normal file
View File

@ -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'])

View File

@ -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'])

View File

@ -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"])